godot-agent-tools-mcp 0.2.0 → 0.3.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/package.json +1 -1
- package/server.mjs +694 -15
package/package.json
CHANGED
package/server.mjs
CHANGED
|
@@ -7,13 +7,67 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
import {
|
|
8
8
|
CallToolRequestSchema,
|
|
9
9
|
ListToolsRequestSchema,
|
|
10
|
+
ListResourcesRequestSchema,
|
|
11
|
+
ReadResourceRequestSchema,
|
|
10
12
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
11
13
|
import net from "node:net";
|
|
14
|
+
import fs from "node:fs";
|
|
15
|
+
import path from "node:path";
|
|
16
|
+
import os from "node:os";
|
|
12
17
|
|
|
13
18
|
const HOST = process.env.GODOT_AGENT_HOST || "127.0.0.1";
|
|
14
|
-
|
|
19
|
+
// GODOT_AGENT_PORT env var forces a specific target, bypassing session discovery.
|
|
20
|
+
// Leave unset to use the multi-session registry.
|
|
21
|
+
const FORCED_PORT = process.env.GODOT_AGENT_PORT ? parseInt(process.env.GODOT_AGENT_PORT, 10) : null;
|
|
15
22
|
const TIMEOUT_MS = parseInt(process.env.GODOT_AGENT_TIMEOUT_MS || "15000", 10);
|
|
16
23
|
|
|
24
|
+
// Multi-editor session registry — matches the plugin-side writer at
|
|
25
|
+
// <home>/.godot-agent-tools/sessions/<pid>.json. Each file describes one
|
|
26
|
+
// running Godot editor with the plugin enabled.
|
|
27
|
+
const SESSION_DIR = path.join(os.homedir(), ".godot-agent-tools", "sessions");
|
|
28
|
+
|
|
29
|
+
function isProcessAlive(pid) {
|
|
30
|
+
try {
|
|
31
|
+
// signal 0 doesn't send anything — just tests whether we can signal the pid.
|
|
32
|
+
process.kill(pid, 0);
|
|
33
|
+
return true;
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// EPERM means the process exists but we can't signal it (still "alive" for us).
|
|
36
|
+
return e.code === "EPERM";
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function listSessions() {
|
|
41
|
+
if (!fs.existsSync(SESSION_DIR)) return [];
|
|
42
|
+
let entries;
|
|
43
|
+
try { entries = fs.readdirSync(SESSION_DIR); }
|
|
44
|
+
catch { return []; }
|
|
45
|
+
const out = [];
|
|
46
|
+
for (const name of entries) {
|
|
47
|
+
if (!name.endsWith(".json")) continue;
|
|
48
|
+
const full = path.join(SESSION_DIR, name);
|
|
49
|
+
let parsed;
|
|
50
|
+
try { parsed = JSON.parse(fs.readFileSync(full, "utf8")); }
|
|
51
|
+
catch { continue; }
|
|
52
|
+
if (!parsed.pid || !parsed.port) continue;
|
|
53
|
+
const alive = isProcessAlive(parsed.pid);
|
|
54
|
+
if (!alive) {
|
|
55
|
+
// Clean up a stale entry opportunistically — plugin _exit_tree didn't fire
|
|
56
|
+
// (probably editor crashed or was killed).
|
|
57
|
+
try { fs.unlinkSync(full); } catch {}
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
out.push(parsed);
|
|
61
|
+
}
|
|
62
|
+
// Most recently started first.
|
|
63
|
+
out.sort((a, b) => (b.started_at_unix || 0) - (a.started_at_unix || 0));
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Tracks which session the shim is currently forwarding tool calls to. Starts
|
|
68
|
+
// unset; first call resolves via listSessions() or FORCED_PORT.
|
|
69
|
+
let activeSessionPid = null;
|
|
70
|
+
|
|
17
71
|
const TOOLS = [
|
|
18
72
|
{
|
|
19
73
|
name: "scene_inspect",
|
|
@@ -125,7 +179,22 @@ const TOOLS = [
|
|
|
125
179
|
name: "scene_set_property",
|
|
126
180
|
method: "scene.set_property",
|
|
127
181
|
description:
|
|
128
|
-
"Set a property on a node.
|
|
182
|
+
"Set a property on a node. JSON-to-Godot coercion: " +
|
|
183
|
+
"primitives (bool/int/float/String/StringName) pass through; " +
|
|
184
|
+
"NodePath from a string; " +
|
|
185
|
+
"Vector2/2i/3/3i/4/4i from [x, y(, z(, w))]; " +
|
|
186
|
+
"Rect2/2i from [x, y, width, height]; " +
|
|
187
|
+
"Quaternion from [x, y, z, w]; " +
|
|
188
|
+
"Color from [r, g, b(, a)] or '#rrggbb(aa)'; " +
|
|
189
|
+
"Transform2D from {origin: [x,y], rotation: radians, scale: [x,y], skew?}; " +
|
|
190
|
+
"Transform3D from {origin: [x,y,z], rotation: [x,y,z] (euler rad), scale: [x,y,z]}; " +
|
|
191
|
+
"Basis from {rotation, scale} (same shape as Transform3D minus origin); " +
|
|
192
|
+
"AABB from {position: [x,y,z], size: [x,y,z]}; " +
|
|
193
|
+
"Plane from {normal: [x,y,z], d: float}; " +
|
|
194
|
+
"Resource-typed properties (e.g. a CollisionShape2D's 'shape') auto-load from 'res://...' or 'uid://...' path strings; " +
|
|
195
|
+
"Packed{String,Int32,Int64,Float32,Float64,Vector2,Vector3,Color}Array from plain JSON arrays (element-wise coerced); " +
|
|
196
|
+
"other types pass through as-is. " +
|
|
197
|
+
"Returns an error if Godot drops the assignment (e.g. type mismatch) instead of echoing a misleading null.",
|
|
129
198
|
inputSchema: {
|
|
130
199
|
type: "object",
|
|
131
200
|
required: ["node_path", "property"],
|
|
@@ -150,6 +219,56 @@ const TOOLS = [
|
|
|
150
219
|
},
|
|
151
220
|
},
|
|
152
221
|
},
|
|
222
|
+
{
|
|
223
|
+
name: "scene_build_tree",
|
|
224
|
+
method: "scene.build_tree",
|
|
225
|
+
description:
|
|
226
|
+
"Build a subtree in the currently-edited scene in one call — recursive spec instead of dozens of scene_add_node + scene_set_property + script_attach round trips. " +
|
|
227
|
+
"Each tree entry: {type: 'ClassName' (required), name?: string, properties?: {propname: value, ...}, script?: 'res://...', children?: [entry, ...]}. " +
|
|
228
|
+
"Properties are coerced via the same rules as scene_set_property (Vectors from arrays, Resources auto-loaded from res:// paths, Transforms from {origin,rotation,scale}, etc.). " +
|
|
229
|
+
"Script is attached before properties so script-exported vars are settable in the same call. " +
|
|
230
|
+
"Atomic: if any entry fails (unknown type, missing property, coercion error, read-only slot, silent-null assignment), every node created during this call is rolled back — the scene is left in its pre-call state.",
|
|
231
|
+
inputSchema: {
|
|
232
|
+
type: "object",
|
|
233
|
+
required: ["nodes"],
|
|
234
|
+
properties: {
|
|
235
|
+
parent_path: { type: "string", default: ".", description: "Where to attach the new subtree. '.' = scene root." },
|
|
236
|
+
nodes: {
|
|
237
|
+
type: "array",
|
|
238
|
+
description: "Top-level tree entries (each may have arbitrarily nested children).",
|
|
239
|
+
items: {
|
|
240
|
+
type: "object",
|
|
241
|
+
required: ["type"],
|
|
242
|
+
properties: {
|
|
243
|
+
type: { type: "string", description: "Godot class name (e.g. 'PanelContainer', 'Button')." },
|
|
244
|
+
name: { type: "string" },
|
|
245
|
+
script: { type: "string", description: "Path to a .gd to attach before properties are applied." },
|
|
246
|
+
properties: { type: "object", additionalProperties: true, description: "Property → value pairs; values use the scene_set_property coercion rules." },
|
|
247
|
+
children: { type: "array", description: "Nested entries using the same shape as this one." },
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
},
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
name: "scene_call_method",
|
|
256
|
+
method: "scene.call_method",
|
|
257
|
+
description:
|
|
258
|
+
"Invoke a method on a node in the currently-edited scene (e.g. a custom helper, or Godot built-ins like 'queue_free', 'add_to_group'). Args are coerced against the method's declared parameter types using the same rules as scene_set_property — including 'res://...' → Resource auto-load. Return value is JSON-native. Useful when properties alone can't express what you need — e.g. calling a script method with multiple args.",
|
|
259
|
+
inputSchema: {
|
|
260
|
+
type: "object",
|
|
261
|
+
required: ["node_path", "method"],
|
|
262
|
+
properties: {
|
|
263
|
+
node_path: { type: "string" },
|
|
264
|
+
method: { type: "string" },
|
|
265
|
+
args: {
|
|
266
|
+
type: "array",
|
|
267
|
+
description: "Method arguments in order. Each is coerced based on the method's declared parameter type.",
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
},
|
|
271
|
+
},
|
|
153
272
|
{
|
|
154
273
|
name: "scene_open",
|
|
155
274
|
method: "scene.open",
|
|
@@ -164,10 +283,11 @@ const TOOLS = [
|
|
|
164
283
|
name: "scene_save",
|
|
165
284
|
method: "scene.save",
|
|
166
285
|
description:
|
|
167
|
-
"Save the currently-edited scene. Pass 'path' to save-as (rebinds the scene to that path)."
|
|
286
|
+
"Save the currently-edited scene. Pass 'path' to save-as (rebinds the scene to that path). " +
|
|
287
|
+
"REQUIRED for fresh scenes: if the scene has never been saved (no backing file), the tool returns an error rather than triggering Godot's native Save-As dialog (which would block the editor waiting for a human click). Always pass 'path' when you built the scene from scratch via scene.build_tree / scene.add_node without going through scene.new.",
|
|
168
288
|
inputSchema: {
|
|
169
289
|
type: "object",
|
|
170
|
-
properties: { path: { type: "string", description: "
|
|
290
|
+
properties: { path: { type: "string", description: "Save-as target, required when the scene has no existing file." } },
|
|
171
291
|
},
|
|
172
292
|
},
|
|
173
293
|
{
|
|
@@ -236,6 +356,32 @@ const TOOLS = [
|
|
|
236
356
|
},
|
|
237
357
|
},
|
|
238
358
|
},
|
|
359
|
+
{
|
|
360
|
+
name: "script_patch",
|
|
361
|
+
method: "script.patch",
|
|
362
|
+
description:
|
|
363
|
+
"Apply targeted edits to an existing .gd file. Two modes: 'replacements' is an array of {old, new} where each 'old' must match exactly once in the file (ambiguous or missing matches return a clean error instead of silently mangling); 'full_source' overwrites the whole file. After writing, the tool parse-checks the result via ResourceLoader.load; if parsing fails the original is restored and an error is returned. Supports dry_run.",
|
|
364
|
+
inputSchema: {
|
|
365
|
+
type: "object",
|
|
366
|
+
required: ["path"],
|
|
367
|
+
properties: {
|
|
368
|
+
path: { type: "string", description: "Target .gd file." },
|
|
369
|
+
replacements: {
|
|
370
|
+
type: "array",
|
|
371
|
+
items: {
|
|
372
|
+
type: "object",
|
|
373
|
+
required: ["old", "new"],
|
|
374
|
+
properties: {
|
|
375
|
+
old: { type: "string" },
|
|
376
|
+
new: { type: "string" },
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
full_source: { type: "string", description: "Alternative to replacements: overwrite with this full source." },
|
|
381
|
+
dry_run: { type: "boolean", default: false },
|
|
382
|
+
},
|
|
383
|
+
},
|
|
384
|
+
},
|
|
239
385
|
{
|
|
240
386
|
name: "script_create",
|
|
241
387
|
method: "script.create",
|
|
@@ -290,7 +436,8 @@ const TOOLS = [
|
|
|
290
436
|
{
|
|
291
437
|
name: "resource_set_property",
|
|
292
438
|
method: "resource.set_property",
|
|
293
|
-
description:
|
|
439
|
+
description:
|
|
440
|
+
"Load a .tres, set one property, and save. Same JSON-to-Godot coercion as scene_set_property: primitives, NodePath, Vector2/2i/3/3i/4/4i, Rect2/2i, Quaternion, Color, Transform2D/3D, Basis, AABB, Plane, Resource auto-load from 'res://'/'uid://' paths, and all Packed*Array variants. Errors on silently-dropped assignments.",
|
|
294
441
|
inputSchema: {
|
|
295
442
|
type: "object",
|
|
296
443
|
required: ["path", "property"],
|
|
@@ -301,6 +448,22 @@ const TOOLS = [
|
|
|
301
448
|
},
|
|
302
449
|
},
|
|
303
450
|
},
|
|
451
|
+
{
|
|
452
|
+
name: "resource_call_method",
|
|
453
|
+
method: "resource.call_method",
|
|
454
|
+
description:
|
|
455
|
+
"Load a .tres, invoke a method on it, save, return the method's result. Rounds out what set_property can't express — e.g. StyleBoxFlat.set_border_width_all(4), set_corner_radius_all(14), Curve.add_point(...). Args coerce via the same rules as resource_set_property. Pass save:false to call without persisting (read-only method calls).",
|
|
456
|
+
inputSchema: {
|
|
457
|
+
type: "object",
|
|
458
|
+
required: ["path", "method"],
|
|
459
|
+
properties: {
|
|
460
|
+
path: { type: "string" },
|
|
461
|
+
method: { type: "string" },
|
|
462
|
+
args: { type: "array", description: "Method arguments in order, coerced per the method's parameter types." },
|
|
463
|
+
save: { type: "boolean", default: true, description: "Save the resource after the call. Set false for read-only method calls." },
|
|
464
|
+
},
|
|
465
|
+
},
|
|
466
|
+
},
|
|
304
467
|
{
|
|
305
468
|
name: "refs_validate_project",
|
|
306
469
|
method: "refs.validate_project",
|
|
@@ -406,6 +569,247 @@ const TOOLS = [
|
|
|
406
569
|
description: "List all registered autoloads with their paths and singleton flags.",
|
|
407
570
|
inputSchema: { type: "object", properties: {} },
|
|
408
571
|
},
|
|
572
|
+
{
|
|
573
|
+
name: "editor_state",
|
|
574
|
+
method: "editor.state",
|
|
575
|
+
description:
|
|
576
|
+
"Consolidated editor + project status in one call: Godot version, project name, current scene (path/class/root_name/open), list of open scenes, is-playing flag, playing scene path.",
|
|
577
|
+
inputSchema: { type: "object", properties: {} },
|
|
578
|
+
},
|
|
579
|
+
{
|
|
580
|
+
name: "editor_selection_get",
|
|
581
|
+
method: "editor.selection_get",
|
|
582
|
+
description: "Return the currently-selected nodes in the editor tree dock — for 'operate on what I clicked' workflows.",
|
|
583
|
+
inputSchema: { type: "object", properties: {} },
|
|
584
|
+
},
|
|
585
|
+
{
|
|
586
|
+
name: "editor_selection_set",
|
|
587
|
+
method: "editor.selection_set",
|
|
588
|
+
description: "Select specific nodes in the editor tree dock. Useful after an agent operation to point the user's attention at the result.",
|
|
589
|
+
inputSchema: {
|
|
590
|
+
type: "object",
|
|
591
|
+
required: ["node_paths"],
|
|
592
|
+
properties: {
|
|
593
|
+
node_paths: { type: "array", items: { type: "string" }, description: "NodePaths relative to the scene root. '.' = root." },
|
|
594
|
+
},
|
|
595
|
+
},
|
|
596
|
+
},
|
|
597
|
+
{
|
|
598
|
+
name: "editor_game_screenshot",
|
|
599
|
+
method: "editor.game_screenshot",
|
|
600
|
+
description:
|
|
601
|
+
"Capture the viewport of the CURRENTLY RUNNING game (after user pressed F5 etc.). Works via the _MCPGameBridge autoload registered by this plugin. If no game is running, returns an error pointing the user to run.scene_headless as the subprocess-based alternative.",
|
|
602
|
+
inputSchema: {
|
|
603
|
+
type: "object",
|
|
604
|
+
properties: {
|
|
605
|
+
output: { type: "string", default: "res://.godot/agent_tools/game_screenshot.png" },
|
|
606
|
+
timeout_ms: { type: "integer", default: 5000 },
|
|
607
|
+
},
|
|
608
|
+
},
|
|
609
|
+
},
|
|
610
|
+
{
|
|
611
|
+
name: "logs_read",
|
|
612
|
+
method: "logs.read",
|
|
613
|
+
description:
|
|
614
|
+
"Read print / push_error / push_warning output from the currently running game (captured by the _MCPGameBridge autoload). Entries include level, message, and timestamp. Returns an empty buffer with a helpful note if the game isn't running.",
|
|
615
|
+
inputSchema: {
|
|
616
|
+
type: "object",
|
|
617
|
+
properties: {
|
|
618
|
+
clear: { type: "boolean", default: false, description: "Clear the buffer after reading." },
|
|
619
|
+
max_lines: { type: "integer", default: 200, description: "Cap on entries returned; older entries are omitted first." },
|
|
620
|
+
},
|
|
621
|
+
},
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: "logs_clear",
|
|
625
|
+
method: "logs.clear",
|
|
626
|
+
description: "Drop the game log buffer. Safe to call whether the game is running or not.",
|
|
627
|
+
inputSchema: { type: "object", properties: {} },
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: "performance_monitors",
|
|
631
|
+
method: "performance.monitors",
|
|
632
|
+
description:
|
|
633
|
+
"Read Godot's Performance monitors (FPS, frame time, memory, object/node counts, draw calls, etc.). Default returns a common set; pass 'monitors' with specific names (fps, frame_time, mem_static, draw_calls, orphan_nodes, ...) for targeted reads.",
|
|
634
|
+
inputSchema: {
|
|
635
|
+
type: "object",
|
|
636
|
+
properties: {
|
|
637
|
+
monitors: {
|
|
638
|
+
type: "array",
|
|
639
|
+
items: { type: "string" },
|
|
640
|
+
description: "Optional subset of monitor names. Full list: fps, frame_time, physics_time, mem_static, mem_static_max, objects, resources, nodes, orphan_nodes, draw_calls, primitives, 2d_items, 2d_draw_calls, video_mem, audio_latency, physics_2d_active_objects, physics_3d_active_objects.",
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
},
|
|
645
|
+
{
|
|
646
|
+
name: "test_run",
|
|
647
|
+
method: "test.run",
|
|
648
|
+
description:
|
|
649
|
+
"Detect and run a GDScript test framework (GUT or GdUnit4), return structured results. Auto-detects the installed framework (via addons/gut or addons/gdUnit4), can be forced via 'framework'. Returns {total, passed, failed, skipped, failures: [{name, file, line, message}], raw_output}. Higher level than run.scene_headless — understands the framework's test concepts and summary format instead of asking you to parse stdout.",
|
|
650
|
+
inputSchema: {
|
|
651
|
+
type: "object",
|
|
652
|
+
properties: {
|
|
653
|
+
framework: { type: "string", enum: ["auto", "gut", "gdunit4"], default: "auto" },
|
|
654
|
+
directory: { type: "string", description: "Test directory (defaults to 'res://test')." },
|
|
655
|
+
pattern: { type: "string", description: "Filename pattern (framework-specific default)." },
|
|
656
|
+
timeout_seconds: { type: "integer", default: 60 },
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
},
|
|
660
|
+
{
|
|
661
|
+
name: "client_list",
|
|
662
|
+
method: "client.list",
|
|
663
|
+
description: "List supported MCP clients and whether each has the godot-agent-tools server configured. Shows the config file path for every client so users know where to look.",
|
|
664
|
+
inputSchema: { type: "object", properties: {} },
|
|
665
|
+
},
|
|
666
|
+
{
|
|
667
|
+
name: "client_configure",
|
|
668
|
+
method: "client.configure",
|
|
669
|
+
description:
|
|
670
|
+
"Write the godot-agent-tools MCP server entry into the specified client's config file. Idempotent (won't duplicate); pass overwrite:true to force-replace an existing entry. Supported clients: claude_code_project, claude_code_user, claude_desktop, cursor_project, cursor_user.",
|
|
671
|
+
inputSchema: {
|
|
672
|
+
type: "object",
|
|
673
|
+
required: ["client"],
|
|
674
|
+
properties: {
|
|
675
|
+
client: { type: "string", enum: ["claude_code_project", "claude_code_user", "claude_desktop", "cursor_project", "cursor_user"] },
|
|
676
|
+
overwrite: { type: "boolean", default: false },
|
|
677
|
+
},
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "client_remove",
|
|
682
|
+
method: "client.remove",
|
|
683
|
+
description: "Remove the godot-agent-tools entry from the specified client's config.",
|
|
684
|
+
inputSchema: {
|
|
685
|
+
type: "object",
|
|
686
|
+
required: ["client"],
|
|
687
|
+
properties: {
|
|
688
|
+
client: { type: "string", enum: ["claude_code_project", "claude_code_user", "claude_desktop", "cursor_project", "cursor_user"] },
|
|
689
|
+
},
|
|
690
|
+
},
|
|
691
|
+
},
|
|
692
|
+
{
|
|
693
|
+
name: "physics_autofit_collision_shape_2d",
|
|
694
|
+
method: "physics.autofit_collision_shape_2d",
|
|
695
|
+
description:
|
|
696
|
+
"Compute a CollisionShape2D sized to a sibling Sprite2D/AnimatedSprite2D's visual bounds. Can auto-create the CollisionShape2D if it doesn't exist yet (pass create:true). Shape type: 'rectangle' (default), 'circle', or 'capsule'. Optional margin shrinks the shape.",
|
|
697
|
+
inputSchema: {
|
|
698
|
+
type: "object",
|
|
699
|
+
required: ["node_path"],
|
|
700
|
+
properties: {
|
|
701
|
+
node_path: { type: "string", description: "CollisionShape2D to fit. Created if missing + create:true." },
|
|
702
|
+
source: { type: "string", description: "NodePath to a Sprite2D/AnimatedSprite2D. Auto-detected among siblings if omitted." },
|
|
703
|
+
shape: { type: "string", enum: ["rectangle", "circle", "capsule"], default: "rectangle" },
|
|
704
|
+
margin: { type: "number", default: 0 },
|
|
705
|
+
create: { type: "boolean", default: false },
|
|
706
|
+
},
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: "theme_set_color",
|
|
711
|
+
method: "theme.set_color",
|
|
712
|
+
description: "Set a color entry in a Theme resource. Wraps Theme.set_color(item, type, color) — e.g. item='font_color', type='Label'.",
|
|
713
|
+
inputSchema: {
|
|
714
|
+
type: "object",
|
|
715
|
+
required: ["path", "item", "type", "color"],
|
|
716
|
+
properties: {
|
|
717
|
+
path: { type: "string", description: "Path to .tres Theme resource." },
|
|
718
|
+
item: { type: "string", description: "Theme item name (e.g. 'font_color', 'bg_color')." },
|
|
719
|
+
type: { type: "string", description: "Control class name (e.g. 'Button', 'Label')." },
|
|
720
|
+
color: { description: "[r,g,b(,a)] or '#hex'." },
|
|
721
|
+
},
|
|
722
|
+
},
|
|
723
|
+
},
|
|
724
|
+
{
|
|
725
|
+
name: "theme_set_constant",
|
|
726
|
+
method: "theme.set_constant",
|
|
727
|
+
description: "Set an int constant in a Theme resource. E.g. item='h_separation', type='HBoxContainer'.",
|
|
728
|
+
inputSchema: {
|
|
729
|
+
type: "object",
|
|
730
|
+
required: ["path", "item", "type", "value"],
|
|
731
|
+
properties: {
|
|
732
|
+
path: { type: "string" },
|
|
733
|
+
item: { type: "string" },
|
|
734
|
+
type: { type: "string" },
|
|
735
|
+
value: { type: "integer" },
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
},
|
|
739
|
+
{
|
|
740
|
+
name: "theme_set_font_size",
|
|
741
|
+
method: "theme.set_font_size",
|
|
742
|
+
description: "Set a font-size entry in a Theme resource. E.g. item='font_size', type='Label'.",
|
|
743
|
+
inputSchema: {
|
|
744
|
+
type: "object",
|
|
745
|
+
required: ["path", "item", "type", "value"],
|
|
746
|
+
properties: {
|
|
747
|
+
path: { type: "string" },
|
|
748
|
+
item: { type: "string" },
|
|
749
|
+
type: { type: "string" },
|
|
750
|
+
value: { type: "integer" },
|
|
751
|
+
},
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
{
|
|
755
|
+
name: "theme_set_stylebox_flat",
|
|
756
|
+
method: "theme.set_stylebox_flat",
|
|
757
|
+
description:
|
|
758
|
+
"Create (or replace) a StyleBoxFlat on a Theme with the given properties and assign it to theme.<item>.<type>. Saves the usual multi-step StyleBoxFlat setup — e.g. {item: 'normal', type: 'Button', properties: {bg_color: [0.1,0.1,0.12,1], corner_radius_top_left: 8, ...}}.",
|
|
759
|
+
inputSchema: {
|
|
760
|
+
type: "object",
|
|
761
|
+
required: ["path", "item", "type"],
|
|
762
|
+
properties: {
|
|
763
|
+
path: { type: "string" },
|
|
764
|
+
item: { type: "string" },
|
|
765
|
+
type: { type: "string" },
|
|
766
|
+
properties: { type: "object", additionalProperties: true },
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
},
|
|
770
|
+
{
|
|
771
|
+
name: "session_list",
|
|
772
|
+
method: "__local__.session_list",
|
|
773
|
+
description:
|
|
774
|
+
"List every running Godot editor with the Agent Tools plugin enabled (each becomes a separate 'session' the shim can target). Each entry: {pid, port, project_path, project_name, godot_version, started_at_unix, active}. The shim's default target is the most-recently-started session; use session_activate to pin a specific one.",
|
|
775
|
+
inputSchema: { type: "object", properties: {} },
|
|
776
|
+
},
|
|
777
|
+
{
|
|
778
|
+
name: "session_activate",
|
|
779
|
+
method: "__local__.session_activate",
|
|
780
|
+
description:
|
|
781
|
+
"Pin subsequent tool calls to a specific Godot editor session (by pid from session_list). Pass pid:null to clear the pin and fall back to 'most-recently-started'. Changing the active session tears down the existing TCP connection; the next call reconnects to the new target.",
|
|
782
|
+
inputSchema: {
|
|
783
|
+
type: "object",
|
|
784
|
+
properties: {
|
|
785
|
+
pid: { type: ["integer", "null"], description: "PID of the session to target; null to clear." },
|
|
786
|
+
},
|
|
787
|
+
},
|
|
788
|
+
},
|
|
789
|
+
{
|
|
790
|
+
name: "batch_execute",
|
|
791
|
+
method: "batch.execute",
|
|
792
|
+
description:
|
|
793
|
+
"Run multiple tool calls in one round trip. Each call is dispatched server-side and results are returned in order. Useful when you know the exact sequence you want — saves TCP round trips vs. parallel MCP calls.",
|
|
794
|
+
inputSchema: {
|
|
795
|
+
type: "object",
|
|
796
|
+
required: ["calls"],
|
|
797
|
+
properties: {
|
|
798
|
+
calls: {
|
|
799
|
+
type: "array",
|
|
800
|
+
items: {
|
|
801
|
+
type: "object",
|
|
802
|
+
required: ["method"],
|
|
803
|
+
properties: {
|
|
804
|
+
method: { type: "string", description: "Dotted method name (e.g. 'scene.add_node')." },
|
|
805
|
+
params: { type: "object" },
|
|
806
|
+
},
|
|
807
|
+
},
|
|
808
|
+
},
|
|
809
|
+
stop_on_error: { type: "boolean", default: false, description: "Halt the batch on the first failure. Default keeps going." },
|
|
810
|
+
},
|
|
811
|
+
},
|
|
812
|
+
},
|
|
409
813
|
{
|
|
410
814
|
name: "editor_reload_filesystem",
|
|
411
815
|
method: "editor.reload_filesystem",
|
|
@@ -450,7 +854,11 @@ const TOOLS = [
|
|
|
450
854
|
name: "input_map_add_event",
|
|
451
855
|
method: "input_map.add_event",
|
|
452
856
|
description:
|
|
453
|
-
"Attach an input event to an existing action.
|
|
857
|
+
"Attach an input event to an existing action. Every event accepts an optional 'device' field (default -1 = all devices; set 0, 1, etc. for local-multiplayer device-specific bindings). Event shapes: " +
|
|
858
|
+
"{type:'key', keycode:'A'|'Space'|...}; " +
|
|
859
|
+
"{type:'mouse_button', button_index:1|2|3}; " +
|
|
860
|
+
"{type:'joy_button', button_index:0..}; " +
|
|
861
|
+
"{type:'joy_motion', axis:0..5 or 'left_x'|'left_y'|'right_x'|'right_y'|'trigger_left'|'trigger_right', axis_value:-1.0..1.0} (axis_value sign picks direction).",
|
|
454
862
|
inputSchema: {
|
|
455
863
|
type: "object",
|
|
456
864
|
required: ["action", "event"],
|
|
@@ -460,10 +868,13 @@ const TOOLS = [
|
|
|
460
868
|
type: "object",
|
|
461
869
|
required: ["type"],
|
|
462
870
|
properties: {
|
|
463
|
-
type: { type: "string", enum: ["key", "mouse_button", "joy_button"] },
|
|
871
|
+
type: { type: "string", enum: ["key", "mouse_button", "joy_button", "joy_motion"] },
|
|
872
|
+
device: { type: "integer", default: -1, description: "-1 = all devices (default); 0, 1, etc. bind a specific controller for local multiplayer." },
|
|
464
873
|
keycode: { description: "For type='key': 'A', 'Space', 'F1', or int keycode." },
|
|
465
|
-
button_index: { type: "integer", description: "For mouse/joy button events." },
|
|
466
874
|
physical: { type: "boolean", default: true, description: "For type='key': use physical keycode (recommended)." },
|
|
875
|
+
button_index: { type: "integer", description: "For mouse/joy button events." },
|
|
876
|
+
axis: { description: "For type='joy_motion': int 0..5 or 'left_x'|'left_y'|'right_x'|'right_y'|'trigger_left'|'trigger_right'." },
|
|
877
|
+
axis_value: { type: "number", description: "For type='joy_motion': -1.0 to 1.0; sign selects direction that triggers the action." },
|
|
467
878
|
},
|
|
468
879
|
},
|
|
469
880
|
},
|
|
@@ -509,18 +920,129 @@ const TOOLS = [
|
|
|
509
920
|
name: "run_scene_headless",
|
|
510
921
|
method: "run.scene_headless",
|
|
511
922
|
description:
|
|
512
|
-
"Run a scene in a
|
|
923
|
+
"Run a scene in a child Godot process with structured output. " +
|
|
924
|
+
"MODES: BARE (default) runs --headless — fast, no window, good for 'does _ready not crash' checks. " +
|
|
925
|
+
"DRIVEN (anything beyond path + quit_after_seconds) runs under a wrapper driver that can inject scripted input, capture screenshots at multiple frames, dump final scene state as JSON, and use a deterministic RNG seed. " +
|
|
926
|
+
"SCREENSHOTS: when screenshot(s) requested the subprocess drops --headless and runs with a real (offscreen) window because Godot 4.6's headless mode uses a dummy renderer. Expect a brief window flash. " +
|
|
927
|
+
"STRUCTURED RESULTS: tool parses stdout for ERROR: / USER ERROR: / SCRIPT ERROR: / WARNING: / USER WARNING: lines and returns them as result.errors and result.warnings arrays so the agent doesn't have to regex the raw output. state_dump:true adds result.final_state with the scene tree + common properties. " +
|
|
928
|
+
"Event types for input_script: " +
|
|
929
|
+
"{frame, type: 'action_tap', action}; " +
|
|
930
|
+
"{frame, type: 'action_press', action, strength?}; " +
|
|
931
|
+
"{frame, type: 'action_release', action}; " +
|
|
932
|
+
"{frame, type: 'key', keycode: 'Space'|int, pressed?: true}; " +
|
|
933
|
+
"{frame, type: 'mouse_click', position: [x, y], button?: 1}; " +
|
|
934
|
+
"{frame, type: 'mouse_motion', position: [x, y]}. " +
|
|
935
|
+
"BLOCKS the editor for the duration — use small quit_after_seconds (1-5).",
|
|
513
936
|
inputSchema: {
|
|
514
937
|
type: "object",
|
|
515
938
|
required: ["path"],
|
|
516
939
|
properties: {
|
|
517
940
|
path: { type: "string", description: "Scene to run, e.g. 'res://Main.tscn'." },
|
|
518
|
-
quit_after_seconds: { type: "number", default: 2, description: "Converted to frames assuming 60 fps." },
|
|
941
|
+
quit_after_seconds: { type: "number", default: 2, description: "Converted to frames assuming 60 fps. In DRIVEN mode this is also the quit_frame the driver targets." },
|
|
519
942
|
extra_args: {
|
|
520
943
|
type: "array",
|
|
521
944
|
items: { type: "string" },
|
|
522
945
|
description: "Additional CLI args passed to the child godot process.",
|
|
523
946
|
},
|
|
947
|
+
input_script: {
|
|
948
|
+
type: "array",
|
|
949
|
+
description: "Optional: enters DRIVEN mode. Array of event specs keyed by frame. See description for event type shapes.",
|
|
950
|
+
items: {
|
|
951
|
+
type: "object",
|
|
952
|
+
required: ["frame", "type"],
|
|
953
|
+
properties: {
|
|
954
|
+
frame: { type: "integer", description: "0-based frame to fire on." },
|
|
955
|
+
type: { type: "string", enum: ["action_press", "action_release", "action_tap", "key", "mouse_click", "mouse_motion"] },
|
|
956
|
+
action: { type: "string" },
|
|
957
|
+
strength: { type: "number", default: 1.0 },
|
|
958
|
+
keycode: { description: "For type='key': 'A', 'Space', 'F1', or int keycode." },
|
|
959
|
+
pressed: { type: "boolean" },
|
|
960
|
+
button: { type: "integer", description: "Mouse button (1=left, 2=right, 3=middle)." },
|
|
961
|
+
position: { type: "array", description: "[x, y] viewport coordinates." },
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
},
|
|
965
|
+
screenshot: {
|
|
966
|
+
type: "string",
|
|
967
|
+
description: "Shorthand: one PNG saved at the final frame. Equivalent to screenshots:[{frame: quit_frame, path: ...}]. Triggers offscreen-windowed subprocess (brief window flash).",
|
|
968
|
+
},
|
|
969
|
+
screenshots: {
|
|
970
|
+
type: "array",
|
|
971
|
+
description: "Capture PNGs at multiple specific frames during the run — useful for verifying state transitions (spawn at frame 30, mid-animation at 60, final at 120).",
|
|
972
|
+
items: {
|
|
973
|
+
type: "object",
|
|
974
|
+
required: ["frame", "path"],
|
|
975
|
+
properties: {
|
|
976
|
+
frame: { type: "integer" },
|
|
977
|
+
path: { type: "string" },
|
|
978
|
+
},
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
resolution: {
|
|
982
|
+
type: "string",
|
|
983
|
+
default: "320x240",
|
|
984
|
+
description: "Window resolution 'WxH' when screenshots are captured. Default is tiny to minimize offscreen footprint; bump to 1280x720 or similar for UI verification.",
|
|
985
|
+
},
|
|
986
|
+
state_dump: {
|
|
987
|
+
type: "boolean",
|
|
988
|
+
default: false,
|
|
989
|
+
description: "When true, driver writes a JSON snapshot of the final scene tree (name, class, node_path, script, children, plus common props like visible/position/text/value/modulate). Returned as result.final_state. Lets agents verify end state programmatically instead of eyeballing the screenshot.",
|
|
990
|
+
},
|
|
991
|
+
seed: {
|
|
992
|
+
type: "integer",
|
|
993
|
+
description: "Optional RNG seed. Set before the target scene is instanced so randi/randf are reproducible. Useful for deterministic tests.",
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
},
|
|
998
|
+
{
|
|
999
|
+
name: "fs_read_text",
|
|
1000
|
+
method: "fs.read_text",
|
|
1001
|
+
description: "Read a text file under res://. Complement to user_fs_read (which targets user:// for runtime-written state).",
|
|
1002
|
+
inputSchema: {
|
|
1003
|
+
type: "object",
|
|
1004
|
+
required: ["path"],
|
|
1005
|
+
properties: { path: { type: "string", description: "Must begin with 'res://'." } },
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: "fs_write_text",
|
|
1010
|
+
method: "fs.write_text",
|
|
1011
|
+
description:
|
|
1012
|
+
"Write a text file under res://. Creates parent directories if needed; triggers the editor's filesystem rescan so the new file shows up in the FileSystem dock immediately. For .gd scripts prefer script_create / script_patch — those run a parse check.",
|
|
1013
|
+
inputSchema: {
|
|
1014
|
+
type: "object",
|
|
1015
|
+
required: ["path", "content"],
|
|
1016
|
+
properties: {
|
|
1017
|
+
path: { type: "string" },
|
|
1018
|
+
content: { type: "string" },
|
|
1019
|
+
overwrite: { type: "boolean", default: false },
|
|
1020
|
+
},
|
|
1021
|
+
},
|
|
1022
|
+
},
|
|
1023
|
+
{
|
|
1024
|
+
name: "user_fs_read",
|
|
1025
|
+
method: "user_fs.read",
|
|
1026
|
+
description:
|
|
1027
|
+
"Read a text file from the user:// data directory — where games persist save files, custom-level JSON, settings, etc. Separate from fs_list (which is res://-only) because user:// is runtime-written state.",
|
|
1028
|
+
inputSchema: {
|
|
1029
|
+
type: "object",
|
|
1030
|
+
required: ["path"],
|
|
1031
|
+
properties: {
|
|
1032
|
+
path: { type: "string", description: "Must begin with 'user://'." },
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
},
|
|
1036
|
+
{
|
|
1037
|
+
name: "user_fs_list",
|
|
1038
|
+
method: "user_fs.list",
|
|
1039
|
+
description:
|
|
1040
|
+
"List files and subdirectories under a user:// directory. Optionally recursive.",
|
|
1041
|
+
inputSchema: {
|
|
1042
|
+
type: "object",
|
|
1043
|
+
properties: {
|
|
1044
|
+
dir: { type: "string", default: "user://", description: "Must begin with 'user://'." },
|
|
1045
|
+
recursive: { type: "boolean", default: false },
|
|
524
1046
|
},
|
|
525
1047
|
},
|
|
526
1048
|
},
|
|
@@ -615,10 +1137,12 @@ const BY_NAME = Object.fromEntries(TOOLS.map((t) => [t.name, t]));
|
|
|
615
1137
|
|
|
616
1138
|
// Persistent Godot TCP client. One socket reused across tool calls; outstanding
|
|
617
1139
|
// requests are tracked by id so multiple in-flight calls don't interleave data.
|
|
1140
|
+
// Target port is resolved lazily per call so session.activate / session death
|
|
1141
|
+
// switch over without proactive teardown.
|
|
618
1142
|
class GodotClient {
|
|
619
|
-
constructor(host
|
|
1143
|
+
constructor(host) {
|
|
620
1144
|
this.host = host;
|
|
621
|
-
this.port =
|
|
1145
|
+
this.port = null;
|
|
622
1146
|
this.socket = null;
|
|
623
1147
|
this.buffer = "";
|
|
624
1148
|
this.pending = new Map(); // id -> { resolve, reject, timer }
|
|
@@ -626,9 +1150,46 @@ class GodotClient {
|
|
|
626
1150
|
this.connecting = null;
|
|
627
1151
|
}
|
|
628
1152
|
|
|
1153
|
+
// Priority: GODOT_AGENT_PORT env > pinned active session > most recent session.
|
|
1154
|
+
_resolvePort() {
|
|
1155
|
+
if (FORCED_PORT != null) return FORCED_PORT;
|
|
1156
|
+
const sessions = listSessions();
|
|
1157
|
+
if (sessions.length === 0) return null;
|
|
1158
|
+
if (activeSessionPid != null) {
|
|
1159
|
+
const pinned = sessions.find((s) => s.pid === activeSessionPid);
|
|
1160
|
+
if (pinned) return pinned.port;
|
|
1161
|
+
activeSessionPid = null; // pinned session died — fall back
|
|
1162
|
+
}
|
|
1163
|
+
return sessions[0].port;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
// If the active session changed, drop the old socket so the next call
|
|
1167
|
+
// reconnects to the new target.
|
|
1168
|
+
_maybeResetForPortChange() {
|
|
1169
|
+
const target = this._resolvePort();
|
|
1170
|
+
if (target !== this.port && this.socket) {
|
|
1171
|
+
try { this.socket.destroy(); } catch {}
|
|
1172
|
+
this.socket = null;
|
|
1173
|
+
this.buffer = "";
|
|
1174
|
+
for (const { reject, timer } of this.pending.values()) {
|
|
1175
|
+
clearTimeout(timer);
|
|
1176
|
+
reject(new Error("session target changed mid-flight"));
|
|
1177
|
+
}
|
|
1178
|
+
this.pending.clear();
|
|
1179
|
+
}
|
|
1180
|
+
this.port = target;
|
|
1181
|
+
}
|
|
1182
|
+
|
|
629
1183
|
async _ensureConnected() {
|
|
1184
|
+
this._maybeResetForPortChange();
|
|
630
1185
|
if (this.socket && !this.socket.destroyed) return;
|
|
631
1186
|
if (this.connecting) return this.connecting;
|
|
1187
|
+
if (this.port == null) {
|
|
1188
|
+
throw new Error(
|
|
1189
|
+
"No Godot editor session found. Open a project with the 'Agent Tools' plugin enabled, " +
|
|
1190
|
+
"or set the GODOT_AGENT_PORT env var to target a specific port."
|
|
1191
|
+
);
|
|
1192
|
+
}
|
|
632
1193
|
|
|
633
1194
|
this.connecting = new Promise((resolve, reject) => {
|
|
634
1195
|
const s = new net.Socket();
|
|
@@ -745,13 +1306,62 @@ class GodotClient {
|
|
|
745
1306
|
}
|
|
746
1307
|
}
|
|
747
1308
|
|
|
748
|
-
const client = new GodotClient(HOST
|
|
1309
|
+
const client = new GodotClient(HOST);
|
|
749
1310
|
|
|
750
1311
|
const server = new Server(
|
|
751
|
-
{ name: "godot-agent-tools", version: "0.
|
|
752
|
-
{ capabilities: { tools: {} } }
|
|
1312
|
+
{ name: "godot-agent-tools", version: "0.3.0" },
|
|
1313
|
+
{ capabilities: { tools: {}, resources: {} } }
|
|
753
1314
|
);
|
|
754
1315
|
|
|
1316
|
+
// MCP Resources — subscribable read-only endpoints. Agents that support
|
|
1317
|
+
// resources can 'watch' these without repeatedly calling tools.
|
|
1318
|
+
const RESOURCES = [
|
|
1319
|
+
{
|
|
1320
|
+
uri: "godot://editor/state",
|
|
1321
|
+
name: "Editor state",
|
|
1322
|
+
description: "Current editor state: Godot version, project name, current scene, playing status.",
|
|
1323
|
+
mimeType: "application/json",
|
|
1324
|
+
method: "editor.state",
|
|
1325
|
+
},
|
|
1326
|
+
{
|
|
1327
|
+
uri: "godot://scene/current",
|
|
1328
|
+
name: "Current scene",
|
|
1329
|
+
description: "Currently-edited scene (path, root name, root class, open?).",
|
|
1330
|
+
mimeType: "application/json",
|
|
1331
|
+
method: "scene.current",
|
|
1332
|
+
},
|
|
1333
|
+
{
|
|
1334
|
+
uri: "godot://scene/hierarchy",
|
|
1335
|
+
name: "Current scene hierarchy",
|
|
1336
|
+
description: "Full tree of the currently-edited scene.",
|
|
1337
|
+
mimeType: "application/json",
|
|
1338
|
+
method: "scene.inspect",
|
|
1339
|
+
},
|
|
1340
|
+
{
|
|
1341
|
+
uri: "godot://selection/current",
|
|
1342
|
+
name: "Editor selection",
|
|
1343
|
+
description: "Nodes currently selected in the editor tree dock.",
|
|
1344
|
+
mimeType: "application/json",
|
|
1345
|
+
method: "editor.selection_get",
|
|
1346
|
+
},
|
|
1347
|
+
{
|
|
1348
|
+
uri: "godot://logs/recent",
|
|
1349
|
+
name: "Recent game logs",
|
|
1350
|
+
description: "Recent print / push_error / push_warning output from the running game.",
|
|
1351
|
+
mimeType: "application/json",
|
|
1352
|
+
method: "logs.read",
|
|
1353
|
+
},
|
|
1354
|
+
{
|
|
1355
|
+
uri: "godot://performance/monitors",
|
|
1356
|
+
name: "Performance monitors",
|
|
1357
|
+
description: "FPS, frame time, memory, draw calls, object counts.",
|
|
1358
|
+
mimeType: "application/json",
|
|
1359
|
+
method: "performance.monitors",
|
|
1360
|
+
},
|
|
1361
|
+
];
|
|
1362
|
+
|
|
1363
|
+
const RESOURCE_BY_URI = Object.fromEntries(RESOURCES.map((r) => [r.uri, r]));
|
|
1364
|
+
|
|
755
1365
|
server.setRequestHandler(ListToolsRequestSchema, async () => ({
|
|
756
1366
|
tools: TOOLS.map(({ name, description, inputSchema }) => ({
|
|
757
1367
|
name,
|
|
@@ -768,6 +1378,37 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
768
1378
|
content: [{ type: "text", text: `Unknown tool: ${req.params.name}` }],
|
|
769
1379
|
};
|
|
770
1380
|
}
|
|
1381
|
+
|
|
1382
|
+
// session_list / session_activate are shim-local — they manage the MCP shim's
|
|
1383
|
+
// own routing state and don't forward to any Godot process.
|
|
1384
|
+
if (tool.method === "__local__.session_list") {
|
|
1385
|
+
const sessions = listSessions().map((s) => ({
|
|
1386
|
+
...s,
|
|
1387
|
+
active: activeSessionPid != null ? s.pid === activeSessionPid : s === listSessions()[0],
|
|
1388
|
+
}));
|
|
1389
|
+
return {
|
|
1390
|
+
content: [{ type: "text", text: JSON.stringify({ sessions, count: sessions.length, active_pid: activeSessionPid }, null, 2) }],
|
|
1391
|
+
};
|
|
1392
|
+
}
|
|
1393
|
+
if (tool.method === "__local__.session_activate") {
|
|
1394
|
+
const pid = req.params.arguments?.pid ?? null;
|
|
1395
|
+
if (pid === null) {
|
|
1396
|
+
activeSessionPid = null;
|
|
1397
|
+
} else {
|
|
1398
|
+
const sessions = listSessions();
|
|
1399
|
+
if (!sessions.some((s) => s.pid === pid)) {
|
|
1400
|
+
return {
|
|
1401
|
+
isError: true,
|
|
1402
|
+
content: [{ type: "text", text: `No active session with pid=${pid}. Call session_list to see candidates.` }],
|
|
1403
|
+
};
|
|
1404
|
+
}
|
|
1405
|
+
activeSessionPid = pid;
|
|
1406
|
+
}
|
|
1407
|
+
return {
|
|
1408
|
+
content: [{ type: "text", text: JSON.stringify({ active_pid: activeSessionPid }, null, 2) }],
|
|
1409
|
+
};
|
|
1410
|
+
}
|
|
1411
|
+
|
|
771
1412
|
try {
|
|
772
1413
|
const result = await client.call(tool.method, req.params.arguments || {});
|
|
773
1414
|
return {
|
|
@@ -781,5 +1422,43 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
|
|
|
781
1422
|
}
|
|
782
1423
|
});
|
|
783
1424
|
|
|
1425
|
+
server.setRequestHandler(ListResourcesRequestSchema, async () => ({
|
|
1426
|
+
resources: RESOURCES.map(({ uri, name, description, mimeType }) => ({
|
|
1427
|
+
uri,
|
|
1428
|
+
name,
|
|
1429
|
+
description,
|
|
1430
|
+
mimeType,
|
|
1431
|
+
})),
|
|
1432
|
+
}));
|
|
1433
|
+
|
|
1434
|
+
server.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
1435
|
+
const resource = RESOURCE_BY_URI[req.params.uri];
|
|
1436
|
+
if (!resource) {
|
|
1437
|
+
throw new Error(`Unknown resource URI: ${req.params.uri}`);
|
|
1438
|
+
}
|
|
1439
|
+
try {
|
|
1440
|
+
const result = await client.call(resource.method, {});
|
|
1441
|
+
return {
|
|
1442
|
+
contents: [
|
|
1443
|
+
{
|
|
1444
|
+
uri: resource.uri,
|
|
1445
|
+
mimeType: resource.mimeType,
|
|
1446
|
+
text: JSON.stringify(result, null, 2),
|
|
1447
|
+
},
|
|
1448
|
+
],
|
|
1449
|
+
};
|
|
1450
|
+
} catch (e) {
|
|
1451
|
+
return {
|
|
1452
|
+
contents: [
|
|
1453
|
+
{
|
|
1454
|
+
uri: resource.uri,
|
|
1455
|
+
mimeType: resource.mimeType,
|
|
1456
|
+
text: JSON.stringify({ error: e.message }, null, 2),
|
|
1457
|
+
},
|
|
1458
|
+
],
|
|
1459
|
+
};
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
|
|
784
1463
|
const transport = new StdioServerTransport();
|
|
785
1464
|
await server.connect(transport);
|