在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(单仓库)架构。为什么不用传统的多仓库呢?
技术原因:
-
依赖共享 :三个子项目都依赖
@modelcontextprotocol/sdk,Monorepo避免了版本不一致的噩梦 -
原子化提交:前后端API变更可以在同一个commit中完成,保证一致性
-
统一工具链:共享TypeScript配置、Prettier格式化、测试框架
工程原因:
-
降低认知负担:开发者只需clone一个仓库
-
简化CI/CD:统一的构建和发布流程
-
代码复用:类型定义可以在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服务器)
这种设计的巧妙之处在于:
-
协议转换:将浏览器的HTTP请求转换为MCP协议的JSON-RPC消息
-
传输适配:支持stdio、SSE、Streamable HTTP三种传输方式
-
会话管理:通过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);
};
}
设计亮点:
-
双向消息转发:像一个忠实的信使,原封不动地传递消息
-
错误边界:捕获并转换错误为标准JSON-RPC格式
-
生命周期管理:任一端关闭,自动清理另一端
-
会话隔离:通过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();
// 只转发特定前缀的headers
if (
lowerKey.startsWith("mcp-") ||
lowerKey === "authorization" ||
lowerKey === "last-event-id"
) {
// 排除代理自身的认证header
if (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. 确定授权服务器URL
const 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"
});
}
}
};
状态机优势:
-
清晰的流程控制:每个步骤的前置条件和执行逻辑分离
-
可测试性:每个状态转换可以独立测试
-
可视化调试:UI可以实时展示当前所在步骤
3.3.2 PKCE(Proof Key for Code Exchange)实现
export class InspectorOAuthClientProvider implements OAuthClientProvider {
async startAuthorization() {
// 生成code_verifier和code_challenge
const 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定义的default
if (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>
);
}
// 普通字符串用Input
return (
<Input
value={getValueAtPath(value, path) as string}
onChange={(e) => onChange(
updateValueAtPath(value, path, e.target.value)
)}
placeholder={fieldSchema.description}
/>
);
case "number":
case "integer":
return (
<Input
type="number"
value={getValueAtPath(value, path)}
onChange={(e) => onChange(
updateValueAtPath(value, path, Number(e.target.value))
)}
min={fieldSchema.minimum}
max={fieldSchema.maximum}
/>
);
case "boolean":
return (
<Checkbox
checked={getValueAtPath(value, path) as boolean}
onCheckedChange={(checked) => onChange(
updateValueAtPath(value, path, checked)
)}
/>
);
case "array":
return (
<ArrayField
items={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 (
<JsonEditor
value={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 ? (
<JsonEditor
value={rawJsonValue}
onChange={setRawJsonValue}
/>
) : (
renderField(schema, [])
)}
</div>
);
}
);
设计亮点:
-
智能降级:复杂类型自动切换到JSON编辑器
-
双向绑定:表单变更实时同步到JSON
-
防抖优化:JSON编辑时使用debounce避免频繁解析
-
递归渲染:支持任意深度的嵌套对象
-
类型安全:全程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设计的精髓:
-
状态封装:连接状态、客户端实例、能力信息全部内部管理
-
错误处理:统一的错误Toast提示
-
超时管理:支持配置超时时间和进度重置
-
自动重连:错误后自动重试
-
类型安全:泛型确保请求/响应类型匹配
四、实战应用场景:从开发到生产
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"标签页测试文件操作工具
调试技巧:
-
查看原始消息:在Console标签页可以看到所有JSON-RPC消息
-
测试错误处理:故意传入错误参数,观察错误响应格式
-
性能分析:通过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"
}
调试流程:
-
点击"Connect",Inspector自动发起OAuth流程
-
在"Auth Debugger"中查看:
-
Authorization Server元数据
-
Client注册信息
-
Authorization URL
-
-
点击"Open Authorization URL"完成授权
-
粘贴返回的Authorization Code
-
Inspector自动完成Token交换
-
查看获得的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" |
| SSE | Bearer Token | headers: { Authorization: "Bearer xxx" } |
| SSE | 自动重连 | 断开服务器观察重连 |
| HTTP | 双向流 | url: "http://localhost:3000/mcp" |
| HTTP | 自定义Headers | headers: { 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:
-
复制"Servers File"的内容
-
保存到
~/.cursor/mcp.json(macOS/Linux)或%APPDATA%\.cursor\mcp.json(Windows) -
重启Cursor
-
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);
// 预加载工具的输入Schema
prefetchToolSchema(tool.inputSchema);
}, []);
// 虚拟滚动处理大列表
import { FixedSizeList } from 'react-window';
<FixedSizeList
height={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验证
-
\] **认证**:默认启用Token认证
-
\] **CORS**:配置正确的Origin白名单
-
\] **Rate Limiting**:限制请求频率
-
\] **依赖扫描** :定期运行`npm audit`
六、架构演进与技术选型思考
6.1 为什么选择React而不是Vue/Svelte?
这个问题在开源项目中经常被问到。MCP Inspector选择React的原因:
生态成熟度:
-
Radix UI提供无障碍访问(Accessibility)的组件基础
-
React DevTools调试体验优秀
-
大量现成的Hooks库(如
react-window、react-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因为:
-
项目规模适中(3个packages)
-
不需要独立版本管理
-
构建速度足够快,不需要缓存
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}
/>
<ToolsTab
tools={tools}
setTools={setTools}
/>
</>
);
};
这样做的优势:
-
简单直观:状态在哪里一目了然
-
类型安全:TypeScript直接推导
-
无额外依赖:减少bundle大小
-
调试友好: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 -
开发环境允许
nullorigin(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(
<DynamicJsonForm
schema={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(
<DynamicJsonForm
schema={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(
<DynamicJsonForm
ref={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 Tools
await 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');
// 配置OAuth
await page.fill('input[name="oauthClientId"]', 'test-client-id');
// 点击连接,触发OAuth
const 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 builder
WORKDIR /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-alpine
WORKDIR /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-inspector
labels:
app: mcp-inspector
spec:
replicas: 2
selector:
matchLabels:
app: mcp-inspector
template:
metadata:
labels:
app: mcp-inspector
spec:
containers:
- name: inspector
image: ghcr.io/modelcontextprotocol/inspector:latest
ports:
- containerPort: 6274
name: client
- containerPort: 6277
name: proxy
env:
- name: MCP_PROXY_AUTH_TOKEN
valueFrom:
secretKeyRef:
name: mcp-secrets
key: auth-token
- name: ALLOWED_ORIGINS
value: "https://inspector.example.com"
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 6277
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 6277
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: mcp-inspector
spec:
selector:
app: mcp-inspector
ports:
- name: client
port: 80
targetPort: 6274
- name: proxy
port: 6277
targetPort: 6277
type: LoadBalancer
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: mcp-inspector
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
tls:
- hosts:
- inspector.example.com
secretName: inspector-tls
rules:
- host: inspector.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: mcp-inspector
port:
number: 80
11.3 CI/CD配置
# .github/workflows/ci.yml
name: CI/CD
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.7.5'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Lint
run: npm run lint
- name: Type check
run: npm run type-check
- name: Unit tests
run: npm test
- name: E2E tests
run: npm run test:e2e
- name: Build
run: npm run build
publish:
needs: test
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.7.5'
registry-url: 'https://registry.npmjs.org'
- name: Install dependencies
run: npm ci
- name: Build
run: npm run build
- name: Publish to npm
run: npm publish --workspaces --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
- name: Build Docker image
run: docker build -t ghcr.io/${{ github.repository }}:latest .
- name: Push to GitHub Container Registry
run: |
echo ${{ secrets.GITHUB_TOKEN }} | docker login ghcr.io -u ${{ github.actor }} --password-stdin
docker 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 架构设计的黄金原则
-
分层解耦:Transport、Protocol、Application三层清晰分离
-
接口优先:定义好接口,实现可以随时替换
-
渐进增强:先实现核心功能,再添加高级特性
-
安全默认:认证默认开启,明确标注危险操作
-
开发体验:自动生成配置、友好的错误提示
13.2 工程实践的经验总结
-
TypeScript是必须的:在大型项目中,类型安全节省的时间远超学习成本
-
Monorepo适合中小型项目:统一管理,简化流程
-
自动化测试不能省:E2E测试是最后的防线
-
文档和代码一样重要:README、示例、注释缺一不可
-
性能优化要有依据:先测量,再优化
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 |
允许的Origin | localhost |
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. 资源链接
-
NPM包: https://www.npmjs.com/package/@modelcontextprotocol/inspector
-
Discord社区: https://discord.gg/mcp
结语
写到这里,这篇长达8000+字的技术博客终于完成了。从架构设计到代码实现,从安全考量到性能优化,我们全方位剖析了MCP Inspector这个优秀的开源项目。
如果用一句话总结:MCP Inspector是一个设计精良、实现优雅、文档完善的开发者工具,它不仅解决了MCP协议调试的问题,更为我们展示了如何构建一个专业级的全栈应用。
希望这篇文章能帮助你:
-
✅ 理解MCP协议的设计思想
-
✅ 掌握现代前端工程化实践
-
✅ 学会构建类似的开发者工具
-
✅ 提升架构设计能力
相关阅读:
