378 lines
20 KiB
Markdown
378 lines
20 KiB
Markdown
# Advanced Configuration
|
||
|
||
These are **optional fine-tuning knobs** that most users never need to touch —
|
||
the defaults are chosen to work well out of the box. They live under
|
||
**Project → Project Settings → `mcp_toolkit/`** (enable *Advanced Settings* to
|
||
see them) and can also be set with `ProjectSettings`.
|
||
|
||
Apart from the response limits under **Limits**, whose ranges the dock and the
|
||
Project Settings inspector both enforce, none of these values are clamped in
|
||
code: the recommended ranges below are guidance, not enforced limits. An extreme
|
||
value is allowed and is your responsibility.
|
||
|
||
## Concurrency
|
||
|
||
The toolkit serialises editor mutations and arbitrates scene access across
|
||
multiple MCP clients. These keys tune that machinery.
|
||
|
||
### `mcp_toolkit/concurrency/scan_idle_timeout_ms` — default `5000`
|
||
|
||
How long a scene save or open waits for an in-progress `EditorFileSystem` scan
|
||
to finish before giving up, in milliseconds. Opening or saving a scene *during*
|
||
a scan can read inconsistent filesystem state and crash the editor, so the
|
||
toolkit waits the scan out first and **aborts the operation with a `TIMEOUT`
|
||
error** if the scan hasn't settled in time — it never proceeds into an active
|
||
scan.
|
||
|
||
- `0` = fail fast (don't wait at all).
|
||
- Recommended `1000`–`30000`.
|
||
- Raise it for large projects whose imports take long to scan, at the cost of
|
||
longer save/open stalls when a scan is genuinely stuck.
|
||
|
||
### `mcp_toolkit/concurrency/mutation_watchdog_grace_ms` — default `60000`
|
||
|
||
Extra grace, in milliseconds, added to a mutation's deadline before the dispatch
|
||
**watchdog** force-clears a wedged mutation lock. The deadline is the in-flight
|
||
command's *declared* `timeout_ms` (or the 300 s system maximum for a command
|
||
that doesn't declare one) **plus** this grace.
|
||
|
||
The watchdog is a **safety net**: it only fires if a mutation handler aborts or
|
||
hangs and would otherwise block *all* further mutations until the editor
|
||
restarts. In normal operation it never fires. Lowering this shortens recovery
|
||
from such a (rare) wedge but risks force-clearing a legitimately slow handler;
|
||
the generous default makes a false fire effectively impossible for any
|
||
well-behaved command.
|
||
|
||
### `mcp_toolkit/concurrency/scene_lease_ttl_ms` — default `8000`
|
||
|
||
When multiple clients edit different scenes, a time-bounded *lease* prevents
|
||
cross-scene contamination: tab-dependent commands from a non-holding client
|
||
queue until the holder's lease expires. This is how long, in milliseconds, the
|
||
holder may go without renewing before a waiting client can steal the lease.
|
||
|
||
- Lower = snappier hand-off between clients, but more tab switching.
|
||
- Higher = fewer tab switches, but a waiting client blocks longer.
|
||
|
||
## Reading large data — caps & the pagination contract
|
||
|
||
Read tools that can return a large payload are **capped per call** and **paginate**
|
||
instead of streaming a whole file in one response (responses are sent whole over a
|
||
size-limited WebSocket frame, so an uncapped read would silently drop). Two tools
|
||
page today — `save_read` (a `user://` file, in **bytes**) and `script_read` (a
|
||
project script, in **lines**) — and they share one **uniform pagination contract**,
|
||
so the loop you learn for one applies to the other.
|
||
|
||
### The uniform pagination contract
|
||
|
||
On a capped read you always get:
|
||
|
||
- **`has_more`** (bool) — `true` when more data remains beyond this window,
|
||
`false` when this window reached the end. Always present on a successful read.
|
||
- **`total_<unit>`** — the full size of the source (`total_bytes` for `save_read`,
|
||
`total_lines` for `script_read`), so you know how much is left.
|
||
- **`returned`** — the size of this window (bytes for `save_read`, lines for
|
||
`script_read`).
|
||
- **`next_<cursor>`** — the value to pass back to resume, present **only when
|
||
`has_more` is `true`** (`next_offset` for `save_read`, `next_start_line` for
|
||
`script_read`).
|
||
- **`hint`** — a one-line prose instruction, present **only when `has_more` is
|
||
`true`**, telling you exactly which cursor to feed back.
|
||
|
||
**The loop is always the same:** call the tool; if `has_more` is `true`, pass the
|
||
returned `next_<cursor>` back as the matching request parameter and call again;
|
||
stop when `has_more` is `false`.
|
||
|
||
**Worked example — `save_read` (bytes).** Request `offset` (default `0`) +
|
||
`max_bytes`; resume with `next_offset`:
|
||
|
||
```
|
||
save_read path=user://saves/big.dat max_bytes=400
|
||
→ { returned: 400, offset: 0, next_offset: 400,
|
||
total_bytes: 1000, has_more: true,
|
||
hint: "more bytes remain — re-call save.read with offset = next_offset (400) until has_more is false" }
|
||
save_read path=user://saves/big.dat offset=400 max_bytes=400
|
||
→ { returned: 400, next_offset: 800, total_bytes: 1000, has_more: true, hint: … }
|
||
save_read path=user://saves/big.dat offset=800 max_bytes=400
|
||
→ { returned: 200, next_offset: 1000, total_bytes: 1000, has_more: false } # done — no hint
|
||
```
|
||
|
||
**Worked example — `script_read` (lines).** Request `start_line`/`end_line`
|
||
(1-based); resume with `next_start_line`:
|
||
|
||
```
|
||
script_read file_path=res://big.gd start_line=1 end_line=200
|
||
→ { start_line: 1, end_line: 200, total_lines: 520, returned: 200, has_more: true,
|
||
next_start_line: 201,
|
||
hint: "more lines remain — re-call script.read with start_line = next_start_line (201) until has_more is false" }
|
||
script_read file_path=res://big.gd start_line=201 end_line=400
|
||
→ { start_line: 201, end_line: 400, total_lines: 520, returned: 200, has_more: true, next_start_line: 401, hint: … }
|
||
script_read file_path=res://big.gd start_line=401 end_line=600
|
||
→ { start_line: 401, end_line: 520, total_lines: 520, returned: 120, has_more: false } # clamped to EOF, done — no hint
|
||
```
|
||
|
||
A **full** `script_read` (no `start_line`) that fits under the cap returns
|
||
`has_more: false` and `total_lines` as well, so both read shapes carry the same
|
||
fields. The request-parameter names stay unit-appropriate (`offset`/`max_bytes`
|
||
for bytes, `start_line`/`end_line` for lines) — only the response *shape* is
|
||
uniform.
|
||
|
||
### When a cap change takes effect (no restart)
|
||
|
||
The read caps below (`save_read_cap_kb`, `script_read_cap_kb`) are read by the
|
||
handler **on every call**, so a change made in the dock, in Project Settings, or by
|
||
an `.mcp.json` override applies to the **very next call**, with no restart and no
|
||
reconnect. `ws_buffer_kb` is the exception: the editor sizes a connection's buffer
|
||
when it accepts the connection, so a change applies from the next connection.
|
||
|
||
**The server is not pushed the value.** The MCP server advertises a *static*
|
||
`max_bytes` schema bound as a sanity ceiling; the **toolkit enforces the real,
|
||
live cap at call time**. A request above the live cap is rejected with
|
||
`INVALID_PARAMS` whose message names the *current* cap — so if you lower the cap,
|
||
the model learns the new limit **reactively from that error**, not from the
|
||
advertised schema. (This is why a value that the schema appears to permit can
|
||
still be rejected: the live cap is authoritative.)
|
||
|
||
## Limits
|
||
|
||
The dock and the Project Settings inspector keep their own edits inside each
|
||
limit's range and step. A value set another way, such as an `.mcp.json` override
|
||
(given in bytes) or a hand edit of `project.godot`, is used as set (the save cap
|
||
still never drops below 64). Both surfaces then show it clamped to the range and
|
||
rounded to the nearest step.
|
||
|
||
### `mcp_toolkit/limits/save_read_cap_kb` — default `256`
|
||
|
||
The largest window `save_read` returns in a single call, in KB (64–4096, in steps
|
||
of 64). A `user://` file bigger than this cap can still be read in full by
|
||
paging — see **Reading large data** above for the `offset` / `next_offset` loop.
|
||
This is the only way to read a file larger than the WebSocket frame ceiling (see
|
||
`ws_buffer_kb` below), because responses are sent whole.
|
||
|
||
Raising this above `ws_buffer_kb` is a footgun: a window that would not fit the
|
||
WebSocket buffer is rejected with `FILE_TOO_LARGE` before it is sent, rather than
|
||
silently dropped. If you raise the cap, raise `ws_buffer_kb` to match (and note
|
||
the runtime caveat below).
|
||
|
||
### `mcp_toolkit/limits/script_read_cap_kb` — default `256`
|
||
|
||
The largest payload `script_read` returns in a single call, in KB (64–4096, in
|
||
steps of 64). A project script bigger than this cap is read in full by paging on
|
||
**lines** — see **Reading large data** above for the `start_line` /
|
||
`next_start_line` loop. A full read that would exceed the cap is rejected with
|
||
`FILE_TOO_LARGE` and a hint to use a line range.
|
||
|
||
If `GODOT_MCP_SCRIPT_READ_LIMIT` is set in `.mcp.json`, it overrides this value:
|
||
the MCP server sends it to the editor when it connects, and the dock then shows
|
||
it, clamped and rounded to the dock's range and step. The editor keeps it until it
|
||
restarts or the value is changed, and the next time project settings are saved, it
|
||
is written to `project.godot`.
|
||
|
||
### `mcp_toolkit/limits/ws_buffer_kb` — default `1024`
|
||
|
||
WebSocket per-peer buffer size, in KB (256–8192, in steps of 256). Raise it if you
|
||
send very large payloads (e.g. big `script_write` bodies) and see truncated or
|
||
dropped connections under load.
|
||
|
||
If `GODOT_MCP_WS_BUFFER_LIMIT` is set in `.mcp.json`, it overrides this value in
|
||
the same way, and the dock then shows it, clamped and rounded to the dock's range
|
||
and step. The editor sizes a connection's buffer when it accepts the connection,
|
||
so the override applies to connections accepted after the server sends it, not to
|
||
the connection that sent it.
|
||
|
||
> **Runtime (exported game) caveat.** This setting tunes the **editor** server
|
||
> only. The runtime server that runs inside an exported game uses a **fixed 1 MB
|
||
> per-peer buffer** and ignores `ws_buffer_kb`, so raising `ws_buffer_kb` does
|
||
> **not** raise the runtime ceiling — page large runtime reads with `save_read`'s
|
||
> `offset` instead.
|
||
|
||
## Listen ports (editor & runtime)
|
||
|
||
Two of the toolkit's three MCP channels bind a **dynamic** TCP port. By default
|
||
each **scans** a small band and publishes the bound port to the machine-wide
|
||
registry, so the MCP server discovers it with no configuration. You can instead
|
||
**pin** an exact port or **relocate** the scan band, per channel. These are read
|
||
from the process environment (the `.mcp.json` `env` block, or a shell `export`
|
||
before launch) — the toolkit never writes them.
|
||
|
||
| Env var | Channel | Effect | Default |
|
||
|---|---|---|---|
|
||
| `GODOT_MCP_EDITOR_PORT` | Editor channel | **Pin** — bind this exact port or fail | — (scan) |
|
||
| `GODOT_MCP_EDITOR_PORT_MIN` / `_MAX` | Editor channel | **Relocate** the scan band (inclusive) | `6550` / `6560` |
|
||
| `GODOT_MCP_RUNTIME_PORT` | Runtime channel (the running game) | **Pin** — bind this exact port or fail | — (scan) |
|
||
| `GODOT_MCP_RUNTIME_PORT_MIN` / `_MAX` | Runtime channel | **Relocate** the scan band (inclusive) | `6570` / `6585` |
|
||
|
||
The same `GODOT_MCP_EDITOR_PORT` / `GODOT_MCP_RUNTIME_PORT` are **also read by the
|
||
MCP server** to decide which port to **dial** — inherited by both processes, a pin
|
||
makes listen and dial agree with zero discovery. The `_MIN`/`_MAX` band vars are
|
||
**listen-side only** (the server never reads them; discovery covers the scanned
|
||
case). The third channel — the GDScript **LSP** — is Godot's to bind, so it stays
|
||
**connect-pin-only** via the server-side `GODOT_MCP_LSP_PORT` / `GODOT_MCP_LSP_HOST`
|
||
(next section).
|
||
|
||
### Pinned vs. Scanned — the two modes are exclusive
|
||
|
||
- **Pinned** (a `*_PORT` pin is set): the listener binds that **exact** port or
|
||
**fails loudly** — it never scans to a different port. If the port is occupied it
|
||
retries the same port briefly (to ride out a previous instance still releasing
|
||
it), then surfaces a **dock warning** (editor) or a loud game-console error
|
||
(runtime) naming the port. A pin makes the `_MIN`/`_MAX` band **irrelevant** — it
|
||
is ignored (a one-line note is logged).
|
||
- **Scanned** (no pin): the listener scans the band (default, or `_MIN`/`_MAX` if
|
||
set), binds the first free port, and publishes it for discovery. This is the
|
||
**low-friction** default — you don't have to manage env on both sides. If the
|
||
**whole band** is occupied, the editor shows the same dock warning naming the
|
||
range and keeps retrying.
|
||
|
||
A malformed pin, an out-of-range port (valid range `1–65535`), or `MIN > MAX` is a
|
||
**clear error** on the dock + console, never a silent fall-back to the default.
|
||
|
||
### An environment variable is not a sync channel
|
||
|
||
The editor process (which **listens**) and the MCP server process (which **dials**)
|
||
read the environment **independently**. A pin only makes them agree if **both**
|
||
processes inherit the **same** value:
|
||
|
||
- **The normal `.mcp.json` case sets `env` for the server only.** If you also launch
|
||
the Godot editor from a **desktop shortcut**, that shortcut does **not** inherit a
|
||
shell's transient `export` (notably on Windows), so the pin reaches the server but
|
||
not the editor — the server would then dial a port nobody is listening on. It now
|
||
**fails fast** with a precise message instead of hanging (and the editor dock shows
|
||
the mismatch), but the real fix is to make both sides inherit the pin.
|
||
- **The supported pattern (harness / parallel tests):** `export` the pin **once**,
|
||
then launch the editor **and** the MCP server/client as children of that same
|
||
environment so both inherit it:
|
||
|
||
```bash
|
||
export GODOT_MCP_EDITOR_PORT=6557
|
||
godot --editor --path /path/to/project & # editor inherits the pin (listens on 6557)
|
||
# …launch the MCP client from the same shell so its server inherits it too (dials 6557)
|
||
```
|
||
|
||
- **Prefer Scanned mode** if you don't want to manage env on both sides — registry
|
||
discovery keeps the two in agreement automatically.
|
||
|
||
## macOS: launching your MCP client (Node / PATH)
|
||
|
||
Modern GUI-launched MCP clients — Claude Desktop, VS Code, and Cursor — capture
|
||
your login shell's environment and resolve a bare `npx` to a version-manager Node
|
||
on their own, so the standard `npx -y @npgamedev/godot-mcp-server` config connects
|
||
whether you launch the client from Finder/Dock or a terminal. The toolkit writes
|
||
that standard command; there is nothing macOS-specific to configure.
|
||
|
||
**If a client won't connect on macOS**, work through these:
|
||
|
||
- **Launch the client from a terminal to see its error.** `open` won't help — start
|
||
the app's binary from a shell. A terminal launch inherits your `PATH` and prints
|
||
the client's real startup error, which tells you what to fix.
|
||
- **Confirm `.mcp.json` is present at your project root** and Node 22+ is installed
|
||
(`node --version`). If `.mcp.json` is missing, click **Write .mcp.json** in the
|
||
MCP Toolkit dock.
|
||
- **Move your version-manager init into `~/.zprofile`.** If your Node lives behind a
|
||
version manager (nvm/fnm/volta) whose init is only in `~/.zshrc`, a login-shell
|
||
launch won't see it — add it to `~/.zprofile` too so Finder/Dock launches resolve
|
||
Node as well.
|
||
- **Zero-config alternative:** install Node from the **official nodejs.org
|
||
installer**. It lands on the default `PATH`, so no shell setup is needed.
|
||
|
||
**Point a Mac at a local server build** (development) by setting
|
||
`GODOT_MCP_DEV_SERVER_PATH` in the environment to your built `dist/index.js` — the
|
||
toolkit then emits a `node <that path>` command instead of the released `npx` form.
|
||
|
||
## Language server (LSP)
|
||
|
||
The `lsp_*` tools connect to Godot's built-in GDScript language server. The MCP
|
||
server discovers the right endpoint per project from the registry, so a **single
|
||
editor needs no configuration**. These two env vars (set in a project's
|
||
`.mcp.json` `env` block) override discovery — needed only when running the LSP in
|
||
**more than one editor at once** (see `docs/multi-instance.md`).
|
||
|
||
### `GODOT_MCP_LSP_PORT` — default: discovered (else `6005`)
|
||
|
||
The GDScript LSP port this project's server connects to. Top priority — bypasses
|
||
registry discovery. Set it to the `--lsp-port` you launched that editor with.
|
||
|
||
### `GODOT_MCP_LSP_HOST` — default: discovered (else `127.0.0.1`)
|
||
|
||
The host for the GDScript LSP. Rarely needed — the LSP is localhost-only. Mirrors
|
||
the familiar `lsp.serverHost` client setting.
|
||
|
||
## Editor responsiveness while unfocused
|
||
|
||
> **Note — these two keys live in Editor Settings, not Project Settings.**
|
||
> Open **Editor → Editor Settings** and search for `mcp_toolkit/performance`.
|
||
> Everything else in this document is a *Project* setting; these two are the
|
||
> exception, because they control a **machine-global editor behaviour** and are a
|
||
> personal battery/CPU preference — so they are deliberately **not** written to
|
||
> `project.godot` (never committed to version control).
|
||
|
||
When the editor loses focus, Godot throttles its process loop to a low-power
|
||
frame rate (the `interface/editor/unfocused_low_processor_mode_sleep_usec`
|
||
editor setting, ~10 fps by default). The toolkit polls its WebSocket inside that
|
||
loop, so an unfocused editor answers MCP commands only ~2–3 times per second.
|
||
During a normal MCP session the editor *is* unfocused (you're looking at the chat
|
||
window), so the toolkit raises the unfocused frame rate while a client is
|
||
connected, then restores it on the last disconnect.
|
||
|
||
### `mcp_toolkit/performance/keep_editor_responsive_unfocused` — default `true`
|
||
|
||
Opt-in switch. When **on** (default), the toolkit boosts the unfocused frame rate
|
||
while at least one MCP client is connected. When **off**, Godot's default
|
||
low-power unfocused throttle is left untouched — choose this if you are
|
||
battery/CPU-sensitive and don't mind slower command pickup while the editor sits
|
||
in the background. A matching toggle and a live **Off / On (idle) / On · active**
|
||
indicator are in the dock's *Server Status* section.
|
||
|
||
### `mcp_toolkit/performance/unfocused_responsive_sleep_usec` — default `16666`
|
||
|
||
The boosted unfocused process sleep, in microseconds. Lower = higher frame rate =
|
||
snappier commands but more background CPU. Not clamped.
|
||
|
||
- `16666` ≈ **60 fps** (default) — maximum snappiness; also keeps automated
|
||
smoke/sweep runs fast. Zero behavioural change from earlier toolkit versions.
|
||
- `33333` ≈ **30 fps** (power-saver) — roughly half the background CPU. The
|
||
difference is imperceptible to an interactive user (command latency is
|
||
dominated by the agent's thinking time) and adds only ~20–30 s to a full
|
||
automated smoke run.
|
||
- The poll loop runs every 4th frame, so the effective MCP poll rate is about a
|
||
quarter of the frame rate (≈15 Hz at 60 fps, ≈7.5 Hz at 30 fps).
|
||
|
||
> A quick CPU sanity-check on one machine showed an idle editor's background CPU
|
||
> at 60 fps vs 30 fps vs the 10 fps default differs only modestly; exact numbers
|
||
> are machine-specific, so treat the above as guidance, not a measurement.
|
||
|
||
### Crash- and concurrency-safety
|
||
|
||
The boosted value is a machine-global setting, and Godot only flushes editor
|
||
settings to disk on certain events (closing the settings dialog, quitting, …), so
|
||
a crash *after* such a flush — or a second editor running at the same time —
|
||
could otherwise leave the setting stranded at the boosted value. To prevent that:
|
||
|
||
- Before boosting, the toolkit records the **true original** value once, to a
|
||
small machine-wide, Godot-version-keyed backup file (in the toolkit's registry
|
||
directory — the same place multi-instance discovery uses), under a
|
||
first-writer-wins file lock. A second editor that connects while the first is
|
||
already boosting will **not** overwrite that backup, so the true original is
|
||
never lost.
|
||
- On the last disconnect — and again as a **self-heal on the next editor
|
||
startup** — the toolkit reverts the setting **conflict-aware**: if the live
|
||
value still equals the value the toolkit wrote, it is restored to the true
|
||
original; if you (or another tool) changed it in the meantime, **your value is
|
||
kept** and the backup is simply cleared. Either way the boost can never persist
|
||
without a live connection.
|
||
|
||
**Documented edge cases:**
|
||
|
||
- If you manually set the key to *exactly* the boosted value while it is already
|
||
boosted, Godot emits no change event (same-value writes are no-ops), so the
|
||
toolkit cannot tell your value from its own — on restore it treats it as its own
|
||
and reverts to the original. This is the single case the conflict-aware check
|
||
cannot detect.
|
||
- With two editors connected at once, if the first disconnects it restores the
|
||
setting while the second is still connected, so the second runs at the default
|
||
unfocused rate until its next fresh connection. This is a brief responsiveness
|
||
dip, never a persistent change, and matches the toolkit's earlier behaviour.
|
||
|
||
---
|
||
|
||
*These are advanced tunables. If you're not sure, leave the defaults.*
|