@npgamedev/godot-mcp-server 1.0.1 → 1.0.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/index.js +7 -2
- package/dist/startup/lifecycle.js +7 -3
- package/dist/startup/startupEnv.js +18 -11
- package/dist/transport/bridge.js +28 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -201,8 +201,8 @@ Everything works with zero configuration; the registry handles discovery. The va
|
|
|
201
201
|
| `GODOT_MCP_TOKEN_PATH` | auto | Auth-token file override (default: the path the plugin publishes in the registry) |
|
|
202
202
|
| `GODOT_MCP_READ_ONLY` | `0` | Set to `1` to hide every mutating tool (server-enforced) |
|
|
203
203
|
| `GODOT_MCP_RATE_LIMIT` | `0` | Max tool calls per second (`0` = unlimited) |
|
|
204
|
-
| `GODOT_MCP_SCRIPT_READ_LIMIT` |
|
|
205
|
-
| `GODOT_MCP_WS_BUFFER_LIMIT` |
|
|
204
|
+
| `GODOT_MCP_SCRIPT_READ_LIMIT` | the project's setting | Overrides the editor's cap on script-read responses, in bytes (minimum 65536) |
|
|
205
|
+
| `GODOT_MCP_WS_BUFFER_LIMIT` | the project's setting | Overrides the editor's per-connection WebSocket buffer size, in bytes (minimum 262144). Applies to connections the editor accepts after the server pushes it, so it takes effect from the next connection |
|
|
206
206
|
| `GODOT_MCP_CONFIG_VERSION` | written by the plugin | Config-schema version stamp in `.mcp.json`; the server warns on stderr if it is missing or does not match |
|
|
207
207
|
|
|
208
208
|
### CLI flags
|
package/dist/index.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
18
18
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
19
|
-
import { createBridge } from "./transport/bridge.js";
|
|
19
|
+
import { capKb, createBridge } from "./transport/bridge.js";
|
|
20
20
|
import { isReadOnly } from "./security/profiles.js";
|
|
21
21
|
import { toolRefCount } from "./registration/toolRefs.js";
|
|
22
22
|
import { createHookPipeline } from "./startup/hooks.js";
|
|
@@ -72,9 +72,14 @@ const extensions = createExtensionManager({ server, bridge, getReadOnly: isReadO
|
|
|
72
72
|
// discover is injected (extensions.discoverExtensions, a closure method — no
|
|
73
73
|
// `this`) so reconcile.ts imports no other composition module (acyclic graph).
|
|
74
74
|
const reconciler = createReconciler({ server, bridge, projectPath, discover: extensions.discoverExtensions });
|
|
75
|
+
// A cap the env sets is shown as the KB value the bridge pushes; an unset one as
|
|
76
|
+
// "project", because the editor's own setting governs it.
|
|
77
|
+
function capLabel(bytes) {
|
|
78
|
+
return bytes === undefined ? "project" : `${capKb(bytes)}KB`;
|
|
79
|
+
}
|
|
75
80
|
function logStartup(extTimedOut = false) {
|
|
76
81
|
const suffix = extTimedOut ? " (ext discovery timed out — extensions_refresh available)" : "";
|
|
77
|
-
process.stderr.write(`[godot-mcp] readOnly=${isReadOnly()} tools=${toolRefCount()} hooks=${hookPipeline.length} caps=${caps.scriptReadLimitBytes
|
|
82
|
+
process.stderr.write(`[godot-mcp] readOnly=${isReadOnly()} tools=${toolRefCount()} hooks=${hookPipeline.length} caps=${capLabel(caps.scriptReadLimitBytes)}/${capLabel(caps.wsBufferLimitBytes)}${suffix}\n`);
|
|
78
83
|
}
|
|
79
84
|
// ── Initial registration ────────────────────────────────────────────
|
|
80
85
|
// Snapshot whether the Godot version is unknown at eager registration. When it
|
|
@@ -42,9 +42,13 @@ export function installProcessHandlers(bridge) {
|
|
|
42
42
|
process.stdout.on("error", (err) => {
|
|
43
43
|
departureShutdown(`stdout unwritable (${err?.code ?? err?.message ?? "unknown"})`);
|
|
44
44
|
});
|
|
45
|
-
// Diagnostics gone: swallow
|
|
46
|
-
//
|
|
47
|
-
//
|
|
45
|
+
// Diagnostics gone: swallow, since nothing can be logged about a dead log sink. Once
|
|
46
|
+
// installed, this listener keeps every direct process.stderr.write in the server safe
|
|
47
|
+
// after the pipe dies, not only logSafely's, so it must not be dropped in favour of
|
|
48
|
+
// logSafely. A try/catch cannot replace it: the failure arrives as an asynchronous
|
|
49
|
+
// 'error' event after write() has already returned. With no listener that event is
|
|
50
|
+
// thrown into uncaughtException, whose handler writes to the same dead stderr and
|
|
51
|
+
// re-enters itself, pinning a core at 100% CPU.
|
|
48
52
|
process.stderr.on("error", () => {
|
|
49
53
|
/* muted */
|
|
50
54
|
});
|
|
@@ -90,19 +90,21 @@ export function applyCliMetaGates(cli) {
|
|
|
90
90
|
process.exit(0);
|
|
91
91
|
}
|
|
92
92
|
}
|
|
93
|
-
//
|
|
94
|
-
const SCRIPT_READ_LIMIT_DEFAULT = 262144; // 256 KB
|
|
95
|
-
const WS_BUFFER_LIMIT_DEFAULT = 1048576; // 1 MB
|
|
93
|
+
// Floors match the minimums the plugin clamps meta.set_limits values to.
|
|
96
94
|
const SCRIPT_READ_LIMIT_FLOOR = 65536; // 64 KB
|
|
97
95
|
const WS_BUFFER_LIMIT_FLOOR = 262144; // 256 KB
|
|
98
|
-
|
|
96
|
+
/** Read one cap from the environment. Unset or empty yields undefined. An invalid
|
|
97
|
+
* value is ignored with a warning instead of becoming a default, since a pushed
|
|
98
|
+
* default would override the project's own setting. A value below the floor is
|
|
99
|
+
* still a configured cap, so it is clamped up to the floor. */
|
|
100
|
+
function parseCapEnv(envName, floor) {
|
|
99
101
|
const raw = process.env[envName];
|
|
100
102
|
if (!raw)
|
|
101
|
-
return
|
|
103
|
+
return undefined;
|
|
102
104
|
const parsed = Number(raw);
|
|
103
105
|
if (!Number.isFinite(parsed) || parsed <= 0) {
|
|
104
|
-
process.stderr.write(`[godot-mcp] WARNING: ${envName}=${raw} is not a valid positive number;
|
|
105
|
-
return
|
|
106
|
+
process.stderr.write(`[godot-mcp] WARNING: ${envName}=${raw} is not a valid positive number; ignoring it, so the project's own setting applies\n`);
|
|
107
|
+
return undefined;
|
|
106
108
|
}
|
|
107
109
|
if (parsed < floor) {
|
|
108
110
|
process.stderr.write(`[godot-mcp] WARNING: ${envName}=${parsed} is below minimum ${floor}; clamping to ${floor}\n`);
|
|
@@ -110,11 +112,16 @@ function parseCapEnv(envName, defaultVal, floor) {
|
|
|
110
112
|
}
|
|
111
113
|
return parsed;
|
|
112
114
|
}
|
|
113
|
-
/**
|
|
115
|
+
/**
|
|
116
|
+
* Resolve the response caps from `GODOT_MCP_SCRIPT_READ_LIMIT` and
|
|
117
|
+
* `GODOT_MCP_WS_BUFFER_LIMIT`, both in bytes. A variable that is unset, empty or
|
|
118
|
+
* invalid leaves its cap undefined; a value below the floor is clamped to it.
|
|
119
|
+
*/
|
|
114
120
|
export function resolveResponseCaps() {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
121
|
+
return {
|
|
122
|
+
scriptReadLimitBytes: parseCapEnv("GODOT_MCP_SCRIPT_READ_LIMIT", SCRIPT_READ_LIMIT_FLOOR),
|
|
123
|
+
wsBufferLimitBytes: parseCapEnv("GODOT_MCP_WS_BUFFER_LIMIT", WS_BUFFER_LIMIT_FLOOR),
|
|
124
|
+
};
|
|
118
125
|
}
|
|
119
126
|
// ── Config version check ─────────────────────────────────────────────
|
|
120
127
|
const EXPECTED_CONFIG_VERSION = 1;
|
package/dist/transport/bridge.js
CHANGED
|
@@ -3,6 +3,15 @@ import { createChannel } from "./channel.js";
|
|
|
3
3
|
import { createRuntimeConnection } from "./runtimeConnection.js";
|
|
4
4
|
import { parseGodotVer } from "../shared/version.js";
|
|
5
5
|
import { lookupProject } from "../registry.js";
|
|
6
|
+
/**
|
|
7
|
+
* Convert a cap in bytes to the whole kilobytes `meta.set_limits` takes: the value
|
|
8
|
+
* the bridge pushes for it.
|
|
9
|
+
*
|
|
10
|
+
* @internal
|
|
11
|
+
*/
|
|
12
|
+
export function capKb(bytes) {
|
|
13
|
+
return Math.round(bytes / 1024);
|
|
14
|
+
}
|
|
6
15
|
/**
|
|
7
16
|
* Build the bridge for one Godot project. Connection is lazy — the first
|
|
8
17
|
* {@link Bridge.call} performs the WebSocket connect + auth handshake; the runtime
|
|
@@ -12,7 +21,7 @@ import { lookupProject } from "../registry.js";
|
|
|
12
21
|
* is re-discovered from the registry on disconnect unless `explicitEditorPort` is
|
|
13
22
|
* set or the project path is unknown
|
|
14
23
|
* @param opts - see {@link BridgeOptions}: project path for registry discovery,
|
|
15
|
-
* static-port overrides, and the response/buffer caps pushed to the plugin
|
|
24
|
+
* static-port overrides, and the optional response/buffer caps pushed to the plugin
|
|
16
25
|
* @returns the {@link Bridge}, augmented with `onNotification` (unsolicited plugin
|
|
17
26
|
* pushes) and `onGodotVersionKnown` (fires once on the unknown → known version
|
|
18
27
|
* transition, so the composition root can complete a tool surface registered
|
|
@@ -49,20 +58,25 @@ export function createBridge(editorUrl, opts) {
|
|
|
49
58
|
}
|
|
50
59
|
}
|
|
51
60
|
}
|
|
52
|
-
//
|
|
53
|
-
//
|
|
61
|
+
// The project's mcp_toolkit/limits/* settings apply unless an env var sets a
|
|
62
|
+
// cap. Then only that cap is pushed, from the initial editor channel's auth
|
|
63
|
+
// callback below; the channel rediscoverEditor opens does not push. The plugin
|
|
64
|
+
// sets it in memory, where it stays in force until the editor restarts or the
|
|
65
|
+
// setting is changed, and the next settings save from any source persists it
|
|
66
|
+
// to project.godot. With none set nothing is sent, so a cap set in the dock or
|
|
67
|
+
// in Project Settings stays in force and stays in project.godot.
|
|
54
68
|
function sendLimitsIfConfigured(channel) {
|
|
55
|
-
const scriptKb = opts?.scriptReadLimitBytes ?
|
|
56
|
-
const wsKb = opts?.wsBufferLimitBytes ?
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
}
|
|
69
|
+
const scriptKb = opts?.scriptReadLimitBytes ? capKb(opts.scriptReadLimitBytes) : 0;
|
|
70
|
+
const wsKb = opts?.wsBufferLimitBytes ? capKb(opts.wsBufferLimitBytes) : 0;
|
|
71
|
+
const params = {};
|
|
72
|
+
if (scriptKb > 0)
|
|
73
|
+
params.script_read_cap_kb = scriptKb;
|
|
74
|
+
if (wsKb > 0)
|
|
75
|
+
params.ws_buffer_kb = wsKb;
|
|
76
|
+
if (Object.keys(params).length === 0)
|
|
77
|
+
return;
|
|
78
|
+
// Fire-and-forget: a failed push leaves the project's own setting in force.
|
|
79
|
+
void channel.call("meta.set_limits", params, 5000).catch(() => { });
|
|
66
80
|
}
|
|
67
81
|
const getNotificationHandler = () => notificationHandler;
|
|
68
82
|
let editor = createChannel(editorUrl, projectPath, ({ version, headless: h }) => {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@npgamedev/godot-mcp-server",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"description": "MCP server that connects AI coding assistants to the Godot 4.2+ editor (scenes, nodes, scripts, ClassDB, playtests) via the companion godot-mcp-toolkit plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|