本章总览
services/mcp/client.ts(约 3300 行)是 Claude Code 接入 Model Context Protocol 的核心:按配置选择 SSE / stdio / WebSocket / HTTP 传输,OAuth 刷新、session 过期重连、tools/list 转 MCPTool,并在 REPL 侧由 MCPConnectionManager 管理动态配置。本章要求你能从 mcp__github__create_issue 工具名反查到 connectToServer 与 fetchToolsForClient 的缓存键。
学完本章你应该能
- 列举 MCP 传输类型(sse、stdio、ws、http、sdk)及选型条件
- 解释 connectToServer memoize 与 ensureConnectedClient 重连语义
- 说明 McpAuthError / McpSessionExpiredError 的处理边界
- 理解 fetchToolsForClient 如何把 MCP schema 映射为应用 Tool
- 能在 UI 层定位 MCPConnectionManager 与 useManageMCPConnections
核心概念(先读懂这些)
MCP 工具名是三层命名
对外暴露给模型的名字通常是 mcp__{serverName}__{toolName}(buildMcpToolName)。权限系统用 mcpInfo: { serverName, toolName } 做细粒度规则。SDK 内嵌 MCP 在 CLAUDE_AGENT_SDK_MCP_NO_PREFIX 时可省略前缀以覆盖内置工具名。读 config.json 的 mcpServers 时,scope(project/user)不影响连接键,areMcpConfigsEqual 会 strip scope 再 JSON 比较。
SSE EventSource 不能套请求 timeout
connectToServer 对 SSE 显式区分:POST/auth 走 wrapFetchWithTimeout,而 eventSourceInit.fetch 不带 60s timeout,否则长连接会被误杀。这是 MCP 连接「刚连上就断」的常见根因之一。
Session 404 与 JSON-RPC -32001
isMcpSessionExpiredError 要求 HTTP 404 且 body 含 Session not found(code -32001),避免把错误 URL 当成 session 过期。过期后 clear cache + ensureConnectedClient 重试是标准恢复路径。
建议学习步骤
- 阅读源码块 A:错误类型 McpAuthError / session expired
- 阅读源码块 B:connectToServer 传输分支
- 阅读源码块 C:ensureConnectedClient 与 config 比较
- 阅读源码块 D:fetchToolsForClient 映射
- 阅读源码块 E:callMCPToolWithUrlElicitationRetry
- 阅读源码块 F:MCPConnectionManager React 上下文
- 在源码树打开 services/mcp/client.ts 对照行号
常见误区
注意
不要把 services/mcp/config.ts 的策略过滤与 client.ts 连接逻辑混读
注意
memoize 的 connectToServer 配置变更需 clearServerCache 才会重建
注意
IDE ws/sse-ide transport 通常无 OAuth,与远程 sse 路径不同
在架构中的位置
MCP 在 Claude Code 中的生命周期:
启动 /settings 变更 → getAllMcpConfigs + policy filter
→ connectToServer (client.ts) 并行 batch
→ fetchToolsForClient / fetchResourcesForClient / fetchCommandsForClient
→ 写入 AppState.mcp.clients
→ query 循环 tools 数组含 MCPTool
→ callMCPTool → transformMCPResult → tool_result 回 transcript
services/mcp/ 还包含 config.ts、auth.ts、elicitationHandler.ts、MCPConnectionManager.tsx 等。本章以 client.ts 为主,连接编排看 useManageMCPConnections.ts。
错误类型与可恢复边界
client.ts 定义若干 可区分 的错误类,供 tool 层与 UI 决策:
- McpAuthError:OAuth token 过期或 401,
serverName字段供状态机切到needs-auth - McpSessionExpiredError(内部):session ID 失效,调用方应 clear cache 后
ensureConnectedClient重试 - McpToolCallError:MCP 返回
isError: true但需保留_meta给 SDK 消费者(符合 MCP spec)
isMcpSessionExpiredError 双信号检测避免误判普通 404。读 tool 执行栈时,auth 错误应更新 client status 而非无限 retry tool call。
源码引用: src/services/mcp/client.ts · 第 146–210 行(共 3349 行)
146| /**
147| * Custom error class to indicate that an MCP tool call failed due to
148| * authentication issues (e.g., expired OAuth token returning 401).
149| * This error should be caught at the tool execution layer to update
150| * the client's status to 'needs-auth'.
151| */
152| export class McpAuthError extends Error {
153| serverName: string
154| constructor(serverName: string, message: string) {
155| super(message)
156| this.name = 'McpAuthError'
157| this.serverName = serverName
158| }
159| }
160|
161| /**
162| * Thrown when an MCP session has expired and the connection cache has been cleared.
163| * The caller should get a fresh client via ensureConnectedClient and retry.
164| */
165| class McpSessionExpiredError extends Error {
166| constructor(serverName: string) {
167| super(`MCP server "${serverName}" session expired`)
168| this.name = 'McpSessionExpiredError'
169| }
170| }
171|
172| /**
173| * Thrown when an MCP tool returns `isError: true`. Carries the result's `_meta`
174| * so SDK consumers can still receive it — per the MCP spec, `_meta` is on the
175| * base Result type and is valid on error results.
176| */
177| export class McpToolCallError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS extends TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS {
178| constructor(
179| message: string,
180| telemetryMessage: string,
181| readonly mcpMeta?: { _meta?: Record<string, unknown> },
182| ) {
183| super(message, telemetryMessage)
184| this.name = 'McpToolCallError'
185| }
186| }
187|
188| /**
189| * Detects whether an error is an MCP "Session not found" error (HTTP 404 + JSON-RPC code -32001).
190| * Per the MCP spec, servers return 404 when a session ID is no longer valid.
191| * We check both signals to avoid false positives from generic 404s (wrong URL, server gone, etc.).
192| */
193| export function isMcpSessionExpiredError(error: Error): boolean {
194| const httpStatus =
195| 'code' in error ? (error as Error & { code?: number }).code : undefined
196| if (httpStatus !== 404) {
197| return false
198| }
199| // The SDK embeds the response body text in the error message.
200| // MCP servers return: {"error":{"code":-32001,"message":"Session not found"},...}
201| // Check for the JSON-RPC error code to distinguish from generic web server 404s.
202| return (
203| error.message.includes('"code":-32001') ||
204| error.message.includes('"code": -32001')
205| )
206| }
207|
208| /**
209| * Default timeout for MCP tool calls (effectively infinite - ~27.8 hours).
210| */
connectToServer:传输与认证
connectToServer 用 lodash memoize 缓存 Promise,键来自 getServerCacheKey(name, serverRef)(name + JSON config)。
传输分支概要:
- sse:
ClaudeAuthProvider+SSEClientTransport;session ingress JWT 存在时可能走 proxy 而非直连 - sse-ide / ws-ide:IDE 集成,常无 OAuth;ws 支持 Bun 与 Node 两套 WebSocket 客户端
- stdio:
StdioClientTransport+subprocessEnv注入环境 - http / streamableHttp:
StreamableHTTPClientTransport - sdk / in-process:
SdkControlClientTransport或InProcessTransport
wrapFetchWithStepUpDetection 在 OAuth step-up 403 时触发 re-auth。getMcpServerHeaders 合并静态与动态 header;日志中对 Authorization 做 [REDACTED]。
连接成功后 register MCP notification handler(tool/list changed 等),并 logEvent 连接耗时与 transport 类型。
源码引用: src/services/mcp/client.ts · 第 581–677 行(共 3349 行)
581| export function getServerCacheKey(
582| name: string,
583| serverRef: ScopedMcpServerConfig,
584| ): string {
585| return `${name}-${jsonStringify(serverRef)}`
586| }
587|
588| /**
589| * TODO (ollie): The memoization here increases complexity by a lot, and im not sure it really improves performance
590| * Attempts to connect to a single MCP server
591| * @param name Server name
592| * @param serverRef Scoped server configuration
593| * @returns A wrapped client (either connected or failed)
594| */
595| export const connectToServer = memoize(
596| async (
597| name: string,
598| serverRef: ScopedMcpServerConfig,
599| serverStats?: {
600| totalServers: number
601| stdioCount: number
602| sseCount: number
603| httpCount: number
604| sseIdeCount: number
605| wsIdeCount: number
606| },
607| ): Promise<MCPServerConnection> => {
608| const connectStartTime = Date.now()
609| let inProcessServer:
610| | { connect(t: Transport): Promise<void>; close(): Promise<void> }
611| | undefined
612| try {
613| let transport
614|
615| // If we have the session ingress JWT, we will connect via the session ingress rather than
616| // to remote MCP's directly.
617| const sessionIngressToken = getSessionIngressAuthToken()
618|
619| if (serverRef.type === 'sse') {
620| // Create an auth provider for this server
621| const authProvider = new ClaudeAuthProvider(name, serverRef)
622|
623| // Get combined headers (static + dynamic)
624| const combinedHeaders = await getMcpServerHeaders(name, serverRef)
625|
626| // Use the auth provider with SSEClientTransport
627| const transportOptions: SSEClientTransportOptions = {
628| authProvider,
629| // Use fresh timeout per request to avoid stale AbortSignal bug.
630| // Step-up detection wraps innermost so the 403 is seen before the
631| // SDK's handler calls auth() → tokens().
632| fetch: wrapFetchWithTimeout(
633| wrapFetchWithStepUpDetection(createFetchWithInit(), authProvider),
634| ),
635| requestInit: {
636| headers: {
637| 'User-Agent': getMCPUserAgent(),
638| ...combinedHeaders,
639| },
640| },
641| }
642|
643| // IMPORTANT: Always set eventSourceInit with a fetch that does NOT use the
644| // timeout wrapper. The EventSource connection is long-lived (stays open indefinitely
645| // to receive server-sent events), so applying a 60-second timeout would kill it.
646| // The timeout is only meant for individual API requests (POST, auth refresh), not
647| // the persistent SSE stream.
648| transportOptions.eventSourceInit = {
649| fetch: async (url: string | URL, init?: RequestInit) => {
650| // Get auth headers from the auth provider
651| const authHeaders: Record<string, string> = {}
652| const tokens = await authProvider.tokens()
653| if (tokens) {
654| authHeaders.Authorization = `Bearer ${tokens.access_token}`
655| }
656|
657| const proxyOptions = getProxyFetchOptions()
658| // eslint-disable-next-line eslint-plugin-n/no-unsupported-features/node-builtins
659| return fetch(url, {
660| ...init,
661| ...proxyOptions,
662| headers: {
663| 'User-Agent': getMCPUserAgent(),
664| ...authHeaders,
665| ...init?.headers,
666| ...combinedHeaders,
667| Accept: 'text/event-stream',
668| },
669| })
670| },
671| }
672|
673| transport = new SSEClientTransport(
674| new URL(serverRef.url),
675| transportOptions,
676| )
677| logMCPDebug(name, `SSE transport initialized, awaiting connection`)
源码引用: src/services/mcp/client.ts · 第 595–620 行(共 3349 行)
595| export const connectToServer = memoize(
596| async (
597| name: string,
598| serverRef: ScopedMcpServerConfig,
599| serverStats?: {
600| totalServers: number
601| stdioCount: number
602| sseCount: number
603| httpCount: number
604| sseIdeCount: number
605| wsIdeCount: number
606| },
607| ): Promise<MCPServerConnection> => {
608| const connectStartTime = Date.now()
609| let inProcessServer:
610| | { connect(t: Transport): Promise<void>; close(): Promise<void> }
611| | undefined
612| try {
613| let transport
614|
615| // If we have the session ingress JWT, we will connect via the session ingress rather than
616| // to remote MCP's directly.
617| const sessionIngressToken = getSessionIngressAuthToken()
618|
619| if (serverRef.type === 'sse') {
620| // Create an auth provider for this server
ensureConnectedClient 与配置等价性
工具调用前应通过 ensureConnectedClient 保证 client.type === 'connected':
config.type === 'sdk'的 in-process 服务器直接返回,不走 connectToServer- 否则 await connectToServer;失败抛 TelemetrySafeError
areMcpConfigsEqual 用于检测 settings 变更:先比 type,再 strip scope 字段后 JSON stringify。配置变化时 useManageMCPConnections 会 disconnect + reconnect,并 clear fetchToolsForClient 的 LRU cache(键为 server name,上限 MCP_FETCH_CACHE_SIZE = 20)。
调试「工具列表陈旧」时,确认是否收到 ToolListChangedNotification 以及 reconnect 是否 clear cache。
源码引用: src/services/mcp/client.ts · 第 1688–1722 行(共 3349 行)
1688| export async function ensureConnectedClient(
1689| client: ConnectedMCPServer,
1690| ): Promise<ConnectedMCPServer> {
1691| // SDK MCP servers run in-process and are handled separately via setupSdkMcpClients
1692| if (client.config.type === 'sdk') {
1693| return client
1694| }
1695|
1696| const connectedClient = await connectToServer(client.name, client.config)
1697| if (connectedClient.type !== 'connected') {
1698| throw new TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS(
1699| `MCP server "${client.name}" is not connected`,
1700| 'MCP server not connected',
1701| )
1702| }
1703| return connectedClient
1704| }
1705|
1706| /**
1707| * Compares two MCP server configurations to determine if they are equivalent.
1708| * Used to detect when a server needs to be reconnected due to config changes.
1709| */
1710| export function areMcpConfigsEqual(
1711| a: ScopedMcpServerConfig,
1712| b: ScopedMcpServerConfig,
1713| ): boolean {
1714| // Quick type check first
1715| if (a.type !== b.type) return false
1716|
1717| // Compare by serializing - this handles all config variations
1718| // We exclude 'scope' from comparison since it's metadata, not connection config
1719| const { scope: _scopeA, ...configA } = a
1720| const { scope: _scopeB, ...configB } = b
1721| return jsonStringify(configA) === jsonStringify(configB)
1722| }
fetchToolsForClient:MCP → Tool 适配
fetchToolsForClient 对 connected client 发 tools/list,结果经 recursivelySanitizeUnicode 清洗后映射为应用 Tool 对象:
- 继承
MCPTool原型,设置isMcp: true name:默认mcp__server__tool,skip-prefix 模式用原始 tool.namesearchHint/alwaysLoad来自_meta['anthropic/searchHint']等扩展字段isReadOnly/isConcurrencySafe读 MCPannotations.readOnlyHint- description/prompt 超长截断(
MAX_MCP_DESCRIPTION_LENGTH)
mcpToolInputToAutoClassifierInput 把 tool input 编码成 auto-mode 分类器字符串(key=value),eval 脚本与生产共用。
工具 discovery 与 ToolSearchTool 的 deferred loading 协作:collapse 分类见 classifyMcpToolForCollapse。
源码引用: src/services/mcp/client.ts · 第 1728–1800 行(共 3349 行)
1728| /**
1729| * Encode MCP tool input for the auto-mode security classifier.
1730| * Exported so the auto-mode eval scripts can mirror production encoding
1731| * for `mcp__*` tool stubs without duplicating this logic.
1732| */
1733| export function mcpToolInputToAutoClassifierInput(
1734| input: Record<string, unknown>,
1735| toolName: string,
1736| ): string {
1737| const keys = Object.keys(input)
1738| return keys.length > 0
1739| ? keys.map(k => `${k}=${String(input[k])}`).join(' ')
1740| : toolName
1741| }
1742|
1743| export const fetchToolsForClient = memoizeWithLRU(
1744| async (client: MCPServerConnection): Promise<Tool[]> => {
1745| if (client.type !== 'connected') return []
1746|
1747| try {
1748| if (!client.capabilities?.tools) {
1749| return []
1750| }
1751|
1752| const result = (await client.client.request(
1753| { method: 'tools/list' },
1754| ListToolsResultSchema,
1755| )) as ListToolsResult
1756|
1757| // Sanitize tool data from MCP server
1758| const toolsToProcess = recursivelySanitizeUnicode(result.tools)
1759|
1760| // Check if we should skip the mcp__ prefix for SDK MCP servers
1761| const skipPrefix =
1762| client.config.type === 'sdk' &&
1763| isEnvTruthy(process.env.CLAUDE_AGENT_SDK_MCP_NO_PREFIX)
1764|
1765| // Convert MCP tools to our Tool format
1766| return toolsToProcess
1767| .map((tool): Tool => {
1768| const fullyQualifiedName = buildMcpToolName(client.name, tool.name)
1769| return {
1770| ...MCPTool,
1771| // In skip-prefix mode, use the original name for model invocation so MCP tools
1772| // can override builtins by name. mcpInfo is used for permission checking.
1773| name: skipPrefix ? tool.name : fullyQualifiedName,
1774| mcpInfo: { serverName: client.name, toolName: tool.name },
1775| isMcp: true,
1776| // Collapse whitespace: _meta is open to external MCP servers, and
1777| // a newline here would inject orphan lines into the deferred-tool
1778| // list (formatDeferredToolLine joins on '\n').
1779| searchHint:
1780| typeof tool._meta?.['anthropic/searchHint'] === 'string'
1781| ? tool._meta['anthropic/searchHint']
1782| .replace(/\s+/g, ' ')
1783| .trim() || undefined
1784| : undefined,
1785| alwaysLoad: tool._meta?.['anthropic/alwaysLoad'] === true,
1786| async description() {
1787| return tool.description ?? ''
1788| },
1789| async prompt() {
1790| const desc = tool.description ?? ''
1791| return desc.length > MAX_MCP_DESCRIPTION_LENGTH
1792| ? desc.slice(0, MAX_MCP_DESCRIPTION_LENGTH) + '… [truncated]'
1793| : desc
1794| },
1795| isConcurrencySafe() {
1796| return tool.annotations?.readOnlyHint ?? false
1797| },
1798| isReadOnly() {
1799| return tool.annotations?.readOnlyHint ?? false
1800| },
callMCPTool 与 URL Elicitation
部分 MCP 服务器在需要用户浏览器授权时返回 UrlElicitationRequired(JSON-RPC -32042)。callMCPToolWithUrlElicitationRetry 循环:
- 调用
callMCPTool - 捕获
McpError且 code 匹配 UrlElicitationRequired - 通过 elicitation handler / REPL queue 展示 URL,等待用户完成
- 最多 MAX_URL_ELICITATION_RETRIES = 3 次
transformMCPResult / processMCPResult 负责大输出截断(truncateMcpContentIfNeeded)、二进制 blob 落盘(persistBinaryContent)、以及 tool_result 持久化(persistToolResult)。
读 Chrome MCP 或 enterprise SSO MCP 时,此路径与 mcpServerApproval.tsx UI 叠加。
源码引用: src/services/mcp/client.ts · 第 2812–2875 行(共 3349 行)
2812| /** @internal Exported for testing. */
2813| export async function callMCPToolWithUrlElicitationRetry({
2814| client: connectedClient,
2815| clientConnection,
2816| tool,
2817| args,
2818| meta,
2819| signal,
2820| setAppState,
2821| onProgress,
2822| callToolFn = callMCPTool,
2823| handleElicitation,
2824| }: {
2825| client: ConnectedMCPServer
2826| clientConnection: MCPServerConnection
2827| tool: string
2828| args: Record<string, unknown>
2829| meta?: Record<string, unknown>
2830| signal: AbortSignal
2831| setAppState: (f: (prev: AppState) => AppState) => void
2832| onProgress?: (data: MCPProgress) => void
2833| /** Injectable for testing. Defaults to callMCPTool. */
2834| callToolFn?: (opts: {
2835| client: ConnectedMCPServer
2836| tool: string
2837| args: Record<string, unknown>
2838| meta?: Record<string, unknown>
2839| signal: AbortSignal
2840| onProgress?: (data: MCPProgress) => void
2841| }) => Promise<MCPToolCallResult>
2842| /** Handler for URL elicitations when no hook handles them.
2843| * In print/SDK mode, delegates to structuredIO. In REPL, falls back to queue. */
2844| handleElicitation?: (
2845| serverName: string,
2846| params: ElicitRequestURLParams,
2847| signal: AbortSignal,
2848| ) => Promise<ElicitResult>
2849| }): Promise<MCPToolCallResult> {
2850| const MAX_URL_ELICITATION_RETRIES = 3
2851| for (let attempt = 0; ; attempt++) {
2852| try {
2853| return await callToolFn({
2854| client: connectedClient,
2855| tool,
2856| args,
2857| meta,
2858| signal,
2859| onProgress,
2860| })
2861| } catch (error) {
2862| // The MCP SDK's Protocol creates plain McpError (not UrlElicitationRequiredError)
2863| // for error responses, so we check the error code instead of instanceof.
2864| if (
2865| !(error instanceof McpError) ||
2866| error.code !== ErrorCode.UrlElicitationRequired
2867| ) {
2868| throw error
2869| }
2870|
2871| // Limit the number of URL elicitation retries
2872| if (attempt >= MAX_URL_ELICITATION_RETRIES) {
2873| throw error
2874| }
2875|
MCPConnectionManager 与 useManageMCPConnections
React 层 MCPConnectionManager 提供 Context:
useMcpReconnect()→reconnectMcpServeruseMcpToggleEnabled()→ 启用/禁用单个 server(写 config + reconnect)
内部 useManageMCPConnections 监听 dynamicMcpConfig 与 isStrictMcpConfig:
- 初次 mount 批量
getMcpToolsCommandsAndResources - 注册 elicitation、channel permission、list_changed notification
- 失败指数退避:
MAX_RECONNECT_ATTEMPTS = 5,INITIAL_BACKOFF_MS = 1000,上限 30s - plugin 错误 dedup 写入
AppState.plugins.errors
TODO 注释提到未来可能把 reconnect/toggle 挪到 AppState,减少 Context 层级。
源码引用: src/services/mcp/MCPConnectionManager.tsx · 第 7–72 行(共 75 行)
7| import type { Command } from '../../commands.js'
8| import type { Tool } from '../../Tool.js'
9| import type {
10| MCPServerConnection,
11| ScopedMcpServerConfig,
12| ServerResource,
13| } from './types.js'
14| import { useManageMCPConnections } from './useManageMCPConnections.js'
15|
16| interface MCPConnectionContextValue {
17| reconnectMcpServer: (serverName: string) => Promise<{
18| client: MCPServerConnection
19| tools: Tool[]
20| commands: Command[]
21| resources?: ServerResource[]
22| }>
23| toggleMcpServer: (serverName: string) => Promise<void>
24| }
25|
26| const MCPConnectionContext = createContext<MCPConnectionContextValue | null>(
27| null,
28| )
29|
30| export function useMcpReconnect() {
31| const context = useContext(MCPConnectionContext)
32| if (!context) {
33| throw new Error('useMcpReconnect must be used within MCPConnectionManager')
34| }
35| return context.reconnectMcpServer
36| }
37|
38| export function useMcpToggleEnabled() {
39| const context = useContext(MCPConnectionContext)
40| if (!context) {
41| throw new Error(
42| 'useMcpToggleEnabled must be used within MCPConnectionManager',
43| )
44| }
45| return context.toggleMcpServer
46| }
47|
48| interface MCPConnectionManagerProps {
49| children: ReactNode
50| dynamicMcpConfig: Record<string, ScopedMcpServerConfig> | undefined
51| isStrictMcpConfig: boolean
52| }
53|
54| // TODO (ollie): We may be able to get rid of this context by putting these function on app state
55| export function MCPConnectionManager({
56| children,
57| dynamicMcpConfig,
58| isStrictMcpConfig,
59| }: MCPConnectionManagerProps): React.ReactNode {
60| const { reconnectMcpServer, toggleMcpServer } = useManageMCPConnections(
61| dynamicMcpConfig,
62| isStrictMcpConfig,
63| )
64| const value = useMemo(
65| () => ({ reconnectMcpServer, toggleMcpServer }),
66| [reconnectMcpServer, toggleMcpServer],
67| )
68|
69| return (
70| <MCPConnectionContext.Provider value={value}>
71| {children}
72| </MCPConnectionContext.Provider>
源码引用: src/services/mcp/useManageMCPConnections.ts · 第 87–120 行(共 1142 行)
87| // Constants for reconnection with exponential backoff
88| const MAX_RECONNECT_ATTEMPTS = 5
89| const INITIAL_BACKOFF_MS = 1000
90| const MAX_BACKOFF_MS = 30000
91|
92| /**
93| * Create a unique key for a plugin error to enable deduplication
94| */
95| function getErrorKey(error: PluginError): string {
96| const plugin = 'plugin' in error ? error.plugin : 'no-plugin'
97| return `${error.type}:${error.source}:${plugin}`
98| }
99|
100| /**
101| * Add errors to AppState, deduplicating to avoid showing the same error multiple times
102| */
103| function addErrorsToAppState(
104| setAppState: (updater: (prev: AppState) => AppState) => void,
105| newErrors: PluginError[],
106| ): void {
107| if (newErrors.length === 0) return
108|
109| setAppState(prevState => {
110| // Build set of existing error keys
111| const existingKeys = new Set(
112| prevState.plugins.errors.map(e => getErrorKey(e)),
113| )
114|
115| // Only add errors that don't already exist
116| const uniqueNewErrors = newErrors.filter(
117| error => !existingKeys.has(getErrorKey(error)),
118| )
119|
120| if (uniqueNewErrors.length === 0) {
源码目录与关联文件
强关联:services/mcp/config.ts、tools/MCPTool/、utils/mcpValidation.ts、services/mcp/auth.ts(ClaudeAuthProvider)。点击 client.ts 跳回本章源码块。
动手练习
- 在 mcp.json 添加 stdio server,观察 connect 日志中的 transport 计数
- 故意让 OAuth token 过期,确认 client status 变为 needs-auth 且出现 McpAuthTool
- 对比
mcpToolInputToAutoClassifierInput输出与 auto_mode 分类器 deny 文案 - 触发 tool list changed notification,验证 fetchToolsForClient cache 是否 invalidate
本章小结与延伸
client.ts = MCP 运行时。下一章 compact,理解 MCP instructions delta 在压缩后如何 re-inject。 继续学习: