@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.
- package/LICENSE +21 -0
- package/README.md +631 -0
- package/README.zh-CN.md +542 -0
- package/dist/agent.js +1450 -0
- package/dist/busy-status.js +29 -0
- package/dist/clipboard-image.js +97 -0
- package/dist/commands.js +109 -0
- package/dist/compact.js +262 -0
- package/dist/config.js +516 -0
- package/dist/error-log.js +80 -0
- package/dist/http.js +89 -0
- package/dist/idle-watchdog.js +88 -0
- package/dist/index.js +2031 -0
- package/dist/input-submit.js +41 -0
- package/dist/mcp/client.js +466 -0
- package/dist/mcp/manager.js +275 -0
- package/dist/mcp/runtime.js +420 -0
- package/dist/mcp/types.js +12 -0
- package/dist/message-bus.js +180 -0
- package/dist/oauth/openai.js +326 -0
- package/dist/prompt.js +156 -0
- package/dist/session-store.js +186 -0
- package/dist/skills/frontmatter.js +85 -0
- package/dist/skills/index.js +2 -0
- package/dist/skills/loader.js +88 -0
- package/dist/skills/render.js +35 -0
- package/dist/skills/types.js +1 -0
- package/dist/subagents.js +64 -0
- package/dist/supervisor.js +58 -0
- package/dist/task-manager.js +280 -0
- package/dist/team-types.js +1 -0
- package/dist/teammate-manager.js +266 -0
- package/dist/tools.js +1068 -0
- package/dist/trust-store.js +42 -0
- package/dist/types.js +1 -0
- package/dist/usage.js +226 -0
- package/dist/utils.js +21 -0
- package/package.json +67 -0
- package/scripts/postinstall.mjs +30 -0
- package/skills/code-review/SKILL.md +22 -0
- package/skills/pdf/SKILL.md +18 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 从一次输入事件中提取“应当立即提交”的文本。
|
|
3
|
+
* 正常终端会把 Enter 解析成 `key.return`,但有些环境会把整行内容连同换行一起塞进 `input`。
|
|
4
|
+
* 这里统一把两种情况折叠成同一种提交值,避免 `/exit` 之类的命令只进入输入框而不执行。
|
|
5
|
+
*
|
|
6
|
+
* 注意:多行粘贴会把“文本 + 换行 + 后续文本”一并塞进 input。
|
|
7
|
+
* 这种情况下绝不能当作 Enter 提交,否则会出现“第一行被自动发送、剩余还在输入框”的 bug。
|
|
8
|
+
* 判定方法:只有当换行符之后再无任何非换行字符时,才视作 Enter 兜底。
|
|
9
|
+
*/
|
|
10
|
+
export function getSubmittedValueFromInput(currentValue, input, keyReturn) {
|
|
11
|
+
if (keyReturn) {
|
|
12
|
+
return currentValue;
|
|
13
|
+
}
|
|
14
|
+
const firstLineBreak = input.search(/[\r\n]/);
|
|
15
|
+
if (firstLineBreak === -1) {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
const tail = input.slice(firstLineBreak).replace(/[\r\n]/g, "");
|
|
19
|
+
if (tail.length > 0) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
return currentValue + input.slice(0, firstLineBreak);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* 某些终端下,普通字符会正常进入输入框,但 Enter 不一定稳定触发 TextInput 的 onSubmit。
|
|
26
|
+
* 这里提供一个非常小的去重器,允许我们在父级 useInput 中补一层 Enter 处理,
|
|
27
|
+
* 同时避免一次按键触发两次提交。
|
|
28
|
+
*/
|
|
29
|
+
export function createSubmitDeduper(windowMs = 80) {
|
|
30
|
+
let lastValue = "";
|
|
31
|
+
let lastAt = 0;
|
|
32
|
+
return {
|
|
33
|
+
shouldSubmit(value) {
|
|
34
|
+
const now = Date.now();
|
|
35
|
+
const isDuplicate = value === lastValue && now - lastAt <= windowMs;
|
|
36
|
+
lastValue = value;
|
|
37
|
+
lastAt = now;
|
|
38
|
+
return !isDuplicate;
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { execFileSync } from "node:child_process";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
import { StreamableHTTPClientTransport, StreamableHTTPError } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
|
|
7
|
+
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
8
|
+
import { McpRuntimeError } from "./types.js";
|
|
9
|
+
import { isPlainRecord } from "../utils.js";
|
|
10
|
+
// 裸命令名(非路径)在 spawn 时可能因 PATH 继承问题找不到,先用 which 解析成绝对路径。
|
|
11
|
+
function resolveCommand(command) {
|
|
12
|
+
if (command.includes(path.sep))
|
|
13
|
+
return command;
|
|
14
|
+
try {
|
|
15
|
+
return execFileSync("which", [command], { encoding: "utf8" }).trim();
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return command;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
const CLIENT_INFO = {
|
|
22
|
+
name: "claude-code-mini",
|
|
23
|
+
version: "1.0.0",
|
|
24
|
+
};
|
|
25
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
26
|
+
const STDERR_BUFFER_LIMIT = 4_000;
|
|
27
|
+
// 新建连接时默认先从空缓存开始,后续成功连接后再填充真实能力列表。
|
|
28
|
+
function emptyCache() {
|
|
29
|
+
return {
|
|
30
|
+
tools: [],
|
|
31
|
+
resources: [],
|
|
32
|
+
prompts: [],
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// 把配置转成适合展示的连接位置字符串,便于 /mcp 报告诊断。
|
|
36
|
+
function formatLocation(config) {
|
|
37
|
+
if (config.transport === "stdio") {
|
|
38
|
+
const command = config.command?.trim() ?? "";
|
|
39
|
+
const args = (config.args ?? []).join(" ").trim();
|
|
40
|
+
return `${command} ${args}`.trim() || "(stdio)";
|
|
41
|
+
}
|
|
42
|
+
return config.url?.trim() || "(streamable-http)";
|
|
43
|
+
}
|
|
44
|
+
// 根据配置构造统一的运行时状态快照,避免不同重置路径产生不一致状态。
|
|
45
|
+
function createState(config, status) {
|
|
46
|
+
return {
|
|
47
|
+
name: config.name,
|
|
48
|
+
enabled: config.enabled,
|
|
49
|
+
transport: config.transport,
|
|
50
|
+
status: status ?? (config.enabled ? "disconnected" : "disabled"),
|
|
51
|
+
timeoutMs: config.timeoutMs,
|
|
52
|
+
location: formatLocation(config),
|
|
53
|
+
cache: emptyCache(),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
// 对外暴露状态时做一层深拷贝,避免调用方意外修改内部缓存数组。
|
|
57
|
+
function cloneState(state) {
|
|
58
|
+
return {
|
|
59
|
+
...state,
|
|
60
|
+
cache: {
|
|
61
|
+
...state.cache,
|
|
62
|
+
tools: [...state.cache.tools],
|
|
63
|
+
resources: [...state.cache.resources],
|
|
64
|
+
prompts: [...state.cache.prompts],
|
|
65
|
+
},
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
// 只截取字符串尾部,用于保存 stderr 等“越新越有诊断价值”的文本。
|
|
69
|
+
function trimTail(text, maxLength = STDERR_BUFFER_LIMIT) {
|
|
70
|
+
if (text.length <= maxLength) {
|
|
71
|
+
return text;
|
|
72
|
+
}
|
|
73
|
+
return text.slice(text.length - maxLength);
|
|
74
|
+
}
|
|
75
|
+
// 把 unknown 错误尽量稳定地转成可读文本,避免出现大量 [object Object]。
|
|
76
|
+
function getErrorMessage(error) {
|
|
77
|
+
if (error instanceof Error) {
|
|
78
|
+
return error.message;
|
|
79
|
+
}
|
|
80
|
+
if (typeof error === "string") {
|
|
81
|
+
return error;
|
|
82
|
+
}
|
|
83
|
+
if (typeof error === "object" && error !== null) {
|
|
84
|
+
try {
|
|
85
|
+
return JSON.stringify(error);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return Object.prototype.toString.call(error);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") {
|
|
92
|
+
return `${error}`;
|
|
93
|
+
}
|
|
94
|
+
if (typeof error === "symbol") {
|
|
95
|
+
return error.toString();
|
|
96
|
+
}
|
|
97
|
+
return "Unknown error";
|
|
98
|
+
}
|
|
99
|
+
// SDK 会用统一的 McpError + ErrorCode 表示超时和连接关闭,这里单独做类型守卫。
|
|
100
|
+
function isTimeoutError(error) {
|
|
101
|
+
return error instanceof McpError && error.code === ErrorCode.RequestTimeout;
|
|
102
|
+
}
|
|
103
|
+
// 连接关闭属于需要“丢弃当前 client 并触发后续重连”的特殊错误。
|
|
104
|
+
function isConnectionClosedError(error) {
|
|
105
|
+
return error instanceof McpError && error.code === ErrorCode.ConnectionClosed;
|
|
106
|
+
}
|
|
107
|
+
// 安全关闭客户端时顺手清掉事件回调,避免 close 过程再次污染状态。
|
|
108
|
+
async function safeClose(client) {
|
|
109
|
+
if (!client) {
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
client.onclose = undefined;
|
|
114
|
+
client.onerror = undefined;
|
|
115
|
+
await client.close();
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
// 忽略清理阶段的异常,避免关闭流程反向影响主逻辑。
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
// McpClientConnection 负责“一个服务”的连接、缓存和调用。
|
|
122
|
+
export class McpClientConnection {
|
|
123
|
+
config;
|
|
124
|
+
client;
|
|
125
|
+
state;
|
|
126
|
+
constructor(config) {
|
|
127
|
+
this.config = config;
|
|
128
|
+
this.state = createState(config);
|
|
129
|
+
}
|
|
130
|
+
// 配置变化时复用已有连接对象,只更新那些和配置直接相关的状态字段。
|
|
131
|
+
updateConfig(config) {
|
|
132
|
+
const previous = this.state;
|
|
133
|
+
this.config = config;
|
|
134
|
+
this.state = {
|
|
135
|
+
...previous,
|
|
136
|
+
enabled: config.enabled,
|
|
137
|
+
transport: config.transport,
|
|
138
|
+
timeoutMs: config.timeoutMs,
|
|
139
|
+
location: formatLocation(config),
|
|
140
|
+
status: config.enabled ? previous.status : "disabled",
|
|
141
|
+
...(config.enabled ? {} : { error: undefined }),
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
getState() {
|
|
145
|
+
return cloneState(this.state);
|
|
146
|
+
}
|
|
147
|
+
// 延迟初始化,避免为未使用的服务提前建立连接。
|
|
148
|
+
async initialize() {
|
|
149
|
+
if (!this.config.enabled) {
|
|
150
|
+
return this.applyDisabled();
|
|
151
|
+
}
|
|
152
|
+
if (!this.client) {
|
|
153
|
+
await this.connectAndLoad();
|
|
154
|
+
}
|
|
155
|
+
return this.getState();
|
|
156
|
+
}
|
|
157
|
+
// refresh 不复用旧连接,直接走完整重连和缓存重载流程。
|
|
158
|
+
async refresh() {
|
|
159
|
+
if (!this.config.enabled) {
|
|
160
|
+
return this.applyDisabled();
|
|
161
|
+
}
|
|
162
|
+
await this.connectAndLoad();
|
|
163
|
+
return this.getState();
|
|
164
|
+
}
|
|
165
|
+
// close 只负责关闭当前连接,不主动改写其他运行时字段。
|
|
166
|
+
async close() {
|
|
167
|
+
const client = this.client;
|
|
168
|
+
this.client = undefined;
|
|
169
|
+
await safeClose(client);
|
|
170
|
+
}
|
|
171
|
+
// 禁用服务时保留最近一次发现的缓存,同时释放现有连接。
|
|
172
|
+
async applyDisabled() {
|
|
173
|
+
await this.close();
|
|
174
|
+
this.state = {
|
|
175
|
+
...createState(this.config, "disabled"),
|
|
176
|
+
cache: this.state.cache,
|
|
177
|
+
};
|
|
178
|
+
return this.getState();
|
|
179
|
+
}
|
|
180
|
+
// 以下三个公共调用入口都先确保“已连接 + 有对应 capability”,再发起真实请求。
|
|
181
|
+
async callTool(name, args) {
|
|
182
|
+
const client = await this.ensureUsableClient();
|
|
183
|
+
this.requireCapability("tools", `tool "${name}"`);
|
|
184
|
+
try {
|
|
185
|
+
return await client.callTool({ name, arguments: args }, undefined, { timeout: this.config.timeoutMs || DEFAULT_TIMEOUT_MS });
|
|
186
|
+
}
|
|
187
|
+
catch (error) {
|
|
188
|
+
throw await this.handleOperationFailure(error);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
async readResource(uri) {
|
|
192
|
+
const client = await this.ensureUsableClient();
|
|
193
|
+
this.requireCapability("resources", `resource "${uri}"`);
|
|
194
|
+
try {
|
|
195
|
+
return await client.readResource({ uri }, { timeout: this.config.timeoutMs || DEFAULT_TIMEOUT_MS });
|
|
196
|
+
}
|
|
197
|
+
catch (error) {
|
|
198
|
+
throw await this.handleOperationFailure(error);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
async getPrompt(name, args) {
|
|
202
|
+
const client = await this.ensureUsableClient();
|
|
203
|
+
this.requireCapability("prompts", `prompt "${name}"`);
|
|
204
|
+
try {
|
|
205
|
+
return await client.getPrompt({ name, arguments: args }, { timeout: this.config.timeoutMs || DEFAULT_TIMEOUT_MS });
|
|
206
|
+
}
|
|
207
|
+
catch (error) {
|
|
208
|
+
throw await this.handleOperationFailure(error);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// 懒建立连接,并在失败后提供稳定的业务错误而不是裸 SDK 错误。
|
|
212
|
+
async ensureUsableClient() {
|
|
213
|
+
if (!this.config.enabled) {
|
|
214
|
+
throw new McpRuntimeError("server_not_connected", `MCP server "${this.config.name}" is disabled.`);
|
|
215
|
+
}
|
|
216
|
+
if (!this.client) {
|
|
217
|
+
await this.connectAndLoad();
|
|
218
|
+
}
|
|
219
|
+
if (!this.client) {
|
|
220
|
+
throw new McpRuntimeError("server_not_connected", `MCP server "${this.config.name}" is not connected.`);
|
|
221
|
+
}
|
|
222
|
+
return this.client;
|
|
223
|
+
}
|
|
224
|
+
// 即使服务已连接,也未必支持 tool/resource/prompt 三种能力,必须先校验。
|
|
225
|
+
requireCapability(capability, target) {
|
|
226
|
+
const capabilities = this.state.capabilities;
|
|
227
|
+
if (!capabilities?.[capability]) {
|
|
228
|
+
throw new McpRuntimeError("capability_unsupported", `MCP server "${this.config.name}" does not support ${target}.`);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
// 每次从头重连,避免配置变更或旧传输层状态泄漏到新会话中。
|
|
232
|
+
async connectAndLoad() {
|
|
233
|
+
const previousCache = this.state.cache;
|
|
234
|
+
await this.close();
|
|
235
|
+
this.state = {
|
|
236
|
+
...this.state,
|
|
237
|
+
...createState(this.config, "connecting"),
|
|
238
|
+
cache: previousCache,
|
|
239
|
+
capabilities: undefined,
|
|
240
|
+
serverVersion: undefined,
|
|
241
|
+
instructions: undefined,
|
|
242
|
+
error: undefined,
|
|
243
|
+
lastStderr: undefined,
|
|
244
|
+
};
|
|
245
|
+
// strict capability 模式可以让客户端在能力声明不匹配时尽早失败。
|
|
246
|
+
const client = new Client(CLIENT_INFO, {
|
|
247
|
+
capabilities: {},
|
|
248
|
+
enforceStrictCapabilities: true,
|
|
249
|
+
});
|
|
250
|
+
// 服务端主动断开时,同步更新本地连接状态。
|
|
251
|
+
client.onclose = () => {
|
|
252
|
+
this.client = undefined;
|
|
253
|
+
if (this.config.enabled) {
|
|
254
|
+
this.state = {
|
|
255
|
+
...this.state,
|
|
256
|
+
status: "disconnected",
|
|
257
|
+
error: this.state.error ?? "Connection closed.",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
// 立即暴露传输层错误,同时尽量保留最后一次已知状态用于诊断。
|
|
262
|
+
client.onerror = (error) => {
|
|
263
|
+
this.state = {
|
|
264
|
+
...this.state,
|
|
265
|
+
status: this.client ? "degraded" : "disconnected",
|
|
266
|
+
error: getErrorMessage(error),
|
|
267
|
+
};
|
|
268
|
+
};
|
|
269
|
+
try {
|
|
270
|
+
// 先建立底层 transport,再把能力、版本和缓存一起写回状态。
|
|
271
|
+
const transport = this.createTransport();
|
|
272
|
+
await client.connect(transport, { timeout: this.config.timeoutMs || DEFAULT_TIMEOUT_MS });
|
|
273
|
+
this.client = client;
|
|
274
|
+
this.state = {
|
|
275
|
+
...this.state,
|
|
276
|
+
status: "connected",
|
|
277
|
+
capabilities: client.getServerCapabilities(),
|
|
278
|
+
serverVersion: client.getServerVersion(),
|
|
279
|
+
instructions: client.getInstructions(),
|
|
280
|
+
lastConnectedAt: Date.now(),
|
|
281
|
+
error: undefined,
|
|
282
|
+
};
|
|
283
|
+
await this.reloadCache();
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
await safeClose(client);
|
|
287
|
+
this.client = undefined;
|
|
288
|
+
this.state = {
|
|
289
|
+
...this.state,
|
|
290
|
+
status: "degraded",
|
|
291
|
+
error: getErrorMessage(error),
|
|
292
|
+
};
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
// 根据 transport 类型创建具体连接对象。
|
|
296
|
+
// stdio 会额外监听 stderr;HTTP 透传 headers 给远端服务。
|
|
297
|
+
createTransport() {
|
|
298
|
+
if (this.config.transport === "stdio") {
|
|
299
|
+
const cwd = this.config.cwd?.trim();
|
|
300
|
+
if (cwd) {
|
|
301
|
+
if (!fs.existsSync(cwd)) {
|
|
302
|
+
throw new Error(`MCP server "${this.config.name}" cwd does not exist: ${cwd}`);
|
|
303
|
+
}
|
|
304
|
+
if (!fs.statSync(cwd).isDirectory()) {
|
|
305
|
+
throw new Error(`MCP server "${this.config.name}" cwd is not a directory: ${cwd}`);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const transport = new StdioClientTransport({
|
|
309
|
+
command: resolveCommand(this.config.command ?? ""),
|
|
310
|
+
args: this.config.args ?? [],
|
|
311
|
+
env: Object.fromEntries(Object.entries({ ...process.env, ...(this.config.env ?? {}) }).filter(([, v]) => v !== undefined)),
|
|
312
|
+
cwd,
|
|
313
|
+
stderr: "pipe",
|
|
314
|
+
});
|
|
315
|
+
const stderr = transport.stderr;
|
|
316
|
+
if (stderr) {
|
|
317
|
+
// 只保留 stderr 尾部内容,避免诊断信息无限增长占用内存。
|
|
318
|
+
stderr.on("data", (chunk) => {
|
|
319
|
+
const next = `${this.state.lastStderr ?? ""}${String(chunk)}`;
|
|
320
|
+
this.state = {
|
|
321
|
+
...this.state,
|
|
322
|
+
lastStderr: trimTail(next),
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
return transport;
|
|
327
|
+
}
|
|
328
|
+
return new StreamableHTTPClientTransport(new URL(this.config.url ?? ""), {
|
|
329
|
+
requestInit: {
|
|
330
|
+
headers: this.config.headers,
|
|
331
|
+
},
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
// 重建缓存时并不要求所有能力都成功,只要能拿到一部分就先保留下来。
|
|
335
|
+
async reloadCache() {
|
|
336
|
+
if (!this.client) {
|
|
337
|
+
return;
|
|
338
|
+
}
|
|
339
|
+
const nextCache = {
|
|
340
|
+
tools: [],
|
|
341
|
+
resources: [],
|
|
342
|
+
prompts: [],
|
|
343
|
+
refreshedAt: Date.now(),
|
|
344
|
+
};
|
|
345
|
+
const errors = [];
|
|
346
|
+
// 各类 capability 独立刷新,局部失败时仍尽量保留可用元数据。
|
|
347
|
+
if (this.state.capabilities?.tools) {
|
|
348
|
+
try {
|
|
349
|
+
nextCache.tools = await this.listTools();
|
|
350
|
+
}
|
|
351
|
+
catch (error) {
|
|
352
|
+
errors.push(`tools/list failed: ${getErrorMessage(error)}`);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
if (this.state.capabilities?.resources) {
|
|
356
|
+
try {
|
|
357
|
+
nextCache.resources = await this.listResources();
|
|
358
|
+
}
|
|
359
|
+
catch (error) {
|
|
360
|
+
errors.push(`resources/list failed: ${getErrorMessage(error)}`);
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
if (this.state.capabilities?.prompts) {
|
|
364
|
+
try {
|
|
365
|
+
nextCache.prompts = await this.listPrompts();
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
errors.push(`prompts/list failed: ${getErrorMessage(error)}`);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
this.state = {
|
|
372
|
+
...this.state,
|
|
373
|
+
cache: nextCache,
|
|
374
|
+
lastRefreshAt: nextCache.refreshedAt,
|
|
375
|
+
status: errors.length > 0 ? "degraded" : "connected",
|
|
376
|
+
error: errors.length > 0 ? errors.join("\n") : undefined,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
// 下面三个 list* 方法负责把 SDK 返回值裁剪成更稳定、更适合缓存的结构。
|
|
380
|
+
async listTools() {
|
|
381
|
+
if (!this.client) {
|
|
382
|
+
return [];
|
|
383
|
+
}
|
|
384
|
+
const tools = [];
|
|
385
|
+
let cursor;
|
|
386
|
+
// 按 MCP 分页协议持续拉取,直到收集完整工具列表。
|
|
387
|
+
do {
|
|
388
|
+
const page = await this.client.listTools(cursor ? { cursor } : undefined, { timeout: this.config.timeoutMs || DEFAULT_TIMEOUT_MS });
|
|
389
|
+
tools.push(...page.tools.map((tool) => ({
|
|
390
|
+
name: tool.name,
|
|
391
|
+
description: tool.description,
|
|
392
|
+
inputSchema: isPlainRecord(tool.inputSchema) ? tool.inputSchema : undefined,
|
|
393
|
+
outputSchema: isPlainRecord(tool.outputSchema) ? tool.outputSchema : undefined,
|
|
394
|
+
})));
|
|
395
|
+
cursor = page.nextCursor;
|
|
396
|
+
} while (cursor);
|
|
397
|
+
return tools;
|
|
398
|
+
}
|
|
399
|
+
async listResources() {
|
|
400
|
+
if (!this.client) {
|
|
401
|
+
return [];
|
|
402
|
+
}
|
|
403
|
+
const resources = [];
|
|
404
|
+
let cursor;
|
|
405
|
+
// resource 列表同样可能分页,不能假设一次就能拿全。
|
|
406
|
+
do {
|
|
407
|
+
const page = await this.client.listResources(cursor ? { cursor } : undefined, { timeout: this.config.timeoutMs || DEFAULT_TIMEOUT_MS });
|
|
408
|
+
resources.push(...page.resources.map((resource) => ({
|
|
409
|
+
uri: resource.uri,
|
|
410
|
+
name: resource.name,
|
|
411
|
+
description: resource.description,
|
|
412
|
+
mimeType: resource.mimeType,
|
|
413
|
+
})));
|
|
414
|
+
cursor = page.nextCursor;
|
|
415
|
+
} while (cursor);
|
|
416
|
+
return resources;
|
|
417
|
+
}
|
|
418
|
+
async listPrompts() {
|
|
419
|
+
if (!this.client) {
|
|
420
|
+
return [];
|
|
421
|
+
}
|
|
422
|
+
const prompts = [];
|
|
423
|
+
let cursor;
|
|
424
|
+
// prompt 定义里只保留名称、描述和参数信息,足够给模型选择与调用。
|
|
425
|
+
do {
|
|
426
|
+
const page = await this.client.listPrompts(cursor ? { cursor } : undefined, { timeout: this.config.timeoutMs || DEFAULT_TIMEOUT_MS });
|
|
427
|
+
prompts.push(...page.prompts.map((prompt) => ({
|
|
428
|
+
name: prompt.name,
|
|
429
|
+
description: prompt.description,
|
|
430
|
+
arguments: prompt.arguments?.map((arg) => ({
|
|
431
|
+
name: arg.name,
|
|
432
|
+
description: arg.description,
|
|
433
|
+
required: arg.required,
|
|
434
|
+
})),
|
|
435
|
+
})));
|
|
436
|
+
cursor = page.nextCursor;
|
|
437
|
+
} while (cursor);
|
|
438
|
+
return prompts;
|
|
439
|
+
}
|
|
440
|
+
// 把底层异常归一成运行时错误,并顺便修正当前连接状态。
|
|
441
|
+
async handleOperationFailure(error) {
|
|
442
|
+
const message = getErrorMessage(error);
|
|
443
|
+
// 连接被硬断开后主动丢弃 client,让下一次请求走干净的重连流程。
|
|
444
|
+
if (isConnectionClosedError(error)) {
|
|
445
|
+
const snapshot = this.state;
|
|
446
|
+
await this.close();
|
|
447
|
+
this.state = {
|
|
448
|
+
...snapshot,
|
|
449
|
+
status: "disconnected",
|
|
450
|
+
error: message,
|
|
451
|
+
};
|
|
452
|
+
return new McpRuntimeError("transport_error", `MCP server "${this.config.name}" disconnected: ${message}`, { cause: error });
|
|
453
|
+
}
|
|
454
|
+
const code = isTimeoutError(error) ? "request_timeout" : "transport_error";
|
|
455
|
+
// HTTP 传输层错误带状态码,补进消息里能更快区分网络故障与服务端响应异常。
|
|
456
|
+
const normalizedMessage = error instanceof StreamableHTTPError
|
|
457
|
+
? `HTTP ${error.code ?? "unknown"}: ${message}`
|
|
458
|
+
: message;
|
|
459
|
+
this.state = {
|
|
460
|
+
...this.state,
|
|
461
|
+
status: "degraded",
|
|
462
|
+
error: normalizedMessage,
|
|
463
|
+
};
|
|
464
|
+
return new McpRuntimeError(code, `MCP request failed for server "${this.config.name}": ${normalizedMessage}`, { cause: error });
|
|
465
|
+
}
|
|
466
|
+
}
|