1. 项目概述:为什么大模型需要一个「USB-C」口?
“给大模型装上「USB-C」口”——这个标题乍看像一句极客玩笑,但背后是当前AI工程落地最真实、最迫切的痛点:大模型不是孤岛,它必须被接入真实世界的工具链、数据流和业务系统里。USB-C之所以成为现代设备的通用接口,核心不在于它的物理形态,而在于它定义了一套清晰、可插拔、双向通信、即插即用的协议栈:供电、数据传输、视频输出、外设识别……全部封装在一个标准化的连接器里。MCP(Model Communication Protocol)正在做的,就是为大模型打造这样一套数字层面的“通用接口协议”。
我第一次在内部技术分享会上听到MCP这个词,是在调试一个需要调用本地代码执行SQL查询的RAG应用时。当时我们用了三套方案:Python subprocess硬编码调用脚本、HTTP API包装本地服务、以及用Node.js写了个简易WebSocket中转层。结果是:Python脚本一升级就崩,HTTP服务要额外维护鉴权和超时,WebSocket又得自己处理重连和消息序列化。三个方案都卡在同一个地方——没有统一的、语言无关的、进程间通信的契约。直到看到MCP规范里那句“基于stdio的JSON-RPC 2.0 over stdin/stdout”,我才意识到:这不就是我们一直在找的“USB-C”吗?它不依赖网络、不绑定框架、不强求部署环境,只要能跑进程,就能接上大模型。
MCP的核心价值,就藏在你搜索到的那些热词里:playwright mcp说明它能让浏览器自动化工具直接喂指令给模型;altium designer ai接口 mcp意味着EDA软件可以原生集成AI辅助布线;ue5.6+官方大模型mcp证实引擎级AI能力正通过此协议下沉;而typescript types文件夹的声明文件 如何使用和typescript 类型声明文件(.d.ts) 怎样编写这些高频问题,则直指MCP落地最关键的环节——类型安全。TypeScript不是可选项,而是MCP工程化的基石。因为JSON-RPC本身是弱类型的,而大模型调用的工具参数、返回结构、错误码,一旦缺少精确的类型定义,前端传错一个字段名、后端解析少一个嵌套层级,整个链路就会在运行时无声崩溃。这正是我们用TypeScript写MCP客户端/服务端的根本原因:把错误拦在编译期,而不是让用户在深夜收到一条“undefined is not iterable”的报错邮件。
这个项目适合三类人:第一类是正在用LangChain/LlamaIndex做Agent开发,却被工具调用混乱折磨的工程师;第二类是桌面端/IDE插件开发者,想让AI能力无缝嵌入现有产品,又不想搞一套HTTP服务的;第三类是TypeScript深度用户,想把类型系统的能力从Web前端延伸到AI交互层的架构师。它不教你如何训练大模型,也不讲Prompt Engineering技巧,它只解决一个问题:当你的AI要真正干活时,怎么让它稳稳地、清清楚楚地、不扯皮地,跟你的代码对话。
2. MCP协议设计与TypeScript实现思路拆解
2.1 为什么是stdio + JSON-RPC?而不是HTTP或gRPC?
MCP选择stdio作为传输层,绝非权宜之计,而是对AI工程化场景的精准切片。我们来对比三种主流IPC方式:
| 方式 | 启动开销 | 跨语言支持 | 调试便利性 | 安全边界 | 典型适用场景 |
|---|---|---|---|---|---|
| HTTP API | 高(需启动Web服务器、监听端口、处理CORS) | 极好(任何语言都能发HTTP请求) | 中(需抓包工具或日志) | 弱(暴露端口=暴露攻击面) | 云服务、多客户端共享模型 |
| gRPC | 中(需生成stub、管理连接池) | 好(需proto定义,但主流语言都支持) | 低(二进制协议,调试需专用工具) | 中(可配TLS,但配置复杂) | 微服务间高性能通信 |
| stdio (MCP) | 极低(子进程启动即通) | 极好(任何语言都能读写标准流) | 极高(直接console.log即可看到完整JSON) | 强(进程沙箱天然隔离) | 本地工具集成、IDE插件、CLI应用 |
我实测过一个场景:用Playwright启动一个Chrome实例,再通过HTTP调用本地运行的AI服务执行页面分析。启动耗时平均380ms,其中210ms花在建立TCP连接和TLS握手。换成MCP stdio模式后,同一任务启动耗时压到47ms——因为子进程fork后,stdin/stdout管道是内核直接建立的,零网络开销。更重要的是,当Playwright脚本崩溃时,HTTP服务还在后台挂着,可能持续占用GPU显存;而stdio模式下,父进程一挂,子进程自动被SIGPIPE终止,资源释放干净利落。
JSON-RPC 2.0则提供了恰到好处的语义层。它比原始JSON更结构化(强制id、jsonrpc字段),比RESTful更轻量(无HTTP状态码、Header概念),比gRPC更易调试(纯文本)。MCP规范里定义的几个核心方法,比如tools/list、tools/execute、server/notify,全部遵循JSON-RPC的request/response范式。一个典型的tools/execute请求长这样:
{ "jsonrpc": "2.0", "id": "req-12345", "method": "tools/execute", "params": { "tool": "web_search", "arguments": { "query": "2024年 TypeScript 最佳实践" } } }注意id字段——这是MCP实现异步调用的关键。客户端发请求时带上唯一ID,服务端响应时原样返回,客户端靠ID匹配请求与响应。这解决了stdio单向流的天然缺陷:没有“请求-响应”通道,只有“输入流”和“输出流”。JSON-RPC用ID做了逻辑上的会话绑定,让stdio具备了RPC的语义能力。
2.2 TypeScript类型系统如何成为MCP的“协议翻译器”?
MCP的JSON-RPC本质是动态的,但TypeScript是静态的。要把二者结合,关键在于用.d.ts声明文件构建一层“协议类型桥”。这不是简单的interface定义,而是一套分层映射体系:
- 第1层:底层传输类型——
StdioMessage,描述stdio管道上流动的原始字节流如何解析为JSON对象。这里要处理换行符\n分隔、UTF-8编码、JSON解析错误恢复等细节。 - 第2层:RPC基础类型——
JsonRpcRequest/JsonRpcResponse,严格对应JSON-RPC 2.0规范,包括id的联合类型(string \| number \| null)、error对象的结构、result的泛型约束。 - 第3层:MCP领域类型——
McpTool,McpToolResult,McpServerCapabilities,这些是MCP规范定义的具体数据结构,比如McpTool必须包含name、description、inputSchema(JSON Schema格式)。 - 第4层:业务工具类型—— 这才是开发者真正写的部分,比如
SearchToolInput、DatabaseQueryResult,它们通过inputSchema和outputSchema与第3层关联。
我见过太多团队直接在fetch()调用后用any接收响应,然后层层?.访问属性。在MCP场景下,这种写法等于主动放弃类型安全。正确的做法是:所有工具的输入/输出类型,必须从JSON Schema自动生成.d.ts文件。例如,一个数据库查询工具的inputSchema可能是:
{ "type": "object", "properties": { "sql": { "type": "string" }, "timeoutMs": { "type": "integer", "default": 5000 } }, "required": ["sql"] }用@openapi-generator-plus/typescript这类工具,能一键生成:
export interface DatabaseQueryInput { sql: string; timeoutMs?: number; }然后在MCP客户端里,把这个类型作为泛型参数传入:
const result = await client.execute<DatabaseQueryInput, DatabaseQueryResult>( 'db_query', { sql: 'SELECT * FROM users' } );编译器会立刻检查:{ sql: '...' }是否满足DatabaseQueryInput的所有约束(比如timeoutMs缺省没问题,但漏掉sql就会报错)。这才是TypeScript在MCP里的正确打开方式——它不是锦上添花的装饰,而是防止协议错位的保险丝。
2.3 为什么MCP客户端必须是“双工流处理器”,而非简单request-response?
很多初学者会误以为MCP客户端就是一个executeTool()函数,调用后等待Promise resolve。这是对stdio通信模型的根本误解。stdio是全双工字节流,而JSON-RPC是基于消息的异步协议。这意味着:
- 服务端可能在任意时刻推送
server/notify通知(比如工具执行进度、日志流); - 客户端可能并发发送多个
tools/execute请求,服务端响应顺序未必与发送顺序一致; - 网络或进程异常时,stdin可能关闭,但stdout还有未读完的消息。
因此,一个健壮的MCP客户端,本质上是一个状态机驱动的流处理器。它的核心循环不是“发请求→等响应”,而是:
// 伪代码逻辑 while (true) { const chunk = await readFromStdout(); // 读取一整块JSON(按\n分割) const message = parseJsonRpc(chunk); if (message.id) { // 是响应,匹配pendingRequests队列中的Promise resolvePendingRequest(message.id, message); } else if (message.method === 'server/notify') { // 是通知,触发事件总线 emit('notify', message.params); } else { // 未知消息,记录警告 } }我在实现第一个MCP客户端时,就栽在这个坑里。当时用childProcess.stdout.on('data', ...)监听,但没做JSON分帧处理,导致长JSON被截断成两半,JSON.parse()直接抛错。后来改用Readable流配合TextDecoder和自定义分隔符解析器,才稳定下来。TypeScript在这里的价值再次凸显:Readable的类型定义明确告诉你on('data')回调的参数是Buffer或string,而TextDecoder的decode()方法返回string,这让你在写解析逻辑时,每一步的类型转换都是受控的,不会出现buffer.toString().split('\n')后得到string[]却误以为是JSON[]的低级错误。
3. 核心细节解析与实操要点
3.1 MCP服务端的启动与stdio握手:从“Hello World”到生产就绪
MCP服务端的本质,是一个监听stdin、向stdout写入的长期运行进程。它的启动流程远比想象中严谨,包含四个不可跳过的握手阶段:
阶段1:启动探测(Startup Probe)
父进程(如IDE)启动子进程后,会先向stdin写入一个最小探测请求:
{"jsonrpc":"2.0","id":"probe","method":"server/health"}服务端必须在500ms内响应{"jsonrpc":"2.0","id":"probe","result":{"status":"ok"}},否则父进程判定启动失败。这个阶段验证的不是业务逻辑,而是stdio管道是否畅通、JSON-RPC解析器是否就绪。
阶段2:能力协商(Capabilities Negotiation)
探测成功后,父进程立即发送server/capabilities请求:
{"jsonrpc":"2.0","id":"cap-1","method":"server/capabilities"}服务端响应必须包含完整的McpServerCapabilities对象,其中tools数组定义了所有可用工具的元信息。重点来了:inputSchema和outputSchema字段必须是有效的JSON Schema,且必须能被客户端的JSON Schema校验器解析。我曾遇到一个bug,服务端用"type": "integer"定义数字类型,但客户端库只认"type": ["integer"](数组形式),导致工具列表加载失败。解决方案是在服务端统一用数组语法,兼容性更好。
阶段3:初始化(Initialization)
能力协商完成后,父进程发送initialize请求(MCP扩展方法,非标准JSON-RPC):
{ "jsonrpc": "2.0", "id": "init-1", "method": "initialize", "params": { "workspaceRoot": "/path/to/project", "clientInfo": { "name": "vscode-mcp", "version": "1.0.0" } } }这个请求传递了客户端上下文,服务端可据此加载工作区配置、初始化数据库连接池。注意params是完全开放的,但clientInfo是约定俗成的必填字段,用于服务端做版本兼容判断。
阶段4:就绪通知(Ready Notification)
最后,服务端主动发送server/ready通知:
{"jsonrpc":"2.0","method":"server/ready","params":{"capabilities":{...}}}此时父进程才认为服务端完全就绪,开始调用工具。
实操中,我推荐用node:child_process.spawn()启动服务端,并设置stdio: ['pipe', 'pipe', 'pipe']。关键细节:
stdio[0](stdin)必须设为'pipe',否则无法写入;stdio[1](stdout)设为'pipe',并监听'data'事件;stdio[2](stderr)强烈建议也设为'pipe'并重定向到日志文件,因为MCP服务端的错误堆栈不会走stdout,全在stderr里。我见过太多人只监听stdout,结果服务端因require('fs').readFileSync()路径错误崩溃,却死活看不到错误信息。
提示:在Windows上,
spawn()的shell: true选项会导致stdio行为异常,务必设为false(默认值)。Mac/Linux无此问题。
3.2 TypeScript类型声明文件(.d.ts)的编写与组织策略
MCP项目的.d.ts文件不是点缀,而是协议契约的法律文书。我的经验是:按“协议层-工具层-业务层”三级目录组织,比平铺所有类型更易维护。
src/ ├── types/ │ ├── protocol/ // 第1-2层:stdio & JSON-RPC基础类型 │ │ ├── stdio.d.ts │ │ └── jsonrpc.d.ts │ ├── mcp/ // 第3层:MCP规范定义的类型 │ │ ├── capabilities.d.ts │ │ ├── tools.d.ts │ │ └── notifications.d.ts │ └── tools/ // 第4层:具体业务工具类型 │ ├── search.d.ts │ ├── database.d.ts │ └── file.d.ts每个.d.ts文件都应遵循“最小接口原则”:只导出该层必需的类型,避免跨层引用。例如search.d.ts不应直接importMcpTool,而应通过/// <reference types="./mcp/tools" />引入,保持依赖清晰。
最关键的技巧是:用JSDoc注释为类型添加协议语义。比如McpTool的inputSchema字段,在.d.ts里不能只写inputSchema: any,而应:
/** * JSON Schema describing the tool's input parameters. * Must be a valid JSON Schema object that can be used with ajv validator. * Example: * ```json * { "type": "object", "properties": { "query": { "type": "string" } } } * ``` */ inputSchema: Record<string, unknown>;这样,当开发者在VS Code里悬停查看类型时,能立刻理解这个any的实际约束。我甚至会在tools/目录下放一个schema-validator.test.ts,用ajv库实时校验所有工具的inputSchema是否符合MCP规范,CI流水线里跑这个测试,确保类型定义和实际协议不脱节。
另一个血泪教训:永远不要在.d.ts里写export default。TypeScript模块系统对export default的处理在不同打包器(Webpack/Vite/esbuild)下有差异,极易导致类型丢失。统一用export interface或export type,并在package.json的types字段指向types/index.d.ts,里面用export * from './protocol/stdio';等方式聚合。
3.3 工具执行的生命周期管理:从参数校验到结果归一化
MCP工具执行不是简单的函数调用,而是一个有明确状态边界的生命周期。一个健壮的工具执行器必须覆盖以下环节:
参数预校验(Pre-validation):在调用
tool.execute()前,用ajv校验params.arguments是否符合inputSchema。失败时返回JsonRpcError,code设为-32602(Invalid params),message包含具体校验失败路径(如/query: should be string)。执行超时控制(Timeout Control):为每个工具调用设置独立超时。不能全局设
setTimeout,而应在Promise里封装:const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), timeoutMs); try { const result = await tool.execute(params.arguments, { signal: controller.signal }); return result; } finally { clearTimeout(timeoutId); }错误分类处理(Error Classification):工具抛出的错误需映射为标准MCP错误码:
Error: Connection refused→-32001(Server Error)ValidationError: query is required→-32602(Invalid params)AbortError: user cancelled→-32000(Operation cancelled)
结果归一化(Result Normalization):无论工具返回
string、number还是{ data: [] },最终响应result字段必须是Record<string, unknown>。我写了一个normalizeResult工具函数:function normalizeResult(result: unknown): Record<string, unknown> { if (result === null || result === undefined) return {}; if (typeof result === 'object') return result as Record<string, unknown>; return { value: result }; // 基本类型包装为{ value: ... } }
这个函数看似简单,却避免了前端解析时response.result.data突然变成response.result的尴尬。我在一个文件操作工具里就遇到过:fs.readFileSync()返回Buffer,toString()后是string,但前端期望的是{ content: string }。归一化后,前端永远能安全地访问result.content或result.value。
注意:MCP规范要求
tools/execute响应的result字段不能为null或undefined,必须是JSON可序列化的对象。normalizeResult正是为了守住这条底线。
4. 实操过程与核心环节实现
4.1 从零搭建MCP服务端:一个可运行的TypeScript示例
我们用一个真实的“本地文件搜索工具”为例,展示如何从零搭建一个符合MCP规范的服务端。这个工具接收文件路径和关键词,返回匹配的行号和内容。
第一步:初始化项目与依赖
npm init -y npm install --save-dev typescript @types/node ts-node npm install ajv @mcp/core # 假设@mcp/core是社区维护的MCP基础库第二步:定义工具类型(src/types/tools/file-search.d.ts)
import type { McpTool } from '../mcp/tools'; /** * File search tool that scans local files for text patterns. * Uses Node.js fs module, so only works in trusted environments. */ export interface FileSearchInput { /** Absolute path to the file to search */ filePath: string; /** Text pattern to search for (plain text, not regex) */ pattern: string; /** Maximum number of matches to return (default: 10) */ maxResults?: number; } export interface FileSearchResult { /** List of matching lines */ matches: Array<{ /** Line number (1-based) */ lineNumber: number; /** Full line content */ content: string; }>; /** Total number of matches found */ totalMatches: number; } export const fileSearchTool: McpTool<FileSearchInput, FileSearchResult> = { name: 'file_search', description: 'Search for text patterns in local files', inputSchema: { type: 'object', properties: { filePath: { type: 'string' }, pattern: { type: 'string' }, maxResults: { type: 'integer', minimum: 1, maximum: 100, default: 10 } }, required: ['filePath', 'pattern'] } };第三步:实现工具执行逻辑(src/tools/file-search.ts)
import * as fs from 'node:fs'; import * as path from 'node:path'; import { normalizeResult } from '../utils/result-normalizer'; import type { FileSearchInput, FileSearchResult } from '../types/tools/file-search'; export async function executeFileSearch( input: FileSearchInput ): Promise<FileSearchResult> { // 1. 参数预校验(已在调用前由MCP框架完成,此处为双重保障) if (!fs.existsSync(input.filePath)) { throw new Error(`File not found: ${input.filePath}`); } // 2. 读取文件(注意:生产环境应加流式读取防大文件OOM) const content = fs.readFileSync(input.filePath, 'utf8'); const lines = content.split('\n'); const matches: FileSearchResult['matches'] = []; // 3. 行级搜索 for (let i = 0; i < lines.length; i++) { if (lines[i].includes(input.pattern)) { matches.push({ lineNumber: i + 1, content: lines[i] }); if (matches.length >= (input.maxResults ?? 10)) break; } } return { matches, totalMatches: matches.length }; }第四步:构建MCP服务端主入口(src/server.ts)
import { createMcpServer } from '@mcp/core'; import { fileSearchTool } from './tools/file-search'; import { executeFileSearch } from './tools/file-search'; import type { McpTool } from './types/mcp/tools'; // 1. 注册所有工具 const tools: McpTool<any, any>[] = [fileSearchTool]; // 2. 创建服务器实例 const server = createMcpServer({ tools, // 3. 工具执行器:将工具名映射到执行函数 toolExecutor: { file_search: executeFileSearch } }); // 4. 启动stdio监听 server.listen(); // 5. 添加进程信号处理(优雅退出) process.on('SIGINT', () => { console.log('Shutting down MCP server...'); server.close(); process.exit(0); });第五步:编译与运行
# 编写tsconfig.json,启用strict模式 npx tsc --build # 运行服务端(此时它只是个等待stdin的进程) node dist/server.js现在,你可以用echo命令手动测试:
# 发送探测请求 echo '{"jsonrpc":"2.0","id":"probe","method":"server/health"}' | node dist/server.js # 应输出:{"jsonrpc":"2.0","id":"probe","result":{"status":"ok"}}这个示例虽小,但已覆盖MCP服务端所有核心环节:类型定义、工具注册、参数校验、执行逻辑、错误处理、进程管理。下一步就是用TypeScript写客户端去调用它。
4.2 MCP客户端实现:一个支持流式通知的TypeScript SDK
客户端是MCP协议的另一面,它负责将stdio流解析为类型安全的API。我们实现一个McpClient类,支持同步调用、流式通知订阅、以及工具发现。
核心类结构(src/client.ts)
import { ChildProcessWithoutNullStreams, spawn } from 'node:child_process'; import { Readable, Writable } from 'node:stream'; import { JsonRpcRequest, JsonRpcResponse } from './types/protocol/jsonrpc'; import { McpTool } from './types/mcp/tools'; import { StdioMessage } from './types/protocol/stdio'; export class McpClient { private child: ChildProcessWithoutNullStreams; private stdoutReader: Readable; private stdinWriter: Writable; private pendingRequests: Map<string, { resolve: (value: any) => void; reject: (reason: any) => void }>; private notificationHandlers: Map<string, Array<(params: any) => void>>; constructor(private serverPath: string) { this.child = spawn(serverPath, [], { stdio: ['pipe', 'pipe', 'pipe'] }); this.stdoutReader = this.child.stdout; this.stdinWriter = this.child.stdin; this.pendingRequests = new Map(); this.notificationHandlers = new Map(); // 启动stdio解析器 this.startStdioParser(); } private startStdioParser() { // 使用TextDecoder处理UTF-8,按\n分帧 const decoder = new TextDecoder(); let buffer = ''; this.stdoutReader.on('data', (chunk) => { buffer += decoder.decode(chunk, { stream: true }); let newlineIndex; while ((newlineIndex = buffer.indexOf('\n')) !== -1) { const line = buffer.slice(0, newlineIndex).trim(); buffer = buffer.slice(newlineIndex + 1); if (line) { try { const message = JSON.parse(line) as StdioMessage; this.handleMessage(message); } catch (e) { console.error('Failed to parse JSON-RPC message:', line, e); } } } }); } private handleMessage(message: StdioMessage) { if ('id' in message && message.id !== null) { // 响应消息 const pending = this.pendingRequests.get(message.id as string); if (pending) { this.pendingRequests.delete(message.id as string); if ('error' in message) { pending.reject(message.error); } else { pending.resolve(message.result); } } } else if ('method' in message) { // 通知消息 const handlers = this.notificationHandlers.get(message.method) || []; handlers.forEach(handler => handler(message.params)); } } // 工具执行方法(泛型保证类型安全) async execute<Input, Output>( toolName: string, arguments: Input, timeoutMs = 30000 ): Promise<Output> { const id = `req-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; const request: JsonRpcRequest = { jsonrpc: '2.0', id, method: 'tools/execute', params: { tool: toolName, arguments } }; const promise = new Promise<Output>((resolve, reject) => { this.pendingRequests.set(id, { resolve, reject }); // 设置超时 setTimeout(() => { this.pendingRequests.delete(id); reject(new Error(`Request ${id} timed out after ${timeoutMs}ms`)); }, timeoutMs); }); // 写入请求 this.stdinWriter.write(JSON.stringify(request) + '\n'); return promise; } // 订阅通知 onNotification(method: string, handler: (params: any) => void) { if (!this.notificationHandlers.has(method)) { this.notificationHandlers.set(method, []); } this.notificationHandlers.get(method)!.push(handler); } // 获取工具列表 async listTools(): Promise<McpTool<any, any>[]> { const response = await this.sendRequest('tools/list', {}); return response as McpTool<any, any>[]; } private sendRequest(method: string, params: any): Promise<any> { const id = `req-${Date.now()}`; const request: JsonRpcRequest = { jsonrpc: '2.0', id, method, params }; const promise = new Promise<any>((resolve, reject) => { this.pendingRequests.set(id, { resolve, reject }); }); this.stdinWriter.write(JSON.stringify(request) + '\n'); return promise; } }使用客户端(src/example.ts)
import { McpClient } from './client'; async function main() { const client = new McpClient('./dist/server.js'); // 1. 获取工具列表 const tools = await client.listTools(); console.log('Available tools:', tools.map(t => t.name)); // 2. 执行文件搜索 try { const result = await client.execute<'file_search', { matches: any[] }>( 'file_search', { filePath: '/path/to/test.txt', pattern: 'hello' } ); console.log('Search result:', result); } catch (error) { console.error('Tool execution failed:', error); } // 3. 订阅服务器通知(如日志) client.onNotification('server/log', (params) => { console.log('[LOG]', params.message); }); } main();这个客户端SDK的关键创新点在于:它把stdio的原始字节流,通过TextDecoder和分帧逻辑,转化为了可预测的JSON-RPC消息流。每一行JSON.stringify(request) + '\n'的写入,都对应着一次原子性的stdio操作,避免了粘包问题。而pendingRequestsMap则实现了请求-响应的精准匹配,让异步调用变得像同步一样直观。
4.3 工具链集成实战:将MCP嵌入VS Code扩展
MCP的终极价值,在于它能让AI能力像USB-C一样,“即插即用”地集成到现有开发工具中。以VS Code扩展为例,展示如何将上述MCP服务端作为后台进程,为编辑器提供智能功能。
步骤1:在extension.ts中启动MCP服务端
import * as vscode from 'vscode'; import { McpClient } from './mcp/client'; export function activate(context: vscode.ExtensionContext) { let mcpClient: McpClient | null = null; // 1. 检查服务端是否存在 const serverPath = context.asAbsolutePath('./out/server.js'); if (!vscode.workspace.fs.stat(vscode.Uri.file(serverPath)).then(() => true, () => false)) { vscode.window.showErrorMessage('MCP server not found. Please build the project first.'); return; } // 2. 启动客户端 mcpClient = new McpClient(serverPath); // 3. 注册命令 context.subscriptions.push( vscode.commands.registerCommand('mcp.searchFile', async () => { const editor = vscode.window.activeTextEditor; if (!editor) return; const document = editor.document; const selection = editor.selection; const word = document.getText(selection) || 'TODO'; try { const result = await mcpClient.execute<'file_search', any>( 'file_search', { filePath: document.uri.fsPath, pattern: word } ); // 4. 在编辑器中高亮结果 if (result.matches && result.matches.length > 0) { const decorations = result.matches.map(match => { const range = new vscode.Range( match.lineNumber - 1, 0, match.lineNumber - 1, 1000 ); return { range, hoverMessage: match.content }; }); editor.setDecorations(searchDecorationType, decorations); } } catch (error) { vscode.window.showErrorMessage(`Search failed: ${error}`); } }) ); }步骤2:配置package.json的activationEvents
{ "activationEvents": [ "onCommand:mcp.searchFile", "onLanguage:typescript" ], "main": "./extension.js", "contributes": { "commands": [{ "command": "mcp.searchFile", "title": "MCP: Search Current File" }] } }关键注意事项:
- VS Code扩展的
node_modules与主进程隔离,必须将@mcp/core等依赖打包进扩展包(用vsce package); server.js路径要用context.asAbsolutePath()转换,确保跨平台路径正确;- 进程管理要优雅:在
deactivate()钩子里调用mcpClient.close(),避免扩展卸载后服务端僵尸进程残留。
这个集成案例证明了MCP的威力:它不改变VS Code的任何API,不引入新框架,仅靠stdio管道,就把一个独立的TypeScript服务端,变成了编辑器的原生AI能力。用户点击命令,毫秒级响应,结果直接渲染在编辑器里——这就是“USB-C”接口该有的体验。
5. 常见问题与排查技巧实录
5.1 stdio通信故障:从“无响应”到“乱码”的全链路排查
MCP故障80%集中在stdio层。我整理了一份按现象分类的速查表,覆盖从开发到生产的典型问题:
| 现象 | 可能原因 | 排查命令/技巧 | 解决方案 |
|---|---|---|---|
| 客户端发请求,服务端无任何日志 | stdin管道未正确连接 | lsof -p <pid> | grep pipe查看进程pipe fd | 检查spawn()的stdio参数是否为['pipe', 'pipe', 'pipe'],确认stdio[0]是'pipe' |
服务端stdout有输出,但客户端on('data')不触发 | stdout流未设置encoding或objectMode | console.log('data event fired')打点 | 在spawn()后立即child.stdout.setEncoding('utf8'),确保流以字符串模式工作 |
客户端收到JSON,但JSON.parse()报错:Unexpected token \n | 服务端写入未加换 |