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.
- package/README.md +101 -0
- package/bin/godot-daedalus-backend.js +4 -0
- package/bin/godot-daedalus-mcp.js +4 -0
- package/bin/godot-daedalus-terminal-mcp.js +4 -0
- package/bin/run-tsx-entry.js +26 -0
- package/package.json +54 -0
- package/scripts/deepseek-tokenizer-server.py +54 -0
- package/src/app-paths.ts +36 -0
- package/src/main.ts +21 -0
- package/src/mcp/content-length-protocol.ts +68 -0
- package/src/mcp/custom-mcp-config-store.ts +397 -0
- package/src/mcp/godot-diagnostics-bridge.ts +1298 -0
- package/src/mcp/godot-editor-bridge.ts +307 -0
- package/src/mcp/godot-mcp-server.ts +3484 -0
- package/src/mcp/godot-paths.ts +151 -0
- package/src/mcp/godot-project-settings.ts +233 -0
- package/src/mcp/godot-tool-registration.ts +46 -0
- package/src/mcp/mcp-config.ts +48 -0
- package/src/mcp/mcp-host.ts +393 -0
- package/src/mcp/mcp-session.ts +81 -0
- package/src/mcp/terminal-mcp-server.ts +576 -0
- package/src/mcp/tscn-tools.ts +302 -0
- package/src/mcp/types.ts +12 -0
- package/src/ping-client.ts +24 -0
- package/src/prompts/registry.ts +97 -0
- package/src/prompts/templates/backend-helper.md +25 -0
- package/src/prompts/templates/gdscript-reviewer.md +19 -0
- package/src/prompts/templates/godot-assistant.md +225 -0
- package/src/prompts/templates/scene-architect.md +15 -0
- package/src/prompts/templates/session-compressor.md +33 -0
- package/src/protocol/schema.ts +486 -0
- package/src/protocol/types.ts +77 -0
- package/src/providers/deepseek-agent.ts +1014 -0
- package/src/providers/deepseek-client.ts +114 -0
- package/src/providers/deepseek-dsml-tools.ts +90 -0
- package/src/providers/deepseek-loose-tools.ts +450 -0
- package/src/providers/provider-config-store.ts +164 -0
- package/src/server/client-session.ts +93 -0
- package/src/server/request-dispatcher.ts +74 -0
- package/src/server/response-helpers.ts +33 -0
- package/src/server/send-json.ts +8 -0
- package/src/server/websocket-server.ts +3997 -0
- package/src/session/session-compressor.ts +68 -0
- package/src/session/session-store.ts +669 -0
- package/src/skills/registry.ts +180 -0
- package/src/skills/templates/backend-helper.md +12 -0
- package/src/skills/templates/file-creator.md +14 -0
- package/src/skills/templates/gdscript-review.md +12 -0
- package/src/skills/templates/godot-project-init.md +29 -0
- package/src/skills/templates/scene-builder.md +12 -0
- package/src/tokens/deepseek-tokenizer-counter.ts +233 -0
- package/src/tokens/model-profiles.ts +38 -0
- package/src/tokens/token-counter-factory.ts +52 -0
- package/src/tokens/token-counter.ts +22 -0
- package/src/tools/approval-gateway.ts +111 -0
- package/src/tools/llm-tools.ts +1415 -0
- package/src/tools/tool-dispatcher.ts +147 -0
- package/src/tools/tool-event-describer.ts +387 -0
- package/src/tools/tool-idempotency.ts +373 -0
- package/src/tools/tool-policy-table.ts +61 -0
- package/src/tools/tool-policy.ts +73 -0
- package/src/workflow/llm-planner.ts +407 -0
- package/src/workflow/planner.ts +201 -0
- package/src/workflow/runner.ts +141 -0
- package/src/workflow/types.ts +69 -0
- package/src/workspace/registry.ts +104 -0
- package/src/workspace/types.ts +7 -0
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import OpenAI from "openai";
|
|
2
|
+
import type {
|
|
3
|
+
ChatCompletionChunk,
|
|
4
|
+
ChatCompletionCreateParamsBase,
|
|
5
|
+
ChatCompletionCreateParamsNonStreaming,
|
|
6
|
+
ChatCompletionCreateParamsStreaming,
|
|
7
|
+
ChatCompletionMessageParam
|
|
8
|
+
} from "openai/resources/chat/completions";
|
|
9
|
+
import type { AiChatParams, ChatMessage } from "../protocol/types.js";
|
|
10
|
+
|
|
11
|
+
const DEFAULT_BASE_URL = "https://api.deepseek.com";
|
|
12
|
+
const DEFAULT_MODEL = "deepseek-v4-flash";
|
|
13
|
+
|
|
14
|
+
export type DeepSeekChatOptions = {
|
|
15
|
+
apiKey: string;
|
|
16
|
+
baseUrl?: string | undefined;
|
|
17
|
+
model?: string | undefined;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export function createDeepSeekClient(options: DeepSeekChatOptions): OpenAI {
|
|
21
|
+
return new OpenAI({
|
|
22
|
+
baseURL: options.baseUrl ?? process.env.DEEPSEEK_BASE_URL ?? DEFAULT_BASE_URL,
|
|
23
|
+
apiKey: options.apiKey
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createMessages(params: AiChatParams, history: ChatMessage[], systemPrompt: string): ChatCompletionMessageParam[] {
|
|
28
|
+
return [
|
|
29
|
+
{
|
|
30
|
+
role: "system",
|
|
31
|
+
content: systemPrompt,
|
|
32
|
+
},
|
|
33
|
+
...history.map((message: ChatMessage): ChatCompletionMessageParam => ({
|
|
34
|
+
role: message.role,
|
|
35
|
+
content: message.content
|
|
36
|
+
})),
|
|
37
|
+
{
|
|
38
|
+
role: "user",
|
|
39
|
+
content: params.message,
|
|
40
|
+
}
|
|
41
|
+
];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function applyChatOptions(requestBody: ChatCompletionCreateParamsBase, params: AiChatParams): void {
|
|
45
|
+
if (params.options?.temperature !== undefined) {
|
|
46
|
+
requestBody.temperature = params.options.temperature;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (params.options?.topP !== undefined) {
|
|
50
|
+
requestBody.top_p = params.options.topP;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (params.options?.maxTokens !== undefined) {
|
|
54
|
+
requestBody.max_tokens = params.options.maxTokens;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (params.options?.stop !== undefined) {
|
|
58
|
+
requestBody.stop = params.options.stop;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (params.options?.responseFormat === "json") {
|
|
62
|
+
requestBody.response_format = { type: "json_object" };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export async function chatWithDeepSeek(
|
|
67
|
+
params: AiChatParams,
|
|
68
|
+
options: DeepSeekChatOptions,
|
|
69
|
+
history: ChatMessage[],
|
|
70
|
+
systemPrompt: string,
|
|
71
|
+
abortSignal?: AbortSignal | undefined
|
|
72
|
+
): Promise<string> {
|
|
73
|
+
const client: OpenAI = createDeepSeekClient(options);
|
|
74
|
+
const requestBody: ChatCompletionCreateParamsNonStreaming = {
|
|
75
|
+
model: options.model ?? process.env.DEEPSEEK_MODEL ?? DEFAULT_MODEL,
|
|
76
|
+
messages: createMessages(params, history, systemPrompt)
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
applyChatOptions(requestBody, params);
|
|
80
|
+
|
|
81
|
+
const completion = await client.chat.completions.create(requestBody, { signal: abortSignal });
|
|
82
|
+
|
|
83
|
+
const text: string | null | undefined = completion.choices[0]?.message.content;
|
|
84
|
+
if (!text) {
|
|
85
|
+
throw new Error("LLM returned empty response");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return text;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function* streamChatWithDeepSeek(
|
|
92
|
+
params: AiChatParams,
|
|
93
|
+
options: DeepSeekChatOptions,
|
|
94
|
+
history: ChatMessage[],
|
|
95
|
+
systemPrompt: string,
|
|
96
|
+
abortSignal?: AbortSignal | undefined
|
|
97
|
+
): AsyncGenerator<string> {
|
|
98
|
+
const client: OpenAI = createDeepSeekClient(options);
|
|
99
|
+
const requestBody: ChatCompletionCreateParamsStreaming = {
|
|
100
|
+
model: options.model ?? process.env.DEEPSEEK_MODEL ?? DEFAULT_MODEL,
|
|
101
|
+
messages: createMessages(params, history, systemPrompt),
|
|
102
|
+
stream: true
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
applyChatOptions(requestBody, params);
|
|
106
|
+
|
|
107
|
+
const stream = await client.chat.completions.create(requestBody, { signal: abortSignal });
|
|
108
|
+
for await (const chunk of stream) {
|
|
109
|
+
const delta: string | null | undefined = (chunk as ChatCompletionChunk).choices[0]?.delta.content;
|
|
110
|
+
if (delta !== undefined && delta !== null && delta.length > 0) {
|
|
111
|
+
yield delta;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { ChatCompletionMessageToolCall } from "openai/resources/chat/completions";
|
|
2
|
+
|
|
3
|
+
const DSML_PREFIX_PATTERN: string = "[||]+\\s*DSML\\s*[||]+";
|
|
4
|
+
const TOOL_CALLS_START_PATTERN: RegExp = new RegExp(`<\\s*${DSML_PREFIX_PATTERN}\\s*tool_calls\\s*>`, "i");
|
|
5
|
+
const TOOL_CALLS_BLOCK_PATTERN: RegExp = new RegExp(
|
|
6
|
+
`<\\s*${DSML_PREFIX_PATTERN}\\s*tool_calls\\s*>([\\s\\S]*?)(?:<\\/\\s*${DSML_PREFIX_PATTERN}\\s*tool_calls\\s*>|$)`,
|
|
7
|
+
"gi"
|
|
8
|
+
);
|
|
9
|
+
const INVOKE_PATTERN: RegExp = new RegExp(
|
|
10
|
+
`<\\s*${DSML_PREFIX_PATTERN}\\s*invoke\\s+name="([^"]+)"\\s*>([\\s\\S]*?)<\\/\\s*${DSML_PREFIX_PATTERN}\\s*invoke\\s*>`,
|
|
11
|
+
"gi"
|
|
12
|
+
);
|
|
13
|
+
const PARAMETER_PATTERN: RegExp = new RegExp(
|
|
14
|
+
`<\\s*${DSML_PREFIX_PATTERN}\\s*parameter\\s+name="([^"]+)"(?:\\s+string="([^"]+)")?\\s*>([\\s\\S]*?)<\\/\\s*${DSML_PREFIX_PATTERN}\\s*parameter\\s*>`,
|
|
15
|
+
"gi"
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
function decodeXmlEntities(text: string): string {
|
|
19
|
+
return text
|
|
20
|
+
.replaceAll(""", "\"")
|
|
21
|
+
.replaceAll("'", "'")
|
|
22
|
+
.replaceAll("<", "<")
|
|
23
|
+
.replaceAll(">", ">")
|
|
24
|
+
.replaceAll("&", "&");
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function parseParameterValue(rawValue: string, stringFlag: string | undefined): unknown {
|
|
28
|
+
const decoded: string = decodeXmlEntities(rawValue);
|
|
29
|
+
|
|
30
|
+
if (stringFlag === undefined || stringFlag.toLowerCase() === "true") {
|
|
31
|
+
return decoded;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(decoded) as unknown;
|
|
36
|
+
} catch {
|
|
37
|
+
return decoded;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function containsDsmlToolCalls(text: string | null | undefined): boolean {
|
|
42
|
+
return text !== null && text !== undefined && TOOL_CALLS_START_PATTERN.test(text);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function stripDsmlToolCalls(text: string): string {
|
|
46
|
+
return text.replace(TOOL_CALLS_BLOCK_PATTERN, "").trim();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function parseDsmlToolCalls(text: string, idPrefix: string = "dsml-tool"): ChatCompletionMessageToolCall[] {
|
|
50
|
+
const toolCalls: ChatCompletionMessageToolCall[] = [];
|
|
51
|
+
let blockMatch: RegExpExecArray | null;
|
|
52
|
+
|
|
53
|
+
TOOL_CALLS_BLOCK_PATTERN.lastIndex = 0;
|
|
54
|
+
while ((blockMatch = TOOL_CALLS_BLOCK_PATTERN.exec(text)) !== null) {
|
|
55
|
+
const block: string = blockMatch[1] ?? "";
|
|
56
|
+
let invokeMatch: RegExpExecArray | null;
|
|
57
|
+
|
|
58
|
+
INVOKE_PATTERN.lastIndex = 0;
|
|
59
|
+
while ((invokeMatch = INVOKE_PATTERN.exec(block)) !== null) {
|
|
60
|
+
const toolName: string = invokeMatch[1] ?? "";
|
|
61
|
+
const invokeBody: string = invokeMatch[2] ?? "";
|
|
62
|
+
const args: Record<string, unknown> = {};
|
|
63
|
+
let parameterMatch: RegExpExecArray | null;
|
|
64
|
+
|
|
65
|
+
PARAMETER_PATTERN.lastIndex = 0;
|
|
66
|
+
while ((parameterMatch = PARAMETER_PATTERN.exec(invokeBody)) !== null) {
|
|
67
|
+
const parameterName: string = parameterMatch[1] ?? "";
|
|
68
|
+
const stringFlag: string | undefined = parameterMatch[2];
|
|
69
|
+
const parameterValue: string = parameterMatch[3] ?? "";
|
|
70
|
+
|
|
71
|
+
if (parameterName.length > 0) {
|
|
72
|
+
args[parameterName] = parseParameterValue(parameterValue, stringFlag);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (toolName.length > 0) {
|
|
77
|
+
toolCalls.push({
|
|
78
|
+
id: `${idPrefix}-${toolCalls.length + 1}`,
|
|
79
|
+
type: "function",
|
|
80
|
+
function: {
|
|
81
|
+
name: toolName,
|
|
82
|
+
arguments: JSON.stringify(args)
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return toolCalls;
|
|
90
|
+
}
|
|
@@ -0,0 +1,450 @@
|
|
|
1
|
+
import type { ChatCompletionMessageToolCall } from "openai/resources/chat/completions";
|
|
2
|
+
|
|
3
|
+
const XML_NAME_PATTERN: string = "[A-Za-z_][A-Za-z0-9_.:-]*";
|
|
4
|
+
const TOOL_TAG_PATTERN: RegExp = new RegExp(`<\\s*(${XML_NAME_PATTERN})\\s*>([\\s\\S]*?)<\\/\\s*\\1\\s*>`, "g");
|
|
5
|
+
const SELF_CLOSING_TOOL_TAG_PATTERN: RegExp = new RegExp(`<\\s*(${XML_NAME_PATTERN})([^<>]*?)\\/\\s*>`, "g");
|
|
6
|
+
const PARAMETER_TAG_PATTERN: RegExp = new RegExp(`<\\s*(${XML_NAME_PATTERN})\\s*>([\\s\\S]*?)<\\/\\s*\\1\\s*>`, "g");
|
|
7
|
+
const ATTRIBUTE_PATTERN: RegExp = new RegExp(`(${XML_NAME_PATTERN})\\s*=\\s*(?:"([^"]*)"|'([^']*)')`, "g");
|
|
8
|
+
|
|
9
|
+
const RAW_TOOL_NAME_MAP: Readonly<Record<string, string>> = {
|
|
10
|
+
get_project_summary: "mcp_godot_get_project_summary",
|
|
11
|
+
list_project_files: "mcp_godot_list_project_files",
|
|
12
|
+
list_scenes: "mcp_godot_list_scenes",
|
|
13
|
+
list_scripts: "mcp_godot_list_scripts",
|
|
14
|
+
read_text_file: "mcp_godot_read_text_file",
|
|
15
|
+
search_text: "mcp_godot_search_text",
|
|
16
|
+
get_project_log_config: "mcp_godot_get_project_log_config",
|
|
17
|
+
list_project_logs: "mcp_godot_list_project_logs",
|
|
18
|
+
read_project_log: "mcp_godot_read_project_log",
|
|
19
|
+
get_project_settings: "mcp_godot_get_project_settings",
|
|
20
|
+
get_editor_config_summary: "mcp_godot_get_editor_config_summary",
|
|
21
|
+
get_editor_settings: "mcp_godot_get_editor_settings",
|
|
22
|
+
list_editor_config_files: "mcp_godot_list_editor_config_files",
|
|
23
|
+
read_editor_config_file: "mcp_godot_read_editor_config_file",
|
|
24
|
+
get_editor_project_state: "mcp_godot_get_editor_project_state",
|
|
25
|
+
get_recent_projects: "mcp_godot_get_recent_projects",
|
|
26
|
+
propose_set_project_setting: "mcp_godot_propose_set_project_setting",
|
|
27
|
+
set_project_setting: "mcp_godot_set_project_setting",
|
|
28
|
+
propose_unset_project_setting: "mcp_godot_propose_unset_project_setting",
|
|
29
|
+
unset_project_setting: "mcp_godot_unset_project_setting",
|
|
30
|
+
propose_create_text_file: "mcp_godot_propose_create_text_file",
|
|
31
|
+
create_text_file: "mcp_godot_create_text_file",
|
|
32
|
+
propose_overwrite_text_file: "mcp_godot_propose_overwrite_text_file",
|
|
33
|
+
overwrite_text_file: "mcp_godot_overwrite_text_file",
|
|
34
|
+
propose_replace_text_in_file: "mcp_godot_propose_replace_text_in_file",
|
|
35
|
+
replace_text_in_file: "mcp_godot_replace_text_in_file",
|
|
36
|
+
delete_file: "mcp_godot_delete_file",
|
|
37
|
+
get_terminal_capabilities: "mcp_terminal_get_capabilities",
|
|
38
|
+
run_safe_preset: "mcp_terminal_run_safe_preset",
|
|
39
|
+
run_write_preset: "mcp_terminal_run_write_preset",
|
|
40
|
+
run_godot_scene_script: "mcp_terminal_run_godot_scene_script",
|
|
41
|
+
inspect_scene_tree: "mcp_godot_inspect_scene_tree",
|
|
42
|
+
propose_create_scene: "mcp_godot_propose_create_scene",
|
|
43
|
+
create_scene: "mcp_godot_create_scene",
|
|
44
|
+
propose_add_node_to_scene: "mcp_godot_propose_add_node_to_scene",
|
|
45
|
+
add_node_to_scene: "mcp_godot_add_node_to_scene",
|
|
46
|
+
propose_attach_script_to_node: "mcp_godot_propose_attach_script_to_node",
|
|
47
|
+
attach_script_to_node: "mcp_godot_attach_script_to_node",
|
|
48
|
+
propose_connect_signal_in_scene: "mcp_godot_propose_connect_signal_in_scene",
|
|
49
|
+
connect_signal_in_scene: "mcp_godot_connect_signal_in_scene",
|
|
50
|
+
propose_apply_scene_patch: "mcp_godot_propose_apply_scene_patch",
|
|
51
|
+
apply_scene_patch: "mcp_godot_apply_scene_patch",
|
|
52
|
+
editor_get_context: "mcp_godot_editor_get_context",
|
|
53
|
+
get_editor_context: "mcp_godot_editor_get_context",
|
|
54
|
+
editor_get_selected_nodes: "mcp_godot_editor_get_selected_nodes",
|
|
55
|
+
get_selected_nodes: "mcp_godot_editor_get_selected_nodes",
|
|
56
|
+
editor_inspect_node: "mcp_godot_editor_inspect_node",
|
|
57
|
+
inspect_live_node: "mcp_godot_editor_inspect_node",
|
|
58
|
+
editor_apply_scene_patch: "mcp_godot_editor_apply_scene_patch",
|
|
59
|
+
apply_editor_scene_patch: "mcp_godot_editor_apply_scene_patch",
|
|
60
|
+
lsp_get_status: "mcp_godot_lsp_get_status",
|
|
61
|
+
get_lsp_status: "mcp_godot_lsp_get_status",
|
|
62
|
+
lsp_get_file_diagnostics: "mcp_godot_lsp_get_file_diagnostics",
|
|
63
|
+
get_file_diagnostics: "mcp_godot_lsp_get_file_diagnostics",
|
|
64
|
+
lsp_get_document_symbols: "mcp_godot_lsp_get_document_symbols",
|
|
65
|
+
get_document_symbols: "mcp_godot_lsp_get_document_symbols",
|
|
66
|
+
lsp_hover: "mcp_godot_lsp_hover",
|
|
67
|
+
hover: "mcp_godot_lsp_hover",
|
|
68
|
+
lsp_goto_definition: "mcp_godot_lsp_goto_definition",
|
|
69
|
+
goto_definition: "mcp_godot_lsp_goto_definition",
|
|
70
|
+
dap_get_status: "mcp_godot_dap_get_status",
|
|
71
|
+
get_dap_status: "mcp_godot_dap_get_status",
|
|
72
|
+
dap_get_last_error: "mcp_godot_dap_get_last_error",
|
|
73
|
+
get_last_error: "mcp_godot_dap_get_last_error",
|
|
74
|
+
dap_get_stack_trace: "mcp_godot_dap_get_stack_trace",
|
|
75
|
+
get_stack_trace: "mcp_godot_dap_get_stack_trace",
|
|
76
|
+
dap_get_variables: "mcp_godot_dap_get_variables",
|
|
77
|
+
get_variables: "mcp_godot_dap_get_variables"
|
|
78
|
+
};
|
|
79
|
+
const RAW_TOOL_NAMES: readonly string[] = Object.keys(RAW_TOOL_NAME_MAP);
|
|
80
|
+
|
|
81
|
+
const RELATIVE_PATH_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
82
|
+
"mcp_godot_read_text_file",
|
|
83
|
+
"mcp_godot_propose_create_text_file",
|
|
84
|
+
"mcp_godot_create_text_file",
|
|
85
|
+
"mcp_godot_propose_overwrite_text_file",
|
|
86
|
+
"mcp_godot_overwrite_text_file",
|
|
87
|
+
"mcp_godot_propose_replace_text_in_file",
|
|
88
|
+
"mcp_godot_replace_text_in_file",
|
|
89
|
+
"mcp_godot_delete_file",
|
|
90
|
+
"mcp_godot_inspect_scene_tree",
|
|
91
|
+
"mcp_godot_propose_create_scene",
|
|
92
|
+
"mcp_godot_create_scene"
|
|
93
|
+
]);
|
|
94
|
+
|
|
95
|
+
const SCENE_PATH_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
96
|
+
"mcp_godot_propose_add_node_to_scene",
|
|
97
|
+
"mcp_godot_add_node_to_scene",
|
|
98
|
+
"mcp_godot_propose_attach_script_to_node",
|
|
99
|
+
"mcp_godot_attach_script_to_node",
|
|
100
|
+
"mcp_godot_propose_connect_signal_in_scene",
|
|
101
|
+
"mcp_godot_connect_signal_in_scene",
|
|
102
|
+
"mcp_godot_propose_apply_scene_patch",
|
|
103
|
+
"mcp_godot_apply_scene_patch",
|
|
104
|
+
"mcp_godot_editor_apply_scene_patch"
|
|
105
|
+
]);
|
|
106
|
+
|
|
107
|
+
const RESOURCE_PATH_TOOL_NAMES: ReadonlySet<string> = new Set([
|
|
108
|
+
"mcp_godot_lsp_get_file_diagnostics",
|
|
109
|
+
"mcp_godot_lsp_get_document_symbols",
|
|
110
|
+
"mcp_godot_lsp_hover",
|
|
111
|
+
"mcp_godot_lsp_goto_definition"
|
|
112
|
+
]);
|
|
113
|
+
|
|
114
|
+
function decodeXmlEntities(text: string): string {
|
|
115
|
+
return text
|
|
116
|
+
.replaceAll(""", "\"")
|
|
117
|
+
.replaceAll("'", "'")
|
|
118
|
+
.replaceAll("<", "<")
|
|
119
|
+
.replaceAll(">", ">")
|
|
120
|
+
.replaceAll("&", "&");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function getXmlLocalName(name: string): string {
|
|
124
|
+
const namespaceSeparatorIndex: number = name.lastIndexOf(":");
|
|
125
|
+
if (namespaceSeparatorIndex < 0) {
|
|
126
|
+
return name;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return name.slice(namespaceSeparatorIndex + 1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function parseLooseValue(rawValue: string): unknown {
|
|
133
|
+
const decoded: string = decodeXmlEntities(rawValue).trim();
|
|
134
|
+
if (decoded.length === 0) {
|
|
135
|
+
return "";
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (
|
|
139
|
+
decoded.startsWith("{")
|
|
140
|
+
|| decoded.startsWith("[")
|
|
141
|
+
|| decoded.startsWith("\"")
|
|
142
|
+
|| decoded === "true"
|
|
143
|
+
|| decoded === "false"
|
|
144
|
+
|| decoded === "null"
|
|
145
|
+
|| /^-?\d+(?:\.\d+)?$/.test(decoded)
|
|
146
|
+
) {
|
|
147
|
+
try {
|
|
148
|
+
return JSON.parse(decoded) as unknown;
|
|
149
|
+
} catch {
|
|
150
|
+
return decoded;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return decoded;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function normalizeKnownToolName(toolName: string): string | undefined {
|
|
158
|
+
const localToolName: string = getXmlLocalName(toolName);
|
|
159
|
+
if (localToolName.startsWith("mcp_")) {
|
|
160
|
+
return localToolName;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return RAW_TOOL_NAME_MAP[localToolName];
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export function isKnownLooseToolTagName(toolName: string): boolean {
|
|
167
|
+
const localToolName: string = getXmlLocalName(toolName);
|
|
168
|
+
return localToolName.startsWith("mcp_") || RAW_TOOL_NAME_MAP[localToolName] !== undefined;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function isPotentialLooseToolTagName(toolNamePrefix: string): boolean {
|
|
172
|
+
const localToolNamePrefix: string = getXmlLocalName(toolNamePrefix);
|
|
173
|
+
return localToolNamePrefix.length > 0 && (
|
|
174
|
+
"mcp_".startsWith(localToolNamePrefix)
|
|
175
|
+
|| localToolNamePrefix.startsWith("mcp_")
|
|
176
|
+
|| RAW_TOOL_NAMES.some((toolName: string): boolean => toolName.startsWith(localToolNamePrefix))
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function normalizeParameterName(toolName: string, parameterName: string): string {
|
|
181
|
+
const localParameterName: string = getXmlLocalName(parameterName);
|
|
182
|
+
if (toolName === "mcp_godot_read_project_log" && (localParameterName === "path" || localParameterName === "resourcePath" || localParameterName === "file")) {
|
|
183
|
+
return "fileName";
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (toolName === "mcp_godot_read_editor_config_file" && (localParameterName === "path" || localParameterName === "resourcePath" || localParameterName === "file")) {
|
|
187
|
+
return "fileId";
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (
|
|
191
|
+
(toolName === "mcp_godot_get_editor_settings" || toolName === "mcp_godot_get_project_settings")
|
|
192
|
+
&& (localParameterName === "setting" || localParameterName === "settingKey")
|
|
193
|
+
) {
|
|
194
|
+
return "prefix";
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (
|
|
198
|
+
(toolName === "mcp_godot_propose_set_project_setting" || toolName === "mcp_godot_set_project_setting")
|
|
199
|
+
&& (localParameterName === "value" || localParameterName === "expression")
|
|
200
|
+
) {
|
|
201
|
+
return "valueExpression";
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
(toolName === "mcp_godot_get_project_settings"
|
|
206
|
+
|| toolName === "mcp_godot_propose_set_project_setting"
|
|
207
|
+
|| toolName === "mcp_godot_set_project_setting"
|
|
208
|
+
|| toolName === "mcp_godot_propose_unset_project_setting"
|
|
209
|
+
|| toolName === "mcp_godot_unset_project_setting")
|
|
210
|
+
&& (localParameterName === "setting" || localParameterName === "settingKey")
|
|
211
|
+
) {
|
|
212
|
+
return "key";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (localParameterName === "path" || localParameterName === "resourcePath") {
|
|
216
|
+
if (SCENE_PATH_TOOL_NAMES.has(toolName)) {
|
|
217
|
+
return "scenePath";
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
if (RELATIVE_PATH_TOOL_NAMES.has(toolName)) {
|
|
221
|
+
return "relativePath";
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
if (RESOURCE_PATH_TOOL_NAMES.has(toolName)) {
|
|
225
|
+
return "resourcePath";
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
if (localParameterName === "preset") {
|
|
230
|
+
return "presetName";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (localParameterName === "operation") {
|
|
234
|
+
return "operationJson";
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return localParameterName;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function defaultParameterName(toolName: string): string | undefined {
|
|
241
|
+
if (SCENE_PATH_TOOL_NAMES.has(toolName)) {
|
|
242
|
+
return "scenePath";
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (RELATIVE_PATH_TOOL_NAMES.has(toolName)) {
|
|
246
|
+
return "relativePath";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (RESOURCE_PATH_TOOL_NAMES.has(toolName)) {
|
|
250
|
+
return "resourcePath";
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (toolName === "mcp_godot_search_text") {
|
|
254
|
+
return "query";
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
if (toolName === "mcp_godot_editor_inspect_node") {
|
|
258
|
+
return "nodePath";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (toolName === "mcp_godot_dap_get_variables") {
|
|
262
|
+
return "variablesReference";
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (toolName === "mcp_godot_read_project_log") {
|
|
266
|
+
return "fileName";
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
if (toolName === "mcp_godot_read_editor_config_file") {
|
|
270
|
+
return "fileId";
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (
|
|
274
|
+
toolName === "mcp_godot_propose_set_project_setting"
|
|
275
|
+
|| toolName === "mcp_godot_set_project_setting"
|
|
276
|
+
|| toolName === "mcp_godot_propose_unset_project_setting"
|
|
277
|
+
|| toolName === "mcp_godot_unset_project_setting"
|
|
278
|
+
) {
|
|
279
|
+
return "key";
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (toolName === "mcp_terminal_run_safe_preset" || toolName === "mcp_terminal_run_write_preset") {
|
|
283
|
+
return "presetName";
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return undefined;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
function parseLooseParameterValue(parameterName: string, rawValue: string): unknown {
|
|
290
|
+
if (parameterName === "valueExpression") {
|
|
291
|
+
return decodeXmlEntities(rawValue).trim();
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return parseLooseValue(rawValue);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function parseLooseArguments(toolName: string, body: string): Record<string, unknown> {
|
|
298
|
+
const args: Record<string, unknown> = {};
|
|
299
|
+
let foundParameter: boolean = false;
|
|
300
|
+
let parameterMatch: RegExpExecArray | null;
|
|
301
|
+
|
|
302
|
+
PARAMETER_TAG_PATTERN.lastIndex = 0;
|
|
303
|
+
while ((parameterMatch = PARAMETER_TAG_PATTERN.exec(body)) !== null) {
|
|
304
|
+
const rawParameterName: string = parameterMatch[1] ?? "";
|
|
305
|
+
const rawParameterValue: string = parameterMatch[2] ?? "";
|
|
306
|
+
if (rawParameterName.length === 0) {
|
|
307
|
+
continue;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
foundParameter = true;
|
|
311
|
+
const parameterName: string = normalizeParameterName(toolName, rawParameterName);
|
|
312
|
+
args[parameterName] = parseLooseParameterValue(parameterName, rawParameterValue);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (!foundParameter) {
|
|
316
|
+
const fallbackName: string | undefined = defaultParameterName(toolName);
|
|
317
|
+
const fallbackValue: unknown = fallbackName === undefined ? undefined : parseLooseParameterValue(fallbackName, body);
|
|
318
|
+
if (fallbackName !== undefined && typeof fallbackValue === "string" && fallbackValue.length > 0) {
|
|
319
|
+
args[fallbackName] = fallbackValue;
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
return args;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function parseLooseAttributeArguments(toolName: string, attributesText: string): Record<string, unknown> {
|
|
327
|
+
const args: Record<string, unknown> = {};
|
|
328
|
+
let attributeMatch: RegExpExecArray | null;
|
|
329
|
+
|
|
330
|
+
ATTRIBUTE_PATTERN.lastIndex = 0;
|
|
331
|
+
while ((attributeMatch = ATTRIBUTE_PATTERN.exec(attributesText)) !== null) {
|
|
332
|
+
const rawParameterName: string = attributeMatch[1] ?? "";
|
|
333
|
+
const rawParameterValue: string = attributeMatch[2] ?? attributeMatch[3] ?? "";
|
|
334
|
+
if (rawParameterName.length === 0) {
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const parameterName: string = normalizeParameterName(toolName, rawParameterName);
|
|
339
|
+
args[parameterName] = parseLooseParameterValue(parameterName, rawParameterValue);
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
return args;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function isAllowedToolName(toolName: string, allowedToolNames?: ReadonlySet<string>): boolean {
|
|
346
|
+
return allowedToolNames === undefined || allowedToolNames.has(toolName);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function containsLooseToolCalls(
|
|
350
|
+
text: string | null | undefined,
|
|
351
|
+
allowedToolNames?: ReadonlySet<string>
|
|
352
|
+
): boolean {
|
|
353
|
+
if (text === null || text === undefined) {
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
let tagMatch: RegExpExecArray | null;
|
|
358
|
+
TOOL_TAG_PATTERN.lastIndex = 0;
|
|
359
|
+
while ((tagMatch = TOOL_TAG_PATTERN.exec(text)) !== null) {
|
|
360
|
+
const rawToolName: string = tagMatch[1] ?? "";
|
|
361
|
+
const toolName: string | undefined = normalizeKnownToolName(rawToolName);
|
|
362
|
+
if (toolName !== undefined && isAllowedToolName(toolName, allowedToolNames)) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
SELF_CLOSING_TOOL_TAG_PATTERN.lastIndex = 0;
|
|
368
|
+
while ((tagMatch = SELF_CLOSING_TOOL_TAG_PATTERN.exec(text)) !== null) {
|
|
369
|
+
const rawToolName: string = tagMatch[1] ?? "";
|
|
370
|
+
const toolName: string | undefined = normalizeKnownToolName(rawToolName);
|
|
371
|
+
if (toolName !== undefined && isAllowedToolName(toolName, allowedToolNames)) {
|
|
372
|
+
return true;
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
return false;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function stripLooseToolCalls(text: string, allowedToolNames?: ReadonlySet<string>): string {
|
|
380
|
+
return text.replace(
|
|
381
|
+
TOOL_TAG_PATTERN,
|
|
382
|
+
(match: string, rawToolName: string): string => {
|
|
383
|
+
const toolName: string | undefined = normalizeKnownToolName(rawToolName);
|
|
384
|
+
if (toolName !== undefined && isAllowedToolName(toolName, allowedToolNames)) {
|
|
385
|
+
return "";
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
return match;
|
|
389
|
+
}
|
|
390
|
+
).replace(
|
|
391
|
+
SELF_CLOSING_TOOL_TAG_PATTERN,
|
|
392
|
+
(match: string, rawToolName: string): string => {
|
|
393
|
+
const toolName: string | undefined = normalizeKnownToolName(rawToolName);
|
|
394
|
+
if (toolName !== undefined && isAllowedToolName(toolName, allowedToolNames)) {
|
|
395
|
+
return "";
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return match;
|
|
399
|
+
}
|
|
400
|
+
).trim();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
export function parseLooseToolCalls(
|
|
404
|
+
text: string,
|
|
405
|
+
idPrefix: string = "loose-tool",
|
|
406
|
+
allowedToolNames?: ReadonlySet<string>
|
|
407
|
+
): ChatCompletionMessageToolCall[] {
|
|
408
|
+
const toolCalls: ChatCompletionMessageToolCall[] = [];
|
|
409
|
+
let tagMatch: RegExpExecArray | null;
|
|
410
|
+
|
|
411
|
+
TOOL_TAG_PATTERN.lastIndex = 0;
|
|
412
|
+
while ((tagMatch = TOOL_TAG_PATTERN.exec(text)) !== null) {
|
|
413
|
+
const rawToolName: string = tagMatch[1] ?? "";
|
|
414
|
+
const body: string = tagMatch[2] ?? "";
|
|
415
|
+
const toolName: string | undefined = normalizeKnownToolName(rawToolName);
|
|
416
|
+
if (toolName === undefined || !isAllowedToolName(toolName, allowedToolNames)) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
toolCalls.push({
|
|
421
|
+
id: `${idPrefix}-${toolCalls.length + 1}`,
|
|
422
|
+
type: "function",
|
|
423
|
+
function: {
|
|
424
|
+
name: toolName,
|
|
425
|
+
arguments: JSON.stringify(parseLooseArguments(toolName, body))
|
|
426
|
+
}
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
SELF_CLOSING_TOOL_TAG_PATTERN.lastIndex = 0;
|
|
431
|
+
while ((tagMatch = SELF_CLOSING_TOOL_TAG_PATTERN.exec(text)) !== null) {
|
|
432
|
+
const rawToolName: string = tagMatch[1] ?? "";
|
|
433
|
+
const attributesText: string = tagMatch[2] ?? "";
|
|
434
|
+
const toolName: string | undefined = normalizeKnownToolName(rawToolName);
|
|
435
|
+
if (toolName === undefined || !isAllowedToolName(toolName, allowedToolNames)) {
|
|
436
|
+
continue;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
toolCalls.push({
|
|
440
|
+
id: `${idPrefix}-${toolCalls.length + 1}`,
|
|
441
|
+
type: "function",
|
|
442
|
+
function: {
|
|
443
|
+
name: toolName,
|
|
444
|
+
arguments: JSON.stringify(parseLooseAttributeArguments(toolName, attributesText))
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
return toolCalls;
|
|
450
|
+
}
|