@lwmxiaobei/xbcode 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.
@@ -0,0 +1,275 @@
1
+ import { McpClientConnection } from "./client.js";
2
+ import { McpRuntimeError } from "./types.js";
3
+ import { ellipsize } from "../utils.js";
4
+ // 状态报告里把时间戳统一格式化为 ISO,便于日志和排查问题时直接比对。
5
+ function formatTimestamp(value) {
6
+ if (!value) {
7
+ return "never";
8
+ }
9
+ return new Date(value).toISOString();
10
+ }
11
+ // 报告里只展示少量名字,避免大量工具或资源把输出刷满。
12
+ function joinNames(values, limit = 4) {
13
+ if (values.length === 0) {
14
+ return "(none)";
15
+ }
16
+ const preview = values.slice(0, limit);
17
+ const suffix = values.length > limit ? `, +${values.length - limit} more` : "";
18
+ return `${preview.join(", ")}${suffix}`;
19
+ }
20
+ // McpManager 管理“多个服务连接”的生命周期。
21
+ // 它本身不直接和 SDK 通信,而是把工作分发给每个 McpClientConnection。
22
+ export class McpManager {
23
+ connections = new Map();
24
+ configWarnings = [];
25
+ initialized = false;
26
+ configSignature = "";
27
+ // 根据最新配置增删改连接对象。
28
+ // 如果配置签名没变,直接跳过,避免重复关闭和重建连接。
29
+ async configure(configs, configWarnings = []) {
30
+ const signature = JSON.stringify({ configs, configWarnings });
31
+ if (signature === this.configSignature) {
32
+ return;
33
+ }
34
+ const nextConnections = new Map();
35
+ for (const config of configs) {
36
+ const existing = this.connections.get(config.name);
37
+ if (existing) {
38
+ existing.updateConfig(config);
39
+ nextConnections.set(config.name, existing);
40
+ }
41
+ else {
42
+ nextConnections.set(config.name, new McpClientConnection(config));
43
+ }
44
+ }
45
+ const removed = [...this.connections.entries()]
46
+ .filter(([name]) => !nextConnections.has(name))
47
+ .map(([, connection]) => connection);
48
+ await Promise.allSettled(removed.map((connection) => connection.close()));
49
+ const disabled = [...nextConnections.values()]
50
+ .filter((connection) => !connection.getState().enabled);
51
+ await Promise.allSettled(disabled.map((connection) => connection.close()));
52
+ this.connections = nextConnections;
53
+ this.configWarnings = [...configWarnings];
54
+ this.configSignature = signature;
55
+ this.initialized = false;
56
+ }
57
+ isInitialized() {
58
+ return this.initialized;
59
+ }
60
+ // 仅初始化启用的服务,并容忍个别服务初始化失败。
61
+ async initializeAll() {
62
+ if (this.initialized) {
63
+ return;
64
+ }
65
+ const enabledConnections = [...this.connections.values()]
66
+ .filter((connection) => connection.getState().enabled);
67
+ await Promise.allSettled(enabledConnections.map((connection) => connection.initialize()));
68
+ this.initialized = true;
69
+ }
70
+ // 刷新单个服务时做精确刷新;不传名称时刷新全部服务缓存和连接状态。
71
+ async refresh(serverName) {
72
+ if (serverName) {
73
+ const connection = this.connections.get(serverName);
74
+ if (!connection) {
75
+ throw new McpRuntimeError("server_not_found", `MCP server "${serverName}" is not configured.`);
76
+ }
77
+ await connection.refresh();
78
+ this.initialized = true;
79
+ return;
80
+ }
81
+ await Promise.allSettled([...this.connections.values()].map((connection) => connection.refresh()));
82
+ this.initialized = true;
83
+ }
84
+ // 返回排序后的服务状态,保证上层展示稳定,不受 Map 插入顺序影响。
85
+ listServers() {
86
+ return [...this.connections.values()]
87
+ .map((connection) => connection.getState())
88
+ .sort((left, right) => left.name.localeCompare(right.name));
89
+ }
90
+ // 把完整状态裁剪成更适合模型消费的能力摘要。
91
+ listCapabilities() {
92
+ return this.listServers().map((state) => ({
93
+ server: state.name,
94
+ status: state.status,
95
+ supportedKinds: this.getSupportedKinds(state),
96
+ toolCount: state.cache.tools.length,
97
+ resourceCount: state.cache.resources.length,
98
+ promptCount: state.cache.prompts.length,
99
+ tools: state.cache.tools.map((tool) => tool.name),
100
+ resources: state.cache.resources.map((resource) => resource.uri),
101
+ prompts: state.cache.prompts.map((prompt) => prompt.name),
102
+ error: state.error,
103
+ }));
104
+ }
105
+ getStatusSummary() {
106
+ return this.computeStatusSummary(this.listServers());
107
+ }
108
+ getConfigWarnings() {
109
+ return [...this.configWarnings];
110
+ }
111
+ // 生成简短的提示词摘要,告诉模型当前有哪些 MCP 服务和能力可用。
112
+ buildPromptSummary() {
113
+ const states = this.listServers().filter((state) => state.enabled);
114
+ if (states.length === 0) {
115
+ return "MCP runtime: no configured servers.";
116
+ }
117
+ const lines = [
118
+ "MCP runtime:",
119
+ "- MCP tools are exposed as regular function tools. Call those tools directly.",
120
+ "- Use `list_mcp_resources` and `read_mcp_resource` for MCP resource access.",
121
+ "- Use `mcp_call` only for MCP prompt access.",
122
+ "- Unsupported in this client: sampling, roots, elicitation.",
123
+ "- Use exact cached server, resource, and prompt names. Do not guess missing names.",
124
+ ];
125
+ for (const state of states.slice(0, 6)) {
126
+ const supportedKinds = this.getSupportedKinds(state);
127
+ const capabilities = supportedKinds.join(", ") || "none";
128
+ const resources = joinNames(state.cache.resources.map((resource) => resource.uri));
129
+ const prompts = joinNames(state.cache.prompts.map((prompt) => prompt.name));
130
+ const errorSuffix = state.error ? ` error=${ellipsize(state.error, 120)}` : "";
131
+ lines.push(`- ${state.name} [${state.status}] capabilities=${capabilities} tools=${state.cache.tools.length} resources=${resources} prompts=${prompts}${errorSuffix}`);
132
+ }
133
+ if (states.length > 6) {
134
+ lines.push(`- ${states.length - 6} more MCP server(s) omitted from prompt summary.`);
135
+ }
136
+ if (this.configWarnings.length > 0) {
137
+ lines.push(`- Config warnings present. Use /mcp to inspect them.`);
138
+ }
139
+ return lines.join("\n");
140
+ }
141
+ // 启动摘要聚焦“有哪些可用 server 和 tool”,方便用户一眼确认当前连接面。
142
+ formatStartupReport() {
143
+ const states = this.listServers().filter((state) => state.enabled);
144
+ const lines = ["MCP servers on startup:"];
145
+ if (states.length === 0) {
146
+ lines.push("(no enabled MCP servers)");
147
+ }
148
+ else {
149
+ for (const state of states) {
150
+ lines.push(`- ${state.name} [${state.status}] ${state.transport}`);
151
+ if (state.error) {
152
+ lines.push(` error: ${ellipsize(state.error, 240)}`);
153
+ }
154
+ }
155
+ }
156
+ if (this.configWarnings.length > 0) {
157
+ lines.push("", "warnings:");
158
+ for (const warning of this.configWarnings) {
159
+ lines.push(`- ${warning}`);
160
+ }
161
+ }
162
+ return lines.join("\n");
163
+ }
164
+ // 生成人类可读的完整状态报告,主要用于命令行诊断。
165
+ formatStatusReport() {
166
+ const states = this.listServers();
167
+ const summary = this.computeStatusSummary(states);
168
+ const lines = [
169
+ `MCP servers: configured ${summary.configured} | enabled ${summary.enabled} | connected ${summary.connected} | degraded ${summary.degraded} | disconnected ${summary.disconnected} | disabled ${summary.disabled}`,
170
+ ];
171
+ if (this.configWarnings.length > 0) {
172
+ lines.push("");
173
+ for (const warning of this.configWarnings) {
174
+ lines.push(`warning: ${warning}`);
175
+ }
176
+ }
177
+ if (states.length === 0) {
178
+ lines.push("", "(no MCP servers configured)");
179
+ return lines.join("\n");
180
+ }
181
+ for (const state of states) {
182
+ const supportedKinds = this.getSupportedKinds(state);
183
+ const capabilities = supportedKinds.join(", ") || "none";
184
+ const cacheLine = ` cache: tools=${state.cache.tools.length} [${joinNames(state.cache.tools.map((tool) => tool.name))}] | resources=${state.cache.resources.length} [${joinNames(state.cache.resources.map((resource) => resource.uri))}] | prompts=${state.cache.prompts.length} [${joinNames(state.cache.prompts.map((prompt) => prompt.name))}]`;
185
+ lines.push("", `- ${state.name} [${state.status}] ${state.transport}`, ` location: ${state.location}`, ` timeout: ${state.timeoutMs}ms`, ` capabilities: ${capabilities}`, cacheLine, ` last connected: ${formatTimestamp(state.lastConnectedAt)}`, ` last refresh: ${formatTimestamp(state.lastRefreshAt)}`);
186
+ if (state.error) {
187
+ lines.push(` error: ${ellipsize(state.error, 320)}`);
188
+ }
189
+ if (state.lastStderr) {
190
+ lines.push(` stderr: ${ellipsize(state.lastStderr.replaceAll(/\s+/g, " ").trim(), 320)}`);
191
+ }
192
+ }
193
+ return lines.join("\n");
194
+ }
195
+ // 以下三个方法是面向上层的统一分发入口。
196
+ async callTool(server, tool, args) {
197
+ return this.getConnection(server).callTool(tool, args);
198
+ }
199
+ async readResource(server, uri) {
200
+ return this.getConnection(server).readResource(uri);
201
+ }
202
+ async getPrompt(server, name, args) {
203
+ return this.getConnection(server).getPrompt(name, args);
204
+ }
205
+ listResourceDefinitions(server) {
206
+ if (server) {
207
+ const state = this.getConnection(server).getState();
208
+ return state.cache.resources.map((resource) => ({
209
+ server: state.name,
210
+ uri: resource.uri,
211
+ name: resource.name,
212
+ description: resource.description,
213
+ mimeType: resource.mimeType,
214
+ }));
215
+ }
216
+ return this.listServers().flatMap((state) => state.cache.resources.map((resource) => ({
217
+ server: state.name,
218
+ uri: resource.uri,
219
+ name: resource.name,
220
+ description: resource.description,
221
+ mimeType: resource.mimeType,
222
+ })));
223
+ }
224
+ // 统一做服务存在性校验,避免每个调用入口重复写同样逻辑。
225
+ getConnection(serverName) {
226
+ const connection = this.connections.get(serverName);
227
+ if (!connection) {
228
+ throw new McpRuntimeError("server_not_found", `MCP server "${serverName}" is not configured.`);
229
+ }
230
+ return connection;
231
+ }
232
+ // 聚合各服务状态,便于上层快速判断 MCP 整体健康度。
233
+ computeStatusSummary(states) {
234
+ const summary = {
235
+ configured: 0,
236
+ enabled: 0,
237
+ connected: 0,
238
+ degraded: 0,
239
+ disabled: 0,
240
+ disconnected: 0,
241
+ };
242
+ for (const state of states) {
243
+ summary.configured += 1;
244
+ if (!state.enabled || state.status === "disabled") {
245
+ summary.disabled += 1;
246
+ continue;
247
+ }
248
+ summary.enabled += 1;
249
+ if (state.status === "connected") {
250
+ summary.connected += 1;
251
+ }
252
+ else if (state.status === "degraded") {
253
+ summary.degraded += 1;
254
+ }
255
+ else if (state.status === "disconnected") {
256
+ summary.disconnected += 1;
257
+ }
258
+ }
259
+ return summary;
260
+ }
261
+ // 从服务能力对象中提取当前客户端真正支持的调用种类。
262
+ getSupportedKinds(state) {
263
+ const supported = [];
264
+ if (state.capabilities?.tools) {
265
+ supported.push("tool");
266
+ }
267
+ if (state.capabilities?.resources) {
268
+ supported.push("resource");
269
+ }
270
+ if (state.capabilities?.prompts) {
271
+ supported.push("prompt");
272
+ }
273
+ return supported;
274
+ }
275
+ }
@@ -0,0 +1,420 @@
1
+ import { getSettingsWarnings, loadSettings, reloadSettings } from "../config.js";
2
+ import { McpManager } from "./manager.js";
3
+ import { McpRuntimeError } from "./types.js";
4
+ import { isPlainRecord } from "../utils.js";
5
+ const OUTPUT_LIMIT = 50_000;
6
+ const DYNAMIC_MCP_TOOL_NAME_LIMIT = 64;
7
+ // 运行时只维护一个全局 McpManager,供工具调用和命令共享。
8
+ export const mcpManager = new McpManager();
9
+ // 初始化过程可能被多个并发调用触发,这里用 promise 去重。
10
+ let initializePromise = null;
11
+ // 把基础标量类型稳定转成字符串;复杂对象交给 JSON 格式化处理。
12
+ function stringifyScalar(value) {
13
+ if (typeof value === "string") {
14
+ return value;
15
+ }
16
+ if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
17
+ return `${value}`;
18
+ }
19
+ if (typeof value === "symbol") {
20
+ return value.toString();
21
+ }
22
+ if (value === undefined || value === null) {
23
+ return "";
24
+ }
25
+ return formatJson(value);
26
+ }
27
+ // 工具参数里的 server/kind/name/uri 理论上都应是字符串,这里统一做裁剪和兜底转换。
28
+ function normalizeStringInput(value) {
29
+ return stringifyScalar(value).trim();
30
+ }
31
+ function stableHash(value) {
32
+ let hash = 2166136261;
33
+ for (let index = 0; index < value.length; index += 1) {
34
+ hash ^= value.charCodeAt(index);
35
+ hash = Math.imul(hash, 16777619);
36
+ }
37
+ return (hash >>> 0).toString(36);
38
+ }
39
+ function sanitizeDynamicToolSegment(value) {
40
+ const sanitized = value
41
+ .toLowerCase()
42
+ .replaceAll(/[^a-z0-9_]+/g, "_")
43
+ .replaceAll(/^_+|_+$/g, "");
44
+ return sanitized || "tool";
45
+ }
46
+ function buildDynamicToolName(server, tool) {
47
+ const readable = `mcp__${sanitizeDynamicToolSegment(server)}__${sanitizeDynamicToolSegment(tool)}`;
48
+ const trimmed = readable.slice(0, DYNAMIC_MCP_TOOL_NAME_LIMIT).replaceAll(/_+$/g, "") || "mcp";
49
+ return trimmed;
50
+ }
51
+ function normalizeDynamicToolSchema(schema) {
52
+ if (!schema) {
53
+ return {
54
+ type: "object",
55
+ properties: {},
56
+ additionalProperties: true,
57
+ };
58
+ }
59
+ if (schema.type === undefined) {
60
+ return {
61
+ type: "object",
62
+ ...schema,
63
+ };
64
+ }
65
+ return schema;
66
+ }
67
+ function buildDynamicToolDescription(server, tool, description) {
68
+ const prefix = `MCP tool "${tool}" from server "${server}".`;
69
+ const suffix = description?.trim();
70
+ return suffix ? `${prefix} ${suffix}` : prefix;
71
+ }
72
+ function ensureUniqueDynamicToolName(name, usedNames, server, tool) {
73
+ if (!usedNames.has(name)) {
74
+ usedNames.add(name);
75
+ return name;
76
+ }
77
+ const hash = stableHash(`${server}\u0000${tool}`);
78
+ const maxBaseLength = Math.max(1, DYNAMIC_MCP_TOOL_NAME_LIMIT - hash.length - 2);
79
+ const trimmed = name.slice(0, maxBaseLength).replaceAll(/_+$/g, "") || "mcp";
80
+ const fallback = `${trimmed}__${hash}`;
81
+ usedNames.add(fallback);
82
+ return fallback;
83
+ }
84
+ // 限制最终字符串输出长度,避免工具结果把上下文窗口塞满。
85
+ function limitText(text, maxLength = OUTPUT_LIMIT) {
86
+ if (text.length <= maxLength) {
87
+ return text;
88
+ }
89
+ return `${text.slice(0, Math.max(0, maxLength - 30))}\n... (truncated)`;
90
+ }
91
+ // 统一 JSON 格式化入口;遇到循环引用等情况时退化成普通字符串。
92
+ function formatJson(value) {
93
+ try {
94
+ return JSON.stringify(value, null, 2);
95
+ }
96
+ catch {
97
+ return Object.prototype.toString.call(value);
98
+ }
99
+ }
100
+ // 资源内容既可能是纯文本,也可能是 base64 二进制,这里统一转成人类可读文本。
101
+ function formatEmbeddedResource(resource) {
102
+ const mime = resource?.mimeType ? ` (${resource.mimeType})` : "";
103
+ if (typeof resource?.text === "string") {
104
+ return `resource ${resource.uri}${mime}\n${resource.text}`;
105
+ }
106
+ if (typeof resource?.blob === "string") {
107
+ return `resource ${resource?.uri ?? "(unknown)"}${mime}\n[binary data: ${resource.blob.length} base64 chars]`;
108
+ }
109
+ return formatJson(resource);
110
+ }
111
+ // MCP content item 是一个联合类型,这里把不同类型收敛到统一字符串表示。
112
+ function formatContentItem(item) {
113
+ switch (item?.type) {
114
+ case "text":
115
+ return String(item.text ?? "");
116
+ case "image":
117
+ return `[image ${item.mimeType ?? "unknown"}, ${String(item.data ?? "").length} base64 chars]`;
118
+ case "audio":
119
+ return `[audio ${item.mimeType ?? "unknown"}, ${String(item.data ?? "").length} base64 chars]`;
120
+ case "resource":
121
+ return formatEmbeddedResource(item.resource);
122
+ case "resource_link": {
123
+ const description = item.description ? `\n${item.description}` : "";
124
+ return `resource link: ${item.name ?? "(unnamed)"} -> ${item.uri ?? "(unknown)"}${description}`;
125
+ }
126
+ default:
127
+ return formatJson(item);
128
+ }
129
+ }
130
+ // 把 content 列表拼成统一文本块,空内容时返回 undefined 让上层省略该段。
131
+ function formatContentList(items) {
132
+ const parts = items
133
+ .map((item) => formatContentItem(item).trim())
134
+ .filter(Boolean);
135
+ if (parts.length === 0) {
136
+ return undefined;
137
+ }
138
+ return `content:\n${parts.join("\n\n")}`;
139
+ }
140
+ function formatSchemaSummary(value) {
141
+ if (value === null)
142
+ return "null";
143
+ if (Array.isArray(value)) {
144
+ if (value.length === 0)
145
+ return "[]";
146
+ return `[${formatSchemaSummary(value[0])}]`;
147
+ }
148
+ if (typeof value === "object") {
149
+ const entries = Object.entries(value).slice(0, 8);
150
+ const suffix = Object.keys(value).length > entries.length ? ", ..." : "";
151
+ return `{${entries.map(([key, item]) => `${key}: ${formatSchemaSummary(item)}`).join(", ")}${suffix}}`;
152
+ }
153
+ return typeof value;
154
+ }
155
+ // tool 调用结果通常最复杂,可能同时带 content、structuredContent、toolResult 和 _meta。
156
+ function formatToolResult(server, name, result) {
157
+ const lines = [
158
+ `server: ${server}`,
159
+ "kind: tool",
160
+ `tool: ${name}`,
161
+ ];
162
+ if ("isError" in result) {
163
+ lines.push(`status: ${result.isError ? "error" : "ok"}`);
164
+ }
165
+ if ("content" in result && Array.isArray(result.content)) {
166
+ const content = formatContentList(result.content);
167
+ if (content) {
168
+ lines.push("", content);
169
+ }
170
+ }
171
+ if ("structuredContent" in result && result.structuredContent !== undefined) {
172
+ lines.push("", `structuredContent (${formatSchemaSummary(result.structuredContent)}):`, formatJson(result.structuredContent));
173
+ }
174
+ if ("toolResult" in result && result.toolResult !== undefined) {
175
+ lines.push("", `toolResult (${formatSchemaSummary(result.toolResult)}):`, formatJson(result.toolResult));
176
+ }
177
+ if (result._meta) {
178
+ lines.push("", "_meta:", formatJson(result._meta));
179
+ }
180
+ return limitText(lines.join("\n"));
181
+ }
182
+ // resource 读取结果可能返回多个内容块,这里逐个展开。
183
+ function formatReadResourceResult(server, uri, result) {
184
+ const lines = [
185
+ `server: ${server}`,
186
+ "kind: resource",
187
+ `uri: ${uri}`,
188
+ ];
189
+ const contents = result.contents
190
+ .map((content) => formatEmbeddedResource(content).trim())
191
+ .filter(Boolean);
192
+ if (contents.length > 0) {
193
+ lines.push("", "contents:", contents.join("\n\n"));
194
+ }
195
+ if (result._meta) {
196
+ lines.push("", "_meta:", formatJson(result._meta));
197
+ }
198
+ return limitText(lines.join("\n"));
199
+ }
200
+ // prompt 结果本质上是一组 role + content 的消息,用文本视图展开便于模型继续消费。
201
+ function formatPromptResult(server, name, result) {
202
+ const lines = [
203
+ `server: ${server}`,
204
+ "kind: prompt",
205
+ `prompt: ${name}`,
206
+ ];
207
+ if (result.description) {
208
+ lines.push(`description: ${result.description}`);
209
+ }
210
+ if (Array.isArray(result.messages) && result.messages.length > 0) {
211
+ lines.push("", "messages:");
212
+ for (const message of result.messages) {
213
+ lines.push(`[${message.role}]`, formatContentItem(message.content), "");
214
+ }
215
+ if (lines.at(-1) === "") {
216
+ lines.pop();
217
+ }
218
+ }
219
+ if (result._meta) {
220
+ lines.push("", "_meta:", formatJson(result._meta));
221
+ }
222
+ return limitText(lines.join("\n"));
223
+ }
224
+ function formatResourceList(server) {
225
+ const items = mcpManager.listResourceDefinitions(server);
226
+ const lines = [
227
+ `kind: resource_list`,
228
+ `scope: ${server ?? "all"}`,
229
+ `count: ${items.length}`,
230
+ ];
231
+ if (items.length === 0) {
232
+ lines.push("", "(no resources found)");
233
+ return lines.join("\n");
234
+ }
235
+ lines.push("", "resources:");
236
+ for (const item of items) {
237
+ const mime = item.mimeType ? ` mime=${item.mimeType}` : "";
238
+ const description = item.description ? `\n description: ${item.description}` : "";
239
+ lines.push(`- [${item.server}] ${item.name} -> ${item.uri}${mime}${description}`);
240
+ }
241
+ return limitText(lines.join("\n"));
242
+ }
243
+ async function handleDynamicMcpToolCall(server, name, args) {
244
+ try {
245
+ await ensureMcpInitialized();
246
+ if (!isPlainRecord(args)) {
247
+ throw new McpRuntimeError("invalid_arguments", `MCP tool "${name}" arguments must be an object.`);
248
+ }
249
+ const result = await mcpManager.callTool(server, name, args);
250
+ return formatToolResult(server, name, result);
251
+ }
252
+ catch (error) {
253
+ return formatRuntimeError(error);
254
+ }
255
+ }
256
+ // MCP prompt 的参数要求是 string map;外部传入 unknown 时需要先做归一化。
257
+ function normalizePromptArguments(args) {
258
+ if (!args || Object.keys(args).length === 0) {
259
+ return undefined;
260
+ }
261
+ return Object.fromEntries(Object.entries(args).map(([key, value]) => {
262
+ if (typeof value === "string") {
263
+ return [key, value];
264
+ }
265
+ if (value === undefined || value === null) {
266
+ return [key, ""];
267
+ }
268
+ if (typeof value === "object") {
269
+ return [key, formatJson(value)];
270
+ }
271
+ return [key, stringifyScalar(value)];
272
+ }));
273
+ }
274
+ // 对外统一错误字符串格式,避免底层异常对象直接泄漏给工具层。
275
+ function formatRuntimeError(error) {
276
+ if (error instanceof McpRuntimeError) {
277
+ return `Error: [${error.code}] ${error.message}`;
278
+ }
279
+ if (error instanceof Error) {
280
+ return `Error: ${error.message}`;
281
+ }
282
+ return `Error: ${stringifyScalar(error) || "Unknown error"}`;
283
+ }
284
+ // 对 mcp_call 的工具参数做严格校验,尽量在发请求前就拦住明显错误。
285
+ function validateMcpCallArgs(args) {
286
+ const server = normalizeStringInput(args.server);
287
+ const kind = normalizeStringInput(args.kind);
288
+ const name = normalizeStringInput(args.name);
289
+ if (!server) {
290
+ throw new McpRuntimeError("invalid_arguments", "mcp_call requires a non-empty server.");
291
+ }
292
+ if (kind !== "prompt") {
293
+ throw new McpRuntimeError("invalid_arguments", `mcp_call kind must be "prompt". Received "${kind || "(empty)"}".`);
294
+ }
295
+ if (args.arguments !== undefined && !isPlainRecord(args.arguments)) {
296
+ throw new McpRuntimeError("invalid_arguments", "mcp_call arguments must be an object when provided.");
297
+ }
298
+ if (!name) {
299
+ throw new McpRuntimeError("invalid_arguments", "mcp_call kind=prompt requires name.");
300
+ }
301
+ return {
302
+ server,
303
+ kind: "prompt",
304
+ name: name || undefined,
305
+ arguments: args.arguments,
306
+ };
307
+ }
308
+ // 从配置系统同步 MCP 设置,并按需决定是否立刻初始化连接。
309
+ async function syncFromSettings(options) {
310
+ const settings = options?.reload ? reloadSettings() : loadSettings();
311
+ const warnings = getSettingsWarnings().filter((warning) => warning.startsWith("[mcp]"));
312
+ await mcpManager.configure(settings.mcp?.servers ?? [], warnings);
313
+ if (options?.initialize) {
314
+ await mcpManager.initializeAll();
315
+ }
316
+ return mcpManager;
317
+ }
318
+ // 确保 MCP 至少初始化一次;并发场景下复用同一个初始化 promise。
319
+ export async function ensureMcpInitialized() {
320
+ if (mcpManager.isInitialized()) {
321
+ return mcpManager;
322
+ }
323
+ initializePromise ??= syncFromSettings({ initialize: true }).finally(() => {
324
+ initializePromise = null;
325
+ });
326
+ return initializePromise;
327
+ }
328
+ // 配置热更新入口:先重读配置,再刷新一个或全部服务。
329
+ export async function refreshMcpFromSettings(serverName) {
330
+ await syncFromSettings({ reload: true, initialize: false });
331
+ await mcpManager.refresh(serverName);
332
+ return mcpManager;
333
+ }
334
+ // 在后台悄悄触发一次初始化,适合程序启动时预热。
335
+ export function primeMcpRuntime() {
336
+ void ensureMcpInitialized();
337
+ }
338
+ // 给主提示词拼接 MCP 能力摘要。
339
+ export function getMcpPromptInstructions() {
340
+ return mcpManager.buildPromptSummary();
341
+ }
342
+ export async function getDynamicMcpToolSurface() {
343
+ await ensureMcpInitialized();
344
+ const responseTools = [];
345
+ const handlers = {};
346
+ const usedNames = new Set();
347
+ for (const state of mcpManager.listServers()) {
348
+ if (!state.enabled || state.cache.tools.length === 0) {
349
+ continue;
350
+ }
351
+ for (const tool of state.cache.tools) {
352
+ const dynamicName = ensureUniqueDynamicToolName(buildDynamicToolName(state.name, tool.name), usedNames, state.name, tool.name);
353
+ const parameters = normalizeDynamicToolSchema(tool.inputSchema);
354
+ responseTools.push({
355
+ type: "function",
356
+ name: dynamicName,
357
+ description: buildDynamicToolDescription(state.name, tool.name, tool.description),
358
+ parameters,
359
+ });
360
+ handlers[dynamicName] = (args) => handleDynamicMcpToolCall(state.name, tool.name, args);
361
+ }
362
+ }
363
+ const chatTools = responseTools.map((tool) => ({
364
+ type: "function",
365
+ function: {
366
+ name: tool.name,
367
+ description: tool.description,
368
+ parameters: tool.parameters,
369
+ },
370
+ }));
371
+ return {
372
+ responseTools,
373
+ chatTools,
374
+ handlers,
375
+ };
376
+ }
377
+ // mcp_call 工具的统一执行入口。
378
+ // 这里负责初始化、参数校验、按 kind 分发,以及最终结果格式化。
379
+ export async function handleMcpCall(args) {
380
+ try {
381
+ await ensureMcpInitialized();
382
+ const call = validateMcpCallArgs(args);
383
+ const result = await mcpManager.getPrompt(call.server, call.name ?? "", normalizePromptArguments(call.arguments));
384
+ return formatPromptResult(call.server, call.name ?? "", result);
385
+ }
386
+ catch (error) {
387
+ return formatRuntimeError(error);
388
+ }
389
+ }
390
+ export async function handleListMcpResources(args) {
391
+ try {
392
+ await ensureMcpInitialized();
393
+ const server = args.server === undefined ? undefined : normalizeStringInput(args.server);
394
+ if (args.server !== undefined && !server) {
395
+ throw new McpRuntimeError("invalid_arguments", "list_mcp_resources server must be a non-empty string when provided.");
396
+ }
397
+ return formatResourceList(server);
398
+ }
399
+ catch (error) {
400
+ return formatRuntimeError(error);
401
+ }
402
+ }
403
+ export async function handleReadMcpResource(args) {
404
+ try {
405
+ await ensureMcpInitialized();
406
+ const server = normalizeStringInput(args.server);
407
+ const uri = normalizeStringInput(args.uri);
408
+ if (!server) {
409
+ throw new McpRuntimeError("invalid_arguments", "read_mcp_resource requires a non-empty server.");
410
+ }
411
+ if (!uri) {
412
+ throw new McpRuntimeError("invalid_arguments", "read_mcp_resource requires a non-empty uri.");
413
+ }
414
+ const result = await mcpManager.readResource(server, uri);
415
+ return formatReadResourceResult(server, uri, result);
416
+ }
417
+ catch (error) {
418
+ return formatRuntimeError(error);
419
+ }
420
+ }