本章总览
entrypoints/mcp.ts 实现 Claude Code 作为 MCP Server 的 stdio 模式:对外暴露内置 tools(getTools)与 review 命令,供外部 MCP client 调用。与 services/mcp/(Claude Code 作为 MCP client 连接外部 server)方向相反。本章讲解 startMCPServer、ListTools/CallTool handler 与 agentSdkTypes 的类型对称。
学完本章你应该能
- 区分 mcp.ts server 与 services/mcp client 架构角色
- 说明 setCwd、readFileStateCache、getEmptyToolPermissionContext 初始化
- 理解 zodToJsonSchema 与 outputSchema type:object 根级限制
- 描述 CallTool 内 hasPermissionsToUseTool 与 abort 行为
- 关联 agentSdkTypes tool() 与 MCP SDK CallToolResult
核心概念(先读懂这些)
Claude Code 可同时是 MCP client 与 server
REPL 内用户配置 MCP servers → client。claude mcp serve 或嵌入场景 → mcp.ts server。server 名 claude/tengu,capabilities.tools。MCP_COMMANDS 目前仅 [review],TODO 注释提到未来 re-expose MCP tools。CallTool 与 ListTools 必须保持 schema 一致以免 client 调用了列表中不存在的 tool。
权限上下文在 server 模式简化
ListTools/CallTool 使用 getEmptyToolPermissionContext(),不弹出 REPL PermissionRequest。hasPermissionsToUseTool 仍执行,fail 时返回 tool error。readFileStateCache LRU 100 文件 / 25MB 防 MCP 长进程内存泄漏。
建议学习步骤
- 阅读 startMCPServer 签名与 Server 构造(源码块 A)
- 阅读 ListToolsRequestSchema handler(源码块 B)
- 阅读 CallTool 路径(源码块 C)
- 对照 agentSdkTypes MCP 类型 import(源码块 D)
- 理解 outputSchema 根级 object 限制原因
常见误区
注意
outputSchema 根级 anyOf/oneOf 会被跳过(GitHub issue #8014)
注意
MCP server 不加载 Ink/React,保持 headless 进程
注意
debug/verbose 参数传入但主要用于 tool execution logging
架构位置:server vs client
MCP 在 Claude Code 中有两个正交入口:
Claude Code 作为 CLIENT(常见):
services/mcp/* → REPL 连接 filesystem/git MCP servers
skills/mcpSkills.ts → MCP prompts 转为 slash skills
Claude Code 作为 SERVER(本章):
entrypoints/mcp.ts → startMCPServer(cwd, debug, verbose)
StdioServerTransport + @modelcontextprotocol/sdk Server
外部 IDE 或 orchestrator 通过 stdio Spawn claude MCP 模式进程,ListTools 获取与 CLI 内 nearly 相同的 tool schema(受 permission context 影响)。
agentSdkTypes.ts 从 MCP SDK 导入 CallToolResult,保证 SDK 用户定义的 tool 与 server 暴露的 tool 共用类型语言。
源码引用: src/entrypoints/mcp.ts · 第 1–28 行(共 197 行)
1| import { Server } from '@modelcontextprotocol/sdk/server/index.js'
2| import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
3| import {
4| CallToolRequestSchema,
5| type CallToolResult,
6| ListToolsRequestSchema,
7| type ListToolsResult,
8| type Tool,
9| } from '@modelcontextprotocol/sdk/types.js'
10| import { getDefaultAppState } from 'src/state/AppStateStore.js'
11| import review from '../commands/review.js'
12| import type { Command } from '../commands.js'
13| import {
14| findToolByName,
15| getEmptyToolPermissionContext,
16| type ToolUseContext,
17| } from '../Tool.js'
18| import { getTools } from '../tools.js'
19| import { createAbortController } from '../utils/abortController.js'
20| import { createFileStateCacheWithSizeLimit } from '../utils/fileStateCache.js'
21| import { logError } from '../utils/log.js'
22| import { createAssistantMessage } from '../utils/messages.js'
23| import { getMainLoopModel } from '../utils/model/model.js'
24| import { hasPermissionsToUseTool } from '../utils/permissions/permissions.js'
25| import { setCwd } from '../utils/Shell.js'
26| import { jsonStringify } from '../utils/slowOperations.js'
27| import { getErrorParts } from '../utils/toolErrors.js'
28| import { zodToJsonSchema } from '../utils/zodToJsonSchema.js'
源码引用: src/entrypoints/agentSdkTypes.ts · 第 12–15 行(共 444 行)
12| import type {
13| CallToolResult,
14| ToolAnnotations,
15| } from '@modelcontextprotocol/sdk/types.js'
startMCPServer 初始化
export async function startMCPServer(cwd, debug, verbose)
启动步骤:
createFileStateCacheWithSizeLimit(100)— LRU readFileStatesetCwd(cwd)— 工具路径解析基准new Server({ name: 'claude/tengu', version: MACRO.VERSION }, { capabilities: { tools: {} } })- 注册 ListTools / CallTool request handlers
StdioServerTransportconnect
MCP_COMMANDS = [review] — slash command 也可暴露为 MCP 可调用项(与纯 tool 区分)。
Server 不调用 init() 全量路径时,caller 负责 prior enableConfigs(视 launch 路径而定)。
源码引用: src/entrypoints/mcp.ts · 第 33–57 行(共 197 行)
33| const MCP_COMMANDS: Command[] = [review]
34|
35| export async function startMCPServer(
36| cwd: string,
37| debug: boolean,
38| verbose: boolean,
39| ): Promise<void> {
40| // Use size-limited LRU cache for readFileState to prevent unbounded memory growth
41| // 100 files and 25MB limit should be sufficient for MCP server operations
42| const READ_FILE_STATE_CACHE_SIZE = 100
43| const readFileStateCache = createFileStateCacheWithSizeLimit(
44| READ_FILE_STATE_CACHE_SIZE,
45| )
46| setCwd(cwd)
47| const server = new Server(
48| {
49| name: 'claude/tengu',
50| version: MACRO.VERSION,
51| },
52| {
53| capabilities: {
54| tools: {},
55| },
56| },
57| )
源码引用: src/entrypoints/mcp.ts · 第 40–46 行(共 197 行)
40| // Use size-limited LRU cache for readFileState to prevent unbounded memory growth
41| // 100 files and 25MB limit should be sufficient for MCP server operations
42| const READ_FILE_STATE_CACHE_SIZE = 100
43| const readFileStateCache = createFileStateCacheWithSizeLimit(
44| READ_FILE_STATE_CACHE_SIZE,
45| )
46| setCwd(cwd)
ListTools:schema 转换与 outputSchema 过滤
ListTools handler:
const toolPermissionContext = getEmptyToolPermissionContext()
const tools = getTools(toolPermissionContext)
return {
tools: await Promise.all(tools.map(async tool => {
// inputSchema via zodToJsonSchema
// outputSchema only if root type === 'object'
}))
}
关键注释:MCP SDK 要求 outputSchema 根级 type: "object"。z.union / discriminatedUnion 转换后可能出现 anyOf/oneOf 根节点,此类 schema 跳过 outputSchema(GitHub issue #8014)。
这与 REPL 内 tool 列表同源 getTools(),但 permission context 为空——server 模式无 interactive allow rules UI。
源码引用: src/entrypoints/mcp.ts · 第 59–80 行(共 197 行)
59| server.setRequestHandler(
60| ListToolsRequestSchema,
61| async (): Promise<ListToolsResult> => {
62| // TODO: Also re-expose any MCP tools
63| const toolPermissionContext = getEmptyToolPermissionContext()
64| const tools = getTools(toolPermissionContext)
65| return {
66| tools: await Promise.all(
67| tools.map(async tool => {
68| let outputSchema: ToolOutput | undefined
69| if (tool.outputSchema) {
70| const convertedSchema = zodToJsonSchema(tool.outputSchema)
71| // MCP SDK requires outputSchema to have type: "object" at root level
72| // Skip schemas with anyOf/oneOf at root (from z.union, z.discriminatedUnion, etc.)
73| // See: https://github.com/anthropics/claude-code/issues/8014
74| if (
75| typeof convertedSchema === 'object' &&
76| convertedSchema !== null &&
77| 'type' in convertedSchema &&
78| convertedSchema.type === 'object'
79| ) {
80| outputSchema = convertedSchema as ToolOutput
源码引用: src/tools.ts · 第 1–30 行(共 390 行)
1| // biome-ignore-all assist/source/organizeImports: ANT-ONLY import markers must not be reordered
2| import { toolMatchesName, type Tool, type Tools } from './Tool.js'
3| import { AgentTool } from './tools/AgentTool/AgentTool.js'
4| import { SkillTool } from './tools/SkillTool/SkillTool.js'
5| import { BashTool } from './tools/BashTool/BashTool.js'
6| import { FileEditTool } from './tools/FileEditTool/FileEditTool.js'
7| import { FileReadTool } from './tools/FileReadTool/FileReadTool.js'
8| import { FileWriteTool } from './tools/FileWriteTool/FileWriteTool.js'
9| import { GlobTool } from './tools/GlobTool/GlobTool.js'
10| import { NotebookEditTool } from './tools/NotebookEditTool/NotebookEditTool.js'
11| import { WebFetchTool } from './tools/WebFetchTool/WebFetchTool.js'
12| import { TaskStopTool } from './tools/TaskStopTool/TaskStopTool.js'
13| import { BriefTool } from './tools/BriefTool/BriefTool.js'
14| // Dead code elimination: conditional import for ant-only tools
15| /* eslint-disable custom-rules/no-process-env-top-level, @typescript-eslint/no-require-imports */
16| const REPLTool =
17| process.env.USER_TYPE === 'ant'
18| ? require('./tools/REPLTool/REPLTool.js').REPLTool
19| : null
20| const SuggestBackgroundPRTool =
21| process.env.USER_TYPE === 'ant'
22| ? require('./tools/SuggestBackgroundPRTool/SuggestBackgroundPRTool.js')
23| .SuggestBackgroundPRTool
24| : null
25| const SleepTool =
26| feature('PROACTIVE') || feature('KAIROS')
27| ? require('./tools/SleepTool/SleepTool.js').SleepTool
28| : null
29| const cronTools = feature('AGENT_TRIGGERS')
30| ? [
CallTool:执行与权限
CallTool handler 流程概要:
- findToolByName(name)
- parse arguments JSON
- createAbortController() — 客户端 disconnect 时 abort
- 构造 ToolUseContext(简化 AppState,无 REPL)
- hasPermissionsToUseTool — deny 则 structured error
- tool.call() → CallToolResult content parts
- jsonStringify 结果
getDefaultAppState() 提供 baseline store;与 REPL 的 canUseTool 队列不同,失败直接返回 error content。
createAssistantMessage / getErrorParts 统一错误格式化。
源码引用: src/entrypoints/mcp.ts · 第 80–120 行(共 197 行)
80| outputSchema = convertedSchema as ToolOutput
81| }
82| }
83| return {
84| ...tool,
85| description: await tool.prompt({
86| getToolPermissionContext: async () => toolPermissionContext,
87| tools,
88| agents: [],
89| }),
90| inputSchema: zodToJsonSchema(tool.inputSchema) as ToolInput,
91| outputSchema,
92| }
93| }),
94| ),
95| }
96| },
97| )
98|
99| server.setRequestHandler(
100| CallToolRequestSchema,
101| async ({ params: { name, arguments: args } }): Promise<CallToolResult> => {
102| const toolPermissionContext = getEmptyToolPermissionContext()
103| // TODO: Also re-expose any MCP tools
104| const tools = getTools(toolPermissionContext)
105| const tool = findToolByName(tools, name)
106| if (!tool) {
107| throw new Error(`Tool ${name} not found`)
108| }
109|
110| // Assume MCP servers do not read messages separately from the tool
111| // call arguments.
112| const toolUseContext: ToolUseContext = {
113| abortController: createAbortController(),
114| options: {
115| commands: MCP_COMMANDS,
116| tools,
117| mainLoopModel: getMainLoopModel(),
118| thinkingConfig: { type: 'disabled' },
119| mcpClients: [],
120| mcpResources: {},
源码引用: src/entrypoints/mcp.ts · 第 120–160 行(共 197 行)
120| mcpResources: {},
121| isNonInteractiveSession: true,
122| debug,
123| verbose,
124| agentDefinitions: { activeAgents: [], allAgents: [] },
125| },
126| getAppState: () => getDefaultAppState(),
127| setAppState: () => {},
128| messages: [],
129| readFileState: readFileStateCache,
130| setInProgressToolUseIDs: () => {},
131| setResponseLength: () => {},
132| updateFileHistoryState: () => {},
133| updateAttributionState: () => {},
134| }
135|
136| // TODO: validate input types with zod
137| try {
138| if (!tool.isEnabled()) {
139| throw new Error(`Tool ${name} is not enabled`)
140| }
141| const validationResult = await tool.validateInput?.(
142| (args as never) ?? {},
143| toolUseContext,
144| )
145| if (validationResult && !validationResult.result) {
146| throw new Error(
147| `Tool ${name} input is invalid: ${validationResult.message}`,
148| )
149| }
150| const finalResult = await tool.call(
151| (args ?? {}) as never,
152| toolUseContext,
153| hasPermissionsToUseTool,
154| createAssistantMessage({
155| content: [],
156| }),
157| )
158|
159| return {
160| content: [
agentSdkTypes 与 MCP server 对称
SDK 侧 tool() helper 返回符合 MCP CallToolResult 的 handler;server 侧 ListTools 把内部 Tool 定义转为 MCP Tool JSON schema。
类型导入链:
@modelcontextprotocol/sdk/types.js
→ agentSdkTypes (tool helper)
→ mcp.ts (CallToolResult in handlers)
SdkMcpToolDefinition(runtimeTypes)允许 SDK 进程内嵌 MCP server 实例配置,与 stdio mcp.ts 互补——前者 in-process,后者 subprocess 隔离。
embed 场景 controlTypes permission subtype 可把 tool permission 转发宿主 UI,弥补 server 模式无 REPL PermissionRequest 的缺口。
源码引用: src/entrypoints/agentSdkTypes.ts · 第 73–90 行(共 444 行)
73| export function tool<Schema extends AnyZodRawShape>(
74| _name: string,
75| _description: string,
76| _inputSchema: Schema,
77| _handler: (
78| args: InferShape<Schema>,
79| extra: unknown,
80| ) => Promise<CallToolResult>,
81| _extras?: {
82| annotations?: ToolAnnotations
83| searchHint?: string
84| alwaysLoad?: boolean
85| },
86| ): SdkMcpToolDefinition<Schema> {
87| throw new Error('not implemented')
88| }
89|
90| type CreateSdkMcpServerOptions = {
源码引用: src/entrypoints/sdk/runtimeTypes.ts · 第 17–22 行(共 23 行)
17| export type AnyZodRawShape = Record<string, unknown>
18| export type InferShape<T> = T
19| export type SdkMcpToolDefinition<Schema> = {
20| schema?: Schema
21| [key: string]: unknown
22| }
与 cli fast-path 的关系
cli.tsx 含 MCP sidecar fast-path:
--claude-in-chrome-mcp→ runClaudeInChromeMcpServer--computer-use-mcp→ runComputerUseMcpServer
这些 不是 entrypoints/mcp.ts,但同属「Claude 子进程作为 MCP server」家族。entrypoints/mcp.ts 通常由 dedicated mcp serve 或测试 harness 调用。
对比 services/mcp-client 章:client 连接外部 server,stdio transport 方向相反。
调试 MCP server:verbose 参数传入 startMCPServer,tool execution 日志经 logError。
源码引用: src/entrypoints/cli.tsx · 第 72–93 行(共 308 行)
72| const prompt = await getSystemPrompt([], model)
73| // biome-ignore lint/suspicious/noConsole:: intentional console output
74| console.log(prompt.join('\n'))
75| return
76| }
77|
78| if (process.argv[2] === '--claude-in-chrome-mcp') {
79| profileCheckpoint('cli_claude_in_chrome_mcp_path')
80| const { runClaudeInChromeMcpServer } = await import(
81| '../utils/claudeInChrome/mcpServer.js'
82| )
83| await runClaudeInChromeMcpServer()
84| return
85| } else if (process.argv[2] === '--chrome-native-host') {
86| profileCheckpoint('cli_chrome_native_host_path')
87| const { runChromeNativeHost } = await import(
88| '../utils/claudeInChrome/chromeNativeHost.js'
89| )
90| await runChromeNativeHost()
91| return
92| } else if (
93| feature('CHICAGO_MCP') &&
源码引用: src/entrypoints/mcp.ts · 第 35–39 行(共 197 行)
35| export async function startMCPServer(
36| cwd: string,
37| debug: boolean,
38| verbose: boolean,
39| ): Promise<void> {
ToolUseContext 简化
MCP server 的 ToolUseContext 无 REPL 全量 AppState:readFileState 来自 LRU;无 canUseTool 队列;hasPermissionsToUseTool 同步 deny。
setCwd(cwd) 必须在 getTools 前调用。debug/verbose 控制 tool 日志粒度。
client disconnect 应 abort in-flight tool,abortController 与 createAbortController 绑定。
jsonStringify 与错误格式化
CallTool 成功路径用 jsonStringify(slowOperations)序列化 tool result,避免大对象阻塞 event loop。
失败路径 getErrorParts + createAssistantMessage 统一 error content blocks,MCP client 收到 isError: true。
getMainLoopModel 在 MCP server 模式仍影响需要 model 的 tool(如 implicit 默认);与 REPL mainLoopModel 同源函数。
MCP_COMMANDS 与 review
const MCP_COMMANDS: Command[] = [review]
review 命令作为 Command 类型注入 MCP 层,允许外部 client 触发 code review 流程而不仅是 raw Bash/Read tools。
ListTools TODO:Also re-expose any MCP tools — 未来可能把用户连接的 MCP server tools 反向代理到 stdio server,当前未实现。
zodToJsonSchema 位于 utils/zodToJsonSchema.ts,与 API tool schema 转换共用逻辑。
源码引用: src/entrypoints/mcp.ts · 第 30–34 行(共 197 行)
30| type ToolInput = Tool['inputSchema']
31| type ToolOutput = Tool['outputSchema']
32|
33| const MCP_COMMANDS: Command[] = [review]
34|
源码引用: src/entrypoints/mcp.ts · 第 61–63 行(共 197 行)
61| async (): Promise<ListToolsResult> => {
62| // TODO: Also re-expose any MCP tools
63| const toolPermissionContext = getEmptyToolPermissionContext()
本章小结与延伸
mcp.ts = Claude Code 作为 MCP tool provider。SDK 类型见 agentSdkTypes;client 见 services/mcp-client 章。ListTools 与 CallTool 共用 getTools 但 permission context 为空。review 是唯一 MCP_COMMANDS 斜杠注入项。 继续学习: