godot-daedalus_backend 1.0.0

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.
Files changed (67) hide show
  1. package/README.md +101 -0
  2. package/bin/godot-daedalus-backend.js +4 -0
  3. package/bin/godot-daedalus-mcp.js +4 -0
  4. package/bin/godot-daedalus-terminal-mcp.js +4 -0
  5. package/bin/run-tsx-entry.js +26 -0
  6. package/package.json +54 -0
  7. package/scripts/deepseek-tokenizer-server.py +54 -0
  8. package/src/app-paths.ts +36 -0
  9. package/src/main.ts +21 -0
  10. package/src/mcp/content-length-protocol.ts +68 -0
  11. package/src/mcp/custom-mcp-config-store.ts +397 -0
  12. package/src/mcp/godot-diagnostics-bridge.ts +1298 -0
  13. package/src/mcp/godot-editor-bridge.ts +307 -0
  14. package/src/mcp/godot-mcp-server.ts +3484 -0
  15. package/src/mcp/godot-paths.ts +151 -0
  16. package/src/mcp/godot-project-settings.ts +233 -0
  17. package/src/mcp/godot-tool-registration.ts +46 -0
  18. package/src/mcp/mcp-config.ts +48 -0
  19. package/src/mcp/mcp-host.ts +393 -0
  20. package/src/mcp/mcp-session.ts +81 -0
  21. package/src/mcp/terminal-mcp-server.ts +576 -0
  22. package/src/mcp/tscn-tools.ts +302 -0
  23. package/src/mcp/types.ts +12 -0
  24. package/src/ping-client.ts +24 -0
  25. package/src/prompts/registry.ts +97 -0
  26. package/src/prompts/templates/backend-helper.md +25 -0
  27. package/src/prompts/templates/gdscript-reviewer.md +19 -0
  28. package/src/prompts/templates/godot-assistant.md +225 -0
  29. package/src/prompts/templates/scene-architect.md +15 -0
  30. package/src/prompts/templates/session-compressor.md +33 -0
  31. package/src/protocol/schema.ts +486 -0
  32. package/src/protocol/types.ts +77 -0
  33. package/src/providers/deepseek-agent.ts +1014 -0
  34. package/src/providers/deepseek-client.ts +114 -0
  35. package/src/providers/deepseek-dsml-tools.ts +90 -0
  36. package/src/providers/deepseek-loose-tools.ts +450 -0
  37. package/src/providers/provider-config-store.ts +164 -0
  38. package/src/server/client-session.ts +93 -0
  39. package/src/server/request-dispatcher.ts +74 -0
  40. package/src/server/response-helpers.ts +33 -0
  41. package/src/server/send-json.ts +8 -0
  42. package/src/server/websocket-server.ts +3997 -0
  43. package/src/session/session-compressor.ts +68 -0
  44. package/src/session/session-store.ts +669 -0
  45. package/src/skills/registry.ts +180 -0
  46. package/src/skills/templates/backend-helper.md +12 -0
  47. package/src/skills/templates/file-creator.md +14 -0
  48. package/src/skills/templates/gdscript-review.md +12 -0
  49. package/src/skills/templates/godot-project-init.md +29 -0
  50. package/src/skills/templates/scene-builder.md +12 -0
  51. package/src/tokens/deepseek-tokenizer-counter.ts +233 -0
  52. package/src/tokens/model-profiles.ts +38 -0
  53. package/src/tokens/token-counter-factory.ts +52 -0
  54. package/src/tokens/token-counter.ts +22 -0
  55. package/src/tools/approval-gateway.ts +111 -0
  56. package/src/tools/llm-tools.ts +1415 -0
  57. package/src/tools/tool-dispatcher.ts +147 -0
  58. package/src/tools/tool-event-describer.ts +387 -0
  59. package/src/tools/tool-idempotency.ts +373 -0
  60. package/src/tools/tool-policy-table.ts +61 -0
  61. package/src/tools/tool-policy.ts +73 -0
  62. package/src/workflow/llm-planner.ts +407 -0
  63. package/src/workflow/planner.ts +201 -0
  64. package/src/workflow/runner.ts +141 -0
  65. package/src/workflow/types.ts +69 -0
  66. package/src/workspace/registry.ts +104 -0
  67. package/src/workspace/types.ts +7 -0
@@ -0,0 +1,576 @@
1
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
+ import { type ChildProcess, spawn } from "node:child_process";
4
+ import * as path from "node:path";
5
+ import { z } from "zod";
6
+
7
+ const MAX_STDOUT_CHARS: number = 12000;
8
+ const MAX_STDERR_CHARS: number = 12000;
9
+ const COMMAND_TIMEOUT_MS: number = 30_000;
10
+
11
+ type CommandRisk = "read" | "verify" | "write" | "destructive";
12
+
13
+ type CommandPreset = {
14
+ name: string;
15
+ description: string;
16
+ command: string[];
17
+ workingDirectory: string;
18
+ risk: CommandRisk;
19
+ requiresGodotProject?: boolean | undefined;
20
+ resourcePathMode?: "optional" | "required" | undefined;
21
+ };
22
+
23
+ type SafePresetInput = {
24
+ presetName: string;
25
+ workingDirectory?: string | undefined;
26
+ resourcePath?: string | undefined;
27
+ };
28
+
29
+ const BACKEND_DIR: string = process.env.BACKEND_DIR ?? process.cwd();
30
+ const GODOT_EXECUTABLE: string = process.env.GODOT_EXECUTABLE_PATH ?? "godot";
31
+ const GODOT_PROJECT: string = process.env.GODOT_PROJECT_PATH ?? "";
32
+ const NPM_TYPECHECK_COMMAND: string[] = process.platform === "win32"
33
+ ? ["cmd.exe", "/d", "/s", "/c", "npm", "run", "typecheck"]
34
+ : ["npm", "run", "typecheck"];
35
+ const ALLOWED_WORKING_ROOTS: string[] = [
36
+ path.resolve(BACKEND_DIR),
37
+ ...(GODOT_PROJECT.length > 0 ? [path.resolve(GODOT_PROJECT)] : [])
38
+ ];
39
+
40
+ const COMMAND_PRESETS: CommandPreset[] = [
41
+ {
42
+ name: "backend.typecheck",
43
+ description: "运行后端 TypeScript 类型检查 (tsc --noEmit)",
44
+ command: NPM_TYPECHECK_COMMAND,
45
+ workingDirectory: BACKEND_DIR,
46
+ risk: "verify"
47
+ },
48
+ {
49
+ name: "git.status",
50
+ description: "显示 Git 工作区状态 (只读)",
51
+ command: ["git", "status", "--short"],
52
+ workingDirectory: BACKEND_DIR,
53
+ risk: "read"
54
+ },
55
+ {
56
+ name: "git.diff",
57
+ description: "显示 Git 工作区差异 (只读)",
58
+ command: ["git", "diff", "--stat"],
59
+ workingDirectory: BACKEND_DIR,
60
+ risk: "read"
61
+ },
62
+ {
63
+ name: "git.init",
64
+ description: "初始化 Git 仓库",
65
+ command: ["git", "init"],
66
+ workingDirectory: BACKEND_DIR,
67
+ risk: "write"
68
+ },
69
+ {
70
+ name: "godot.check_only",
71
+ description: "运行 Godot 语法检查 (只读)。可传 resourcePath 精确检查 .gd 脚本或加载 .tscn 场景。",
72
+ command: [GODOT_EXECUTABLE, "--headless", "--disable-crash-handler", "--path", GODOT_PROJECT, "--check-only", "--quit"],
73
+ workingDirectory: GODOT_PROJECT || BACKEND_DIR,
74
+ risk: "verify",
75
+ requiresGodotProject: true,
76
+ resourcePathMode: "optional"
77
+ },
78
+ {
79
+ name: "godot.validate_scene",
80
+ description: "验证指定场景文件的语法正确性 (只读)。必须传 resourcePath,例如 scenes/main.tscn。",
81
+ command: [GODOT_EXECUTABLE, "--headless", "--disable-crash-handler", "--path", GODOT_PROJECT, "--check-only", "--quit"],
82
+ workingDirectory: GODOT_PROJECT || BACKEND_DIR,
83
+ risk: "verify",
84
+ requiresGodotProject: true,
85
+ resourcePathMode: "required"
86
+ }
87
+ ];
88
+
89
+ function truncateOutput(text: string, maxChars: number): { text: string; truncated: boolean } {
90
+ if (text.length <= maxChars) {
91
+ return { text, truncated: false };
92
+ }
93
+
94
+ return {
95
+ text: text.slice(0, maxChars) + `\n\n[输出已截断,原始长度 ${text.length} 字符]`,
96
+ truncated: true
97
+ };
98
+ }
99
+
100
+ async function runCommand(command: string[], cwd: string): Promise<{
101
+ exitCode: number | null;
102
+ stdout: string;
103
+ stderr: string;
104
+ durationMs: number;
105
+ truncated: boolean;
106
+ }> {
107
+ return new Promise((resolve) => {
108
+ const startMs: number = Date.now();
109
+ let stdout: string = "";
110
+ let stderr: string = "";
111
+ let child: ChildProcess;
112
+
113
+ try {
114
+ child = spawn(command[0]!, command.slice(1), {
115
+ cwd,
116
+ stdio: ["ignore", "pipe", "pipe"],
117
+ timeout: COMMAND_TIMEOUT_MS
118
+ });
119
+ } catch (error: unknown) {
120
+ resolve({
121
+ exitCode: null,
122
+ stdout,
123
+ stderr: error instanceof Error ? `Process error: ${error.message}` : "Process spawn failed",
124
+ durationMs: Date.now() - startMs,
125
+ truncated: false
126
+ });
127
+ return;
128
+ }
129
+
130
+ child.stdout?.on("data", (data: Buffer): void => {
131
+ stdout += data.toString("utf8");
132
+ });
133
+
134
+ child.stderr?.on("data", (data: Buffer): void => {
135
+ stderr += data.toString("utf8");
136
+ });
137
+
138
+ child.on("error", (error: Error): void => {
139
+ stderr += `\nProcess error: ${error.message}`;
140
+ resolve({
141
+ exitCode: null,
142
+ stdout,
143
+ stderr,
144
+ durationMs: Date.now() - startMs,
145
+ truncated: false
146
+ });
147
+ });
148
+
149
+ child.on("close", (exitCode: number | null): void => {
150
+ const stdoutResult = truncateOutput(stdout, MAX_STDOUT_CHARS);
151
+ const stderrResult = truncateOutput(stderr, MAX_STDERR_CHARS);
152
+
153
+ resolve({
154
+ exitCode,
155
+ stdout: stdoutResult.text,
156
+ stderr: stderrResult.text,
157
+ durationMs: Date.now() - startMs,
158
+ truncated: stdoutResult.truncated || stderrResult.truncated
159
+ });
160
+ });
161
+ });
162
+ }
163
+
164
+ function findPreset(name: string): CommandPreset {
165
+ const preset: CommandPreset | undefined = COMMAND_PRESETS.find((p: CommandPreset): boolean => p.name === name);
166
+
167
+ if (!preset) {
168
+ throw new Error(`Unknown preset: ${name}. Available: ${COMMAND_PRESETS.map((p: CommandPreset): string => p.name).join(", ")}`);
169
+ }
170
+
171
+ return preset;
172
+ }
173
+
174
+ function asJsonTextResult(value: unknown): { content: Array<{ type: "text"; text: string }> } {
175
+ return {
176
+ content: [{
177
+ type: "text",
178
+ text: JSON.stringify(value, null, 2)
179
+ }]
180
+ };
181
+ }
182
+
183
+ function parseJsonObjectsFromOutput(output: string): unknown[] {
184
+ const lines: string[] = output
185
+ .split(/\r?\n/)
186
+ .map((line: string): string => line.trim())
187
+ .filter((line: string): boolean => line.startsWith("{") && line.endsWith("}"));
188
+ const values: unknown[] = [];
189
+
190
+ for (const line of lines) {
191
+ try {
192
+ values.push(JSON.parse(line));
193
+ } catch {
194
+ continue;
195
+ }
196
+ }
197
+
198
+ return values;
199
+ }
200
+
201
+ function selectGodotOperationResult(values: unknown[]): unknown {
202
+ const objectValues: Array<Record<string, unknown>> = values.filter(
203
+ (value: unknown): value is Record<string, unknown> =>
204
+ typeof value === "object" && value !== null && !Array.isArray(value)
205
+ );
206
+
207
+ const changedValue: Record<string, unknown> | undefined = objectValues.find(
208
+ (value: Record<string, unknown>): boolean =>
209
+ value.ok === true && (value.created === true || value.modified === true)
210
+ );
211
+ if (changedValue !== undefined) {
212
+ return changedValue;
213
+ }
214
+
215
+ const okValue: Record<string, unknown> | undefined = objectValues.find(
216
+ (value: Record<string, unknown>): boolean => value.ok === true
217
+ );
218
+ if (okValue !== undefined) {
219
+ return okValue;
220
+ }
221
+
222
+ return objectValues.at(-1) ?? null;
223
+ }
224
+
225
+ function isPathInsideRoot(candidatePath: string, rootPath: string): boolean {
226
+ const relativePath: string = path.relative(rootPath, candidatePath);
227
+ return relativePath.length === 0 || (!relativePath.startsWith("..") && !path.isAbsolute(relativePath));
228
+ }
229
+
230
+ function resolveWorkingDirectory(workingDirectory: string | undefined, preset: CommandPreset): string {
231
+ const requestedPath: string = workingDirectory ?? preset.workingDirectory;
232
+ const resolvedPath: string = path.resolve(requestedPath);
233
+
234
+ for (const allowedRoot of ALLOWED_WORKING_ROOTS) {
235
+ if (isPathInsideRoot(resolvedPath, allowedRoot)) {
236
+ return resolvedPath;
237
+ }
238
+ }
239
+
240
+ throw new Error(`Working directory is outside allowed roots: ${resolvedPath}`);
241
+ }
242
+
243
+ function createMissingGodotProjectResult(presetName: string): { content: Array<{ type: "text"; text: string }> } {
244
+ return asJsonTextResult({
245
+ preset: presetName,
246
+ ok: false,
247
+ error: "GODOT_PROJECT_PATH is not configured for the terminal MCP server. Configure the Godot project path in the client and restart or reconnect the backend workspace before running Godot presets.",
248
+ godotProjectPath: GODOT_PROJECT || null,
249
+ godotExecutablePath: GODOT_EXECUTABLE
250
+ });
251
+ }
252
+
253
+ function toProjectRelativePath(resourcePath: string): string {
254
+ const trimmedPath: string = resourcePath.trim();
255
+ if (trimmedPath.length === 0) {
256
+ throw new Error("resourcePath cannot be empty");
257
+ }
258
+
259
+ if (GODOT_PROJECT.length === 0) {
260
+ throw new Error("Cannot resolve resourcePath without GODOT_PROJECT_PATH");
261
+ }
262
+
263
+ const projectRoot: string = path.resolve(GODOT_PROJECT);
264
+ if (trimmedPath.startsWith("res://")) {
265
+ const relativePath: string = trimmedPath.slice("res://".length).replaceAll("\\", "/");
266
+ const absolutePath: string = path.resolve(projectRoot, relativePath);
267
+ if (!isPathInsideRoot(absolutePath, projectRoot)) {
268
+ throw new Error(`resourcePath is outside the Godot project: ${trimmedPath}`);
269
+ }
270
+
271
+ return path.relative(projectRoot, absolutePath).replaceAll(path.sep, "/");
272
+ }
273
+
274
+ if (path.isAbsolute(trimmedPath)) {
275
+ const absolutePath: string = path.resolve(trimmedPath);
276
+ if (!isPathInsideRoot(absolutePath, projectRoot)) {
277
+ throw new Error(`resourcePath is outside the Godot project: ${absolutePath}`);
278
+ }
279
+
280
+ return path.relative(projectRoot, absolutePath).replaceAll(path.sep, "/");
281
+ }
282
+
283
+ const relativePath: string = trimmedPath.replaceAll("\\", "/");
284
+ const absolutePath: string = path.resolve(projectRoot, relativePath);
285
+ if (!isPathInsideRoot(absolutePath, projectRoot)) {
286
+ throw new Error(`resourcePath is outside the Godot project: ${trimmedPath}`);
287
+ }
288
+
289
+ return path.relative(projectRoot, absolutePath).replaceAll(path.sep, "/");
290
+ }
291
+
292
+ function toResPath(relativePath: string): string {
293
+ return `res://${relativePath.replace(/^\/+/u, "")}`;
294
+ }
295
+
296
+ function createGodotResourceCommand(preset: CommandPreset, resourcePath: string | undefined): string[] {
297
+ if (!preset.requiresGodotProject) {
298
+ return preset.command;
299
+ }
300
+
301
+ if (GODOT_PROJECT.length === 0) {
302
+ return preset.command;
303
+ }
304
+
305
+ const trimmedResourcePath: string = resourcePath?.trim() ?? "";
306
+ if (preset.resourcePathMode === "required" && trimmedResourcePath.length === 0) {
307
+ throw new Error(`Preset '${preset.name}' requires resourcePath`);
308
+ }
309
+
310
+ if (trimmedResourcePath.length === 0) {
311
+ return preset.command;
312
+ }
313
+
314
+ const relativePath: string = toProjectRelativePath(trimmedResourcePath);
315
+ const extension: string = path.extname(relativePath).toLowerCase();
316
+ const resPath: string = toResPath(relativePath);
317
+
318
+ if (extension === ".gd") {
319
+ return [
320
+ GODOT_EXECUTABLE,
321
+ "--headless",
322
+ "--disable-crash-handler",
323
+ "--path", GODOT_PROJECT,
324
+ "--script", resPath,
325
+ "--check-only"
326
+ ];
327
+ }
328
+
329
+ if (extension === ".tscn" || extension === ".scn") {
330
+ return [
331
+ GODOT_EXECUTABLE,
332
+ "--headless",
333
+ "--disable-crash-handler",
334
+ "--path", GODOT_PROJECT,
335
+ resPath,
336
+ "--quit-after", "1"
337
+ ];
338
+ }
339
+
340
+ throw new Error(`Unsupported Godot resourcePath extension for '${preset.name}': ${extension || "(none)"}. Use a .gd or .tscn file.`);
341
+ }
342
+
343
+ function describePresetCommand(command: string[]): string {
344
+ return command.map((part: string): string => {
345
+ if (!/[\s"]/u.test(part)) {
346
+ return part;
347
+ }
348
+
349
+ return `"${part.replaceAll("\"", "\\\"")}"`;
350
+ }).join(" ");
351
+ }
352
+
353
+ async function main(): Promise<void> {
354
+ const server: McpServer = new McpServer({
355
+ name: "terminal-mcp-server",
356
+ version: "1.0.0"
357
+ });
358
+
359
+ server.registerTool(
360
+ "get_terminal_capabilities",
361
+ {
362
+ title: "Get Terminal Capabilities",
363
+ description: "返回当前终端 MCP 支持的所有预设命令列表及其风险等级。",
364
+ inputSchema: z.object({})
365
+ },
366
+ async () => asJsonTextResult({
367
+ presets: COMMAND_PRESETS.map((p: CommandPreset) => ({
368
+ name: p.name,
369
+ description: p.description,
370
+ workingDirectory: p.workingDirectory,
371
+ risk: p.risk,
372
+ resourcePathMode: p.resourcePathMode ?? "none",
373
+ godotProjectPath: p.requiresGodotProject ? GODOT_PROJECT || null : undefined,
374
+ godotExecutablePath: p.requiresGodotProject ? GODOT_EXECUTABLE : undefined,
375
+ command: p.requiresGodotProject ? describePresetCommand(p.command) : undefined
376
+ }))
377
+ })
378
+ );
379
+
380
+ server.registerTool(
381
+ "run_safe_preset",
382
+ {
383
+ title: "Run Safe Command Preset",
384
+ description: "执行安全的预设命令(read/verify 风险),自动允许。包括 git.status、git.diff、backend.typecheck、godot.check_only。Godot 预设可传 resourcePath 精确检查 .gd 或 .tscn。",
385
+ inputSchema: z.object({
386
+ presetName: z.string().min(1).describe("安全预设名称"),
387
+ resourcePath: z.string().optional().describe("Godot 资源路径,可用 res://、项目相对路径或项目内绝对路径。例如 scripts/main.gd、scenes/main.tscn。"),
388
+ workingDirectory: z.string().optional().describe("覆盖默认工作目录")
389
+ })
390
+ },
391
+ async ({ presetName, workingDirectory, resourcePath }: SafePresetInput) => {
392
+ const preset: CommandPreset = findPreset(presetName);
393
+
394
+ if (preset.risk !== "read" && preset.risk !== "verify") {
395
+ return asJsonTextResult({
396
+ preset: presetName,
397
+ ok: false,
398
+ error: `Preset '${presetName}' has risk '${preset.risk}', not allowed via run_safe_preset. Use run_write_preset for write commands.`,
399
+ requiredRisk: preset.risk
400
+ });
401
+ }
402
+
403
+ if (preset.requiresGodotProject && GODOT_PROJECT.length === 0) {
404
+ return createMissingGodotProjectResult(presetName);
405
+ }
406
+
407
+ let cwd: string;
408
+ try {
409
+ cwd = resolveWorkingDirectory(workingDirectory, preset);
410
+ } catch (error: unknown) {
411
+ return asJsonTextResult({
412
+ preset: presetName,
413
+ ok: false,
414
+ error: error instanceof Error ? error.message : "Invalid working directory"
415
+ });
416
+ }
417
+
418
+ let command: string[];
419
+ try {
420
+ command = createGodotResourceCommand(preset, resourcePath);
421
+ } catch (error: unknown) {
422
+ return asJsonTextResult({
423
+ preset: presetName,
424
+ ok: false,
425
+ error: error instanceof Error ? error.message : "Invalid preset arguments",
426
+ resourcePath: resourcePath ?? null,
427
+ godotProjectPath: preset.requiresGodotProject ? GODOT_PROJECT || null : undefined,
428
+ godotExecutablePath: preset.requiresGodotProject ? GODOT_EXECUTABLE : undefined
429
+ });
430
+ }
431
+
432
+ const result = await runCommand(command, cwd);
433
+
434
+ return asJsonTextResult({
435
+ preset: presetName,
436
+ ok: result.exitCode === 0,
437
+ exitCode: result.exitCode,
438
+ command,
439
+ commandLine: describePresetCommand(command),
440
+ cwd,
441
+ resourcePath: resourcePath ?? null,
442
+ godotProjectPath: preset.requiresGodotProject ? GODOT_PROJECT || null : undefined,
443
+ godotExecutablePath: preset.requiresGodotProject ? GODOT_EXECUTABLE : undefined,
444
+ stdout: result.stdout,
445
+ stderr: result.stderr,
446
+ durationMs: result.durationMs,
447
+ truncated: result.truncated
448
+ });
449
+ }
450
+ );
451
+
452
+ server.registerTool(
453
+ "run_write_preset",
454
+ {
455
+ title: "Run Write Command Preset",
456
+ description: "执行写操作预设命令(write 风险),需要通过审批系统批准。可用的写预设:git.init。破坏性预设会被直接拒绝。",
457
+ inputSchema: z.object({
458
+ presetName: z.string().min(1).describe("写操作预设名称"),
459
+ workingDirectory: z.string().optional().describe("覆盖默认工作目录")
460
+ })
461
+ },
462
+ async ({ presetName, workingDirectory }) => {
463
+ const preset: CommandPreset = findPreset(presetName);
464
+
465
+ if (preset.risk === "destructive") {
466
+ return asJsonTextResult({
467
+ preset: presetName,
468
+ ok: false,
469
+ error: `Preset '${presetName}' has destructive risk and is permanently forbidden.`
470
+ });
471
+ }
472
+
473
+ if (preset.risk !== "write") {
474
+ return asJsonTextResult({
475
+ preset: presetName,
476
+ ok: false,
477
+ error: `Preset '${presetName}' has risk '${preset.risk}', use run_safe_preset instead.`
478
+ });
479
+ }
480
+
481
+ let cwd: string;
482
+ try {
483
+ cwd = resolveWorkingDirectory(workingDirectory, preset);
484
+ } catch (error: unknown) {
485
+ return asJsonTextResult({
486
+ preset: presetName,
487
+ ok: false,
488
+ error: error instanceof Error ? error.message : "Invalid working directory"
489
+ });
490
+ }
491
+
492
+ const result = await runCommand(preset.command, cwd);
493
+
494
+ return asJsonTextResult({
495
+ preset: presetName,
496
+ ok: result.exitCode === 0,
497
+ exitCode: result.exitCode,
498
+ stdout: result.stdout,
499
+ stderr: result.stderr,
500
+ durationMs: result.durationMs,
501
+ truncated: result.truncated
502
+ });
503
+ }
504
+ );
505
+
506
+ server.registerTool(
507
+ "run_godot_scene_script",
508
+ {
509
+ title: "Run Godot Scene Script",
510
+ description: "通过 Godot headless 模式调用 scene_operator.gd 执行场景操作(创建场景、添加节点、挂载脚本、连接信号、查看场景树)。操作通过审批后实际写入磁盘。接受的 operation JSON 格式示例:{\"operation\":\"create_scene\",\"path\":\"scenes/foo.tscn\",\"root_type\":\"Node2D\",\"root_name\":\"Main\"}。支持的操作:create_scene、add_node、attach_script、connect_signal、inspect。",
511
+ inputSchema: z.object({
512
+ operationJson: z.string().min(1).describe("JSON 格式的场景操作,包含 operation 字段和对应参数")
513
+ })
514
+ },
515
+ async ({ operationJson }) => {
516
+ let parsed: unknown;
517
+ try {
518
+ parsed = JSON.parse(operationJson);
519
+ } catch {
520
+ return asJsonTextResult({ ok: false, error: "Invalid JSON: operationJson must be valid JSON" });
521
+ }
522
+
523
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
524
+ return asJsonTextResult({ ok: false, error: "operationJson must be a JSON object" });
525
+ }
526
+
527
+ const op = parsed as Record<string, unknown>;
528
+ if (typeof op.operation !== "string" || op.operation.length === 0) {
529
+ return asJsonTextResult({ ok: false, error: "Missing required field: operation" });
530
+ }
531
+
532
+ const scriptPath: string = "res://addons/godot_daedalus/tools/scene_operator.gd";
533
+ const command: string[] = [
534
+ GODOT_EXECUTABLE,
535
+ "--headless",
536
+ "--disable-crash-handler",
537
+ "--path", GODOT_PROJECT,
538
+ "--script", scriptPath,
539
+ "--", operationJson
540
+ ];
541
+
542
+ const cwd: string = GODOT_PROJECT.length > 0 ? path.resolve(GODOT_PROJECT) : BACKEND_DIR;
543
+
544
+ if (!isPathInsideRoot(cwd, ALLOWED_WORKING_ROOTS[0] ?? BACKEND_DIR) && !(ALLOWED_WORKING_ROOTS.length > 1 && isPathInsideRoot(cwd, ALLOWED_WORKING_ROOTS[1]!))) {
545
+ return asJsonTextResult({ ok: false, error: "Godot project path is outside allowed roots" });
546
+ }
547
+
548
+ const result = await runCommand(command, cwd);
549
+
550
+ // Godot may print banners or warnings before/after the script result.
551
+ const parsedEvents: unknown[] = parseJsonObjectsFromOutput(result.stdout);
552
+ const parsedOutput: unknown = selectGodotOperationResult(parsedEvents);
553
+
554
+ return asJsonTextResult({
555
+ ok: result.exitCode === 0 && parsedOutput !== null && typeof parsedOutput === "object" && (parsedOutput as Record<string, unknown>).ok === true,
556
+ exitCode: result.exitCode,
557
+ stdout: result.stdout,
558
+ stderr: result.stderr,
559
+ durationMs: result.durationMs,
560
+ truncated: result.truncated,
561
+ parsed: parsedOutput,
562
+ parsedEvents
563
+ });
564
+ }
565
+ );
566
+
567
+ const transport: StdioServerTransport = new StdioServerTransport();
568
+ await server.connect(transport);
569
+
570
+ console.error(`Terminal MCP Server started`);
571
+ }
572
+
573
+ main().catch((error: unknown): void => {
574
+ console.error("Terminal MCP server fatal error:", error);
575
+ process.exit(1);
576
+ });