1. 项目概述:为什么AI需要一个“插件系统”?
如果你最近在AI圈子里混,尤其是跟Claude、Cursor这些工具打交道,大概率会频繁听到一个词:MCP。全称是Model Context Protocol,翻译过来叫“模型上下文协议”。乍一听很技术,很抽象,但它的核心目标其实非常朴素:让AI大模型能像我们人类使用浏览器、操作Excel一样,去安全、可控地调用外部的工具、数据和功能。
这听起来不就是API吗?没错,但MCP想解决的是更深一层的问题。过去,我们想让AI干点“实事”,比如查天气、读文件、发邮件,通常有两种方式:一是靠提示词工程,把复杂的指令和上下文硬塞进有限的上下文窗口里,既低效又容易出错;二是为特定模型(比如ChatGPT)开发专属的插件或Function Calling,但这意味着你的工具链被牢牢绑定在某个生态里。今天为Claude写的工具,明天想给Gemini用?对不起,重写吧。
MCP的出现,就是为了打破这种“烟囱式”的孤岛。它定义了一套标准化的通信协议,让任何兼容MCP的AI应用(客户端),都能无缝连接和使用任何同样兼容MCP的工具(服务器)。你可以把它想象成AI世界的“USB协议”或“蓝牙协议”——只要设备支持这个标准,就能即插即用,不用关心对方是哪个品牌、哪个型号。
对我而言,MCP最吸引人的地方在于它的“去中心化”和“开发者友好”。它不是一个由某家巨头垄断的封闭平台,而是一个开放的协议。这意味着,无论是个人开发者还是大公司,都可以基于这个协议,为自己私有的数据源、内部系统或者独特的业务逻辑,快速构建一个AI可用的“工具”,并且这个工具可以服务于所有支持MCP的AI助手。这极大地降低了AI应用化的门槛,也让AI的能力边界从“纯文本生成”真正扩展到了“与真实世界交互”。
2. MCP核心架构与工作原理拆解
要理解MCP能做什么,首先得弄明白它是怎么工作的。整个MCP生态的核心是“客户端-服务器”架构,但这个架构和我们传统的Web服务有些不同,它更轻量、更专注于“资源”和“工具”的抽象。
2.1 核心组件:客户端、服务器与传输层
MCP 客户端 (Client)这就是你日常打交道的AI应用本身,比如Cursor编辑器、Claude桌面端,或者任何集成了MCP SDK的应用。客户端的核心职责是:
- 发现与连接:根据配置,找到并连接到指定的MCP服务器。
- 管理上下文:向服务器请求可用的“资源”(如文件列表、数据库表结构)和“工具”(如执行命令、搜索网页),并将这些信息以结构化的方式纳入模型的上下文。
- 调用与执行:当模型判断需要调用某个工具时,客户端负责向服务器发起调用请求,并返回结果给模型。
- 呈现结果:将工具执行的结果(可能是文本、图片、数据)整合进对话或编辑界面。
一个关键点是,客户端不负责实现具体的工具逻辑,它只负责协议的调度和通信。
MCP 服务器 (Server)这是能力的提供方,也是开发者主要耕耘的地方。一个MCP服务器可以很简单,只暴露一个“获取当前时间”的工具;也可以非常复杂,比如连接整个公司的JIRA系统、内部知识库,或者控制智能家居。 服务器的核心职责是:
- 声明能力:启动时,向客户端宣告自己提供了哪些“资源”和“工具”。例如,一个文件系统服务器会声明“我可以列出
/home/user/docs目录下的所有文件”(这是一个资源),以及“我可以读取/home/user/docs/xxx.txt文件的内容”(这是一个工具)。 - 处理请求:接收客户端发来的工具调用请求,执行真正的业务逻辑(如调用第三方API、查询数据库、运行本地脚本)。
- 返回结果:将执行结果(成功或错误)按照协议格式返回给客户端。
传输层 (Transport)MCP协议本身是传输层无关的。这意味着客户端和服务器可以通过多种方式通信:
- stdio (标准输入输出):最常见的方式,适用于服务器是一个本地进程。客户端启动服务器进程,并通过管道进行JSON-RPC通信。这种方式简单、安全,适合大多数本地工具。
- SSH:可以连接到远程主机上的MCP服务器,实现远程能力调用。
- HTTP/WebSocket:适用于服务器是一个长期运行的网络服务,允许多个客户端连接。
这种设计让部署变得非常灵活。你可以在本地电脑上运行一个服务器供个人使用,也可以在公司内网部署一个企业级服务器供所有员工调用。
2.2 核心概念:资源、工具与提示词
MCP协议的核心抽象是“资源”和“工具”,它们共同构成了AI模型的“可操作上下文”。
资源 (Resources)资源代表的是“数据”或“信息的引用”。它本身不一定包含完整的数据内容,而更像是一个目录或索引。例如:
file:///home/user/project/README.md指向一个文件。jira://project/TASK-123指向一个JIRA任务。db://sales/customers指向数据库中的一个表。
服务器可以向客户端提供一个资源的“URI”和“描述”。当模型需要了解某个资源时,客户端可以向服务器请求该资源的详细内容(例如,读取文件内容或获取任务详情)。资源是静态的、可供查询的信息源。
工具 (Tools)工具代表的是“可执行的动作”。这是AI与外界交互的主要手段。每个工具都有:
- 名称 (name):唯一标识符,如
search_web。 - 描述 (description):用自然语言清晰说明这个工具是做什么的。这个描述至关重要,因为AI模型完全依赖它来决定是否以及何时调用该工具。
- 输入参数 (inputSchema):定义调用工具时需要提供的参数,采用JSON Schema格式。例如,一个搜索工具可能需要
query(查询词)和max_results(最大结果数)参数。
当模型在对话或编码过程中,认为自己需要执行某个操作(比如“帮我查一下最新的React版本”),它会根据工具描述匹配需求,然后通过客户端调用对应的工具(如search_web(query=“React latest version”))。
提示词 (Prompts)这是MCP一个非常巧妙的设计。除了资源和工具,服务器还可以提供预定义的“提示词模板”。你可以把它理解为可复用的“对话种子”或“任务指令集”。 例如,一个代码审查服务器可以提供名为“review_python_code”的提示词。当用户在客户端选择这个提示词时,客户端会向服务器请求该提示词的详细内容(可能是一个包含占位符的模板),然后将其填充到对话中,引导模型进入代码审查的角色和流程。这标准化了复杂任务的启动方式,提升了体验。
2.3 工作流程全景图
让我们通过一个具体场景,串联起整个工作流程: 假设你正在Cursor里写代码,并配置了一个“文件系统”MCP服务器和一个“网络搜索”MCP服务器。
- 初始化连接:你启动Cursor(客户端)。Cursor读取你的配置文件,发现你配置了两个MCP服务器。它分别启动这两个服务器进程(通过stdio),并建立连接。
- 能力发现:Cursor向两个服务器发送
initialize请求。文件系统服务器回复:“我提供了list_directory工具和read_file资源。”网络搜索服务器回复:“我提供了search_web工具。” - 上下文注入:Cursor将这些工具和资源的描述,作为系统提示词的一部分,悄悄地提供给其内置的AI模型(比如Claude 3)。现在模型知道:“哦,我现在除了聊天,还能列出目录、读文件、搜索网页。”
- 用户交互:你在Cursor里问:“我项目根目录下的
src文件夹里有什么文件?” - 模型决策:模型分析你的请求,匹配工具描述。它发现
list_directory工具的描述是“列出指定路径下的文件和子目录”。于是它决定调用这个工具,并生成一个结构化的调用请求:list_directory(path=“/project/src”)。 - 客户端转发:Cursor收到模型发来的调用请求,将其通过MCP协议转发给文件系统服务器。
- 服务器执行:文件系统服务器收到请求,在本地实际执行
ls /project/src命令,获取文件列表。 - 结果返回:服务器将文件列表结果格式化,通过MCP协议返回给Cursor。
- 结果呈现:Cursor将文件列表结果返回给模型。模型将这个结果融入自己的思考,最终生成给你的回答:“你的
src文件夹下包含main.py,utils.py, 和一个components子目录。” - 循环往复:整个对话中,模型可以根据需要,多次、混合调用不同服务器提供的工具,从而完成复杂的、需要多步外部交互的任务。
这个流程的关键在于,模型始终处于核心决策地位,它根据对用户意图的理解和可用的工具描述,自主决定调用什么、何时调用。而MCP协议,则确保了这种调用的标准化和安全隔离。
3. 如何构建你的第一个MCP服务器:从零到一实战
理解了原理,最好的巩固方式就是动手做一个。我们将构建一个最简单的MCP服务器:一个“随机笑话生成器”。它提供一个工具,当被调用时,会从预设列表中随机返回一个笑话。
3.1 环境准备与工具选型
构建MCP服务器,主流语言(Python, JavaScript, TypeScript, Go等)都可以,官方和社区也提供了相应的SDK来简化开发。这里我选择TypeScript,因为它结合了JavaScript的生态优势和静态类型检查,对构建这类需要清晰定义接口(工具参数、返回值)的项目非常友好。
所需环境:
- Node.js:版本18或以上。这是运行TypeScript和MCP SDK的基础。
- npm 或 yarn:包管理工具。我习惯用
pnpm,速度更快,这里也推荐。 - 代码编辑器:VS Code或Cursor皆可,确保有好的TypeScript支持。
初始化项目:打开终端,创建一个新目录并初始化项目。
mkdir mcp-joke-server cd mcp-joke-server pnpm init -y安装核心依赖:我们需要安装官方提供的@modelcontextprotocol/sdk。
pnpm add @modelcontextprotocol/sdk同时,因为用TypeScript开发,需要安装类型定义和开发依赖。
pnpm add -D typescript @types/node tsxtsx是一个TypeScript执行器,可以让我们直接运行.ts文件,非常方便开发调试。
配置TypeScript:创建tsconfig.json文件。
{ "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "outDir": "./dist", "rootDir": "./src", "strict": true, "esModuleInterop": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true, "resolveJsonModule": true }, "include": ["src/**/*"], "exclude": ["node_modules"] }3.2 服务器核心代码实现
现在,我们来编写服务器的核心逻辑。在src目录下创建index.ts文件。
第一步:导入SDK并定义工具
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; // 1. 创建Server实例 const server = new Server( { name: "joke-server", // 服务器名称 version: "0.1.0", // 版本 }, { capabilities: { // 声明服务器能力 tools: {}, // 我们提供工具 }, } ); // 2. 定义我们的笑话库 const jokes = [ "为什么程序员总是分不清万圣节和圣诞节?因为 Oct 31 == Dec 25。", "我写代码的速度,取决于咖啡因的浓度和死线的接近度。", "曾经有个程序员去钓鱼,他钓到了一条鱼。鱼说:‘把我放了吧,我可以实现你三个愿望。’程序员说:‘好啊,我要一个无敌的框架,永远不会出bug的代码,和...’鱼打断他:‘等等,你说的是三个愿望还是一个愿望?’", "问:如何让一个程序员崩溃?答:让他看一段没有注释的、他自己一年前写的代码。", "硬件是舞台,软件是演员,而用户是观众——只是他们经常在演员忘词时喝倒彩。", ]; // 3. 定义“get_random_joke”工具 const getRandomJokeTool = { name: "get_random_joke", // 工具名称,建议用蛇形命名 description: "从服务器预设的笑话库中随机返回一个程序员笑话。当用户需要轻松一下、缓解压力或请求讲个笑话时调用此工具。", // 描述务必清晰!AI靠它做决策。 inputSchema: { type: "object", properties: { category: { // 可以设计一个参数,虽然我们现在不用,但展示了如何定义 type: "string", description: "笑话类别(暂未实现,保留字段)", enum: ["programmer", "general"], }, }, }, };关键点解析:
- 工具描述 (description):这是最重要的部分。我写的描述不仅说明了功能(“随机返回一个程序员笑话”),还给出了调用场景的建议(“当用户需要轻松一下...时调用”)。这能极大地帮助AI模型更准确地理解何时该使用这个工具。模糊的描述会导致模型要么滥用,要么完全忽略这个工具。
- 输入参数 (inputSchema):这里我定义了一个
category参数,并使用了enum枚举了可选值。即使当前逻辑用不到这个参数,这样定义也展示了如何构建更复杂的工具。在实际调用时,AI模型会提供符合这个schema的JSON对象。
第二步:实现工具处理逻辑
我们需要告诉服务器,当收到调用get_random_joke工具的请求时,应该执行什么操作。
// 4. 处理工具调用请求 server.setRequestHandler(CallToolRequestSchema, async (request) => { // 检查调用的工具名称是否匹配 if (request.params.name === getRandomJokeTool.name) { // 从笑话库中随机选取一个 const randomIndex = Math.floor(Math.random() * jokes.length); const selectedJoke = jokes[randomIndex]; // 返回成功结果,内容类型为文本 return { content: [ { type: "text", text: selectedJoke, }, ], }; } // 如果收到未知的工具调用请求,返回错误 throw new Error(`未知的工具: ${request.params.name}`); });第三步:声明可用的工具列表
服务器启动时,需要告诉客户端它提供了哪些工具。
// 5. 处理客户端查询可用工具的请求 server.setRequestHandler(ListToolsRequestSchema, async () => { // 返回我们定义的工具列表(目前只有一个) return { tools: [getRandomJokeTool], }; });第四步:启动服务器并配置传输层
最后,我们需要启动服务器,并指定通过stdio(标准输入输出)进行通信,这是与像Cursor这样的客户端集成最常用的方式。
// 6. 启动服务器 async function main() { // 使用Stdio传输层 const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP 笑话服务器已启动,通过 stdio 通信"); // 使用 console.error 输出日志,避免干扰协议通信 } main().catch((error) => { console.error("服务器启动失败:", error); process.exit(1); });完整的src/index.ts代码:
将以上所有步骤组合起来,就是完整的服务器代码。保存文件。
3.3 构建、测试与配置客户端
构建项目:在package.json中添加构建和启动脚本。
{ "name": "mcp-joke-server", "version": "0.1.0", "type": "module", "scripts": { "build": "tsc", "start": "node dist/index.js", "dev": "tsx watch src/index.ts" }, "dependencies": { "@modelcontextprotocol/sdk": "^0.5.0" }, "devDependencies": { "@types/node": "^20.0.0", "tsx": "^4.0.0", "typescript": "^5.0.0" } }运行pnpm run build会将TypeScript编译成JavaScript到dist目录。开发时可以直接用pnpm run dev启动监听模式。
手动测试服务器:为了验证服务器逻辑是否正确,我们可以创建一个简单的测试脚本test_client.js(放在项目根目录,仅用于测试,非MCP标准客户端)。
// test_client.js - 这是一个简化的模拟测试 import { spawn } from 'child_process'; const serverProcess = spawn('node', ['dist/index.js'], { stdio: ['pipe', 'pipe', 'inherit'] // 继承stderr以便看日志 }); // 模拟发送一个ListTools请求(简化版JSON-RPC消息) const listToolsRequest = JSON.stringify({ jsonrpc: "2.0", id: 1, method: "tools/list", params: {} }) + '\n'; serverProcess.stdin.write(listToolsRequest); serverProcess.stdin.end(); serverProcess.stdout.on('data', (data) => { console.log('服务器响应:', data.toString()); serverProcess.kill(); });运行node test_client.js,你应该能看到服务器返回的包含get_random_joke工具定义的JSON消息。这说明你的服务器基本逻辑是通的。
配置Cursor客户端:这才是重头戏。要让你的笑话服务器在Cursor里真正被AI调用,需要在Cursor的MCP配置文件中添加它。
找到Cursor的MCP配置文件。通常位于:
- macOS:
~/Library/Application Support/Cursor/User/globalStorage/mcp.json - Windows:
%APPDATA%/Cursor/User/globalStorage/mcp.json - Linux:
~/.config/Cursor/User/globalStorage/mcp.json
如果文件或目录不存在,可以手动创建。
- macOS:
编辑
mcp.json文件。其基本结构是一个JSON对象,键是服务器名称,值是该服务器的配置。
{ "mcpServers": { "my-joke-server": { "command": "node", "args": [ "/ABSOLUTE/PATH/TO/YOUR/mcp-joke-server/dist/index.js" ], "env": {} } } }关键提示:
args中的路径必须是绝对路径。相对路径在Cursor的上下文中可能无法正确解析。你可以使用pwd命令获取你项目dist/index.js的绝对路径。
- 保存配置文件,并完全重启Cursor。配置只在启动时加载。
在Cursor中验证:重启Cursor后,新建一个对话。你可以尝试直接问AI:“讲个笑话听听?”或者“我有点累,来个程序员笑话放松一下。” 如果配置成功,AI模型(如Claude)会在后台看到get_random_joke工具的描述,并在认为合适的时候调用它。调用时,你可能会在Cursor的界面看到短暂的“思考”或“调用工具”的提示,然后回答中就会出现来自你服务器的随机笑话!
4. 进阶实战:构建一个实用的“系统信息查询”服务器
单一的笑话服务器只是个开始。让我们构建一个更实用、能返回多种系统信息的服务器,它将展示如何定义多个工具、处理不同参数以及返回结构化数据。
4.1 设计工具集与依赖选择
这个服务器将提供以下工具:
get_system_info: 获取操作系统、CPU架构、内存总量等基本信息。get_memory_usage: 获取当前内存使用情况(已用、空闲、百分比)。get_disk_usage: 获取指定路径的磁盘使用情况。get_process_list: 获取当前运行的进程列表(简化版,如Top 10 by CPU)。
我们将使用Node.js内置的os模块和child_process模块,无需额外安装依赖。
4.2 多工具服务器的实现
在src目录下创建system-info-server.ts。
import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { CallToolRequestSchema, ListToolsRequestSchema, } from "@modelcontextprotocol/sdk/types.js"; import os from "os"; import { exec } from "child_process"; import { promisify } from "util"; const execAsync = promisify(exec); const server = new Server( { name: "system-info-server", version: "0.2.0", }, { capabilities: { tools: {}, }, } ); // 定义工具列表 const tools = [ { name: "get_system_info", description: "获取当前系统的基本信息,包括操作系统类型、平台、CPU架构、总内存、主机名以及系统运行时间。当用户询问‘我的电脑是什么系统?’、‘系统配置如何?’或需要诊断环境问题时调用。", inputSchema: { type: "object", properties: {}, // 此工具无需参数 }, }, { name: "get_memory_usage", description: "获取当前系统的内存使用情况,包括总内存、空闲内存、已使用内存及其百分比。当用户关心内存占用、排查性能问题时调用。", inputSchema: { type: "object", properties: {}, }, }, { name: "get_disk_usage", description: "获取指定路径所在磁盘分区的使用情况,包括总空间、已用空间、可用空间和使用百分比。参数‘path’是文件系统上的任意有效路径,默认为当前工作目录(‘.’)。当用户询问‘磁盘还剩多少空间?’或需要清理存储时调用。", inputSchema: { type: "object", properties: { path: { type: "string", description: "需要查询磁盘使用情况的文件系统路径。", default: ".", }, }, }, }, { name: "get_process_list", description: "获取当前正在运行的前N个进程列表,默认按CPU使用率降序排列前10个。参数‘limit’控制返回的进程数量。当用户需要了解哪些进程占用了大量资源时调用。注意:此工具在Windows和Unix-like系统上的输出格式有差异。", inputSchema: { type: "object", properties: { limit: { type: "number", description: "需要返回的进程数量上限。", default: 10, minimum: 1, maximum: 50, }, }, }, }, ]; // 工具实现函数 async function handleGetSystemInfo() { const uptime = Math.floor(os.uptime()); const hours = Math.floor(uptime / 3600); const minutes = Math.floor((uptime % 3600) / 60); const seconds = uptime % 60; return { content: [ { type: "text", text: `**系统信息概览**\n` + `- **操作系统**: ${os.type()} ${os.release()}\n` + `- **平台**: ${os.platform()} (${os.arch()})\n` + `- **主机名**: ${os.hostname()}\n` + `- **总内存**: ${(os.totalmem() / (1024 ** 3)).toFixed(2)} GB\n` + `- **CPU核心数**: ${os.cpus().length}\n` + `- **系统运行时间**: ${hours}小时 ${minutes}分钟 ${seconds}秒`, }, ], }; } async function handleGetMemoryUsage() { const totalMem = os.totalmem(); const freeMem = os.freemem(); const usedMem = totalMem - freeMem; const usagePercent = ((usedMem / totalMem) * 100).toFixed(1); return { content: [ { type: "text", text: `**内存使用情况**\n` + `- **总内存**: ${(totalMem / (1024 ** 3)).toFixed(2)} GB\n` + `- **已使用**: ${(usedMem / (1024 ** 3)).toFixed(2)} GB\n` + `- **可用内存**: ${(freeMem / (1024 ** 3)).toFixed(2)} GB\n` + `- **使用率**: ${usagePercent}%`, }, ], }; } async function handleGetDiskUsage(params: any) { const path = params.path || "."; let command: string; let parseOutput: (stdout: string) => string; if (os.platform() === "win32") { // Windows: 使用 wmic command = `wmic logicaldisk where "DeviceID='${path.charAt(0).toUpperCase()}:'" get Size,FreeSpace`; parseOutput = (stdout) => { const lines = stdout.trim().split("\r\n"); if (lines.length < 2) return `无法获取路径 "${path}" 的磁盘信息。`; const numbers = lines[1].trim().split(/\s+/).map(Number); const total = numbers[0]; const free = numbers[1]; const used = total - free; const percent = ((used / total) * 100).toFixed(1); return `**磁盘使用情况 (${path.charAt(0).toUpperCase()}:)**\n` + `- **总空间**: ${(total / (1024**3)).toFixed(2)} GB\n` + `- **已用空间**: ${(used / (1024**3)).toFixed(2)} GB\n` + `- **可用空间**: ${(free / (1024**3)).toFixed(2)} GB\n` + `- **使用率**: ${percent}%`; }; } else { // Unix-like (Linux, macOS): 使用 df command = `df -k "${path}" | tail -1`; parseOutput = (stdout) => { const parts = stdout.trim().split(/\s+/); if (parts.length < 6) return `无法获取路径 "${path}" 的磁盘信息。`; const total = parseInt(parts[1]) * 1024; // 1K blocks to bytes const used = parseInt(parts[2]) * 1024; const available = parseInt(parts[3]) * 1024; const percent = parts[4]; return `**磁盘使用情况 (${path})**\n` + `- **总空间**: ${(total / (1024**3)).toFixed(2)} GB\n` + `- **已用空间**: ${(used / (1024**3)).toFixed(2)} GB\n` + `- **可用空间**: ${(available / (1024**3)).toFixed(2)} GB\n` + `- **使用率**: ${percent}`; }; } try { const { stdout } = await execAsync(command); return { content: [{ type: "text", text: parseOutput(stdout) }], }; } catch (error: any) { return { content: [{ type: "text", text: `执行磁盘查询命令时出错: ${error.message}\n请检查路径 "${path}" 是否有效。`, }], isError: true, }; } } async function handleGetProcessList(params: any) { const limit = Math.min(Math.max(1, params.limit || 10), 50); // 限制在1-50之间 let command: string; if (os.platform() === "win32") { command = `powershell "Get-Process | Sort-Object CPU -Descending | Select-Object -First ${limit} | Format-Table Name, CPU, WorkingSet, Id -AutoSize"`; } else { // Linux/macOS: 使用 ps command = `ps aux --sort=-%cpu | head -n ${limit + 1}`; // +1 for header } try { const { stdout } = await execAsync(command); return { content: [{ type: "text", text: `**前 ${limit} 个进程 (按CPU使用率)**\n\`\`\`\n${stdout}\n\`\`\``, }], }; } catch (error: any) { return { content: [{ type: "text", text: `获取进程列表失败: ${error.message}` }], isError: true, }; } } // 注册工具调用处理器 server.setRequestHandler(CallToolRequestSchema, async (request) => { const { name, arguments: args = {} } = request.params; switch (name) { case "get_system_info": return await handleGetSystemInfo(); case "get_memory_usage": return await handleGetMemoryUsage(); case "get_disk_usage": return await handleGetDiskUsage(args); case "get_process_list": return await handleGetProcessList(args); default: throw new Error(`未知的工具: ${name}`); } }); // 注册工具列表处理器 server.setRequestHandler(ListToolsRequestSchema, async () => { return { tools }; }); // 启动服务器 async function main() { const transport = new StdioServerTransport(); await server.connect(transport); console.error("MCP 系统信息服务器已启动"); } main().catch((error) => { console.error("服务器启动失败:", error); process.exit(1); });核心要点与避坑指南:
- 跨平台兼容性:这是系统工具类服务器最大的挑战。注意
get_disk_usage和get_process_list工具中,我们根据os.platform()判断操作系统,并执行不同的命令(Windows用wmic/powershell,Unix用df/ps)。永远不要假设服务器只运行在一种系统上。 - 参数验证与默认值:在工具定义(
inputSchema)中,我们为get_disk_usage的path参数设置了默认值".",为get_process_list的limit参数设置了默认值10,并规定了最小值和最大值。这能引导AI提供合理的参数,并在AI未提供时使用安全默认值。 - 错误处理:在
execAsync调用外包裹了try...catch。任何外部命令执行都可能失败(路径不存在、权限不足等)。必须捕获错误并通过isError: true标志或清晰的错误信息返回给客户端和用户,而不是让整个服务器崩溃。 - 输出格式化:返回的
text内容中,我们使用了Markdown格式的粗体(**)和代码块(```)。这能帮助AI客户端(如Cursor)更好地渲染和呈现结果,提升可读性。 - 工具描述的精确性:描述中明确说明了调用场景(“当用户询问...时调用”)和注意事项(“注意:此工具在Windows和Unix-like系统上的输出格式有差异”)。这能极大提升AI模型调用的准确性和用户体验。
按照之前的方法编译、配置到Cursor中,你就可以直接问:“我的系统内存用了多少?”、“C盘还剩多少空间?”、“看看现在什么进程最耗CPU?”。AI会自动选择正确的工具并返回格式化的系统信息。
5. 高级主题:资源、提示词与生产级考量
掌握了基础工具构建后,让我们探索MCP更强大的能力,并讨论如何打造一个健壮、可维护的生产级MCP服务器。
5.1 利用“资源”暴露数据索引
“资源”非常适合暴露那些结构化的、可供查询的数据目录。例如,为你的项目文档构建一个服务器。
// 示例:文档资源服务器片段 import fs from 'fs/promises'; import path from 'path'; const server = new Server(...); // 声明一个“列出文档”的资源 const docsResource = { uri: "doc:///index", name: "project-docs-index", description: "项目文档根目录索引", mimeType: "text/plain", }; // 处理“列出文档”请求 server.setRequestHandler(ReadResourceRequestSchema, async (request) => { if (request.params.uri === docsResource.uri) { const docsDir = './docs'; const files = await fs.readdir(docsDir); const fileList = files.map(f => `- ${f}`).join('\n'); return { contents: [{ uri: request.params.uri, mimeType: "text/plain", text: `可用文档:\n${fileList}` }] }; } // ... 处理其他资源读取,比如 doc:///docs/api.md }); // 在initialize或listResources时返回资源列表 server.setRequestHandler(ListResourcesRequestSchema, async () => { return { resources: [docsResource] }; });这样,AI模型可以先“浏览”doc:///index资源获取文档列表,然后再决定读取哪一个具体的文档资源(如doc:///docs/api.md)。资源提供了比工具更“只读”、更“数据导向”的交互模式。
5.2 使用“提示词”标准化复杂任务
提示词模板能封装复杂的多轮对话逻辑。例如,一个代码重构助手。
// 示例:代码重构提示词 const refactorPrompt = { name: "refactor_for_clarity", description: "启动一个代码重构会话。你将分析用户提供的代码,并提出提高可读性、可维护性的具体重构建议。", arguments: [ // 提示词可以接受参数 { name: "code_language", description: "代码的编程语言", required: true } ] }; server.setRequestHandler(GetPromptRequestSchema, async (request) => { if (request.params.name === refactorPrompt.name) { const language = request.params.arguments?.code_language || "unknown"; return { messages: [ // 返回一个消息数组,作为对话的初始上下文 { role: "user", content: { type: "text", text: `你是一个资深的${language}代码重构专家。我将给你一段代码,请你: 1. 首先,分析代码在可读性、函数拆分、命名、复杂度方面存在的主要问题。 2. 然后,针对每个问题,提供具体的重构代码示例。 3. 最后,总结重构带来的好处。 请保持专业和友好的态度。` } } ] }; } });当用户在客户端选择这个提示词时,会直接进入一个预设好角色和任务的对话,极大地提升了复杂任务的处理效率和一致性。
5.3 生产级服务器开发要点
当你打算长期运行或与他人共享MCP服务器时,需要考虑以下几点:
- 配置化:不要将服务器行为硬编码。使用环境变量或配置文件来管理API密钥、服务端点、路径等。例如,数据库连接字符串应从环境变量
DATABASE_URL读取。 - 日志与监控:使用成熟的日志库(如
winston、pino)替代console.error,记录信息、警告、错误等级别的日志,并输出到文件或日志服务,便于排查问题。 - 安全性:
- 输入验证:对所有来自客户端的输入(工具参数、资源URI)进行严格的验证和清理,防止命令注入(尤其在执行系统命令时)。
- 权限控制:考虑实现简单的权限模型。例如,通过客户端传递的某种令牌来限制可以访问的工具或资源。
- 沙箱化:对于执行任意代码或命令的工具,考虑在沙箱环境(如Docker容器、
vm2模块)中运行,隔离潜在风险。
- 错误处理与重试:对外部API或服务的调用必须有完善的错误处理、超时设置和重试机制。向客户端返回用户友好的错误信息,同时保留详细的错误日志供开发者查看。
- 性能优化:对于计算密集型或IO密集型的工具,考虑实现缓存机制(如内存缓存、Redis),避免重复计算或请求,提升响应速度。
- 打包与分发:将你的服务器打包成Docker镜像,是最方便的分发和部署方式。确保提供清晰的
README,说明配置方法、工具列表和使用示例。
6. 生态、工具与未来展望
MCP的价值不仅在于协议本身,更在于其蓬勃发展的生态。
官方与明星服务器:
- 文件系统 (filesystem):最基础也是最常用的服务器,让AI能读写本地文件。
- Git:集成Git操作,让AI可以查看状态、提交、拉取代码。
- Brave Search / Tavily:网络搜索服务器,让AI能获取实时信息。
- PostgreSQL / MySQL:数据库服务器,允许AI安全地查询数据。
- Fig:集成终端操作,功能强大但需谨慎授权。
- GitHub:直接与GitHub Issues、PR等交互。
开发与调试工具:
- MCP Inspector:一个图形化调试工具,可以连接到任何MCP服务器,查看其提供的资源、工具和提示词,并手动测试调用,是开发调试的利器。
- MCP CLI:命令行工具,用于快速测试服务器连接和基本功能。
客户端支持:除了Cursor,Claude Desktop也原生支持MCP。未来预计会有更多AI应用和IDE插件加入这一生态。
MCP的意义与未来:MCP正在做的,是为AI世界构建一套“标准外设接口”。它降低了AI能力扩展的门槛,让开发者不必再为每个模型、每个平台重复造轮子。一个为数据分析写的MCP服务器,可以同时被Claude、Cursor、未来可能还有VS Code Copilot调用。
它的挑战在于,如何平衡能力开放与安全可控。用户必须信任服务器不会执行恶意操作。因此,未来的发展可能会围绕权限管理的精细化、工具调用的可视化确认、以及服务器市场的信誉体系展开。
对我个人而言,MCP最令人兴奋的点在于,它让AI助理真正开始“上手干活”了。从查资料、读文件、操作数据库,到未来控制智能家居、管理云资源,MCP为AI融入我们的数字工作流铺平了道路。现在,是时候为你自己的数字世界,打造专属的AI工具了。