@beremaran/godot-agent-loop 1.0.1 → 1.1.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 +59 -34
- package/addons/godot_agent_loop/README.md +26 -16
- package/addons/godot_agent_loop/plugin.cfg +3 -2
- package/addons/godot_agent_loop/plugin.gd +840 -25
- package/agent-plugin/.claude-plugin/plugin.json +1 -1
- package/agent-plugin/.codex-plugin/plugin.json +1 -1
- package/agent-plugin/.mcp.json +1 -1
- package/agent-plugin/adapter-manifest.json +3 -3
- package/agent-plugin/pi/extension.ts +1 -1
- package/agent-plugin/skills/build-godot-game/SKILL.md +26 -5
- package/agent-plugin/skills/debug-godot-game/SKILL.md +20 -7
- package/agent-plugin/skills/ship-godot-game/SKILL.md +13 -3
- package/agent-plugin/skills/verify-godot-change/SKILL.md +17 -2
- package/build/authoring-session-manager.js +24 -1
- package/build/domain-tool-registries.js +5 -1
- package/build/editor-authoring-router.js +228 -0
- package/build/editor-bridge-protocol.js +2 -1
- package/build/editor-connection.js +29 -24
- package/build/editor-mutation-guard.js +3 -1
- package/build/editor-plugin-installer.js +2 -1
- package/build/editor-session-registry.js +392 -0
- package/build/editor-sync-queue.js +135 -0
- package/build/index.js +208 -29
- package/build/lifecycle-trace.js +80 -0
- package/build/scripts/mcp_editor_plugin.gd +840 -25
- package/build/scripts/mcp_interaction_server.gd +76 -2
- package/build/scripts/mcp_runtime/core_domain.gd +2 -1
- package/build/scripts/mcp_runtime/system_domain.gd +2 -0
- package/build/server-instructions.js +5 -5
- package/build/session-timing.js +17 -1
- package/build/tool-definitions.js +99 -6
- package/build/tool-handlers/game-tool-handlers.js +21 -4
- package/build/tool-handlers/lifecycle-tool-handlers.js +361 -27
- package/build/tool-handlers/project-handler-services.js +45 -6
- package/build/tool-handlers/project-tool-handlers.js +4 -4
- package/build/tool-manifest.js +29 -1
- package/build/tool-mutation-policy.js +1 -0
- package/build/tool-surface.js +4 -4
- package/build/utils.js +2 -2
- package/package.json +4 -3
- package/product.json +7 -5
|
@@ -875,8 +875,8 @@ func _cmd_pause(params: Dictionary) -> void:
|
|
|
875
875
|
# --- Get Performance ---
|
|
876
876
|
func _cmd_get_performance(params: Dictionary) -> void:
|
|
877
877
|
var action: String = str(params.get("action", "sample"))
|
|
878
|
-
if not ["sample", "start", "stop", "report", "leaks"].has(action):
|
|
879
|
-
_send_response({"error": "action must be sample, start, stop, report, or leaks"})
|
|
878
|
+
if not ["sample", "start", "stop", "report", "stress", "leaks"].has(action):
|
|
879
|
+
_send_response({"error": "action must be sample, start, stop, report, stress, or leaks"})
|
|
880
880
|
return
|
|
881
881
|
if action == "start":
|
|
882
882
|
_profile_active = true
|
|
@@ -911,6 +911,16 @@ func _cmd_get_performance(params: Dictionary) -> void:
|
|
|
911
911
|
result["samples"] = samples
|
|
912
912
|
result["profiling"] = _profile_active
|
|
913
913
|
result["requested_sample_count"] = sample_count
|
|
914
|
+
result["timing_mode"] = OS.get_environment("GODOT_MCP_TIMING_MODE") if not OS.get_environment("GODOT_MCP_TIMING_MODE").is_empty() else "external"
|
|
915
|
+
result["distribution"] = _performance_distribution(samples)
|
|
916
|
+
result["metric_availability"] = {
|
|
917
|
+
"realtime_fps": {"available": true, "metric": "fps"},
|
|
918
|
+
"process_time": {"available": true, "metric": "process_time_ms"},
|
|
919
|
+
"rendering_time": {"available": true, "metric": "render_setup_cpu_ms", "scope": "CPU render setup only"},
|
|
920
|
+
"gpu_time": {"available": false, "reason": "Renderer/platform does not expose a stable GPU frame timer through the public runtime API"},
|
|
921
|
+
}
|
|
922
|
+
if action == "stress":
|
|
923
|
+
result["stress_window"] = _stress_comparison(samples)
|
|
914
924
|
if action == "leaks":
|
|
915
925
|
result["leak_diagnostics"] = {
|
|
916
926
|
"object_count": result.get("object_count", 0),
|
|
@@ -926,7 +936,11 @@ func _performance_snapshot() -> Dictionary:
|
|
|
926
936
|
return {
|
|
927
937
|
"fps": Performance.get_monitor(Performance.TIME_FPS),
|
|
928
938
|
"frame_time": Performance.get_monitor(Performance.TIME_PROCESS),
|
|
939
|
+
"process_time_ms": Performance.get_monitor(Performance.TIME_PROCESS) * 1000.0,
|
|
929
940
|
"physics_frame_time": Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS),
|
|
941
|
+
"physics_process_time_ms": Performance.get_monitor(Performance.TIME_PHYSICS_PROCESS) * 1000.0,
|
|
942
|
+
"render_setup_cpu_ms": RenderingServer.get_frame_setup_time_cpu() * 1000.0,
|
|
943
|
+
"gpu_time_ms": null,
|
|
930
944
|
"memory_static": Performance.get_monitor(Performance.MEMORY_STATIC),
|
|
931
945
|
"memory_static_max": Performance.get_monitor(Performance.MEMORY_STATIC_MAX),
|
|
932
946
|
"object_count": Performance.get_monitor(Performance.OBJECT_COUNT),
|
|
@@ -954,15 +968,75 @@ func _profile_report() -> Dictionary:
|
|
|
954
968
|
"latest_object_count": latest.get("object_count", 0),
|
|
955
969
|
"latest_orphan_node_count": latest.get("object_orphan_node_count", 0),
|
|
956
970
|
}
|
|
971
|
+
report["distribution"] = _performance_distribution(_profile_samples)
|
|
972
|
+
report["metric_availability"] = {
|
|
973
|
+
"gpu_time": {"available": false, "reason": "Unavailable from the public runtime API on this renderer/platform"},
|
|
974
|
+
"render_setup_cpu": {"available": true},
|
|
975
|
+
}
|
|
957
976
|
return report
|
|
958
977
|
|
|
959
978
|
|
|
979
|
+
func _performance_distribution(samples: Array[Dictionary]) -> Dictionary:
|
|
980
|
+
if samples.is_empty(): return {}
|
|
981
|
+
var fps_values: Array[float] = []
|
|
982
|
+
var frame_values: Array[float] = []
|
|
983
|
+
for sample: Dictionary in samples:
|
|
984
|
+
fps_values.append(_metric_float(sample.get("fps", 0.0)))
|
|
985
|
+
frame_values.append(_metric_float(sample.get("process_time_ms", 0.0)))
|
|
986
|
+
fps_values.sort()
|
|
987
|
+
frame_values.sort()
|
|
988
|
+
var percentile_index: int = mini(frame_values.size() - 1, floori(float(frame_values.size() - 1) * 0.95))
|
|
989
|
+
return {
|
|
990
|
+
"sample_count": samples.size(),
|
|
991
|
+
"fps_min": fps_values[0], "fps_max": fps_values[fps_values.size() - 1],
|
|
992
|
+
"fps_average": _array_average(fps_values),
|
|
993
|
+
"process_time_ms_min": frame_values[0], "process_time_ms_max": frame_values[frame_values.size() - 1],
|
|
994
|
+
"process_time_ms_average": _array_average(frame_values),
|
|
995
|
+
"process_time_ms_p95": frame_values[percentile_index],
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
func _stress_comparison(samples: Array[Dictionary]) -> Dictionary:
|
|
1000
|
+
if samples.size() < 3:
|
|
1001
|
+
return {"available": false, "reason": "stress comparison requires at least 3 samples"}
|
|
1002
|
+
var baseline: Dictionary = samples[0]
|
|
1003
|
+
var recovery: Dictionary = samples[samples.size() - 1]
|
|
1004
|
+
var peak_process_ms: float = 0.0
|
|
1005
|
+
var peak_objects: int = 0
|
|
1006
|
+
for sample: Dictionary in samples:
|
|
1007
|
+
peak_process_ms = maxf(peak_process_ms, _metric_float(sample.get("process_time_ms", 0.0)))
|
|
1008
|
+
peak_objects = maxi(peak_objects, _metric_int(sample.get("object_count", 0)))
|
|
1009
|
+
return {
|
|
1010
|
+
"available": true,
|
|
1011
|
+
"baseline": baseline,
|
|
1012
|
+
"peak": {"process_time_ms": peak_process_ms, "object_count": peak_objects},
|
|
1013
|
+
"recovery": recovery,
|
|
1014
|
+
"object_count_delta_recovery": _metric_int(recovery.get("object_count", 0)) - _metric_int(baseline.get("object_count", 0)),
|
|
1015
|
+
"process_time_ms_delta_recovery": _metric_float(recovery.get("process_time_ms", 0.0)) - _metric_float(baseline.get("process_time_ms", 0.0)),
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
|
|
1019
|
+
func _array_average(values: Array[float]) -> float:
|
|
1020
|
+
var total: float = 0.0
|
|
1021
|
+
for value: float in values: total += value
|
|
1022
|
+
return total / values.size()
|
|
1023
|
+
|
|
1024
|
+
|
|
960
1025
|
func _metric_float(value: Variant) -> float:
|
|
961
1026
|
if value is int or value is float:
|
|
962
1027
|
return value
|
|
963
1028
|
return 0.0
|
|
964
1029
|
|
|
965
1030
|
|
|
1031
|
+
func _metric_int(value: Variant) -> int:
|
|
1032
|
+
if value is int:
|
|
1033
|
+
return value
|
|
1034
|
+
if value is float:
|
|
1035
|
+
var float_value: float = value
|
|
1036
|
+
return roundi(float_value)
|
|
1037
|
+
return 0
|
|
1038
|
+
|
|
1039
|
+
|
|
966
1040
|
# --- Wait N Frames ---
|
|
967
1041
|
func _cmd_wait(params: Dictionary) -> void:
|
|
968
1042
|
var reader := CommandParams.new(params)
|
|
@@ -35,7 +35,8 @@ func _cmd_get_scene_tree(params: Dictionary) -> void:
|
|
|
35
35
|
return
|
|
36
36
|
var traversal: Dictionary = {"count": 0, "truncated": false, "max_nodes": max_nodes}
|
|
37
37
|
var tree: Dictionary = _build_tree_node(get_tree().root, traversal)
|
|
38
|
-
respond({"success": true, "tree": tree, "node_count": traversal["count"], "truncated": traversal["truncated"], "max_nodes": max_nodes
|
|
38
|
+
respond({"success": true, "tree": tree, "node_count": traversal["count"], "truncated": traversal["truncated"], "max_nodes": max_nodes,
|
|
39
|
+
"current_scene": "" if get_tree().current_scene == null else get_tree().current_scene.scene_file_path})
|
|
39
40
|
|
|
40
41
|
func _build_tree_node(node: Node, traversal: Dictionary) -> Dictionary:
|
|
41
42
|
var count_value: Variant = traversal.get("count", 0)
|
|
@@ -77,6 +77,8 @@ func _cmd_time_scale(params: Dictionary) -> void:
|
|
|
77
77
|
"success": true,
|
|
78
78
|
"time_scale": Engine.time_scale,
|
|
79
79
|
"fixed_fps": _configured_fixed_fps(),
|
|
80
|
+
"timing_mode": OS.get_environment("GODOT_MCP_TIMING_MODE") if not OS.get_environment("GODOT_MCP_TIMING_MODE").is_empty() else "external",
|
|
81
|
+
"display_pacing": OS.get_environment("GODOT_MCP_TIMING_MODE") == "realtime",
|
|
80
82
|
"ticks_msec": Time.get_ticks_msec(),
|
|
81
83
|
"fps": Engine.get_frames_per_second(),
|
|
82
84
|
})
|
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Initialization guidance is paid for in every MCP session. Keep this to the
|
|
3
3
|
* durable operating method; detailed procedures belong in plugin skills.
|
|
4
4
|
*/
|
|
5
|
-
export const SERVER_INSTRUCTIONS = `
|
|
5
|
+
export const SERVER_INSTRUCTIONS = `Use an author → run → observe → assert loop.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
2. Run with run_project, then observe through game_get_scene_tree, game_get_ui, game_screenshot, and get_debug_output.
|
|
9
|
-
3. Assert outcomes with verify_project or run_project_tests instead of assembling fragile manual checks. Stop the project when finished.
|
|
7
|
+
For watched work, call editor_session ensure and prefer editor_transaction or editor-routed tools. Report fallbacks and conflicts. Persist structure in scenes/resources and behavior in scripts; justify procedural construction.
|
|
10
8
|
|
|
11
|
-
|
|
9
|
+
Use realtime run_project when watched. Observe with scene/UI queries, screenshots, and logs. Assert with deterministic verify_project, game_wait_until, game_scenario, or run_project_tests. Stop and report diagnostics, unsupported metrics, and cleanup.
|
|
10
|
+
|
|
11
|
+
Prefer compound tools. Runtime injection and cleanup are automatic; never add MCP files. Reflection, code execution, and network are denied by default. Enable only needed GODOT_MCP_PRIVILEGED_GROUPS, or explicitly allow all with GODOT_MCP_ALLOW_PRIVILEGED_COMMANDS=true.`;
|
package/build/session-timing.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
export const GODOT_SESSION_FIXED_FPS = 60;
|
|
3
3
|
export const GODOT_SESSION_INITIAL_TIME_SCALE = 1;
|
|
4
4
|
export const GODOT_SESSION_FIXED_FPS_ENV = 'GODOT_MCP_FIXED_FPS';
|
|
5
|
+
export const GODOT_SESSION_TIMING_MODE_ENV = 'GODOT_MCP_TIMING_MODE';
|
|
5
6
|
/**
|
|
6
7
|
* Fixed FPS makes each rendered frame advance the same simulation delta.
|
|
7
8
|
* max-fps retains a wall-clock cap so an idle persistent session does not spin
|
|
@@ -16,5 +17,20 @@ export function deterministicSessionArguments() {
|
|
|
16
17
|
}
|
|
17
18
|
/** Metadata mirrored into the child because Godot strips recognized CLI flags from script-visible args. */
|
|
18
19
|
export function deterministicSessionEnvironment() {
|
|
19
|
-
return {
|
|
20
|
+
return {
|
|
21
|
+
[GODOT_SESSION_FIXED_FPS_ENV]: String(GODOT_SESSION_FIXED_FPS),
|
|
22
|
+
[GODOT_SESSION_TIMING_MODE_ENV]: 'deterministic',
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
/** Watched runs retain normal display/VSync pacing and do not force fixed simulation deltas. */
|
|
26
|
+
export function realtimeSessionArguments() {
|
|
27
|
+
return ['--time-scale', String(GODOT_SESSION_INITIAL_TIME_SCALE)];
|
|
28
|
+
}
|
|
29
|
+
export function realtimeSessionEnvironment() {
|
|
30
|
+
return { [GODOT_SESSION_FIXED_FPS_ENV]: '', [GODOT_SESSION_TIMING_MODE_ENV]: 'realtime' };
|
|
31
|
+
}
|
|
32
|
+
export function timingPolicy(mode) {
|
|
33
|
+
return mode === 'deterministic'
|
|
34
|
+
? { mode, fixed_fps: GODOT_SESSION_FIXED_FPS, max_fps: GODOT_SESSION_FIXED_FPS, time_scale: 1, display_pacing: false }
|
|
35
|
+
: { mode, fixed_fps: null, max_fps: null, time_scale: 1, display_pacing: true };
|
|
20
36
|
}
|
|
@@ -17,7 +17,7 @@ export const toolDefinitions = [
|
|
|
17
17
|
},
|
|
18
18
|
{
|
|
19
19
|
name: 'launch_editor',
|
|
20
|
-
description: '
|
|
20
|
+
description: 'Attach to an existing matching editor or launch one when needed',
|
|
21
21
|
inputSchema: {
|
|
22
22
|
type: 'object',
|
|
23
23
|
properties: {
|
|
@@ -29,6 +29,20 @@ export const toolDefinitions = [
|
|
|
29
29
|
required: ['projectPath'],
|
|
30
30
|
},
|
|
31
31
|
},
|
|
32
|
+
{
|
|
33
|
+
name: 'editor_session',
|
|
34
|
+
description: 'Discover, attach, inspect, or disconnect a per-project Godot editor session',
|
|
35
|
+
inputSchema: {
|
|
36
|
+
type: 'object',
|
|
37
|
+
properties: {
|
|
38
|
+
projectPath: { type: 'string', description: 'Godot project path' },
|
|
39
|
+
action: { type: 'string', enum: ['ensure', 'status', 'disconnect'], description: 'Session action' },
|
|
40
|
+
launchIfNeeded: { type: 'boolean', description: 'For ensure, launch an editor only after discovery finds none. Default: false' },
|
|
41
|
+
timeoutSeconds: { type: 'number', minimum: 0, maximum: 30, description: 'Bounded discovery/attach wait. Default: 2' },
|
|
42
|
+
},
|
|
43
|
+
required: ['projectPath', 'action'],
|
|
44
|
+
},
|
|
45
|
+
},
|
|
32
46
|
{
|
|
33
47
|
name: 'editor_control',
|
|
34
48
|
description: 'Inspect and edit open scenes through an authenticated editor bridge',
|
|
@@ -47,6 +61,37 @@ export const toolDefinitions = [
|
|
|
47
61
|
required: ['projectPath', 'action'],
|
|
48
62
|
},
|
|
49
63
|
},
|
|
64
|
+
{
|
|
65
|
+
name: 'editor_transaction',
|
|
66
|
+
description: 'Apply one validated compound scene edit as one editor undo step',
|
|
67
|
+
inputSchema: {
|
|
68
|
+
type: 'object',
|
|
69
|
+
properties: {
|
|
70
|
+
projectPath: { type: 'string', description: 'Godot project path whose editor is attached' },
|
|
71
|
+
scenePath: { type: 'string', description: 'Project-relative or res:// scene path' },
|
|
72
|
+
name: { type: 'string', minLength: 1, maxLength: 128, description: 'Human-readable undo action name' },
|
|
73
|
+
rootType: { type: 'string', maxLength: 128, description: 'Root node type when creating a missing scene' },
|
|
74
|
+
operations: {
|
|
75
|
+
type: 'array', minItems: 1, maxItems: 256, description: 'Ordered editor-native scene operations',
|
|
76
|
+
items: {
|
|
77
|
+
type: 'object',
|
|
78
|
+
properties: {
|
|
79
|
+
op: { type: 'string', enum: ['add_node', 'remove_node', 'rename_node', 'duplicate_node', 'reparent_node', 'set_properties', 'instantiate_scene', 'attach_script', 'assign_resource', 'save'] },
|
|
80
|
+
nodePath: { type: 'string' }, parentPath: { type: 'string' }, newParentPath: { type: 'string' },
|
|
81
|
+
nodeType: { type: 'string' }, nodeName: { type: 'string' }, name: { type: 'string' },
|
|
82
|
+
properties: { type: 'object' }, property: { type: 'string' }, value: {},
|
|
83
|
+
scenePath: { type: 'string' }, scriptPath: { type: 'string' }, resourcePath: { type: 'string' },
|
|
84
|
+
keepGlobalTransform: { type: 'boolean' },
|
|
85
|
+
},
|
|
86
|
+
required: ['op'],
|
|
87
|
+
},
|
|
88
|
+
},
|
|
89
|
+
focusPath: { type: 'string', description: 'Node to reveal after commit' },
|
|
90
|
+
save: { type: 'boolean', description: 'Save and independently reopen/read the scene. Default: true' },
|
|
91
|
+
},
|
|
92
|
+
required: ['projectPath', 'scenePath', 'name', 'operations'],
|
|
93
|
+
},
|
|
94
|
+
},
|
|
50
95
|
{
|
|
51
96
|
name: 'run_project',
|
|
52
97
|
description: 'Run the Godot project and capture output',
|
|
@@ -61,6 +106,10 @@ export const toolDefinitions = [
|
|
|
61
106
|
type: 'string',
|
|
62
107
|
description: 'Optional: Specific scene to run',
|
|
63
108
|
},
|
|
109
|
+
timingMode: {
|
|
110
|
+
type: 'string', enum: ['realtime', 'deterministic'],
|
|
111
|
+
description: 'realtime follows display/VSync; deterministic uses fixed 60 FPS. Default: realtime',
|
|
112
|
+
},
|
|
64
113
|
},
|
|
65
114
|
required: ['projectPath'],
|
|
66
115
|
},
|
|
@@ -143,6 +192,7 @@ export const toolDefinitions = [
|
|
|
143
192
|
sourcePath: { type: 'string', description: 'Existing project-relative path for rename preview' },
|
|
144
193
|
destinationPath: { type: 'string', description: 'Proposed project-relative rename destination' },
|
|
145
194
|
maxFiles: { type: 'integer', minimum: 1, maximum: 50000, description: 'Resource scan limit. Default: 10000' },
|
|
195
|
+
allowProceduralMainScene: { type: 'boolean', description: 'Suppress the trivial-main-scene warning when procedural construction is an explicit design requirement. Default: false' },
|
|
146
196
|
},
|
|
147
197
|
required: ['projectPath', 'action'],
|
|
148
198
|
},
|
|
@@ -426,10 +476,12 @@ export const toolDefinitions = [
|
|
|
426
476
|
},
|
|
427
477
|
{
|
|
428
478
|
name: 'game_screenshot',
|
|
429
|
-
description: '
|
|
479
|
+
description: 'Capture a PNG preview with dimensions, digest, and optional temp artifact',
|
|
430
480
|
inputSchema: {
|
|
431
481
|
type: 'object',
|
|
432
|
-
properties: {
|
|
482
|
+
properties: {
|
|
483
|
+
retainArtifact: { type: 'boolean', description: 'Retain a PNG in the system temp artifact directory. Default: false' },
|
|
484
|
+
},
|
|
433
485
|
required: [],
|
|
434
486
|
},
|
|
435
487
|
},
|
|
@@ -704,7 +756,7 @@ export const toolDefinitions = [
|
|
|
704
756
|
inputSchema: {
|
|
705
757
|
type: 'object',
|
|
706
758
|
properties: {
|
|
707
|
-
action: { type: 'string', enum: ['sample', 'start', 'stop', 'report', 'leaks'], description: 'Profiler action. Default: sample' },
|
|
759
|
+
action: { type: 'string', enum: ['sample', 'start', 'stop', 'report', 'stress', 'leaks'], description: 'Profiler action. Default: sample' },
|
|
708
760
|
sampleCount: { type: 'integer', minimum: 1, maximum: 120, description: 'Number of samples for a bounded session. Default: 1' },
|
|
709
761
|
},
|
|
710
762
|
required: [],
|
|
@@ -729,6 +781,47 @@ export const toolDefinitions = [
|
|
|
729
781
|
required: [],
|
|
730
782
|
},
|
|
731
783
|
},
|
|
784
|
+
{
|
|
785
|
+
name: 'game_wait_until',
|
|
786
|
+
description: 'Wait once for a bounded runtime condition and return the last observation',
|
|
787
|
+
inputSchema: {
|
|
788
|
+
type: 'object',
|
|
789
|
+
properties: {
|
|
790
|
+
projectPath: { type: 'string', description: 'Godot project path for trace correlation' },
|
|
791
|
+
condition: { type: 'string', enum: ['connection', 'node', 'property', 'signal', 'log', 'scene'], description: 'Condition kind' },
|
|
792
|
+
nodePath: { type: 'string' }, property: { type: 'string' }, value: {}, signal: { type: 'string' },
|
|
793
|
+
text: { type: 'string', maxLength: 1000 }, scenePath: { type: 'string' },
|
|
794
|
+
timeoutSeconds: { type: 'number', minimum: 0.05, maximum: 60, description: 'Maximum wait. Default: 10' },
|
|
795
|
+
pollIntervalMs: { type: 'integer', minimum: 20, maximum: 1000, description: 'Internal poll interval. Default: 100' },
|
|
796
|
+
},
|
|
797
|
+
required: ['condition'],
|
|
798
|
+
},
|
|
799
|
+
},
|
|
800
|
+
{
|
|
801
|
+
name: 'game_scenario',
|
|
802
|
+
description: 'Run bounded input, wait, assertion, screenshot, and performance steps',
|
|
803
|
+
inputSchema: {
|
|
804
|
+
type: 'object',
|
|
805
|
+
properties: {
|
|
806
|
+
projectPath: { type: 'string', description: 'Godot project path for trace correlation' },
|
|
807
|
+
name: { type: 'string', minLength: 1, maxLength: 128 },
|
|
808
|
+
timeoutSeconds: { type: 'number', minimum: 0.1, maximum: 120, description: 'Whole scenario timeout. Default: 60' },
|
|
809
|
+
steps: {
|
|
810
|
+
type: 'array', minItems: 1, maxItems: 100,
|
|
811
|
+
items: {
|
|
812
|
+
type: 'object',
|
|
813
|
+
properties: {
|
|
814
|
+
type: { type: 'string', enum: ['input', 'wait', 'observe', 'assert', 'screenshot', 'performance'] },
|
|
815
|
+
tool: { type: 'string' }, arguments: { type: 'object' }, condition: { type: 'object' },
|
|
816
|
+
label: { type: 'string', maxLength: 200 },
|
|
817
|
+
},
|
|
818
|
+
required: ['type'],
|
|
819
|
+
},
|
|
820
|
+
},
|
|
821
|
+
},
|
|
822
|
+
required: ['name', 'steps'],
|
|
823
|
+
},
|
|
824
|
+
},
|
|
732
825
|
{
|
|
733
826
|
name: 'read_scene',
|
|
734
827
|
description: 'Read scene file as JSON node tree (headless)',
|
|
@@ -2704,7 +2797,7 @@ export const toolDefinitions = [
|
|
|
2704
2797
|
projectPath: { type: 'string', description: 'Absolute path to Godot project' },
|
|
2705
2798
|
action: { type: 'string', enum: ['create', 'read'], description: 'Action: create or read' },
|
|
2706
2799
|
platforms: { type: 'array', description: 'Target platforms: windows, linux, macos, web', items: { type: 'string', enum: ['windows', 'linux', 'macos', 'web'] } },
|
|
2707
|
-
godotVersion: { type: 'string', pattern: '^4
|
|
2800
|
+
godotVersion: { type: 'string', pattern: '^4\\.(?:[7-9]|\\d{2,})(?:\\.\\d+)?(?:-stable)?$', description: 'Supported Godot 4.7+ version (e.g. 4.7-stable)' },
|
|
2708
2801
|
},
|
|
2709
2802
|
required: ['projectPath', 'action'],
|
|
2710
2803
|
},
|
|
@@ -2717,7 +2810,7 @@ export const toolDefinitions = [
|
|
|
2717
2810
|
properties: {
|
|
2718
2811
|
projectPath: { type: 'string', description: 'Absolute path to Godot project' },
|
|
2719
2812
|
action: { type: 'string', enum: ['create', 'read'], description: 'Action: create or read' },
|
|
2720
|
-
godotVersion: { type: 'string', pattern: '^4
|
|
2813
|
+
godotVersion: { type: 'string', pattern: '^4\\.(?:[7-9]|\\d{2,})(?:\\.\\d+)?(?:-stable)?$', description: 'Supported Godot 4.7+ version (e.g. 4.7-stable)' },
|
|
2721
2814
|
exportPreset: { type: 'string', pattern: '^[A-Za-z0-9][A-Za-z0-9 _./-]{0,127}$', description: 'Export preset name (letters, digits, spaces, _, ., /, and -)' },
|
|
2722
2815
|
baseImage: { type: 'string', enum: ['ubuntu:22.04', 'ubuntu:24.04'], description: 'Supported base Docker image (default: ubuntu:22.04)' },
|
|
2723
2816
|
},
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import { createErrorResponse, errorMessage, normalizeParameters } from '../utils.js';
|
|
2
2
|
import { VisualRegressionService } from './visual-regression-service.js';
|
|
3
|
+
import { createHash } from 'node:crypto';
|
|
4
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join } from 'node:path';
|
|
3
7
|
/**
|
|
4
8
|
* ParticleProcessMaterial fields the runtime reads, keyed by the camelCase name
|
|
5
9
|
* the MCP surface uses. `processMaterial` is a free-form object, so its keys are
|
|
@@ -44,7 +48,7 @@ export class GameToolHandlers {
|
|
|
44
48
|
};
|
|
45
49
|
this.visualRegression = new VisualRegressionService(context.commands);
|
|
46
50
|
}
|
|
47
|
-
async handleGameScreenshot() {
|
|
51
|
+
async handleGameScreenshot(args = {}) {
|
|
48
52
|
if (!this.context.commands.hasActiveProcess()) {
|
|
49
53
|
return createErrorResponse('No active Godot process. Use run_project first.');
|
|
50
54
|
}
|
|
@@ -57,6 +61,15 @@ export class GameToolHandlers {
|
|
|
57
61
|
return createErrorResponse(`Screenshot failed: ${response.error.message}`);
|
|
58
62
|
}
|
|
59
63
|
const result = response.result;
|
|
64
|
+
const bytes = Buffer.from(result.data ?? '', 'base64');
|
|
65
|
+
const digest = createHash('sha256').update(bytes).digest('hex');
|
|
66
|
+
let artifactPath = null;
|
|
67
|
+
if (args.retainArtifact === true && bytes.length > 0) {
|
|
68
|
+
const artifactDirectory = join(tmpdir(), 'godot-agent-loop-artifacts');
|
|
69
|
+
mkdirSync(artifactDirectory, { recursive: true });
|
|
70
|
+
artifactPath = join(artifactDirectory, `${digest}.png`);
|
|
71
|
+
writeFileSync(artifactPath, bytes);
|
|
72
|
+
}
|
|
60
73
|
return {
|
|
61
74
|
content: [
|
|
62
75
|
{
|
|
@@ -66,7 +79,11 @@ export class GameToolHandlers {
|
|
|
66
79
|
},
|
|
67
80
|
{
|
|
68
81
|
type: 'text',
|
|
69
|
-
text:
|
|
82
|
+
text: JSON.stringify({
|
|
83
|
+
captured: bytes.length > 0, width: result.width, height: result.height,
|
|
84
|
+
bytes: bytes.length, sha256: digest, artifact_path: artifactPath,
|
|
85
|
+
project_artifact: false, visual_regression_metadata: null,
|
|
86
|
+
}, null, 2),
|
|
70
87
|
},
|
|
71
88
|
],
|
|
72
89
|
};
|
|
@@ -174,8 +191,8 @@ export class GameToolHandlers {
|
|
|
174
191
|
async handleGamePerformance(args = {}) {
|
|
175
192
|
args = normalizeParameters(args || {});
|
|
176
193
|
const action = args.action ?? 'sample';
|
|
177
|
-
if (!['sample', 'start', 'stop', 'report', 'leaks'].includes(action)) {
|
|
178
|
-
return createErrorResponse('action must be sample, start, stop, report, or leaks.');
|
|
194
|
+
if (!['sample', 'start', 'stop', 'report', 'stress', 'leaks'].includes(action)) {
|
|
195
|
+
return createErrorResponse('action must be sample, start, stop, report, stress, or leaks.');
|
|
179
196
|
}
|
|
180
197
|
return this.context.gameCommand('get_performance', args, a => ({
|
|
181
198
|
action: a.action ?? 'sample', sample_count: a.sampleCount ?? a.sample_count ?? 1, sampleCount: a.sampleCount ?? a.sample_count ?? 1,
|