u1s1-cli 1.7.1 → 1.8.1

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.
@@ -49,6 +49,13 @@ export declare function writeErrorHumanizeExtension(): void;
49
49
  * print/json 一次性模式不起轮询;import 失败静默跳过(同 error-humanize)。
50
50
  */
51
51
  export declare function writeAnnouncementsExtension(): void;
52
+ /**
53
+ * 生成「自定义 MCP 服务」扩展到 <agentDir>/extensions/u1s1-mcp.js。
54
+ * 读 ~/.u1s1/mcp.json,把每个服务的工具注册成 mcp_<服务>_<工具>;逻辑在 mcp/extension.ts,
55
+ * 这里只负责投影(Desktop App 共享)。没配置任何服务时扩展直接返回,零开销。
56
+ * 子会话(spawn_subagent)里不挂 MCP 工具:避免每个子进程都拉起一份服务。
57
+ */
58
+ export declare function writeMcpExtension(): void;
52
59
  /**
53
60
  * 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
54
61
  * 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
@@ -63,7 +70,7 @@ export declare function writeAttributionExtension(): void;
63
70
  * - 每轮回复结束追加一条汇总行,如 ⚙ 9 tools · bash×4 · read×3 · ✗ 1
64
71
  * - 思考块折叠标签设为空,配合 pi 补丁(见 patches/)消除多余空行
65
72
  */
66
- export declare function writeCompactUiExtension(): void;
73
+ export declare function writeCompactUiExtension(outputDir?: string): void;
67
74
  /**
68
75
  * pi-web-ui 的就绪检测只认 <agentDir>/auth.json 里有没有凭据条目(models.json
69
76
  * 的 apiKey 它不看),空 {} 会在网页里弹「未检测到 pi agent 配置」引导装 pi。
@@ -332,6 +332,35 @@ export function writeAnnouncementsExtension() {
332
332
  ` });\n` +
333
333
  `}\n`);
334
334
  }
335
+ /**
336
+ * 生成「自定义 MCP 服务」扩展到 <agentDir>/extensions/u1s1-mcp.js。
337
+ * 读 ~/.u1s1/mcp.json,把每个服务的工具注册成 mcp_<服务>_<工具>;逻辑在 mcp/extension.ts,
338
+ * 这里只负责投影(Desktop App 共享)。没配置任何服务时扩展直接返回,零开销。
339
+ * 子会话(spawn_subagent)里不挂 MCP 工具:避免每个子进程都拉起一份服务。
340
+ */
341
+ export function writeMcpExtension() {
342
+ const dir = join(agentDir, "extensions");
343
+ mkdirSync(dir, { recursive: true });
344
+ const url = new URL("./mcp/extension.js", import.meta.url).href;
345
+ writeFileSync(join(dir, "u1s1-mcp.js"), `// 由 u1s1 每次启动自动生成,请勿手改\n` +
346
+ `export default async function (pi) {\n` +
347
+ ` if (process.env.U1S1_IN_SUBAGENT === "1") return;\n` +
348
+ ` let registerMcpServers;\n` +
349
+ ` try {\n` +
350
+ ` registerMcpServers = (await import(${JSON.stringify(url)})).registerMcpServers;\n` +
351
+ ` } catch {\n` +
352
+ ` return;\n` +
353
+ ` }\n` +
354
+ ` try {\n` +
355
+ ` await registerMcpServers(pi);\n` +
356
+ ` } catch (err) {\n` +
357
+ ` const msg = err instanceof Error ? err.message : String(err);\n` +
358
+ ` pi.on("session_start", (_event, ctx) => {\n` +
359
+ ` if (ctx.hasUI) ctx.ui.notify("MCP 服务加载失败:" + msg, "warning");\n` +
360
+ ` });\n` +
361
+ ` }\n` +
362
+ `}\n`);
363
+ }
335
364
  /**
336
365
  * 生成「OpenRouter 应用归因」扩展到 <agentDir>/extensions/u1s1-attribution.js。
337
366
  * 用户自己配 OPENROUTER_API_KEY 直连时,pi 内置的归因头归到 pi.dev 名下(且受遥测
@@ -359,8 +388,8 @@ export function writeAttributionExtension() {
359
388
  * - 每轮回复结束追加一条汇总行,如 ⚙ 9 tools · bash×4 · read×3 · ✗ 1
360
389
  * - 思考块折叠标签设为空,配合 pi 补丁(见 patches/)消除多余空行
361
390
  */
362
- export function writeCompactUiExtension() {
363
- const dir = join(agentDir, "extensions");
391
+ export function writeCompactUiExtension(outputDir) {
392
+ const dir = outputDir ?? join(agentDir, "extensions");
364
393
  mkdirSync(dir, { recursive: true });
365
394
  writeFileSync(join(dir, "u1s1-compact-ui.js"), `// 由 u1s1 每次启动自动生成,请勿手改
366
395
  function oneLine(text, max = 160) {
@@ -378,7 +407,7 @@ export default async function (pi) {
378
407
  let counts = {};
379
408
  let errors = 0;
380
409
  // 工具调用被折叠后,这一行摘要是新手了解「AI 刚才做了什么」的唯一入口,内置工具名给人话
381
- const TOOL_SUMMARY_LABELS: Record<string, string> = {
410
+ const TOOL_SUMMARY_LABELS = {
382
411
  read: "读文件", write: "写文件", edit: "改文件", bash: "跑命令", grep: "搜内容", find: "找文件", ls: "列目录",
383
412
  };
384
413
  pi.on("agent_start", async () => {
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { execSync, spawnSync } from "node:child_process";
3
3
  import { writeFileSync } from "node:fs";
4
- import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, ensureWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAnnouncementsExtension, writeAttributionExtension, writeCompactUiExtension, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
4
+ import { cleanupBrandThemes, endpointKeyEnv, endpointProviderEntry, ensureBrandPrompt, ensureDefaultSettings, ensureProviderModels, ensureWorkflowPromptTemplate, scrubForeignProviderEnv, toProviderModels, writeAnnouncementsExtension, writeAttributionExtension, writeMcpExtension, writeCompactUiExtension, writeErrorHumanizeExtension, writeWebToolsExtension, } from "./agent-setup.js";
5
5
  import { Text } from "@earendil-works/pi-tui";
6
6
  import { printConsoleBanner } from "./brand.js";
7
7
  import { agentDir, apiModelToDef, CUSTOM_ENDPOINTS, isPortableInstall, loadConfig, MODELS, persistPreferredModel, PROVIDER_ID, readSettings, refValid, resolvePreferredModel, setModelsFromApi, VERSION, } from "./config.js";
@@ -255,6 +255,8 @@ async function runAgent(cfg, args) {
255
255
  writeAnnouncementsExtension();
256
256
  // OpenRouter 应用归因:直连流量计入 u1s1 的公开排行
257
257
  writeAttributionExtension();
258
+ // 自定义 MCP 服务的工具(~/.u1s1/mcp.json,同样投影,Desktop 共享)
259
+ writeMcpExtension();
258
260
  ensureTmuxKeyboardProtocol();
259
261
  // must be set before pi reads them (getAgentDir() reads at call time);
260
262
  // 常量部分已在 runAgent 开头、发起 import 之前设好
@@ -537,6 +539,7 @@ async function run() {
537
539
  console.log(" u1s1 deploy remove 删除已发布的站点");
538
540
  console.log(" u1s1 feedback \"一句话\" 反馈问题或建议(自动建工单,--bug/--question…)");
539
541
  console.log(" u1s1 import 导入历史会话(import skills 导入技能)");
542
+ console.log(" u1s1 mcp 配置第三方 MCP 服务(add/list/test/remove/import)");
540
543
  console.log(" u1s1 bench 模型编码能力评测");
541
544
  console.log(" u1s1 --version 查看版本");
542
545
  console.log("");
@@ -583,6 +586,11 @@ async function run() {
583
586
  await feedbackCommand(args.slice(1));
584
587
  return;
585
588
  }
589
+ if (cmd === "mcp") {
590
+ const { mcpCommand } = await import("./mcp/command.js");
591
+ await mcpCommand(args.slice(1));
592
+ return;
593
+ }
586
594
  if (cmd === "model") {
587
595
  const { modelCommand } = await import("./model.js");
588
596
  await modelCommand(args[1]);
@@ -656,7 +664,7 @@ async function run() {
656
664
  // autoUpdate 开着且启动时发现了新版:现在装(TUI 已退出,原地重铺安全)
657
665
  installPendingUpdate();
658
666
  }
659
- const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "bench", "feedback", "help"];
667
+ const KNOWN_COMMANDS = ["deploy", "login", "logout", "usage", "model", "update", "import", "mcp", "bench", "feedback", "help"];
660
668
  /** 没有自己 --help 处理的简单子命令:问用法时只打印说明,不执行任何动作。 */
661
669
  const SIMPLE_COMMAND_HELP = {
662
670
  login: [
@@ -0,0 +1,53 @@
1
+ import type { McpServerConfig, McpToolInfo } from "./config.js";
2
+ /**
3
+ * 极简 MCP 客户端:只做 initialize / tools/list / tools/call 三件事,
4
+ * 支持 stdio(本机进程,按行 JSON-RPC)和 Streamable HTTP(POST JSON,
5
+ * 响应可能是 JSON 也可能是 SSE)两种传输。不引 SDK:pi 本身没有 MCP client,
6
+ * 我们只需要把远端工具挂成 pi 工具,协议面很窄,自己写反而少一坨依赖。
7
+ */
8
+ export declare const MCP_PROTOCOL_VERSION = "2025-06-18";
9
+ export declare const DEFAULT_CONNECT_TIMEOUT_MS = 20000;
10
+ export declare const DEFAULT_CALL_TIMEOUT_MS = 120000;
11
+ export interface McpClientOptions {
12
+ connectTimeoutMs?: number;
13
+ callTimeoutMs?: number;
14
+ /** 服务端 stderr / 传输层告警;默认丢弃 */
15
+ onLog?: (line: string) => void;
16
+ }
17
+ export interface McpServerInfo {
18
+ serverName?: string;
19
+ serverVersion?: string;
20
+ protocolVersion: string;
21
+ }
22
+ export interface McpCallResult {
23
+ text: string;
24
+ isError: boolean;
25
+ }
26
+ export declare class McpError extends Error {
27
+ readonly code?: number | undefined;
28
+ constructor(message: string, code?: number | undefined);
29
+ }
30
+ /** 解析 SSE 正文,返回每个 event 的 data 拼接结果 */
31
+ export declare function parseSseData(body: string): string[];
32
+ /** 把 tools/call 返回的 content 数组压成模型可读的文本 */
33
+ export declare function contentToText(result: unknown): McpCallResult;
34
+ export declare function normalizeToolInfo(raw: unknown): McpToolInfo | undefined;
35
+ export declare class McpClient {
36
+ readonly name: string;
37
+ private readonly config;
38
+ private readonly opts;
39
+ private transport;
40
+ private nextId;
41
+ private readonly connectTimeoutMs;
42
+ private readonly callTimeoutMs;
43
+ private connected;
44
+ info: McpServerInfo | undefined;
45
+ constructor(name: string, config: McpServerConfig, opts?: McpClientOptions);
46
+ /** 建连并完成 initialize 握手;重复调用返回同一个 promise,失败后可重试 */
47
+ connect(): Promise<McpServerInfo>;
48
+ private doConnect;
49
+ private rpc;
50
+ listTools(): Promise<McpToolInfo[]>;
51
+ callTool(name: string, args: Record<string, unknown>, signal?: AbortSignal): Promise<McpCallResult>;
52
+ close(): Promise<void>;
53
+ }
@@ -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>;