8.4 KiB
Q18-Q19: Provider 与 MCP
Q18: Claude Agent SDK、OpenCode、Ollama 三个 provider 怎么抽象成统一接口?切换 provider 改什么?
答案
统一接口:AgentProvider
定义在 container/agent-runner/src/providers/types.ts:1-92:
interface AgentProvider {
readonly supportsNativeSlashCommands: boolean; // SDK 是否原生处理 /commands?
query(input: QueryInput): AgentQuery; // 开始一轮对话
isSessionInvalid(err: unknown): boolean; // 检测过期的延续语境错误
}
QueryInput(line 40-60):携带prompt、continuation(不透明 session token)、cwd、systemContext。Provider 自己决定 "continuation" 的含义AgentQuery(line 68-80):流式句柄,push(message)推送后续输入,events: AsyncIterable<ProviderEvent>流式输出,abort()强制停止ProviderEvent(line 82-92):判别联合类型init | result | error | progress | activityProviderOptions(line 23-38):assistantName、mcpServers、env、additionalDirectories、model、effort
当前 Providers
ClaudeProvider(claude.ts:253-360):
- 包装
@anthropic-ai/claude-agent-sdk的query()函数 - 用自定义
MessageStreamasync generator(line 80-112)把后续消息推入 SDK 流式输入 - 把原始 SDK 事件翻译成
ProviderEvent联合类型(translateEvents(),line 318-346) - Hooks:PreToolUse(记录 in-flight tool + 屏蔽不允许的 SDK builtins)、PostToolUse(清除 in-flight)、PreCompact(转录归档)
- 在 line 360 注册:
registerProvider('claude', (opts) => new ClaudeProvider(opts))
MockProvider(mock.ts:8-76):
- 测试用——从
responseFactory返回预设回复
OpenCode: 从 providers 分支通过 /add-opencode skill 安装,实现 AgentProvider 接口
Ollama: 尚未实现。会实现 AgentProvider 直接调用 Ollama API
factory.ts 的选择逻辑
container/agent-runner/src/providers/factory.ts(全部 13 行):
export function createProvider(name: string, options: ProviderOptions = {}): AgentProvider {
return getProviderFactory(name)(options);
}
这是对自注册 registry(provider-registry.ts)的薄分发。Registry 是 Map<string, ProviderFactory>,每个 provider 模块在模块作用域调用 registerProvider(name, factory)。
选择逻辑:
- 调用者传入 provider name 字符串(来自
container_configs.provider列) getProviderFactory(name)查 Map——未找到抛错:"Unknown provider: ${name}"- 调用 factory 函数并传入 options
注册流程
- 每个 provider 文件在顶层调用
registerProvider()(如claude.ts:360) - Barrel
container/agent-runner/src/providers/index.ts导入所有 provider 模块做副作用:import './claude.js'; import './mock.js'; - 运行时 agent-runner poll loop 从
container_configs解析 provider name →createProvider(name, options)→ 获取AgentProvider→provider.query(input)
切换 Provider
修改 agent group 的 container_configs.provider 列:
ncl groups config update --id <group-id> --provider opencode
写入中央库 → 下次容器 spawn 物化到 container.json → agent-runner 调用 createProvider('opencode', ...)
Host 端 Provider 注册
对于需要 host 端 setup 的 provider(额外 mounts、env 传递),有独立的 registry:src/providers/provider-container-registry.ts:43-54。Provider 注册 ProviderContainerConfigFn,container-runner 在 spawn 时调用它来合并额外的 mounts/env。目前仅 claude 内建——用默认容器,无需额外注册。非默认 provider(如 OpenCode)会在这里注册。
Q19: 容器里的 MCP server 怎么启动?内置工具和外部 MCP server 有什么不同?
答案
MCP Server Bootstrap
入口是 container/agent-runner/src/mcp-tools/index.ts:1-22:
1. 导入链:index.ts → 导入 core.ts, scheduling.ts, interactive.ts,
agents.ts, self-mod.ts 以便它们的副作用 registerTools([...]) 调用生效
2. 所有导入解决后,调用 startMcpServer()
3. 如果 server 崩溃,process.exit(1)
startMcpServer()(server.ts:35-54):
- 创建 MCP
Server实例,name='nanoclaw',version='2.0.0' - 注册两个请求 handler:
ListToolsRequestSchema→ 返回allTools.map(t => t.tool)(line 38-40)CallToolRequestSchema→ 查toolMap.get(name),调tool.handler(args)(line 42-48)
- 创建
StdioServerTransport并连接(line 51-52)——Claude SDK 通过 stdio 发现 MCP server - 记录所有注册的 tool 名称
Tool 自注册模式
每个 tool 模块在模块作用域调用 registerTools([...])。registerTools()(server.ts:24-33):将每个 McpToolDefinition 推入两个结构:allTools[](ListTools 用)和 toolMap(name→definition,CallTool 用)。重复的名称警告但不报错。
McpToolDefinition 类型(types.ts:1-6):
interface McpToolDefinition {
tool: Tool; // MCP SDK Tool schema (name, description, inputSchema)
handler: (args) => Promise<CallToolResult>;
}
内置 Tool vs 外部 MCP Server
内置(in-tree)工具 — 定义在 container/agent-runner/src/mcp-tools/:
| 模块 | 工具 | 机制 |
|---|---|---|
core.ts(line 95-263) |
send_message、send_file、edit_message、add_reaction |
写 outbound.db 的 messages_out 表;通过 local destinations map 解析目标 |
scheduling.ts |
schedule_task |
持久调度,process_after / recurrence 字段 |
interactive.ts |
ask_user_question |
写中央库 pending_questions,host poll 并发送卡片 |
agents.ts |
create_agent |
通过 system actions spawn 子 agent 容器 |
self-mod.ts |
install_packages、add_mcp_server |
写 pending_approvals 表;host 处理审批和容器重建 |
这些工具以 JavaScript 函数形式运行在容器进程内部。与 host 通信通过写 DB 表(messages_out、pending_approvals 等)——不是 IPC。
外部 MCP server — per-agent-group 配置在 container_configs.mcp_servers(JSON 字符串,默认 '{}'):
{
"server-name": {
"command": "npx",
"args": ["-y", "@some/mcp-server"],
"env": { "API_KEY": "..." }
}
}
Claude provider 把 this.mcpServers 直接传给 SDK 的 mcpServers 选项(claude.ts:306)。SDK 以子进程方式 spawn 它们,通过 stdio 自动发现它们的工具。Tool allowlist(claude.ts:66-68)派生 MCP patterns:
function mcpAllowPattern(serverName: string): string {
return `mcp__${serverName.replace(/[^a-zA-Z0-9_-]/g, '_')}__*`;
}
关键区别: 内置工具写 session DB;外部 MCP server 是由 SDK 管理的 stdio 子进程,通过 MCP 协议通信。内置工具能直接访问所有内部 DB 表和 destination/resolution 系统;外部 MCP 工具只能看到 provider SDK 通过 MCP 协议暴露的内容。
MCP Tools ↔ Provider 交互
交互是间接的:
- Provider 启动 → 传
mcpServersconfig 给 SDK → SDK spawn 外部 MCP 进程并发现它们的 tool schema - NanoClaw MCP server(stdio)在 provider 之前由
index.ts启动——SDK 连接它并发现内置 tool schema - Agent(Claude 模型)决定调用 tool → SDK 路由到内置或外部 MCP server → 结果返回模型
- Provider hooks(
PreToolUse、PostToolUse)对所有 tool 调用运行,不管来源(claude.ts:160-189)——用于为 host sweep 的卡住容忍度逻辑跟踪 tool-in-flight 状态
Provider 永远不会直接"调用"MCP tool。模型调用。Provider 只设置环境(MCP server configs、tool allowlists、hooks)。
不允许的 SDK Builtins
claude.ts:25-35 定义 SDK_DISALLOWED_TOOLS —— Claude Code SDK builtins 被屏蔽,因为 NanoClaw 有等价实现或它们不适合无头模型:
CronCreate/CronDelete/CronList/ScheduleWakeup→ 被mcp__nanoclaw__schedule_task替代AskUserQuestion→ 被mcp__nanoclaw__ask_user_question替代EnterPlanMode/ExitPlanMode/EnterWorktree/ExitWorktree→ Claude Code UI 功能;在无头容器中挂起
如果 disallowed tool 意外通过 allowlist 过滤器,preToolUseHook(line 160-169)在调用时拦截它——深度防御。