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

8.4 KiB
Raw Permalink Blame History

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;         // 检测过期的延续语境错误
}
  • QueryInputline 40-60携带 promptcontinuation(不透明 session tokencwdsystemContext。Provider 自己决定 "continuation" 的含义
  • AgentQueryline 68-80流式句柄push(message) 推送后续输入,events: AsyncIterable<ProviderEvent> 流式输出,abort() 强制停止
  • ProviderEventline 82-92判别联合类型 init | result | error | progress | activity
  • ProviderOptionsline 23-38assistantNamemcpServersenvadditionalDirectoriesmodeleffort

当前 Providers

ClaudeProviderclaude.ts:253-360

  • 包装 @anthropic-ai/claude-agent-sdkquery() 函数
  • 用自定义 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))

MockProvidermock.ts:8-76

  • 测试用——从 responseFactory 返回预设回复

OpenCodeproviders 分支通过 /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);
}

这是对自注册 registryprovider-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 模块做副作用:
    import './claude.js';
    import './mock.js';
    
  3. 运行时 agent-runner poll loop 从 container_configs 解析 provider name → createProvider(name, options) → 获取 AgentProviderprovider.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 传递),有独立的 registrysrc/providers/provider-container-registry.ts:43-54。Provider 注册 ProviderContainerConfigFncontainer-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 用)和 toolMapname→definitionCallTool 用)。重复的名称警告但不报错。

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.tsline 95-263 send_messagesend_fileedit_messageadd_reaction outbound.dbmessages_out 表;通过 local destinations map 解析目标
scheduling.ts schedule_task 持久调度,process_after / recurrence 字段
interactive.ts ask_user_question 写中央库 pending_questionshost poll 并发送卡片
agents.ts create_agent 通过 system actions spawn 子 agent 容器
self-mod.ts install_packagesadd_mcp_server pending_approvalshost 处理审批和容器重建

这些工具以 JavaScript 函数形式运行在容器进程内部。与 host 通信通过写 DB 表(messages_outpending_approvals 等)——不是 IPC。

外部 MCP server — per-agent-group 配置在 container_configs.mcp_serversJSON 字符串,默认 '{}'

{
  "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 allowlistclaude.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 交互

交互是间接的:

  1. Provider 启动 → 传 mcpServers config 给 SDK → SDK spawn 外部 MCP 进程并发现它们的 tool schema
  2. NanoClaw MCP serverstdio在 provider 之前由 index.ts 启动——SDK 连接它并发现内置 tool schema
  3. AgentClaude 模型)决定调用 tool → SDK 路由到内置或外部 MCP server → 结果返回模型
  4. Provider hooksPreToolUsePostToolUse)对所有 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 过滤器,preToolUseHookline 160-169在调用时拦截它——深度防御。