@beremaran/godot-agent-loop 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +23 -0
- package/README.md +925 -0
- package/addons/godot_agent_loop/LICENSE +23 -0
- package/addons/godot_agent_loop/README.md +31 -0
- package/addons/godot_agent_loop/plugin.cfg +8 -0
- package/addons/godot_agent_loop/plugin.gd +417 -0
- package/agent-plugin/.claude-plugin/plugin.json +19 -0
- package/agent-plugin/.codex-plugin/plugin.json +37 -0
- package/agent-plugin/.mcp.json +14 -0
- package/agent-plugin/adapter-manifest.json +45 -0
- package/agent-plugin/pi/extension.ts +136 -0
- package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
- package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
- package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
- package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
- package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
- package/build/authoring-session-manager.js +237 -0
- package/build/domain-tool-registries.js +207 -0
- package/build/editor-bridge-protocol.js +1 -0
- package/build/editor-connection.js +112 -0
- package/build/editor-mutation-guard.js +30 -0
- package/build/editor-plugin-installer.js +220 -0
- package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
- package/build/game-command-service.js +49 -0
- package/build/game-connection.js +337 -0
- package/build/godot-executable.js +156 -0
- package/build/godot-process-manager.js +112 -0
- package/build/godot-subprocess.js +23 -0
- package/build/headless-operation-runner.js +68 -0
- package/build/headless-operation-service.js +65 -0
- package/build/index.js +478 -0
- package/build/interaction-server-installer.js +234 -0
- package/build/opencode-setup.js +199 -0
- package/build/project-support.js +219 -0
- package/build/runtime-protocol.js +202 -0
- package/build/scripts/godot_operations.gd +1534 -0
- package/build/scripts/mcp_editor_plugin.gd +417 -0
- package/build/scripts/mcp_interaction_server.gd +1255 -0
- package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
- package/build/scripts/mcp_runtime/command_params.gd +339 -0
- package/build/scripts/mcp_runtime/core_domain.gd +591 -0
- package/build/scripts/mcp_runtime/input_domain.gd +695 -0
- package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
- package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
- package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
- package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
- package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
- package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
- package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
- package/build/scripts/mcp_runtime/system_domain.gd +277 -0
- package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
- package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
- package/build/scripts/validate_script.gd +28 -0
- package/build/server-instructions.js +11 -0
- package/build/session-timing.js +20 -0
- package/build/tool-argument-validation.js +120 -0
- package/build/tool-definitions.js +2727 -0
- package/build/tool-handlers/game-tool-handlers.js +1345 -0
- package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
- package/build/tool-handlers/project-handler-services.js +1458 -0
- package/build/tool-handlers/project-tool-handlers.js +1506 -0
- package/build/tool-handlers/visual-regression-service.js +139 -0
- package/build/tool-manifest.js +1193 -0
- package/build/tool-mutation-policy.js +122 -0
- package/build/tool-registry.js +34 -0
- package/build/tool-surface.js +70 -0
- package/build/utils.js +273 -0
- package/package.json +110 -0
- package/product.json +52 -0
- package/scripts/prepare-package.js +42 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { toolManifest } from './tool-manifest.js';
|
|
2
|
+
/**
|
|
3
|
+
* Calls which only observe state. Everything not listed here is deliberately
|
|
4
|
+
* classified as mutating so a newly added tool cannot bypass the editor's
|
|
5
|
+
* cooperative driver lock by omission.
|
|
6
|
+
*/
|
|
7
|
+
export const READ_ONLY_TOOLS = new Set([
|
|
8
|
+
'get_debug_output',
|
|
9
|
+
'get_godot_version',
|
|
10
|
+
'list_projects',
|
|
11
|
+
'get_project_info',
|
|
12
|
+
'get_uid',
|
|
13
|
+
'game_screenshot',
|
|
14
|
+
'game_get_ui',
|
|
15
|
+
'game_get_scene_tree',
|
|
16
|
+
'game_get_property',
|
|
17
|
+
'game_get_node_info',
|
|
18
|
+
'game_get_nodes_in_group',
|
|
19
|
+
'game_find_nodes_by_class',
|
|
20
|
+
'read_scene',
|
|
21
|
+
'read_project_settings',
|
|
22
|
+
'list_project_files',
|
|
23
|
+
'read_file',
|
|
24
|
+
'game_get_errors',
|
|
25
|
+
'game_get_logs',
|
|
26
|
+
'game_get_camera',
|
|
27
|
+
'game_raycast',
|
|
28
|
+
'game_get_audio',
|
|
29
|
+
'game_list_signals',
|
|
30
|
+
'game_os_info',
|
|
31
|
+
'game_physics_3d',
|
|
32
|
+
'game_physics_2d',
|
|
33
|
+
'validate_script',
|
|
34
|
+
'validate_scripts',
|
|
35
|
+
]);
|
|
36
|
+
/** Read-only modes on tools whose other actions mutate project/runtime state. */
|
|
37
|
+
export const READ_ONLY_ACTIONS = {
|
|
38
|
+
godot_tools: ['search', 'describe'],
|
|
39
|
+
editor_control: ['inspect'],
|
|
40
|
+
run_project_tests: ['discover'],
|
|
41
|
+
manage_import_pipeline: ['inspect', 'dependencies'],
|
|
42
|
+
analyze_project_integrity: ['analyze', 'preview_rename', 'assets', 'localization', 'accessibility', 'extensions', 'leaks'],
|
|
43
|
+
verify_export_readiness: ['inspect'],
|
|
44
|
+
verify_dotnet_project: ['inspect'],
|
|
45
|
+
manage_addon: ['inspect'],
|
|
46
|
+
game_performance: ['sample', 'report', 'leaks'],
|
|
47
|
+
game_play_animation: ['get_list'],
|
|
48
|
+
manage_autoloads: ['list'],
|
|
49
|
+
manage_input_map: ['list'],
|
|
50
|
+
manage_export_presets: ['list'],
|
|
51
|
+
game_tilemap: ['get_cell', 'get_used_cells'],
|
|
52
|
+
game_environment: ['get'],
|
|
53
|
+
game_manage_group: ['get_groups'],
|
|
54
|
+
game_bone_pose: ['list', 'get'],
|
|
55
|
+
game_viewport: ['get'],
|
|
56
|
+
game_websocket: ['status'],
|
|
57
|
+
game_multiplayer: ['status'],
|
|
58
|
+
game_input_state: ['query'],
|
|
59
|
+
game_input_action: ['list'],
|
|
60
|
+
game_script: ['get_source'],
|
|
61
|
+
game_window: ['get'],
|
|
62
|
+
game_time_scale: ['get'],
|
|
63
|
+
game_world_settings: ['get'],
|
|
64
|
+
game_multimesh: ['get_info'],
|
|
65
|
+
game_gridmap: ['get_cell', 'get_used'],
|
|
66
|
+
game_path_3d: ['get_points'],
|
|
67
|
+
game_camera_attributes: ['get'],
|
|
68
|
+
game_path_2d: ['get_points'],
|
|
69
|
+
game_shape_2d: ['get_points'],
|
|
70
|
+
game_animation_tree: ['get_state'],
|
|
71
|
+
game_animation_control: ['get_info'],
|
|
72
|
+
game_audio_effect: ['list'],
|
|
73
|
+
game_audio_bus_layout: ['list'],
|
|
74
|
+
game_audio_spatial: ['get_info'],
|
|
75
|
+
manage_resource: ['read'],
|
|
76
|
+
manage_scene_signals: ['list'],
|
|
77
|
+
manage_layers: ['list'],
|
|
78
|
+
manage_plugins: ['list'],
|
|
79
|
+
manage_shader: ['read'],
|
|
80
|
+
manage_theme_resource: ['read'],
|
|
81
|
+
manage_translations: ['list'],
|
|
82
|
+
game_locale: ['get', 'translate'],
|
|
83
|
+
game_ui_control: ['get_info'],
|
|
84
|
+
game_ui_text: ['get'],
|
|
85
|
+
game_ui_popup: ['get_info'],
|
|
86
|
+
game_ui_tree: ['get_items'],
|
|
87
|
+
game_ui_item_list: ['get_items'],
|
|
88
|
+
game_ui_tabs: ['get_tabs'],
|
|
89
|
+
game_ui_menu: ['get_items'],
|
|
90
|
+
game_ui_range: ['get'],
|
|
91
|
+
game_render_settings: ['get'],
|
|
92
|
+
game_resource: ['exists'],
|
|
93
|
+
game_visual_shader: ['get_nodes'],
|
|
94
|
+
game_terrain: ['get_height'],
|
|
95
|
+
game_video: ['get_status'],
|
|
96
|
+
manage_ci_pipeline: ['read'],
|
|
97
|
+
manage_docker_export: ['read'],
|
|
98
|
+
};
|
|
99
|
+
/**
|
|
100
|
+
* Returns true unless a call is explicitly proven observational. Unknown tools,
|
|
101
|
+
* missing selectors, and action fields used as data all remain mutating.
|
|
102
|
+
*/
|
|
103
|
+
export function isToolCallMutating(name, args) {
|
|
104
|
+
if (name === 'godot_tools' && args.action === 'call') {
|
|
105
|
+
if (typeof args.toolName !== 'string' || args.toolName === 'godot_tools')
|
|
106
|
+
return true;
|
|
107
|
+
const nested = args.arguments && typeof args.arguments === 'object' && !Array.isArray(args.arguments)
|
|
108
|
+
? args.arguments
|
|
109
|
+
: {};
|
|
110
|
+
return isToolCallMutating(args.toolName, nested);
|
|
111
|
+
}
|
|
112
|
+
if (!Object.prototype.hasOwnProperty.call(toolManifest, name))
|
|
113
|
+
return true;
|
|
114
|
+
const toolName = name;
|
|
115
|
+
if (READ_ONLY_TOOLS.has(toolName))
|
|
116
|
+
return false;
|
|
117
|
+
const entry = toolManifest[toolName];
|
|
118
|
+
if (entry.actionParamIsData)
|
|
119
|
+
return true;
|
|
120
|
+
const readOnlyActions = READ_ONLY_ACTIONS[toolName];
|
|
121
|
+
return !readOnlyActions?.includes(typeof args.action === 'string' ? args.action : '');
|
|
122
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from '@modelcontextprotocol/sdk/types.js';
|
|
2
|
+
import { toolDefinitions } from './tool-definitions.js';
|
|
3
|
+
import { createToolArgumentParser, ToolArgumentValidationError } from './tool-argument-validation.js';
|
|
4
|
+
/**
|
|
5
|
+
* Dispatches MCP tool calls to their handlers.
|
|
6
|
+
*/
|
|
7
|
+
export class ToolRegistry {
|
|
8
|
+
preflight;
|
|
9
|
+
handlers = new Map();
|
|
10
|
+
parseArguments = createToolArgumentParser(toolDefinitions);
|
|
11
|
+
constructor(handlers, preflight) {
|
|
12
|
+
this.preflight = preflight;
|
|
13
|
+
this.handlers = new Map(Object.entries(handlers));
|
|
14
|
+
}
|
|
15
|
+
dispatch(name, args) {
|
|
16
|
+
const handler = this.handlers.get(name);
|
|
17
|
+
if (!handler) {
|
|
18
|
+
throw new McpError(ErrorCode.MethodNotFound, `Unknown tool: ${name}`);
|
|
19
|
+
}
|
|
20
|
+
try {
|
|
21
|
+
const parsed = this.parseArguments(name, args);
|
|
22
|
+
if (!this.preflight)
|
|
23
|
+
return handler(parsed);
|
|
24
|
+
return Promise.resolve(this.preflight(name, parsed))
|
|
25
|
+
.then(blocked => blocked ?? handler(parsed));
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof ToolArgumentValidationError) {
|
|
29
|
+
throw new McpError(ErrorCode.InvalidParams, error.message);
|
|
30
|
+
}
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { toolDefinitions } from './tool-definitions.js';
|
|
2
|
+
import { toolManifest } from './tool-manifest.js';
|
|
3
|
+
export const TOOL_SURFACE_ENV = 'GODOT_MCP_TOOL_SURFACE';
|
|
4
|
+
/**
|
|
5
|
+
* Stable, task-oriented starting surface. The complete catalog remains callable
|
|
6
|
+
* through godot_tools and can be advertised directly with TOOL_SURFACE_ENV=full.
|
|
7
|
+
*/
|
|
8
|
+
export const CORE_TOOL_NAMES = new Set([
|
|
9
|
+
'godot_tools',
|
|
10
|
+
'launch_editor', 'editor_control', 'run_project', 'verify_project', 'run_project_tests',
|
|
11
|
+
'get_debug_output', 'stop_project', 'get_godot_version', 'list_projects', 'get_project_info',
|
|
12
|
+
'create_project', 'create_scene', 'add_node', 'read_scene', 'modify_scene_node',
|
|
13
|
+
'remove_scene_node', 'save_scene', 'create_script', 'attach_script', 'read_file',
|
|
14
|
+
'write_file', 'validate_script', 'validate_scripts', 'read_project_settings',
|
|
15
|
+
'modify_project_settings', 'list_project_files', 'manage_input_map', 'set_main_scene',
|
|
16
|
+
'game_get_scene_tree', 'game_get_ui', 'game_screenshot', 'game_get_node_info',
|
|
17
|
+
'game_get_errors', 'game_get_logs', 'game_wait', 'game_click', 'game_key_press',
|
|
18
|
+
'game_key_release',
|
|
19
|
+
]);
|
|
20
|
+
export const TOOL_SURFACE_BUDGETS = {
|
|
21
|
+
fullBytesMax: 100_000,
|
|
22
|
+
coreBytesMax: 25_000,
|
|
23
|
+
coreEstimatedTokensMax: 6_250,
|
|
24
|
+
coreToolCountMax: 40,
|
|
25
|
+
coreReductionPercentMin: 70,
|
|
26
|
+
};
|
|
27
|
+
export function resolveToolSurfaceMode(value = process.env[TOOL_SURFACE_ENV]) {
|
|
28
|
+
return value === 'full' ? 'full' : 'core';
|
|
29
|
+
}
|
|
30
|
+
export function advertisedToolDefinitions(mode = resolveToolSurfaceMode()) {
|
|
31
|
+
return mode === 'full'
|
|
32
|
+
? toolDefinitions
|
|
33
|
+
: toolDefinitions.filter(definition => CORE_TOOL_NAMES.has(definition.name));
|
|
34
|
+
}
|
|
35
|
+
export function compactToolSurfaceBytes(definitions) {
|
|
36
|
+
return Buffer.byteLength(JSON.stringify(definitions), 'utf8');
|
|
37
|
+
}
|
|
38
|
+
export function estimatedToolSurfaceTokens(definitions) {
|
|
39
|
+
return Math.ceil(compactToolSurfaceBytes(definitions) / 4);
|
|
40
|
+
}
|
|
41
|
+
export function searchToolCatalog(query, domain, limit = 20) {
|
|
42
|
+
const terms = query.toLowerCase().trim().split(/\s+/).filter(Boolean);
|
|
43
|
+
return toolDefinitions
|
|
44
|
+
.filter(definition => {
|
|
45
|
+
const manifest = toolManifest[definition.name];
|
|
46
|
+
if (domain && manifest.domain !== domain)
|
|
47
|
+
return false;
|
|
48
|
+
const searchable = [definition.name, definition.description, ...(manifest.actions ?? [])]
|
|
49
|
+
.join(' ').toLowerCase();
|
|
50
|
+
return terms.every(term => searchable.includes(term));
|
|
51
|
+
})
|
|
52
|
+
.slice(0, limit)
|
|
53
|
+
.map(definition => {
|
|
54
|
+
const manifest = toolManifest[definition.name];
|
|
55
|
+
return {
|
|
56
|
+
name: definition.name,
|
|
57
|
+
domain: manifest.domain,
|
|
58
|
+
description: definition.description,
|
|
59
|
+
actions: manifest.actions ?? ['*'],
|
|
60
|
+
privileged: manifest.privileged,
|
|
61
|
+
};
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
export function describeCatalogTool(name) {
|
|
65
|
+
const definition = toolDefinitions.find(candidate => candidate.name === name);
|
|
66
|
+
if (!definition)
|
|
67
|
+
return undefined;
|
|
68
|
+
const manifest = toolManifest[definition.name];
|
|
69
|
+
return { definition, domain: manifest.domain, backend: manifest.backend, privileged: manifest.privileged };
|
|
70
|
+
}
|
package/build/utils.js
ADDED
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import { isAbsolute, relative, resolve } from 'path';
|
|
3
|
+
export function errorMessage(error) {
|
|
4
|
+
return error instanceof Error ? error.message : 'Unknown error';
|
|
5
|
+
}
|
|
6
|
+
export const PARAMETER_MAPPINGS = {
|
|
7
|
+
'msaa_2d': 'msaa2d',
|
|
8
|
+
'msaa_3d': 'msaa3d',
|
|
9
|
+
// Runtime mouse handlers historically consume these wire names directly.
|
|
10
|
+
'relative_x': 'relative_x',
|
|
11
|
+
'relative_y': 'relative_y',
|
|
12
|
+
};
|
|
13
|
+
const OPAQUE_PARAMETER_OBJECTS = new Set(['properties', 'shapeParams', 'overrides', 'headers', 'args']);
|
|
14
|
+
export const REVERSE_PARAMETER_MAPPINGS = Object.fromEntries(Object.entries(PARAMETER_MAPPINGS).map(([snake, camel]) => [camel, snake]));
|
|
15
|
+
export function normalizeParameters(params) {
|
|
16
|
+
if (!params || typeof params !== 'object') {
|
|
17
|
+
return params;
|
|
18
|
+
}
|
|
19
|
+
const result = {};
|
|
20
|
+
for (const key in params) {
|
|
21
|
+
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
|
22
|
+
const normalizedKey = PARAMETER_MAPPINGS[key] || key.replace(/_([a-z0-9])/g, (_, letter) => letter.toUpperCase());
|
|
23
|
+
const value = params[key];
|
|
24
|
+
result[normalizedKey] = OPAQUE_PARAMETER_OBJECTS.has(normalizedKey) ? value : normalizeValue(value);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return result;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Normalize a value of any shape. Arrays must stay arrays at every depth: a
|
|
31
|
+
* nested array is still `typeof "object"`, so treating it as a record turned
|
|
32
|
+
* mesh buffers like `[[x, y, z], ...]` into `[{"0": x, "1": y, "2": z}, ...]`
|
|
33
|
+
* and the engine read them as empty.
|
|
34
|
+
*/
|
|
35
|
+
function normalizeValue(value) {
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return value.map(normalizeValue);
|
|
38
|
+
if (value && typeof value === 'object')
|
|
39
|
+
return normalizeParameters(value);
|
|
40
|
+
return value;
|
|
41
|
+
}
|
|
42
|
+
export function convertCamelToSnakeCase(params) {
|
|
43
|
+
const result = {};
|
|
44
|
+
for (const key in params) {
|
|
45
|
+
if (Object.prototype.hasOwnProperty.call(params, key)) {
|
|
46
|
+
const snakeKey = REVERSE_PARAMETER_MAPPINGS[key] || key.replace(/[A-Z]/g, letter => `_${letter.toLowerCase()}`);
|
|
47
|
+
const value = params[key];
|
|
48
|
+
result[snakeKey] = OPAQUE_PARAMETER_OBJECTS.has(key) || OPAQUE_PARAMETER_OBJECTS.has(snakeKey)
|
|
49
|
+
? value
|
|
50
|
+
: snakeCaseValue(value);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return result;
|
|
54
|
+
}
|
|
55
|
+
/** Mirrors normalizeValue: nested arrays stay arrays, so mesh buffers survive. */
|
|
56
|
+
function snakeCaseValue(value) {
|
|
57
|
+
if (Array.isArray(value))
|
|
58
|
+
return value.map(snakeCaseValue);
|
|
59
|
+
if (value && typeof value === 'object')
|
|
60
|
+
return convertCamelToSnakeCase(value);
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
export function validatePath(path) {
|
|
64
|
+
if (!path || path.includes('..')) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
/** Centralized filesystem policy for project and project-relative paths. */
|
|
70
|
+
export class PathSecurity {
|
|
71
|
+
allowedRoots;
|
|
72
|
+
supportsRealpath = Object.keys(fs).includes('realpathSync');
|
|
73
|
+
constructor(allowedRoots) {
|
|
74
|
+
const configured = allowedRoots ?? (process.env.GODOT_MCP_ALLOWED_DIRS || '')
|
|
75
|
+
.split(process.platform === 'win32' ? /[;,]/ : /[:,]/)
|
|
76
|
+
.map(value => value.trim())
|
|
77
|
+
.filter(Boolean);
|
|
78
|
+
this.allowedRoots = configured.map(root => this.realpathWithFallback(root));
|
|
79
|
+
}
|
|
80
|
+
isProjectPathAllowed(projectPath, allowMissing = false) {
|
|
81
|
+
if (!validatePath(projectPath))
|
|
82
|
+
return false;
|
|
83
|
+
if (this.allowedRoots.length === 0)
|
|
84
|
+
return true;
|
|
85
|
+
if (!allowMissing && !fs.existsSync(projectPath))
|
|
86
|
+
return false;
|
|
87
|
+
if (this.allowedRoots.some(root => resolve(projectPath) === root))
|
|
88
|
+
return true;
|
|
89
|
+
return this.isWithinAllowedRoots(this.realpathWithFallback(projectPath));
|
|
90
|
+
}
|
|
91
|
+
resolveProjectPath(projectPath, relativePath) {
|
|
92
|
+
const projectRelativePath = relativePath.startsWith('res://') ? relativePath.slice('res://'.length) : relativePath;
|
|
93
|
+
if (!this.hasRealpath() && this.allowedRoots.some(root => resolve(projectPath) === root) && validatePath(projectRelativePath)) {
|
|
94
|
+
return resolve(projectPath, projectRelativePath);
|
|
95
|
+
}
|
|
96
|
+
if (!this.supportsRealpath && validatePath(projectPath) && validateRelativePath(relativePath)) {
|
|
97
|
+
return resolve(projectPath, projectRelativePath);
|
|
98
|
+
}
|
|
99
|
+
if (!this.isProjectPathAllowed(projectPath) || !validateRelativePath(relativePath))
|
|
100
|
+
return null;
|
|
101
|
+
const projectRoot = this.realpathWithFallback(projectPath);
|
|
102
|
+
const candidate = resolve(projectRoot, projectRelativePath);
|
|
103
|
+
return this.isWithin(candidate, projectRoot) && this.isWithin(this.realpathWithFallback(candidate), projectRoot)
|
|
104
|
+
? candidate
|
|
105
|
+
: null;
|
|
106
|
+
}
|
|
107
|
+
isRelativePathAllowed(projectPath, relativePath) {
|
|
108
|
+
return this.resolveProjectPath(projectPath, relativePath) !== null;
|
|
109
|
+
}
|
|
110
|
+
isWithinAllowedRoots(target) {
|
|
111
|
+
return this.allowedRoots.length === 0 || this.allowedRoots.some(root => this.isWithin(target, root));
|
|
112
|
+
}
|
|
113
|
+
isWithin(target, root) {
|
|
114
|
+
const rel = relative(root, target);
|
|
115
|
+
return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
|
|
116
|
+
}
|
|
117
|
+
realpathWithFallback(target) {
|
|
118
|
+
const absolute = resolve(target);
|
|
119
|
+
if (fs.existsSync(absolute)) {
|
|
120
|
+
const mockedRealpath = (Object.keys(fs).includes('realpathSync'))
|
|
121
|
+
? fs.realpathSync
|
|
122
|
+
: undefined;
|
|
123
|
+
const realpath = mockedRealpath?.native ?? mockedRealpath;
|
|
124
|
+
return realpath ? realpath(absolute) : absolute;
|
|
125
|
+
}
|
|
126
|
+
const parent = resolve(absolute, '..');
|
|
127
|
+
return parent === absolute ? absolute : resolve(this.realpathWithFallback(parent), absolute.slice(parent.length + 1));
|
|
128
|
+
}
|
|
129
|
+
hasRealpath() {
|
|
130
|
+
try {
|
|
131
|
+
return typeof fs.realpathSync === 'function';
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
function validateRelativePath(path) {
|
|
139
|
+
return Boolean(path) && !isAbsolute(path) && validatePath(path);
|
|
140
|
+
}
|
|
141
|
+
export function createErrorResponse(message) {
|
|
142
|
+
console.error(`[SERVER] Error response: ${message}`);
|
|
143
|
+
return {
|
|
144
|
+
content: [
|
|
145
|
+
{
|
|
146
|
+
type: 'text',
|
|
147
|
+
text: message,
|
|
148
|
+
},
|
|
149
|
+
],
|
|
150
|
+
isError: true,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
export function isGodot44OrLater(version) {
|
|
154
|
+
const match = /^(\d+)\.(\d+)/.exec(version);
|
|
155
|
+
if (match) {
|
|
156
|
+
const major = parseInt(match[1], 10);
|
|
157
|
+
const minor = parseInt(match[2], 10);
|
|
158
|
+
return major > 4 || (major === 4 && minor >= 4);
|
|
159
|
+
}
|
|
160
|
+
return false;
|
|
161
|
+
}
|
|
162
|
+
export function parseGodotScriptDiagnostics(output) {
|
|
163
|
+
const lines = (output || '').split(/\r?\n/);
|
|
164
|
+
const diagnostics = [];
|
|
165
|
+
const locRe = /\((res:\/\/.+):(\d+)\)/;
|
|
166
|
+
for (let i = 0; i < lines.length; i++) {
|
|
167
|
+
const m = /SCRIPT ERROR:\s*(.+?)\s*$/.exec(lines[i]);
|
|
168
|
+
if (!m)
|
|
169
|
+
continue;
|
|
170
|
+
const message = m[1].replace(/^Parse Error:\s*/, '');
|
|
171
|
+
let file;
|
|
172
|
+
let line;
|
|
173
|
+
for (const j of [i + 1, i]) {
|
|
174
|
+
if (j >= lines.length)
|
|
175
|
+
continue;
|
|
176
|
+
const loc = locRe.exec(lines[j]);
|
|
177
|
+
if (loc) {
|
|
178
|
+
file = loc[1];
|
|
179
|
+
line = parseInt(loc[2], 10);
|
|
180
|
+
break;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
diagnostics.push({ message, ...(file ? { file } : {}), ...(line !== undefined ? { line } : {}) });
|
|
184
|
+
}
|
|
185
|
+
return diagnostics;
|
|
186
|
+
}
|
|
187
|
+
export function collectGdPaths(outputs) {
|
|
188
|
+
const result = [];
|
|
189
|
+
const seen = new Set();
|
|
190
|
+
for (const out of outputs) {
|
|
191
|
+
for (const rawLine of (out || '').split(/\r?\n/)) {
|
|
192
|
+
const filePath = rawLine.trim().replace(/\\/g, '/');
|
|
193
|
+
if (!/\.gd$/i.test(filePath))
|
|
194
|
+
continue;
|
|
195
|
+
if (!seen.has(filePath)) {
|
|
196
|
+
seen.add(filePath);
|
|
197
|
+
result.push(filePath);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return result;
|
|
202
|
+
}
|
|
203
|
+
export const DEFAULT_GODOT_NET_SDK_VERSION = '4.4.0';
|
|
204
|
+
export const DEFAULT_DOTNET_TARGET_FRAMEWORK = 'net8.0';
|
|
205
|
+
export function toDotnetIdentifier(name) {
|
|
206
|
+
const cleaned = (name || '').replace(/[^A-Za-z0-9_]/g, '_');
|
|
207
|
+
if (cleaned.length === 0)
|
|
208
|
+
return 'Game';
|
|
209
|
+
return /^[0-9]/.test(cleaned) ? '_' + cleaned : cleaned;
|
|
210
|
+
}
|
|
211
|
+
export function toDotnetNamespace(name) {
|
|
212
|
+
return (name || '')
|
|
213
|
+
.split('.')
|
|
214
|
+
.map(seg => toDotnetIdentifier(seg))
|
|
215
|
+
.join('.');
|
|
216
|
+
}
|
|
217
|
+
export function isValidCsharpIdentifier(name) {
|
|
218
|
+
return /^[A-Za-z_][A-Za-z0-9_]*$/.test(name);
|
|
219
|
+
}
|
|
220
|
+
export function generateGodotProjectFeatures(isDotnet, version = '4.4') {
|
|
221
|
+
return isDotnet
|
|
222
|
+
? `PackedStringArray("${version}", "C#")`
|
|
223
|
+
: `PackedStringArray("${version}")`;
|
|
224
|
+
}
|
|
225
|
+
export function generateCsprojContent(projectName, sdkVersion = DEFAULT_GODOT_NET_SDK_VERSION, targetFramework = DEFAULT_DOTNET_TARGET_FRAMEWORK) {
|
|
226
|
+
const rootNamespace = toDotnetIdentifier(projectName);
|
|
227
|
+
return `<Project Sdk="Godot.NET.Sdk/${sdkVersion}">
|
|
228
|
+
<PropertyGroup>
|
|
229
|
+
<TargetFramework>${targetFramework}</TargetFramework>
|
|
230
|
+
<EnableDynamicLoading>true</EnableDynamicLoading>
|
|
231
|
+
<Nullable>enable</Nullable>
|
|
232
|
+
<RootNamespace>${rootNamespace}</RootNamespace>
|
|
233
|
+
</PropertyGroup>
|
|
234
|
+
</Project>
|
|
235
|
+
`;
|
|
236
|
+
}
|
|
237
|
+
const CSHARP_GODOT_OVERRIDES = {
|
|
238
|
+
_Ready: 'public override void _Ready()',
|
|
239
|
+
_Process: 'public override void _Process(double delta)',
|
|
240
|
+
_PhysicsProcess: 'public override void _PhysicsProcess(double delta)',
|
|
241
|
+
_Input: 'public override void _Input(InputEvent @event)',
|
|
242
|
+
_UnhandledInput: 'public override void _UnhandledInput(InputEvent @event)',
|
|
243
|
+
_EnterTree: 'public override void _EnterTree()',
|
|
244
|
+
_ExitTree: 'public override void _ExitTree()',
|
|
245
|
+
};
|
|
246
|
+
export function generateCsharpScriptSource(opts) {
|
|
247
|
+
const className = toDotnetIdentifier(opts.className);
|
|
248
|
+
const baseClass = (opts.baseClass?.trim()) || 'Node';
|
|
249
|
+
const indent = opts.namespaceName ? '\t\t' : '\t';
|
|
250
|
+
const bodyIndent = indent + '\t';
|
|
251
|
+
const methodBlocks = [];
|
|
252
|
+
const seenMethods = new Set();
|
|
253
|
+
for (const raw of opts.methods || []) {
|
|
254
|
+
const name = String(raw).trim();
|
|
255
|
+
if (!name || seenMethods.has(name))
|
|
256
|
+
continue;
|
|
257
|
+
seenMethods.add(name);
|
|
258
|
+
const signature = CSHARP_GODOT_OVERRIDES[name] || `public void ${toDotnetIdentifier(name)}()`;
|
|
259
|
+
methodBlocks.push(`${indent}${signature}\n${indent}{\n${bodyIndent}\n${indent}}`);
|
|
260
|
+
}
|
|
261
|
+
const body = methodBlocks.length > 0 ? methodBlocks.join('\n\n') : indent;
|
|
262
|
+
const classIndent = opts.namespaceName ? '\t' : '';
|
|
263
|
+
const classBlock = `${classIndent}public partial class ${className} : ${baseClass}\n` +
|
|
264
|
+
`${classIndent}{\n` +
|
|
265
|
+
`${body}\n` +
|
|
266
|
+
`${classIndent}}`;
|
|
267
|
+
const lines = ['using Godot;', ''];
|
|
268
|
+
if (opts.namespaceName) {
|
|
269
|
+
lines.push(`namespace ${toDotnetNamespace(opts.namespaceName)};`, '');
|
|
270
|
+
}
|
|
271
|
+
lines.push(classBlock, '');
|
|
272
|
+
return lines.join('\n');
|
|
273
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@beremaran/godot-agent-loop",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Evidence-first MCP automation loop with 167 tested tools for Godot 4",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"godot",
|
|
7
|
+
"mcp",
|
|
8
|
+
"ai",
|
|
9
|
+
"claude",
|
|
10
|
+
"cline",
|
|
11
|
+
"game-engine",
|
|
12
|
+
"gdscript",
|
|
13
|
+
"runtime",
|
|
14
|
+
"agent-workflows",
|
|
15
|
+
"evidence-first",
|
|
16
|
+
"model-context-protocol",
|
|
17
|
+
"pi-package"
|
|
18
|
+
],
|
|
19
|
+
"homepage": "https://github.com/beremaran/godot-agent-loop",
|
|
20
|
+
"bugs": {
|
|
21
|
+
"url": "https://github.com/beremaran/godot-agent-loop/issues"
|
|
22
|
+
},
|
|
23
|
+
"repository": {
|
|
24
|
+
"type": "git",
|
|
25
|
+
"url": "https://github.com/beremaran/godot-agent-loop.git"
|
|
26
|
+
},
|
|
27
|
+
"mcpName": "io.github.beremaran/godot-agent-loop",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"author": "Berke Arslan",
|
|
30
|
+
"contributors": [
|
|
31
|
+
"Tuğcan Topaloğlu (https://github.com/tugcantopaloglu)",
|
|
32
|
+
"Solomon Elias <solomon@sippy.ai> (https://github.com/Coding-Solo)"
|
|
33
|
+
],
|
|
34
|
+
"type": "module",
|
|
35
|
+
"bin": {
|
|
36
|
+
"godot-agent-loop": "./build/index.js"
|
|
37
|
+
},
|
|
38
|
+
"files": [
|
|
39
|
+
"build",
|
|
40
|
+
"agent-plugin",
|
|
41
|
+
"addons/godot_agent_loop",
|
|
42
|
+
"product.json",
|
|
43
|
+
"scripts/prepare-package.js"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "tsc && node scripts/sync-product-metadata.js && node scripts/sync-agent-adapters.js && node scripts/build.js",
|
|
47
|
+
"product:sync": "node scripts/sync-product-metadata.js --write && npm run adapters:sync",
|
|
48
|
+
"product:check": "node scripts/sync-product-metadata.js",
|
|
49
|
+
"adapters:sync": "node scripts/sync-agent-adapters.js --write && npm install --package-lock-only --ignore-scripts",
|
|
50
|
+
"adapters:check": "node scripts/sync-agent-adapters.js",
|
|
51
|
+
"inspector": "npx @modelcontextprotocol/inspector build/index.js",
|
|
52
|
+
"prepare": "node scripts/prepare-package.js",
|
|
53
|
+
"check": "npm test && npm run lint && npm run coverage:check",
|
|
54
|
+
"coverage:report": "npm run build && node scripts/generate-coverage-report.js",
|
|
55
|
+
"coverage:check": "npm run build && node scripts/generate-coverage-report.js --check && npm run coverage:engine -- --check",
|
|
56
|
+
"coverage:engine": "node scripts/engine-surface-audit.js",
|
|
57
|
+
"benchmark:loop": "npm run build && node scripts/benchmark-authoring-loop.mjs && npm run coverage:report",
|
|
58
|
+
"watch": "tsc --watch",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"test:watch": "vitest",
|
|
61
|
+
"test:godot": "npm run test:godot:typecheck && npm run test:godot:validate-script && npm run test:godot:operations && npm run test:godot:runtime && npm run test:godot:launch-demo",
|
|
62
|
+
"test:godot:typecheck": "tests/godot/run-typecheck.sh",
|
|
63
|
+
"test:godot:validate-script": "tests/godot/run-validate-script.sh",
|
|
64
|
+
"test:godot:operations": "tests/godot/run-headless-operations.sh",
|
|
65
|
+
"test:godot:runtime": "tests/godot/run-integration-tests.sh",
|
|
66
|
+
"test:godot:launch-demo": "tests/godot/run-launch-demo.sh",
|
|
67
|
+
"lint": "eslint . && npm run lint:md",
|
|
68
|
+
"lint:md": "markdownlint-cli2 \"**/*.md\" \"#node_modules\"",
|
|
69
|
+
"lint:md:fix": "markdownlint-cli2 --fix \"**/*.md\" \"#node_modules\"",
|
|
70
|
+
"test:e2e": "npm run build && vitest run --config vitest.e2e.config.ts",
|
|
71
|
+
"test:golden-agent": "npm run build && vitest run --config vitest.e2e.config.ts tests/e2e/golden-agent-game.test.ts",
|
|
72
|
+
"assetlib:pack": "node scripts/build-assetlib-archive.js",
|
|
73
|
+
"assetlib:preview": "node scripts/capture-addon-preview.js",
|
|
74
|
+
"assetlib:submission": "node scripts/build-assetlib-submission.js",
|
|
75
|
+
"launch:video": "node scripts/build-launch-video.js",
|
|
76
|
+
"release:candidate": "node scripts/build-release-candidate.js"
|
|
77
|
+
},
|
|
78
|
+
"dependencies": {
|
|
79
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
80
|
+
"jsonc-parser": "^3.3.1",
|
|
81
|
+
"pngjs": "^7.0.0"
|
|
82
|
+
},
|
|
83
|
+
"devDependencies": {
|
|
84
|
+
"@eslint/js": "^10.0.1",
|
|
85
|
+
"@types/node": "^20.19.43",
|
|
86
|
+
"@types/pngjs": "^6.0.5",
|
|
87
|
+
"@types/yazl": "^3.3.1",
|
|
88
|
+
"@typescript-eslint/eslint-plugin": "^8.63.0",
|
|
89
|
+
"@typescript-eslint/parser": "^8.63.0",
|
|
90
|
+
"@vitest/coverage-v8": "^4.1.10",
|
|
91
|
+
"eslint": "^10.7.0",
|
|
92
|
+
"husky": "^9.1.7",
|
|
93
|
+
"markdownlint-cli2": "^0.23.0",
|
|
94
|
+
"typescript": "^5.3.3",
|
|
95
|
+
"typescript-eslint": "^8.63.0",
|
|
96
|
+
"vitest": "^4.1.10",
|
|
97
|
+
"yazl": "^3.3.1"
|
|
98
|
+
},
|
|
99
|
+
"engines": {
|
|
100
|
+
"node": ">=18.0.0"
|
|
101
|
+
},
|
|
102
|
+
"pi": {
|
|
103
|
+
"extensions": [
|
|
104
|
+
"./agent-plugin/pi/extension.ts"
|
|
105
|
+
],
|
|
106
|
+
"skills": [
|
|
107
|
+
"./agent-plugin/skills"
|
|
108
|
+
]
|
|
109
|
+
}
|
|
110
|
+
}
|
package/product.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "./docs/product.schema.json",
|
|
3
|
+
"schemaVersion": 1,
|
|
4
|
+
"name": "Godot Agent Loop",
|
|
5
|
+
"tagline": "Build it. Play it. Prove it.",
|
|
6
|
+
"category": "An evidence-first MCP automation loop for Godot 4",
|
|
7
|
+
"description": "Evidence-first MCP automation loop with 167 tested tools for Godot 4",
|
|
8
|
+
"version": "1.0.0",
|
|
9
|
+
"repository": {
|
|
10
|
+
"owner": "beremaran",
|
|
11
|
+
"name": "godot-agent-loop",
|
|
12
|
+
"url": "https://github.com/beremaran/godot-agent-loop",
|
|
13
|
+
"issues": "https://github.com/beremaran/godot-agent-loop/issues"
|
|
14
|
+
},
|
|
15
|
+
"npm": {
|
|
16
|
+
"package": "@beremaran/godot-agent-loop",
|
|
17
|
+
"binary": "godot-agent-loop"
|
|
18
|
+
},
|
|
19
|
+
"mcp": {
|
|
20
|
+
"registryName": "io.github.beremaran/godot-agent-loop",
|
|
21
|
+
"serverName": "godot-agent-loop",
|
|
22
|
+
"title": "Godot Agent Loop"
|
|
23
|
+
},
|
|
24
|
+
"agentBundle": {
|
|
25
|
+
"root": "agent-plugin",
|
|
26
|
+
"pluginName": "godot-agent-loop",
|
|
27
|
+
"serverKey": "godot-agent-loop"
|
|
28
|
+
},
|
|
29
|
+
"addon": {
|
|
30
|
+
"directory": "addons/godot_agent_loop",
|
|
31
|
+
"name": "Godot Agent Loop Bridge",
|
|
32
|
+
"category": "Addons/Tools",
|
|
33
|
+
"minimumGodotVersion": "4.4",
|
|
34
|
+
"primaryGodotVersion": "4.7",
|
|
35
|
+
"protocolVersion": "1",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"description": "Authenticated editor companion with live agent activity, compatibility status, setup help, and human Pause/Resume control.",
|
|
38
|
+
"icon": "assets/previews/assetlib-icon.png",
|
|
39
|
+
"previews": [
|
|
40
|
+
"assets/previews/assetlib-editor-overview.png",
|
|
41
|
+
"assets/previews/assetlib-editor-dock.png"
|
|
42
|
+
]
|
|
43
|
+
},
|
|
44
|
+
"observability": {
|
|
45
|
+
"serverComponent": "godot-agent-loop-server",
|
|
46
|
+
"runtimeComponent": "godot-agent-loop-runtime"
|
|
47
|
+
},
|
|
48
|
+
"lineage": {
|
|
49
|
+
"originalRepository": "https://github.com/Coding-Solo/godot-mcp",
|
|
50
|
+
"inheritedRepository": "https://github.com/tugcantopaloglu/godot-mcp"
|
|
51
|
+
}
|
|
52
|
+
}
|