# Q18-Q19: Provider 与 MCP --- ## Q18: Claude Agent SDK、OpenCode、Ollama 三个 provider 怎么抽象成统一接口?切换 provider 改什么? ### 答案 ### 统一接口:`AgentProvider` 定义在 `container/agent-runner/src/providers/types.ts:1-92`: ```typescript 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` 流式输出,`abort()` 强制停止 - `ProviderEvent`(line 82-92):判别联合类型 `init | result | error | progress | activity` - `ProviderOptions`(line 23-38):`assistantName`、`mcpServers`、`env`、`additionalDirectories`、`model`、`effort` ### 当前 Providers **ClaudeProvider**(`claude.ts:253-360`): - 包装 `@anthropic-ai/claude-agent-sdk` 的 `query()` 函数 - 用自定义 `MessageStream` async 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 行): ```typescript export function createProvider(name: string, options: ProviderOptions = {}): AgentProvider { return getProviderFactory(name)(options); } ``` 这是对自注册 registry(`provider-registry.ts`)的薄分发。Registry 是 `Map`,每个 provider 模块在模块作用域调用 `registerProvider(name, factory)`。 选择逻辑: 1. 调用者传入 provider name 字符串(来自 `container_configs.provider` 列) 2. `getProviderFactory(name)` 查 Map——未找到抛错:`"Unknown provider: ${name}"` 3. 调用 factory 函数并传入 options ### 注册流程 1. 每个 provider 文件在顶层调用 `registerProvider()`(如 `claude.ts:360`) 2. Barrel `container/agent-runner/src/providers/index.ts` 导入所有 provider 模块做副作用: ```typescript import './claude.js'; import './mock.js'; ``` 3. 运行时 agent-runner poll loop 从 `container_configs` 解析 provider name → `createProvider(name, options)` → 获取 `AgentProvider` → `provider.query(input)` ### 切换 Provider 修改 agent group 的 `container_configs.provider` 列: ```bash ncl groups config update --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`): 1. 创建 MCP `Server` 实例,name=`'nanoclaw'`,version=`'2.0.0'` 2. 注册两个请求 handler: - `ListToolsRequestSchema` → 返回 `allTools.map(t => t.tool)`(line 38-40) - `CallToolRequestSchema` → 查 `toolMap.get(name)`,调 `tool.handler(args)`(line 42-48) 3. 创建 `StdioServerTransport` 并连接(line 51-52)——Claude SDK 通过 stdio 发现 MCP server 4. 记录所有注册的 tool 名称 ### Tool 自注册模式 每个 tool 模块在模块作用域调用 `registerTools([...])`。`registerTools()`(`server.ts:24-33`):将每个 `McpToolDefinition` 推入两个结构:`allTools[]`(ListTools 用)和 `toolMap`(name→definition,CallTool 用)。重复的名称警告但不报错。 `McpToolDefinition` 类型(`types.ts:1-6`): ```typescript interface McpToolDefinition { tool: Tool; // MCP SDK Tool schema (name, description, inputSchema) handler: (args) => Promise; } ``` ### 内置 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 字符串,默认 `'{}'`): ```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: ```typescript 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 交互 交互是间接的: 1. Provider 启动 → 传 `mcpServers` config 给 SDK → SDK spawn 外部 MCP 进程并发现它们的 tool schema 2. **NanoClaw MCP server**(stdio)在 provider 之前由 `index.ts` 启动——SDK 连接它并发现内置 tool schema 3. Agent(Claude 模型)决定调用 tool → SDK 路由到内置或外部 MCP server → 结果返回模型 4. 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)在调用时拦截它——深度防御。