171 lines
8.4 KiB
Markdown
171 lines
8.4 KiB
Markdown
# 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<ProviderEvent>` 流式输出,`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<string, ProviderFactory>`,每个 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 <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`):
|
||
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<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 字符串,默认 `'{}'`):
|
||
```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)在调用时拦截它——深度防御。
|