当前位置: 首页 > news >正文

解密Anthropic的MCP Inspector:从协议调试到AI应用开发的全栈架构之旅

在AI应用开发的浪潮中,Model Context Protocol(MCP)就像一座连接AI模型与外部世界的桥梁。而MCP Inspector,则是这座桥梁的"质检员"和"设计师"。本文将带你深入这个由Anthropic开源的开发者工具,从架构设计到代码实现,揭开一个专业级调试工具背后的技术秘密。


一、开篇:当AI遇上工具链——开发者的痛点与机遇

1.1 从一个真实场景说起

想象一下,你正在开发一个AI助手,它需要访问文件系统、调用API、操作数据库。传统做法是什么?硬编码?写一堆if-else?还是为每个功能写一个专用接口?这就像用螺丝刀拧螺丝——能用,但效率低下且容易出错。

这时候,Model Context Protocol(MCP) 横空出世了。它提供了一套标准化的协议,让AI模型能够以统一的方式访问各种资源(Resources)、调用工具(Tools)、使用提示词模板(Prompts)。听起来很美好,对吧?

但问题来了:当你实现了一个MCP服务器,如何确保它能正常工作? 如何调试那些看不见摸不着的JSON-RPC消息?如何验证OAuth认证流程?如何测试工具调用的参数是否正确?

这就是MCP Inspector诞生的理由——它不仅是一个调试工具,更是一个完整的开发环境,一个让MCP协议"可视化"的魔法棒。

1.2 MCP Inspector到底是什么?

用最简单的话说,MCP Inspector就像是Chrome DevTools之于Web开发,Postman之于API测试。它提供了:

  • 可视化界面:将抽象的协议交互变成直观的UI操作

  • 实时调试:查看每一条JSON-RPC消息的往返

  • 协议兼容性测试:支持stdio、SSE、Streamable HTTP三种传输方式

  • OAuth流程调试:完整的OAuth 2.0认证流程可视化

  • 配置导出:一键生成可用于生产环境的配置文件

更重要的是,它的架构设计堪称教科书级别——Monorepo管理、前后端分离、代理模式、类型安全、安全防护……每一个细节都值得深入学习。


二、技术全景图:从30000英尺俯瞰架构设计

2.1 Monorepo架构:为什么选择"大仓库"?

打开项目的package.json,你会发现这样的配置:

{"workspaces": ["client","server","cli"]
}

这是典型的Monorepo(单仓库)架构。为什么不用传统的多仓库呢?

技术原因

  1. 依赖共享:三个子项目都依赖@modelcontextprotocol/sdk,Monorepo避免了版本不一致的噩梦

  2. 原子化提交:前后端API变更可以在同一个commit中完成,保证一致性

  3. 统一工具链:共享TypeScript配置、Prettier格式化、测试框架

工程原因

  1. 降低认知负担:开发者只需clone一个仓库

  2. 简化CI/CD:统一的构建和发布流程

  3. 代码复用:类型定义可以在workspaces间共享

看看项目结构:

inspector/
├── client/          # React前端 (MCPI - MCP Inspector Client)
│   ├── src/
│   │   ├── components/  # UI组件
│   │   ├── lib/         # 核心逻辑
│   │   └── utils/       # 工具函数
│   └── dist/        # 构建产物
├── server/          # Express后端 (MCPP - MCP Proxy)
│   └── src/
│       ├── index.ts     # 主服务器
│       └── mcpProxy.ts  # 代理逻辑
├── cli/             # 命令行工具
│   └── src/
│       └── cli.ts       # CLI入口
└── package.json     # 根配置

这种组织方式让人一眼就能看清项目的"骨架"。

2.2 双端架构:浏览器与协议的巧妙桥接

MCP Inspector面临一个有趣的挑战:浏览器无法直接与stdio进程通信

stdio是什么?它是标准输入输出流,很多MCP服务器通过stdin/stdout与客户端通信。但浏览器运行在沙箱环境中,根本无法spawn子进程!

解决方案:代理模式(Proxy Pattern)

┌─────────────┐         ┌─────────────┐         ┌─────────────┐
│   Browser   │  HTTP   │  MCP Proxy  │  stdio  │ MCP Server  │
│   (MCPI)    │ ◄─────► │   (MCPP)    │ ◄─────► │             │
└─────────────┘         └─────────────┘         └─────────────┘Port 6274               Port 6277

MCPI(前端)

  • 运行在浏览器中

  • 使用React + TypeScript + Vite

  • 通过HTTP与代理服务器通信

MCPP(后端)

  • 运行在Node.js中

  • 使用Express框架

  • 既是HTTP服务器(面向浏览器),也是MCP客户端(面向MCP服务器)

这种设计的巧妙之处在于:

  1. 协议转换:将浏览器的HTTP请求转换为MCP协议的JSON-RPC消息

  2. 传输适配:支持stdio、SSE、Streamable HTTP三种传输方式

  3. 会话管理:通过sessionId维护多个并发连接

2.3 三种传输协议:各有千秋的通信方式

MCP协议支持三种传输方式,MCP Inspector全部支持:

2.3.1 Stdio传输(Standard Input/Output)

适用场景:本地开发、命令行工具

// server/src/index.ts
const transportToServer = new StdioClientTransport({command: "node",args: ["build/index.js"],env: { API_KEY: "xxx" }
});

优点

  • 简单直接,无需网络配置

  • 天然支持进程隔离

  • 适合快速原型开发

缺点

  • 浏览器无法直接使用(需要代理)

  • 跨网络通信困难

2.3.2 SSE传输(Server-Sent Events)

适用场景:服务器推送、实时更新

// client/src/lib/hooks/useConnection.ts
const transport = new SSEClientTransport(new URL(sseUrl),{headers: {'Authorization': `Bearer ${token}`}}
);

优点

  • 浏览器原生支持

  • 单向推送高效

  • 自动重连机制

缺点

  • 仅支持服务器到客户端的推送

  • 需要额外的POST端点处理客户端请求

2.3.3 Streamable HTTP传输

适用场景:双向流式通信

const transport = new StreamableHTTPClientTransport(new URL(url),{headers: customHeaders}
);

优点

  • 支持双向流

  • 更好的错误处理

  • 可以设置自定义headers

缺点

  • 相对复杂

  • 需要服务器端支持

2.4 技术栈总览:现代化的工具选择

前端技术栈
{"核心框架": "React 18 + TypeScript","构建工具": "Vite 6.0","UI组件": "Radix UI + Tailwind CSS","状态管理": "React Hooks","表单处理": "Zod + JSON Schema","代码规范": "ESLint + Prettier","测试框架": "Jest + Playwright"
}

为什么选Vite?

  • 极速的冷启动(基于ESM)

  • 热模块替换(HMR)比Webpack快10倍以上

  • 开箱即用的TypeScript支持

为什么选Tailwind?

  • Utility-first理念减少CSS命名困扰

  • 响应式设计简单直观

  • 生产环境自动PurgeCSS

后端技术栈
{"运行时": "Node.js 22.7.5+","Web框架": "Express","MCP SDK": "@modelcontextprotocol/sdk 1.18.0","进程管理": "spawn-rx","验证库": "Zod","类型系统": "TypeScript"
}

三、核心架构深度解析:魔鬼在细节中

3.1 代理服务器:双面间谍的精妙设计

MCP Proxy是整个系统的核心枢纽。它同时扮演两个角色:

// server/src/mcpProxy.ts
export default function mcpProxy({transportToClient,   // 面向浏览器的传输层transportToServer,   // 面向MCP服务器的传输层
}: {transportToClient: Transport;transportToServer: Transport;
}) {// 转发客户端消息到服务器transportToClient.onmessage = (message) => {transportToServer.send(message).catch((error) => {// 错误处理:返回标准的JSON-RPC错误响应if (isJSONRPCRequest(message)) {const errorResponse = {jsonrpc: "2.0" as const,id: message.id,error: {code: -32001,message: error.message,data: error,},};transportToClient.send(errorResponse).catch(onClientError);}});};// 转发服务器消息到客户端transportToServer.onmessage = (message) => {transportToClient.send(message).catch(onClientError);};// 连接关闭时的清理逻辑transportToClient.onclose = () => {transportToServer.close().catch(onServerError);};
}

设计亮点

  1. 双向消息转发:像一个忠实的信使,原封不动地传递消息

  2. 错误边界:捕获并转换错误为标准JSON-RPC格式

  3. 生命周期管理:任一端关闭,自动清理另一端

  4. 会话隔离:通过Map结构维护多个独立会话

const webAppTransports = new Map<string, Transport>();
const serverTransports = new Map<string, Transport>();

3.2 安全设计:不仅仅是功能实现

安全性在MCP Inspector中被认真对待。让我们看看几个关键的安全措施:

3.2.1 Token认证机制
// 生成随机session token
const sessionToken = process.env.MCP_PROXY_AUTH_TOKEN || randomBytes(32).toString('hex');// 认证中间件
const authMiddleware = (req, res, next) => {if (authDisabled) return next();const authHeader = req.headers['authorization'];if (!authHeader?.startsWith('Bearer ')) {return res.status(401).json({error: 'Unauthorized',message: 'Missing or invalid authorization header'});}const token = authHeader.substring(7);// 使用时间安全比较防止时序攻击if (!timingSafeEqual(Buffer.from(token), Buffer.from(sessionToken))) {return res.status(401).json({error: 'Unauthorized',message: 'Invalid session token'});}next();
};

安全要点

  • 使用crypto.randomBytes生成高熵token

  • timingSafeEqual防止时序攻击(Timing Attack)

  • 默认启用认证,除非显式设置DANGEROUSLY_OMIT_AUTH

3.2.2 Origin验证防DNS重绑定
const originValidationMiddleware = (req, res, next) => {const origin = req.headers.origin;const clientPort = process.env.CLIENT_PORT || "6274";const defaultOrigin = `http://localhost:${clientPort}`;const allowedOrigins = process.env.ALLOWED_ORIGINS?.split(",") || [defaultOrigin];if (origin && !allowedOrigins.includes(origin)) {return res.status(403).json({error: "Forbidden - invalid origin",message: "Request blocked to prevent DNS rebinding attacks"});}next();
};

DNS重绑定攻击是什么? 假设攻击者注册域名evil.com,先返回127.0.0.1让浏览器通过CORS检查,然后快速切换DNS到192.168.1.100访问内网服务。Origin验证就是防止这种攻击。

3.2.3 自定义Headers的安全处理
const getHttpHeaders = (req: express.Request): Record<string, string> => {const headers: Record<string, string> = {};for (const key in req.headers) {const lowerKey = key.toLowerCase();// 只转发特定前缀的headersif (lowerKey.startsWith("mcp-") ||lowerKey === "authorization" ||lowerKey === "last-event-id") {// 排除代理自身的认证headerif (lowerKey !== "x-mcp-proxy-auth" && lowerKey !== "mcp-session-id") {headers[key] = req.headers[key];}}}return headers;
};

这种白名单策略确保只有必要的headers被转发,避免信息泄露。

3.3 OAuth 2.0完整实现:从元数据发现到Token刷新

OAuth认证是MCP Inspector的重头戏之一。它不仅实现了标准流程,还提供了调试模式。

3.3.1 状态机设计
// client/src/lib/oauth-state-machine.ts
export const oauthTransitions: Record<OAuthStep, StateTransition> = {metadata_discovery: {canTransition: async () => true,execute: async (context) => {// 1. 发现资源服务器元数据const resourceMetadata = await discoverOAuthProtectedResourceMetadata(context.serverUrl);// 2. 确定授权服务器URLconst authServerUrl = resourceMetadata?.authorization_servers?.[0]? new URL(resourceMetadata.authorization_servers[0]): new URL("/", context.serverUrl);// 3. 发现授权服务器元数据const metadata = await discoverAuthorizationServerMetadata(authServerUrl);context.updateState({resourceMetadata,oauthMetadata: metadata,oauthStep: "client_registration"});}},client_registration: {canTransition: async (context) => !!context.state.oauthMetadata,execute: async (context) => {// 支持静态注册和动态客户端注册(DCR)let clientInfo = await context.provider.clientInformation();if (!clientInfo) {// 动态注册clientInfo = await registerClient(context.serverUrl, {metadata: context.state.oauthMetadata,clientMetadata: context.provider.clientMetadata});context.provider.saveClientInformation(clientInfo);}context.updateState({oauthClientInfo: clientInfo,oauthStep: "authorization_redirect"});}},authorization_redirect: {execute: async (context) => {const { authorizationUrl, codeVerifier } = await startAuthorization(context.serverUrl, {metadata: context.state.oauthMetadata,clientInformation: context.state.oauthClientInfo,redirectUrl: context.provider.redirectUrl,scope: await discoverScopes(context.serverUrl),state: generateOAuthState()});context.provider.saveCodeVerifier(codeVerifier);context.updateState({authorizationUrl,oauthStep: "authorization_code"});}},authorization_code: {execute: async (context) => {// 等待用户授权并获取code...}},token_exchange: {execute: async (context) => {const tokens = await exchangeAuthorization({authorizationCode: context.state.authorizationCode,codeVerifier: context.provider.getCodeVerifier(),// ...});context.updateState({tokens,oauthStep: "completed"});}}
};

状态机优势

  1. 清晰的流程控制:每个步骤的前置条件和执行逻辑分离

  2. 可测试性:每个状态转换可以独立测试

  3. 可视化调试:UI可以实时展示当前所在步骤

3.3.2 PKCE(Proof Key for Code Exchange)实现
export class InspectorOAuthClientProvider implements OAuthClientProvider {async startAuthorization() {// 生成code_verifier和code_challengeconst codeVerifier = generateCodeVerifier();const codeChallenge = await generateCodeChallenge(codeVerifier);const authUrl = new URL(metadata.authorization_endpoint);authUrl.searchParams.set('response_type', 'code');authUrl.searchParams.set('client_id', clientInformation.client_id);authUrl.searchParams.set('redirect_uri', this.redirectUrl);authUrl.searchParams.set('code_challenge', codeChallenge);authUrl.searchParams.set('code_challenge_method', 'S256');// 保存code_verifier供后续token交换使用this.saveCodeVerifier(codeVerifier);return { authorizationUrl: authUrl.toString(), codeVerifier };}
}

PKCE为什么重要?

  • 防止授权码拦截攻击

  • 适用于无法安全存储client_secret的场景(如SPA应用)

  • 使用SHA256哈希确保安全性

3.3.3 调试模式:OAuth流程可视化
// client/src/components/AuthDebugger.tsx
export default function AuthDebugger({authState,onStateChange,onClose
}: AuthDebuggerProps) {return (<div className="oauth-debugger">{/* 步骤指示器 */}<StepIndicator currentStep={authState.oauthStep} />{/* 元数据展示 */}{authState.oauthMetadata && (<JsonView data={authState.oauthMetadata} />)}{/* 授权URL */}{authState.authorizationUrl && (<div><Button onClick={() => window.open(authState.authorizationUrl)}>打开授权页面</Button><Input placeholder="粘贴授权码"onChange={(e) => onStateChange({authorizationCode: e.target.value})}/></div>)}{/* Token信息 */}{authState.tokens && (<div><h3>Access Token</h3><code>{authState.tokens.access_token}</code>{authState.tokens.refresh_token && (<><h3>Refresh Token</h3><code>{authState.tokens.refresh_token}</code></>)}</div>)}</div>);
}

这个调试器让开发者能够:

  • 查看每一步的元数据

  • 手动粘贴授权码测试

  • 检查Token的有效性

  • 导出配置用于其他客户端

3.4 动态表单生成:从JSON Schema到UI组件

MCP协议中,工具(Tools)的参数由JSON Schema定义。MCP Inspector需要根据Schema动态生成表单。这是一个经典的元编程问题。

3.4.1 Schema解析与默认值生成
// client/src/utils/schemaUtils.ts
export const generateDefaultValue = (schema: JsonSchemaType,key: string,parentSchema?: JsonSchemaType
): JsonValue => {// 1. 优先使用schema定义的defaultif (schema.default !== undefined) {return schema.default;}// 2. 根据type生成合理的默认值switch (schema.type) {case "string":return schema.enum ? schema.enum[0] : "";case "number":case "integer":return schema.minimum ?? 0;case "boolean":return false;case "array":// 数组默认为空,除非有minItems约束return schema.minItems ? Array(schema.minItems).fill(generateDefaultValue(schema.items, key)) : [];case "object":// 递归生成嵌套对象的默认值const obj: Record<string, JsonValue> = {};for (const [propKey, propSchema] of Object.entries(schema.properties ?? {})) {if (isPropertyRequired(propKey, schema)) {obj[propKey] = generateDefaultValue(propSchema, propKey, schema);}}return obj;default:return null;}
};
3.4.2 动态表单组件
// client/src/components/DynamicJsonForm.tsx
const DynamicJsonForm = forwardRef<DynamicJsonFormRef, DynamicJsonFormProps>(({ schema, value, onChange, maxDepth = 3 }, ref) => {// 判断是否可以用表单,还是只能用JSON编辑器const isOnlyJSON = !isSimpleObject(schema);const [isJsonMode, setIsJsonMode] = useState(isOnlyJSON);// 验证逻辑useImperativeHandle(ref, () => ({validateJson: () => {try {const parsed = JSON.parse(rawJsonValue);// 这里可以集成Zod或Ajv进行Schema验证return { isValid: true, error: null };} catch (error) {return { isValid: false, error: error.message };}}}));// 根据schema.type渲染不同的输入组件const renderField = (fieldSchema: JsonSchemaType, path: string[]) => {switch (fieldSchema.type) {case "string":if (fieldSchema.enum) {// 枚举类型用下拉选择return (<Select value={getValueAtPath(value, path)}onValueChange={(v) => onChange(updateValueAtPath(value, path, v))}>{fieldSchema.enum.map(option => (<SelectItem key={option} value={option}>{option}</SelectItem>))}</Select>);}// 普通字符串用Inputreturn (<Inputvalue={getValueAtPath(value, path) as string}onChange={(e) => onChange(updateValueAtPath(value, path, e.target.value))}placeholder={fieldSchema.description}/>);case "number":case "integer":return (<Inputtype="number"value={getValueAtPath(value, path)}onChange={(e) => onChange(updateValueAtPath(value, path, Number(e.target.value)))}min={fieldSchema.minimum}max={fieldSchema.maximum}/>);case "boolean":return (<Checkboxchecked={getValueAtPath(value, path) as boolean}onCheckedChange={(checked) => onChange(updateValueAtPath(value, path, checked))}/>);case "array":return (<ArrayFielditems={getValueAtPath(value, path) as JsonValue[]}itemSchema={fieldSchema.items}onAdd={() => {const current = getValueAtPath(value, path) as JsonValue[];onChange(updateValueAtPath(value, path, [...current,generateDefaultValue(fieldSchema.items, "")]));}}onRemove={(index) => {const current = getValueAtPath(value, path) as JsonValue[];onChange(updateValueAtPath(value, path, current.filter((_, i) => i !== index)));}}/>);case "object":return (<div className="nested-object">{Object.entries(fieldSchema.properties ?? {}).map(([key, propSchema]) => (<div key={key} className="form-field"><Label>{key}</Label>{renderField(propSchema, [...path, key])}</div>))}</div>);default:// 复杂类型降级到JSON编辑器return (<JsonEditorvalue={JSON.stringify(getValueAtPath(value, path), null, 2)}onChange={(json) => {try {const parsed = JSON.parse(json);onChange(updateValueAtPath(value, path, parsed));} catch {// 解析失败时保持原值}}}/>);}};return (<div className="dynamic-form">{/* 表单/JSON切换 */}{!isOnlyJSON && (<div className="mode-toggle"><Button variant={!isJsonMode ? "default" : "outline"}onClick={() => setIsJsonMode(false)}>表单模式</Button><Button variant={isJsonMode ? "default" : "outline"}onClick={() => setIsJsonMode(true)}>JSON模式</Button></div>)}{/* 渲染表单或JSON编辑器 */}{isJsonMode ? (<JsonEditorvalue={rawJsonValue}onChange={setRawJsonValue}/>) : (renderField(schema, []))}</div>);}
);

设计亮点

  1. 智能降级:复杂类型自动切换到JSON编辑器

  2. 双向绑定:表单变更实时同步到JSON

  3. 防抖优化:JSON编辑时使用debounce避免频繁解析

  4. 递归渲染:支持任意深度的嵌套对象

  5. 类型安全:全程TypeScript类型检查

3.5 连接管理:useConnection Hook的精妙设计

在React中管理WebSocket、SSE等长连接是个挑战。MCP Inspector使用自定义Hook封装了所有复杂性。

// client/src/lib/hooks/useConnection.ts
export function useConnection({transportType,command,args,sseUrl,env,customHeaders,connectionType = "proxy",onNotification,onPendingRequest,
}: UseConnectionOptions) {const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>("disconnected");const [mcpClient, setMcpClient] = useState<Client | null>(null);const [serverCapabilities, setServerCapabilities] = useState<ServerCapabilities | null>(null);const { toast } = useToast();// 核心:建立连接const connect = useCallback(async () => {setConnectionStatus("connecting");try {// 1. 创建传输层let transport: Transport;if (connectionType === "direct") {// 直连模式:浏览器直接连接SSE/HTTP服务器if (transportType === "sse") {transport = new SSEClientTransport(new URL(sseUrl),{headers: buildHeaders(customHeaders),eventSourceOptions: {withCredentials: true}});} else if (transportType === "streamable-http") {transport = new StreamableHTTPClientTransport(new URL(sseUrl),{ headers: buildHeaders(customHeaders) });}} else {// 代理模式:通过MCP Proxy连接const proxyUrl = getMCPProxyAddress();const authToken = getMCPProxyAuthToken();// 构建代理请求transport = new StreamableHTTPClientTransport(new URL(`${proxyUrl}/mcp`),{headers: {'Authorization': `Bearer ${authToken}`,'x-mcp-proxy-auth': authToken,// 传递MCP服务器配置'x-mcp-command': command,'x-mcp-args': JSON.stringify(args.split(' ')),'x-mcp-env': JSON.stringify(env),'x-mcp-transport': transportType,}});}// 2. 创建MCP客户端const client = new Client({name: "mcp-inspector",version: "1.0.0"},{capabilities: {sampling: {},  // 支持采样roots: { listChanged: true },  // 支持根目录变更通知}});// 3. 设置通知处理器client.setNotificationHandler((notification) => {// 资源更新通知if (ResourceUpdatedNotificationSchema.safeParse(notification).success) {onNotification?.({type: "resource_updated",data: notification.params});}// 日志通知if (LoggingMessageNotificationSchema.safeParse(notification).success) {console.log(`[MCP Server] ${notification.params.data}`);}// 进度通知if (notification.method === "notifications/progress") {// 更新UI进度条...}});// 4. 设置请求处理器(处理服务器主动请求)client.setRequestHandler(async (request) => {// 采样请求(服务器请求客户端生成内容)if (CreateMessageRequestSchema.safeParse(request).success) {return new Promise((resolve, reject) => {onPendingRequest?.(request, resolve, reject);});}// 根目录列表请求if (ListRootsRequestSchema.safeParse(request).success) {return { roots: getRoots?.() ?? [] };}throw new Error(`Unsupported request: ${request.method}`);});// 5. 连接到传输层await client.connect(transport);// 6. 获取服务器能力const capabilities = await client.getServerCapabilities();setMcpClient(client);setServerCapabilities(capabilities);setConnectionStatus("connected");toast({title: "连接成功",description: `已连接到MCP服务器`});} catch (error) {setConnectionStatus("error");toast({title: "连接失败",description: error.message,variant: "destructive"});}}, [transportType, command, args, sseUrl, env, customHeaders]);// 断开连接const disconnect = useCallback(async () => {if (mcpClient) {await mcpClient.close();setMcpClient(null);setConnectionStatus("disconnected");}}, [mcpClient]);// 发送请求的通用方法const sendRequest = useCallback(async <T extends Result>(method: string,params?: object,options?: RequestOptions): Promise<T> => {if (!mcpClient) {throw new Error("Not connected");}const timeout = options?.timeout ?? getMCPServerRequestTimeout(config);const shouldResetOnProgress = resetRequestTimeoutOnProgress(config);try {const response = await mcpClient.request({ method, params } as ClientRequest,{ timeout,onProgress: shouldResetOnProgress ? () => { /* 重置超时计时器 */ } : undefined});return response as T;} catch (error) {if (error instanceof McpError) {toast({title: `错误 ${error.code}`,description: error.message,variant: "destructive"});}throw error;}}, [mcpClient, config]);// 自动重连useEffect(() => {let reconnectTimer: NodeJS.Timeout;if (connectionStatus === "error" && config.autoReconnect) {reconnectTimer = setTimeout(() => {connect();}, 5000);}return () => clearTimeout(reconnectTimer);}, [connectionStatus, config.autoReconnect, connect]);return {connectionStatus,serverCapabilities,connect,disconnect,sendRequest,// 便捷方法listResources: () => sendRequest("resources/list"),readResource: (uri: string) => sendRequest("resources/read", { uri }),listTools: () => sendRequest("tools/list"),callTool: (name: string, arguments: Record<string, unknown>) =>sendRequest("tools/call", { name, arguments }),listPrompts: () => sendRequest("prompts/list"),getPrompt: (name: string, arguments: Record<string, string>) =>sendRequest("prompts/get", { name, arguments }),};
}

Hook设计的精髓

  1. 状态封装:连接状态、客户端实例、能力信息全部内部管理

  2. 错误处理:统一的错误Toast提示

  3. 超时管理:支持配置超时时间和进度重置

  4. 自动重连:错误后自动重试

  5. 类型安全:泛型确保请求/响应类型匹配


四、实战应用场景:从开发到生产

4.1 场景一:本地MCP服务器开发

需求:你正在开发一个文件系统MCP服务器,需要测试各种边界情况。

步骤

# 1. 启动Inspector
npx @modelcontextprotocol/inspector node ./my-server/index.js# 2. Inspector自动打开浏览器
# http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=xxxxx# 3. 在UI中测试
# - 点击"Resources"标签页
# - 点击"List Resources"查看文件列表
# - 选择一个文件,点击"Read"查看内容
# - 在"Tools"标签页测试文件操作工具

调试技巧

  1. 查看原始消息:在Console标签页可以看到所有JSON-RPC消息

  2. 测试错误处理:故意传入错误参数,观察错误响应格式

  3. 性能分析:通过History查看每个请求的耗时

4.2 场景二:OAuth集成测试

需求:你的MCP服务器需要访问Google Drive API,使用OAuth认证。

配置OAuth

// 在Inspector UI中配置
{"transportType": "sse","sseUrl": "https://api.example.com/mcp","oauthClientId": "your-client-id.apps.googleusercontent.com","oauthScope": "https://www.googleapis.com/auth/drive.readonly"
}

调试流程

  1. 点击"Connect",Inspector自动发起OAuth流程

  2. 在"Auth Debugger"中查看:
    • Authorization Server元数据

    • Client注册信息

    • Authorization URL

  3. 点击"Open Authorization URL"完成授权

  4. 粘贴返回的Authorization Code

  5. Inspector自动完成Token交换

  6. 查看获得的Access Token和Refresh Token

导出配置

// 点击"Export Config"获取
{"mcpServers": {"google-drive": {"type": "sse","url": "https://api.example.com/mcp","oauth": {"client_id": "your-client-id","scope": "https://www.googleapis.com/auth/drive.readonly"}}}
}

4.3 场景三:多传输协议兼容性测试

需求:你的MCP服务器需要同时支持stdio、SSE、Streamable HTTP三种传输。

测试矩阵

传输方式测试项Inspector配置
stdio基本连接command: "node", args: ["server.js"]
stdio环境变量env: { API_KEY: "test" }
SSE基本连接url: "http://localhost:3000/sse"
SSEBearer Tokenheaders: { Authorization: "Bearer xxx" }
SSE自动重连断开服务器观察重连
HTTP双向流url: "http://localhost:3000/mcp"
HTTP自定义Headersheaders: { X-API-Key: "xxx" }

自动化测试脚本

# 使用CLI模式进行自动化测试
npx @modelcontextprotocol/inspector \--cli \--transport stdio \node server.js \<< EOF
{"method": "tools/list"
}
{"method": "tools/call","params": {"name": "read_file","arguments": { "path": "/tmp/test.txt" }}
}
EOF

4.4 场景四:生产环境配置导出

需求:在Inspector中调试完成,需要部署到Cursor、Claude Desktop等客户端。

一键导出

// Inspector自动生成的配置
{"mcpServers": {"my-server": {// stdio传输"command": "node","args": ["/path/to/server/index.js","--log-level", "info"],"env": {"DATABASE_URL": "postgresql://localhost/mydb","API_KEY": "${API_KEY}",  // 支持环境变量引用"DEBUG": "false"}},"remote-server": {// SSE传输"type": "sse","url": "https://api.example.com/mcp/events","headers": {"Authorization": "Bearer ${REMOTE_API_TOKEN}"}}}
}

部署到Cursor

  1. 复制"Servers File"的内容

  2. 保存到~/.cursor/mcp.json(macOS/Linux)或%APPDATA%\.cursor\mcp.json(Windows)

  3. 重启Cursor

  4. MCP服务器自动加载


五、开发最佳实践:从源码中学到的经验

5.1 TypeScript类型安全的极致应用

MCP Inspector几乎没有使用any类型,所有数据流都有严格的类型定义。

示例:JSON Schema到TypeScript类型的映射

// client/src/utils/jsonUtils.ts
export type JsonSchemaType = {type?: "string" | "number" | "integer" | "boolean" | "object" | "array" | "null" | (string | "string" | "number" | "boolean")[];properties?: Record<string, JsonSchemaType>;items?: JsonSchemaType;required?: string[];enum?: JsonValue[];default?: JsonValue;minimum?: number;maximum?: number;minItems?: number;maxItems?: number;description?: string;
};export type JsonValue =| string| number| boolean| null| JsonValue[]| { [key: string]: JsonValue };// 类型守卫
export function isJsonObject(value: JsonValue
): value is Record<string, JsonValue> {return typeof value === "object" && value !== null && !Array.isArray(value);
}

收益

  • 编译时发现90%的错误

  • IDE智能提示完美

  • 重构时自动发现破坏性变更

5.2 错误处理的层次化设计

// 1. Transport层错误
transport.onerror = (error: Error) => {if (error instanceof SseError) {// SSE特定错误if (error.code === 401) {showAuthError();}}
};// 2. Protocol层错误
try {const result = await client.request(request);
} catch (error) {if (error instanceof McpError) {// MCP协议错误switch (error.code) {case ErrorCode.MethodNotFound:toast({ title: "服务器不支持此方法" });break;case ErrorCode.InvalidParams:toast({ title: "参数验证失败" });break;}}
}// 3. Application层错误
const handleToolCall = async (name: string, params: unknown) => {try {setIsLoading(true);const result = await callTool(name, params);setToolResult(result);} catch (error) {// 用户友好的错误提示setError(`工具调用失败: ${error.message}`);} finally {setIsLoading(false);}
};

原则

  • 在合适的层次捕获错误

  • 给用户有意义的错误提示

  • 记录详细的错误日志供调试

5.3 性能优化技巧

5.3.1 React渲染优化
// 使用memo避免不必要的重渲染
const ToolCard = memo(({ tool, onSelect }: ToolCardProps) => {return (<div onClick={() => onSelect(tool)}><h3>{tool.name}</h3><p>{tool.description}</p></div>);
}, (prev, next) => {// 自定义比较逻辑return prev.tool.name === next.tool.name &&prev.tool.description === next.tool.description;
});// 使用useCallback缓存回调
const handleToolSelect = useCallback((tool: Tool) => {setSelectedTool(tool);// 预加载工具的输入SchemaprefetchToolSchema(tool.inputSchema);
}, []);// 虚拟滚动处理大列表
import { FixedSizeList } from 'react-window';<FixedSizeListheight={600}itemCount={tools.length}itemSize={80}width="100%"
>{({ index, style }) => (<div style={style}><ToolCard tool={tools[index]} /></div>)}
</FixedSizeList>
5.3.2 网络请求优化
// 请求去重
const pendingRequests = new Map<string, Promise<any>>();async function fetchWithDedup<T>(key: string, fetcher: () => Promise<T>): Promise<T> {if (pendingRequests.has(key)) {return pendingRequests.get(key)!;}const promise = fetcher().finally(() => {pendingRequests.delete(key);});pendingRequests.set(key, promise);return promise;
}// 使用示例
const resources = await fetchWithDedup("resources/list",() => client.request({ method: "resources/list" })
);
5.3.3 JSON解析优化
// 使用Web Worker进行大JSON解析
const jsonWorker = new Worker('json-worker.js');function parseLargeJSON(jsonString: string): Promise<any> {return new Promise((resolve, reject) => {jsonWorker.postMessage({ json: jsonString });jsonWorker.onmessage = (e) => {if (e.data.error) {reject(new Error(e.data.error));} else {resolve(e.data.result);}};});
}

5.4 安全开发清单

基于MCP Inspector的安全实践,总结出以下清单:

  • [ ] 输入验证:所有用户输入使用Zod验证

  • [ ] 输出编码:JSON输出使用JSON.stringify防止注入

  • [ ] 认证:默认启用Token认证

  • [ ] 授权:检查每个请求的权限

  • [ ] CORS:配置正确的Origin白名单

  • [ ] CSP:设置Content-Security-Policy头

  • [ ] Rate Limiting:限制请求频率

  • [ ] 日志脱敏:不记录敏感信息(Token、密码)

  • [ ] 依赖扫描:定期运行npm audit

  • [ ] HTTPS:生产环境强制HTTPS

六、架构演进与技术选型思考

6.1 为什么选择React而不是Vue/Svelte?

这个问题在开源项目中经常被问到。MCP Inspector选择React的原因:

生态成熟度

  • Radix UI提供无障碍访问(Accessibility)的组件基础

  • React DevTools调试体验优秀

  • 大量现成的Hooks库(如react-windowreact-hook-form

团队熟悉度

  • Anthropic团队可能更熟悉React生态

  • 社区贡献者门槛更低

类型支持

  • React 18 + TypeScript的类型推导非常完善

  • JSX/TSX的类型检查比模板语法更严格

但如果是你的项目,选择框架时应考虑:

  • 项目规模:小型工具Svelte更轻量

  • 团队技能:选择团队最熟悉的

  • 性能要求:Svelte编译时优化更激进

  • 生态需求:需要特定库时优先考虑其生态

6.2 Vite vs Webpack:构建工具的代际差异

MCP Inspector使用Vite而非Webpack,对比一下:

// Vite配置 (vite.config.ts)
export default defineConfig({plugins: [react()],build: {rollupOptions: {output: {manualChunks: {'vendor': ['react', 'react-dom'],'mcp-sdk': ['@modelcontextprotocol/sdk']}}}},server: {port: 6274,proxy: {'/mcp': 'http://localhost:6277'}}
});

Vite的优势

  • 开发服务器启动:Webpack需要10-30秒,Vite只需1-2秒

  • 热更新速度:Webpack全量重编译,Vite按需编译

  • 配置简洁:默认配置即可用,Webpack需要大量配置

Webpack的优势

  • 生态成熟:各种loader和plugin更丰富

  • 构建优化:对复杂场景的优化更细致

  • 兼容性:支持更老的浏览器

选择建议

  • 新项目优先Vite

  • 需要兼容IE的项目用Webpack

  • 已有Webpack项目迁移成本高,谨慎切换

6.3 Monorepo管理:npm workspaces vs Lerna vs Turborepo

MCP Inspector使用npm原生workspaces,为什么不用Lerna或Turborepo?

npm workspaces

{"workspaces": ["client", "server", "cli"],"scripts": {"build": "npm run build-server && npm run build-client && npm run build-cli","dev": "concurrently \"npm run dev-server\" \"npm run dev-client\""}
}

Lerna

{"packages": ["packages/*"],"version": "independent","command": {"publish": {"conventionalCommits": true}}
}

Turborepo

{"pipeline": {"build": {"dependsOn": ["^build"],"outputs": ["dist/**"]},"dev": {"cache": false}}
}

对比总结

工具适用场景优势劣势
npm workspaces简单Monorepo零配置、原生支持功能较少
Lerna需要版本管理独立版本、发布流程配置复杂
Turborepo大型Monorepo增量构建、任务缓存学习曲线

MCP Inspector选择npm workspaces因为:

  1. 项目规模适中(3个packages)

  2. 不需要独立版本管理

  3. 构建速度足够快,不需要缓存

6.4 状态管理:为什么不用Redux/Zustand?

细心的读者会发现,MCP Inspector没有使用任何状态管理库,全部用React Hooks。

原因分析

// 状态都在App.tsx中管理
const App = () => {const [resources, setResources] = useState<Resource[]>([]);const [tools, setTools] = useState<Tool[]>([]);const [prompts, setPrompts] = useState<Prompt[]>([]);// 通过props传递给子组件return (<><ResourcesTab resources={resources}setResources={setResources}/><ToolsTabtools={tools}setTools={setTools}/></>);
};

这样做的优势

  1. 简单直观:状态在哪里一目了然

  2. 类型安全:TypeScript直接推导

  3. 无额外依赖:减少bundle大小

  4. 调试友好:React DevTools直接看到state

何时需要状态管理库

  • 状态需要跨多个无关组件共享

  • 需要时间旅行调试(Time Travel Debugging)

  • 状态变更逻辑复杂,需要中间件

  • 团队习惯特定的状态管理模式

教训

不要过度设计!很多项目一开始就引入Redux,结果90%的状态只在一个组件内使用。先用useState,痛了再重构。


七、踩坑记录与解决方案

7.1 跨域问题:CORS配置的正确姿势

问题:浏览器访问http://localhost:6274,请求http://localhost:6277/mcp时报CORS错误。

错误配置

// ❌ 错误:允许所有来源
app.use(cors());

正确配置

// ✅ 正确:只允许特定来源
app.use(cors({origin: (origin, callback) => {const allowedOrigins = ['http://localhost:6274',process.env.CLIENT_URL].filter(Boolean);if (!origin || allowedOrigins.includes(origin)) {callback(null, true);} else {callback(new Error('Not allowed by CORS'));}},credentials: true,exposedHeaders: ['mcp-session-id']
}));

关键点

  • credentials: true允许携带Cookie

  • exposedHeaders让浏览器能读取自定义header

  • 开发环境允许null origin(Electron等场景)

7.2 WebSocket连接不稳定

问题:SSE连接经常断开,尤其是移动网络。

解决方案:实现指数退避重连

class ReconnectingSSE {private reconnectDelay = 1000;  // 初始1秒private maxDelay = 60000;       // 最大60秒private reconnectAttempts = 0;connect() {this.eventSource = new EventSource(this.url);this.eventSource.onerror = () => {this.eventSource.close();// 指数退避const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts),this.maxDelay);console.log(`Reconnecting in ${delay}ms...`);setTimeout(() => {this.reconnectAttempts++;this.connect();}, delay);};this.eventSource.onopen = () => {// 连接成功,重置计数器this.reconnectAttempts = 0;this.reconnectDelay = 1000;};}
}

进阶:添加抖动(Jitter)避免雷鸣效应

const delay = Math.min(this.reconnectDelay * Math.pow(2, this.reconnectAttempts),this.maxDelay
);// 添加±25%的随机抖动
const jitter = delay * 0.25 * (Math.random() - 0.5);
const finalDelay = delay + jitter;

7.3 大JSON解析导致UI卡顿

问题:服务器返回10MB的JSON时,JSON.parse()阻塞主线程。

解决方案1:流式解析

import { parse } from 'streaming-json';async function parseStreamingJSON(response: Response) {const reader = response.body.getReader();const parser = parse();let result;while (true) {const { done, value } = await reader.read();if (done) break;parser.write(value);// 可以逐步处理数据if (parser.current) {updateUIPartially(parser.current);}}return parser.result;
}

解决方案2:Web Worker

// main.ts
const worker = new Worker('json-worker.js');worker.postMessage({ type: 'parse', data: largeJsonString 
});worker.onmessage = (e) => {if (e.data.type === 'progress') {updateProgress(e.data.percent);} else if (e.data.type === 'result') {setData(e.data.result);}
};// json-worker.js
self.onmessage = (e) => {if (e.data.type === 'parse') {const result = JSON.parse(e.data.data);self.postMessage({ type: 'result', result });}
};

7.4 OAuth重定向URI不匹配

问题:OAuth授权后返回redirect_uri_mismatch错误。

原因

  • 注册时用http://localhost:6274/oauth/callback

  • 实际重定向是http://127.0.0.1:6274/oauth/callback

解决方案

class InspectorOAuthClientProvider {get redirectUrl() {// 使用window.location.origin确保一致return window.location.origin + '/oauth/callback';}get clientMetadata() {return {// 注册所有可能的变体redirect_uris: ['http://localhost:6274/oauth/callback','http://127.0.0.1:6274/oauth/callback',this.redirectUrl  // 动态获取],// ...};}
}

最佳实践

  • 开发环境同时注册localhost和127.0.0.1

  • 生产环境使用HTTPS和域名

  • 使用动态获取而非硬编码

7.5 Stdio进程僵尸问题

问题:使用stdio传输时,Node.js子进程没有正确清理。

错误代码

// ❌ 进程没有被杀死
const transport = new StdioClientTransport({command: "node",args: ["server.js"]
});// 用户关闭页面,进程仍在运行

正确代码

// ✅ 监听进程退出
const cleanup = () => {transport.close();process.exit(0);
};process.on('SIGINT', cleanup);
process.on('SIGTERM', cleanup);
process.on('exit', cleanup);// React组件清理
useEffect(() => {return () => {transport.close();};
}, []);

进阶:使用进程管理器

import { spawn } from 'spawn-rx';const childProcess = spawn('node', ['server.js']);// 自动清理
childProcess.stdout.subscribe({next: (line) => console.log(line),error: (err) => console.error(err),complete: () => console.log('Process exited')
});// 组件卸载时杀死进程
return () => childProcess.dispose();

八、扩展与定制:打造你自己的Inspector

8.1 添加自定义协议支持

假设你想支持gRPC传输,如何扩展?

步骤1:实现Transport接口

// custom-transports/grpc.ts
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import * as grpc from '@grpc/grpc-js';export class GrpcClientTransport implements Transport {private client: grpc.Client;constructor(private address: string,private credentials: grpc.ChannelCredentials) {this.client = new grpc.Client(address,credentials,{});}async start() {// 建立gRPC连接await this.client.waitForReady(Date.now() + 5000);}async send(message: JSONRPCMessage) {// 通过gRPC发送消息return new Promise((resolve, reject) => {this.client.makeUnaryRequest('/mcp.MCP/Request',(value) => Buffer.from(JSON.stringify(value)),(value) => JSON.parse(value.toString()),message,(error, response) => {if (error) reject(error);else resolve(response);});});}async close() {this.client.close();}// 事件处理onmessage?: (message: JSONRPCMessage) => void;onerror?: (error: Error) => void;onclose?: () => void;
}

步骤2:在UI中添加选项

// client/src/components/Sidebar.tsx
<Select value={transportType} onValueChange={setTransportType}><SelectItem value="stdio">Standard I/O</SelectItem><SelectItem value="sse">Server-Sent Events</SelectItem><SelectItem value="streamable-http">Streamable HTTP</SelectItem><SelectItem value="grpc">gRPC</SelectItem> {/* 新增 */}
</Select>{transportType === 'grpc' && (<div><Label>gRPC Address</Label><Input placeholder="localhost:50051"value={grpcAddress}onChange={(e) => setGrpcAddress(e.target.value)}/></div>
)}

步骤3:在连接逻辑中处理

// client/src/lib/hooks/useConnection.ts
const connect = async () => {let transport: Transport;switch (transportType) {case 'grpc':transport = new GrpcClientTransport(grpcAddress,grpc.credentials.createInsecure());break;// ... 其他case}const client = new Client(CLIENT_IDENTITY, {});await client.connect(transport);
};

8.2 自定义UI主题

MCP Inspector使用Tailwind CSS,自定义主题很简单:

// tailwind.config.js
module.exports = {theme: {extend: {colors: {// 自定义品牌色primary: {50: '#f0f9ff',500: '#0ea5e9',900: '#0c4a6e',},// 深色模式dark: {bg: '#0f172a',surface: '#1e293b',border: '#334155',}},fontFamily: {// 自定义字体mono: ['JetBrains Mono', 'monospace'],}}},// 深色模式策略darkMode: 'class',plugins: [require('@tailwindcss/forms'),require('@tailwindcss/typography'),]
}

使用深色模式

// App.tsx
const [theme, setTheme] = useState<'light' | 'dark'>('light');useEffect(() => {if (theme === 'dark') {document.documentElement.classList.add('dark');} else {document.documentElement.classList.remove('dark');}
}, [theme]);return (<div className="bg-white dark:bg-dark-bg text-gray-900 dark:text-gray-100">{/* 你的组件 */}</div>
);

8.3 添加请求录制与回放

实现类似Postman的Collection功能:

interface RecordedRequest {id: string;timestamp: number;method: string;params: unknown;response?: unknown;error?: unknown;duration: number;
}const useRecording = () => {const [recordings, setRecordings] = useState<RecordedRequest[]>([]);const [isRecording, setIsRecording] = useState(false);const recordRequest = async (method: string,params: unknown,execute: () => Promise<unknown>) => {const id = randomUUID();const start = Date.now();const request: RecordedRequest = {id,timestamp: start,method,params,duration: 0};try {const response = await execute();request.response = response;request.duration = Date.now() - start;if (isRecording) {setRecordings(prev => [...prev, request]);}return response;} catch (error) {request.error = error;request.duration = Date.now() - start;if (isRecording) {setRecordings(prev => [...prev, request]);}throw error;}};const replay = async (recordingId: string) => {const recording = recordings.find(r => r.id === recordingId);if (!recording) return;// 重新执行请求return sendRequest(recording.method, recording.params);};const exportRecordings = () => {const json = JSON.stringify(recordings, null, 2);const blob = new Blob([json], { type: 'application/json' });const url = URL.createObjectURL(blob);const a = document.createElement('a');a.href = url;a.download = `mcp-recordings-${Date.now()}.json`;a.click();};return {recordings,isRecording,setIsRecording,recordRequest,replay,exportRecordings};
};

UI组件

const RecordingsPanel = () => {const { recordings, isRecording, setIsRecording, replay, exportRecordings } = useRecording();return (<div className="recordings-panel"><div className="flex justify-between"><Button variant={isRecording ? "destructive" : "default"}onClick={() => setIsRecording(!isRecording)}>{isRecording ? '⏹️ Stop Recording' : '⏺️ Start Recording'}</Button><Button onClick={exportRecordings}>📥 Export</Button></div><div className="recordings-list">{recordings.map(rec => (<div key={rec.id} className="recording-item"><div className="flex justify-between"><span>{rec.method}</span><span>{rec.duration}ms</span></div><Button onClick={() => replay(rec.id)}>▶️ Replay</Button></div>))}</div></div>);
};

九、性能监控与调试技巧

9.1 构建性能分析

使用Vite的内置工具分析构建产物:

# 生成构建分析报告
npm run build -- --mode analyze# 或在vite.config.ts中配置
import { visualizer } from 'rollup-plugin-visualizer';export default defineConfig({plugins: [react(),visualizer({open: true,gzipSize: true,brotliSize: true,})]
});

关键指标

  • Total Size: 初始加载大小应<500KB

  • Largest Chunks: 识别需要代码分割的大依赖

  • Duplicate Dependencies: 发现重复打包的库

9.2 运行时性能监控

// client/src/lib/performance.ts
export class PerformanceMonitor {private static measurements = new Map<string, number>();static start(label: string) {this.measurements.set(label, performance.now());}static end(label: string) {const start = this.measurements.get(label);if (!start) return;const duration = performance.now() - start;this.measurements.delete(label);// 记录到分析服务if (duration > 100) {  // 超过100ms报警console.warn(`[Performance] ${label} took ${duration.toFixed(2)}ms`);}return duration;}static async measure<T>(label: string, fn: () => Promise<T>): Promise<T> {this.start(label);try {return await fn();} finally {this.end(label);}}
}// 使用示例
const tools = await PerformanceMonitor.measure('tools/list',() => client.request({ method: 'tools/list' })
);

9.3 React DevTools Profiler

import { Profiler, ProfilerOnRenderCallback } from 'react';const onRender: ProfilerOnRenderCallback = (id,phase,actualDuration,baseDuration,startTime,commitTime
) => {console.log({component: id,phase,          // "mount" or "update"actualDuration, // 实际渲染耗时baseDuration,   // 预估耗时});if (actualDuration > 50) {console.warn(`Slow render detected in ${id}`);}
};export default function App() {return (<Profiler id="App" onRender={onRender}><YourComponents /></Profiler>);
}

9.4 网络请求追踪

// 拦截所有fetch请求
const originalFetch = window.fetch;
window.fetch = async (...args) => {const start = performance.now();const [url, options] = args;console.log(`[Fetch] ${options?.method || 'GET'} ${url}`);try {const response = await originalFetch(...args);const duration = performance.now() - start;console.log(`[Fetch] ${url} - ${response.status} (${duration.toFixed(2)}ms)`);return response;} catch (error) {console.error(`[Fetch] ${url} - Error:`, error);throw error;}
};

十、测试策略:从单元测试到E2E

10.1 单元测试:Jest + React Testing Library

// client/src/__tests__/DynamicJsonForm.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import DynamicJsonForm from '../components/DynamicJsonForm';describe('DynamicJsonForm', () => {it('should render string input for string type', () => {const schema = {type: 'string',description: 'Enter your name'};const onChange = jest.fn();render(<DynamicJsonFormschema={schema}value=""onChange={onChange}/>);const input = screen.getByPlaceholderText('Enter your name');expect(input).toBeInTheDocument();fireEvent.change(input, { target: { value: 'John' } });expect(onChange).toHaveBeenCalledWith('John');});it('should render select for enum type', () => {const schema = {type: 'string',enum: ['apple', 'banana', 'orange']};render(<DynamicJsonFormschema={schema}value="apple"onChange={jest.fn()}/>);expect(screen.getByText('apple')).toBeInTheDocument();});it('should validate required fields', () => {const schema = {type: 'object',properties: {name: { type: 'string' },age: { type: 'number' }},required: ['name']};const ref = React.createRef<DynamicJsonFormRef>();render(<DynamicJsonFormref={ref}schema={schema}value={{}}onChange={jest.fn()}/>);const result = ref.current?.validateJson();expect(result?.isValid).toBe(false);expect(result?.error).toContain('name is required');});
});

10.2 集成测试:测试API交互

// client/src/__tests__/useConnection.test.ts
import { renderHook, waitFor } from '@testing-library/react';
import { useConnection } from '../lib/hooks/useConnection';describe('useConnection', () => {it('should connect to MCP server', async () => {const { result } = renderHook(() => useConnection({transportType: 'stdio',command: 'node',args: 'test-server.js',env: {},config: defaultConfig}));await result.current.connect();await waitFor(() => {expect(result.current.connectionStatus).toBe('connected');});});it('should handle connection errors', async () => {const { result } = renderHook(() =>useConnection({transportType: 'stdio',command: 'invalid-command',args: '',env: {},config: defaultConfig}));await result.current.connect();await waitFor(() => {expect(result.current.connectionStatus).toBe('error');});});it('should list resources', async () => {const { result } = renderHook(() => useConnection(config));await result.current.connect();const resources = await result.current.listResources();expect(Array.isArray(resources)).toBe(true);});
});

10.3 E2E测试:Playwright

// client/e2e/startup-state.spec.ts
import { test, expect } from '@playwright/test';test.describe('MCP Inspector', () => {test('should show connection form on startup', async ({ page }) => {await page.goto('http://localhost:6274');// 检查UI元素await expect(page.locator('text=Transport Type')).toBeVisible();await expect(page.locator('button:has-text("Connect")')).toBeVisible();});test('should connect to stdio server', async ({ page }) => {await page.goto('http://localhost:6274');// 选择stdio传输await page.selectOption('select[name="transport"]', 'stdio');// 填写命令await page.fill('input[name="command"]', 'node');await page.fill('input[name="args"]', 'test-server.js');// 点击连接await page.click('button:has-text("Connect")');// 等待连接成功await expect(page.locator('text=Connected')).toBeVisible({ timeout: 10000 });});test('should list and call tools', async ({ page }) => {await page.goto('http://localhost:6274');// 连接服务器(省略...)// 切换到Tools标签await page.click('button:has-text("Tools")');// 点击List Toolsawait page.click('button:has-text("List Tools")');// 等待工具列表加载await expect(page.locator('.tool-item').first()).toBeVisible();// 选择第一个工具await page.click('.tool-item:first-child');// 填写参数await page.fill('input[name="param1"]', 'test value');// 调用工具await page.click('button:has-text("Call Tool")');// 验证结果await expect(page.locator('.tool-result')).toBeVisible();});test('should handle OAuth flow', async ({ page, context }) => {await page.goto('http://localhost:6274');// 配置OAuthawait page.fill('input[name="oauthClientId"]', 'test-client-id');// 点击连接,触发OAuthconst popupPromise = context.waitForEvent('page');await page.click('button:has-text("Connect")');// 在OAuth弹窗中授权const popup = await popupPromise;await popup.click('button:has-text("Authorize")');// 验证回到主页面并连接成功await expect(page.locator('text=Connected')).toBeVisible();});
});

10.4 CLI测试:自动化脚本

// cli/scripts/cli-tests.js
const { spawn } = require('child_process');
const assert = require('assert');async function testListTools() {const cli = spawn('node', ['cli/build/cli.js','--cli','--transport', 'stdio','node', 'test-server.js']);// 发送请求cli.stdin.write(JSON.stringify({method: 'tools/list'}) + '\n');// 读取响应let output = '';cli.stdout.on('data', (data) => {output += data.toString();});await new Promise(resolve => setTimeout(resolve, 1000));const response = JSON.parse(output);assert(Array.isArray(response.result.tools), 'Should return tools array');cli.kill();console.log('✅ testListTools passed');
}async function testToolCall() {const cli = spawn('node', ['cli/build/cli.js','--cli','--transport', 'stdio','node', 'test-server.js']);cli.stdin.write(JSON.stringify({method: 'tools/call',params: {name: 'echo',arguments: { message: 'Hello, MCP!' }}}) + '\n');let output = '';cli.stdout.on('data', (data) => {output += data.toString();});await new Promise(resolve => setTimeout(resolve, 1000));const response = JSON.parse(output);assert(response.result.content[0].text === 'Hello, MCP!');cli.kill();console.log('✅ testToolCall passed');
}// 运行所有测试
(async () => {await testListTools();await testToolCall();console.log('All tests passed! 🎉');
})();

十一、部署与发布

11.1 Docker部署

# Dockerfile
FROM node:22.7.5-alpine AS builderWORKDIR /app# 复制package文件
COPY package*.json ./
COPY client/package*.json ./client/
COPY server/package*.json ./server/
COPY cli/package*.json ./cli/# 安装依赖
RUN npm ci# 复制源码
COPY . .# 构建
RUN npm run build# 生产镜像
FROM node:22.7.5-alpineWORKDIR /app# 只复制必要文件
COPY --from=builder /app/package*.json ./
COPY --from=builder /app/client/dist ./client/dist
COPY --from=builder /app/server/build ./server/build
COPY --from=builder /app/cli/build ./cli/build
COPY --from=builder /app/node_modules ./node_modules# 暴露端口
EXPOSE 6274 6277# 启动
CMD ["node", "server/build/index.js"]

构建和运行

# 构建镜像
docker build -t mcp-inspector:latest .# 运行容器
docker run -d \--name mcp-inspector \-p 6274:6274 \-p 6277:6277 \-e MCP_PROXY_AUTH_TOKEN=your-secret-token \mcp-inspector:latest# 查看日志
docker logs -f mcp-inspector

11.2 Kubernetes部署

# k8s/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:name: mcp-inspectorlabels:app: mcp-inspector
spec:replicas: 2selector:matchLabels:app: mcp-inspectortemplate:metadata:labels:app: mcp-inspectorspec:containers:- name: inspectorimage: ghcr.io/modelcontextprotocol/inspector:latestports:- containerPort: 6274name: client- containerPort: 6277name: proxyenv:- name: MCP_PROXY_AUTH_TOKENvalueFrom:secretKeyRef:name: mcp-secretskey: auth-token- name: ALLOWED_ORIGINSvalue: "https://inspector.example.com"resources:requests:memory: "256Mi"cpu: "250m"limits:memory: "512Mi"cpu: "500m"livenessProbe:httpGet:path: /healthport: 6277initialDelaySeconds: 30periodSeconds: 10readinessProbe:httpGet:path: /readyport: 6277initialDelaySeconds: 5periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:name: mcp-inspector
spec:selector:app: mcp-inspectorports:- name: clientport: 80targetPort: 6274- name: proxyport: 6277targetPort: 6277type: LoadBalancer
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:name: mcp-inspectorannotations:cert-manager.io/cluster-issuer: letsencrypt-prod
spec:tls:- hosts:- inspector.example.comsecretName: inspector-tlsrules:- host: inspector.example.comhttp:paths:- path: /pathType: Prefixbackend:service:name: mcp-inspectorport:number: 80

11.3 CI/CD配置

# .github/workflows/ci.yml
name: CI/CDon:push:branches: [main]pull_request:branches: [main]jobs:test:runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: '22.7.5'cache: 'npm'- name: Install dependenciesrun: npm ci- name: Lintrun: npm run lint- name: Type checkrun: npm run type-check- name: Unit testsrun: npm test- name: E2E testsrun: npm run test:e2e- name: Buildrun: npm run buildpublish:needs: testif: github.ref == 'refs/heads/main'runs-on: ubuntu-lateststeps:- uses: actions/checkout@v4- name: Setup Node.jsuses: actions/setup-node@v4with:node-version: '22.7.5'registry-url: 'https://registry.npmjs.org'- name: Install dependenciesrun: npm ci- name: Buildrun: npm run build- name: Publish to npmrun: npm publish --workspaces --access publicenv:NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}- name: Build Docker imagerun: docker build -t ghcr.io/${{ github.repository }}:latest .- name: Push to GitHub Container Registryrun: |echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdindocker push ghcr.io/${{ github.repository }}:latest

11.4 版本管理

# 更新版本号
npm run update-version 0.18.0# 这个脚本会:
# 1. 更新所有package.json中的版本号
# 2. 创建git tag
# 3. 更新CHANGELOG.md
// scripts/update-version.js
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');const newVersion = process.argv[2];
if (!newVersion) {console.error('Usage: node update-version.js <version>');process.exit(1);
}// 更新根package.json
const rootPkg = require('../package.json');
rootPkg.version = newVersion;
fs.writeFileSync(path.join(__dirname, '../package.json'),JSON.stringify(rootPkg, null, 2) + '\n'
);// 更新workspace的package.json
['client', 'server', 'cli'].forEach(workspace => {const pkgPath = path.join(__dirname, `../${workspace}/package.json`);const pkg = require(pkgPath);pkg.version = newVersion;fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2) + '\n');
});// 创建git tag
execSync(`git add .`);
execSync(`git commit -m "chore: bump version to ${newVersion}"`);
execSync(`git tag v${newVersion}`);console.log(`✅ Version updated to ${newVersion}`);
console.log('Run `git push && git push --tags` to publish');

十二、未来展望与技术趋势

12.1 MCP协议的演进方向

根据MCP Inspector的设计,可以预见MCP协议未来会向以下方向发展:

1. 流式响应支持

// 未来可能的API
const stream = client.streamRequest({method: 'tools/call',params: { name: 'generate_report' }
});for await (const chunk of stream) {updateUI(chunk);  // 实时更新UI
}

2. 双向通信增强

// 服务器主动推送更新
client.on('resource:updated', (resource) => {refreshResource(resource.uri);
});client.on('tool:progress', (progress) => {updateProgressBar(progress.percent);
});

3. 模式验证标准化

// 使用JSON Schema验证所有消息
const validator = new SchemaValidator(protocolSchema);transport.onmessage = (message) => {const result = validator.validate(message);if (!result.valid) {console.error('Invalid message:', result.errors);}
};

12.2 AI开发工具的未来

MCP Inspector代表了AI开发工具的一个方向:协议优先、可组合、开放生态

趋势1:低代码AI应用开发

// 通过拖拽构建AI工作流
const workflow = new AIWorkflow().addStep('readFile', { uri: 'file:///data.csv' }).addStep('analyze', { model: 'claude-3-opus' }).addStep('generate', { template: 'report' }).addStep('sendEmail', { to: 'user@example.com' });await workflow.execute();

趋势2:多模态交互

// 支持图片、音频、视频的MCP工具
client.callTool('analyze_image', {image: await readFile('photo.jpg', 'base64'),prompt: 'What objects are in this image?'
});

趋势3:边缘计算集成

// MCP服务器运行在边缘设备
const edgeServer = new MCPServer({transport: 'webrtc',  // 点对点通信capabilities: {local: true,        // 本地模型推理offline: true       // 离线工作}
});

12.3 开源社区的力量

MCP Inspector的成功离不开开源社区。未来可能出现:

  • 更多语言的SDK:Python、Go、Rust、Java

  • 垂直领域的MCP服务器:数据库、云服务、IoT设备

  • IDE集成:VS Code、JetBrains全家桶的原生支持

  • 商业化产品:基于MCP协议的企业级AI平台


十三、总结:从Inspector学到的架构智慧

经过这次深度剖析,我们从MCP Inspector中学到了什么?

13.1 架构设计的黄金原则

  1. 分层解耦:Transport、Protocol、Application三层清晰分离

  2. 接口优先:定义好接口,实现可以随时替换

  3. 渐进增强:先实现核心功能,再添加高级特性

  4. 安全默认:认证默认开启,明确标注危险操作

  5. 开发体验:自动生成配置、友好的错误提示

13.2 工程实践的经验总结

  1. TypeScript是必须的:在大型项目中,类型安全节省的时间远超学习成本

  2. Monorepo适合中小型项目:统一管理,简化流程

  3. 自动化测试不能省:E2E测试是最后的防线

  4. 文档和代码一样重要:README、示例、注释缺一不可

  5. 性能优化要有依据:先测量,再优化

13.3 给开发者的建议

如果你要开发类似的工具

  • 从最简单的MVP开始,不要追求完美

  • 多参考现有项目(如MCP Inspector)的设计

  • 重视用户反馈,快速迭代

  • 写好文档,降低使用门槛

如果你要使用MCP协议

  • 先用Inspector熟悉协议细节

  • 从简单的服务器开始(只实现一两个工具)

  • 充分利用SDK,不要重新发明轮子

  • 加入社区,分享经验

13.4 最后的思考

MCP Inspector不仅仅是一个调试工具,它体现了Anthropic对AI应用开发生态的思考:

标准化的协议 + 丰富的工具 + 活跃的社区 = 繁荣的生态

这个公式同样适用于其他技术领域。作为开发者,我们不仅要会写代码,更要理解技术背后的设计哲学。


附录:快速参考

A. 常用命令

# 安装和运行
npx @modelcontextprotocol/inspector
npx @modelcontextprotocol/inspector node server.js
npx @modelcontextprotocol/inspector -e KEY=value node server.js# 开发模式
npm run dev
npm run dev:windows  # Windows系统# 构建
npm run build
npm run build-client
npm run build-server
npm run build-cli# 测试
npm test
npm run test:e2e
npm run test-cli# 代码质量
npm run lint
npm run prettier-fix
npm run type-check

B. 环境变量

变量说明默认值
CLIENT_PORT客户端端口6274
SERVER_PORT代理服务器端口6277
MCP_PROXY_AUTH_TOKEN认证Token随机生成
DANGEROUSLY_OMIT_AUTH禁用认证(危险)false
ALLOWED_ORIGINS允许的Originlocalhost
MCP_AUTO_OPEN_ENABLED自动打开浏览器true

C. 配置文件示例

{"mcpServers": {"filesystem": {"command": "npx","args": ["-y", "@modelcontextprotocol/server-filesystem", "/tmp"],"env": {"LOG_LEVEL": "debug"}},"github": {"type": "sse","url": "https://api.github.com/mcp/events","headers": {"Authorization": "Bearer ${GITHUB_TOKEN}"}},"custom": {"type": "streamable-http","url": "http://localhost:3000/mcp"}}
}

D. 资源链接

  • 官方文档: https://modelcontextprotocol.io

  • GitHub仓库: https://github.com/modelcontextprotocol/inspector

  • NPM包: https://www.npmjs.com/package/@modelcontextprotocol/inspector

  • Discord社区: https://discord.gg/mcp

  • 示例服务器: https://github.com/modelcontextprotocol/servers


更多AIGC文章

RAG技术全解:从原理到实战的简明指南

更多VibeCoding文章

http://www.dtcms.com/a/494493.html

相关文章:

  • 数据结构——二十一、哈夫曼树(王道408)
  • Amazon ElastiCache 全解析:打造高性能的智能缓存架构
  • Set数据结构【ES6】
  • 【算法与数据结构】图的遍历与生成树实战:从顶点3出发,DFS/BFS生成树完整代码+流程拆解
  • AI游戏素材创作全攻略
  • 杭州网站app开发公司大连市网站制作电话
  • C标准库--错误信息<errno.h>
  • SpringCloud 获取Feign请求的真实IP地址
  • 目标检测算法在家禽养殖领域中的应用
  • MUI组件库与主题系统全面指南
  • 用 PyTorch 搭建 CIFAR10 线性分类器:从数据加载到模型推理全流程解析
  • 什么是机械设备制造ERP?哲霖软件如何助力企业实现降本增效?
  • 【小白笔记】关于 Python 类、初始化以及 PyTorch 数据处理的问题
  • HTTPS 内容抓取实战 能抓到什么、怎么抓、不可解密时如何定位(面向开发与 iOS 真机排查)
  • Gartner发布数据安全态势管理市场指南:将功能扩展到AI的特定数据安全保护是DSPM发展方向
  • 建站系统的应用场景一条龙搭建网站
  • 公司网站自己做的网站怎么被搜录
  • item_video:获得淘宝商品视频 API 接口实战演示说明
  • appium学习
  • [Linux]学习笔记系列 -- [kernel][irq]softirq
  • 家庭相册私有化:Immich+cpolar构建你的数字记忆堡垒
  • 存储同步管理器SyncManager 归纳
  • 做游戏网站多少钱建设电子商务网站要多少钱
  • iBizModel 实体通知(PSDENOTIFY)模型详解
  • mysql函数大全及举例
  • 人工智能综合项目开发3-----农业病虫害识别dataclean.py
  • R语言手搓一个计算生存分析C指数(C-index)的函数算法
  • 使用leaflet库加载服务器离线地图瓦片(这边以本地nginx服务器为例)
  • 无状态协议HTTP/HTTPS (笔记)
  • 模式识别与机器学习课程笔记(8):特征提取与选择