godot-daedalus_backend 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 (67) hide show
  1. package/README.md +101 -0
  2. package/bin/godot-daedalus-backend.js +4 -0
  3. package/bin/godot-daedalus-mcp.js +4 -0
  4. package/bin/godot-daedalus-terminal-mcp.js +4 -0
  5. package/bin/run-tsx-entry.js +26 -0
  6. package/package.json +54 -0
  7. package/scripts/deepseek-tokenizer-server.py +54 -0
  8. package/src/app-paths.ts +36 -0
  9. package/src/main.ts +21 -0
  10. package/src/mcp/content-length-protocol.ts +68 -0
  11. package/src/mcp/custom-mcp-config-store.ts +397 -0
  12. package/src/mcp/godot-diagnostics-bridge.ts +1298 -0
  13. package/src/mcp/godot-editor-bridge.ts +307 -0
  14. package/src/mcp/godot-mcp-server.ts +3484 -0
  15. package/src/mcp/godot-paths.ts +151 -0
  16. package/src/mcp/godot-project-settings.ts +233 -0
  17. package/src/mcp/godot-tool-registration.ts +46 -0
  18. package/src/mcp/mcp-config.ts +48 -0
  19. package/src/mcp/mcp-host.ts +393 -0
  20. package/src/mcp/mcp-session.ts +81 -0
  21. package/src/mcp/terminal-mcp-server.ts +576 -0
  22. package/src/mcp/tscn-tools.ts +302 -0
  23. package/src/mcp/types.ts +12 -0
  24. package/src/ping-client.ts +24 -0
  25. package/src/prompts/registry.ts +97 -0
  26. package/src/prompts/templates/backend-helper.md +25 -0
  27. package/src/prompts/templates/gdscript-reviewer.md +19 -0
  28. package/src/prompts/templates/godot-assistant.md +225 -0
  29. package/src/prompts/templates/scene-architect.md +15 -0
  30. package/src/prompts/templates/session-compressor.md +33 -0
  31. package/src/protocol/schema.ts +486 -0
  32. package/src/protocol/types.ts +77 -0
  33. package/src/providers/deepseek-agent.ts +1014 -0
  34. package/src/providers/deepseek-client.ts +114 -0
  35. package/src/providers/deepseek-dsml-tools.ts +90 -0
  36. package/src/providers/deepseek-loose-tools.ts +450 -0
  37. package/src/providers/provider-config-store.ts +164 -0
  38. package/src/server/client-session.ts +93 -0
  39. package/src/server/request-dispatcher.ts +74 -0
  40. package/src/server/response-helpers.ts +33 -0
  41. package/src/server/send-json.ts +8 -0
  42. package/src/server/websocket-server.ts +3997 -0
  43. package/src/session/session-compressor.ts +68 -0
  44. package/src/session/session-store.ts +669 -0
  45. package/src/skills/registry.ts +180 -0
  46. package/src/skills/templates/backend-helper.md +12 -0
  47. package/src/skills/templates/file-creator.md +14 -0
  48. package/src/skills/templates/gdscript-review.md +12 -0
  49. package/src/skills/templates/godot-project-init.md +29 -0
  50. package/src/skills/templates/scene-builder.md +12 -0
  51. package/src/tokens/deepseek-tokenizer-counter.ts +233 -0
  52. package/src/tokens/model-profiles.ts +38 -0
  53. package/src/tokens/token-counter-factory.ts +52 -0
  54. package/src/tokens/token-counter.ts +22 -0
  55. package/src/tools/approval-gateway.ts +111 -0
  56. package/src/tools/llm-tools.ts +1415 -0
  57. package/src/tools/tool-dispatcher.ts +147 -0
  58. package/src/tools/tool-event-describer.ts +387 -0
  59. package/src/tools/tool-idempotency.ts +373 -0
  60. package/src/tools/tool-policy-table.ts +61 -0
  61. package/src/tools/tool-policy.ts +73 -0
  62. package/src/workflow/llm-planner.ts +407 -0
  63. package/src/workflow/planner.ts +201 -0
  64. package/src/workflow/runner.ts +141 -0
  65. package/src/workflow/types.ts +69 -0
  66. package/src/workspace/registry.ts +104 -0
  67. package/src/workspace/types.ts +7 -0
@@ -0,0 +1,393 @@
1
+ import { buildMcpServerConfigs } from "./mcp-config.js";
2
+ import { buildCustomMcpServerConfigs } from "./custom-mcp-config-store.js";
3
+ import { GODOT_DIAGNOSTICS_SERVER_ID, GodotDiagnosticsBridge } from "./godot-diagnostics-bridge.js";
4
+ import { GODOT_EDITOR_SERVER_ID, GodotEditorBridge } from "./godot-editor-bridge.js";
5
+ import { McpSession } from "./mcp-session.js";
6
+ import type { McpServerConfig } from "./types.js";
7
+ import { findWorkspace, getDefaultWorkspace } from "../workspace/registry.js";
8
+ import type { WorkspaceConfig } from "../workspace/types.js";
9
+ import { replaceDynamicMcpTools, type DynamicMcpToolSource } from "../tools/llm-tools.js";
10
+
11
+ const CUSTOM_MCP_CONNECT_TIMEOUT_MS: number = 30_000;
12
+ const CUSTOM_MCP_LIST_TOOLS_TIMEOUT_MS: number = 10_000;
13
+ const CUSTOM_MCP_CLOSE_TIMEOUT_MS: number = 2_000;
14
+
15
+ type McpToolListResult = {
16
+ tools: Array<{
17
+ name: string;
18
+ description?: string | undefined;
19
+ inputSchema?: unknown;
20
+ }>;
21
+ };
22
+
23
+ export type CustomMcpServerRuntimeStatus = {
24
+ id: string;
25
+ status: "connected" | "error";
26
+ toolCount: number;
27
+ error?: string | undefined;
28
+ };
29
+
30
+ async function withTimeout<T>(promise: Promise<T>, timeoutMs: number, label: string): Promise<T> {
31
+ let timeout: NodeJS.Timeout | undefined;
32
+ try {
33
+ return await Promise.race([
34
+ promise,
35
+ new Promise<never>((_resolve, reject): void => {
36
+ timeout = setTimeout((): void => {
37
+ reject(new Error(`${label} timed out after ${timeoutMs}ms`));
38
+ }, timeoutMs);
39
+ })
40
+ ]);
41
+ } finally {
42
+ if (timeout !== undefined) {
43
+ clearTimeout(timeout);
44
+ }
45
+ }
46
+ }
47
+
48
+ export class McpHost {
49
+ private workspaceSessions: Map<string, Map<string, McpSession>> = new Map();
50
+ private workspaceCustomTools: Map<string, Map<string, DynamicMcpToolSource[]>> = new Map();
51
+ private customServerStatuses: Map<string, CustomMcpServerRuntimeStatus> = new Map();
52
+ private activeWorkspaceId?: string | undefined;
53
+ private readonly editorBridge: GodotEditorBridge = new GodotEditorBridge();
54
+ private readonly diagnosticsBridge: GodotDiagnosticsBridge = new GodotDiagnosticsBridge();
55
+
56
+ async connectAll(): Promise<void> {
57
+ if (process.env.MCP_AUTO_CONNECT !== "1") {
58
+ console.log("MCP host is using lazy workspace startup");
59
+ return;
60
+ }
61
+
62
+ const workspace: WorkspaceConfig | undefined = getDefaultWorkspace();
63
+ if (!workspace) {
64
+ console.log("MCP host has no default workspace to connect");
65
+ return;
66
+ }
67
+
68
+ await this.switchWorkspace(workspace);
69
+ }
70
+
71
+ async switchWorkspace(workspace: WorkspaceConfig): Promise<void> {
72
+ await this.ensureWorkspace(workspace);
73
+ this.activeWorkspaceId = workspace.id;
74
+ this.diagnosticsBridge.setWorkspace(workspace);
75
+ this.syncActiveDynamicTools();
76
+ console.log(`MCP active workspace: ${workspace.id} -> ${workspace.rootPath}`);
77
+ }
78
+
79
+ private async ensureWorkspace(workspace: WorkspaceConfig): Promise<void> {
80
+ if (this.workspaceSessions.has(workspace.id)) {
81
+ return;
82
+ }
83
+
84
+ const configs: McpServerConfig[] = [
85
+ ...buildMcpServerConfigs(workspace),
86
+ ...await buildCustomMcpServerConfigs(workspace)
87
+ ];
88
+ if (configs.length === 0) {
89
+ throw new Error(`MCP workspace has no project path: ${workspace.id}`);
90
+ }
91
+
92
+ const sessions: Map<string, McpSession> = new Map();
93
+
94
+ try {
95
+ for (const config of configs) {
96
+ const session: McpSession = new McpSession(config);
97
+ try {
98
+ await this.connectSession(config, session);
99
+ if (config.custom === true) {
100
+ await this.cacheCustomServerTools(workspace.id, config, session);
101
+ }
102
+ sessions.set(config.id, session);
103
+ console.log(`MCP session connected: ${workspace.id}/${config.id}`);
104
+ } catch (error: unknown) {
105
+ if (config.custom === true) {
106
+ await this.closeCustomSessionQuietly(session);
107
+ this.setCustomServerError(config.id, error);
108
+ console.warn(`Custom MCP session failed: ${workspace.id}/${config.id}:`, error instanceof Error ? error.message : error);
109
+ continue;
110
+ }
111
+
112
+ await session.close().catch((): void => undefined);
113
+ throw error;
114
+ }
115
+ }
116
+ } catch (error: unknown) {
117
+ for (const session of sessions.values()) {
118
+ await session.close().catch((): void => undefined);
119
+ }
120
+
121
+ throw error;
122
+ }
123
+
124
+ this.workspaceSessions.set(workspace.id, sessions);
125
+ }
126
+
127
+ private async connectSession(config: McpServerConfig, session: McpSession): Promise<void> {
128
+ if (config.custom === true) {
129
+ await withTimeout(
130
+ session.connect(),
131
+ CUSTOM_MCP_CONNECT_TIMEOUT_MS,
132
+ `Custom MCP "${config.name}" connect`
133
+ );
134
+ return;
135
+ }
136
+
137
+ await session.connect();
138
+ }
139
+
140
+ private async closeCustomSessionQuietly(session: McpSession): Promise<void> {
141
+ await withTimeout(
142
+ session.close(),
143
+ CUSTOM_MCP_CLOSE_TIMEOUT_MS,
144
+ `Custom MCP "${session.name}" close`
145
+ ).catch((): void => undefined);
146
+ }
147
+
148
+ private async cacheCustomServerTools(workspaceId: string, config: McpServerConfig, session: McpSession): Promise<void> {
149
+ const toolsResult: McpToolListResult = await withTimeout(
150
+ session.listTools(),
151
+ CUSTOM_MCP_LIST_TOOLS_TIMEOUT_MS,
152
+ `Custom MCP "${config.name}" listTools`
153
+ ) as McpToolListResult;
154
+ const toolSources: DynamicMcpToolSource[] = toolsResult.tools.map((tool): DynamicMcpToolSource => ({
155
+ serverId: config.id,
156
+ serverName: config.name,
157
+ toolName: tool.name,
158
+ description: tool.description,
159
+ inputSchema: tool.inputSchema
160
+ }));
161
+
162
+ let workspaceTools: Map<string, DynamicMcpToolSource[]> | undefined = this.workspaceCustomTools.get(workspaceId);
163
+ if (workspaceTools === undefined) {
164
+ workspaceTools = new Map();
165
+ this.workspaceCustomTools.set(workspaceId, workspaceTools);
166
+ }
167
+ workspaceTools.set(config.id, toolSources);
168
+ this.customServerStatuses.set(config.id, {
169
+ id: config.id,
170
+ status: "connected",
171
+ toolCount: toolSources.length
172
+ });
173
+ }
174
+
175
+ private setCustomServerError(serverId: string, error: unknown): void {
176
+ this.customServerStatuses.set(serverId, {
177
+ id: serverId,
178
+ status: "error",
179
+ toolCount: 0,
180
+ error: error instanceof Error ? error.message : "Custom MCP server failed"
181
+ });
182
+ }
183
+
184
+ private syncActiveDynamicTools(): void {
185
+ if (!this.activeWorkspaceId) {
186
+ replaceDynamicMcpTools([]);
187
+ return;
188
+ }
189
+
190
+ const workspaceTools: Map<string, DynamicMcpToolSource[]> | undefined = this.workspaceCustomTools.get(this.activeWorkspaceId);
191
+ if (workspaceTools === undefined) {
192
+ replaceDynamicMcpTools([]);
193
+ return;
194
+ }
195
+
196
+ replaceDynamicMcpTools(Array.from(workspaceTools.values()).flat());
197
+ }
198
+
199
+ async refreshCustomServersForActiveWorkspace(): Promise<void> {
200
+ if (!this.activeWorkspaceId) {
201
+ return;
202
+ }
203
+
204
+ const workspace: WorkspaceConfig | undefined = findWorkspace(this.activeWorkspaceId);
205
+ if (workspace === undefined) {
206
+ return;
207
+ }
208
+
209
+ await this.ensureWorkspace(workspace);
210
+ const sessions: Map<string, McpSession> | undefined = this.workspaceSessions.get(workspace.id);
211
+ if (sessions === undefined) {
212
+ return;
213
+ }
214
+
215
+ for (const [serverId, session] of sessions.entries()) {
216
+ if (!session.isCustom) {
217
+ continue;
218
+ }
219
+
220
+ await this.closeCustomSessionQuietly(session);
221
+ sessions.delete(serverId);
222
+ }
223
+
224
+ this.workspaceCustomTools.set(workspace.id, new Map());
225
+ const customConfigs: McpServerConfig[] = await buildCustomMcpServerConfigs(workspace);
226
+ const enabledCustomIds: Set<string> = new Set(customConfigs.map((config: McpServerConfig): string => config.id));
227
+ for (const serverId of this.customServerStatuses.keys()) {
228
+ if (!enabledCustomIds.has(serverId)) {
229
+ this.customServerStatuses.delete(serverId);
230
+ }
231
+ }
232
+
233
+ for (const config of customConfigs) {
234
+ const session: McpSession = new McpSession(config);
235
+ try {
236
+ await this.connectSession(config, session);
237
+ await this.cacheCustomServerTools(workspace.id, config, session);
238
+ sessions.set(config.id, session);
239
+ console.log(`Custom MCP session connected: ${workspace.id}/${config.id}`);
240
+ } catch (error: unknown) {
241
+ await this.closeCustomSessionQuietly(session);
242
+ this.setCustomServerError(config.id, error);
243
+ console.warn(`Custom MCP session failed: ${workspace.id}/${config.id}:`, error instanceof Error ? error.message : error);
244
+ }
245
+ }
246
+
247
+ this.syncActiveDynamicTools();
248
+ }
249
+
250
+ private getActiveSessions(): Map<string, McpSession> {
251
+ if (!this.activeWorkspaceId) {
252
+ throw new Error("MCP workspace is not selected");
253
+ }
254
+
255
+ const sessions: Map<string, McpSession> | undefined = this.workspaceSessions.get(this.activeWorkspaceId);
256
+ if (!sessions) {
257
+ throw new Error(`MCP workspace is not connected: ${this.activeWorkspaceId}`);
258
+ }
259
+
260
+ return sessions;
261
+ }
262
+
263
+ async closeWorkspace(workspaceId: string): Promise<void> {
264
+ const sessions: Map<string, McpSession> | undefined = this.workspaceSessions.get(workspaceId);
265
+ if (!sessions) {
266
+ return;
267
+ }
268
+
269
+ for (const session of sessions.values()) {
270
+ await session.close();
271
+ }
272
+
273
+ this.workspaceSessions.delete(workspaceId);
274
+ this.workspaceCustomTools.delete(workspaceId);
275
+
276
+ if (this.activeWorkspaceId === workspaceId) {
277
+ this.activeWorkspaceId = undefined;
278
+ this.diagnosticsBridge.clearWorkspace(workspaceId);
279
+ this.syncActiveDynamicTools();
280
+ }
281
+ }
282
+
283
+ getActiveWorkspaceId(): string | undefined {
284
+ return this.activeWorkspaceId;
285
+ }
286
+
287
+ getEditorBridge(): GodotEditorBridge {
288
+ return this.editorBridge;
289
+ }
290
+
291
+ getDiagnosticsBridge(): GodotDiagnosticsBridge {
292
+ return this.diagnosticsBridge;
293
+ }
294
+
295
+ getSession(id: string): McpSession {
296
+ const session: McpSession | undefined = this.getActiveSessions().get(id);
297
+
298
+ if (!session) {
299
+ throw new Error(`MCP session not found in active workspace: ${id}`);
300
+ }
301
+
302
+ return session;
303
+ }
304
+
305
+ getConnectedServerIds(): string[] {
306
+ if (!this.activeWorkspaceId) {
307
+ return this.editorBridge.isOnline() ? [GODOT_EDITOR_SERVER_ID] : [];
308
+ }
309
+
310
+ const sessions: Map<string, McpSession> | undefined = this.workspaceSessions.get(this.activeWorkspaceId);
311
+ if (!sessions) {
312
+ return this.editorBridge.isOnline() ? [GODOT_EDITOR_SERVER_ID] : [];
313
+ }
314
+
315
+ const serverIds: string[] = Array.from(sessions.keys());
316
+ serverIds.push(GODOT_DIAGNOSTICS_SERVER_ID);
317
+ if (this.editorBridge.isOnline()) {
318
+ serverIds.push(GODOT_EDITOR_SERVER_ID);
319
+ }
320
+ return serverIds.sort();
321
+ }
322
+
323
+ getConnectedWorkspaceIds(): string[] {
324
+ return Array.from(this.workspaceSessions.keys()).sort();
325
+ }
326
+
327
+ getCustomServerStatuses(): CustomMcpServerRuntimeStatus[] {
328
+ return Array.from(this.customServerStatuses.values());
329
+ }
330
+
331
+ async listTools(serverId: string) {
332
+ if (serverId === GODOT_EDITOR_SERVER_ID) {
333
+ return this.editorBridge.listTools();
334
+ }
335
+
336
+ if (serverId === GODOT_DIAGNOSTICS_SERVER_ID) {
337
+ return this.diagnosticsBridge.listTools();
338
+ }
339
+
340
+ return this.getSession(serverId).listTools();
341
+ }
342
+
343
+ async callTool(serverId: string, name: string, args: Record<string, unknown>) {
344
+ if (serverId === GODOT_EDITOR_SERVER_ID) {
345
+ return this.editorBridge.callTool(name, args);
346
+ }
347
+
348
+ if (serverId === GODOT_DIAGNOSTICS_SERVER_ID) {
349
+ return this.diagnosticsBridge.callTool(name, args);
350
+ }
351
+
352
+ return this.getSession(serverId).callTool(name, args);
353
+ }
354
+
355
+ async listResources(serverId: string) {
356
+ if (serverId === GODOT_EDITOR_SERVER_ID) {
357
+ return this.editorBridge.listResources();
358
+ }
359
+
360
+ if (serverId === GODOT_DIAGNOSTICS_SERVER_ID) {
361
+ return this.diagnosticsBridge.listResources();
362
+ }
363
+
364
+ return this.getSession(serverId).listResources();
365
+ }
366
+
367
+ async readResource(serverId: string, uri: string) {
368
+ if (serverId === GODOT_EDITOR_SERVER_ID) {
369
+ return this.editorBridge.readResource(uri);
370
+ }
371
+
372
+ if (serverId === GODOT_DIAGNOSTICS_SERVER_ID) {
373
+ return this.diagnosticsBridge.readResource(uri);
374
+ }
375
+
376
+ return this.getSession(serverId).readResource(uri);
377
+ }
378
+
379
+ async closeAll(): Promise<void> {
380
+ for (const sessions of this.workspaceSessions.values()) {
381
+ for (const session of sessions.values()) {
382
+ await session.close();
383
+ }
384
+ }
385
+
386
+ this.workspaceSessions.clear();
387
+ this.workspaceCustomTools.clear();
388
+ this.customServerStatuses.clear();
389
+ this.activeWorkspaceId = undefined;
390
+ this.diagnosticsBridge.clearWorkspace();
391
+ this.syncActiveDynamicTools();
392
+ }
393
+ }
@@ -0,0 +1,81 @@
1
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
2
+ import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import type { Transport } from "@modelcontextprotocol/sdk/shared/transport.js";
5
+ import type { McpServerConfig } from "./types.js";
6
+
7
+ export class McpSession {
8
+ private client: Client;
9
+ private transport: StdioClientTransport | StreamableHTTPClientTransport | undefined;
10
+
11
+ constructor(private readonly config: McpServerConfig) {
12
+ this.client = new Client({
13
+ name: `daedalus-${config.id}-client`,
14
+ version: "1.0.0"
15
+ });
16
+ }
17
+
18
+ async connect(): Promise<void> {
19
+ if (this.config.transport === "http") {
20
+ if (this.config.url === undefined) {
21
+ throw new Error(`HTTP MCP server has no URL: ${this.config.id}`);
22
+ }
23
+
24
+ this.transport = new StreamableHTTPClientTransport(new URL(this.config.url), {
25
+ requestInit: {
26
+ headers: this.config.headers ?? {}
27
+ }
28
+ });
29
+ } else {
30
+ if (this.config.command === undefined) {
31
+ throw new Error(`STDIO MCP server has no command: ${this.config.id}`);
32
+ }
33
+
34
+ this.transport = new StdioClientTransport({
35
+ command: this.config.command,
36
+ args: this.config.args ?? [],
37
+ env: {
38
+ ...Object.fromEntries(Object.entries(process.env).filter((entry): entry is [string, string] => entry[1] !== undefined)),
39
+ ...this.config.env
40
+ } as Record<string, string>
41
+ });
42
+ }
43
+
44
+ await this.client.connect(this.transport as unknown as Transport);
45
+ }
46
+
47
+ async listTools() {
48
+ return this.client.listTools();
49
+ }
50
+
51
+ async callTool(name: string, args: Record<string, unknown>) {
52
+ return this.client.callTool({
53
+ name,
54
+ arguments: args
55
+ });
56
+ }
57
+
58
+ async listResources() {
59
+ return this.client.listResources();
60
+ }
61
+
62
+ async readResource(uri: string) {
63
+ return this.client.readResource({ uri });
64
+ }
65
+
66
+ async close(): Promise<void> {
67
+ await this.client.close();
68
+ }
69
+
70
+ get id(): string {
71
+ return this.config.id;
72
+ }
73
+
74
+ get name(): string {
75
+ return this.config.name;
76
+ }
77
+
78
+ get isCustom(): boolean {
79
+ return this.config.custom === true;
80
+ }
81
+ }