从零构建MCP服务器:极简实现与Claude集成指南
模型上下文协议(MCP)作为AI模型与外部系统间的标准化接口协议,其核心原理是通过定义工具(Tools)、资源(Resources)和提示词模板(Prompts)三大组件,实现大语言模型(LLM)对自定义功能的统一调用。该协议的技术价值在于为不同AI模型(如Claude、GPTs)提供了安全、高效访问外部工具和数据的通用桥梁,解决了AI应用开发中接口碎片化的问题。在实际应用场景中,开发者可通过构建
1. 项目概述:一个极简的MCP服务器实现
最近在折腾AI应用开发,特别是想让大语言模型(LLM)能更灵活地调用外部工具和数据。在这个过程中,我反复遇到了一个核心问题:如何为不同的AI模型(比如Claude、GPTs)提供一个统一、标准化的接口,让它们能安全、高效地访问我自定义的功能?答案就是 模型上下文协议(Model Context Protocol, MCP) 。而今天要拆解的,正是GitHub上一个名为 LeZuse/minimal-mcp-server 的项目。这个项目,正如其名,提供了一个构建MCP服务器的极简骨架和清晰范例。
简单来说,MCP可以理解为AI模型与外部世界(你的代码、数据库、API)之间的“通用翻译官”和“安全网关”。它定义了一套标准,让模型能通过结构化的方式发现、描述并调用你提供的工具(Tools)或访问你提供的数据资源(Resources)。 minimal-mcp-server 这个项目,就是教你如何从零开始,用最少的代码,搭建起这样一个“翻译官”的基础框架。它不追求功能大而全,而是聚焦于展示MCP核心概念(工具、资源、提示词模板)的实现精髓,非常适合开发者快速上手,理解MCP协议的工作机制,并以此为基础扩展出满足自己业务需求的强大服务。
无论你是想为内部AI助手添加查询公司数据库的能力,还是想为Claude Desktop创建一个自定义的代码片段管理器,亦或是想探索AI智能体(Agent)的更多可能性,理解并实现一个MCP服务器都是关键一步。这个项目就是那块最好的敲门砖。
2. MCP协议核心概念与项目设计思路
在动手写代码之前,我们必须先吃透MCP协议的几个核心抽象。这就像盖房子要先看懂图纸,理解了这些概念,再看 minimal-mcp-server 的代码就会豁然开朗。
2.1 MCP的三大核心组件:工具、资源与提示词
MCP协议主要围绕三个核心组件来构建模型与服务器的交互上下文:
-
工具(Tools) :这是最常用、最核心的概念。你可以把工具理解为一个函数或一个API端点,它接收模型提供的参数,执行某些操作(如计算、查询、写入),并返回结果。例如,“获取天气”、“执行SQL查询”、“发送邮件”都可以被定义为一个工具。模型通过调用工具来“做事”。
-
资源(Resources) :资源代表模型可以读取的静态或动态数据。它通过一个URI来标识,内容可以是文本、JSON等格式。例如,一个“系统状态文档”、“用户待办事项列表”的JSON端点,或者一个“项目README文件”都可以作为资源。模型通过读取资源来“获取信息”。
-
提示词模板(Prompts) :这是一组预定义的、参数化的文本模板。模型可以请求这些模板,并填入特定参数,快速生成符合特定场景的提示词(Prompt),用于引导自身或其他模型的对话。这有助于实现提示词的复用和标准化管理。
minimal-mcp-server 项目的设计目标非常明确: 用最直观的代码,分别演示如何实现一个工具、一个资源和一个提示词模板 。它剥离了复杂的业务逻辑、身份认证和性能优化,只保留MCP协议通信和核心组件定义的最小集合。这种“极简”风格使得代码结构极其清晰,每个文件、每行代码的目的都一目了然,是学习协议本身的绝佳材料。
2.2 项目技术栈与架构选择
该项目基于 Node.js 环境,使用 TypeScript 编写,这几乎是当前JavaScript/Node.js生态中开发此类中间件服务的主流选择。TypeScript的静态类型检查对于实现需要严格遵循外部协议(如MCP)的项目来说,能极大减少低级错误,提升开发体验。
它依赖的核心库是官方提供的 @modelcontextprotocol/sdk 。这个SDK封装了MCP协议的底层通信细节(如基于JSON-RPC 2.0的Stdio传输),提供了类型安全的客户端(Client)和服务器(Server)抽象。开发者只需要关注业务逻辑的实现,即“提供哪些工具/资源/提示词”,以及“如何实现它们”,而无需关心消息如何序列化、传输和路由。
项目的架构是典型的 “协议适配层 + 业务逻辑层” :
- 协议适配层 :由MCP SDK的
Server类处理,负责与AI客户端(如Claude Desktop)建立连接,接收请求并派发到对应的处理器。 - 业务逻辑层 :也就是我们需要编写的部分,即定义
Tool、Resource等对象,并实现它们的handler(处理函数)。
这种架构的优点是职责分离,业务逻辑纯净,未来如果需要更换通信协议或升级SDK,影响范围可以控制在很小的范围内。
注意 :虽然项目本身极简,但在实际生产环境中,你需要考虑更多,例如错误处理、日志记录、性能监控、身份验证与授权(确保只有可信的AI客户端能连接)、以及工具调用的限流和审计等。
minimal-mcp-server为你打开了门,但门后的世界需要你根据业务需求自行建设和加固。
3. 代码逐行解析与核心实现细节
现在,让我们深入到项目的核心代码中,看看一个极简的MCP服务器是如何构建的。我将以典型的项目结构为例进行解析,虽然具体文件可能略有不同,但核心模式是一致的。
3.1 服务器初始化与协议配置
一切始于服务器的创建。通常会在一个 index.ts 或 server.ts 的入口文件中进行初始化。
import { Server } from "@modelcontextprotocol/sdk/server/index.js";
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
// 1. 创建MCP服务器实例
const server = new Server(
{
name: "minimal-mcp-server", // 服务器名称
version: "0.1.0", // 版本号
},
{
capabilities: { // 声明服务器支持的能力
tools: {}, // 支持提供工具
resources: {}, // 支持提供资源
prompts: {}, // 支持提供提示词模板
},
}
);
关键点解析 :
Server构造函数接收两个参数:服务器元信息(名称、版本)和服务器选项。在选项中,capabilities字段至关重要,它像一份“菜单”,告诉连接的AI客户端:“我这里有工具、资源和提示词可以提供”。这里全部启用,意味着我们将实现这三类组件。- 传输层使用
StdioServerTransport。这是MCP最常见的一种传输方式,特别适用于Claude Desktop这类桌面应用。它通过标准输入(stdin)和标准输出(stdout)与宿主进程通信,无需处理复杂的网络端口。这对于本地集成场景来说既简单又安全。
3.2 实现一个简单的“工具”
工具是交互的核心。我们来看一个经典的“加法计算器”工具实现。
// 2. 定义一个工具
server.setRequestHandler(
// 处理“列出所有工具”的请求
ListToolsRequestSchema,
async () => {
return {
tools: [
{
name: "add_numbers", // 工具的唯一标识符,模型调用时使用
description: "Add two numbers together.", // 工具描述,模型据此理解工具用途
inputSchema: { // 输入参数的模式定义(基于JSON Schema)
type: "object",
properties: {
a: { type: "number", description: "The first number" },
b: { type: "number", description: "The second number" },
},
required: ["a", "b"],
},
},
],
};
}
);
// 3. 处理工具调用请求
server.setRequestHandler(
CallToolRequestSchema,
async (request) => {
const { name, arguments: args } = request.params;
if (name === "add_numbers") {
// 参数验证和类型转换在实际项目中更重要
const a = Number(args?.a);
const b = Number(args?.b);
if (isNaN(a) || isNaN(b)) {
throw new Error("Parameters 'a' and 'b' must be valid numbers.");
}
const sum = a + b;
// 返回工具调用结果
return {
content: [
{
type: "text",
text: `The sum of ${a} and ${b} is ${sum}.`, // 返回给模型的文本内容
},
],
};
}
// 如果工具名未匹配,抛出错误
throw new Error(`Unknown tool: ${name}`);
}
);
实操要点与避坑指南 :
- 工具描述(description)是灵魂 :AI模型完全依赖这个描述来理解何时以及如何使用你的工具。务必清晰、准确。例如,“Add two numbers”就比“Calculate”好得多。
- 输入模式(inputSchema)要严谨 :使用JSON Schema严格定义参数类型、是否必需、以及参数描述。这既是给模型的说明书,也是第一道安全校验。在上面的例子中,我们定义了
a和b为必需的数字类型。 - 错误处理必不可少 :在
CallToolRequestSchema的处理函数中,一定要对参数进行验证(如类型检查、范围检查)。即使Schema定义了类型,在实际调用时也可能收到格式错误的数据。良好的错误信息(如“参数‘a’必须为数字”)能帮助AI模型(和背后的开发者)快速定位问题。 - 返回格式标准化 :工具调用结果需要包装在
content数组中,通常包含type和text(或image等)字段。保持返回结构的一致性,便于客户端解析。
3.3 实现一个动态“资源”
资源让模型能够读取数据。我们实现一个返回当前服务器时间的动态资源。
// 4. 定义资源
server.setRequestHandler(
ListResourcesRequestSchema,
async () => {
return {
resources: [
{
uri: "example://current-time", // 资源的唯一标识URI
name: "Current Server Time", // 资源的人类可读名称
description: "Gets the current time on the server.",
mimeType: "text/plain", // 资源的MIME类型,告诉客户端如何解析内容
},
],
};
}
);
// 5. 处理读取资源请求
server.setRequestHandler(
ReadResourceRequestSchema,
async (request) => {
const { uri } = request.params;
if (uri === "example://current-time") {
const now = new Date().toISOString();
return {
contents: [
{
uri: uri,
mimeType: "text/plain",
text: `The current server time is: ${now}`,
},
],
};
}
throw new Error(`Resource not found: ${uri}`);
}
);
核心细节解析 :
- URI设计 :资源的
uri应该具有唯一性和一定的语义。可以使用自定义协议(如example://)或类似路径的结构。好的URI设计有助于模型理解和组织上下文。 - MIME类型 :
mimeType字段非常重要。text/plain表示纯文本,application/json表示JSON数据。AI客户端可能会根据MIME类型决定如何处理内容(例如,尝试解析JSON为结构化数据)。 - 动态性 :每次调用
ReadResourceRequestSchema处理器时,我们都会生成新的时间字符串,这展示了资源可以是动态的。它也可以是读取一个静态文件、查询数据库的最新结果等。
3.4 实现一个“提示词模板”
提示词模板用于标准化和复用提示词。我们创建一个用于代码审查的模板。
// 6. 定义提示词模板
server.setRequestHandler(
ListPromptsRequestSchema,
async () => {
return {
prompts: [
{
name: "code_review", // 模板名称
description: "A template for generating code review prompts.",
arguments: [ // 模板所需的参数
{ name: "language", description: "Programming language", required: true },
{ name: "code_snippet", description: "The code to review", required: true },
],
},
],
};
}
);
// 7. 处理获取提示词请求
server.setRequestHandler(
GetPromptRequestSchema,
async (request) => {
const { name, arguments: args } = request.params;
if (name === "code_review") {
const language = args?.language as string;
const snippet = args?.code_snippet as string;
if (!language || !snippet) {
throw new Error("Both 'language' and 'code_snippet' arguments are required.");
}
// 构建并返回填充好的提示词
const filledPrompt = `Please review the following ${language} code for best practices, potential bugs, and readability issues:\n\n\`\`\`${language}\n${snippet}\n\`\`\`\n\nProvide your feedback in a structured list.`;
return {
messages: [
{
role: "user",
content: {
type: "text",
text: filledPrompt,
},
},
],
};
}
throw new Error(`Prompt not found: ${name}`);
}
);
经验分享 :
- 参数化设计 :提示词模板的强大之处在于参数化。通过
arguments定义模板变量,模型可以在不同场景下复用同一个模板,只需填入不同的值(如不同的编程语言、不同的代码片段)。 - 返回结构 :注意
GetPromptRequestSchema的处理器返回的是messages数组,其中包含role(通常是"user"或"assistant")和content。这直接对应了聊天API中的消息格式,方便AI客户端直接将其用于后续的对话交互。
3.5 启动服务器
最后,将服务器与传输层绑定并启动。
// 8. 启动服务器
async function main() {
const transport = new StdioServerTransport();
await server.connect(transport);
console.error("Minimal MCP server running on stdio...");
}
main().catch((error) => {
console.error("Server error:", error);
process.exit(1);
});
重要提示 :这里使用 console.error 来输出日志信息,是因为标准输出(stdout)已经被用于MCP协议通信。任何向 stdout 的非协议输出都会破坏通信,导致连接失败。所以,调试信息、日志等都必须输出到 stderr 。
4. 项目构建、运行与集成实战
理解了代码之后,我们需要让它跑起来,并集成到真正的AI客户端中。这里以集成到 Claude Desktop 为例,这是目前体验MCP最直接的方式之一。
4.1 环境准备与项目初始化
首先,确保你的开发环境已经就绪:
- Node.js :建议安装最新的LTS版本(如18.x或20.x)。你可以从官网下载或使用nvm等版本管理工具。
- 包管理器 :使用
npm或yarn或pnpm。本文以npm为例。 - TypeScript :虽然项目可能已配置,但全局安装TypeScript编译器有助于检查和调试。
npm install -g typescript
接下来,获取并初始化项目:
# 克隆项目(假设项目地址,请替换为实际地址)
git clone https://github.com/LeZuse/minimal-mcp-server.git
cd minimal-mcp-server
# 安装依赖
npm install
# 编译TypeScript代码(如果项目有build脚本)
npm run build
# 或者直接使用tsc编译
tsc
编译后,你通常会在 dist 或 build 目录下找到生成的JavaScript文件(如 index.js )。
4.2 配置Claude Desktop集成
Claude Desktop允许通过配置文件添加自定义的MCP服务器。配置文件的位置因操作系统而异:
- macOS :
~/Library/Application Support/Claude/claude_desktop_config.json - Windows :
%APPDATA%\Claude\claude_desktop_config.json - Linux :
~/.config/Claude/claude_desktop_config.json
如果文件不存在,可以手动创建。我们需要在其中添加 mcpServers 配置项。
{
"mcpServers": {
"minimal-mcp-server": {
"command": "node",
"args": [
"/ABSOLUTE/PATH/TO/YOUR/minimal-mcp-server/dist/index.js"
]
}
}
}
配置详解与避坑 :
"minimal-mcp-server":这是你给这个服务器实例起的名字,可以自定义。"command":启动服务器的命令。由于我们编译成了JS,所以用node。"args":传递给命令的参数。 最关键的一点:必须使用绝对路径 。使用相对路径(如./dist/index.js)几乎一定会失败,因为Claude Desktop的工作目录不是你的项目目录。- 权限问题 :确保Node.js脚本具有可执行权限,并且路径可访问。
4.3 运行测试与验证
-
保存配置文件 ,然后 完全重启Claude Desktop 。配置只在启动时加载。
-
打开Claude Desktop,新建一个对话。如果配置成功,你通常不会看到明显的提示,但服务器进程应该已经被Claude Desktop在后台启动。
-
在聊天框中,你可以尝试让Claude使用你定义的工具。例如,输入:
“请使用
add_numbers工具计算一下 23 和 47 的和。”如果一切正常,Claude会识别到这个工具,并返回调用结果:“The sum of 23 and 47 is 70.”
-
你也可以尝试询问资源或提示词,例如:
“读取一下
example://current-time这个资源。” “给我一个用于代码审查的提示词模板,语言是Python,代码片段是def foo(x): return x*2。”
调试技巧 :
- 如果Claude没有反应或报错“未找到工具”,首先检查Claude Desktop的日志。在macOS上,你可以在终端运行
log stream --predicate 'sender == "Claude"'来查看实时日志。 - 更直接的调试方式是在终端手动运行你的服务器脚本,检查是否有报错:
如果脚本有语法错误或依赖问题,这里会直接暴露出来。一个正常的MCP服务器在启动后会“安静”地等待 stdin 输入,不会主动输出内容到 stdout。node /ABSOLUTE/PATH/TO/dist/index.js - 确保你的服务器代码在处理完请求后没有意外退出。服务器进程需要持续运行。
5. 从极简到实用:扩展指南与高级实践
minimal-mcp-server 展示了骨架,但真实世界的需求要复杂得多。下面分享一些扩展思路和高级实践,帮助你将其打造成一个实用的MCP服务器。
5.1 工具扩展:连接真实世界API
一个只会做加法的工具意义有限。让我们扩展一个实用的工具: 查询天气 。
// 扩展:一个需要调用外部API的工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
// ... 保留原有的 add_numbers
{
name: "get_weather",
description: "Get the current weather for a given city.",
inputSchema: {
type: "object",
properties: {
city: {
type: "string",
description: "The name of the city (e.g., 'Beijing', 'New York')"
},
units: {
type: "string",
enum: ["metric", "imperial"],
description: "Temperature units: 'metric' for Celsius, 'imperial' for Fahrenheit",
default: "metric"
}
},
required: ["city"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// ... 处理 add_numbers
if (name === "get_weather") {
const city = (args?.city as string)?.trim();
const units = (args?.units as string) || "metric";
if (!city) {
throw new Error("The 'city' parameter is required.");
}
// 注意:这里需要替换为真实的API密钥和端点
// 实际项目中,密钥应从环境变量等安全位置读取
const apiKey = process.env.WEATHER_API_KEY;
if (!apiKey) {
throw new Error("Weather API key is not configured.");
}
const apiUrl = `https://api.openweathermap.org/data/2.5/weather?q=${encodeURIComponent(city)}&units=${units}&appid=${apiKey}`;
let response;
try {
response = await fetch(apiUrl);
if (!response.ok) {
// 根据API错误码返回更友好的信息
if (response.status === 404) {
throw new Error(`City '${city}' not found.`);
}
throw new Error(`Weather API error: ${response.statusText}`);
}
} catch (error: any) {
// 网络或请求错误
throw new Error(`Failed to fetch weather data: ${error.message}`);
}
const data = await response.json();
// 解析API响应,提取关键信息
const temp = data.main.temp;
const description = data.weather[0].description;
const humidity = data.main.humidity;
const unitSymbol = units === "metric" ? "°C" : "°F";
return {
content: [{
type: "text",
text: `The current weather in ${city} is ${description}. Temperature is ${temp}${unitSymbol}, humidity is ${humidity}%.`,
}],
};
}
throw new Error(`Unknown tool: ${name}`);
});
高级实践要点 :
- 异步操作 :调用外部API是异步的,处理函数必须声明为
async,并使用await。 - 错误处理精细化 :区分参数错误、网络错误、API业务错误(如城市不存在),并给出明确的错误信息。这能极大提升模型的调试效率和用户体验。
- 安全第一 :API密钥等敏感信息 绝对不要 硬编码在代码中。使用环境变量(
process.env)或安全的配置管理服务。 - 输入验证与清理 :对用户输入(如
city)进行清理(trim())和编码(encodeURIComponent()),防止注入攻击或API调用失败。
5.2 资源扩展:提供结构化数据
资源不仅可以返回文本,更可以返回结构化的JSON数据,方便模型进行更复杂的推理。
// 扩展:一个返回结构化JSON的资源
server.setRequestHandler(ListResourcesRequestSchema, async () => {
return {
resources: [
// ... 保留原有的 example://current-time
{
uri: "internal://system/stats",
name: "System Statistics",
description: "Get current system resource usage (CPU, memory).",
mimeType: "application/json", // 注意MIME类型改为JSON
},
],
};
});
import os from 'os';
server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
const { uri } = request.params;
// ... 处理 example://current-time
if (uri === "internal://system/stats") {
const stats = {
timestamp: new Date().toISOString(),
platform: process.platform,
arch: process.arch,
nodeVersion: process.version,
memory: {
total: os.totalmem(),
free: os.freemem(),
used: os.totalmem() - os.freemem(),
usagePercentage: ((1 - os.freemem() / os.totalmem()) * 100).toFixed(2)
},
cpu: {
cores: os.cpus().length,
loadavg: os.loadavg(), // 1, 5, 15分钟平均负载
},
uptime: process.uptime(),
};
return {
contents: [{
uri: uri,
mimeType: "application/json",
text: JSON.stringify(stats, null, 2), // 美化输出
}],
};
}
throw new Error(`Resource not found: ${uri}`);
});
价值分析 :当资源以 application/json 格式返回时,像Claude这样的AI模型能够更好地理解数据的结构,从而进行更精准的分析、总结或基于此数据的计算。例如,模型可以轻松地说出“当前系统内存使用率为45%”,而不是面对一段需要解析的文本。
5.3 状态管理与上下文保持
一个常见的需求是工具调用之间需要共享状态。MCP服务器是无状态的,但我们可以利用JavaScript闭包或模块级变量在单次服务器进程生命周期内维持状态。
// 扩展:一个带有简单状态管理的工具 - 计数器
let counter = 0;
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
// ... 其他工具
{
name: "counter",
description: "Manage a simple counter. Use 'action' to 'increment', 'decrement', 'reset', or 'get' the value.",
inputSchema: {
type: "object",
properties: {
action: {
type: "string",
enum: ["increment", "decrement", "reset", "get"],
description: "The action to perform on the counter."
}
},
required: ["action"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
// ... 处理其他工具
if (name === "counter") {
const action = args?.action as string;
switch (action) {
case "increment":
counter++;
break;
case "decrement":
counter--;
break;
case "reset":
counter = 0;
break;
case "get":
// 不改变状态,仅获取
break;
default:
throw new Error(`Unknown counter action: ${action}. Use 'increment', 'decrement', 'reset', or 'get'.`);
}
return {
content: [{
type: "text",
text: `Counter ${action}ed. Current value is: ${counter}`,
}],
};
}
throw new Error(`Unknown tool: ${name}`);
});
重要提醒 :这种内存状态仅在当前服务器进程内有效。如果Claude Desktop重启了服务器进程,状态就会丢失。对于需要持久化的状态,你必须引入数据库(如SQLite、Redis)或文件存储。
5.4 性能优化与错误恢复
对于可能耗时的工具(如调用慢速API、处理大文件),需要考虑超时和取消机制。虽然基础SDK可能支持有限,但我们可以通过异步编程技巧来模拟。
// 扩展:一个模拟长时间运行并支持超时的工具
server.setRequestHandler(ListToolsRequestSchema, async () => {
return {
tools: [
{
name: "long_running_task",
description: "Simulates a long-running task that can be cancelled.",
inputSchema: {
type: "object",
properties: {
duration: {
type: "number",
description: "Duration to simulate in seconds (max 30).",
minimum: 1,
maximum: 30
}
},
required: ["duration"],
},
},
],
};
});
server.setRequestHandler(CallToolRequestSchema, async (request) => {
const { name, arguments: args } = request.params;
if (name === "long_running_task") {
const duration = Number(args?.duration);
if (isNaN(duration) || duration < 1 || duration > 30) {
throw new Error("Duration must be a number between 1 and 30 seconds.");
}
// 使用Promise.race实现超时控制
const taskPromise = new Promise<string>((resolve) => {
setTimeout(() => {
resolve(`Task completed successfully after ${duration} seconds.`);
}, duration * 1000);
});
const timeoutPromise = new Promise<never>((_, reject) => {
setTimeout(() => {
reject(new Error(`Task timed out after ${duration + 5} seconds.`));
}, (duration + 5) * 1000); // 设置比任务时长稍长的超时
});
try {
const result = await Promise.race([taskPromise, timeoutPromise]);
return {
content: [{ type: "text", text: result }],
};
} catch (error: any) {
throw new Error(`Long running task failed: ${error.message}`);
}
}
// ... 处理其他工具
});
实操心得 :对于真正的生产环境,考虑以下优化:
- 连接池 :如果工具需要频繁访问数据库或外部服务,使用连接池而非每次创建新连接。
- 请求队列 :对于可能并发的耗时操作,实现一个简单的队列机制,避免服务器过载。
- 健康检查 :可以暴露一个简单的工具或资源(如
health)供客户端检查服务器状态。 - 日志与监控 :集成像Winston、Pino这样的日志库,并考虑将关键指标(工具调用次数、平均耗时、错误率)发送到监控系统。
6. 常见问题排查与调试技巧实录
在实际开发和集成过程中,你肯定会遇到各种问题。下面是我在多次实践中总结的一些常见坑点和解决方法。
6.1 连接与配置问题
问题1:Claude Desktop启动后,服务器进程立刻退出,或者工具列表为空。
- 排查步骤 :
- 检查配置文件路径 :确认
claude_desktop_config.json的路径绝对正确,并且使用了Node.js脚本的 绝对路径 。 - 手动测试服务器 :在终端中,用配置文件中相同的命令和参数手动运行你的服务器脚本(例如
node /path/to/index.js)。观察是否有立即报错(如语法错误、模块找不到)。 - 查看Stderr :Claude Desktop会将服务器的stderr输出捕获到自己的日志中。查看Claude Desktop的日志(如前文所述),寻找错误信息。
- 验证传输层 :确保你的服务器代码正确创建了
StdioServerTransport并调用了server.connect(transport),且没有在初始化后意外调用process.exit。
- 检查配置文件路径 :确认
问题2:工具能被列出,但调用时失败,返回“Tool execution error”或类似信息。
- 排查步骤 :
- 检查工具处理函数 :在
CallToolRequestSchema的处理函数中,确保工具名匹配(大小写敏感)。添加详细的console.error日志到处理函数中,输出接收到的参数,日志会出现在Claude Desktop的日志里。 - 验证参数模式 :确认
inputSchema的定义与实际处理函数中期望的参数匹配。例如,如果Schema里参数a是string,但处理函数中直接做数字加法,就会出错。 - 捕获异步错误 :确保工具处理函数中的异步操作被
try...catch包裹,并将错误清晰地抛出(throw new Error(“友好信息”)),这样错误信息才能传递回客户端。
- 检查工具处理函数 :在
6.2 开发与调试工作流
建立一个高效的调试循环至关重要:
- 使用独立测试脚本 :创建一个简单的测试脚本(
test-server.js),模拟MCP客户端与你的服务器通信。这可以让你在不依赖Claude Desktop的情况下快速验证核心逻辑。你可以使用@modelcontextprotocol/sdk中的客户端类,或者直接用child_process生成子进程来测试Stdio通信。 - 利用VS Code调试器 :在
launch.json中配置一个调试任务,直接启动你的服务器脚本。然后,你可以使用调试控制台向stdin发送模拟的JSON-RPC请求,观察代码执行和变量状态。这是定位复杂逻辑错误的最有效方法。 - 结构化日志 :不要只用
console.log。使用结构化日志库,将工具调用、参数、耗时、结果和错误都记录下来,并输出到文件。当问题在集成环境中复现时,这些日志是唯一的线索。
6.3 安全与生产化考量
当你的MCP服务器开始处理真实数据或操作时,安全就成为头等大事。
- 输入验证(再次强调) :对所有来自模型的输入进行严格的验证和清理。防止命令注入、路径遍历、SQL注入等攻击。即使你认为AI模型是“友好”的,也可能因提示词被污染或模型幻觉而产生恶意输入。
- 权限最小化 :你的服务器进程应该以最低必要的系统权限运行。特别是当工具涉及文件系统操作或系统命令时。
- 访问控制 :虽然Stdio传输相对安全(本地进程间通信),但如果未来扩展到网络传输(如SSE),必须实现身份验证和授权机制,确保只有合法的客户端可以连接。
- 审计日志 :记录谁(哪个会话/用户)在什么时间调用了什么工具、使用了什么参数。这对于问题回溯和安全审计至关重要。
从 LeZuse/minimal-mcp-server 这个极简的起点出发,你已经掌握了MCP服务器的核心构建方法。接下来,就是发挥你的创造力,将AI模型与你所在领域的具体知识和系统连接起来的时候了。无论是构建一个智能的代码库问答机器人、一个个性化的数据分析助手,还是一个自动化的运维管家,MCP都为你提供了强大而标准化的桥梁。
更多推荐



所有评论(0)