u1s1-cli 1.7.0 → 1.8.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/dist/agent-setup.d.ts +7 -0
- package/dist/agent-setup.js +35 -2
- package/dist/api.d.ts +5 -0
- package/dist/api.js +5 -0
- package/dist/brand.js +1 -1
- package/dist/error-humanize.js +21 -1
- package/dist/index.js +47 -4
- package/dist/login.js +2 -0
- package/dist/mcp/client.d.ts +53 -0
- package/dist/mcp/client.js +452 -0
- package/dist/mcp/command.d.ts +25 -0
- package/dist/mcp/command.js +349 -0
- package/dist/mcp/config.d.ts +59 -0
- package/dist/mcp/config.js +0 -0
- package/dist/mcp/extension.d.ts +14 -0
- package/dist/mcp/extension.js +128 -0
- package/dist/mcp/tools.d.ts +22 -0
- package/dist/mcp/tools.js +96 -0
- package/dist/nudges.d.ts +1 -1
- package/dist/nudges.js +1 -1
- package/dist/update.js +1 -0
- package/dist/usage.d.ts +6 -2
- package/dist/usage.js +12 -5
- package/dist/web.js +3 -1
- package/package.json +1 -1
- package/scripts/patch-pi.js +151 -0
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { VERSION } from "../config.js";
|
|
3
|
+
/**
|
|
4
|
+
* 极简 MCP 客户端:只做 initialize / tools/list / tools/call 三件事,
|
|
5
|
+
* 支持 stdio(本机进程,按行 JSON-RPC)和 Streamable HTTP(POST JSON,
|
|
6
|
+
* 响应可能是 JSON 也可能是 SSE)两种传输。不引 SDK:pi 本身没有 MCP client,
|
|
7
|
+
* 我们只需要把远端工具挂成 pi 工具,协议面很窄,自己写反而少一坨依赖。
|
|
8
|
+
*/
|
|
9
|
+
export const MCP_PROTOCOL_VERSION = "2025-06-18";
|
|
10
|
+
export const DEFAULT_CONNECT_TIMEOUT_MS = 20_000;
|
|
11
|
+
export const DEFAULT_CALL_TIMEOUT_MS = 120_000;
|
|
12
|
+
export class McpError extends Error {
|
|
13
|
+
code;
|
|
14
|
+
constructor(message, code) {
|
|
15
|
+
super(message);
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.name = "McpError";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
/** 谁先到算谁的:结果、超时、取消三者任一落定就清掉定时器与监听,别让悬着的定时器吊住进程 */
|
|
21
|
+
function withTimeout(promise, timeoutMs, signal, what) {
|
|
22
|
+
return new Promise((resolve, reject) => {
|
|
23
|
+
let settled = false;
|
|
24
|
+
const finish = (fn) => (v) => {
|
|
25
|
+
if (settled)
|
|
26
|
+
return;
|
|
27
|
+
settled = true;
|
|
28
|
+
clearTimeout(timer);
|
|
29
|
+
signal?.removeEventListener("abort", onAbort);
|
|
30
|
+
fn(v);
|
|
31
|
+
};
|
|
32
|
+
const onAbort = () => finish(reject)(new McpError(`${what} 已取消`));
|
|
33
|
+
const timer = setTimeout(() => finish(reject)(new McpError(`${what} 超过 ${Math.round(timeoutMs / 1000)}s 未响应`)), timeoutMs);
|
|
34
|
+
promise.then(finish(resolve), finish(reject));
|
|
35
|
+
if (signal?.aborted)
|
|
36
|
+
onAbort();
|
|
37
|
+
else
|
|
38
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
function asRecord(value) {
|
|
42
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
43
|
+
}
|
|
44
|
+
// ---------------- stdio ----------------
|
|
45
|
+
class StdioTransport {
|
|
46
|
+
command;
|
|
47
|
+
args;
|
|
48
|
+
env;
|
|
49
|
+
onLog;
|
|
50
|
+
child;
|
|
51
|
+
buffer = "";
|
|
52
|
+
pending = new Map();
|
|
53
|
+
exited;
|
|
54
|
+
stderrTail = [];
|
|
55
|
+
constructor(command, args, env, onLog) {
|
|
56
|
+
this.command = command;
|
|
57
|
+
this.args = args;
|
|
58
|
+
this.env = env;
|
|
59
|
+
this.onLog = onLog;
|
|
60
|
+
}
|
|
61
|
+
start() {
|
|
62
|
+
const child = spawn(this.command, this.args, {
|
|
63
|
+
env: { ...process.env, ...this.env },
|
|
64
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
65
|
+
// Windows 上 npx / *.cmd 只能经 shell 启动
|
|
66
|
+
shell: process.platform === "win32",
|
|
67
|
+
windowsHide: true,
|
|
68
|
+
});
|
|
69
|
+
this.child = child;
|
|
70
|
+
child.stdout?.setEncoding("utf8");
|
|
71
|
+
child.stdout?.on("data", (chunk) => this.onData(chunk));
|
|
72
|
+
child.stderr?.setEncoding("utf8");
|
|
73
|
+
child.stderr?.on("data", (chunk) => {
|
|
74
|
+
for (const line of chunk.split(/\r?\n/)) {
|
|
75
|
+
if (!line.trim())
|
|
76
|
+
continue;
|
|
77
|
+
this.stderrTail.push(line);
|
|
78
|
+
if (this.stderrTail.length > 20)
|
|
79
|
+
this.stderrTail.shift();
|
|
80
|
+
this.onLog?.(line);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
// CLI 自己退出时把服务进程一起带走,别留孤儿
|
|
84
|
+
const onParentExit = () => {
|
|
85
|
+
try {
|
|
86
|
+
child.kill("SIGKILL");
|
|
87
|
+
}
|
|
88
|
+
catch { }
|
|
89
|
+
};
|
|
90
|
+
process.once("exit", onParentExit);
|
|
91
|
+
child.on("error", (err) => {
|
|
92
|
+
// spawn 失败(ENOENT 等)不会再有 exit 事件
|
|
93
|
+
if (child.pid === undefined)
|
|
94
|
+
process.removeListener("exit", onParentExit);
|
|
95
|
+
this.fail(new McpError(`启动 ${this.command} 失败:${err.message}`));
|
|
96
|
+
});
|
|
97
|
+
child.on("exit", (code, sig) => {
|
|
98
|
+
process.removeListener("exit", onParentExit);
|
|
99
|
+
const tail = this.stderrTail.slice(-5).join("\n");
|
|
100
|
+
this.fail(new McpError(`MCP 服务进程已退出(${sig ?? `code ${code ?? 0}`})${tail ? `\n${tail}` : ""}`));
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
fail(err) {
|
|
104
|
+
if (this.exited)
|
|
105
|
+
return;
|
|
106
|
+
this.exited = err;
|
|
107
|
+
for (const p of this.pending.values())
|
|
108
|
+
p.reject(err);
|
|
109
|
+
this.pending.clear();
|
|
110
|
+
}
|
|
111
|
+
onData(chunk) {
|
|
112
|
+
this.buffer += chunk;
|
|
113
|
+
let idx = this.buffer.indexOf("\n");
|
|
114
|
+
while (idx >= 0) {
|
|
115
|
+
const line = this.buffer.slice(0, idx).trim();
|
|
116
|
+
this.buffer = this.buffer.slice(idx + 1);
|
|
117
|
+
if (line)
|
|
118
|
+
this.onLine(line);
|
|
119
|
+
idx = this.buffer.indexOf("\n");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
onLine(line) {
|
|
123
|
+
let msg;
|
|
124
|
+
try {
|
|
125
|
+
msg = JSON.parse(line);
|
|
126
|
+
}
|
|
127
|
+
catch {
|
|
128
|
+
this.onLog?.(line);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (msg.id !== undefined && msg.method === undefined) {
|
|
132
|
+
const p = this.pending.get(msg.id);
|
|
133
|
+
if (p) {
|
|
134
|
+
this.pending.delete(msg.id);
|
|
135
|
+
p.resolve(msg);
|
|
136
|
+
}
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (msg.id !== undefined && msg.method) {
|
|
140
|
+
// 服务端反向请求(sampling / roots 等)不支持,回一个 method not found 免得它挂着
|
|
141
|
+
this.write({ jsonrpc: "2.0", id: msg.id, error: { code: -32601, message: `Method not supported by u1s1: ${msg.method}` } });
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
write(msg) {
|
|
145
|
+
if (this.exited)
|
|
146
|
+
throw this.exited;
|
|
147
|
+
const stdin = this.child?.stdin;
|
|
148
|
+
if (!stdin || stdin.destroyed)
|
|
149
|
+
throw new McpError("MCP 服务进程 stdin 已关闭");
|
|
150
|
+
stdin.write(`${JSON.stringify(msg)}\n`);
|
|
151
|
+
}
|
|
152
|
+
request(msg, timeoutMs, signal) {
|
|
153
|
+
const id = msg.id;
|
|
154
|
+
const promise = new Promise((resolve, reject) => {
|
|
155
|
+
this.pending.set(id, { resolve, reject });
|
|
156
|
+
try {
|
|
157
|
+
this.write(msg);
|
|
158
|
+
}
|
|
159
|
+
catch (err) {
|
|
160
|
+
this.pending.delete(id);
|
|
161
|
+
reject(err);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
return withTimeout(promise, timeoutMs, signal, msg.method ?? "请求").finally(() => this.pending.delete(id));
|
|
165
|
+
}
|
|
166
|
+
async notify(msg) {
|
|
167
|
+
this.write(msg);
|
|
168
|
+
}
|
|
169
|
+
async close() {
|
|
170
|
+
const child = this.child;
|
|
171
|
+
if (!child)
|
|
172
|
+
return;
|
|
173
|
+
this.fail(new McpError("MCP 连接已关闭"));
|
|
174
|
+
try {
|
|
175
|
+
child.stdin?.end();
|
|
176
|
+
}
|
|
177
|
+
catch { }
|
|
178
|
+
// 没 pid = 根本没启动起来(spawn error),不会有 exit 事件,别等
|
|
179
|
+
if (child.pid !== undefined && child.exitCode === null && child.signalCode === null) {
|
|
180
|
+
await new Promise((resolve) => {
|
|
181
|
+
const done = () => {
|
|
182
|
+
clearTimeout(soft);
|
|
183
|
+
clearTimeout(hard);
|
|
184
|
+
resolve();
|
|
185
|
+
};
|
|
186
|
+
child.once("exit", done);
|
|
187
|
+
child.kill();
|
|
188
|
+
const soft = setTimeout(() => child.kill("SIGKILL"), 2_000);
|
|
189
|
+
const hard = setTimeout(done, 3_000);
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// ---------------- Streamable HTTP ----------------
|
|
195
|
+
/** 解析 SSE 正文,返回每个 event 的 data 拼接结果 */
|
|
196
|
+
export function parseSseData(body) {
|
|
197
|
+
const out = [];
|
|
198
|
+
let data = [];
|
|
199
|
+
for (const rawLine of body.split(/\r?\n/)) {
|
|
200
|
+
if (rawLine === "") {
|
|
201
|
+
if (data.length)
|
|
202
|
+
out.push(data.join("\n"));
|
|
203
|
+
data = [];
|
|
204
|
+
continue;
|
|
205
|
+
}
|
|
206
|
+
if (rawLine.startsWith(":"))
|
|
207
|
+
continue;
|
|
208
|
+
const idx = rawLine.indexOf(":");
|
|
209
|
+
const field = idx === -1 ? rawLine : rawLine.slice(0, idx);
|
|
210
|
+
if (field !== "data")
|
|
211
|
+
continue;
|
|
212
|
+
let value = idx === -1 ? "" : rawLine.slice(idx + 1);
|
|
213
|
+
if (value.startsWith(" "))
|
|
214
|
+
value = value.slice(1);
|
|
215
|
+
data.push(value);
|
|
216
|
+
}
|
|
217
|
+
if (data.length)
|
|
218
|
+
out.push(data.join("\n"));
|
|
219
|
+
return out;
|
|
220
|
+
}
|
|
221
|
+
class HttpTransport {
|
|
222
|
+
url;
|
|
223
|
+
headers;
|
|
224
|
+
sessionId;
|
|
225
|
+
protocolVersion;
|
|
226
|
+
constructor(url, headers) {
|
|
227
|
+
this.url = url;
|
|
228
|
+
this.headers = headers;
|
|
229
|
+
}
|
|
230
|
+
setProtocolVersion(v) {
|
|
231
|
+
this.protocolVersion = v;
|
|
232
|
+
}
|
|
233
|
+
buildHeaders() {
|
|
234
|
+
const h = {
|
|
235
|
+
"content-type": "application/json",
|
|
236
|
+
accept: "application/json, text/event-stream",
|
|
237
|
+
...this.headers,
|
|
238
|
+
};
|
|
239
|
+
if (this.sessionId)
|
|
240
|
+
h["mcp-session-id"] = this.sessionId;
|
|
241
|
+
if (this.protocolVersion)
|
|
242
|
+
h["mcp-protocol-version"] = this.protocolVersion;
|
|
243
|
+
return h;
|
|
244
|
+
}
|
|
245
|
+
async post(msg, timeoutMs, signal) {
|
|
246
|
+
const ctrl = new AbortController();
|
|
247
|
+
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
248
|
+
const onAbort = () => ctrl.abort();
|
|
249
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
250
|
+
try {
|
|
251
|
+
return await fetch(this.url, { method: "POST", headers: this.buildHeaders(), body: JSON.stringify(msg), signal: ctrl.signal });
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
if (signal?.aborted)
|
|
255
|
+
throw new McpError(`${msg.method ?? "请求"} 已取消`);
|
|
256
|
+
if (ctrl.signal.aborted)
|
|
257
|
+
throw new McpError(`${msg.method ?? "请求"} 超过 ${Math.round(timeoutMs / 1000)}s 未响应`);
|
|
258
|
+
throw new McpError(`连接 ${this.url} 失败:${err.message}`);
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
clearTimeout(timer);
|
|
262
|
+
signal?.removeEventListener("abort", onAbort);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
async request(msg, timeoutMs, signal) {
|
|
266
|
+
const resp = await this.post(msg, timeoutMs, signal);
|
|
267
|
+
const sid = resp.headers.get("mcp-session-id");
|
|
268
|
+
if (sid)
|
|
269
|
+
this.sessionId = sid;
|
|
270
|
+
if (!resp.ok) {
|
|
271
|
+
const text = (await resp.text().catch(() => "")).slice(0, 300);
|
|
272
|
+
if (resp.status === 401 || resp.status === 403) {
|
|
273
|
+
throw new McpError(`服务拒绝访问(HTTP ${resp.status}),请检查 headers 里的鉴权信息${text ? `:${text}` : ""}`, resp.status);
|
|
274
|
+
}
|
|
275
|
+
throw new McpError(`HTTP ${resp.status}${text ? `:${text}` : ""}`, resp.status);
|
|
276
|
+
}
|
|
277
|
+
const ctype = (resp.headers.get("content-type") ?? "").toLowerCase();
|
|
278
|
+
const body = await resp.text();
|
|
279
|
+
const candidates = ctype.includes("text/event-stream") ? parseSseData(body) : [body];
|
|
280
|
+
for (const raw of candidates) {
|
|
281
|
+
let parsed;
|
|
282
|
+
try {
|
|
283
|
+
parsed = JSON.parse(raw);
|
|
284
|
+
}
|
|
285
|
+
catch {
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
const list = Array.isArray(parsed) ? parsed : [parsed];
|
|
289
|
+
for (const item of list) {
|
|
290
|
+
const m = item;
|
|
291
|
+
if (m && m.id === msg.id && m.method === undefined)
|
|
292
|
+
return m;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
throw new McpError(`服务没有返回 ${msg.method ?? "请求"} 的结果(content-type ${ctype || "unknown"})`);
|
|
296
|
+
}
|
|
297
|
+
async notify(msg) {
|
|
298
|
+
const resp = await this.post(msg, 10_000);
|
|
299
|
+
// 规范要求 202;有些实现回 200/204,都当成功。鉴权失败要冒出来
|
|
300
|
+
if (resp.status === 401 || resp.status === 403)
|
|
301
|
+
throw new McpError(`服务拒绝访问(HTTP ${resp.status})`, resp.status);
|
|
302
|
+
await resp.body?.cancel().catch(() => { });
|
|
303
|
+
}
|
|
304
|
+
async close() {
|
|
305
|
+
if (!this.sessionId)
|
|
306
|
+
return;
|
|
307
|
+
try {
|
|
308
|
+
await fetch(this.url, { method: "DELETE", headers: this.buildHeaders(), signal: AbortSignal.timeout(3_000) });
|
|
309
|
+
}
|
|
310
|
+
catch { }
|
|
311
|
+
this.sessionId = undefined;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
// ---------------- client ----------------
|
|
315
|
+
/** 把 tools/call 返回的 content 数组压成模型可读的文本 */
|
|
316
|
+
export function contentToText(result) {
|
|
317
|
+
const rec = asRecord(result) ?? {};
|
|
318
|
+
const parts = [];
|
|
319
|
+
const content = Array.isArray(rec["content"]) ? rec["content"] : [];
|
|
320
|
+
for (const raw of content) {
|
|
321
|
+
const item = asRecord(raw);
|
|
322
|
+
if (!item)
|
|
323
|
+
continue;
|
|
324
|
+
const type = item["type"];
|
|
325
|
+
if (type === "text" && typeof item["text"] === "string") {
|
|
326
|
+
parts.push(item["text"]);
|
|
327
|
+
}
|
|
328
|
+
else if (type === "image" || type === "audio") {
|
|
329
|
+
const data = typeof item["data"] === "string" ? item["data"] : "";
|
|
330
|
+
parts.push(`[${type} ${String(item["mimeType"] ?? "")} ${Buffer.from(data, "base64").length} bytes, omitted]`);
|
|
331
|
+
}
|
|
332
|
+
else if (type === "resource") {
|
|
333
|
+
const res = asRecord(item["resource"]);
|
|
334
|
+
if (res && typeof res["text"] === "string")
|
|
335
|
+
parts.push(res["text"]);
|
|
336
|
+
else
|
|
337
|
+
parts.push(`[resource ${String(res?.["uri"] ?? "")} (binary, omitted)]`);
|
|
338
|
+
}
|
|
339
|
+
else if (type === "resource_link") {
|
|
340
|
+
parts.push(`[link] ${String(item["uri"] ?? "")}${item["name"] ? ` ${String(item["name"])}` : ""}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
if (parts.length === 0 && rec["structuredContent"] !== undefined) {
|
|
344
|
+
parts.push(JSON.stringify(rec["structuredContent"], null, 2));
|
|
345
|
+
}
|
|
346
|
+
return { text: parts.join("\n").trim(), isError: rec["isError"] === true };
|
|
347
|
+
}
|
|
348
|
+
export function normalizeToolInfo(raw) {
|
|
349
|
+
const rec = asRecord(raw);
|
|
350
|
+
if (!rec || typeof rec["name"] !== "string" || !rec["name"])
|
|
351
|
+
return undefined;
|
|
352
|
+
const schema = asRecord(rec["inputSchema"]) ?? { type: "object", properties: {} };
|
|
353
|
+
return {
|
|
354
|
+
name: rec["name"],
|
|
355
|
+
description: typeof rec["description"] === "string" ? rec["description"] : "",
|
|
356
|
+
inputSchema: { ...schema, type: "object" },
|
|
357
|
+
};
|
|
358
|
+
}
|
|
359
|
+
export class McpClient {
|
|
360
|
+
name;
|
|
361
|
+
config;
|
|
362
|
+
opts;
|
|
363
|
+
transport;
|
|
364
|
+
nextId = 1;
|
|
365
|
+
connectTimeoutMs;
|
|
366
|
+
callTimeoutMs;
|
|
367
|
+
connected;
|
|
368
|
+
info;
|
|
369
|
+
constructor(name, config, opts = {}) {
|
|
370
|
+
this.name = name;
|
|
371
|
+
this.config = config;
|
|
372
|
+
this.opts = opts;
|
|
373
|
+
this.connectTimeoutMs = opts.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS;
|
|
374
|
+
this.callTimeoutMs = opts.callTimeoutMs ?? DEFAULT_CALL_TIMEOUT_MS;
|
|
375
|
+
}
|
|
376
|
+
/** 建连并完成 initialize 握手;重复调用返回同一个 promise,失败后可重试 */
|
|
377
|
+
connect() {
|
|
378
|
+
if (!this.connected) {
|
|
379
|
+
this.connected = this.doConnect().catch(async (err) => {
|
|
380
|
+
this.connected = undefined;
|
|
381
|
+
await this.close().catch(() => { });
|
|
382
|
+
throw err;
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
return this.connected;
|
|
386
|
+
}
|
|
387
|
+
async doConnect() {
|
|
388
|
+
const transport = this.config.type === "stdio"
|
|
389
|
+
? new StdioTransport(this.config.command, this.config.args, this.config.env, this.opts.onLog)
|
|
390
|
+
: new HttpTransport(this.config.url, this.config.headers);
|
|
391
|
+
if (transport instanceof StdioTransport)
|
|
392
|
+
transport.start();
|
|
393
|
+
this.transport = transport;
|
|
394
|
+
const result = await this.rpc("initialize", {
|
|
395
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
396
|
+
capabilities: {},
|
|
397
|
+
clientInfo: { name: "u1s1", version: VERSION },
|
|
398
|
+
}, this.connectTimeoutMs);
|
|
399
|
+
const rec = asRecord(result) ?? {};
|
|
400
|
+
const serverInfo = asRecord(rec["serverInfo"]);
|
|
401
|
+
const protocolVersion = typeof rec["protocolVersion"] === "string" ? rec["protocolVersion"] : MCP_PROTOCOL_VERSION;
|
|
402
|
+
if (transport instanceof HttpTransport)
|
|
403
|
+
transport.setProtocolVersion(protocolVersion);
|
|
404
|
+
await transport.notify({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
405
|
+
this.info = {
|
|
406
|
+
serverName: typeof serverInfo?.["name"] === "string" ? serverInfo["name"] : undefined,
|
|
407
|
+
serverVersion: typeof serverInfo?.["version"] === "string" ? serverInfo["version"] : undefined,
|
|
408
|
+
protocolVersion,
|
|
409
|
+
};
|
|
410
|
+
return this.info;
|
|
411
|
+
}
|
|
412
|
+
async rpc(method, params, timeoutMs, signal) {
|
|
413
|
+
const transport = this.transport;
|
|
414
|
+
if (!transport)
|
|
415
|
+
throw new McpError("尚未连接");
|
|
416
|
+
const id = this.nextId++;
|
|
417
|
+
const resp = await transport.request({ jsonrpc: "2.0", id, method, params }, timeoutMs, signal);
|
|
418
|
+
if (resp.error) {
|
|
419
|
+
throw new McpError(`${resp.error.message}${resp.error.data !== undefined ? ` ${JSON.stringify(resp.error.data).slice(0, 300)}` : ""}`, resp.error.code);
|
|
420
|
+
}
|
|
421
|
+
return resp.result;
|
|
422
|
+
}
|
|
423
|
+
async listTools() {
|
|
424
|
+
await this.connect();
|
|
425
|
+
const tools = [];
|
|
426
|
+
let cursor;
|
|
427
|
+
for (let page = 0; page < 50; page++) {
|
|
428
|
+
const result = asRecord(await this.rpc("tools/list", cursor ? { cursor } : {}, this.connectTimeoutMs)) ?? {};
|
|
429
|
+
for (const raw of Array.isArray(result["tools"]) ? result["tools"] : []) {
|
|
430
|
+
const tool = normalizeToolInfo(raw);
|
|
431
|
+
if (tool)
|
|
432
|
+
tools.push(tool);
|
|
433
|
+
}
|
|
434
|
+
cursor = typeof result["nextCursor"] === "string" && result["nextCursor"] ? result["nextCursor"] : undefined;
|
|
435
|
+
if (!cursor)
|
|
436
|
+
break;
|
|
437
|
+
}
|
|
438
|
+
return tools;
|
|
439
|
+
}
|
|
440
|
+
async callTool(name, args, signal) {
|
|
441
|
+
await this.connect();
|
|
442
|
+
const result = await this.rpc("tools/call", { name, arguments: args }, this.callTimeoutMs, signal);
|
|
443
|
+
return contentToText(result);
|
|
444
|
+
}
|
|
445
|
+
async close() {
|
|
446
|
+
const t = this.transport;
|
|
447
|
+
this.transport = undefined;
|
|
448
|
+
this.connected = undefined;
|
|
449
|
+
if (t)
|
|
450
|
+
await t.close();
|
|
451
|
+
}
|
|
452
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type McpServerConfig } from "./config.js";
|
|
2
|
+
/**
|
|
3
|
+
* `u1s1 mcp` 子命令:管理自定义 MCP 服务。
|
|
4
|
+
* add 时先连一次拉工具清单再落盘,坏配置不会拖慢每次启动。
|
|
5
|
+
*/
|
|
6
|
+
export declare function printMcpHelp(): void;
|
|
7
|
+
export declare class McpUsageError extends Error {
|
|
8
|
+
}
|
|
9
|
+
export interface ParsedAdd {
|
|
10
|
+
name: string;
|
|
11
|
+
server: McpServerConfig;
|
|
12
|
+
force: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare function parseAddArgs(args: string[]): ParsedAdd;
|
|
15
|
+
export interface ImportCandidate {
|
|
16
|
+
name: string;
|
|
17
|
+
server: McpServerConfig;
|
|
18
|
+
source: string;
|
|
19
|
+
}
|
|
20
|
+
/** Claude Code 的 MCP 配置:~/.claude.json(用户级 + 各项目)与项目 .mcp.json */
|
|
21
|
+
export declare function collectClaudeMcpServers(home?: string, cwd?: string): {
|
|
22
|
+
candidates: ImportCandidate[];
|
|
23
|
+
unsupported: string[];
|
|
24
|
+
};
|
|
25
|
+
export declare function mcpCommand(args: string[]): Promise<void>;
|