@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.
Files changed (70) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +925 -0
  3. package/addons/godot_agent_loop/LICENSE +23 -0
  4. package/addons/godot_agent_loop/README.md +31 -0
  5. package/addons/godot_agent_loop/plugin.cfg +8 -0
  6. package/addons/godot_agent_loop/plugin.gd +417 -0
  7. package/agent-plugin/.claude-plugin/plugin.json +19 -0
  8. package/agent-plugin/.codex-plugin/plugin.json +37 -0
  9. package/agent-plugin/.mcp.json +14 -0
  10. package/agent-plugin/adapter-manifest.json +45 -0
  11. package/agent-plugin/pi/extension.ts +136 -0
  12. package/agent-plugin/skills/build-godot-game/SKILL.md +41 -0
  13. package/agent-plugin/skills/debug-godot-game/SKILL.md +37 -0
  14. package/agent-plugin/skills/ship-godot-game/SKILL.md +46 -0
  15. package/agent-plugin/skills/ship-godot-game/agents/openai.yaml +4 -0
  16. package/agent-plugin/skills/verify-godot-change/SKILL.md +35 -0
  17. package/build/authoring-session-manager.js +237 -0
  18. package/build/domain-tool-registries.js +207 -0
  19. package/build/editor-bridge-protocol.js +1 -0
  20. package/build/editor-connection.js +112 -0
  21. package/build/editor-mutation-guard.js +30 -0
  22. package/build/editor-plugin-installer.js +220 -0
  23. package/build/engine-api/extension_api-4.7.stable.official.5b4e0cb0f.json +356830 -0
  24. package/build/game-command-service.js +49 -0
  25. package/build/game-connection.js +337 -0
  26. package/build/godot-executable.js +156 -0
  27. package/build/godot-process-manager.js +112 -0
  28. package/build/godot-subprocess.js +23 -0
  29. package/build/headless-operation-runner.js +68 -0
  30. package/build/headless-operation-service.js +65 -0
  31. package/build/index.js +478 -0
  32. package/build/interaction-server-installer.js +234 -0
  33. package/build/opencode-setup.js +199 -0
  34. package/build/project-support.js +219 -0
  35. package/build/runtime-protocol.js +202 -0
  36. package/build/scripts/godot_operations.gd +1534 -0
  37. package/build/scripts/mcp_editor_plugin.gd +417 -0
  38. package/build/scripts/mcp_interaction_server.gd +1255 -0
  39. package/build/scripts/mcp_runtime/audio_animation_domain.gd +568 -0
  40. package/build/scripts/mcp_runtime/command_params.gd +339 -0
  41. package/build/scripts/mcp_runtime/core_domain.gd +591 -0
  42. package/build/scripts/mcp_runtime/input_domain.gd +695 -0
  43. package/build/scripts/mcp_runtime/networking_domain.gd +356 -0
  44. package/build/scripts/mcp_runtime/physics_domain.gd +653 -0
  45. package/build/scripts/mcp_runtime/privileged_command_policy.gd +81 -0
  46. package/build/scripts/mcp_runtime/rendering_domain.gd +807 -0
  47. package/build/scripts/mcp_runtime/runtime_domain.gd +108 -0
  48. package/build/scripts/mcp_runtime/scene_2d_domain.gd +527 -0
  49. package/build/scripts/mcp_runtime/scene_3d_domain.gd +573 -0
  50. package/build/scripts/mcp_runtime/system_domain.gd +277 -0
  51. package/build/scripts/mcp_runtime/ui_domain.gd +619 -0
  52. package/build/scripts/mcp_runtime/variant_codec.gd +335 -0
  53. package/build/scripts/validate_script.gd +28 -0
  54. package/build/server-instructions.js +11 -0
  55. package/build/session-timing.js +20 -0
  56. package/build/tool-argument-validation.js +120 -0
  57. package/build/tool-definitions.js +2727 -0
  58. package/build/tool-handlers/game-tool-handlers.js +1345 -0
  59. package/build/tool-handlers/lifecycle-tool-handlers.js +346 -0
  60. package/build/tool-handlers/project-handler-services.js +1458 -0
  61. package/build/tool-handlers/project-tool-handlers.js +1506 -0
  62. package/build/tool-handlers/visual-regression-service.js +139 -0
  63. package/build/tool-manifest.js +1193 -0
  64. package/build/tool-mutation-policy.js +122 -0
  65. package/build/tool-registry.js +34 -0
  66. package/build/tool-surface.js +70 -0
  67. package/build/utils.js +273 -0
  68. package/package.json +110 -0
  69. package/product.json +52 -0
  70. package/scripts/prepare-package.js +42 -0
@@ -0,0 +1,136 @@
1
+ import type { ExtensionAPI } from '@earendil-works/pi-coding-agent';
2
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
3
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
4
+ import type { Tool } from '@modelcontextprotocol/sdk/types.js';
5
+ import { existsSync, readFileSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+
9
+ const PACKAGE_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
10
+ const SERVER_ENTRY = join(PACKAGE_ROOT, 'build', 'index.js');
11
+ const ADAPTER_MANIFEST = join(PACKAGE_ROOT, 'agent-plugin', 'adapter-manifest.json');
12
+
13
+ interface PiServerLaunchOptions {
14
+ localServerEntry?: string;
15
+ adapterManifestPath?: string;
16
+ exists?: (path: string) => boolean;
17
+ }
18
+
19
+ export function resolvePiServerLaunch(options: PiServerLaunchOptions = {}): { command: string; args: string[] } {
20
+ const localServerEntry = options.localServerEntry ?? SERVER_ENTRY;
21
+ if ((options.exists ?? existsSync)(localServerEntry)) {
22
+ return { command: process.execPath, args: [localServerEntry] };
23
+ }
24
+
25
+ const manifestPath = options.adapterManifestPath ?? ADAPTER_MANIFEST;
26
+ const manifest = JSON.parse(readFileSync(manifestPath, 'utf8')) as { mcp?: { command?: unknown } };
27
+ const pinned = manifest.mcp?.command;
28
+ if (!Array.isArray(pinned) || pinned.length === 0 || !pinned.every(value => typeof value === 'string')) {
29
+ throw new Error(`Invalid MCP command in ${manifestPath}`);
30
+ }
31
+ return { command: pinned[0], args: pinned.slice(1) };
32
+ }
33
+
34
+ function inheritedEnvironment(): Record<string, string> {
35
+ const environment = Object.fromEntries(
36
+ Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined),
37
+ );
38
+ delete environment.VITEST;
39
+ environment.NODE_ENV = 'production';
40
+ return environment;
41
+ }
42
+
43
+ function resultContent(content: unknown[]): ({ type: 'text'; text: string } | { type: 'image'; data: string; mimeType: string })[] {
44
+ return content.map(item => {
45
+ const value = item as Record<string, unknown>;
46
+ if (value.type === 'text' && typeof value.text === 'string') {
47
+ return { type: 'text' as const, text: value.text };
48
+ }
49
+ if (value.type === 'image' && typeof value.data === 'string' && typeof value.mimeType === 'string') {
50
+ return { type: 'image' as const, data: value.data, mimeType: value.mimeType };
51
+ }
52
+ return { type: 'text' as const, text: JSON.stringify(value) };
53
+ });
54
+ }
55
+
56
+ function errorText(content: unknown[]): string {
57
+ return resultContent(content).map(item => item.type === 'text' ? item.text : `[image: ${item.mimeType}]`).join('\n');
58
+ }
59
+
60
+ export default function godotAgentLoopPi(pi: ExtensionAPI) {
61
+ let client: Client | undefined;
62
+
63
+ const close = async () => {
64
+ const active = client;
65
+ client = undefined;
66
+ if (active) await active.close().catch(() => undefined);
67
+ };
68
+
69
+ const registerTools = (tools: Tool[]) => {
70
+ for (const tool of tools) {
71
+ pi.registerTool({
72
+ name: tool.name,
73
+ label: tool.annotations?.title ?? tool.name,
74
+ description: tool.description ?? `Call the ${tool.name} Godot Agent Loop tool.`,
75
+ parameters: tool.inputSchema as never,
76
+ async execute(_toolCallId, params, signal) {
77
+ if (!client) throw new Error('Godot Agent Loop MCP server is not connected');
78
+ const result = await client.callTool(
79
+ { name: tool.name, arguments: params as Record<string, unknown> },
80
+ undefined,
81
+ { signal },
82
+ );
83
+ if (result.isError) throw new Error(errorText(result.content));
84
+ return {
85
+ content: resultContent(result.content),
86
+ details: {
87
+ structuredContent: result.structuredContent,
88
+ metadata: result._meta,
89
+ },
90
+ };
91
+ },
92
+ });
93
+ }
94
+ };
95
+
96
+ pi.on('session_start', async (_event, ctx) => {
97
+ await close();
98
+ try {
99
+ const launch = resolvePiServerLaunch();
100
+ const next = new Client(
101
+ { name: 'godot-agent-loop-pi', version: '1.0.0' },
102
+ {
103
+ capabilities: {},
104
+ listChanged: {
105
+ tools: {
106
+ onChanged(error, result) {
107
+ if (error) {
108
+ ctx.ui.notify(`Godot Agent Loop tool refresh failed: ${error.message}`, 'warning');
109
+ return;
110
+ }
111
+ if (result) registerTools(result.tools);
112
+ },
113
+ },
114
+ },
115
+ },
116
+ );
117
+ const transport = new StdioClientTransport({
118
+ command: launch.command,
119
+ args: launch.args,
120
+ env: { ...inheritedEnvironment(), GODOT_MCP_TOOL_SURFACE: 'compact' },
121
+ stderr: 'pipe',
122
+ });
123
+ await next.connect(transport);
124
+ client = next;
125
+ const listed = await next.listTools();
126
+ registerTools(listed.tools);
127
+ ctx.ui.notify(`Godot Agent Loop connected (${listed.tools.length} tools)`, 'info');
128
+ } catch (error) {
129
+ await close();
130
+ const message = error instanceof Error ? error.message : String(error);
131
+ ctx.ui.notify(`Godot Agent Loop failed to start: ${message}`, 'error');
132
+ }
133
+ });
134
+
135
+ pi.on('session_shutdown', close);
136
+ }
@@ -0,0 +1,41 @@
1
+ ---
2
+ name: build-godot-game
3
+ description: Build a new playable Godot 4 game or substantial gameplay slice through Godot Agent Loop. Use for requests to create a game from an empty project or project path, add a complete mechanic, or deliver a playable scene with input, visible state, and win/lose behavior.
4
+ ---
5
+
6
+ # Build a Godot game
7
+
8
+ Use the MCP tools for project changes and engine evidence. Do not add MCP
9
+ autoloads, addons, or bridge files; `run_project` installs and removes its runtime
10
+ bridge automatically.
11
+
12
+ ## Workflow
13
+
14
+ 1. Inspect before writing.
15
+ - Call `list_project_files`, `read_project_settings`, and `read_scene` when a
16
+ project exists.
17
+ - Call `create_project` only when `project.godot` is absent.
18
+ - Define the playable loop, controls, visible feedback, and objective before
19
+ choosing nodes.
20
+ 2. Author the smallest complete game.
21
+ - Use `create_scene` and `add_node` for the scene tree.
22
+ - Use `create_script` or `write_file`, then `attach_script`.
23
+ - Use `manage_input_map` for named actions and `set_main_scene` for startup.
24
+ - Use `godot_tools` `search` then `describe` when a specialized node/resource
25
+ tool is needed; invoke it with `godot_tools` `call`.
26
+ 3. Validate static artifacts.
27
+ - Call `validate_scripts` and fix every error.
28
+ - Re-read the scene and project settings rather than trusting write responses.
29
+ 4. Run and exercise the game.
30
+ - Call `run_project`, wait for connection, then inspect with
31
+ `game_get_scene_tree`, `game_get_ui`, and `game_screenshot`.
32
+ - Inject the documented controls with `game_key_press`, `game_click`, or the
33
+ relevant input tool. Observe state before and after input.
34
+ 5. Assert and finish.
35
+ - Prefer `verify_project` for bounded node/log/screenshot assertions and
36
+ `run_project_tests` for project tests.
37
+ - Prove both ordinary play and the win/lose transition independently.
38
+ - Inspect `get_debug_output` and `game_get_errors`; stop the project when done.
39
+
40
+ Keep the project tree game-only. Remove temporary probes, screenshots, and test
41
+ artifacts unless the user requested them as deliverables.
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: debug-godot-game
3
+ description: Diagnose and fix a Godot 4 game that crashes, logs errors, renders incorrectly, ignores input, has wrong scene state, or fails a gameplay expectation. Use for runtime bugs, script failures, broken scenes/resources, flaky behavior, and regressions that need reproduction before repair.
4
+ ---
5
+
6
+ # Debug a Godot game
7
+
8
+ Preserve the failing evidence, isolate one cause, make the smallest repair, and
9
+ rerun the original reproduction. Do not begin with speculative edits.
10
+
11
+ ## Workflow
12
+
13
+ 1. Reproduce.
14
+ - Call `run_project` and repeat the user's input with `game_key_press`,
15
+ `game_click`, or the relevant input tool.
16
+ - Capture `get_debug_output`, `game_get_errors`, `game_get_logs`, scene/UI
17
+ state, and a screenshot when rendering is involved.
18
+ 2. Classify the boundary.
19
+ - Parse/startup: run `validate_scripts` and inspect project/main-scene settings.
20
+ - Saved scene/resource: use `read_scene`, `read_file`, and `manage_resource`
21
+ (discover it through `godot_tools` when hidden).
22
+ - Runtime state: use `game_get_scene_tree`, `game_get_ui`, and
23
+ `game_get_node_info`.
24
+ - Timing/input: use deterministic `game_wait`, then query input or time tools
25
+ through `godot_tools` if needed.
26
+ 3. Form one falsifiable hypothesis. Gather a second observation that distinguishes
27
+ it from the nearest alternative. Use privileged property/method inspection only
28
+ when the user enabled the `reflection` group; avoid `game_eval` by default.
29
+ 4. Stop the project before editing persistent files. Apply the minimal fix with
30
+ the matching scene, script, resource, or settings tool.
31
+ 5. Validate and rerun.
32
+ - Run static validation first.
33
+ - Repeat the exact failing input and observation.
34
+ - Use `verify_project` or `run_project_tests` for stable regression evidence.
35
+ - Confirm errors are gone and adjacent behavior still works.
36
+ 6. Stop the project and remove temporary probes. Report the root cause, changed
37
+ artifact, reproduction, and independent passing evidence.
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: ship-godot-game
3
+ description: Prepare and verify a Godot 4 game for a reproducible release. Use for export readiness, project test gates, .NET/C# verification, addon and import integrity, export artifact inspection, smoke runs, release evidence, or deterministic teardown before shipping.
4
+ ---
5
+
6
+ # Ship a Godot game
7
+
8
+ Treat shipping as an evidence gate. Preserve the project until every requested
9
+ target has a reproducible artifact and a recorded independent check.
10
+
11
+ ## Workflow
12
+
13
+ 1. Establish the release boundary.
14
+ - Inspect project settings, the main scene, export presets, enabled addons,
15
+ imported assets, and repository status.
16
+ - Record target platforms, Godot version/build flavor, renderer, .NET needs,
17
+ expected artifact paths, and any signing or export-template prerequisites.
18
+ 2. Validate source and dependencies.
19
+ - Run `validate_scripts` and `analyze_project_integrity`.
20
+ - Run `manage_import_pipeline` in inspection mode and resolve missing or stale
21
+ imports without deleting source assets.
22
+ - Inspect enabled addons with `manage_addon`; do not update or remove
23
+ user-managed addons unless the user explicitly requests it.
24
+ 3. Prove project behavior.
25
+ - Discover and run project tests with `run_project_tests`.
26
+ - Use `verify_project` for bounded startup, scene, log, and screenshot
27
+ assertions. Exercise required win, lose, menu, save, or input paths.
28
+ - Inspect `game_get_errors` and debug output, then stop the project.
29
+ 4. Verify build-specific requirements.
30
+ - Run `verify_dotnet_project` for C# projects and require a successful restore
31
+ and build with the selected Godot .NET version.
32
+ - Use `verify_export_readiness` to check templates, presets, dependencies,
33
+ export output, and supported smoke execution before the final export.
34
+ 5. Inspect the artifact independently.
35
+ - Confirm the output exists at the expected path, has the expected file type
36
+ and non-zero size, and contains the required sidecar data or pack files.
37
+ - Smoke-run supported local artifacts and compare logs, exit status, and
38
+ visible behavior with the acceptance criteria.
39
+ 6. Tear down and report.
40
+ - Stop all MCP-owned projects/editors and remove only generated probes,
41
+ temporary exports, screenshots, and logs that are not deliverables.
42
+ - Recheck the project tree and processes. Report exact commands, versions,
43
+ targets, hashes or sizes, assertions, limitations, and any unverified target.
44
+
45
+ Never claim an unavailable platform, signing identity, export template, or .NET
46
+ toolchain passed. Separate locally proven artifacts from CI-only or manual gates.
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Ship Godot Game"
3
+ short_description: "Verify and prepare a Godot game release"
4
+ default_prompt: "Use $ship-godot-game to verify export readiness and prepare this Godot game for release."
@@ -0,0 +1,35 @@
1
+ ---
2
+ name: verify-godot-change
3
+ description: Verify a Godot 4 project change with static checks and independent evidence from the running game. Use after editing scenes, scripts, resources, input, UI, rendering, or gameplay, and when deciding whether a Godot task is genuinely complete.
4
+ ---
5
+
6
+ # Verify a Godot change
7
+
8
+ Turn the requested behavior into observable acceptance criteria before running
9
+ the game. A successful mutation response is not evidence that the change works.
10
+
11
+ ## Workflow
12
+
13
+ 1. Check saved state.
14
+ - Use `validate_script` or `validate_scripts` for code.
15
+ - Use `read_scene`, `read_project_settings`, or `read_file` to confirm the
16
+ intended artifact persisted.
17
+ 2. Prefer compound verification.
18
+ - Use `verify_project` for bounded startup, node/group/log assertions,
19
+ optional screenshot evidence, and deterministic teardown.
20
+ - Use `run_project_tests` with `discover` before `run` when tests exist.
21
+ 3. Exercise interactive behavior when a compound assertion is insufficient.
22
+ - Call `run_project`, then observe the baseline with `game_get_scene_tree`,
23
+ `game_get_ui`, `game_get_node_info`, and/or `game_screenshot`.
24
+ - Inject input with `game_key_press`, `game_click`, or a specialized input tool
25
+ discovered through `godot_tools`.
26
+ - Wait only as long as the behavior requires with `game_wait`, then make the
27
+ same observation again and compare the changed state.
28
+ 4. Check negative evidence.
29
+ - Inspect `game_get_errors`, `get_debug_output`, and relevant logs.
30
+ - Verify unrelated state still works when the change has regression risk.
31
+ 5. Stop the project and report the exact assertions and evidence. If any
32
+ acceptance criterion remains unobserved, say verification is incomplete.
33
+
34
+ Use privileged reflection only when the required group is already enabled; do
35
+ not replace observable gameplay evidence with `game_eval`.
@@ -0,0 +1,237 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { createServer } from 'node:net';
3
+ import { GameConnection } from './game-connection.js';
4
+ import { GodotProcessManager } from './godot-process-manager.js';
5
+ import { convertCamelToSnakeCase, errorMessage } from './utils.js';
6
+ import { deterministicSessionArguments, deterministicSessionEnvironment } from './session-timing.js';
7
+ import { RENDERING_CONTEXT_CAPABILITY } from './runtime-protocol.js';
8
+ /** A startup-only error that is safe to route to the subprocess fallback. */
9
+ export class AuthoringSessionUnavailableError extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = 'AuthoringSessionUnavailableError';
13
+ }
14
+ }
15
+ /** A headed session could not reach a real or virtual desktop renderer. */
16
+ export class RenderingContextUnavailableError extends Error {
17
+ constructor(message = 'Headed authoring session requires a reachable rendering context. Set DISPLAY or WAYLAND_DISPLAY on Linux, or run under a virtual display such as Xvfb.') {
18
+ super(message);
19
+ this.name = 'RenderingContextUnavailableError';
20
+ }
21
+ }
22
+ /** Owns one serialized, reusable authoring process for the currently edited project. */
23
+ export class AuthoringSessionManager {
24
+ options;
25
+ logDebug;
26
+ processManager;
27
+ current = null;
28
+ generation = 0;
29
+ operationTail = Promise.resolve();
30
+ constructor(options) {
31
+ this.options = options;
32
+ this.logDebug = options.logDebug ?? (() => undefined);
33
+ this.processManager = options.processManager ?? new GodotProcessManager(this.logDebug);
34
+ }
35
+ get activeProjectPath() {
36
+ return this.current?.projectPath ?? null;
37
+ }
38
+ async execute(backend, params, projectPath) {
39
+ return this.serialize(async () => {
40
+ await this.ensureSession(projectPath);
41
+ const state = this.current;
42
+ if (!state?.connection.isConnected) {
43
+ throw new AuthoringSessionUnavailableError('Authoring session did not establish its JSON-RPC connection');
44
+ }
45
+ try {
46
+ const response = await state.connection.send(backend.command, convertCamelToSnakeCase(params), 30_000);
47
+ if ('error' in response) {
48
+ const details = response.error.data === undefined ? '' : `: ${JSON.stringify(response.error.data)}`;
49
+ return {
50
+ stdout: '',
51
+ stderr: `${response.error.message}${details}`,
52
+ exitCode: 1,
53
+ signal: null,
54
+ };
55
+ }
56
+ const result = response.result;
57
+ if (!result || typeof result !== 'object') {
58
+ return {
59
+ stdout: '', stderr: 'Authoring session returned an invalid result', exitCode: 1, signal: null,
60
+ };
61
+ }
62
+ const payload = result;
63
+ if (isMutatingAuthoringCommand(backend.command, params)) {
64
+ const scenePath = affectedScenePath(params);
65
+ const focusPath = authoringFocusPath(backend.command, params);
66
+ this.options.onProjectWrite?.({
67
+ project_path: projectPath,
68
+ command: backend.command,
69
+ ...(scenePath === undefined ? {} : { scene_path: toResourcePath(scenePath) }),
70
+ ...(typeof params.resourcePath === 'string' ? { resource_path: toResourcePath(params.resourcePath) } : {}),
71
+ ...(focusPath === undefined ? {} : { focus_path: focusPath }),
72
+ });
73
+ }
74
+ return {
75
+ stdout: typeof payload.stdout === 'string' ? payload.stdout : JSON.stringify(payload),
76
+ stderr: '',
77
+ exitCode: 0,
78
+ signal: null,
79
+ };
80
+ }
81
+ catch (error) {
82
+ // Once a request may have reached Godot, retrying it through the
83
+ // subprocess can duplicate a partial mutation. Report the transport
84
+ // failure and leave recovery to the caller instead.
85
+ return {
86
+ stdout: '',
87
+ stderr: `Authoring session command failed: ${errorMessage(error)}`,
88
+ exitCode: 1,
89
+ signal: null,
90
+ };
91
+ }
92
+ });
93
+ }
94
+ stop() {
95
+ const state = this.current;
96
+ if (!state)
97
+ return;
98
+ this.current = null;
99
+ state.connection.disconnect();
100
+ this.processManager.stop();
101
+ this.options.installer.remove(state.projectPath, state.ownedInstallation);
102
+ this.logDebug(`Stopped authoring session for ${state.projectPath}`);
103
+ }
104
+ async ensureSession(projectPath) {
105
+ if (this.current?.projectPath === projectPath
106
+ && this.current.connection.isConnected
107
+ && this.processManager.active)
108
+ return;
109
+ if (this.options.canStart && !this.options.canStart()) {
110
+ throw new AuthoringSessionUnavailableError('A user-facing Godot process is active');
111
+ }
112
+ this.stop();
113
+ let ownedInstallation = false;
114
+ try {
115
+ const executable = await this.options.resolveGodotPath();
116
+ const port = await (this.options.allocatePort ?? allocateLoopbackPort)();
117
+ const secret = (this.options.secretFactory ?? (() => randomBytes(32).toString('base64url')))();
118
+ ownedInstallation = this.options.installer.install(projectPath);
119
+ const connection = (this.options.createConnection ?? ((connectionPort, authSecret) => new GameConnection({
120
+ port: connectionPort,
121
+ initialDelayMs: 0,
122
+ retryDelayMs: 50,
123
+ maxAttempts: 200,
124
+ authSecret,
125
+ log: this.logDebug,
126
+ onLifecycleEvent: this.options.onLifecycleEvent,
127
+ })))(port, secret);
128
+ const generation = ++this.generation;
129
+ const state = { projectPath, ownedInstallation, connection, generation };
130
+ this.current = state;
131
+ this.processManager.start({
132
+ executable,
133
+ args: [
134
+ ...deterministicSessionArguments(),
135
+ '--path', projectPath, '--script', this.options.operationsScriptPath, '--serve-authoring',
136
+ ],
137
+ env: {
138
+ ...deterministicSessionEnvironment(),
139
+ GODOT_MCP_RUNTIME_PORT: String(port),
140
+ GODOT_MCP_RUNTIME_SECRET: secret,
141
+ },
142
+ onExit: () => { this.handleProcessExit(generation); },
143
+ onError: error => {
144
+ this.logDebug(`Authoring session process error: ${error.message}`);
145
+ this.handleProcessExit(generation);
146
+ },
147
+ });
148
+ await connection.connect(projectPath, () => this.current === state && this.processManager.active);
149
+ if (!connection.isConnected) {
150
+ throw new RenderingContextUnavailableError('Headed authoring session exited before its renderer became reachable. Set DISPLAY or WAYLAND_DISPLAY on Linux, or run under a virtual display such as Xvfb.');
151
+ }
152
+ if (!connection.supportsCapability(RENDERING_CONTEXT_CAPABILITY)) {
153
+ throw new RenderingContextUnavailableError();
154
+ }
155
+ this.logDebug(`Started authoring session for ${projectPath} on port ${port}`);
156
+ }
157
+ catch (error) {
158
+ if (this.current)
159
+ this.stop();
160
+ else if (ownedInstallation)
161
+ this.options.installer.remove(projectPath, true);
162
+ if (error instanceof RenderingContextUnavailableError)
163
+ throw error;
164
+ throw new AuthoringSessionUnavailableError(`Could not start authoring session: ${errorMessage(error)}`);
165
+ }
166
+ }
167
+ handleProcessExit(generation) {
168
+ if (this.current?.generation !== generation)
169
+ return;
170
+ const state = this.current;
171
+ this.current = null;
172
+ state.connection.disconnect();
173
+ this.options.installer.remove(state.projectPath, state.ownedInstallation);
174
+ }
175
+ serialize(operation) {
176
+ const result = this.operationTail.then(operation, operation);
177
+ this.operationTail = result.then(() => undefined, () => undefined);
178
+ return result;
179
+ }
180
+ }
181
+ function isMutatingAuthoringCommand(command, params) {
182
+ if (command === 'authoring_get_uid' || command === 'authoring_read_scene')
183
+ return false;
184
+ if (command === 'authoring_manage_resource' || command === 'authoring_manage_theme_resource') {
185
+ return params.action !== 'read';
186
+ }
187
+ if (command === 'authoring_manage_scene_signals')
188
+ return params.action !== 'list';
189
+ return true;
190
+ }
191
+ function toResourcePath(path) {
192
+ return path.startsWith('res://') ? path : `res://${path.replace(/^\/+/, '')}`;
193
+ }
194
+ function affectedScenePath(params) {
195
+ if (typeof params.newPath === 'string')
196
+ return params.newPath;
197
+ return typeof params.scenePath === 'string' ? params.scenePath : undefined;
198
+ }
199
+ function authoringFocusPath(command, params) {
200
+ if (command === 'authoring_create_scene' || command === 'authoring_save_scene')
201
+ return '.';
202
+ if (command === 'authoring_add_node'
203
+ && typeof params.nodeName === 'string') {
204
+ const parent = typeof params.parentNodePath === 'string' ? params.parentNodePath : 'root';
205
+ const relativeParent = parent.replace(/^\/?root\/?/, '').replace(/^\.\/?/, '').replace(/\/$/, '');
206
+ return relativeParent ? `${relativeParent}/${params.nodeName}` : params.nodeName;
207
+ }
208
+ if (typeof params.nodePath !== 'string')
209
+ return undefined;
210
+ const relative = params.nodePath.replace(/^\/?root\/?/, '') || '.';
211
+ if (command !== 'authoring_remove_node')
212
+ return relative;
213
+ const slash = relative.lastIndexOf('/');
214
+ return slash < 0 ? '.' : relative.slice(0, slash);
215
+ }
216
+ /** Reserves an ephemeral loopback port long enough to learn its assigned number. */
217
+ export function allocateLoopbackPort() {
218
+ return new Promise((resolve, reject) => {
219
+ const server = createServer();
220
+ server.once('error', reject);
221
+ server.listen(0, '127.0.0.1', () => {
222
+ const address = server.address();
223
+ if (!address || typeof address === 'string') {
224
+ server.close();
225
+ reject(new Error('Could not allocate a loopback port'));
226
+ return;
227
+ }
228
+ const port = address.port;
229
+ server.close(error => {
230
+ if (error)
231
+ reject(error);
232
+ else
233
+ resolve(port);
234
+ });
235
+ });
236
+ });
237
+ }