gopeak 2.0.1 → 2.2.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.
package/build/index.js CHANGED
@@ -8,21 +8,22 @@
8
8
  */
9
9
  import { fileURLToPath } from 'url';
10
10
  import { join, dirname, basename, normalize } from 'path';
11
- import { existsSync, readdirSync, mkdirSync } from 'fs';
11
+ import { existsSync, readdirSync, mkdirSync, readFileSync, appendFileSync, writeFileSync } from 'fs';
12
12
  import { spawn } from 'child_process';
13
13
  import { promisify } from 'util';
14
14
  import { exec } from 'child_process';
15
- import { createServer } from 'http';
16
15
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
17
16
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
18
17
  import { CallToolRequestSchema, ErrorCode, ListToolsRequestSchema, McpError, } from '@modelcontextprotocol/sdk/types.js';
19
18
  import { setupResourceHandlers } from './resources.js';
20
19
  import { GodotLSPClient, createLSPTools, handleLSPTool } from './lsp_client.js';
21
20
  import { GodotDAPClient, createDAPTools, handleDAPTool } from './dap_client.js';
21
+ import { mapProject } from './gdscript_parser.js';
22
+ import { serveVisualization, setProjectPath } from './visualizer-server.js';
23
+ import { getDefaultBridge } from './godot-bridge.js';
22
24
  // Check if debug mode is enabled
23
25
  const DEBUG_MODE = process.env.DEBUG === 'true';
24
26
  const GODOT_DEBUG_MODE = true; // Always use GODOT DEBUG MODE
25
- const HEALTH_PORT = parseInt(process.env.MCP_HEALTH_PORT || '8080', 10);
26
27
  const execAsync = promisify(exec);
27
28
  // Derive __filename and __dirname in ESM
28
29
  const __filename = fileURLToPath(import.meta.url);
@@ -40,6 +41,37 @@ class GodotServer {
40
41
  lspClient = null;
41
42
  dapClient = null;
42
43
  lastProjectPath = null;
44
+ recordingMode = (process.env.LOG_MODE === 'full' ? 'full' : 'lite');
45
+ logQueue = [];
46
+ logFlushTimer = null;
47
+ logFlushIntervalMs = 1500;
48
+ godotBridge;
49
+ cachedToolDefinitions = [];
50
+ toolExposureProfile;
51
+ toolsListPageSize;
52
+ compactAliasToLegacy = {
53
+ 'tool.catalog': 'tool_catalog',
54
+ 'project.list': 'list_projects',
55
+ 'project.info': 'get_project_info',
56
+ 'project.search': 'search_project',
57
+ 'editor.launch': 'launch_editor',
58
+ 'editor.run': 'run_project',
59
+ 'editor.stop': 'stop_project',
60
+ 'editor.debug_output': 'get_debug_output',
61
+ 'editor.status': 'get_editor_status',
62
+ 'scene.create': 'create_scene',
63
+ 'scene.node.add': 'add_node',
64
+ 'scene.save': 'save_scene',
65
+ 'script.create': 'create_script',
66
+ 'script.modify': 'modify_script',
67
+ 'script.info': 'get_script_info',
68
+ 'class.query': 'query_classes',
69
+ 'class.info': 'query_class_info',
70
+ 'runtime.status': 'get_runtime_status',
71
+ 'visualizer.map': 'map_project',
72
+ 'lsp.diagnostics': 'lsp_get_diagnostics',
73
+ 'dap.output': 'dap_get_output',
74
+ };
43
75
  /**
44
76
  * Parameter name mappings between snake_case and camelCase
45
77
  * This allows the server to accept both formats
@@ -87,6 +119,17 @@ class GodotServer {
87
119
  */
88
120
  reverseParameterMappings = {};
89
121
  constructor(config) {
122
+ const rawProfile = (process.env.GOPEAK_TOOL_PROFILE || process.env.MCP_TOOL_PROFILE || 'compact').toLowerCase();
123
+ if (rawProfile === 'full' || rawProfile === 'legacy' || rawProfile === 'compact') {
124
+ this.toolExposureProfile = rawProfile;
125
+ }
126
+ else {
127
+ this.toolExposureProfile = 'compact';
128
+ }
129
+ const rawToolsPageSize = parseInt(process.env.GOPEAK_TOOLS_PAGE_SIZE || '20', 10);
130
+ this.toolsListPageSize = Number.isFinite(rawToolsPageSize) && rawToolsPageSize > 0
131
+ ? rawToolsPageSize
132
+ : 20;
90
133
  // Initialize reverse parameter mappings
91
134
  for (const [snakeCase, camelCase] of Object.entries(this.parameterMappings)) {
92
135
  this.reverseParameterMappings[camelCase] = snakeCase;
@@ -118,6 +161,8 @@ class GodotServer {
118
161
  }
119
162
  // Set the path to the operations script
120
163
  this.operationsScriptPath = join(__dirname, 'scripts', 'godot_operations.gd');
164
+ // Initialize the Godot Editor Bridge (WebSocket server for editor plugin)
165
+ this.godotBridge = getDefaultBridge();
121
166
  if (debugMode)
122
167
  console.error(`[DEBUG] Operations script path: ${this.operationsScriptPath}`);
123
168
  // Initialize the MCP server
@@ -344,6 +389,11 @@ class GodotServer {
344
389
  */
345
390
  async cleanup() {
346
391
  this.logDebug('Cleaning up resources');
392
+ if (this.logFlushTimer) {
393
+ clearTimeout(this.logFlushTimer);
394
+ this.logFlushTimer = null;
395
+ }
396
+ this.flushLogQueue();
347
397
  if (this.activeProcess) {
348
398
  this.logDebug('Killing active Godot process');
349
399
  this.activeProcess.process.kill();
@@ -363,6 +413,12 @@ class GodotServer {
363
413
  catch { }
364
414
  this.dapClient = null;
365
415
  }
416
+ if (this.godotBridge) {
417
+ try {
418
+ await this.godotBridge.stop();
419
+ }
420
+ catch { }
421
+ }
366
422
  await this.server.close();
367
423
  }
368
424
  async handleRuntimeCommand(command, args) {
@@ -431,6 +487,86 @@ class GodotServer {
431
487
  }
432
488
  return handleDAPTool(this.dapClient, toolName, args);
433
489
  }
490
+ resolveToolAlias(requestedToolName) {
491
+ return this.compactAliasToLegacy[requestedToolName] || requestedToolName;
492
+ }
493
+ buildCompactTools(allTools) {
494
+ const compactTools = [];
495
+ for (const [compactName, legacyName] of Object.entries(this.compactAliasToLegacy)) {
496
+ const source = allTools.find((tool) => tool.name === legacyName);
497
+ if (!source) {
498
+ continue;
499
+ }
500
+ compactTools.push({
501
+ ...source,
502
+ name: compactName,
503
+ description: `[compact alias of ${legacyName}] ${source.description}`,
504
+ });
505
+ }
506
+ return compactTools;
507
+ }
508
+ getExposedTools(allTools) {
509
+ if (this.toolExposureProfile === 'full' || this.toolExposureProfile === 'legacy') {
510
+ return allTools;
511
+ }
512
+ return this.buildCompactTools(allTools);
513
+ }
514
+ parseToolsListCursor(cursor, total) {
515
+ if (typeof cursor !== 'string' || cursor.length === 0) {
516
+ return 0;
517
+ }
518
+ const offset = Number.parseInt(cursor, 10);
519
+ if (!Number.isInteger(offset) || offset < 0 || offset > total) {
520
+ throw new McpError(ErrorCode.InvalidParams, `Invalid tools/list cursor: ${cursor}`);
521
+ }
522
+ return offset;
523
+ }
524
+ paginateToolsForList(tools, cursor) {
525
+ const start = this.parseToolsListCursor(cursor, tools.length);
526
+ const end = Math.min(start + this.toolsListPageSize, tools.length);
527
+ const page = tools.slice(start, end);
528
+ if (end < tools.length) {
529
+ return {
530
+ tools: page,
531
+ nextCursor: String(end),
532
+ };
533
+ }
534
+ return { tools: page };
535
+ }
536
+ async handleToolCatalog(args) {
537
+ const normalizedArgs = this.normalizeParameters(args || {});
538
+ const query = typeof normalizedArgs.query === 'string' ? normalizedArgs.query.trim().toLowerCase() : '';
539
+ const rawLimit = typeof normalizedArgs.limit === 'number' ? normalizedArgs.limit : 30;
540
+ const limit = Math.max(1, Math.min(100, rawLimit));
541
+ const tools = this.cachedToolDefinitions;
542
+ const reverseAlias = new Map();
543
+ for (const [compactName, legacyName] of Object.entries(this.compactAliasToLegacy)) {
544
+ reverseAlias.set(legacyName, compactName);
545
+ }
546
+ const filtered = tools.filter((tool) => {
547
+ if (!query)
548
+ return true;
549
+ const haystack = `${tool.name} ${tool.description}`.toLowerCase();
550
+ return haystack.includes(query);
551
+ });
552
+ const items = filtered.slice(0, limit).map((tool) => ({
553
+ tool: tool.name,
554
+ compactAlias: reverseAlias.get(tool.name) || null,
555
+ description: tool.description,
556
+ }));
557
+ return {
558
+ content: [{
559
+ type: 'text',
560
+ text: JSON.stringify({
561
+ profile: this.toolExposureProfile,
562
+ totalTools: tools.length,
563
+ query: query || null,
564
+ returned: items.length,
565
+ tools: items,
566
+ }, null, 2),
567
+ }],
568
+ };
569
+ }
434
570
  /**
435
571
  * Check if the Godot version is 4.4 or later
436
572
  * @param version The Godot version string
@@ -675,8 +811,8 @@ class GodotServer {
675
811
  */
676
812
  setupToolHandlers() {
677
813
  // Define available tools
678
- this.server.setRequestHandler(ListToolsRequestSchema, async () => ({
679
- tools: [
814
+ this.server.setRequestHandler(ListToolsRequestSchema, async (request) => {
815
+ const allTools = [
680
816
  {
681
817
  name: 'launch_editor',
682
818
  description: 'Opens the Godot editor GUI for a project. Use when visual inspection or manual editing of scenes/scripts is needed. Opens a new window on the host system. Requires: project directory with project.godot file.',
@@ -774,6 +910,210 @@ class GodotServer {
774
910
  required: ['projectPath'],
775
911
  },
776
912
  },
913
+ {
914
+ name: 'scaffold_gameplay_prototype',
915
+ description: 'Creates a minimal playable prototype scaffold in one shot: main scene, player scene, basic nodes, common input actions, and optional starter player script.',
916
+ inputSchema: {
917
+ type: 'object',
918
+ properties: {
919
+ projectPath: {
920
+ type: 'string',
921
+ description: 'Absolute path to project directory containing project.godot.',
922
+ },
923
+ scenePath: {
924
+ type: 'string',
925
+ description: 'Main scene path relative to project. Default: scenes/Main.tscn',
926
+ },
927
+ playerScenePath: {
928
+ type: 'string',
929
+ description: 'Player scene path relative to project. Default: scenes/Player.tscn',
930
+ },
931
+ includePlayerScript: {
932
+ type: 'boolean',
933
+ description: 'If true, creates scripts/player.gd starter script. Default: true',
934
+ },
935
+ },
936
+ required: ['projectPath'],
937
+ },
938
+ },
939
+ {
940
+ name: 'validate_patch_with_lsp',
941
+ description: 'Runs Godot LSP diagnostics for a script and returns whether it is safe to apply changes. Intended as a pre-apply quality gate.',
942
+ inputSchema: {
943
+ type: 'object',
944
+ properties: {
945
+ projectPath: {
946
+ type: 'string',
947
+ description: 'Absolute path to project directory containing project.godot.',
948
+ },
949
+ scriptPath: {
950
+ type: 'string',
951
+ description: 'Script path relative to project (e.g., scripts/player.gd).',
952
+ },
953
+ },
954
+ required: ['projectPath', 'scriptPath'],
955
+ },
956
+ },
957
+ {
958
+ name: 'enforce_version_gate',
959
+ description: 'Checks Godot version and runtime addon protocol/capabilities against minimum requirements before risky operations.',
960
+ inputSchema: {
961
+ type: 'object',
962
+ properties: {
963
+ projectPath: {
964
+ type: 'string',
965
+ description: 'Absolute path to project directory containing project.godot.',
966
+ },
967
+ minGodotVersion: {
968
+ type: 'string',
969
+ description: 'Minimum required Godot version (major.minor). Default: 4.2',
970
+ },
971
+ minProtocolVersion: {
972
+ type: 'string',
973
+ description: 'Minimum required runtime protocol version. Default: 1.0',
974
+ },
975
+ },
976
+ required: ['projectPath'],
977
+ },
978
+ },
979
+ {
980
+ name: 'capture_intent_snapshot',
981
+ description: 'Capture/update an intent snapshot for current work (goal, constraints, acceptance criteria) and persist it for handoff.',
982
+ inputSchema: {
983
+ type: 'object',
984
+ properties: {
985
+ projectPath: { type: 'string', description: 'Absolute path to project directory containing project.godot.' },
986
+ goal: { type: 'string', description: 'Primary goal of the current work.' },
987
+ why: { type: 'string', description: 'Why this work matters.' },
988
+ constraints: { type: 'array', items: { type: 'string' }, description: 'Operational/technical constraints.' },
989
+ acceptanceCriteria: { type: 'array', items: { type: 'string' }, description: 'Definition of done.' },
990
+ nonGoals: { type: 'array', items: { type: 'string' }, description: 'Out of scope items.' },
991
+ priority: { type: 'string', description: 'Priority label (e.g., P0, P1).' }
992
+ },
993
+ required: ['projectPath', 'goal'],
994
+ },
995
+ },
996
+ {
997
+ name: 'record_decision_log',
998
+ description: 'Record a structured decision log entry with rationale and alternatives.',
999
+ inputSchema: {
1000
+ type: 'object',
1001
+ properties: {
1002
+ projectPath: { type: 'string', description: 'Absolute path to project directory containing project.godot.' },
1003
+ intentId: { type: 'string', description: 'Related intent id. Optional if latest intent should be inferred.' },
1004
+ decision: { type: 'string', description: 'Decision statement.' },
1005
+ rationale: { type: 'string', description: 'Why this decision was made.' },
1006
+ alternativesRejected: { type: 'array', items: { type: 'string' }, description: 'Alternatives considered and rejected.' },
1007
+ evidenceRefs: { type: 'array', items: { type: 'string' }, description: 'References supporting the decision.' }
1008
+ },
1009
+ required: ['projectPath', 'decision'],
1010
+ },
1011
+ },
1012
+ {
1013
+ name: 'generate_handoff_brief',
1014
+ description: 'Generate a handoff brief from saved intents, decisions, and execution traces for the next AI/operator.',
1015
+ inputSchema: {
1016
+ type: 'object',
1017
+ properties: {
1018
+ projectPath: { type: 'string', description: 'Absolute path to project directory containing project.godot.' },
1019
+ maxItems: { type: 'number', description: 'Max items per section. Default: 5' }
1020
+ },
1021
+ required: ['projectPath'],
1022
+ },
1023
+ },
1024
+ {
1025
+ name: 'summarize_intent_context',
1026
+ description: 'Summarize current intent context (goal, open decisions, risks, next actions) in compact form.',
1027
+ inputSchema: {
1028
+ type: 'object',
1029
+ properties: {
1030
+ projectPath: { type: 'string', description: 'Absolute path to project directory containing project.godot.' }
1031
+ },
1032
+ required: ['projectPath'],
1033
+ },
1034
+ },
1035
+ {
1036
+ name: 'record_work_step',
1037
+ description: 'Unified operation: records execution trace and optionally refreshes handoff pack in one call.',
1038
+ inputSchema: {
1039
+ type: 'object',
1040
+ properties: {
1041
+ projectPath: { type: 'string', description: 'Absolute path to project directory containing project.godot.' },
1042
+ intentId: { type: 'string', description: 'Related intent id. Optional: auto-link active intent.' },
1043
+ action: { type: 'string', description: 'Executed action name.' },
1044
+ command: { type: 'string', description: 'Command or tool invocation.' },
1045
+ filesChanged: { type: 'array', items: { type: 'string' }, description: 'Changed file paths.' },
1046
+ result: { type: 'string', description: 'success|failed|partial' },
1047
+ artifact: { type: 'string', description: 'Artifact reference (branch, commit, build id).' },
1048
+ error: { type: 'string', description: 'Error details when failed.' },
1049
+ refreshHandoffPack: { type: 'boolean', description: 'If true, regenerates handoff pack after recording. Default: true' },
1050
+ maxItems: { type: 'number', description: 'Max items for refreshed handoff pack. Default: 10' }
1051
+ },
1052
+ required: ['projectPath', 'action', 'result'],
1053
+ },
1054
+ },
1055
+ {
1056
+ name: 'record_execution_trace',
1057
+ description: 'Record execution trace for a work step (command/tool, files changed, result, artifacts).',
1058
+ inputSchema: {
1059
+ type: 'object',
1060
+ properties: {
1061
+ projectPath: { type: 'string', description: 'Absolute path to project directory containing project.godot.' },
1062
+ intentId: { type: 'string', description: 'Related intent id. Optional: auto-link active intent.' },
1063
+ action: { type: 'string', description: 'Executed action name.' },
1064
+ command: { type: 'string', description: 'Command or tool invocation.' },
1065
+ filesChanged: { type: 'array', items: { type: 'string' }, description: 'Changed file paths.' },
1066
+ result: { type: 'string', description: 'success|failed|partial' },
1067
+ artifact: { type: 'string', description: 'Artifact reference (branch, commit, build id).' },
1068
+ error: { type: 'string', description: 'Error details when failed.' }
1069
+ },
1070
+ required: ['projectPath', 'action', 'result'],
1071
+ },
1072
+ },
1073
+ {
1074
+ name: 'export_handoff_pack',
1075
+ description: 'Export a machine-readable handoff pack combining intent, decisions, and execution traces.',
1076
+ inputSchema: {
1077
+ type: 'object',
1078
+ properties: {
1079
+ projectPath: { type: 'string', description: 'Absolute path to project directory containing project.godot.' },
1080
+ maxItems: { type: 'number', description: 'Maximum decisions/traces to include. Default: 10' }
1081
+ },
1082
+ required: ['projectPath'],
1083
+ },
1084
+ },
1085
+ {
1086
+ name: 'set_recording_mode',
1087
+ description: 'Set recording mode: lite (minimal overhead) or full (richer context).',
1088
+ inputSchema: {
1089
+ type: 'object',
1090
+ properties: {
1091
+ mode: { type: 'string', description: 'lite|full' }
1092
+ },
1093
+ required: ['mode'],
1094
+ },
1095
+ },
1096
+ {
1097
+ name: 'get_recording_mode',
1098
+ description: 'Get current recording mode and queue status.',
1099
+ inputSchema: {
1100
+ type: 'object',
1101
+ properties: {},
1102
+ required: [],
1103
+ },
1104
+ },
1105
+ {
1106
+ name: 'tool_catalog',
1107
+ description: 'Discover available tools including hidden legacy tools. Use query to search by capability keywords (e.g., animation, import, tilemap, audio).',
1108
+ inputSchema: {
1109
+ type: 'object',
1110
+ properties: {
1111
+ query: { type: 'string', description: 'Optional keyword search over tool names and descriptions.' },
1112
+ limit: { type: 'number', description: 'Maximum results to return. Default: 30, max: 100.' },
1113
+ },
1114
+ required: [],
1115
+ },
1116
+ },
777
1117
  {
778
1118
  name: 'create_scene',
779
1119
  description: 'Creates a new Godot scene file (.tscn) with a specified root node type. Use to start building new game levels, UI screens, or reusable components. The scene is saved automatically after creation.',
@@ -1789,6 +2129,10 @@ class GodotServer {
1789
2129
  type: 'string',
1790
2130
  description: 'Optional: template name - "singleton", "state_machine", "component", "resource"',
1791
2131
  },
2132
+ reason: {
2133
+ type: 'string',
2134
+ description: 'Optional reason/context for this change. Displayed in visualizer audit timeline.',
2135
+ },
1792
2136
  },
1793
2137
  required: ['projectPath', 'scriptPath'],
1794
2138
  },
@@ -1861,6 +2205,10 @@ class GodotServer {
1861
2205
  required: ['type', 'name'],
1862
2206
  },
1863
2207
  },
2208
+ reason: {
2209
+ type: 'string',
2210
+ description: 'Optional reason/context for this change. Displayed in visualizer audit timeline.',
2211
+ },
1864
2212
  },
1865
2213
  required: ['projectPath', 'scriptPath', 'modifications'],
1866
2214
  },
@@ -2684,12 +3032,38 @@ class GodotServer {
2684
3032
  required: ['x', 'y'],
2685
3033
  },
2686
3034
  },
3035
+ // Editor Plugin Bridge Status
3036
+ {
3037
+ name: 'get_editor_status',
3038
+ description: 'Returns the connection status of the Godot Editor Plugin bridge. Use to check if the editor is connected before using scene/resource tools that require the editor plugin.',
3039
+ inputSchema: {
3040
+ type: 'object',
3041
+ properties: {},
3042
+ },
3043
+ },
3044
+ // Project Visualizer Tool
3045
+ {
3046
+ name: 'map_project',
3047
+ description: 'Crawl the entire Godot project and build an interactive visual map of all scripts showing their structure (variables, functions, signals), connections (extends, preloads, signal connections), and descriptions. Opens an interactive browser-based visualization at localhost:6505.',
3048
+ inputSchema: {
3049
+ type: 'object',
3050
+ properties: {
3051
+ projectPath: { type: 'string', description: 'Absolute path to the Godot project directory' },
3052
+ root: { type: 'string', description: 'Root path to start crawling from (default: res://)' },
3053
+ include_addons: { type: 'boolean', description: 'Whether to include scripts in addons/ folder (default: false)' },
3054
+ },
3055
+ required: ['projectPath'],
3056
+ },
3057
+ },
2687
3058
  // Godot LSP Tools (GDScript diagnostics via Godot editor LSP on port 6005)
2688
3059
  ...createLSPTools(),
2689
3060
  // Godot DAP Tools (Debug Adapter Protocol via Godot editor DAP on port 6006)
2690
3061
  ...createDAPTools(),
2691
- ],
2692
- }));
3062
+ ];
3063
+ this.cachedToolDefinitions = allTools;
3064
+ const exposedTools = this.getExposedTools(allTools);
3065
+ return this.paginateToolsForList(exposedTools, request.params?.cursor);
3066
+ });
2693
3067
  // Handle tool calls
2694
3068
  this.server.setRequestHandler(CallToolRequestSchema, async (request) => {
2695
3069
  this.logDebug(`Handling tool request: ${request.params.name}`);
@@ -2697,7 +3071,8 @@ class GodotServer {
2697
3071
  if (rawArgs?.projectPath && typeof rawArgs.projectPath === 'string') {
2698
3072
  this.lastProjectPath = rawArgs.projectPath;
2699
3073
  }
2700
- switch (request.params.name) {
3074
+ const resolvedToolName = this.resolveToolAlias(request.params.name);
3075
+ switch (resolvedToolName) {
2701
3076
  case 'launch_editor':
2702
3077
  return await this.handleLaunchEditor(request.params.arguments);
2703
3078
  case 'run_project':
@@ -2712,31 +3087,57 @@ class GodotServer {
2712
3087
  return await this.handleListProjects(request.params.arguments);
2713
3088
  case 'get_project_info':
2714
3089
  return await this.handleGetProjectInfo(request.params.arguments);
3090
+ case 'scaffold_gameplay_prototype':
3091
+ return await this.handleScaffoldGameplayPrototype(request.params.arguments);
3092
+ case 'validate_patch_with_lsp':
3093
+ return await this.handleValidatePatchWithLsp(request.params.arguments);
3094
+ case 'enforce_version_gate':
3095
+ return await this.handleEnforceVersionGate(request.params.arguments);
3096
+ case 'capture_intent_snapshot':
3097
+ return await this.handleCaptureIntentSnapshot(request.params.arguments);
3098
+ case 'record_decision_log':
3099
+ return await this.handleRecordDecisionLog(request.params.arguments);
3100
+ case 'generate_handoff_brief':
3101
+ return await this.handleGenerateHandoffBrief(request.params.arguments);
3102
+ case 'summarize_intent_context':
3103
+ return await this.handleSummarizeIntentContext(request.params.arguments);
3104
+ case 'record_work_step':
3105
+ return await this.handleRecordWorkStep(request.params.arguments);
3106
+ case 'record_execution_trace':
3107
+ return await this.handleRecordExecutionTrace(request.params.arguments);
3108
+ case 'export_handoff_pack':
3109
+ return await this.handleExportHandoffPack(request.params.arguments);
3110
+ case 'set_recording_mode':
3111
+ return await this.handleSetRecordingMode(request.params.arguments);
3112
+ case 'get_recording_mode':
3113
+ return await this.handleGetRecordingMode();
3114
+ case 'tool_catalog':
3115
+ return await this.handleToolCatalog(request.params.arguments);
2715
3116
  case 'create_scene':
2716
- return await this.handleCreateScene(request.params.arguments);
3117
+ return await this.handleViaBridge('create_scene', request.params.arguments);
2717
3118
  case 'add_node':
2718
- return await this.handleAddNode(request.params.arguments);
3119
+ return await this.handleViaBridge('add_node', request.params.arguments);
2719
3120
  case 'load_sprite':
2720
- return await this.handleLoadSprite(request.params.arguments);
3121
+ return await this.handleViaBridge('load_sprite', request.params.arguments);
2721
3122
  case 'save_scene':
2722
- return await this.handleSaveScene(request.params.arguments);
3123
+ return await this.handleViaBridge('save_scene', request.params.arguments);
2723
3124
  case 'get_uid':
2724
3125
  return await this.handleGetUid(request.params.arguments);
2725
3126
  case 'update_project_uids':
2726
3127
  return await this.handleUpdateProjectUids(request.params.arguments);
2727
3128
  // Phase 1: Scene Operations handlers
2728
3129
  case 'list_scene_nodes':
2729
- return await this.handleListSceneNodes(request.params.arguments);
3130
+ return await this.handleViaBridge('list_scene_nodes', request.params.arguments);
2730
3131
  case 'get_node_properties':
2731
- return await this.handleGetNodeProperties(request.params.arguments);
3132
+ return await this.handleViaBridge('get_node_properties', request.params.arguments);
2732
3133
  case 'set_node_properties':
2733
- return await this.handleSetNodeProperties(request.params.arguments);
3134
+ return await this.handleViaBridge('set_node_properties', request.params.arguments);
2734
3135
  case 'delete_node':
2735
- return await this.handleDeleteNode(request.params.arguments);
3136
+ return await this.handleViaBridge('delete_node', request.params.arguments);
2736
3137
  case 'duplicate_node':
2737
- return await this.handleDuplicateNode(request.params.arguments);
3138
+ return await this.handleViaBridge('duplicate_node', request.params.arguments);
2738
3139
  case 'reparent_node':
2739
- return await this.handleReparentNode(request.params.arguments);
3140
+ return await this.handleViaBridge('reparent_node', request.params.arguments);
2740
3141
  // Phase 2: Import/Export Pipeline handlers
2741
3142
  case 'get_import_status':
2742
3143
  return await this.handleGetImportStatus(request.params.arguments);
@@ -2776,11 +3177,11 @@ class GodotServer {
2776
3177
  return await this.handleSetMainScene(request.params.arguments);
2777
3178
  // Signal Management handlers
2778
3179
  case 'connect_signal':
2779
- return await this.handleConnectSignal(request.params.arguments);
3180
+ return await this.handleViaBridge('connect_signal', request.params.arguments);
2780
3181
  case 'disconnect_signal':
2781
- return await this.handleDisconnectSignal(request.params.arguments);
3182
+ return await this.handleViaBridge('disconnect_signal', request.params.arguments);
2782
3183
  case 'list_connections':
2783
- return await this.handleListConnections(request.params.arguments);
3184
+ return await this.handleViaBridge('list_connections', request.params.arguments);
2784
3185
  // Phase 4: Runtime Tools handlers
2785
3186
  case 'get_runtime_status':
2786
3187
  return await this.handleGetRuntimeStatus(request.params.arguments);
@@ -2794,11 +3195,11 @@ class GodotServer {
2794
3195
  return await this.handleGetRuntimeMetrics(request.params.arguments);
2795
3196
  // Resource Creation Tools handlers
2796
3197
  case 'create_resource':
2797
- return await this.handleCreateResource(request.params.arguments);
3198
+ return await this.handleViaBridge('create_resource', request.params.arguments);
2798
3199
  case 'create_material':
2799
- return await this.handleCreateMaterial(request.params.arguments);
3200
+ return await this.handleViaBridge('create_material', request.params.arguments);
2800
3201
  case 'create_shader':
2801
- return await this.handleCreateShader(request.params.arguments);
3202
+ return await this.handleViaBridge('create_shader', request.params.arguments);
2802
3203
  // GDScript File Operations handlers
2803
3204
  case 'create_script':
2804
3205
  return await this.handleCreateScript(request.params.arguments);
@@ -2808,9 +3209,9 @@ class GodotServer {
2808
3209
  return await this.handleGetScriptInfo(request.params.arguments);
2809
3210
  // Animation Tools handlers
2810
3211
  case 'create_animation':
2811
- return await this.handleCreateAnimation(request.params.arguments);
3212
+ return await this.handleViaBridge('create_animation', request.params.arguments);
2812
3213
  case 'add_animation_track':
2813
- return await this.handleAddAnimationTrack(request.params.arguments);
3214
+ return await this.handleViaBridge('add_animation_track', request.params.arguments);
2814
3215
  // Plugin Management handlers
2815
3216
  case 'list_plugins':
2816
3217
  return await this.handleListPlugins(request.params.arguments);
@@ -2826,9 +3227,9 @@ class GodotServer {
2826
3227
  return await this.handleSearchProject(request.params.arguments);
2827
3228
  // 2D Tile Tools handlers
2828
3229
  case 'create_tileset':
2829
- return await this.handleCreateTileset(request.params.arguments);
3230
+ return await this.handleViaBridge('create_tileset', request.params.arguments);
2830
3231
  case 'set_tilemap_cells':
2831
- return await this.handleSetTilemapCells(request.params.arguments);
3232
+ return await this.handleViaBridge('set_tilemap_cells', request.params.arguments);
2832
3233
  // Audio System Tools handlers
2833
3234
  case 'create_audio_bus':
2834
3235
  return await this.handleCreateAudioBus(request.params.arguments);
@@ -2842,24 +3243,24 @@ class GodotServer {
2842
3243
  // Physics Tools handlers
2843
3244
  // Navigation Tools handlers
2844
3245
  case 'create_navigation_region':
2845
- return await this.handleCreateNavigationRegion(request.params.arguments);
3246
+ return await this.handleViaBridge('create_navigation_region', request.params.arguments);
2846
3247
  case 'create_navigation_agent':
2847
- return await this.handleCreateNavigationAgent(request.params.arguments);
3248
+ return await this.handleViaBridge('create_navigation_agent', request.params.arguments);
2848
3249
  // Rendering Tools handlers
2849
3250
  // Animation Tree Tools handlers
2850
3251
  case 'create_animation_tree':
2851
- return await this.handleCreateAnimationTree(request.params.arguments);
3252
+ return await this.handleViaBridge('create_animation_tree', request.params.arguments);
2852
3253
  case 'add_animation_state':
2853
- return await this.handleAddAnimationState(request.params.arguments);
3254
+ return await this.handleViaBridge('add_animation_state', request.params.arguments);
2854
3255
  case 'connect_animation_states':
2855
- return await this.handleConnectAnimationStates(request.params.arguments);
3256
+ return await this.handleViaBridge('connect_animation_states', request.params.arguments);
2856
3257
  // UI/Theme Tools handlers
2857
3258
  case 'set_theme_color':
2858
- return await this.handleSetThemeColor(request.params.arguments);
3259
+ return await this.handleViaBridge('set_theme_color', request.params.arguments);
2859
3260
  case 'set_theme_font_size':
2860
- return await this.handleSetThemeFontSize(request.params.arguments);
3261
+ return await this.handleViaBridge('set_theme_font_size', request.params.arguments);
2861
3262
  case 'apply_theme_shader':
2862
- return await this.handleApplyThemeShader(request.params.arguments);
3263
+ return await this.handleViaBridge('apply_theme_shader', request.params.arguments);
2863
3264
  case 'search_assets':
2864
3265
  return await this.handleSearchAssets(request.params.arguments);
2865
3266
  case 'fetch_asset':
@@ -2875,7 +3276,13 @@ class GodotServer {
2875
3276
  return await this.handleInspectInheritance(request.params.arguments);
2876
3277
  // Resource Modification Tool
2877
3278
  case 'modify_resource':
2878
- return await this.handleModifyResource(request.params.arguments);
3279
+ return await this.handleViaBridge('modify_resource', request.params.arguments);
3280
+ // Editor Plugin Bridge Status
3281
+ case 'get_editor_status':
3282
+ return { content: [{ type: 'text', text: JSON.stringify(this.godotBridge.getStatus(), null, 2) }] };
3283
+ // Project Visualizer Tool
3284
+ case 'map_project':
3285
+ return await this.handleMapProject(request.params.arguments);
2879
3286
  case 'capture_screenshot':
2880
3287
  return await this.handleRuntimeCommand('capture_screenshot', request.params.arguments);
2881
3288
  case 'capture_viewport':
@@ -2892,7 +3299,7 @@ class GodotServer {
2892
3299
  case 'lsp_get_completions':
2893
3300
  case 'lsp_get_hover':
2894
3301
  case 'lsp_get_symbols':
2895
- return await this.handleLSP(request.params.name, request.params.arguments);
3302
+ return await this.handleLSP(resolvedToolName, request.params.arguments);
2896
3303
  case 'dap_get_output':
2897
3304
  case 'dap_set_breakpoint':
2898
3305
  case 'dap_remove_breakpoint':
@@ -2900,7 +3307,7 @@ class GodotServer {
2900
3307
  case 'dap_pause':
2901
3308
  case 'dap_step_over':
2902
3309
  case 'dap_get_stack_trace':
2903
- return await this.handleDAP(request.params.name, request.params.arguments);
3310
+ return await this.handleDAP(resolvedToolName, request.params.arguments);
2904
3311
  default:
2905
3312
  throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${request.params.name}`);
2906
3313
  }
@@ -3046,6 +3453,33 @@ class GodotServer {
3046
3453
  ]);
3047
3454
  }
3048
3455
  }
3456
+ /**
3457
+ * Route a tool call through the Godot Editor Plugin bridge (WebSocket).
3458
+ * Returns an error response if the editor is not connected.
3459
+ */
3460
+ async handleViaBridge(toolName, args) {
3461
+ if (!this.godotBridge.isConnected()) {
3462
+ return {
3463
+ content: [{ type: 'text', text: JSON.stringify({
3464
+ error: 'Godot Editor not connected. Launch Godot Editor and enable the "Godot MCP Editor" plugin to use this tool.',
3465
+ suggestion: 'Use the launch_editor tool to open the Godot Editor, then enable the plugin in Project > Project Settings > Plugins.',
3466
+ }, null, 2) }],
3467
+ isError: true,
3468
+ };
3469
+ }
3470
+ try {
3471
+ const result = await this.godotBridge.invokeTool(toolName, args);
3472
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }] };
3473
+ }
3474
+ catch (error) {
3475
+ return {
3476
+ content: [{ type: 'text', text: JSON.stringify({
3477
+ error: error instanceof Error ? error.message : String(error),
3478
+ }, null, 2) }],
3479
+ isError: true,
3480
+ };
3481
+ }
3482
+ }
3049
3483
  /**
3050
3484
  * Handle the get_debug_output tool
3051
3485
  */
@@ -3299,6 +3733,699 @@ class GodotServer {
3299
3733
  ]);
3300
3734
  }
3301
3735
  }
3736
+ compareMajorMinorVersions(actual, minimum) {
3737
+ const parse = (value) => {
3738
+ const m = value.match(/(\d+)\.(\d+)/);
3739
+ if (!m)
3740
+ return [0, 0];
3741
+ return [parseInt(m[1], 10), parseInt(m[2], 10)];
3742
+ };
3743
+ const [aMaj, aMin] = parse(actual);
3744
+ const [mMaj, mMin] = parse(minimum);
3745
+ if (aMaj > mMaj)
3746
+ return true;
3747
+ if (aMaj < mMaj)
3748
+ return false;
3749
+ return aMin >= mMin;
3750
+ }
3751
+ /**
3752
+ * One-shot gameplay prototype scaffold
3753
+ */
3754
+ async handleScaffoldGameplayPrototype(args) {
3755
+ args = this.normalizeParameters(args);
3756
+ if (!args.projectPath) {
3757
+ return this.createErrorResponse('Project path is required', ['Provide projectPath']);
3758
+ }
3759
+ const projectFile = join(args.projectPath, 'project.godot');
3760
+ if (!existsSync(projectFile)) {
3761
+ return this.createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, [
3762
+ 'Ensure project.godot exists in the provided path',
3763
+ ]);
3764
+ }
3765
+ const scenePath = args.scenePath || 'scenes/Main.tscn';
3766
+ const playerScenePath = args.playerScenePath || 'scenes/Player.tscn';
3767
+ const includePlayerScript = args.includePlayerScript !== false;
3768
+ const steps = [];
3769
+ const runOperation = async (operation, params) => {
3770
+ const { stdout, stderr } = await this.executeOperation(operation, params, args.projectPath);
3771
+ const ok = !(stderr && stderr.includes('ERROR'));
3772
+ return { ok, stdout: stdout?.trim() || '', stderr: stderr?.trim() || '' };
3773
+ };
3774
+ try {
3775
+ // 1) Create main scene
3776
+ const createMain = await runOperation('create_scene', {
3777
+ scenePath,
3778
+ rootNodeType: 'Node2D',
3779
+ });
3780
+ steps.push({ step: 'create_main_scene', ok: createMain.ok, detail: createMain.stderr || createMain.stdout });
3781
+ if (!createMain.ok) {
3782
+ return this.createErrorResponse('Failed to scaffold: could not create main scene', [createMain.stderr || 'Unknown error']);
3783
+ }
3784
+ // 2) Create player scene
3785
+ const createPlayerScene = await runOperation('create_scene', {
3786
+ scenePath: playerScenePath,
3787
+ rootNodeType: 'CharacterBody2D',
3788
+ });
3789
+ steps.push({ step: 'create_player_scene', ok: createPlayerScene.ok, detail: createPlayerScene.stderr || createPlayerScene.stdout });
3790
+ if (!createPlayerScene.ok) {
3791
+ return this.createErrorResponse('Failed to scaffold: could not create player scene', [createPlayerScene.stderr || 'Unknown error']);
3792
+ }
3793
+ // 3) Add common player child nodes
3794
+ const playerNodeAdds = [
3795
+ { nodeType: 'Sprite2D', nodeName: 'Sprite2D' },
3796
+ { nodeType: 'CollisionShape2D', nodeName: 'CollisionShape2D' },
3797
+ { nodeType: 'Camera2D', nodeName: 'Camera2D', properties: { enabled: true } },
3798
+ ];
3799
+ for (const node of playerNodeAdds) {
3800
+ const add = await runOperation('add_node', {
3801
+ scenePath: playerScenePath,
3802
+ nodeType: node.nodeType,
3803
+ nodeName: node.nodeName,
3804
+ properties: node.properties,
3805
+ });
3806
+ steps.push({ step: `add_node_${node.nodeName}`, ok: add.ok, detail: add.stderr || add.stdout });
3807
+ }
3808
+ // 4) Add Player instance placeholder to main scene (as Node2D) and attach player scene path as meta hint
3809
+ const addPlayerRoot = await runOperation('add_node', {
3810
+ scenePath,
3811
+ nodeType: 'Node2D',
3812
+ nodeName: 'Player',
3813
+ });
3814
+ steps.push({ step: 'add_player_root_to_main', ok: addPlayerRoot.ok, detail: addPlayerRoot.stderr || addPlayerRoot.stdout });
3815
+ // 5) Input actions
3816
+ const inputActions = [
3817
+ {
3818
+ actionName: 'move_left',
3819
+ events: [{ type: 'key', keycode: 'A' }, { type: 'key', keycode: 'Left' }],
3820
+ },
3821
+ {
3822
+ actionName: 'move_right',
3823
+ events: [{ type: 'key', keycode: 'D' }, { type: 'key', keycode: 'Right' }],
3824
+ },
3825
+ {
3826
+ actionName: 'jump',
3827
+ events: [{ type: 'key', keycode: 'Space' }],
3828
+ },
3829
+ ];
3830
+ for (const action of inputActions) {
3831
+ const inputResult = await runOperation('add_input_action', action);
3832
+ steps.push({ step: `add_input_${action.actionName}`, ok: inputResult.ok, detail: inputResult.stderr || inputResult.stdout });
3833
+ }
3834
+ // 6) Optional starter player script
3835
+ if (includePlayerScript) {
3836
+ const scriptResult = await runOperation('create_script', {
3837
+ script_path: 'scripts/player.gd',
3838
+ class_name: 'PlayerController',
3839
+ extends_class: 'CharacterBody2D',
3840
+ content: "@export var speed: float = 220.0\n@export var jump_velocity: float = -420.0\n@export var gravity: float = 980.0\n\nfunc _physics_process(delta: float) -> void:\n\tvar dir := Input.get_axis(\"move_left\", \"move_right\")\n\tvelocity.x = dir * speed\n\tif not is_on_floor():\n\t\tvelocity.y += gravity * delta\n\tif is_on_floor() and Input.is_action_just_pressed(\"jump\"):\n\t\tvelocity.y = jump_velocity\n\tmove_and_slide()\n",
3841
+ });
3842
+ steps.push({ step: 'create_player_script', ok: scriptResult.ok, detail: scriptResult.stderr || scriptResult.stdout });
3843
+ // Attach script to player root
3844
+ const attachScript = await runOperation('set_node_properties', {
3845
+ scenePath: playerScenePath,
3846
+ nodePath: '.',
3847
+ properties: { script: 'res://scripts/player.gd' },
3848
+ });
3849
+ steps.push({ step: 'attach_player_script', ok: attachScript.ok, detail: attachScript.stderr || attachScript.stdout });
3850
+ }
3851
+ // 7) Set main scene
3852
+ const setMain = await runOperation('set_main_scene', { scenePath });
3853
+ steps.push({ step: 'set_main_scene', ok: setMain.ok, detail: setMain.stderr || setMain.stdout });
3854
+ const allOk = steps.every((s) => s.ok);
3855
+ return {
3856
+ content: [
3857
+ {
3858
+ type: 'text',
3859
+ text: JSON.stringify({
3860
+ success: allOk,
3861
+ summary: allOk
3862
+ ? 'Gameplay prototype scaffold completed.'
3863
+ : 'Scaffold completed with some failed steps. Check steps[] details.',
3864
+ outputs: {
3865
+ mainScene: scenePath,
3866
+ playerScene: playerScenePath,
3867
+ playerScript: includePlayerScript ? 'scripts/player.gd' : null,
3868
+ },
3869
+ steps,
3870
+ }, null, 2),
3871
+ },
3872
+ ],
3873
+ };
3874
+ }
3875
+ catch (error) {
3876
+ return this.createErrorResponse(`Failed to scaffold gameplay prototype: ${error?.message || 'Unknown error'}`, [
3877
+ 'Ensure Godot is installed and accessible',
3878
+ 'Check project path and write permissions',
3879
+ ]);
3880
+ }
3881
+ }
3882
+ /**
3883
+ * Pre-apply LSP validation gate
3884
+ */
3885
+ async handleValidatePatchWithLsp(args) {
3886
+ args = this.normalizeParameters(args);
3887
+ if (!args.projectPath || !args.scriptPath) {
3888
+ return this.createErrorResponse('Missing required parameters', ['Provide projectPath and scriptPath']);
3889
+ }
3890
+ try {
3891
+ const lspResult = await this.handleLSP('lsp_get_diagnostics', {
3892
+ projectPath: args.projectPath,
3893
+ scriptPath: args.scriptPath,
3894
+ });
3895
+ const textPayload = lspResult?.content?.[0]?.text || '{}';
3896
+ let diagnostics = [];
3897
+ try {
3898
+ const parsed = JSON.parse(textPayload);
3899
+ diagnostics = Array.isArray(parsed?.diagnostics) ? parsed.diagnostics : [];
3900
+ }
3901
+ catch {
3902
+ diagnostics = [];
3903
+ }
3904
+ const hasBlocking = diagnostics.some((d) => {
3905
+ const severity = d?.severity;
3906
+ return severity === 1 || severity === 'error' || severity === 'ERROR';
3907
+ });
3908
+ return {
3909
+ content: [
3910
+ {
3911
+ type: 'text',
3912
+ text: JSON.stringify({
3913
+ scriptPath: args.scriptPath,
3914
+ diagnosticsCount: diagnostics.length,
3915
+ blockOnError: hasBlocking,
3916
+ canApply: !hasBlocking,
3917
+ diagnostics,
3918
+ }, null, 2),
3919
+ },
3920
+ ],
3921
+ };
3922
+ }
3923
+ catch (error) {
3924
+ return this.createErrorResponse(`Failed LSP validation: ${error?.message || 'Unknown error'}`, [
3925
+ 'Ensure Godot editor is running with LSP enabled (port 6005)',
3926
+ ]);
3927
+ }
3928
+ }
3929
+ /**
3930
+ * Version and protocol gate
3931
+ */
3932
+ async handleEnforceVersionGate(args) {
3933
+ args = this.normalizeParameters(args);
3934
+ if (!args.projectPath) {
3935
+ return this.createErrorResponse('Project path is required', ['Provide projectPath']);
3936
+ }
3937
+ const minGodotVersion = args.minGodotVersion || '4.2';
3938
+ const minProtocolVersion = args.minProtocolVersion || '1.0';
3939
+ try {
3940
+ const versionResult = await this.handleGetGodotVersion();
3941
+ const godotVersion = (versionResult?.content?.[0]?.text || '').trim();
3942
+ const godotOk = this.compareMajorMinorVersions(godotVersion, minGodotVersion);
3943
+ let runtimeProtocol = 'unknown';
3944
+ let runtimeConnected = false;
3945
+ let protocolOk = false;
3946
+ let capabilityInfo = {};
3947
+ const runtime = await this.handleRuntimeCommand('ping', {});
3948
+ const runtimeText = runtime?.content?.[0]?.text || '{}';
3949
+ try {
3950
+ const parsed = JSON.parse(runtimeText);
3951
+ runtimeConnected = !parsed?.error;
3952
+ runtimeProtocol = parsed?.protocol_version || parsed?.protocolVersion || '1.0';
3953
+ capabilityInfo = {
3954
+ hasRuntime: runtimeConnected,
3955
+ responseType: parsed?.type || null,
3956
+ };
3957
+ }
3958
+ catch {
3959
+ runtimeConnected = false;
3960
+ }
3961
+ protocolOk = this.compareMajorMinorVersions(runtimeProtocol, minProtocolVersion);
3962
+ return {
3963
+ content: [
3964
+ {
3965
+ type: 'text',
3966
+ text: JSON.stringify({
3967
+ success: godotOk && (runtimeConnected ? protocolOk : true),
3968
+ requirements: {
3969
+ minGodotVersion,
3970
+ minProtocolVersion,
3971
+ },
3972
+ actual: {
3973
+ godotVersion,
3974
+ runtimeConnected,
3975
+ runtimeProtocol,
3976
+ },
3977
+ checks: {
3978
+ godotOk,
3979
+ protocolOk: runtimeConnected ? protocolOk : null,
3980
+ },
3981
+ capabilityInfo,
3982
+ recommendation: godotOk
3983
+ ? runtimeConnected
3984
+ ? protocolOk
3985
+ ? 'Version gate passed.'
3986
+ : 'Runtime protocol is below minimum. Update runtime addon.'
3987
+ : 'Godot version is compatible. Runtime addon not connected; run project/addon for full protocol check.'
3988
+ : 'Godot version below minimum requirement. Upgrade Godot.',
3989
+ }, null, 2),
3990
+ },
3991
+ ],
3992
+ };
3993
+ }
3994
+ catch (error) {
3995
+ return this.createErrorResponse(`Failed to enforce version gate: ${error?.message || 'Unknown error'}`, [
3996
+ 'Ensure Godot is installed and runtime addon is available',
3997
+ ]);
3998
+ }
3999
+ }
4000
+ getIntentMemoryDir(projectPath) {
4001
+ const dir = join(projectPath, '.godot-mcp-memory');
4002
+ if (!existsSync(dir)) {
4003
+ mkdirSync(dir, { recursive: true });
4004
+ }
4005
+ return dir;
4006
+ }
4007
+ scheduleLogFlush() {
4008
+ if (this.logFlushTimer)
4009
+ return;
4010
+ this.logFlushTimer = setTimeout(() => {
4011
+ this.flushLogQueue();
4012
+ }, this.logFlushIntervalMs);
4013
+ }
4014
+ flushLogQueue() {
4015
+ const batch = this.logQueue.splice(0, this.logQueue.length);
4016
+ this.logFlushTimer = null;
4017
+ if (batch.length === 0)
4018
+ return;
4019
+ const grouped = new Map();
4020
+ for (const item of batch) {
4021
+ const line = JSON.stringify(item.payload) + '\n';
4022
+ const existing = grouped.get(item.filePath) || [];
4023
+ existing.push(line);
4024
+ grouped.set(item.filePath, existing);
4025
+ }
4026
+ for (const [filePath, lines] of grouped.entries()) {
4027
+ appendFileSync(filePath, lines.join(''), 'utf8');
4028
+ }
4029
+ }
4030
+ appendJsonl(filePath, payload) {
4031
+ // Lite mode: asynchronous queued write to reduce user-facing latency
4032
+ // Full mode: still queued/batched to avoid frequent fs sync stalls
4033
+ this.logQueue.push({ filePath, payload });
4034
+ // Backpressure guard: if queue grows too large, flush immediately
4035
+ if (this.logQueue.length >= 50) {
4036
+ this.flushLogQueue();
4037
+ return;
4038
+ }
4039
+ this.scheduleLogFlush();
4040
+ }
4041
+ readJsonArray(filePath) {
4042
+ if (!existsSync(filePath))
4043
+ return [];
4044
+ try {
4045
+ const raw = readFileSync(filePath, 'utf8');
4046
+ const parsed = JSON.parse(raw);
4047
+ return Array.isArray(parsed) ? parsed : [];
4048
+ }
4049
+ catch {
4050
+ return [];
4051
+ }
4052
+ }
4053
+ writeJsonArray(filePath, value) {
4054
+ writeFileSync(filePath, JSON.stringify(value, null, 2), 'utf8');
4055
+ }
4056
+ readJsonl(filePath) {
4057
+ if (!existsSync(filePath))
4058
+ return [];
4059
+ const raw = readFileSync(filePath, 'utf8');
4060
+ return raw
4061
+ .split('\n')
4062
+ .map((line) => line.trim())
4063
+ .filter((line) => line.length > 0)
4064
+ .map((line) => {
4065
+ try {
4066
+ return JSON.parse(line);
4067
+ }
4068
+ catch {
4069
+ return null;
4070
+ }
4071
+ })
4072
+ .filter((v) => v !== null);
4073
+ }
4074
+ /**
4075
+ * Capture/update current intent snapshot
4076
+ */
4077
+ async handleCaptureIntentSnapshot(args) {
4078
+ args = this.normalizeParameters(args);
4079
+ if (!args.projectPath || !args.goal) {
4080
+ return this.createErrorResponse('Missing required parameters', ['Provide projectPath and goal']);
4081
+ }
4082
+ const projectFile = join(args.projectPath, 'project.godot');
4083
+ if (!existsSync(projectFile)) {
4084
+ return this.createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure project.godot exists']);
4085
+ }
4086
+ const memoryDir = this.getIntentMemoryDir(args.projectPath);
4087
+ const indexPath = join(memoryDir, 'intent-index.json');
4088
+ const existing = this.readJsonArray(indexPath);
4089
+ const intentId = `intent_${Date.now()}`;
4090
+ const snapshot = {
4091
+ intent_id: intentId,
4092
+ ts: new Date().toISOString(),
4093
+ goal: args.goal,
4094
+ why: args.why || '',
4095
+ constraints: Array.isArray(args.constraints) ? args.constraints : [],
4096
+ acceptance_criteria: Array.isArray(args.acceptanceCriteria) ? args.acceptanceCriteria : [],
4097
+ non_goals: Array.isArray(args.nonGoals) ? args.nonGoals : [],
4098
+ priority: args.priority || 'P1',
4099
+ status: 'active',
4100
+ };
4101
+ for (const item of existing) {
4102
+ if (item && item.status === 'active') {
4103
+ item.status = 'archived';
4104
+ }
4105
+ }
4106
+ existing.push(snapshot);
4107
+ this.writeJsonArray(indexPath, existing);
4108
+ const eventsPath = join(memoryDir, 'dev-activity.jsonl');
4109
+ this.appendJsonl(eventsPath, {
4110
+ ts: new Date().toISOString(),
4111
+ actor: 'mcp-server',
4112
+ action: 'capture_intent_snapshot',
4113
+ intent_id: intentId,
4114
+ result: 'success',
4115
+ });
4116
+ return {
4117
+ content: [
4118
+ {
4119
+ type: 'text',
4120
+ text: JSON.stringify({ success: true, intent: snapshot, files: { indexPath, eventsPath } }, null, 2),
4121
+ },
4122
+ ],
4123
+ };
4124
+ }
4125
+ /**
4126
+ * Record decision log entry
4127
+ */
4128
+ async handleRecordDecisionLog(args) {
4129
+ args = this.normalizeParameters(args);
4130
+ if (!args.projectPath || !args.decision) {
4131
+ return this.createErrorResponse('Missing required parameters', ['Provide projectPath and decision']);
4132
+ }
4133
+ const projectFile = join(args.projectPath, 'project.godot');
4134
+ if (!existsSync(projectFile)) {
4135
+ return this.createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure project.godot exists']);
4136
+ }
4137
+ const memoryDir = this.getIntentMemoryDir(args.projectPath);
4138
+ const indexPath = join(memoryDir, 'intent-index.json');
4139
+ const decisionsPath = join(memoryDir, 'decision-log.jsonl');
4140
+ const intents = this.readJsonArray(indexPath);
4141
+ const activeIntent = intents.find((i) => i && i.status === 'active');
4142
+ const intentId = args.intentId || activeIntent?.intent_id || null;
4143
+ const decisionRecord = {
4144
+ decision_id: `dec_${Date.now()}`,
4145
+ ts: new Date().toISOString(),
4146
+ intent_id: intentId,
4147
+ decision: args.decision,
4148
+ rationale: args.rationale || '',
4149
+ alternatives_rejected: Array.isArray(args.alternativesRejected) ? args.alternativesRejected : [],
4150
+ evidence_refs: Array.isArray(args.evidenceRefs) ? args.evidenceRefs : [],
4151
+ };
4152
+ this.appendJsonl(decisionsPath, decisionRecord);
4153
+ return {
4154
+ content: [
4155
+ {
4156
+ type: 'text',
4157
+ text: JSON.stringify({ success: true, decision: decisionRecord, file: decisionsPath }, null, 2),
4158
+ },
4159
+ ],
4160
+ };
4161
+ }
4162
+ /**
4163
+ * Generate handoff brief
4164
+ */
4165
+ async handleGenerateHandoffBrief(args) {
4166
+ args = this.normalizeParameters(args);
4167
+ if (!args.projectPath) {
4168
+ return this.createErrorResponse('Project path is required', ['Provide projectPath']);
4169
+ }
4170
+ const projectFile = join(args.projectPath, 'project.godot');
4171
+ if (!existsSync(projectFile)) {
4172
+ return this.createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure project.godot exists']);
4173
+ }
4174
+ const maxItems = Number.isFinite(Number(args.maxItems)) ? Number(args.maxItems) : 5;
4175
+ const memoryDir = this.getIntentMemoryDir(args.projectPath);
4176
+ const indexPath = join(memoryDir, 'intent-index.json');
4177
+ const decisionsPath = join(memoryDir, 'decision-log.jsonl');
4178
+ const eventsPath = join(memoryDir, 'dev-activity.jsonl');
4179
+ const intents = this.readJsonArray(indexPath);
4180
+ const decisions = this.readJsonl(decisionsPath);
4181
+ const events = this.readJsonl(eventsPath);
4182
+ const activeIntent = intents.find((i) => i && i.status === 'active');
4183
+ const relatedDecisions = decisions.filter((d) => d?.intent_id && activeIntent?.intent_id && d.intent_id === activeIntent.intent_id).slice(-maxItems);
4184
+ const recentEvents = events.slice(-maxItems);
4185
+ const brief = {
4186
+ handoff_id: `handoff_${Date.now()}`,
4187
+ ts: new Date().toISOString(),
4188
+ current_goal: activeIntent?.goal || null,
4189
+ constraints: activeIntent?.constraints || [],
4190
+ acceptance_criteria: activeIntent?.acceptance_criteria || [],
4191
+ open_decisions: relatedDecisions.map((d) => d.decision),
4192
+ recent_actions: recentEvents.map((e) => `${e.ts} ${e.action}`),
4193
+ next_actions: [
4194
+ 'Validate active intent against latest user message',
4195
+ 'Resolve top open decision and record rationale',
4196
+ 'Execute next implementation step and append execution trace',
4197
+ ],
4198
+ };
4199
+ const handoffPath = join(memoryDir, 'handoff-latest.json');
4200
+ writeFileSync(handoffPath, JSON.stringify(brief, null, 2), 'utf8');
4201
+ return {
4202
+ content: [
4203
+ {
4204
+ type: 'text',
4205
+ text: JSON.stringify({ success: true, handoff: brief, file: handoffPath }, null, 2),
4206
+ },
4207
+ ],
4208
+ };
4209
+ }
4210
+ /**
4211
+ * Summarize current intent context
4212
+ */
4213
+ async handleSummarizeIntentContext(args) {
4214
+ args = this.normalizeParameters(args);
4215
+ if (!args.projectPath) {
4216
+ return this.createErrorResponse('Project path is required', ['Provide projectPath']);
4217
+ }
4218
+ const projectFile = join(args.projectPath, 'project.godot');
4219
+ if (!existsSync(projectFile)) {
4220
+ return this.createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure project.godot exists']);
4221
+ }
4222
+ const memoryDir = this.getIntentMemoryDir(args.projectPath);
4223
+ const indexPath = join(memoryDir, 'intent-index.json');
4224
+ const decisionsPath = join(memoryDir, 'decision-log.jsonl');
4225
+ const intents = this.readJsonArray(indexPath);
4226
+ const decisions = this.readJsonl(decisionsPath);
4227
+ const activeIntent = intents.find((i) => i && i.status === 'active');
4228
+ const relatedDecisions = decisions.filter((d) => d?.intent_id && activeIntent?.intent_id && d.intent_id === activeIntent.intent_id).slice(-3);
4229
+ const summary = {
4230
+ goal: activeIntent?.goal || null,
4231
+ why: activeIntent?.why || null,
4232
+ constraints: activeIntent?.constraints || [],
4233
+ acceptance_criteria: activeIntent?.acceptance_criteria || [],
4234
+ recent_decisions: relatedDecisions.map((d) => ({
4235
+ decision: d.decision,
4236
+ rationale: d.rationale,
4237
+ })),
4238
+ risk: relatedDecisions.length === 0 ? 'No decisions logged yet; context may be weak.' : null,
4239
+ next_action: 'Call generate_handoff_brief after next major change.',
4240
+ };
4241
+ return {
4242
+ content: [
4243
+ {
4244
+ type: 'text',
4245
+ text: JSON.stringify(summary, null, 2),
4246
+ },
4247
+ ],
4248
+ };
4249
+ }
4250
+ /**
4251
+ * Unified work-step recorder (trace + optional handoff pack refresh)
4252
+ */
4253
+ async handleRecordWorkStep(args) {
4254
+ args = this.normalizeParameters(args);
4255
+ const traceResponse = await this.handleRecordExecutionTrace(args);
4256
+ const refreshHandoffPack = args.refreshHandoffPack !== false;
4257
+ if (!refreshHandoffPack) {
4258
+ return traceResponse;
4259
+ }
4260
+ const handoffResponse = await this.handleExportHandoffPack({
4261
+ projectPath: args.projectPath,
4262
+ maxItems: args.maxItems,
4263
+ });
4264
+ const traceText = traceResponse?.content?.[0]?.text;
4265
+ const handoffText = handoffResponse?.content?.[0]?.text;
4266
+ let tracePayload = null;
4267
+ let handoffPayload = null;
4268
+ try {
4269
+ tracePayload = traceText ? JSON.parse(traceText) : null;
4270
+ }
4271
+ catch { }
4272
+ try {
4273
+ handoffPayload = handoffText ? JSON.parse(handoffText) : null;
4274
+ }
4275
+ catch { }
4276
+ return {
4277
+ content: [
4278
+ {
4279
+ type: 'text',
4280
+ text: JSON.stringify({
4281
+ success: !!(tracePayload?.success && handoffPayload?.success),
4282
+ mode: 'record_work_step',
4283
+ trace: tracePayload,
4284
+ handoffPack: handoffPayload,
4285
+ }, null, 2),
4286
+ },
4287
+ ],
4288
+ };
4289
+ }
4290
+ /**
4291
+ * Record execution trace
4292
+ */
4293
+ async handleRecordExecutionTrace(args) {
4294
+ args = this.normalizeParameters(args);
4295
+ if (!args.projectPath || !args.action || !args.result) {
4296
+ return this.createErrorResponse('Missing required parameters', ['Provide projectPath, action, result']);
4297
+ }
4298
+ const projectFile = join(args.projectPath, 'project.godot');
4299
+ if (!existsSync(projectFile)) {
4300
+ return this.createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure project.godot exists']);
4301
+ }
4302
+ const memoryDir = this.getIntentMemoryDir(args.projectPath);
4303
+ const indexPath = join(memoryDir, 'intent-index.json');
4304
+ const eventsPath = join(memoryDir, 'dev-activity.jsonl');
4305
+ const tracesPath = join(memoryDir, 'execution-trace.jsonl');
4306
+ const intents = this.readJsonArray(indexPath);
4307
+ const activeIntent = intents.find((i) => i && i.status === 'active');
4308
+ const intentId = args.intentId || activeIntent?.intent_id || null;
4309
+ const liteMode = this.recordingMode === 'lite';
4310
+ const trace = {
4311
+ trace_id: `trace_${Date.now()}`,
4312
+ ts: new Date().toISOString(),
4313
+ intent_id: intentId,
4314
+ action: args.action,
4315
+ command: args.command || null,
4316
+ files_changed: Array.isArray(args.filesChanged) ? args.filesChanged : [],
4317
+ result: args.result,
4318
+ artifact: args.artifact || null,
4319
+ error: args.error || null,
4320
+ mode: this.recordingMode,
4321
+ };
4322
+ this.appendJsonl(tracesPath, trace);
4323
+ this.appendJsonl(eventsPath, {
4324
+ ts: new Date().toISOString(),
4325
+ actor: 'mcp-server',
4326
+ action: 'record_execution_trace',
4327
+ intent_id: intentId,
4328
+ result: args.result,
4329
+ summary: liteMode ? `${args.action}:${args.result}` : `${args.action}:${args.result} files=${trace.files_changed.length}`,
4330
+ });
4331
+ return {
4332
+ content: [
4333
+ {
4334
+ type: 'text',
4335
+ text: JSON.stringify({ success: true, trace, file: tracesPath }, null, 2),
4336
+ },
4337
+ ],
4338
+ };
4339
+ }
4340
+ /**
4341
+ * Export handoff pack for team-mode relay
4342
+ */
4343
+ async handleExportHandoffPack(args) {
4344
+ args = this.normalizeParameters(args);
4345
+ if (!args.projectPath) {
4346
+ return this.createErrorResponse('Project path is required', ['Provide projectPath']);
4347
+ }
4348
+ const projectFile = join(args.projectPath, 'project.godot');
4349
+ if (!existsSync(projectFile)) {
4350
+ return this.createErrorResponse(`Not a valid Godot project: ${args.projectPath}`, ['Ensure project.godot exists']);
4351
+ }
4352
+ const maxItems = Number.isFinite(Number(args.maxItems)) ? Number(args.maxItems) : 10;
4353
+ const memoryDir = this.getIntentMemoryDir(args.projectPath);
4354
+ const indexPath = join(memoryDir, 'intent-index.json');
4355
+ const decisionsPath = join(memoryDir, 'decision-log.jsonl');
4356
+ const tracesPath = join(memoryDir, 'execution-trace.jsonl');
4357
+ const intents = this.readJsonArray(indexPath);
4358
+ const decisions = this.readJsonl(decisionsPath);
4359
+ const traces = this.readJsonl(tracesPath);
4360
+ const activeIntent = intents.find((i) => i && i.status === 'active');
4361
+ const intentId = activeIntent?.intent_id || null;
4362
+ const relatedDecisions = decisions.filter((d) => d?.intent_id && intentId && d.intent_id === intentId).slice(-maxItems);
4363
+ const relatedTraces = traces.filter((t) => t?.intent_id && intentId && t.intent_id === intentId).slice(-maxItems);
4364
+ const handoffPack = {
4365
+ pack_id: `pack_${Date.now()}`,
4366
+ ts: new Date().toISOString(),
4367
+ mode: this.recordingMode,
4368
+ intent: activeIntent || null,
4369
+ decisions: relatedDecisions,
4370
+ execution_traces: relatedTraces,
4371
+ summary: {
4372
+ decisions_count: relatedDecisions.length,
4373
+ traces_count: relatedTraces.length,
4374
+ latest_result: relatedTraces.length > 0 ? relatedTraces[relatedTraces.length - 1].result : null,
4375
+ },
4376
+ next_actions: [
4377
+ 'Check intent acceptance criteria against latest changes',
4378
+ 'Resolve remaining open decisions',
4379
+ 'Execute next highest-priority trace and record result',
4380
+ ],
4381
+ };
4382
+ const packPath = join(memoryDir, 'handoff_pack.json');
4383
+ writeFileSync(packPath, JSON.stringify(handoffPack, null, 2), 'utf8');
4384
+ return {
4385
+ content: [
4386
+ {
4387
+ type: 'text',
4388
+ text: JSON.stringify({ success: true, file: packPath, handoffPack }, null, 2),
4389
+ },
4390
+ ],
4391
+ };
4392
+ }
4393
+ /**
4394
+ * Set recording mode
4395
+ */
4396
+ async handleSetRecordingMode(args) {
4397
+ args = this.normalizeParameters(args);
4398
+ const mode = `${args.mode || ''}`.toLowerCase();
4399
+ if (mode !== 'lite' && mode !== 'full') {
4400
+ return this.createErrorResponse('Invalid mode', ['Use mode="lite" or mode="full"']);
4401
+ }
4402
+ this.recordingMode = mode;
4403
+ return {
4404
+ content: [
4405
+ {
4406
+ type: 'text',
4407
+ text: JSON.stringify({ success: true, recordingMode: this.recordingMode }, null, 2),
4408
+ },
4409
+ ],
4410
+ };
4411
+ }
4412
+ /**
4413
+ * Get recording mode
4414
+ */
4415
+ async handleGetRecordingMode() {
4416
+ return {
4417
+ content: [
4418
+ {
4419
+ type: 'text',
4420
+ text: JSON.stringify({
4421
+ recordingMode: this.recordingMode,
4422
+ queueSize: this.logQueue.length,
4423
+ flushIntervalMs: this.logFlushIntervalMs,
4424
+ }, null, 2),
4425
+ },
4426
+ ],
4427
+ };
4428
+ }
3302
4429
  /**
3303
4430
  * Handle the create_scene tool
3304
4431
  */
@@ -5397,7 +6524,9 @@ class GodotServer {
5397
6524
  const transport = new StdioServerTransport();
5398
6525
  await this.server.connect(transport);
5399
6526
  console.error('Godot MCP server running on stdio');
5400
- this.startHealthServer();
6527
+ // Start the Godot Editor Bridge (WebSocket server for editor plugin)
6528
+ await this.godotBridge.start();
6529
+ console.error('[SERVER] Godot Editor Bridge started on port 6505');
5401
6530
  }
5402
6531
  catch (error) {
5403
6532
  const errorMessage = error instanceof Error ? error.message : 'Unknown error';
@@ -5405,78 +6534,6 @@ class GodotServer {
5405
6534
  process.exit(1);
5406
6535
  }
5407
6536
  }
5408
- /**
5409
- * Start a lightweight HTTP server for health checks from Godot editor plugin.
5410
- * Supports GET /health and POST / (MCP initialize handshake).
5411
- */
5412
- startHealthServer() {
5413
- const httpServer = createServer((req, res) => {
5414
- res.setHeader('Access-Control-Allow-Origin', '*');
5415
- res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
5416
- res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Accept');
5417
- if (req.method === 'OPTIONS') {
5418
- res.writeHead(204);
5419
- res.end();
5420
- return;
5421
- }
5422
- if (req.method === 'GET' && (req.url === '/health' || req.url === '/')) {
5423
- const payload = {
5424
- status: 'ok',
5425
- serverName: 'godot-mcp',
5426
- version: '1.1.0',
5427
- godotPath: this.godotPath,
5428
- uptime: process.uptime(),
5429
- timestamp: new Date().toISOString(),
5430
- };
5431
- res.writeHead(200, { 'Content-Type': 'application/json' });
5432
- res.end(JSON.stringify(payload));
5433
- return;
5434
- }
5435
- if (req.method === 'POST' && (req.url === '/' || req.url === '/mcp')) {
5436
- let body = '';
5437
- req.on('data', (chunk) => { body += chunk.toString(); });
5438
- req.on('end', () => {
5439
- try {
5440
- const parsed = JSON.parse(body);
5441
- if (parsed.method === 'initialize') {
5442
- const response = {
5443
- jsonrpc: '2.0',
5444
- id: parsed.id ?? 1,
5445
- result: {
5446
- protocolVersion: parsed.params?.protocolVersion || '2025-06-18',
5447
- capabilities: {},
5448
- serverInfo: { name: 'godot-mcp', version: '1.1.0' },
5449
- },
5450
- };
5451
- res.writeHead(200, { 'Content-Type': 'application/json' });
5452
- res.end(JSON.stringify(response));
5453
- return;
5454
- }
5455
- res.writeHead(400, { 'Content-Type': 'application/json' });
5456
- res.end(JSON.stringify({ error: 'Unsupported method' }));
5457
- }
5458
- catch {
5459
- res.writeHead(400, { 'Content-Type': 'application/json' });
5460
- res.end(JSON.stringify({ error: 'Invalid JSON' }));
5461
- }
5462
- });
5463
- return;
5464
- }
5465
- res.writeHead(404, { 'Content-Type': 'application/json' });
5466
- res.end(JSON.stringify({ error: 'Not found' }));
5467
- });
5468
- httpServer.listen(HEALTH_PORT, () => {
5469
- console.error(`[SERVER] Health endpoint: http://localhost:${HEALTH_PORT}/health`);
5470
- });
5471
- httpServer.on('error', (err) => {
5472
- if (err.code === 'EADDRINUSE') {
5473
- console.error(`[SERVER] Port ${HEALTH_PORT} in use — health endpoint disabled`);
5474
- }
5475
- else {
5476
- console.error(`[SERVER] Health endpoint error: ${err.message}`);
5477
- }
5478
- });
5479
- }
5480
6537
  // ============================================
5481
6538
  // 2D Tile Tools Handlers
5482
6539
  // ============================================
@@ -6265,6 +7322,40 @@ uniform float dissolve_amount : hint_range(0.0, 1.0) = 0.0;
6265
7322
  }
6266
7323
  return await this.executeOperation('modify_resource', params, projectPath);
6267
7324
  }
7325
+ async handleMapProject(args) {
7326
+ const projectPath = args?.projectPath || args?.project_path;
7327
+ if (!projectPath) {
7328
+ throw new McpError(ErrorCode.InvalidParams, 'projectPath is required');
7329
+ }
7330
+ const root = args?.root || 'res://';
7331
+ const includeAddons = args?.include_addons || false;
7332
+ const result = mapProject(projectPath, root, includeAddons);
7333
+ if (!result.ok || !result.project_map) {
7334
+ return {
7335
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: result.error || 'Failed to map project' }) }],
7336
+ isError: true,
7337
+ };
7338
+ }
7339
+ setProjectPath(projectPath);
7340
+ try {
7341
+ const url = await serveVisualization(result.project_map, this.godotBridge);
7342
+ return {
7343
+ content: [{ type: 'text', text: JSON.stringify({
7344
+ ok: true,
7345
+ url,
7346
+ total_scripts: result.project_map.total_scripts,
7347
+ total_connections: result.project_map.total_connections,
7348
+ message: `Interactive project map opened at ${url} — ${result.project_map.total_scripts} scripts, ${result.project_map.total_connections} connections`,
7349
+ }, null, 2) }],
7350
+ };
7351
+ }
7352
+ catch (error) {
7353
+ const errMsg = error instanceof Error ? error.message : 'Unknown error';
7354
+ return {
7355
+ content: [{ type: 'text', text: JSON.stringify({ ok: false, error: `Failed to start visualizer: ${errMsg}`, project_map: result.project_map }) }],
7356
+ };
7357
+ }
7358
+ }
6268
7359
  }
6269
7360
  // Create and run the server
6270
7361
  const server = new GodotServer();