Files
nanoclaw/docs/answers/07-provider-and-mcp.md

171 lines
8.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 generatorline 80-112把后续消息推入 SDK 流式输入
- 把原始 SDK 事件翻译成 `ProviderEvent` 联合类型(`translateEvents()`line 318-346
- HooksPreToolUse记录 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→definitionCallTool 用)。重复的名称警告但不报错。
`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. AgentClaude 模型)决定调用 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在调用时拦截它——深度防御。