c0de-agent 1.13.1 → 1.14.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/cli/commands/serve.js +5 -1
- package/dist/cli/deps.d.ts +2 -4
- package/dist/cli/deps.js +22 -36
- package/dist/cli/modes/print.js +27 -17
- package/dist/core/agent.js +59 -36
- package/dist/core/agents/parallel.js +4 -2
- package/dist/core/config-path.d.ts +3 -1
- package/dist/core/config-path.js +33 -3
- package/dist/core/config.js +81 -0
- package/dist/core/context.js +6 -3
- package/dist/core/loop/persist.js +4 -1
- package/dist/core/loop/subagent.d.ts +19 -0
- package/dist/core/loop/subagent.js +73 -7
- package/dist/core/loop.d.ts +4 -1
- package/dist/core/loop.js +22 -6
- package/dist/{server/registry-config.d.ts → core/provider-registry.d.ts} +10 -2
- package/dist/core/provider-registry.js +66 -0
- package/dist/core/redact.d.ts +2 -1
- package/dist/core/redact.js +4 -2
- package/dist/core/secret.d.ts +5 -0
- package/dist/core/secret.js +14 -0
- package/dist/core/skills.d.ts +2 -0
- package/dist/core/skills.js +61 -0
- package/dist/core/slash.js +31 -18
- package/dist/core/title.js +4 -2
- package/dist/core/todo-tags.d.ts +4 -3
- package/dist/core/todo-tags.js +20 -6
- package/dist/core/workflow.d.ts +4 -13
- package/dist/core/workflow.js +6 -6
- package/dist/core/workflows/context.js +31 -59
- package/dist/core/workflows/runtime.js +10 -8
- package/dist/dap/index.d.ts +1 -0
- package/dist/dap/index.js +1 -0
- package/dist/dap/protocol.d.ts +8 -3
- package/dist/dap/protocol.js +54 -18
- package/dist/dap/session.js +71 -12
- package/dist/dap/spawn.d.ts +11 -0
- package/dist/dap/spawn.js +48 -0
- package/dist/dap/transport.js +18 -0
- package/dist/kanban/index.d.ts +1 -1
- package/dist/kanban/index.js +1 -1
- package/dist/kanban/store.d.ts +8 -1
- package/dist/kanban/store.js +61 -13
- package/dist/llm/protocols/openai-compat.d.ts +7 -0
- package/dist/llm/protocols/openai-compat.js +19 -2
- package/dist/llm/protocols/utils/tool-stream.d.ts +12 -3
- package/dist/llm/protocols/utils/tool-stream.js +47 -23
- package/dist/llm/provider.js +18 -3
- package/dist/llm/retry.d.ts +1 -0
- package/dist/llm/retry.js +20 -7
- package/dist/llm/routing.js +14 -2
- package/dist/llm/schema/messages.d.ts +9 -2
- package/dist/llm/token.d.ts +9 -0
- package/dist/llm/token.js +10 -6
- package/dist/llm/transport.js +7 -2
- package/dist/mcp/client.d.ts +11 -0
- package/dist/mcp/client.js +70 -0
- package/dist/mcp/index.d.ts +46 -0
- package/dist/mcp/index.js +106 -0
- package/dist/mcp/tool-adapter.d.ts +14 -0
- package/dist/mcp/tool-adapter.js +65 -0
- package/dist/mcp/transport.d.ts +35 -0
- package/dist/mcp/transport.js +243 -0
- package/dist/mcp/types.d.ts +55 -0
- package/dist/mcp/types.js +3 -0
- package/dist/plugins/init.d.ts +2 -0
- package/dist/plugins/init.js +10 -1
- package/dist/project/project.js +50 -3
- package/dist/project/resolve.d.ts +7 -0
- package/dist/project/resolve.js +52 -5
- package/dist/server/permission/interactive.js +9 -4
- package/dist/server/permission/store.js +8 -2
- package/dist/server/routes/auth.js +28 -13
- package/dist/server/routes/chat.js +92 -26
- package/dist/server/routes/config.js +38 -10
- package/dist/server/routes/files.js +75 -16
- package/dist/server/routes/filesystem.js +9 -2
- package/dist/server/routes/kanban.js +146 -9
- package/dist/server/routes/permissions.js +32 -0
- package/dist/server/routes/project.js +15 -6
- package/dist/server/routes/provider.js +10 -5
- package/dist/server/routes/session.js +72 -26
- package/dist/server/routes/terminal.js +10 -2
- package/dist/server/routes/todo.js +18 -0
- package/dist/server/routes/tool.js +13 -2
- package/dist/server/routes/update.js +5 -1
- package/dist/server/routes/workflows.js +17 -6
- package/dist/server/server.d.ts +1 -1
- package/dist/server/server.js +31 -17
- package/dist/server/terminal/pty-manager.d.ts +3 -0
- package/dist/server/terminal/pty-manager.js +8 -3
- package/dist/server/types.d.ts +2 -0
- package/dist/server/util/json-body.d.ts +31 -0
- package/dist/server/util/json-body.js +38 -0
- package/dist/session/archive.js +4 -4
- package/dist/session/branch.d.ts +10 -1
- package/dist/session/branch.js +54 -9
- package/dist/session/compaction.d.ts +5 -0
- package/dist/session/compaction.js +15 -10
- package/dist/session/context.d.ts +7 -3
- package/dist/session/context.js +35 -9
- package/dist/session/import.js +53 -6
- package/dist/session/index.d.ts +2 -2
- package/dist/session/index.js +2 -2
- package/dist/session/message.d.ts +12 -3
- package/dist/session/message.js +44 -4
- package/dist/session/session.d.ts +11 -5
- package/dist/session/session.js +99 -88
- package/dist/session/shake.js +35 -25
- package/dist/session/squash.js +3 -4
- package/dist/session/token.d.ts +7 -8
- package/dist/session/token.js +19 -16
- package/dist/session/usage.js +4 -2
- package/dist/shared/index.d.ts +1 -0
- package/dist/shared/index.js +1 -0
- package/dist/shared/round.d.ts +12 -0
- package/dist/shared/round.js +18 -0
- package/dist/shared/timer.d.ts +23 -0
- package/dist/shared/timer.js +27 -0
- package/dist/shared/types/tool.d.ts +4 -0
- package/dist/shared/utils/markdown-code.d.ts +15 -0
- package/dist/shared/utils/markdown-code.js +44 -0
- package/dist/shared/utils/path.d.ts +4 -1
- package/dist/shared/utils/path.js +5 -2
- package/dist/shared/utils/string.d.ts +5 -0
- package/dist/shared/utils/string.js +52 -0
- package/dist/tools/builtin/bash.js +79 -10
- package/dist/tools/builtin/edit.js +67 -8
- package/dist/tools/builtin/glob.js +150 -14
- package/dist/tools/builtin/grep.js +53 -11
- package/dist/tools/builtin/kanban.d.ts +14 -0
- package/dist/tools/builtin/kanban.js +4 -1
- package/dist/tools/builtin/read.js +15 -2
- package/dist/tools/builtin/resolvers.d.ts +9 -2
- package/dist/tools/builtin/resolvers.js +20 -6
- package/dist/tools/builtin/task.js +107 -25
- package/dist/tools/builtin/todo.js +84 -9
- package/dist/tools/builtin/write.js +7 -1
- package/dist/tools/builtin/yield.js +9 -1
- package/dist/tools/executor.js +53 -1
- package/dist/tools/hashline/patch.js +114 -38
- package/dist/tools/truncate.js +45 -16
- package/dist/tools/validate.js +66 -20
- package/dist/tools/websearch/index.js +8 -4
- package/dist/update/index.d.ts +1 -1
- package/dist/update/index.js +1 -1
- package/dist/update/scheduler.js +9 -2
- package/dist/update/snapshot.d.ts +14 -4
- package/dist/update/snapshot.js +22 -22
- package/dist/update/updater.d.ts +16 -2
- package/dist/update/updater.js +39 -8
- package/dist/update/version.d.ts +3 -1
- package/dist/update/version.js +63 -9
- package/package.json +1 -1
- package/dist/server/registry-config.js +0 -44
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Config, MCPServerConfig } from '../shared/types/config.js';
|
|
2
|
+
import type { ToolDef, ToolResult } from '../shared/types/tool.js';
|
|
3
|
+
import type { ToolRegistry } from '../tools/types.js';
|
|
4
|
+
import { type MCPTransport } from './transport.js';
|
|
5
|
+
import type { MCPSession } from './types.js';
|
|
6
|
+
/** initialize/tools/list 的默认超时。 */
|
|
7
|
+
declare const CONNECT_TIMEOUT_MS = 30000;
|
|
8
|
+
type ConnectOptions = {
|
|
9
|
+
/** initialize/tools/list 超时(默认 30s)。 */
|
|
10
|
+
timeoutMs?: number;
|
|
11
|
+
/** 测试注入:直接提供 transport,跳过按配置构建。 */
|
|
12
|
+
transport?: MCPTransport;
|
|
13
|
+
/** 测试注入:按配置构建 transport 的工厂(默认 buildTransport)。 */
|
|
14
|
+
createTransport?: (config: MCPServerConfig) => MCPTransport;
|
|
15
|
+
};
|
|
16
|
+
/** 连接一个 MCP 服务器:initialize 握手 → tools/list → 返回会话。失败抛错。 */
|
|
17
|
+
declare function connectMCPServer(config: MCPServerConfig, opts?: ConnectOptions): Promise<MCPSession>;
|
|
18
|
+
/** 会话已发现工具 → 内部 ToolDef[](spec §6.2 discoverTools)。 */
|
|
19
|
+
declare function discoverTools(session: MCPSession): ToolDef[];
|
|
20
|
+
/** 直接调用一个 MCP 工具(spec §6.2 callMCPTool)。 */
|
|
21
|
+
declare function callMCPTool(session: MCPSession, name: string, args: unknown): Promise<ToolResult>;
|
|
22
|
+
/** 断开连接(spec §6.2 disconnectMCPServer)。幂等。 */
|
|
23
|
+
declare function disconnectMCPServer(session: MCPSession): void;
|
|
24
|
+
type RegisterResult = {
|
|
25
|
+
/** 已连接会话(宿主持有,用于关闭清理)。 */
|
|
26
|
+
sessions: MCPSession[];
|
|
27
|
+
/** 连接失败列表:`<serverName>: <原因>`。 */
|
|
28
|
+
failures: string[];
|
|
29
|
+
};
|
|
30
|
+
type RegisterOptions = {
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
createTransport?: (config: MCPServerConfig) => MCPTransport;
|
|
33
|
+
};
|
|
34
|
+
/** 连接多台 MCP 服务器并把工具注册进 registry。逐台隔离,失败不抛。 */
|
|
35
|
+
declare function registerMCPServers(registry: ToolRegistry, servers: MCPServerConfig[], opts?: RegisterOptions): Promise<RegisterResult>;
|
|
36
|
+
/**
|
|
37
|
+
* 从配置作用域收集「当前应连接」的 MCP 服务器:
|
|
38
|
+
* 全局作用域(用户本机显式配置)始终连接;项目作用域(随 git clone 传播的
|
|
39
|
+
* 任意命令执行面)仅在项目已信任时连接——与插件加载、工作流发现同口径。
|
|
40
|
+
*/
|
|
41
|
+
declare function collectScopedMCPServers(scopes: {
|
|
42
|
+
global?: Partial<Config>;
|
|
43
|
+
project?: Partial<Config>;
|
|
44
|
+
}, projectTrusted: boolean): MCPServerConfig[];
|
|
45
|
+
export type { ConnectOptions, RegisterOptions, RegisterResult };
|
|
46
|
+
export { CONNECT_TIMEOUT_MS, callMCPTool, collectScopedMCPServers, connectMCPServer, disconnectMCPServer, discoverTools, registerMCPServers, };
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
// MCP 包入口(spec §6):连接外部 MCP 服务器(stdio / http / sse),发现其工具,
|
|
2
|
+
// 适配为内部 ToolDef 并注册进 ToolRegistry——对 agent 透明,与内置工具同接口。
|
|
3
|
+
//
|
|
4
|
+
// 连接失败(配置坏/服务器不可用)不抛给宿主:registerMCPServers 逐台隔离,
|
|
5
|
+
// 失败告警并跳过该台(与插件加载「单个坏插件不影响其余」同口径),
|
|
6
|
+
// 保证一个坏 MCP 配置不会拖垮 serve/chat 启动。
|
|
7
|
+
import { registerTool } from '../tools/registry.js';
|
|
8
|
+
import { createMCPClient } from './client.js';
|
|
9
|
+
import { adaptMCPTool, mcpResultToToolResult } from './tool-adapter.js';
|
|
10
|
+
import { buildTransport } from './transport.js';
|
|
11
|
+
/** initialize/tools/list 的默认超时。 */
|
|
12
|
+
const CONNECT_TIMEOUT_MS = 30_000;
|
|
13
|
+
/** 连接一个 MCP 服务器:initialize 握手 → tools/list → 返回会话。失败抛错。 */
|
|
14
|
+
async function connectMCPServer(config, opts = {}) {
|
|
15
|
+
const transport = opts.transport ?? (opts.createTransport ?? buildTransport)(config);
|
|
16
|
+
const client = createMCPClient(transport);
|
|
17
|
+
try {
|
|
18
|
+
const timeoutMs = opts.timeoutMs ?? CONNECT_TIMEOUT_MS;
|
|
19
|
+
await client.request('initialize', {
|
|
20
|
+
protocolVersion: '2025-03-26',
|
|
21
|
+
capabilities: {},
|
|
22
|
+
clientInfo: { name: 'c0de-agent', version: '0.0.0' },
|
|
23
|
+
}, timeoutMs);
|
|
24
|
+
// 握手后半步:声明客户端已就绪(notifications/initialized)。
|
|
25
|
+
client.notify('notifications/initialized');
|
|
26
|
+
const list = (await client.request('tools/list', undefined, timeoutMs));
|
|
27
|
+
const tools = Array.isArray(list?.tools) ? list.tools : [];
|
|
28
|
+
return {
|
|
29
|
+
name: config.name,
|
|
30
|
+
config,
|
|
31
|
+
tools,
|
|
32
|
+
request: (method, params, t) => client.request(method, params, t),
|
|
33
|
+
close: () => client.close(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
client.close();
|
|
38
|
+
throw error;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** 会话已发现工具 → 内部 ToolDef[](spec §6.2 discoverTools)。 */
|
|
42
|
+
function discoverTools(session) {
|
|
43
|
+
return session.tools.map((t) => adaptMCPTool(session, t));
|
|
44
|
+
}
|
|
45
|
+
/** 直接调用一个 MCP 工具(spec §6.2 callMCPTool)。 */
|
|
46
|
+
async function callMCPTool(session, name, args) {
|
|
47
|
+
try {
|
|
48
|
+
const result = await session.request('tools/call', { name, arguments: args ?? {} });
|
|
49
|
+
return mcpResultToToolResult(result);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
return { _tag: 'error', error: error instanceof Error ? error.message : String(error) };
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/** 断开连接(spec §6.2 disconnectMCPServer)。幂等。 */
|
|
56
|
+
function disconnectMCPServer(session) {
|
|
57
|
+
session.close();
|
|
58
|
+
}
|
|
59
|
+
/** 连接多台 MCP 服务器并把工具注册进 registry。逐台隔离,失败不抛。 */
|
|
60
|
+
async function registerMCPServers(registry, servers, opts = {}) {
|
|
61
|
+
const sessions = [];
|
|
62
|
+
const failures = [];
|
|
63
|
+
for (const server of servers) {
|
|
64
|
+
try {
|
|
65
|
+
const session = await connectMCPServer(server, opts);
|
|
66
|
+
sessions.push(session);
|
|
67
|
+
for (const def of discoverTools(session)) {
|
|
68
|
+
registerTool(registry, def);
|
|
69
|
+
}
|
|
70
|
+
if (session.tools.length > 0) {
|
|
71
|
+
console.info(`[mcp] 已连接 ${server.name}(${server.transport}),注册 ${session.tools.length} 个工具`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
76
|
+
failures.push(`${server.name}: ${reason}`);
|
|
77
|
+
console.warn(`[mcp] 连接 ${server.name} 失败,跳过该服务器:${reason}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return { sessions, failures };
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* 从配置作用域收集「当前应连接」的 MCP 服务器:
|
|
84
|
+
* 全局作用域(用户本机显式配置)始终连接;项目作用域(随 git clone 传播的
|
|
85
|
+
* 任意命令执行面)仅在项目已信任时连接——与插件加载、工作流发现同口径。
|
|
86
|
+
*/
|
|
87
|
+
function collectScopedMCPServers(scopes, projectTrusted) {
|
|
88
|
+
const out = [];
|
|
89
|
+
const add = (list) => {
|
|
90
|
+
if (!Array.isArray(list))
|
|
91
|
+
return;
|
|
92
|
+
for (const entry of list) {
|
|
93
|
+
if (entry === null || typeof entry !== 'object')
|
|
94
|
+
continue;
|
|
95
|
+
const e = entry;
|
|
96
|
+
if (typeof e.name !== 'string' || typeof e.transport !== 'string')
|
|
97
|
+
continue;
|
|
98
|
+
out.push(e);
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
add(scopes.global?.mcpServers);
|
|
102
|
+
if (projectTrusted)
|
|
103
|
+
add(scopes.project?.mcpServers);
|
|
104
|
+
return out;
|
|
105
|
+
}
|
|
106
|
+
export { CONNECT_TIMEOUT_MS, callMCPTool, collectScopedMCPServers, connectMCPServer, disconnectMCPServer, discoverTools, registerMCPServers, };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ToolDef, ToolResult } from '../shared/types/tool.js';
|
|
2
|
+
import type { MCPSession, MCPTool } from './types.js';
|
|
3
|
+
/** 工具调用超时:外部服务器执行可慢,给足余量。 */
|
|
4
|
+
declare const TOOL_CALL_TIMEOUT_MS = 120000;
|
|
5
|
+
/** MCP 工具名 → provider 函数名:`mcp__<server>__<tool>`,两侧片段各自规范化并按
|
|
6
|
+
* 64 字符上限分配预算。超长时优先保留工具名(截尾保尾:工具语义在尾部,同一
|
|
7
|
+
* 服务器下的不同工具因此仍互不相同),服务器名段相应截断。 */
|
|
8
|
+
declare function toolName(sessionName: string, name: string): string;
|
|
9
|
+
/** MCP tools/call 结果 → ToolResult:text 内容拼接;isError 标记映射为 error。 */
|
|
10
|
+
declare function mcpResultToToolResult(result: unknown): ToolResult;
|
|
11
|
+
/** 把一个 MCP 工具适配为内部 ToolDef(execute 闭包持有 session)。 */
|
|
12
|
+
declare function adaptMCPTool(session: MCPSession, tool: MCPTool): ToolDef;
|
|
13
|
+
export type { MCPSession, MCPTool };
|
|
14
|
+
export { adaptMCPTool, mcpResultToToolResult, TOOL_CALL_TIMEOUT_MS, toolName };
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
// MCP tool → 内部 ToolDef 适配(spec §6.2):MCP 工具的 inputSchema 直接映射为
|
|
2
|
+
// ToolDef.parameters;工具名命名空间化为 `mcp__<server>__<tool>` 防与内置/跨服务器
|
|
3
|
+
// 冲突;执行时经 session 发 tools/call 并把 MCP content 折回 ToolResult。
|
|
4
|
+
// 权限 ask:MCP 服务器是外部任意代码执行面(与插件同级),默认需确认。
|
|
5
|
+
/** 工具调用超时:外部服务器执行可慢,给足余量。 */
|
|
6
|
+
const TOOL_CALL_TIMEOUT_MS = 120_000;
|
|
7
|
+
/** provider 的函数名文法(OpenAI 与 Anthropic 一致):`^[a-zA-Z0-9_-]{1,64}$`。
|
|
8
|
+
* 服务器名来自用户配置、工具名来自远端服务——两者都可能带 `.`/`@`/`/`/空格
|
|
9
|
+
* (例如照抄 npm 包名 `@modelcontextprotocol/server-filesystem`)或超长。未规范化
|
|
10
|
+
* 就拼进该槽位时,请求体 tools[].function.name 非法,provider 400 拒绝**整个请求**:
|
|
11
|
+
* 不是这台服务器的工具不可用,而是该工作区所有对话都发不出去。 */
|
|
12
|
+
const TOOL_NAME_MAX = 64;
|
|
13
|
+
const TOOL_NAME_SEP = '__';
|
|
14
|
+
const TOOL_NAME_PREFIX = 'mcp';
|
|
15
|
+
/** 规范化单个名字片段:非法字符折成 `_`,去掉首尾 `_`。 */
|
|
16
|
+
function sanitizeSegment(value) {
|
|
17
|
+
return value.replace(/[^a-zA-Z0-9_-]+/g, '_').replace(/^_+|_+$/g, '');
|
|
18
|
+
}
|
|
19
|
+
/** MCP 工具名 → provider 函数名:`mcp__<server>__<tool>`,两侧片段各自规范化并按
|
|
20
|
+
* 64 字符上限分配预算。超长时优先保留工具名(截尾保尾:工具语义在尾部,同一
|
|
21
|
+
* 服务器下的不同工具因此仍互不相同),服务器名段相应截断。 */
|
|
22
|
+
function toolName(sessionName, name) {
|
|
23
|
+
const server = sanitizeSegment(sessionName) || 'server';
|
|
24
|
+
const tool = sanitizeSegment(name) || 'tool';
|
|
25
|
+
const budget = TOOL_NAME_MAX - TOOL_NAME_PREFIX.length - TOOL_NAME_SEP.length * 2;
|
|
26
|
+
const keepTool = tool.length < budget ? tool : tool.slice(-(budget - 1));
|
|
27
|
+
const keepServer = server.slice(0, Math.max(1, budget - keepTool.length));
|
|
28
|
+
return `${TOOL_NAME_PREFIX}${TOOL_NAME_SEP}${keepServer}${TOOL_NAME_SEP}${keepTool}`;
|
|
29
|
+
}
|
|
30
|
+
/** MCP tools/call 结果 → ToolResult:text 内容拼接;isError 标记映射为 error。 */
|
|
31
|
+
function mcpResultToToolResult(result) {
|
|
32
|
+
const r = result;
|
|
33
|
+
const parts = Array.isArray(r?.content) ? r.content : [];
|
|
34
|
+
const text = parts
|
|
35
|
+
.filter((c) => c !== null && typeof c === 'object' && c.type === 'text')
|
|
36
|
+
.map((c) => (typeof c.text === 'string' ? c.text : JSON.stringify(c.text)))
|
|
37
|
+
.join('\n');
|
|
38
|
+
if (r?.isError) {
|
|
39
|
+
return { _tag: 'error', error: text || 'MCP tool reported an error' };
|
|
40
|
+
}
|
|
41
|
+
if (text.length === 0) {
|
|
42
|
+
const nonText = parts.length > 0 ? `(${parts.length} 个非文本 content 项,不支持渲染)` : '';
|
|
43
|
+
return { _tag: 'success', output: `(empty result)${nonText}` };
|
|
44
|
+
}
|
|
45
|
+
return { _tag: 'success', output: text };
|
|
46
|
+
}
|
|
47
|
+
/** 把一个 MCP 工具适配为内部 ToolDef(execute 闭包持有 session)。 */
|
|
48
|
+
function adaptMCPTool(session, tool) {
|
|
49
|
+
return {
|
|
50
|
+
name: toolName(session.name, tool.name),
|
|
51
|
+
description: tool.description?.trim() || `MCP tool \`${tool.name}\` from server \`${session.name}\`.`,
|
|
52
|
+
parameters: tool.inputSchema,
|
|
53
|
+
permission: 'ask',
|
|
54
|
+
execute: async (input) => {
|
|
55
|
+
try {
|
|
56
|
+
const result = await session.request('tools/call', { name: tool.name, arguments: input ?? {} }, TOOL_CALL_TIMEOUT_MS);
|
|
57
|
+
return mcpResultToToolResult(result);
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
return { _tag: 'error', error: error instanceof Error ? error.message : String(error) };
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
export { adaptMCPTool, mcpResultToToolResult, TOOL_CALL_TIMEOUT_MS, toolName };
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { type ChildProcessWithoutNullStreams } from 'node:child_process';
|
|
2
|
+
import type { JSONRPCNotification, JSONRPCRequest, MCPIncoming } from './types.js';
|
|
3
|
+
/** 底层传输抽象:发 JSON-RPC 消息、收服务端消息、关连接。 */
|
|
4
|
+
type MCPTransport = {
|
|
5
|
+
send: (msg: JSONRPCRequest | JSONRPCNotification) => void;
|
|
6
|
+
onMessage: (handler: (msg: MCPIncoming) => void) => void;
|
|
7
|
+
onClose: (handler: () => void) => void;
|
|
8
|
+
close: () => void;
|
|
9
|
+
};
|
|
10
|
+
/** 可注入的 spawn:返回带 stdin/stdout/stderr 流的子进程(测试注入假进程)。 */
|
|
11
|
+
type SpawnFn = (command: string, args: string[]) => ChildProcessWithoutNullStreams;
|
|
12
|
+
/** 可注入的 fetch(测试注入假 Response)。 */
|
|
13
|
+
type FetchFn = (input: string, init: RequestInit) => Promise<Response>;
|
|
14
|
+
declare function createStdioTransport(command: string, args?: string[], spawnFn?: SpawnFn): MCPTransport;
|
|
15
|
+
/** 从 ReadableStream 增量解析 SSE(`data:` 行 + 空行分事件),收到即回调。 */
|
|
16
|
+
declare function parseSSEStream(body: ReadableStream<Uint8Array>, onEvent: (data: string) => void, shouldStop: () => boolean): Promise<void>;
|
|
17
|
+
/**
|
|
18
|
+
* Streamable HTTP 传输:每条消息一次 POST,响应按 Content-Type 解析
|
|
19
|
+
* (application/json 直接读 body;text/event-stream 流式解析)。
|
|
20
|
+
* `sse` 为 true 时请求头声明 Accept: text/event-stream(spec §6.2 的 'sse' 传输)。
|
|
21
|
+
* 收到匹配当前请求的 response 后主动取消读取并断连,避免挂住长连接。
|
|
22
|
+
*/
|
|
23
|
+
declare function createHttpTransport(url: string, opts?: {
|
|
24
|
+
sse?: boolean;
|
|
25
|
+
fetchFn?: FetchFn;
|
|
26
|
+
}): MCPTransport;
|
|
27
|
+
/** 按配置构建生产传输。校验失败抛错(配置面错误,连接前暴露)。 */
|
|
28
|
+
declare function buildTransport(config: {
|
|
29
|
+
transport: 'stdio' | 'sse' | 'http';
|
|
30
|
+
command?: string;
|
|
31
|
+
args?: string[];
|
|
32
|
+
url?: string;
|
|
33
|
+
}): MCPTransport;
|
|
34
|
+
export type { FetchFn, MCPTransport, SpawnFn };
|
|
35
|
+
export { buildTransport, createHttpTransport, createStdioTransport, parseSSEStream };
|
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
// MCP 传输层(spec §6.1):stdio(子进程 + 换行分帧 JSON)与 streamable HTTP
|
|
2
|
+
// (POST JSON / SSE 流式响应)两种传输。零外部依赖;IO 边界(spawn/fetch)可注入,
|
|
3
|
+
// 协议分帧与客户端逻辑纯逻辑可测。
|
|
4
|
+
import { spawn } from 'node:child_process';
|
|
5
|
+
/** 错误响应构造(-32000 = server error,JSON-RPC 惯例)。 */
|
|
6
|
+
function errorResponse(msg, message) {
|
|
7
|
+
return {
|
|
8
|
+
jsonrpc: '2.0',
|
|
9
|
+
id: 'id' in msg ? msg.id : 0,
|
|
10
|
+
error: { code: -32000, message },
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* stdio 传输:spawn 子进程,stdout 按行解析 JSON(MCP stdio 规范:
|
|
15
|
+
* 每行一条 JSON-RPC 消息,无 Content-Length 分帧)。跨 chunk / 粘包已处理。
|
|
16
|
+
*/
|
|
17
|
+
/** 单条 stdio 消息行上限(字符):服务器持续输出不换行或单条消息超大时,
|
|
18
|
+
* `buffer += chunk` 会无限增长直至 OOM——超限整行丢弃并告警,防缓冲无界。 */
|
|
19
|
+
const MAX_LINE_CHARS = 32 * 1024 * 1024;
|
|
20
|
+
function createStdioTransport(command, args = [], spawnFn = spawn) {
|
|
21
|
+
const child = spawnFn(command, args);
|
|
22
|
+
// 子进程不参与父进程事件循环存活:父退出 → stdin 管道关闭 → 合规 MCP
|
|
23
|
+
// 服务器读到 EOF 自行退出。否则长驻的 MCP 服务器会让 CLI 进程无法结束。
|
|
24
|
+
child.unref();
|
|
25
|
+
const handlers = [];
|
|
26
|
+
const closeHandlers = [];
|
|
27
|
+
let buffer = '';
|
|
28
|
+
child.stdout.setEncoding('utf8');
|
|
29
|
+
child.stdout.on('data', (chunk) => {
|
|
30
|
+
buffer += chunk;
|
|
31
|
+
// 超长行防护:无换行的持续输出或单条超限消息立即丢弃,防缓冲无界增长。
|
|
32
|
+
// 丢弃不改变「完整合法行仍可解析」的语义——超限行本身本就无法作为
|
|
33
|
+
// 合法 JSON-RPC 消息使用。
|
|
34
|
+
const nl = buffer.indexOf('\n');
|
|
35
|
+
if (buffer.length > MAX_LINE_CHARS && (nl === -1 || nl > MAX_LINE_CHARS)) {
|
|
36
|
+
const drop = nl === -1 ? buffer.length : nl + 1;
|
|
37
|
+
console.warn(`[mcp:stdio] 丢弃超长输出 ${drop} 字符(单行超过 ${MAX_LINE_CHARS} 上限)`);
|
|
38
|
+
buffer = buffer.slice(drop);
|
|
39
|
+
}
|
|
40
|
+
const consume = () => {
|
|
41
|
+
const idx = buffer.indexOf('\n');
|
|
42
|
+
if (idx < 0)
|
|
43
|
+
return;
|
|
44
|
+
const line = buffer.slice(0, idx).trim();
|
|
45
|
+
buffer = buffer.slice(idx + 1);
|
|
46
|
+
if (line.length > 0) {
|
|
47
|
+
let msg;
|
|
48
|
+
try {
|
|
49
|
+
msg = JSON.parse(line);
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
consume(); // 非 JSON 行(日志等)忽略,继续解析后续行
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
for (const h of handlers)
|
|
56
|
+
h(msg);
|
|
57
|
+
}
|
|
58
|
+
consume();
|
|
59
|
+
};
|
|
60
|
+
consume();
|
|
61
|
+
});
|
|
62
|
+
// 不读取 stderr 会让管道写满卡死子进程——消费并透传到父进程 stderr。
|
|
63
|
+
child.stderr.setEncoding('utf8');
|
|
64
|
+
child.stderr.on('data', (chunk) => {
|
|
65
|
+
process.stderr.write(`[mcp:stdio] ${chunk}`);
|
|
66
|
+
});
|
|
67
|
+
// spawn 失败(ENOENT 等):无监听器时 unhandled 'error' 会击穿父进程。
|
|
68
|
+
// 仅记录——随后的 'close' 事件走既有关闭路径 reject pending。
|
|
69
|
+
child.on('error', (err) => {
|
|
70
|
+
console.warn(`[mcp] stdio 子进程错误:${err.message}`);
|
|
71
|
+
});
|
|
72
|
+
// 服务器崩溃/提前 close(0) 时 stdin 管道写入会异步触发 EPIPE 'error',
|
|
73
|
+
// 该事件发在 stdin socket 上而非 child 上——无监听器同样击穿宿主进程。
|
|
74
|
+
child.stdin.on('error', (err) => {
|
|
75
|
+
console.warn(`[mcp] stdio 写入失败(服务器可能已退出):${err.message}`);
|
|
76
|
+
});
|
|
77
|
+
child.on('close', () => {
|
|
78
|
+
for (const h of closeHandlers)
|
|
79
|
+
h();
|
|
80
|
+
});
|
|
81
|
+
return {
|
|
82
|
+
send(msg) {
|
|
83
|
+
if (child.stdin.destroyed)
|
|
84
|
+
return;
|
|
85
|
+
child.stdin.write(`${JSON.stringify(msg)}\n`);
|
|
86
|
+
},
|
|
87
|
+
onMessage(handler) {
|
|
88
|
+
handlers.push(handler);
|
|
89
|
+
},
|
|
90
|
+
onClose(handler) {
|
|
91
|
+
closeHandlers.push(handler);
|
|
92
|
+
},
|
|
93
|
+
close() {
|
|
94
|
+
if (!child.killed)
|
|
95
|
+
child.kill();
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
/** 从 ReadableStream 增量解析 SSE(`data:` 行 + 空行分事件),收到即回调。 */
|
|
100
|
+
async function parseSSEStream(body, onEvent, shouldStop) {
|
|
101
|
+
const reader = body.getReader();
|
|
102
|
+
const decoder = new TextDecoder();
|
|
103
|
+
let buffer = '';
|
|
104
|
+
try {
|
|
105
|
+
const consume = () => {
|
|
106
|
+
const idx = buffer.indexOf('\n\n');
|
|
107
|
+
if (idx < 0)
|
|
108
|
+
return;
|
|
109
|
+
const eventBlock = buffer.slice(0, idx);
|
|
110
|
+
buffer = buffer.slice(idx + 2);
|
|
111
|
+
for (const line of eventBlock.split('\n')) {
|
|
112
|
+
if (!line.startsWith('data:'))
|
|
113
|
+
continue;
|
|
114
|
+
const data = line.slice(5).trimStart();
|
|
115
|
+
if (data.length > 0)
|
|
116
|
+
onEvent(data);
|
|
117
|
+
}
|
|
118
|
+
consume();
|
|
119
|
+
};
|
|
120
|
+
for (;;) {
|
|
121
|
+
const { done, value } = await reader.read();
|
|
122
|
+
if (done) {
|
|
123
|
+
// 流正常关闭:flush decoder 内部缓冲 + 残留尾部事件。最后一个 data: 行
|
|
124
|
+
// 无尾随空行(服务器发完直接关流)时事件此前被静默丢弃——MCP 客户端
|
|
125
|
+
// 等最后一条响应直到超时。空行分帧 consume 无法覆盖该帧,须显式收尾。
|
|
126
|
+
buffer += decoder.decode();
|
|
127
|
+
buffer = buffer.replace(/\r\n/g, '\n');
|
|
128
|
+
if (buffer.trim().length > 0) {
|
|
129
|
+
for (const line of buffer.trim().split('\n')) {
|
|
130
|
+
if (!line.startsWith('data:'))
|
|
131
|
+
continue;
|
|
132
|
+
const data = line.slice(5).trimStart();
|
|
133
|
+
if (data.length > 0)
|
|
134
|
+
onEvent(data);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
buffer += decoder.decode(value, { stream: true });
|
|
140
|
+
// SSE 规范允许 CRLF 行结束——归一化为 LF 再按空行分事件,
|
|
141
|
+
// 否则 \r\n\r\n 分隔的事件滞留缓冲区永不派发。
|
|
142
|
+
buffer = buffer.replace(/\r\n/g, '\n');
|
|
143
|
+
consume();
|
|
144
|
+
if (shouldStop()) {
|
|
145
|
+
await reader.cancel();
|
|
146
|
+
break;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// 流被取消/连接被关闭:调用方已不需要继续读取
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Streamable HTTP 传输:每条消息一次 POST,响应按 Content-Type 解析
|
|
156
|
+
* (application/json 直接读 body;text/event-stream 流式解析)。
|
|
157
|
+
* `sse` 为 true 时请求头声明 Accept: text/event-stream(spec §6.2 的 'sse' 传输)。
|
|
158
|
+
* 收到匹配当前请求的 response 后主动取消读取并断连,避免挂住长连接。
|
|
159
|
+
*/
|
|
160
|
+
function createHttpTransport(url, opts = {}) {
|
|
161
|
+
const fetchFn = opts.fetchFn ?? fetch;
|
|
162
|
+
const handlers = [];
|
|
163
|
+
const closeHandlers = [];
|
|
164
|
+
const controller = new AbortController();
|
|
165
|
+
let closed = false;
|
|
166
|
+
const emit = (msg) => {
|
|
167
|
+
for (const h of handlers)
|
|
168
|
+
h(msg);
|
|
169
|
+
};
|
|
170
|
+
const send = (msg) => {
|
|
171
|
+
void (async () => {
|
|
172
|
+
try {
|
|
173
|
+
const res = await fetchFn(url, {
|
|
174
|
+
method: 'POST',
|
|
175
|
+
headers: {
|
|
176
|
+
'content-type': 'application/json',
|
|
177
|
+
accept: opts.sse ? 'text/event-stream' : 'application/json, text/event-stream',
|
|
178
|
+
},
|
|
179
|
+
body: JSON.stringify(msg),
|
|
180
|
+
signal: controller.signal,
|
|
181
|
+
});
|
|
182
|
+
if (!res.ok) {
|
|
183
|
+
emit(errorResponse(msg, `MCP HTTP ${res.status}: ${res.statusText}`));
|
|
184
|
+
return;
|
|
185
|
+
}
|
|
186
|
+
const contentType = res.headers.get('content-type') ?? '';
|
|
187
|
+
if (contentType.includes('text/event-stream') && res.body) {
|
|
188
|
+
let receivedResponse = false;
|
|
189
|
+
await parseSSEStream(res.body, (data) => {
|
|
190
|
+
let parsed;
|
|
191
|
+
try {
|
|
192
|
+
parsed = JSON.parse(data);
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
if ('id' in parsed && 'id' in msg && parsed.id === msg.id)
|
|
198
|
+
receivedResponse = true;
|
|
199
|
+
emit(parsed);
|
|
200
|
+
}, () => receivedResponse);
|
|
201
|
+
}
|
|
202
|
+
else {
|
|
203
|
+
const parsed = (await res.json());
|
|
204
|
+
emit(parsed);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
catch (err) {
|
|
208
|
+
if (closed)
|
|
209
|
+
return; // close() 触发的 abort,静默
|
|
210
|
+
emit(errorResponse(msg, err instanceof Error ? err.message : String(err)));
|
|
211
|
+
}
|
|
212
|
+
})();
|
|
213
|
+
};
|
|
214
|
+
return {
|
|
215
|
+
send,
|
|
216
|
+
onMessage(handler) {
|
|
217
|
+
handlers.push(handler);
|
|
218
|
+
},
|
|
219
|
+
onClose(handler) {
|
|
220
|
+
closeHandlers.push(handler);
|
|
221
|
+
},
|
|
222
|
+
close() {
|
|
223
|
+
if (closed)
|
|
224
|
+
return;
|
|
225
|
+
closed = true;
|
|
226
|
+
controller.abort();
|
|
227
|
+
for (const h of closeHandlers)
|
|
228
|
+
h();
|
|
229
|
+
},
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
/** 按配置构建生产传输。校验失败抛错(配置面错误,连接前暴露)。 */
|
|
233
|
+
function buildTransport(config) {
|
|
234
|
+
if (config.transport === 'stdio') {
|
|
235
|
+
if (!config.command)
|
|
236
|
+
throw new Error('stdio transport requires "command"');
|
|
237
|
+
return createStdioTransport(config.command, config.args ?? []);
|
|
238
|
+
}
|
|
239
|
+
if (!config.url)
|
|
240
|
+
throw new Error(`${config.transport} transport requires "url"`);
|
|
241
|
+
return createHttpTransport(config.url, { sse: config.transport === 'sse' });
|
|
242
|
+
}
|
|
243
|
+
export { buildTransport, createHttpTransport, createStdioTransport, parseSSEStream };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { JSONSchema } from '../shared/types/base.js';
|
|
2
|
+
import type { MCPServerConfig } from '../shared/types/config.js';
|
|
3
|
+
/** JSON-RPC 2.0 request(client → server,带 id,期待 response)。 */
|
|
4
|
+
type JSONRPCRequest = {
|
|
5
|
+
jsonrpc: '2.0';
|
|
6
|
+
id: number;
|
|
7
|
+
method: string;
|
|
8
|
+
params?: unknown;
|
|
9
|
+
};
|
|
10
|
+
/** JSON-RPC 2.0 notification(client → server,无 id,不期待响应)。 */
|
|
11
|
+
type JSONRPCNotification = {
|
|
12
|
+
jsonrpc: '2.0';
|
|
13
|
+
method: string;
|
|
14
|
+
params?: unknown;
|
|
15
|
+
};
|
|
16
|
+
/** JSON-RPC 2.0 response(server → client)。 */
|
|
17
|
+
type JSONRPCResponse = {
|
|
18
|
+
jsonrpc: '2.0';
|
|
19
|
+
id: number;
|
|
20
|
+
result?: unknown;
|
|
21
|
+
error?: {
|
|
22
|
+
code: number;
|
|
23
|
+
message: string;
|
|
24
|
+
data?: unknown;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/** 服务端发来的消息:响应或通知。 */
|
|
28
|
+
type MCPIncoming = JSONRPCResponse | JSONRPCNotification;
|
|
29
|
+
/** MCP tools/list 返回的单个工具描述(inputSchema 是 JSON Schema)。 */
|
|
30
|
+
type MCPTool = {
|
|
31
|
+
name: string;
|
|
32
|
+
description?: string;
|
|
33
|
+
inputSchema: JSONSchema;
|
|
34
|
+
};
|
|
35
|
+
/** MCP tools/call 返回结果:content 数组(text 为主),isError 标记工具语义失败。 */
|
|
36
|
+
type MCPCallResult = {
|
|
37
|
+
content: Array<{
|
|
38
|
+
type: string;
|
|
39
|
+
[key: string]: unknown;
|
|
40
|
+
}>;
|
|
41
|
+
isError?: boolean;
|
|
42
|
+
};
|
|
43
|
+
/** 已连接的 MCP 会话:持有连接与已发现工具,供 tool-adapter 消费。 */
|
|
44
|
+
type MCPSession = {
|
|
45
|
+
/** 配置中的服务器名(工具命名空间来源)。 */
|
|
46
|
+
name: string;
|
|
47
|
+
config: MCPServerConfig;
|
|
48
|
+
/** tools/list 快照(连接时发现,会话内不变)。 */
|
|
49
|
+
tools: MCPTool[];
|
|
50
|
+
/** 发送 JSON-RPC request(带默认超时,可覆盖)。 */
|
|
51
|
+
request: (method: string, params?: unknown, timeoutMs?: number) => Promise<unknown>;
|
|
52
|
+
/** 关闭底层连接。幂等。 */
|
|
53
|
+
close: () => void;
|
|
54
|
+
};
|
|
55
|
+
export type { JSONRPCNotification, JSONRPCRequest, JSONRPCResponse, MCPCallResult, MCPIncoming, MCPSession, MCPTool, };
|
package/dist/plugins/init.d.ts
CHANGED
|
@@ -16,6 +16,8 @@ type InitPluginsOptions = {
|
|
|
16
16
|
type InitPluginsResult = {
|
|
17
17
|
pluginRegistry: PluginRegistry;
|
|
18
18
|
hookRunner: HookRunner;
|
|
19
|
+
/** config:resolve hook 链之后的最终配置(无 handler 时为传入的 config 原引用)。 */
|
|
20
|
+
config: Config;
|
|
19
21
|
};
|
|
20
22
|
/**
|
|
21
23
|
* 初始化插件系统:注册表 + hook runner + 内置插件 + 外部插件发现。
|
package/dist/plugins/init.js
CHANGED
|
@@ -46,6 +46,15 @@ async function initPlugins(opts) {
|
|
|
46
46
|
registerPlugin(pluginRegistry, plugin);
|
|
47
47
|
await activatePlugin(pluginRegistry, plugin, services);
|
|
48
48
|
}
|
|
49
|
-
|
|
49
|
+
// config:resolve hook(HookMap 承诺的挂载点):插件激活后对最终合并配置做链式
|
|
50
|
+
// 改写(增补默认值/注入运行时密钥等)。handler 返回 false 视为「保持原配置」。
|
|
51
|
+
// 结果随 InitPluginsResult.config 返回,宿主用于后续运行时消费(权限/预算/auth/
|
|
52
|
+
// 调度器等)。注册表(llm/tool)在 resolve 之前构建、不重建——provider 运行时
|
|
53
|
+
// 变更本就需要重启,与 Web PATCH「providers 改动提示重启」的既有语义一致。
|
|
54
|
+
let resolvedConfig = opts.config;
|
|
55
|
+
const hookResult = await hookRunner.runHooks('config:resolve', { config: opts.config });
|
|
56
|
+
if (hookResult !== false)
|
|
57
|
+
resolvedConfig = hookResult.config;
|
|
58
|
+
return { pluginRegistry, hookRunner, config: resolvedConfig };
|
|
50
59
|
}
|
|
51
60
|
export { initPlugins };
|