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/README.md +148 -786
- package/build/addon/godot_mcp_editor/mcp_client.gd +161 -0
- package/build/addon/godot_mcp_editor/plugin.cfg +6 -0
- package/build/addon/godot_mcp_editor/plugin.gd +84 -0
- package/build/addon/godot_mcp_editor/tool_executor.gd +114 -0
- package/build/addon/godot_mcp_editor/tools/animation_tools.gd +502 -0
- package/build/addon/godot_mcp_editor/tools/resource_tools.gd +425 -0
- package/build/addon/godot_mcp_editor/tools/scene_tools.gd +710 -0
- package/build/gdscript_parser.js +828 -0
- package/build/godot-bridge.js +470 -0
- package/build/index.js +1203 -112
- package/build/visualizer/canvas.js +832 -0
- package/build/visualizer/events.js +814 -0
- package/build/visualizer/layout.js +304 -0
- package/build/visualizer/main.js +245 -0
- package/build/visualizer/modals.js +239 -0
- package/build/visualizer/panel.js +1091 -0
- package/build/visualizer/state.js +210 -0
- package/build/visualizer/syntax.js +106 -0
- package/build/visualizer/usages.js +352 -0
- package/build/visualizer/websocket.js +85 -0
- package/build/visualizer-server.js +375 -0
- package/build/visualizer.html +6395 -0
- package/package.json +7 -4
- package/build/addon/auto_reload/auto_reload.gd.uid +0 -1
- package/build/addon/godot_mcp_runtime/godot_mcp_runtime.gd.uid +0 -1
- package/build/addon/godot_mcp_runtime/mcp_runtime_autoload.gd.uid +0 -1
- package/build/scripts/godot_operations.gd.uid +0 -1
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
|
-
|
|
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
|
-
|
|
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.
|
|
3117
|
+
return await this.handleViaBridge('create_scene', request.params.arguments);
|
|
2717
3118
|
case 'add_node':
|
|
2718
|
-
return await this.
|
|
3119
|
+
return await this.handleViaBridge('add_node', request.params.arguments);
|
|
2719
3120
|
case 'load_sprite':
|
|
2720
|
-
return await this.
|
|
3121
|
+
return await this.handleViaBridge('load_sprite', request.params.arguments);
|
|
2721
3122
|
case 'save_scene':
|
|
2722
|
-
return await this.
|
|
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.
|
|
3130
|
+
return await this.handleViaBridge('list_scene_nodes', request.params.arguments);
|
|
2730
3131
|
case 'get_node_properties':
|
|
2731
|
-
return await this.
|
|
3132
|
+
return await this.handleViaBridge('get_node_properties', request.params.arguments);
|
|
2732
3133
|
case 'set_node_properties':
|
|
2733
|
-
return await this.
|
|
3134
|
+
return await this.handleViaBridge('set_node_properties', request.params.arguments);
|
|
2734
3135
|
case 'delete_node':
|
|
2735
|
-
return await this.
|
|
3136
|
+
return await this.handleViaBridge('delete_node', request.params.arguments);
|
|
2736
3137
|
case 'duplicate_node':
|
|
2737
|
-
return await this.
|
|
3138
|
+
return await this.handleViaBridge('duplicate_node', request.params.arguments);
|
|
2738
3139
|
case 'reparent_node':
|
|
2739
|
-
return await this.
|
|
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.
|
|
3180
|
+
return await this.handleViaBridge('connect_signal', request.params.arguments);
|
|
2780
3181
|
case 'disconnect_signal':
|
|
2781
|
-
return await this.
|
|
3182
|
+
return await this.handleViaBridge('disconnect_signal', request.params.arguments);
|
|
2782
3183
|
case 'list_connections':
|
|
2783
|
-
return await this.
|
|
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.
|
|
3198
|
+
return await this.handleViaBridge('create_resource', request.params.arguments);
|
|
2798
3199
|
case 'create_material':
|
|
2799
|
-
return await this.
|
|
3200
|
+
return await this.handleViaBridge('create_material', request.params.arguments);
|
|
2800
3201
|
case 'create_shader':
|
|
2801
|
-
return await this.
|
|
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.
|
|
3212
|
+
return await this.handleViaBridge('create_animation', request.params.arguments);
|
|
2812
3213
|
case 'add_animation_track':
|
|
2813
|
-
return await this.
|
|
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.
|
|
3230
|
+
return await this.handleViaBridge('create_tileset', request.params.arguments);
|
|
2830
3231
|
case 'set_tilemap_cells':
|
|
2831
|
-
return await this.
|
|
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.
|
|
3246
|
+
return await this.handleViaBridge('create_navigation_region', request.params.arguments);
|
|
2846
3247
|
case 'create_navigation_agent':
|
|
2847
|
-
return await this.
|
|
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.
|
|
3252
|
+
return await this.handleViaBridge('create_animation_tree', request.params.arguments);
|
|
2852
3253
|
case 'add_animation_state':
|
|
2853
|
-
return await this.
|
|
3254
|
+
return await this.handleViaBridge('add_animation_state', request.params.arguments);
|
|
2854
3255
|
case 'connect_animation_states':
|
|
2855
|
-
return await this.
|
|
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.
|
|
3259
|
+
return await this.handleViaBridge('set_theme_color', request.params.arguments);
|
|
2859
3260
|
case 'set_theme_font_size':
|
|
2860
|
-
return await this.
|
|
3261
|
+
return await this.handleViaBridge('set_theme_font_size', request.params.arguments);
|
|
2861
3262
|
case 'apply_theme_shader':
|
|
2862
|
-
return await this.
|
|
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.
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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();
|