1. 项目概述一个连接大模型与真实世界的“搜索工具箱”如果你正在开发一个基于大模型LLM的应用比如一个智能客服、一个文档分析助手或者一个能帮你规划行程的AI伙伴你可能会遇到一个核心痛点大模型的知识是静态的它无法实时获取最新的信息。当用户问“今天北京的天气怎么样”或者“帮我查一下最新的AI芯片发布会新闻”时一个仅依赖训练数据的大模型就会显得力不从心。这正是NeaByteLab/WebSearch-MCP这个项目要解决的核心问题。简单来说WebSearch-MCP是一个实现了模型上下文协议Model Context Protocol MCP的服务器。它的核心使命是让大模型应用能够安全、可控、高效地调用外部的网络搜索能力。你可以把它想象成一个为大模型量身定制的“搜索工具箱”当大模型需要最新信息时它不再需要“凭空想象”而是可以调用这个工具箱里的“搜索工具”获取真实、实时的网络数据再基于这些数据生成更准确、更有价值的回答。这个项目并非简单地封装一个搜索引擎API。它的价值在于严格遵循了MCP这一新兴标准。MCP可以理解为大模型应用与外部工具如数据库、文件系统、API服务之间的一套“通用插座”标准。WebSearch-MCP作为这个标准下的一个“搜索插座”使得任何兼容MCP的大模型客户端如Claude Desktop、Cursor等或自建应用都能以统一、标准化的方式接入网络搜索功能无需为每个模型或每个应用单独编写复杂的集成代码。这极大地降低了开发复杂度提升了工具的可复用性。对于开发者而言这意味着你可以快速为你的AI应用赋予“实时联网”能力。对于最终用户这意味着他们与AI的交互将更加智能和有用AI的回答将基于最新的网络事实而不仅仅是过时的训练数据。接下来我将深入拆解这个项目的设计思路、核心实现、以及在实际部署和使用中会遇到的各种细节与“坑”。2. 核心架构与MCP协议深度解析2.1 为什么是MCP协议层的价值与选择在深入代码之前我们必须先理解为什么WebSearch-MCP选择基于MCP来构建而不是直接提供一个Python库或一个简单的REST API。这背后是对大模型应用开发生态趋势的一个关键判断。过去为大模型添加工具能力Tool Calling通常是一种“紧耦合”的方式。开发者需要针对特定的模型API如OpenAI的Function Calling Anthropic的Tools编写特定的代码结构定义工具的名称、参数和描述。这种方式存在几个明显问题一是移植性差为OpenAI GPT写的工具代码很难直接用在Claude上二是管理混乱当工具数量增多时定义、版本管理和发现都变得困难三是客户端与服务器绑定过紧任何一方的改动都可能影响另一方。MCP的出现正是为了解决这些痛点。它定义了一套与具体模型无关的、标准化的协议用于在工具提供方Server和工具使用方Client通常是大模型应用之间进行通信。这套协议的核心思想是“服务发现”和“标准化调用”Server如WebSearch-MCP启动后向Client宣告自己提供了哪些“工具”Tools和“资源”Resources。Client如Claude Desktop在连接时会获取到这些工具和资源的清单。当用户与大模型交互时模型可以根据需求决定调用哪个工具。Client按照MCP定义的JSON-RPC格式向Server发起工具调用请求。Server执行工具例如执行一次网络搜索并将结果以标准格式返回给Client。Client将结果呈现给大模型大模型再基于此生成最终回复给用户。WebSearch-MCP正是扮演了这个Server的角色。它通过实现MCP协议告诉外界“我这里提供了一个叫做web_search的工具你可以用关键词来调用它进行搜索。” 任何兼容MCP的Client无论是Anthropic官方的还是社区开发的都能立刻识别并使用这个工具无需任何额外的适配工作。这种“一次实现处处可用”的特性是项目最大的架构优势。2.2 项目整体设计思路与模块拆解基于MCP协议WebSearch-MCP项目的设计思路非常清晰构建一个轻量级、可配置、安全的MCP服务器其核心功能是提供网络搜索。我们可以将其核心模块拆解如下协议实现层MCP Server Core这是项目的基石。它使用MCP的官方SDK例如modelcontextprotocol/sdkfor JavaScript/TypeScript来搭建一个标准的MCP服务器框架。这一层负责处理与Client的WebSocket连接、管理会话状态、响应MCP标准请求如initialize,tools/list,tools/call。工具定义层Tools Definition在这一层项目定义了具体的工具。对于WebSearch-MCP核心工具就是web_search。这里需要精确定义工具的“调用规范”工具的名称、描述、以及它接受的参数例如一个名为query的字符串参数表示搜索关键词。清晰的工具定义是模型能否正确理解和调用它的关键。搜索执行引擎Search Engine Abstraction这是项目的业务核心。它接收来自协议层的query参数需要调用一个真实的搜索引擎来获取结果。这里的设计关键在于“抽象”和“可插拔”。项目不应该硬编码死某一个搜索引擎如Google而应该定义一个统一的搜索接口然后通过配置来接入不同的搜索引擎提供商如Google Custom Search JSON API, SerpAPI, Bing Search API等。这提高了项目的灵活性。结果处理与格式化层Result Processing原始搜索引擎返回的可能是复杂的JSON数据。这一层需要从中提取出对模型最有用的信息通常是每个结果的标题title、链接link和内容摘要snippet。然后按照MCP协议要求的格式或一种模型易于理解的文本格式进行封装返回给Client。格式化做得好大模型就能更好地理解和利用这些信息。配置与安全管理层Configuration Security网络搜索涉及API密钥、访问频率限制、内容过滤等敏感问题。这一层负责从环境变量或配置文件中安全地读取搜索引擎的API密钥。同时它可能还需要实现一些安全策略例如对搜索关键词进行基础的敏感词过滤或者设置请求速率限制以防止滥用。可观测性与日志层Observability对于一个服务尤其是可能被频繁调用的工具服务良好的日志记录至关重要。这一层需要记录每一次工具调用的请求参数、响应状态、耗时以及可能发生的错误便于后期监控、调试和计费分析。这个模块化设计确保了项目的核心功能搜索与协议实现、具体引擎解耦使得维护、升级和扩展例如未来增加图片搜索工具都变得更加容易。3. 核心细节解析与实操要点3.1 搜索引擎的选择与API集成权衡WebSearch-MCP的核心价值在于获取网络信息因此搜索引擎的选择直接决定了工具的能力上限和成本。市面上主要有几种集成方式各有优劣方案一使用官方搜索API如Google Custom Search JSON API优点结果质量通常很高来自Google的索引。API相对稳定文档齐全。缺点有严格的每日免费额度通常100次/天超出后费用较高。需要申请API Key和自定义搜索引擎IDCX配置步骤稍多。并且Google Custom Search更偏向于站内搜索对于全网通用搜索的覆盖可能不如想象中全面需要仔细调整CX的设置。实操要点申请时在创建“可编程搜索引擎”时最好选择“搜索整个网络”以获得更通用的搜索能力。务必在代码中做好错误处理特别是针对“额度不足”429状态码和“无效密钥”等情况的处理。方案二使用聚合型搜索API如SerpAPI, Serper.dev优点开发者友好通常提供更慷慨的免费套餐例如Serper.dev每月2500次免费。它们本身已经处理了与各大搜索引擎Google, Bing等的对接提供了统一、简洁的接口。有些还额外提供即时答案、购物结果等结构化数据。缺点是第三方服务存在服务依赖风险。自定义化和对底层结果的控制力较弱。实操要点这是快速启动和原型验证的推荐方案。以Serper.dev为例其API响应格式非常干净直接包含了organic自然结果数组每个结果都有title,link,snippet字段几乎无需额外处理即可返回给模型。在代码中建议将API Base URL和端点作为可配置项以便未来切换。方案三自建爬虫与解析引擎如使用Playwright/Puppeteer模拟浏览器优点完全自主可控无查询次数限制但需遵守robots.txt成本最低仅服务器成本。缺点技术复杂度极高需要处理反爬机制如验证码、IP封锁、动态页面渲染JavaScript、HTML解析与信息提取。维护成本巨大且法律和伦理风险较高极易因滥用而对目标网站造成压力。实操要点对于WebSearch-MCP这类通用工具项目强烈不推荐此方案。它偏离了项目“提供标准化搜索工具”的核心目标会将项目拖入无尽的爬虫工程和维护泥潭。除非有非常特殊的、受限的搜索需求否则应优先使用商业API。我的经验与建议对于个人项目或中小规模应用从Serper.dev或类似的聚合API开始是最佳选择。它的免费额度足够用于开发和早期用户测试集成简单能让你快速验证“大模型搜索”的价值。当应用规模增长对搜索结果质量、定制化有更高要求且预算充足时再考虑迁移到Google官方API或同时支持多引擎。3.2 MCP工具定义的“艺术”如何让大模型更好地理解和使用在MCP中工具的定义ToolSchema不仅仅是告诉Client有个功能可用更重要的是它提供的描述和参数信息会被Client用来构建“系统提示词”System Prompt从而教导大模型何时以及如何使用这个工具。因此工具定义的好坏直接影响了大模型调用工具的准确性和智能性。一个糟糕的工具定义可能是这样的{ name: search, description: A tool to search the web., inputSchema: { type: object, properties: { q: { type: string } } } }这个定义过于简陋。description没有说明工具的具体用途和适用场景。参数q的含义不清晰模型可能不知道这里该填什么。一个良好的工具定义应该是这样的{ name: web_search, description: Perform a web search to get current, real-world information. Use this when the user asks about recent events, factual queries, weather, news, or any information that may not be in your training data. Prefer to use this for objective facts rather than opinions., inputSchema: { type: object, properties: { query: { type: string, description: The search query string, using concise and relevant keywords. Example: latest iPhone 16 rumors March 2024, Python async/await tutorial, weather in Tokyo tomorrow. }, num_results: { type: number, description: Number of search results to return. Default is 5. Maximum is 10., default: 5 } }, required: [query] } }为什么这个定义更好名称明确web_search比search更具体。描述具有指导性它明确告诉模型何时使用“when the user asks about recent events, factual queries...”和何时不使用“objective facts rather than opinions”。这相当于给模型提供了使用说明书。参数描述清晰query参数的描述给出了具体的示例引导模型如何构造一个有效的搜索关键词使用简洁相关的关键词。这能显著提高搜索结果的准确性。提供可选参数与默认值num_results让模型可以控制返回信息的量default值确保了向后兼容和基本可用性。在实现WebSearch-MCP时花时间精心打磨工具定义是提升整个系统可用性性价比最高的一步。你需要站在大模型“思考”的角度去设计这些元数据。4. 实操过程与核心环节实现4.1 从零开始部署与运行WebSearch-MCP假设我们选择使用Node.js环境和Serper.dev作为搜索引擎来构建和运行WebSearch-MCP。以下是详细的步骤和代码解析。第一步环境准备与项目初始化# 1. 确保已安装Node.js (版本18或以上)和npm node --version npm --version # 2. 创建一个新的项目目录并初始化 mkdir my-websearch-mcp-server cd my-websearch-mcp-server npm init -y # 3. 安装核心依赖MCP官方SDK和HTTP请求库如axios npm install modelcontextprotocol/sdk axios第二步创建核心服务器文件server.jsconst { Server } require(modelcontextprotocol/sdk/server/index.js); const { StdioServerTransport } require(modelcontextprotocol/sdk/server/stdio.js); const axios require(axios); // 1. 创建MCP服务器实例 const server new Server( { name: web-search-mcp-server, version: 0.1.0, }, { capabilities: { tools: {}, // 声明本服务器提供工具 }, } ); // 2. 从环境变量获取Serper.dev的API密钥 const SERPER_API_KEY process.env.SERPER_API_KEY; const SERPER_API_URL https://google.serper.dev/search; if (!SERPER_API_KEY) { console.error(错误未设置SERPER_API_KEY环境变量。); process.exit(1); } // 3. 定义web_search工具 server.setRequestHandler(tools/list, async () { return { tools: [ { name: web_search, description: Perform a web search to get current, real-world information. Use this for recent events, factual queries, weather, news, or any information not in the model\s training data., inputSchema: { type: object, properties: { query: { type: string, description: The search query string. Use concise keywords. E.g., Python async tutorial 2024, weather New York today., }, num: { type: number, description: Number of results to return (1-10). Default is 5., default: 5, }, }, required: [query], }, }, ], }; }); // 4. 处理工具调用请求 server.setRequestHandler(tools/call, async (request) { if (request.params.name ! web_search) { throw new Error(未知工具: ${request.params.name}); } const { query, num 5 } request.params.arguments || {}; if (!query || typeof query ! string) { throw new Error(必须提供有效的搜索关键词(query)。); } const numResults Math.min(Math.max(1, num), 10); // 限制在1-10之间 try { console.log([INFO] 执行搜索: ${query} 数量: ${numResults}); // 调用Serper.dev API const response await axios.post( SERPER_API_URL, { q: query, gl: us, hl: en, num: numResults }, // 基础参数可扩展 { headers: { X-API-KEY: SERPER_API_KEY, Content-Type: application/json, }, } ); const results response.data.organic || []; // 提取自然搜索结果 // 5. 格式化结果使其对大模型友好 let formattedContent 以下是关于${query}的搜索结果共${results.length}条\n\n; results.forEach((item, index) { formattedContent ${index 1}. **${item.title}**\n; formattedContent 链接${item.link}\n; formattedContent 摘要${item.snippet}\n\n; }); if (results.length 0) { formattedContent 未找到关于${query}的相关网页结果。; } return { content: [ { type: text, text: formattedContent, }, ], }; } catch (error) { console.error([ERROR] 搜索请求失败:, error.message); // 返回一个结构化的错误信息便于Client和模型理解 return { content: [ { type: text, text: 搜索执行失败。错误原因${error.response?.data?.message || error.message}。请检查网络或API配置。, }, ], isError: true, // MCP协议中表示调用出错的标志 }; } }); // 5. 启动服务器使用stdio传输这是与MCP客户端通信的标准方式 async function main() { const transport new StdioServerTransport(); await server.connect(transport); console.error([INFO] WebSearch MCP Server 已启动并等待连接...); } main().catch((error) { console.error([FATAL] 服务器启动失败:, error); process.exit(1); });第三步配置与运行在Serper.dev官网注册并获取API密钥。在终端中设置环境变量并运行服务器# 在Linux/macOS上 export SERPER_API_KEY你的_serper_api_密钥_here node server.js # 在Windows PowerShell上 $env:SERPER_API_KEY你的_serper_api_密钥_here node server.js此时服务器会在标准输入/输出上等待MCP客户端的连接。它本身不会启动一个HTTP服务。第四步在MCP客户端中配置以Claude Desktop为例你需要修改其MCP配置文件通常位于~/Library/Application Support/Claude/claude_desktop_config.jsonon macOS。{ mcpServers: { web-search: { command: node, args: [ /你的/绝对/路径/to/my-websearch-mcp-server/server.js ], env: { SERPER_API_KEY: 你的_serper_api_密钥_here } } } }保存配置并重启Claude Desktop后Claude就应该能识别并使用web_search工具了。你可以尝试问它“今天科技圈有什么大新闻” 观察它是否会调用你的工具。4.2 高级配置多引擎支持与结果后处理基础版本只能对接一个搜索引擎。一个更健壮、更实用的WebSearch-MCP应该支持可配置的引擎。我们可以通过环境变量SEARCH_ENGINE来选择。1. 抽象搜索引擎接口首先定义一个统一的搜索函数接口。// searchEngines.js const axios require(axios); class SearchEngine { constructor(apiKey) { this.apiKey apiKey; } async search(query, numResults) { throw new Error(必须在子类中实现 search 方法); } } class SerperEngine extends SearchEngine { constructor(apiKey) { super(apiKey); this.baseUrl https://google.serper.dev/search; } async search(query, numResults) { const response await axios.post( this.baseUrl, { q: query, num: numResults }, { headers: { X-API-KEY: this.apiKey, Content-Type: application/json, }, } ); // 统一返回结构{ title, link, snippet }[] return (response.data.organic || []).map(item ({ title: item.title, link: item.link, snippet: item.snippet, })); } } class GoogleCustomSearchEngine extends SearchEngine { constructor(apiKey, cx) { super(apiKey); this.cx cx; this.baseUrl https://www.googleapis.com/customsearch/v1; } async search(query, numResults) { const params new URLSearchParams({ key: this.apiKey, cx: this.cx, q: query, num: Math.min(numResults, 10), // Google API 限制 }); const url ${this.baseUrl}?${params.toString()}; const response await axios.get(url); return (response.data.items || []).map(item ({ title: item.title, link: item.link, snippet: item.snippet, })); } } // 工厂函数根据配置创建引擎实例 function createSearchEngine(config) { switch (config.engine) { case serper: if (!config.apiKey) throw new Error(Serper引擎需要 SERPER_API_KEY); return new SerperEngine(config.apiKey); case google: if (!config.apiKey) throw new Error(Google引擎需要 GOOGLE_API_KEY); if (!config.cx) throw new Error(Google引擎需要 GOOGLE_CX); return new GoogleCustomSearchEngine(config.apiKey, config.cx); default: throw new Error(不支持的搜索引擎: ${config.engine}); } } module.exports { createSearchEngine };2. 在主服务器中使用抽象引擎修改server.js引入引擎工厂并从环境变量读取配置。// ... 顶部引入 const { createSearchEngine } require(./searchEngines.js); // 读取配置 const SEARCH_ENGINE process.env.SEARCH_ENGINE || serper; const SERPER_API_KEY process.env.SERPER_API_KEY; const GOOGLE_API_KEY process.env.GOOGLE_API_KEY; const GOOGLE_CX process.env.GOOGLE_CX; // 创建引擎实例 let searchEngine; try { const config { engine: SEARCH_ENGINE }; if (SEARCH_ENGINE serper) config.apiKey SERPER_API_KEY; if (SEARCH_ENGINE google) { config.apiKey GOOGLE_API_KEY; config.cx GOOGLE_CX; } searchEngine createSearchEngine(config); console.error([INFO] 使用搜索引擎: ${SEARCH_ENGINE}); } catch (error) { console.error([FATAL] 初始化搜索引擎失败:, error.message); process.exit(1); } // 在 tools/call 处理器中替换掉直接的axios调用 server.setRequestHandler(tools/call, async (request) { // ... 参数验证 ... try { const results await searchEngine.search(query, numResults); // 使用抽象引擎 // ... 后续格式化逻辑 ... } catch (error) { // ... 错误处理 ... } });这样通过设置不同的环境变量SEARCH_ENGINEgoogle等你就可以轻松切换底层搜索引擎而服务器的主逻辑和工具定义完全不需要改动。3. 结果后处理增强原始搜索结果可能包含广告、低质量网站或格式混乱的摘要。可以在格式化前加入简单的后处理逻辑function postProcessResults(results) { return results .filter(item { // 基础过滤剔除明显无效或低质量结果 const blacklistDomains [spam-site.com, advertisement.com]; const url new URL(item.link).hostname; const isBlacklisted blacklistDomains.some(domain url.includes(domain)); return !isBlacklisted item.title item.snippet item.snippet.length 20; }) .map(item { // 清理摘要移除多余换行和空白 item.snippet item.snippet.replace(/\s/g, ).trim(); // 可以在这里添加高亮关键词等功能 return item; }); } // 在获取results后调用 const processedResults postProcessResults(results);这些增强能小幅提升返回给模型的信息质量。5. 常见问题与排查技巧实录在实际部署和使用WebSearch-MCP的过程中你几乎一定会遇到下面这些问题。这里记录了我踩过的坑和解决方案。5.1 连接与配置问题排查表问题现象可能原因排查步骤与解决方案Claude Desktop 完全无法识别工具1. MCP配置文件路径或格式错误。2. Node命令路径错误或环境变量未生效。3. 服务器启动失败有语法或运行时错误。1.检查配置文件确认路径正确JSON格式合法可用在线校验器。重启Claude Desktop。2.检查服务器日志在终端直接运行node /path/to/server.js看是否有错误输出。确保Node版本符合要求。3.使用绝对路径在args中务必使用服务器文件的绝对路径。工具出现在列表中但调用时无反应或报错1. 环境变量在客户端配置中未正确传递。2. 搜索引擎API密钥无效或额度用尽。3. 服务器代码在tools/call处理中有未捕获的异常。1.检查客户端env配置确保API密钥等环境变量在Claude配置的env字段中正确设置。2.测试API用curl或Postman直接测试搜索引擎API确认密钥有效且有额度。3.增强服务器日志在tools/call处理函数的开头和catch块中加入详细的console.error日志查看具体错误信息。搜索返回结果为空或“未找到”1. 搜索关键词构造不佳过于复杂或模糊。2. 搜索引擎区域/语言设置问题。3. 特定网站或内容被搜索引擎过滤。1.优化提示词在工具定义的query参数描述中更明确地指导模型如“使用简洁的关键词组合”。2.调整API参数例如在Serper或Google API调用中尝试添加gl国家/地区如gl‘cn’和hl语言如hl‘zh-cn’参数。3.手动验证将模型生成的query复制到搜索引擎网页版看是否有结果。服务器进程意外退出1. 未捕获的异步异常。2. API请求超时或网络不稳定。3. 内存泄漏长时间运行后。1.全局错误监听在server.js开头添加process.on(uncaughtException, (err) { console.error(未捕获异常:, err); });和process.on(unhandledRejection, ...)。2.设置请求超时在axios调用中配置timeout: 1000010秒。3.使用进程管理工具对于生产环境使用pm2或systemd来守护进程实现崩溃自动重启。5.2 性能、成本与安全优化实践1. 速率限制Rate Limiting无限制地调用搜索API会导致成本飙升或IP被禁。必须在服务器端实现速率限制。// 简单的内存型速率限制器适用于单实例 class RateLimiter { constructor(maxRequests, windowMs) { this.maxRequests maxRequests; // 时间窗口内最大请求数 this.windowMs windowMs; // 时间窗口毫秒 this.requests []; // 记录每次请求的时间戳 } canMakeRequest() { const now Date.now(); // 清除时间窗口之外的记录 this.requests this.requests.filter(time now - time this.windowMs); if (this.requests.length this.maxRequests) { return false; } this.requests.push(now); return true; } } // 在server.js中初始化例如每分钟最多10次搜索 const searchRateLimiter new RateLimiter(10, 60 * 1000); // 在 tools/call 处理函数中检查 server.setRequestHandler(tools/call, async (request) { // ... 参数验证 ... if (!searchRateLimiter.canMakeRequest()) { return { content: [{ type: text, text: 请求过于频繁请稍后再试。当前限制为每分钟${searchRateLimiter.maxRequests}次搜索。 }], isError: true, }; } // ... 继续搜索逻辑 ... });对于多实例部署需要使用Redis等分布式存储来共享计数。2. 结果缓存对于热门或重复的查询缓存可以极大减少API调用提升响应速度并节省成本。const NodeCache require(node-cache); const queryCache new NodeCache({ stdTTL: 300 }); // 缓存5分钟 server.setRequestHandler(tools/call, async (request) { const { query, num 5 } request.params.arguments || {}; const cacheKey search:${query}:${num}; // 检查缓存 const cachedResult queryCache.get(cacheKey); if (cachedResult) { console.log([INFO] 缓存命中: ${query}); return cachedResult; // 直接返回缓存的MCP响应格式 } // ... 执行搜索 ... const mcpResponse { content: [{ type: text, text: formattedContent }] }; // 存储到缓存 queryCache.set(cacheKey, mcpResponse); return mcpResponse; });注意缓存不适合用于对实时性要求极高的查询如“最新股价”。3. 基础安全过滤虽然MCP客户端如Claude本身可能有内容安全策略但在Server端增加一层基础过滤是良好的实践。const blockedKeywords [极端内容关键词1, 非法内容关键词2 /* ... */]; // 谨慎维护此列表 function containsBlockedContent(text) { const lowerText text.toLowerCase(); return blockedKeywords.some(keyword lowerText.includes(keyword)); } // 在调用搜索API前检查query if (containsBlockedContent(query)) { return { content: [{ type: text, text: 搜索请求因包含受限内容而被拒绝。 }], isError: true, }; } // 在收到搜索结果后也可以选择性地过滤掉snippet或title中包含违规内容的结果这是一个非常基础的示例实际生产环境需要更复杂、更谨慎的内容安全方案。5.3 与不同MCP客户端的兼容性笔记不同的MCP客户端在实现上可能有细微差别以下是需要注意的地方Claude Desktop目前对MCP支持最友好。确保配置文件正确重启是使配置生效的最可靠方式。它倾向于频繁调用tools/list因此该处理函数应轻量高效。Cursor IDECursor也支持MCP。其配置方式可能与Claude不同通常需要在IDE的设置界面中指定MCP服务器的启动命令和环境变量请查阅其最新文档。自建客户端如果你在用自己的应用集成MCP请确保严格遵循MCP协议序列。正确的顺序是建立连接 - 发送initialize- 接收initialized- 发送tools/list- 之后才能处理tools/call。连接管理心跳、重连也需要自己实现。一个常见的兼容性问题是工具响应格式。MCP协议允许返回content数组其中可以包含text和image等类型。有些客户端可能对复杂的嵌套结构支持不完善。最稳妥的方式是始终返回一个type: text的content将所有信息格式化成清晰的纯文本字符串。这正是我们在示例代码中采用的方式它拥有最好的兼容性。最后调试MCP服务器的最佳方式除了查看日志就是使用一个MCP协议调试客户端比如modelcontextprotocol/sdk包自带的mcpCLI工具或者社区开发的MCP Inspector。它们可以让你直接发送原始的MCP请求并查看响应对于排查协议层面的问题非常有帮助。