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协议主要围绕三个核心组件来构建模型与服务器的交互上下文:

  1. 工具(Tools) :这是最常用、最核心的概念。你可以把工具理解为一个函数或一个API端点,它接收模型提供的参数,执行某些操作(如计算、查询、写入),并返回结果。例如,“获取天气”、“执行SQL查询”、“发送邮件”都可以被定义为一个工具。模型通过调用工具来“做事”。

  2. 资源(Resources) :资源代表模型可以读取的静态或动态数据。它通过一个URI来标识,内容可以是文本、JSON等格式。例如,一个“系统状态文档”、“用户待办事项列表”的JSON端点,或者一个“项目README文件”都可以作为资源。模型通过读取资源来“获取信息”。

  3. 提示词模板(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}`);
  }
);

实操要点与避坑指南

  1. 工具描述(description)是灵魂 :AI模型完全依赖这个描述来理解何时以及如何使用你的工具。务必清晰、准确。例如,“Add two numbers”就比“Calculate”好得多。
  2. 输入模式(inputSchema)要严谨 :使用JSON Schema严格定义参数类型、是否必需、以及参数描述。这既是给模型的说明书,也是第一道安全校验。在上面的例子中,我们定义了 a b 为必需的数字类型。
  3. 错误处理必不可少 :在 CallToolRequestSchema 的处理函数中,一定要对参数进行验证(如类型检查、范围检查)。即使Schema定义了类型,在实际调用时也可能收到格式错误的数据。良好的错误信息(如“参数‘a’必须为数字”)能帮助AI模型(和背后的开发者)快速定位问题。
  4. 返回格式标准化 :工具调用结果需要包装在 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 环境准备与项目初始化

首先,确保你的开发环境已经就绪:

  1. Node.js :建议安装最新的LTS版本(如18.x或20.x)。你可以从官网下载或使用nvm等版本管理工具。
  2. 包管理器 :使用 npm yarn pnpm 。本文以 npm 为例。
  3. 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 运行测试与验证

  1. 保存配置文件 ,然后 完全重启Claude Desktop 。配置只在启动时加载。

  2. 打开Claude Desktop,新建一个对话。如果配置成功,你通常不会看到明显的提示,但服务器进程应该已经被Claude Desktop在后台启动。

  3. 在聊天框中,你可以尝试让Claude使用你定义的工具。例如,输入:

    “请使用 add_numbers 工具计算一下 23 和 47 的和。”

    如果一切正常,Claude会识别到这个工具,并返回调用结果:“The sum of 23 and 47 is 70.”

  4. 你也可以尝试询问资源或提示词,例如:

    “读取一下 example://current-time 这个资源。” “给我一个用于代码审查的提示词模板,语言是Python,代码片段是 def foo(x): return x*2 。”

调试技巧

  • 如果Claude没有反应或报错“未找到工具”,首先检查Claude Desktop的日志。在macOS上,你可以在终端运行 log stream --predicate 'sender == "Claude"' 来查看实时日志。
  • 更直接的调试方式是在终端手动运行你的服务器脚本,检查是否有报错:
    node /ABSOLUTE/PATH/TO/dist/index.js
    
    如果脚本有语法错误或依赖问题,这里会直接暴露出来。一个正常的MCP服务器在启动后会“安静”地等待 stdin 输入,不会主动输出内容到 stdout。
  • 确保你的服务器代码在处理完请求后没有意外退出。服务器进程需要持续运行。

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}`);
});

高级实践要点

  1. 异步操作 :调用外部API是异步的,处理函数必须声明为 async ,并使用 await
  2. 错误处理精细化 :区分参数错误、网络错误、API业务错误(如城市不存在),并给出明确的错误信息。这能极大提升模型的调试效率和用户体验。
  3. 安全第一 :API密钥等敏感信息 绝对不要 硬编码在代码中。使用环境变量( process.env )或安全的配置管理服务。
  4. 输入验证与清理 :对用户输入(如 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启动后,服务器进程立刻退出,或者工具列表为空。

  • 排查步骤
    1. 检查配置文件路径 :确认 claude_desktop_config.json 的路径绝对正确,并且使用了Node.js脚本的 绝对路径
    2. 手动测试服务器 :在终端中,用配置文件中相同的命令和参数手动运行你的服务器脚本(例如 node /path/to/index.js )。观察是否有立即报错(如语法错误、模块找不到)。
    3. 查看Stderr :Claude Desktop会将服务器的stderr输出捕获到自己的日志中。查看Claude Desktop的日志(如前文所述),寻找错误信息。
    4. 验证传输层 :确保你的服务器代码正确创建了 StdioServerTransport 并调用了 server.connect(transport) ,且没有在初始化后意外调用 process.exit

问题2:工具能被列出,但调用时失败,返回“Tool execution error”或类似信息。

  • 排查步骤
    1. 检查工具处理函数 :在 CallToolRequestSchema 的处理函数中,确保工具名匹配(大小写敏感)。添加详细的 console.error 日志到处理函数中,输出接收到的参数,日志会出现在Claude Desktop的日志里。
    2. 验证参数模式 :确认 inputSchema 的定义与实际处理函数中期望的参数匹配。例如,如果Schema里参数 a string ,但处理函数中直接做数字加法,就会出错。
    3. 捕获异步错误 :确保工具处理函数中的异步操作被 try...catch 包裹,并将错误清晰地抛出( throw new Error(“友好信息”) ),这样错误信息才能传递回客户端。

6.2 开发与调试工作流

建立一个高效的调试循环至关重要:

  1. 使用独立测试脚本 :创建一个简单的测试脚本( test-server.js ),模拟MCP客户端与你的服务器通信。这可以让你在不依赖Claude Desktop的情况下快速验证核心逻辑。你可以使用 @modelcontextprotocol/sdk 中的客户端类,或者直接用 child_process 生成子进程来测试Stdio通信。
  2. 利用VS Code调试器 :在 launch.json 中配置一个调试任务,直接启动你的服务器脚本。然后,你可以使用调试控制台向 stdin 发送模拟的JSON-RPC请求,观察代码执行和变量状态。这是定位复杂逻辑错误的最有效方法。
  3. 结构化日志 :不要只用 console.log 。使用结构化日志库,将工具调用、参数、耗时、结果和错误都记录下来,并输出到文件。当问题在集成环境中复现时,这些日志是唯一的线索。

6.3 安全与生产化考量

当你的MCP服务器开始处理真实数据或操作时,安全就成为头等大事。

  • 输入验证(再次强调) :对所有来自模型的输入进行严格的验证和清理。防止命令注入、路径遍历、SQL注入等攻击。即使你认为AI模型是“友好”的,也可能因提示词被污染或模型幻觉而产生恶意输入。
  • 权限最小化 :你的服务器进程应该以最低必要的系统权限运行。特别是当工具涉及文件系统操作或系统命令时。
  • 访问控制 :虽然Stdio传输相对安全(本地进程间通信),但如果未来扩展到网络传输(如SSE),必须实现身份验证和授权机制,确保只有合法的客户端可以连接。
  • 审计日志 :记录谁(哪个会话/用户)在什么时间调用了什么工具、使用了什么参数。这对于问题回溯和安全审计至关重要。

LeZuse/minimal-mcp-server 这个极简的起点出发,你已经掌握了MCP服务器的核心构建方法。接下来,就是发挥你的创造力,将AI模型与你所在领域的具体知识和系统连接起来的时候了。无论是构建一个智能的代码库问答机器人、一个个性化的数据分析助手,还是一个自动化的运维管家,MCP都为你提供了强大而标准化的桥梁。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐