@npgamedev/godot-mcp-server 1.0.0 → 1.0.1
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 +3 -3
- package/dist/shared/version.js +9 -2
- package/dist/startup/lifecycle.js +52 -15
- package/dist/transport/channel.js +3 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
# Godot MCP Server
|
|
5
5
|
|
|
6
6
|
[](https://github.com/NPGameDev/godot-mcp-server/actions/workflows/ci.yml)
|
|
7
|
-
](https://www.npmjs.com/package/@npgamedev/godot-mcp-server)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
10
|
The npm bridge that connects AI coding assistants to the Godot 4.2+ editor over the [Model Context Protocol](https://modelcontextprotocol.io). Your assistant can create scenes, edit scripts, inspect nodes, run playtests, and read the results back, directly inside the editor while you watch. It pairs with the [Godot MCP Toolkit](https://github.com/NPGameDev/godot-mcp-toolkit) editor plugin, which hosts the WebSocket servers this bridge talks to.
|
|
@@ -118,7 +118,7 @@ Launch your MCP client from the project root. The server discovers the plugin th
|
|
|
118
118
|
Then try the first prompt below. This is the kind of result it produces:
|
|
119
119
|
|
|
120
120
|
<!-- captured: pre-1.0, Godot 4.7, 2026-07-24, human-recorded (editor + MCP dock, 1 peer connected, game running); image lives in the toolkit repo (docs/media/), absolute raw URL so it renders on npm. -->
|
|
121
|
-

|
|
122
122
|
|
|
123
123
|
If a step does not produce its "you should see", head to the [troubleshooting guide](https://github.com/NPGameDev/godot-mcp-toolkit/blob/main/docs/troubleshooting.md). It starts with a 60-second checklist and a connectivity probe.
|
|
124
124
|
|
|
@@ -132,7 +132,7 @@ If a step does not produce its "you should see", head to the [troubleshooting gu
|
|
|
132
132
|
The last three run in seconds. The first one is a real project, the same kind of small game we build end-to-end when validating a release, in a single agent session. Larger games span multiple sessions, with or without MCP.
|
|
133
133
|
|
|
134
134
|
<!-- captured: pre-1.0, Godot 4.5, 2026-07-24, via runtime_screenshot of the running brick-breaker (mid-flight); image lives in the toolkit repo (docs/media/). -->
|
|
135
|
-

|
|
136
136
|
|
|
137
137
|
*The brick-breaker from prompt 1 above.*
|
|
138
138
|
|
package/dist/shared/version.js
CHANGED
|
@@ -60,9 +60,14 @@ export function isVersionCompatible(connected, min, max) {
|
|
|
60
60
|
*
|
|
61
61
|
* Returns:
|
|
62
62
|
* "ok" — versions match (all components equal)
|
|
63
|
-
* "
|
|
63
|
+
* "patch" — same major+minor, different patch
|
|
64
|
+
* "minor" — same major, different minor
|
|
64
65
|
* "major" — different major version
|
|
65
66
|
* "unknown" — remote is undefined/empty (pre-handshake peer)
|
|
67
|
+
*
|
|
68
|
+
* A patch difference is its own severity because compatibility floors are declared
|
|
69
|
+
* at major.minor (ADR 0024) — the patch segment carries no compatibility meaning, so
|
|
70
|
+
* a patch-level difference between the two halves is not something to warn about.
|
|
66
71
|
*/
|
|
67
72
|
export function compareVersions(local, remote) {
|
|
68
73
|
if (remote == null || remote === "")
|
|
@@ -76,7 +81,9 @@ export function compareVersions(local, remote) {
|
|
|
76
81
|
}
|
|
77
82
|
if (localParts[0] !== remoteParts[0])
|
|
78
83
|
return "major";
|
|
79
|
-
if (localParts[1] !== remoteParts[1]
|
|
84
|
+
if (localParts[1] !== remoteParts[1])
|
|
80
85
|
return "minor";
|
|
86
|
+
if (localParts[2] !== remoteParts[2])
|
|
87
|
+
return "patch";
|
|
81
88
|
return "ok";
|
|
82
89
|
}
|
|
@@ -1,22 +1,59 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
1
|
+
/** Upper bound on the graceful close. A frozen editor makes the WebSocket close
|
|
2
|
+
* handshake wait for ws's 30 s timeout; a departing server exits well before that. */
|
|
3
|
+
export const SHUTDOWN_DEADLINE_MS = 2_000;
|
|
4
|
+
/** Write a diagnostic line without ever throwing. A file-backed stderr can throw
|
|
5
|
+
* synchronously; a pipe-backed one reports failure through its 'error' event, which
|
|
6
|
+
* the muted listener in installProcessHandlers absorbs. Either way the caller continues. */
|
|
7
|
+
export function logSafely(line) {
|
|
8
|
+
try {
|
|
9
|
+
process.stderr.write(line);
|
|
10
|
+
}
|
|
11
|
+
catch {
|
|
12
|
+
// The diagnostics sink is unusable; dropping the line is the only safe option.
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** Install the departure shutdown (stdin EOF, dead stdout, SIGINT/SIGTERM → await
|
|
16
|
+
* bridge.close bounded by SHUTDOWN_DEADLINE_MS → exit 0), the muted stderr listener,
|
|
17
|
+
* and the unhandledRejection / uncaughtException loggers that keep the bridge alive. */
|
|
3
18
|
export function installProcessHandlers(bridge) {
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
19
|
+
let shuttingDown = false;
|
|
20
|
+
// The single exit path. First trigger wins: a dying client can deliver stdin EOF, a
|
|
21
|
+
// stream error and a signal within one tick, and bridge.close() must run once. The
|
|
22
|
+
// close races the deadline so a hung peer can never keep a departed server alive.
|
|
23
|
+
// Always exit 0: the server ended because its job ended, not because it failed.
|
|
24
|
+
function departureShutdown(reason) {
|
|
25
|
+
if (shuttingDown)
|
|
26
|
+
return;
|
|
27
|
+
shuttingDown = true;
|
|
28
|
+
logSafely(`[godot-mcp] shutting down: ${reason}\n`);
|
|
29
|
+
const deadline = setTimeout(() => process.exit(0), SHUTDOWN_DEADLINE_MS);
|
|
30
|
+
deadline.unref();
|
|
31
|
+
void bridge.close().then(() => process.exit(0), () => process.exit(0));
|
|
11
32
|
}
|
|
12
|
-
process.on("SIGINT",
|
|
13
|
-
process.on("SIGTERM",
|
|
14
|
-
//
|
|
15
|
-
//
|
|
33
|
+
process.on("SIGINT", () => departureShutdown("SIGINT"));
|
|
34
|
+
process.on("SIGTERM", () => departureShutdown("SIGTERM"));
|
|
35
|
+
// Client departure: the client closed its end of our stdin. Fires whether or not we
|
|
36
|
+
// ever write again, and it is the only signal that fires while an editor socket keeps
|
|
37
|
+
// the event loop alive.
|
|
38
|
+
process.stdin.on("end", () => departureShutdown("stdin closed by the client"));
|
|
39
|
+
process.stdin.on("close", () => departureShutdown("stdin closed"));
|
|
40
|
+
// Transport dead: any stdout failure means no response can reach the client — not
|
|
41
|
+
// tied to one error code on purpose (an unlisted code would leave a zombie).
|
|
42
|
+
process.stdout.on("error", (err) => {
|
|
43
|
+
departureShutdown(`stdout unwritable (${err?.code ?? err?.message ?? "unknown"})`);
|
|
44
|
+
});
|
|
45
|
+
// Diagnostics gone: swallow. The listener's presence is the fix — an 'error' with
|
|
46
|
+
// no listener is thrown, lands in uncaughtException, gets logged to the same dead
|
|
47
|
+
// stderr, and so on forever. Nothing can be logged about a dead log sink.
|
|
48
|
+
process.stderr.on("error", () => {
|
|
49
|
+
/* muted */
|
|
50
|
+
});
|
|
51
|
+
// Stray errors never end the process (§7.6). Logging goes through logSafely so a
|
|
52
|
+
// dead stderr cannot re-enter these handlers.
|
|
16
53
|
process.on("unhandledRejection", (reason) => {
|
|
17
|
-
|
|
54
|
+
logSafely(`[godot-mcp] unhandledRejection: ${String(reason)}\n`);
|
|
18
55
|
});
|
|
19
56
|
process.on("uncaughtException", (err) => {
|
|
20
|
-
|
|
57
|
+
logSafely(`[godot-mcp] uncaughtException: ${err?.stack ?? err}\n`);
|
|
21
58
|
});
|
|
22
59
|
}
|
|
@@ -132,6 +132,9 @@ export function createChannel(url, projectPath, onAuthResolved, onNotification,
|
|
|
132
132
|
if (!skipVersionCheck) {
|
|
133
133
|
const serverVer = getServerVersion();
|
|
134
134
|
const severity = compareVersions(serverVer, authResp.toolkitVersion);
|
|
135
|
+
// "patch" deliberately matches no branch below. Compatibility floors are declared at
|
|
136
|
+
// major.minor (ADR 0024), so a patch difference is compatible by construction and stays
|
|
137
|
+
// silent by fall-through — the omission is the behaviour, not an oversight.
|
|
135
138
|
if (severity === "major") {
|
|
136
139
|
process.stderr.write(`[bridge] ERROR: major version mismatch — server ${serverVer}, toolkit ${authResp.toolkitVersion}. Update both to the same major version.\n`);
|
|
137
140
|
}
|
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.1",
|
|
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",
|
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
"test:integration:portpin": "tsx test/integration/port-pin-probe.ts",
|
|
50
50
|
"repro:lsp-conflict": "tsx test/integration/lsp-conflict-repro.ts",
|
|
51
51
|
"probe:screenshot": "tsx test/screenshot-window-probe.ts",
|
|
52
|
+
"probe:departure": "tsx test/probes/client-departure-probe.ts",
|
|
52
53
|
"stress:dispatch": "tsx test/integration/dispatch-safety-stress.ts",
|
|
53
54
|
"lint": "eslint src/ test/",
|
|
54
55
|
"lint:fix": "eslint src/ test/ --fix",
|