Modular AI-native starter kit for Godot 4.x. Ships an Event Bus, base resources, a Model Context Protocol (MCP) runtime bridge, and a Node.js MCP server so that LLM agents can author scenes, validate resources, and drive a running game through a stable tool surface.
- Godot 4.6 or newer.
- Node.js 20 or newer (required by the
@forgekitstudio/core-mcppackage). npm10 or newer (ships with Node.js 20).
ForgeKit Core is one of several MCP servers targeting the Godot editor. The table below lists factual differences against the communities' most active alternatives at the time of writing. Rows mark "no" when the upstream project does not advertise the capability in its README; see each project's documentation for the latest state.
| Capability | ForgeKit Core | godot-mcp-pro | tomyud1/godot-mcp | Coding-Solo/godot-mcp |
|---|---|---|---|---|
| Tool count | ~271 (Full profile) | ~150 | ~120 | ~90 |
| Editor channel | yes (WebSocket, 6010-6019) | yes | yes | no |
| Runtime channel | yes (UDP, 6020-6029) | no | no | no |
| CLI channel | yes (spawn godot --headless) |
yes | yes | yes |
| UndoRedo integration | yes (every editor mutation) | partial | no | no |
| Self-healing loop | yes (.tres inspect + suggest + bounded retry) |
no | no | no |
| Modules / licensing | yes (paid forgekit_rpg + HMAC license store) |
no | no | no |
| Observability | structured JSONL logs + trace/span ids + Prometheus metrics + health endpoint | no | no | no |
The comparison is about architectural scope only — every project listed above ships the fundamentals agents need for editor automation and is worth evaluating alongside ForgeKit Core based on the team's day-to-day workflow.
This quickstart takes you from zero to the first crafting.execute call in
roughly five minutes, assuming you have already purchased and downloaded the
paid ForgeKit RPG Module.
- Click Use this template on ForgeKitStudio/forgekit-core to create your own GitHub repository.
- Clone your new repository and open the folder in Godot 4.6 or newer.
- Install the MCP server:
cd mcp-server npm ci npm run build - Install the required git hooks (see
Required git hooks for what they do and why they
are required):
The
npx -y @forgekitstudio/core-mcp install-hooks
-yflag skips the interactive install prompt so the command can be run non-interactively in CI and setup scripts. - Purchase and download the ForgeKit RPG Module from
itch.io or
Gumroad, then unzip
the archive into
addons/forgekit_rpg/. - Copy the config template and fill in your auth token:
cp addons/forgekit_core/mcp/plugin_config.tres.template \ addons/forgekit_core/mcp/plugin_config.tres # edit plugin_config.tres and set auth_token = "<a strong random string>" - In Godot, enable the plugin under Project → Project Settings → Plugins → ForgeKit Core.
- Launch the MCP server in the
RPG-onlyprofile:npx -y @forgekitstudio/core-mcp --profile RPG-only
- Activate the module license through your MCP client:
modules.activate_license("forgekit_rpg", "<your-license-key>") - Call
crafting.execute("iron_ingot")from the same MCP client and watch the item appear in the inventory.
ForgeKit ships three independent update channels, one per product.
- MCP Server (
@forgekitstudio/core-mcp). Runnpx -y @forgekitstudio/core-mcp@latestto pull the newest version from npm. The editor plugin polls the GitHub releases endpoint once per hour; when a newer ForgeKit Core version is detected it appends a single line toeditor.get_output_log:Clients that scrape the editor log stream (Kiro, Claude Code, Cursor, ...) surface the notice without any extra wiring. The one-hour rate limit is enforced through a small cache atUPDATE_AVAILABLE: ForgeKit Core v<new> available (running v<current>). Run 'npx -y @forgekitstudio/core-mcp@latest' to upgrade.user://mcp_update_check.json; delete that file if you want to force an immediate re-check. Network failures (offline, DNS error, non-200 response) are silent: the checker reports no update rather than surfacing a false positive, and the cache is only written on a successful fetch so the next call retries. - ForgeKit Core addon (
addons/forgekit_core/). Replace the directory with the newest release tarball, or pull the update through Godot's AssetLib "Update" action. The addon never rewrites itself in place. - ForgeKit RPG Module (
addons/forgekit_rpg/). Replace the module directory with the newest ZIP from itch.io or Gumroad. After extracting, callmodules.check_compatibility(module_id= "forgekit_rpg")— the tool compares the module'score_min_versionagainst the installed Core version and returns{compatible: false, required, installed}when the module needs a newer Core than you have.modules.check_compatibilityis the authoritative source of truth for module / Core version compatibility; agents MUST consult it before activating a module and SHOULD consult it after every update.
Required after cloning. Before making any commits, run
npx @forgekitstudio/core-mcp install-hooksfrom the repository root. The same rules are enforced by CI, so running the hooks locally catches failures before they block your pull request.
npx -y @forgekitstudio/core-mcp install-hooksRun this command once, immediately after your first git clone (or after
creating a new repository from the template) and before your first commit.
The command writes two shim scripts into .git/hooks/:
commit-msg— validates that every commit message follows the Conventional Commits format<type>(<scope>)?: <subject>. Commits whose subject lines do not match are rejected with error code-32013(CONVENTIONAL_COMMITS_FORMAT_VIOLATION) and a short hint about the expected shape.pre-commit— enforces the Context Commits policy. When a commit touches code underaddons/forgekit_core/**,mcp-server/src/**, oraddons/forgekit_rpg/**, the hook checks that the corresponding context files (CLAUDE.md,.cursorrules) — as mapped in.forgekit/context-map.json— are staged in the same commit. Stale-context commits are rejected with error code-32012(CONTEXT_FILE_STALE).
CI runs the same validators (tools/validate-language-policy.js, the
commit-msg validator, and the context-commit check) on every pull request.
Without the local hooks a commit can look fine on your machine and then fail
in CI, forcing a rebase and re-push. Installing the hooks keeps the
fast-feedback loop on your laptop where it belongs.
If you genuinely need to bypass the pre-commit check — for example while
landing an emergency hotfix — pass --no-verify:
git commit --no-verify -m "fix(core): emergency patch"The skip is recorded in .git/hooks/context-commit-skips.log as a JSON line
with {ts, author, files, reason} fields so reviewers can audit bypasses
during code review. Note that --no-verify also skips the local
commit-msg hook, but CI re-runs the Conventional Commits check on every
pull request, so a malformed subject will still be caught before merge.
forgekit-core/
├── addons/
│ ├── forgekit_core/ # This addon — MIT. Event bus, resources, MCP bridge.
│ └── forgekit_rpg/ # Placeholder for the paid RPG module (purchase required).
├── mcp-server/ # @forgekitstudio/core-mcp Node.js server (published to npm).
├── docs/ # Architecture notes, MCP API reference, SKILLS pack.
├── tests/ # GUT unit, property, integration, smoke, and static tests.
├── tools/ # CLI helpers (run_tests.sh, check_imports.sh, run_check_imports.mjs).
├── .github/ # Issue templates, PR template, CI workflows.
├── CLAUDE.md # Context file for AI agents.
├── .cursorrules # Context file for Cursor (mirror of CLAUDE.md).
├── NOTICE.md # Installed modules and their licenses.
└── project.godot # Godot project with registered autoloads.
The MCP server ships as the npm package
@forgekitstudio/core-mcp.
The recommended way to run it is through npx, which fetches the latest
published version on demand and exits cleanly after the server stops.
# Fetch and run the server non-interactively.
npx -y @forgekitstudio/core-mcp --profile Full
# Pin a specific version to avoid surprises during long sessions.
npx -y @forgekitstudio/core-mcp@0.1.0 --profile Lite-y skips the npx interactive install prompt; use it whenever the
command runs inside CI, a container, or a script that cannot answer
prompts. For local development where you want to inspect the install
prompt, drop the -y.
Once installed globally or through npx, the executable is exposed as
forgekit-mcp. That binary accepts the same flags as the npx form
(for example forgekit-mcp --profile RPG-only); the examples in this
README use npx -y @forgekitstudio/core-mcp so they work without a global
install.
The MCP server exposes four tool profiles, selected with the --profile
flag. Counts are approximate and shift with each minor release.
| Profile | Tool count | Intended clients |
|---|---|---|
Full |
~271 | Claude Code, Cline, VS Code Copilot, Cursor, Kiro |
Lite |
~194 | Windsurf, JetBrains Junie, Gemini CLI (~100-tool limits) |
Minimal |
~21 | OpenCode, local LLMs, Antigravity via stdio |
RPG-only |
~77 + 21 core | Users focused on the RPG module after activating its license |
Full includes every scope: core tool plus every module tool unlocked
by an active license; the current surface covers the Phase 6A baseline
of 215 tools across 34 categories and now ships additional runtime,
workspace, and RPG-domain tools beyond it. Lite is the union of
core-minimal and core tools. Minimal is only the core-minimal
set (available in every profile). RPG-only is core-minimal plus the
fifteen RPG subsystem categories (combat, crafting, inventory,
stats, effects, magic, equipment, progression, enemies,
loot, spawner, chests, npc, dialog, vendor), unlocked in one
step by the forgekit_rpg license.
ForgeKit allocates ports in dedicated ranges and scans for the first free one
at startup. All services bind to 127.0.0.1 by default.
| Service | Range | Protocol |
|---|---|---|
| Editor plugin WebSocket | 6010–6019 | WebSocket |
| Runtime bridge | 6020–6029 | UDP |
| Browser visualizer | 6030–6039 | HTTP |
| Health endpoint | 6040–6049 | HTTP |
The active port for each service is written to
user://mcp_active_port.json so that the MCP server can discover the exact
values at runtime. Updates to this file are crash-safe: each service writes
its entry through a sibling .tmp file plus a rename, so a reader always
observes either the complete previous contents or the complete new
contents, and entries written by other services are preserved even if the
write fails mid-flight.
ForgeKit deliberately uses ranges outside the 6505–6509 block used by
other Godot MCP servers (godot-mcp-pro, tomyud1/godot-mcp) so that
both can coexist on the same machine. When a port is already taken, the
editor plugin, runtime bridge, visualizer, and health endpoint each
scan upward inside their dedicated range and bind to the first free slot.
What to do if startup still fails with "no free port":
- Find the listener. On macOS or Linux:
On Windows:
lsof -iTCP:6010-6019 -sTCP:LISTEN lsof -iUDP:6020-6029
Stop whatever is bound to the range, or reconfigure it to a different port.Get-NetTCPConnection -LocalPort 6010..6019 -State Listen Get-NetUDPEndpoint -LocalPort 6020..6029
- Tell ForgeKit a different range. Override the default range by
editing
addons/forgekit_core/mcp/plugin_config.tres(editor and visualizer) oraddons/forgekit_core/mcp/runtime_config.tres(runtime bridge) and settingbind_portto a specific free port. The scan still runs, but it starts from the value you chose. - Confirm the active port. After startup, read
user://mcp_active_port.jsonto see the port the server actually bound to — the MCP server reads the same file to find Godot, so the two stay in sync automatically.
If another agent is already running on the same machine with
--profile Full, launch a second copy with a different profile
(--profile Minimal, for example) so that the tool sets do not
conflict inside the shared MCP client.
GameEvents is a project-wide autoload registered at
addons/forgekit_core/event_bus/game_events.gd. It declares a fixed set of
global signals so that subsystems (inventory, crafting, combat) stay loosely
coupled and testable.
The bus declares seventeen signals in total.
Phase 0–3:
| Signal | Payload |
|---|---|
damage_dealt |
source: Node, target: Node, damage: float, damage_type: StringName |
crafting_completed |
recipe_id: StringName, outputs: Array |
item_added |
item_id: StringName, amount: int |
item_removed |
item_id: StringName, amount: int |
Phase 4B (consumed by the RPG module's effects, magic, and equipment subsystems):
| Signal | Payload |
|---|---|
status_effect_ticked |
owner: StringName, effect_id: StringName, tick_index: int |
status_effect_expired |
owner: StringName, effect_id: StringName |
spell_cast |
caster: StringName, spell_id: StringName, target: Node, status: StringName |
item_equipped |
owner: StringName, slot: StringName, item_id: StringName |
item_unequipped |
owner: StringName, slot: StringName, item_id: StringName |
For spell_cast, status is the CastResult.Status name (ok,
insufficient_mana, on_cooldown, ...), so a single subscriber can react to
both successful and failed casts.
Phase 5 (XP and level-up progression):
| Signal | Payload |
|---|---|
xp_gained |
owner: StringName, amount: float, source: StringName |
leveled_up |
owner: StringName, new_level: int, reward_tier: StringName |
xp_gained fires once per XPSystem.grant_xp(...) call, before any
resulting level-up signals. source identifies the XP origin — &"manual"
for direct grants, &"kill" when driven by the died signal, &"quest"
for quest rewards — so subscribers can route XP popups to the right UI
channel. leveled_up fires once per level crossed; a single grant_xp
call that spans multiple levels produces N sequential signals, and
reward_tier echoes LevelUpRewardResource.unlock_tier (or &"" when
the level-up applied no reward).
Phase 6 (world layer — death, loot, scene transitions, dialog, shops):
| Signal | Payload |
|---|---|
died |
victim: StringName, killer: StringName |
chest_opened |
chest_id: StringName, opener: StringName |
scene_transition_requested |
from_scene: String, to_scene: String, target_spawn_point: StringName |
dialog_started |
npc_id: StringName, dialog_tree_id: StringName |
dialog_completed |
npc_id: StringName, dialog_tree_id: StringName, outcome: StringName |
shop_transaction |
actor: StringName, vendor_id: StringName, transaction_type: StringName, item_id: StringName, amount: int, currency_delta: int |
died.killer is &"" for environmental, suicide, or poison-tick deaths.
chest_opened fires exactly once per chest instance even if the chest is
interacted with again afterwards. scene_transition_requested announces
intent only — the event bus does not change scenes itself; gameplay code
drives the actual SceneTree.change_scene_to_file and snaps the player
to target_spawn_point. dialog_completed.outcome is an optional
StringName tag the dialog author can attach to the terminal node (for
example &"quest_accepted"); it is &"" when no outcome tag was set.
For shop_transaction, transaction_type is &"buy" or &"sell", and
currency_delta is the net change in currency on actor's side:
negative on buy (spent gold), positive on sell (received gold).
list_signals() -> Array[String]— returns the declared signal names in sorted ascending order. Used by MCP introspection and stable in golden tests.emit_validated(signal_name: StringName, args: Array) -> bool— emits a declared signal after type-checking its payload against the registered schema. Returnstrueon success; returnsfalseand emits a singlepush_errorwhen the signal is unknown, the argument count is wrong, or an argument type does not match the schema. The error message always includes the signal name and, for type mismatches, the expected type at the offending argument position.
Subscribing works through the standard Godot API:
GameEvents.crafting_completed.connect(_on_crafting_completed)
func _on_crafting_completed(recipe_id: StringName, outputs: Array) -> void:
print("Crafted ", recipe_id, " -> ", outputs)TestReport (addons/forgekit_core/testing/test_report.gd) is the canonical
JSON-serializable payload emitted by GUT, property, and gameplay runs. The
MCP server, the self-healing loop, and CI all consume the same shape.
TestReport { run_id, timestamp, total, passed, failed, tests[], suggested_action }
TestCase { name, status, duration_ms, assertions[], failure_message, stack_trace }
Assertion { description, passed, expected, actual }
status is one of "passed", "failed", or "skipped" (the producer
chooses the vocabulary). expected and actual accept any
JSON-serializable Variant.
to_dict() -> Dictionary/to_json() -> String— serialize the report and all nested cases / assertions.static from_dict(data: Dictionary) -> TestReport/static from_json(text: String) -> TestReport— reconstruct a report. Missing keys fall back to documented defaults; malformed JSON, non-Dictionaryroots, and malformed entries intests[]are skipped defensively so consumers can always inspect the result.is_suggested_action_valid() -> bool— returnstruewhensuggested_actionrespects the bounded-value contract below.
ALLOWED_SUGGESTED_ACTIONS is a PackedStringArray with exactly four
entries:
| Value | When producers set it |
|---|---|
inspect_tres |
A .tres resource looks malformed or out of date. |
validate_gdscript |
A script-level failure (parse error, invalid signature). |
rerun_test |
The failure looks flaky and a retry is recommended. |
manual_review |
No automated recovery path; a human should investigate. |
suggested_action may be left empty when failed == 0. When failed > 0,
it MUST be one of the four values above; is_suggested_action_valid()
enforces both halves of this rule.
On the MCP server side, test_report.serialize and test_report.parse
are backed by serializeTestReport and parseTestReport in
@forgekitstudio/core-mcp. They use the same JSON shape as the GDScript
producer, but the server parser is strict: any malformed input
(non-string payload, invalid JSON, wrong root type, missing or mistyped
field, non-object entry in tests[] or assertions[]) raises
TestReportParseError with code TEST_REPORT_PARSE_ERROR. The error
message identifies the offending field by path (for example
tests[2].assertions[0].expected is missing.). This contrasts with the
GDScript loader, which skips malformed entries defensively because it
runs inside the engine and must not crash the self-healing loop; on the
server boundary we surface explicit errors so that JSON-RPC clients can
react.
For every valid TestReport r, both
TestReport.from_json(r.to_json()) in GDScript and
parseTestReport(serializeTestReport(r).json) in the MCP server
reproduce every field on r, including Unicode strings (emoji, CJK,
combining marks, and astral-plane code points) inside failure_message,
stack_trace, TestCase.name, and Assertion descriptions. Numeric
counters (total, passed, failed, TestCase.duration_ms) round-trip
as int.
The MCP server exposes project.get_settings as the canonical way to
inspect a project's project.godot from an agent. The tool reads directly
from disk on every call, so the response always reflects the authoritative
on-disk state rather than the editor's in-memory copy. After an
update_settings write completes, the next get_settings call observes
the new value.
| Field | Type | Required | Notes |
|---|---|---|---|
projectRoot |
string | yes | Absolute path to the directory containing project.godot. |
section |
string | no | Section header without brackets (e.g. application). No /. |
- Without
section:{ "settings": { "<section>/<key>": "<raw value>", ... } } - With
section:{ "settings": { "<key>": "<raw value>", ... } }— only keys inside the requested section, with the section prefix stripped. Unknown sections resolve to{ "settings": {} }.
Values are returned verbatim — exactly the text to the right of = in
project.godot. Godot literals such as PackedStringArray("4.6", "Forward Plus") or quoted strings like "ForgeKit Core Template" are not coerced;
the caller owns interpretation.
ToolInputError—projectRootis missing or empty, orsectioncontains/or is otherwise malformed.ProjectIoError—project.godotcannot be read (missing file, permissions, etc.). The message includes the attempted path and the underlying reason.
Writes issued through project.update_settings are crash-safe: the server
serializes the merged INI tree into a sibling temp file, fsyncs it, and
atomically renames it over project.godot. A concurrent reader — including
a follow-up project.get_settings call — therefore observes either the
complete previous contents or the complete new contents, never a truncated
file, even if the server is killed mid-write. This is what lets
project.update_settings merge a single key without risking the known
"overwrites events" class of bugs seen in naive implementations.
project.check_imports statically enforces the separation between
forgekit_core and forgekit_rpg. The tool walks every .gd file under
addons/forgekit_core/** and addons/forgekit_rpg/**, extracts every
preload("res://..."), load("res://..."), and extends "res://..."
target, and reports any file that crosses a module boundary. CI and the
tools/cli_runner/check_imports.sh helper call the same implementation.
tools/run_check_imports.mjs is a Node-only counterpart that imports
mcp-server/dist/src/tools/project/check_imports.js directly and exits
with code 1 when violations are found — handy for running the check
locally after npm run build without spawning a shell pipeline.
| Field | Type | Required | Notes |
|---|---|---|---|
projectRoot |
string | yes | Absolute path to the directory containing project.godot. |
{ "violations": [ { "file": "<project-relative path>", "imports": ["res://..."], "reason": "<english explanation>" }, ... ] }
Files that respect the boundary rules are omitted entirely; an empty
violations array means the project is clean. Each violation aggregates
every offending target from a single file, deduplicated in first-seen
order, so consumers act on files rather than individual lines.
- Core → elsewhere is forbidden. A file under
addons/forgekit_core/may only reference otheraddons/forgekit_core/paths. Anyres://addons/forgekit_<other>/...target is flagged. - RPG subsystems talk through
public_api.gd. A file underaddons/forgekit_rpg/<subsystem>/may reference its own subsystem,addons/forgekit_core/..., oraddons/forgekit_rpg/public_api.gd. Any otheraddons/forgekit_rpg/...target (a different subsystem) or anyaddons/forgekit_<other-module>/...target is flagged. - Non-
forgekit_*targets are ignored. References to scenes, assets, or third-party addons outside theforgekit_*namespace are not part of the boundary contract.
ToolInputError—projectRootis missing, not a string, or empty.
- Bug reports and feature requests. File an issue on the
ForgeKitStudio/forgekit-coreissue tracker. Use the "Bug report" or "Feature request" template under.github/ISSUE_TEMPLATE/— both ask for the information needed to reproduce the problem. - Discussion forum. Open-ended questions, design proposals, and show-and-tell posts belong in GitHub Discussions rather than the issue tracker.
- Security issues. Do not file them publicly. Follow the private
disclosure process in
SECURITY.md. - Paid RPG module. Use the support channel listed on the itch.io or Gumroad product page for license and download issues; module code bugs still go to the public issue tracker.
ForgeKit Core is distributed under the MIT License. The
forgekit_rpg module shipped under addons/forgekit_rpg/ is sold
separately under a commercial EULA. See NOTICE.md for the
current list of installed modules and their licenses.
- Contributing guide — branching, Conventional Commits, pull request checklist.
- Code of Conduct — expectations for participation.
- Security policy — how to report vulnerabilities privately.