跳转到内容

12. Sources、MCP、API 与凭据

Source 是 Craft Agents 最具产品辨识度的抽象:用户不是在全局安装一堆工具,而是在 workspace 定义外部能力,再按 session 启用。MCP、REST API 与本地资源最终都变成统一工具名和 guide-driven 使用体验。

sources/types.ts 的主 discriminant:

type SourceType = 'mcp' | 'api' | 'local'
  • transport:stdio/http/sse;
  • command/args/env 或 URL/headers;
  • auth:none、bearer、OAuth 等;
  • 可选 OAuth client metadata。
  • base URL;
  • endpoint/tool 描述;
  • auth:none/bearer/header/query/basic/oauth;
  • provider-specific Google/Slack/Microsoft;
  • generic OAuth;
  • 非 OAuth renew endpoint;
  • custom header names/default headers。

描述本地目录/资源类型,受 workspace local MCP/文件权限约束。

stateDiagram-v2
[*] --> Configured: source config exists
Configured --> NeedsAuth: auth required, no valid credential
NeedsAuth --> Authenticated: OAuth / credential prompt
Configured --> Enabled: no auth or valid credential
Authenticated --> Enabled: session selects source
Enabled --> Connected: builder + pool sync
Connected --> Enabled: connection failure / token expiry
Connected --> Inactive: session deselects
Inactive --> Connected: runtime activation

isAuthenticated 是方便 UI/配置的状态,不应成为唯一真相;真正执行前仍要从 credential manager 获取有效 token。

完整链:

sources/<slug>/config.json
→ storage + schema validation
→ LoadedSource
→ SourceCredentialManager 获取/刷新 secret
→ SourceServerBuilder
MCP → SdkMcpServerConfig
API → in-process McpServer
→ McpClientPool.sync()
→ listTools + cache
→ mcp__<slug>__<tool> ProxyToolDef
→ Claude/Pi register tools
→ tool call 回 McpClientPool.callTool()

每一层都把复杂度往下收敛:backend 不知道 OAuth,模型不关心 transport,UI 不持有 MCP client。

源码:server-builder.ts

要求 command,返回:

{ type: 'stdio', command, args, env }

是否允许本地 stdio 还由 workspace localMcpEnabled 控制。远程 server 的“local”指服务器本地,不是用户 Electron 本机。

URL 规范化;header 以优先级合并:

静态非 secret headers
< credential-store multi headers
< OAuth/Bearer Authorization

若 config 声称已认证但 token 丢失,builder 返回 null/error,不能创建一个必然 401 的“connected” source。

API source 被包装成 in-process MCP server。不同 auth:

  • Google/Slack/generic OAuth:每请求调用 token getter,支持刷新;
  • none:空 credential;
  • renew endpoint:用动态 token getter;
  • api key/header/query/basic:优先用每请求 credential getter;
  • 只有测试/legacy caller 才用构建时静态 credential。

“每请求 getter”修复一个典型热更新 bug:用户在 credential prompt 粘贴新 JWT 后,旧 tool closure 不能继续永久捕获旧 token。

api-tools.ts 根据 source config 创建 tools:

  • JSON schema 描述 path/query/body;
  • 执行时拼 URL/header/auth;
  • 校验 endpoint/URL;
  • 解析 JSON/text/binary;
  • 大响应落文件并可摘要;
  • mutation metadata进入权限管道。

模型看到的仍是 mcp__slug__toolName,Claude/Pi 不需要专门的 REST 工具协议。

这是一种很实用的统一:MCP 是工具承载格式,不要求外部服务本身实现 MCP。

McpClientPool 在主进程拥有所有 source connection。

内部表:

clients: slug → MCP/API PoolClient
activeConfigs: slug → connection config
toolCache: slug → Tool[]
proxyTools: proxyName → {slug, originalName}
  • Claude/Pi 共用一条连接/错误处理;
  • secret 不写 provider 子进程配置;
  • source token 更新只重连 pool;
  • tool result guard 一处实现;
  • 可复用连接与统一 debug;
  • runtime source switching 不依赖 provider 原生 MCP lifecycle。

12.7 sync() 是 reconcile,不是全量重启

Section titled “12.7 sync() 是 reconcile,不是全量重启”

McpClientPool.sync

  1. 若 workspace 禁 local MCP,过滤 stdio;
  2. 计算 desired/current slugs;
  3. 断开已移除 source;
  4. 连接新 MCP;
  5. URL 或 Authorization 改变时只重连该 source;
  6. 连接新 in-process API server;
  7. 收集失败 slugs,不因一个失败取消全部;
  8. onToolsChanged() 通知 backend 更新定义。

配置 change detection 当前重点比较 transport、URL 与 Authorization。若未来影响连接的其他 header/env 改变,也要纳入签名,否则 pool 可能继续用旧配置。

工具统一为:

mcp__<sourceSlug>__<originalToolName>

getProxyToolDefs() 从缓存 tool schema 派生,并移除顶层 $schema,因为 Pi/AJV 未注册某些 meta-schema URI 会拒绝。

命名同时提供:

  • 全局无冲突 namespace;
  • PreToolUse 可提取 source slug;
  • result/error 能归因到 source;
  • UI 能按 source icon/名称展示。

McpClientPool.callTool

proxyName → slug/originalName
→ 找 PoolClient
→ callTool(originalName,args)
→ normalize content blocks
→ text 拼接
→ image/audio base64 解码并保存 downloads
→ guardLargeResult(落文件/摘要)
→ {content,isError,sourceSlug?}

非标准 MCP server 可能把 text 返回 object,代码会 JSON stringify,而不是崩溃。Binary 用 magic bytes 检测扩展名,不完全信任 MIME/文件名。

大响应保护很关键:source API 一次返回几 MB JSON,如果原样回模型,不仅超 context,还会把 session JSONL 和 UI 卡死。

模型调用 inactive source tool 时,PreToolUse 返回 activation needed。成功路径:

SessionManager 激活 source
→ load credential / build server
→ pool.sync
→ backend re-register proxy tools
→ source_activated AgentEvent
→ 对原 user message 做去重 auto-retry

Auto-retry 需要 key,防 source activation/event 重复导致无限重发。工具变化只在安全 turn boundary 对 Pi 重建 session;Claude 可根据 SDK server/tool机制更新。

中央 CredentialManager 用结构化 CredentialId 区分:

  • LLM connection;
  • workspace/source;
  • credential name/type;
  • OAuth/API key/IAM/service account 等形态。

Source config 不保存 secret,只保存 client id、auth scheme、header names、endpoint 等非秘密元数据。多 header credential 是 map,而不是把多个 key 拼成一个字符串。

SecureStorageBackend 的格式:

64-byte header
magic CRAFT01\0
flags
32-byte PBKDF2 salt
reserved
payload
12-byte random IV
16-byte GCM auth tag
ciphertext(JSON)

Key 由 OS 稳定机器 ID 派生;旧 hostname key 可自动迁移。每次写新 IV,权限 0600

Environment backend 代码存在但 isAvailable() 返回 false,当前设计要求用户显式输入 secret;provider connection 仍可能按 authType 直接从运行环境解析特定变量,这与通用 credential backend 是两条路径,需区分。

CredentialManager 的 isExpired()

  • expiresAt:提前 5 分钟视为过期;
  • 无 expiry 但有 refresh token:保守视为过期,触发刷新;
  • 无 refresh token 的 API key:视为长期有效。

源码:CredentialManager.isExpired

SourceCredentialManager 再按 provider/renew endpoint 执行刷新。并发 refresh 应合并/串行,避免 rotation token 被两次使用导致其中一条作废。

支持:

  • Claude OAuth + PKCE;
  • Google;
  • Slack user OAuth/rotation;
  • Microsoft;
  • 通用 API OAuth;
  • MCP OAuth progressive discovery;
  • Cloud relay/deeplink,用于要求 HTTPS 或跨设备回调的场景。

Provider-specific code处理 scope、token response 和 refresh 差异;共同 flow 通过 OAuthFlowStore 保存短期 state/TTL 与 session/workspace context。

auth/oauth.ts 采用渐进发现:

  1. RFC 9728:访问 protected resource,解析 401 WWW-Authenticateresource_metadata
  2. 读取 protected resource metadata 中 authorization server;
  3. 获取其 authorization server metadata;
  4. fallback 到 RFC 8414 well-known 候选;
  5. 可选 dynamic client registration;
  6. PKCE + state;
  7. code exchange/store。

这比假设 /.well-known/oauth-authorization-server 固定在 MCP host 根路径更兼容真实服务。

OAuth discovery 会 fetch source 提供或远端 header 指向的 URL,是典型 SSRF 面。isUrlSafeToFetch() 拒绝:

  • 非 HTTPS;
  • localhost;
  • 私网 IPv4;
  • link-local / 169.254/16(云 metadata);
  • 其他明显内部地址。

源码:auth/oauth.ts

基础字符串/IP 检查仍可能受 DNS rebinding、IPv6 映射、redirect 影响。高安全部署应在每次 redirect/DNS resolution 后验证最终地址,并从网络层禁止 metadata/private egress。

Headless server 拥有 credential 与 source,但用户浏览器在 client:

server prepare flow + state
→ RPC auth_request(authUrl)
→ Electron/WebUI 打开本地浏览器
→ callback/deeplink/cookie 返回 code
→ server exchange + encrypted store
→ source pool refresh
→ session retry

Preload 注释强调 OAuth 浏览器交互要在客户端编排,因为 headless host 不能假设有 GUI。

Source 的 guide.md 承担长文档;tool description 只保留操作摘要与 schema。PrerequisiteManager 确保第一次使用前读 guide。

这是 Source 自助配置的关键:Agent 可以创建 config + guide,source_test 做 schema/连接/auth/完整性检查,并默认 enable + 当前 session activate,无需应用发布新代码。

故障 行为
一个 source 连接失败 记录 slug,其他 source 继续
token 缺失/过期 needs auth/refresh,不注册坏 server
tool 不存在 proxy 返回结构化 error
MCP client 断开 sourceSlug 归因,允许重连
二进制返回 落 session downloads,返回路径
大文本 落 long response + 可选 summary
新 credential 输入 getter 下一请求读取,无需 restart
tool schema 变化 cache 更新,backend安全边界重注册
local MCP 被禁 sync 时过滤 stdio

优点:

  • Source 是文件化、可由 Agent创建的扩展点;
  • MCP 与 REST 统一为 tool;
  • pool 集中 secret、连接、结果保护;
  • 每 session enable,减少不必要工具/context;
  • OAuth/credential 热刷新贯穿执行链。

风险:

  • shared 中 source/auth 代码量很大,provider 分支持续增长;
  • MCP mutation classification 依赖外部质量;
  • connection change signature 需覆盖所有 header/env;
  • generic OAuth/URL 是高风险输入面;
  • 多 session pool 共享与 sessionPath-specific result storage 需明确实例作用域。

Source 子系统把“连接一个外部服务”拆成可验证的文件配置、独立秘密、动态 server builder、中央 connection pool 和 backend proxy tools。真正的价值是热更新链闭环:凭据或 source 改变后,不必重启整个产品就能在当前 session 使用。

下一章回到 session 数据生命周期,详细看 JSONL 写入队列、严格 branch、bundle transfer 与 public share。