当前位置: 首页 > news >正文

从零构建MCP服务:AI应用外部工具集成入门指南

1. 项目概述:从零构建你的第一个MCP服务

最近在AI应用开发圈里,MCP(Model Context Protocol)这个词的热度越来越高。如果你正在尝试将大型语言模型(LLM)的能力集成到自己的应用里,或者想为你的AI助手扩展一些自定义功能,那么绕不开的一个环节就是如何让模型安全、可控地访问外部数据和工具。MCP协议正是为了解决这个问题而生的。简单来说,它定义了一套标准化的通信方式,让AI模型可以像调用本地函数一样,去请求和使用外部服务器上的资源,而无需将敏感数据或复杂逻辑直接暴露给模型本身。

vivy-yi/mcp-tutorial这个项目,就是一个非常棒的入门实践。它不是一个复杂的生产级框架,而是一个清晰、直接的“脚手架”和“说明书”。通过它,你可以快速理解MCP的核心概念,并亲手搭建一个能实际运行的MCP服务器。无论你是前端工程师想给聊天机器人加个查天气的功能,还是后端开发者希望模型能安全地查询你的数据库,这个教程都能帮你迈出第一步。它把看似抽象的协议,拆解成了几行可以运行的代码和几个关键的配置文件,让“协议”这个词不再那么吓人。

2. MCP协议核心概念与项目价值解析

2.1 为什么我们需要MCP?

在深入代码之前,我们必须先搞清楚MCP要解决的根本问题。假设你开发了一个AI客服,用户问:“帮我查一下订单12345的物流状态。” 模型本身并不知道怎么查物流,它需要调用一个外部的“查订单”接口。最原始的做法可能是:在提示词里告诉模型一个API的地址和参数格式,然后让模型生成一个HTTP请求字符串,你再解析这个字符串去真正调用API。

这种做法问题很多:首先,提示词工程复杂且脆弱,接口一改全得重写;其次,把API密钥、数据库连接字符串等敏感信息写在提示词或模型能接触到的上下文里,是巨大的安全风险;最后,模型生成的请求格式可能五花八门,解析起来非常麻烦,错误处理更是噩梦。

MCP的解决思路很巧妙:它引入了一个“中间人”——MCP服务器。你的应用(客户端)不再直接和模型对话,而是先和MCP服务器建立连接。MCP服务器会告诉客户端:“我这里提供了哪些工具(Tools)和资源(Resources)。” 客户端再把这些工具和资源的描述,以一种标准化的格式提供给模型。当模型说“我想用那个查物流的工具,参数是订单号12345”时,客户端会把这条标准化的请求转发给MCP服务器,由服务器来执行真正的业务逻辑(比如查询数据库),并将结果返回,客户端再呈现给模型。

这样一来,敏感逻辑和数据被隔离在MCP服务器后,模型接触到的只是工具的名称和参数描述;工具的管理和版本升级也变得集中和可控;通信格式标准化,大大降低了集成复杂度。

2.2mcp-tutorial项目的定位与结构

vivy-yi/mcp-tutorial项目正是基于上述理念,为你展示了一个最小可行MCP服务器的构建过程。它没有选择用Python、Go等后端主流语言,而是使用了TypeScript/JavaScript,这其实降低了前端开发者和全栈开发者的入门门槛。项目结构通常非常简洁:

mcp-tutorial/ ├── package.json # 项目依赖和脚本定义 ├── tsconfig.json # TypeScript编译配置 ├── src/ │ ├── server.ts # MCP服务器主文件 │ └── tools/ # 自定义工具的实现目录 │ └── exampleTool.ts ├── build/ # 编译后的JS文件(可选) └── README.md # 详细的步骤说明

它的核心价值在于提供了一个“开箱即用”的模板。你不需要从零开始研究MCP协议的每一行规范,只需要关注两件事:1. 如何定义和注册一个工具(Tool);2. 如何启动服务器并与客户端(比如Claude Desktop、Cursor等)连接。项目中的server.ts文件就是一个标准的样板,清晰地展示了初始化服务器、定义工具、处理请求的完整流程。

3. 环境准备与项目初始化实操

3.1 开发环境搭建

要运行这个教程项目,你需要准备好Node.js环境。我建议使用Node.js 18或更高的LTS版本,因为一些新的ES模块特性在更老的版本上可能支持不佳。你可以通过终端命令node -v来检查当前版本。

接下来是包管理器的选择,npm是Node.js自带的,但近年来yarnpnpm因为更快的速度和更好的依赖管理而被广泛使用。这个教程项目通常使用npm,但用pnpm也完全兼容。我个人更推荐pnpm,它的磁盘空间利用率和安装速度优势在大型项目中非常明显。你可以通过npm install -g pnpm来安装它。

然后,你需要一个代码编辑器。Visual Studio Code (VSCode) 是目前最流行的选择,它对TypeScript和JavaScript的支持是顶级的,有丰富的插件生态。确保安装好VSCode,并可以考虑安装诸如“ESLint”、“Prettier”这类代码格式化和语法检查插件,它们能让你的开发体验更顺畅。

3.2 克隆项目与依赖安装

首先,打开你的终端,找一个合适的目录,比如~/projects,然后克隆教程仓库:

git clone https://github.com/vivy-yi/mcp-tutorial.git cd mcp-tutorial

进入项目目录后,第一件事就是安装依赖。项目根目录下的package.json文件已经定义好了所有必需的依赖。运行安装命令:

# 如果你用 npm npm install # 如果你用 pnpm (推荐) pnpm install

这个过程会下载@modelcontextprotocol/sdk这个核心的MCP SDK,以及TypeScript相关的编译和类型定义依赖。安装完成后,你会看到一个node_modules文件夹,里面包含了所有库文件。

注意:有时网络问题可能导致安装失败,特别是如果你在某些地区。如果遇到npm安装缓慢或失败,可以尝试切换为淘宝的镜像源:npm config set registry https://registry.npmmirror.com。使用pnpm的话,可以执行pnpm config set registry https://registry.npmmirror.com。这能显著提升依赖下载速度。

3.3 项目结构初探与配置理解

安装完依赖,让我们仔细看看几个关键文件:

  1. package.json:这是项目的“身份证”和“说明书”。重点关注scripts字段,里面定义了快捷命令,比如“dev”: “tsx src/server.ts”意味着你可以用npm run dev来启动开发服务器。dependencies里列明了项目运行必需的包,devDependencies里则是开发工具包。

  2. tsconfig.json:这是TypeScript编译器的配置文件。它告诉TypeScript如何将你的.ts代码转换成.js代码。对于这个入门项目,通常已经配置好了,你不需要修改。但可以了解一下关键选项,比如“target”: “ES2022”表示编译成较新的JavaScript标准,“module”: “commonjs”“ESNext”定义了模块系统。

  3. src/server.ts:这是我们的主战场。用VSCode打开它,你会看到导入语句、服务器初始化、工具定义和服务器启动逻辑。先不用深究每一行,有个整体印象即可。

4. 核心代码拆解:构建你的第一个MCP工具

4.1 MCP服务器初始化流程

打开src/server.ts,代码通常从导入开始。核心的导入是@modelcontextprotocol/sdk中的Server类。这个Server类就是MCP SDK提供给我们的、用于创建MCP服务器的核心对象。

import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";

第一行导入了Server类。第二行导入StdioServerTransport,这是一个“传输层”的实现。MCP协议本身是独立于通信方式的,它可以通过标准输入输出(stdio)、HTTP、WebSocket等多种方式通信。StdioServerTransport是最简单、最常用的一种,特别适合与本地客户端(如Claude Desktop)集成,因为它通过进程间的标准流进行通信,无需处理网络端口。

初始化服务器的代码一般如下:

const server = new Server( { name: "my-first-mcp-server", // 你的服务器名称 version: "0.1.0", // 版本号 }, { capabilities: { // 声明服务器支持的能力 tools: {}, // 声明支持提供“工具” // 还可以声明 resources(资源)、logging(日志)等 }, } );

这里创建了一个Server实例。第一个参数是服务器的元信息,nameversion会在客户端连接时被获取。第二个参数是服务器选项,其中capabilities字段至关重要,它像一份“菜单”,告诉客户端本服务器能提供什么服务。这里我们只声明了tools,意味着我们只提供工具调用功能。

4.2 定义与注册一个自定义工具

工具(Tool)是MCP的核心抽象之一。一个工具就是一个模型可以调用的函数,它有名字、描述、参数定义和执行逻辑。我们来看如何定义一个最简单的工具,比如一个“计算器”,它能对两个数进行加法运算。

首先,我们需要按照MCP SDK要求的格式定义工具的描述信息。这通常在一个独立的函数或对象中完成:

// 定义工具的描述(Schema) const calculatorTool = { name: "calculate_sum", // 工具的唯一标识,模型通过这个名字调用它 description: "计算两个整数的和。", // 给模型的自然语言描述,至关重要! inputSchema: { type: "object", properties: { a: { type: "number", description: "第一个加数", }, b: { type: "number", description: "第二个加数", }, }, required: ["a", "b"], // 声明哪些参数是必需的 }, };

inputSchema是一个JSON Schema对象,它严格定义了工具接受的参数格式。这里我们定义了两个属性ab,类型都是number,并且都是必需的。清晰的description能帮助模型更好地理解何时以及如何使用这个工具。

定义好描述后,我们需要实现工具的实际执行逻辑,并将其注册到服务器上:

// 实现工具的处理函数 async function handleCalculateSum(params: { a: number; b: number }) { const { a, b } = params; const sum = a + b; // 返回一个符合MCP协议的结果对象 return { content: [ { type: "text", text: `计算结果:${a} + ${b} = ${sum}`, }, ], }; } // 将工具注册到服务器 server.setRequestHandler( // 这是MCP协议中定义的一个标准请求类型,用于处理工具调用 // 当客户端转发模型的工具调用请求时,会触发这个处理器 // 处理器函数会收到一个包含 `params` 等信息的请求对象 // 我们需要从中提取参数,调用对应的处理函数,并返回标准格式的结果 // 下面的代码展示了如何将我们定义的 `calculatorTool` 和 `handleCalculateSum` 函数关联起来 // 当模型调用 `calculate_sum` 工具时,`handleCalculateSum` 函数就会被执行 // 这种设计实现了工具描述与执行逻辑的解耦,非常清晰 // 你可以很容易地在这里添加更多的工具,只需要重复定义和注册的步骤即可 // 服务器启动后,它会通过 `initialize` 握手过程,将已注册的所有工具列表告知客户端 // 客户端再将这些工具的描述注入到模型的上下文中,模型便“知道”了可以调用哪些工具 // 整个流程的自动化程度很高,开发者只需要关注工具本身的定义和实现 // 接下来,我们将看到如何启动服务器并建立连接 // 这是将我们的代码变为一个可用服务的关键一步 // 我们将使用 `StdioServerTransport`,它是最简单的集成方式 // 适合与支持MCP协议并通过标准输入输出通信的客户端(如一些AI桌面应用)进行对接 // 启动后,服务器会进入监听状态,等待客户端的连接请求 // 一旦连接建立,之前注册的工具就可以被模型使用了 // 这个过程涉及协议层面的握手、能力交换等,但SDK已经帮我们封装好了 // 我们只需要调用几行简单的启动代码即可 // 现在,让我们回到代码,完成工具的注册和服务的启动 // 下面的代码片段展示了完整的注册和启动流程 // 请注意其中的错误处理和日志输出,这对于调试至关重要 // 一个健壮的服务器应该能够处理连接异常和无效请求 // 好的,我们继续补充具体的代码实现 );

上面的注释解释了注册的逻辑,但缺少了具体的代码。让我们补全它:

// 将工具注册到服务器 server.setRequestHandler( // 这是MCP协议中定义的一个标准请求类型,用于处理工具调用 // 当客户端转发模型的工具调用请求时,会触发这个处理器 // 处理器函数会收到一个包含 `params` 等信息的请求对象 // 我们需要从中提取参数,调用对应的处理函数,并返回标准格式的结果 // 下面的代码展示了如何将我们定义的 `calculatorTool` 和 `handleCalculateSum` 函数关联起来 // 当模型调用 `calculate_sum` 工具时,`handleCalculateSum` 函数就会被执行 // 这种设计实现了工具描述与执行逻辑的解耦,非常清晰 // 你可以很容易地在这里添加更多的工具,只需要重复定义和注册的步骤即可 // 服务器启动后,它会通过 `initialize` 握手过程,将已注册的所有工具列表告知客户端 // 客户端再将这些工具的描述注入到模型的上下文中,模型便“知道”了可以调用哪些工具 // 整个流程的自动化程度很高,开发者只需要关注工具本身的定义和实现 // 接下来,我们将看到如何启动服务器并建立连接 // 这是将我们的代码变为一个可用服务的关键一步 // 我们将使用 `StdioServerTransport`,它是最简单的集成方式 // 适合与支持MCP协议并通过标准输入输出通信的客户端(如一些AI桌面应用)进行对接 // 启动后,服务器会进入监听状态,等待客户端的连接请求 // 一旦连接建立,之前注册的工具就可以被模型使用了 // 这个过程涉及协议层面的握手、能力交换等,但SDK已经帮我们封装好了 // 我们只需要调用几行简单的启动代码即可 // 现在,让我们回到代码,完成工具的注册和服务的启动 // 请注意其中的错误处理和日志输出,这对于调试至关重要 // 一个健壮的服务器应该能够处理连接异常和无效请求 // 好的,我们继续补充具体的代码实现 // 首先,我们需要导入工具调用请求的类型定义 // 然后,在 `setRequestHandler` 中,我们为 `ToolsCallRequest` 类型设置处理器 // 处理器函数需要检查请求中的工具名称,并路由到对应的处理函数 // 下面是一个完整的示例: );

让我们用具体的代码替换上面的长注释:

import { Server } from "@modelcontextprotocol/sdk/server/index.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; // 导入协议定义中的请求类型 import { ToolsCallRequest } from "@modelcontextprotocol/sdk/types.js"; const server = new Server( { name: "my-first-mcp-server", version: "0.1.0", }, { capabilities: { tools: {}, }, } ); // 定义工具 const calculatorTool = { name: "calculate_sum", description: "计算两个整数的和。", inputSchema: { type: "object", properties: { a: { type: "number", description: "第一个加数" }, b: { type: "number", description: "第二个加数" }, }, required: ["a", "b"], }, }; // 工具处理函数 async function handleCalculateSum(params: { a: number; b: number }) { const sum = params.a + params.b; return { content: [{ type: "text", text: `计算结果:${params.a} + ${params.b} = ${sum}`, }], }; } // 注册工具列表(在初始化时告知客户端我们有哪些工具) server.setRequestHandler( // 这是一个特殊的请求类型 `list_tools`,客户端在连接后会调用它来获取工具列表 // 我们需要返回一个包含所有工具定义的数组 // 下面的代码展示了如何处理 `list_tools` 请求 // 注意:`setRequestHandler` 可以针对不同的请求类型设置不同的处理器 // 我们需要为 `list_tools` 和 `tools/call` 分别设置 // 首先处理 `list_tools` );

我们需要为两种类型的请求设置处理器:list_tools(列出工具)和tools/call(调用工具)。让我们补全:

// 1. 处理 list_tools 请求:当客户端询问“你有什么工具?”时响应 server.setRequestHandler( // 注意:SDK的 `setRequestHandler` 方法通常用于设置特定请求类型的处理器 // 但更常见的模式是使用 `server.setRequestHandler` 配合一个条件判断 // 或者使用SDK提供的更高级的抽象。为了清晰,我们分步说明。 // 首先,我们告诉服务器,当收到 `list_tools` 请求时,返回我们定义的工具数组。 // 在MCP SDK中,这通常通过为 `list_tools` 请求类型注册一个异步函数来实现。 // 函数内部直接返回 `{ tools: [calculatorTool] }` 这样的结构。 // 其次,我们需要为 `tools/call` 请求类型注册处理器。 // 当模型通过客户端发起工具调用时,客户端会发送一个 `tools/call` 请求到服务器。 // 这个请求会包含工具名 `name` 和参数 `arguments`。 // 我们的处理器需要根据 `name` 找到对应的处理函数(如 `handleCalculateSum`), // 传入 `arguments`,执行它,并返回标准格式的结果。 // 下面的代码将展示这两种处理器的完整写法。 // 请注意错误处理:如果收到未知的工具名,应该返回一个错误响应。 // 现在,让我们写出具体的代码。 ); // 更具体的实现如下: server.setRequestHandler( // 处理 list_tools async (request) => { if (request.method === "tools/list") { // 注意:协议中方法名可能是 `tools/list` return { tools: [calculatorTool], }; } // 对于其他未处理的请求方法,可以返回null或抛出错误,SDK会处理 return null; } ); // 处理 tools/call server.setRequestHandler( async (request) => { if (request.method === "tools/call") { const params = request.params as ToolsCallRequest["params"]; const toolName = params.name; const args = params.arguments as { a: number; b: number }; // 根据工具定义进行类型断言 if (toolName === "calculate_sum") { try { const result = await handleCalculateSum(args); return { content: result.content, }; } catch (error) { // 返回错误信息 return { content: [{ type: "text", text: `调用工具 ${toolName} 时出错:${error}`, }], isError: true, // 标记这是一个错误响应 }; } } else { // 工具不存在 return { content: [{ type: "text", text: `未知的工具:${toolName}`, }], isError: true, }; } } return null; } );

实操心得:在定义inputSchema时,description字段的质量直接决定了模型使用工具的准确度。不要写“参数a”,而要写“订单号”或“城市名称”。好的描述能让模型“理解”参数的含义,减少误用。另外,工具名name最好使用蛇形命名(snake_case),如get_weather,这与许多AI系统的习惯保持一致。

4.3 启动服务器与建立连接

工具注册好后,最后一步就是启动服务器,让它开始监听客户端的连接。这通常只需要几行代码:

async function main() { // 创建一个基于标准输入输出的传输层 const transport = new StdioServerTransport(); // 将服务器连接到这个传输层 await server.connect(transport); // 打印日志,提示服务器已启动 console.error("MCP服务器已启动,正在通过 stdio 等待连接..."); } main().catch((error) => { console.error("服务器启动失败:", error); process.exit(1); });

StdioServerTransport()创建了一个使用标准输入(stdin)和标准输出(stdout)进行通信的传输通道。这意味着我们的服务器期望从 stdin 读取请求,并将响应写入 stdout。这是一种非常简单的进程间通信方式,许多AI客户端(如Claude Desktop)支持通过配置命令行参数来启动这样的服务器进程并与之通信。

server.connect(transport)是关键,它建立了服务器与传输通道的绑定。调用后,服务器就进入了事件循环,等待客户端发起连接握手。

console.error用于输出日志信息到标准错误流(stderr)。这是一个好习惯,因为正常的协议通信输出到 stdout,而调试信息输出到 stderr,可以避免混淆。

最后,我们用main().catch(...)来捕获并处理启动过程中可能出现的任何未捕获异常,防止进程无声无息地崩溃。

5. 编译、运行与客户端配置实战

5.1 编译TypeScript与运行服务器

我们的代码是TypeScript写的,需要编译成JavaScript才能被Node.js执行。package.json里通常已经配置好了脚本。检查一下scripts部分,可能会有:

"scripts": { "build": "tsc", "dev": "tsx src/server.ts" }
  • npm run build:使用TypeScript编译器(tsc)将整个src目录下的.ts文件编译到builddist目录,生成.js文件。适合生产环境。
  • npm run dev:使用tsxts-node这类工具,直接运行.ts文件,无需显式编译。适合开发环境,修改代码后可以快速重启测试。

对于学习和测试,我们直接使用开发模式:

npm run dev # 或 pnpm dev

如果一切正常,你会在终端看到输出:“MCP服务器已启动,正在通过 stdio 等待连接...”。此时服务器已经在运行,但它只是在等待输入,因为没有客户端连接它。要测试它,我们需要一个MCP客户端。

5.2 使用MCP Inspector进行本地测试

在集成到真正的AI应用(如Claude Desktop)之前,我们可以用一个官方提供的调试工具——MCP Inspector 来测试我们的服务器。它是一个命令行工具,可以模拟客户端连接你的服务器,并允许你手动调用工具。

首先,全局安装MCP Inspector:

npm install -g @modelcontextprotocol/inspector

然后,在一个新的终端窗口,导航到你的项目目录,运行以下命令来启动Inspector并连接到你的服务器:

# 假设你的服务器通过 `npm run dev` 启动,它本质上是在运行 `tsx src/server.ts` # Inspector需要知道如何启动你的服务器。我们可以用 `npx` 来直接运行本地脚本 # 但更简单的方式是,如果你的服务器脚本可以直接被Node执行(比如编译后的JS),可以这样: # mcp-inspector node ./build/server.js # 对于开发中的TypeScript项目,我们可以让Inspector直接调用 `npm run dev` 这个命令 # 注意:Inspector会启动一个新的子进程来运行你给的命令 # 你需要告诉它命令和参数。在项目根目录下运行: mcp-inspector npm run dev

运行后,MCP Inspector 会启动一个新的进程来运行npm run dev,并与之建立stdio连接。然后,Inspector会打开一个本地网页(通常是http://localhost:5173),你可以在浏览器中访问这个页面。

在Inspector的Web界面里,你应该能看到一个“Tools”标签页,里面列出了你的服务器提供的工具(例如calculate_sum)。你可以点击这个工具,在右侧的面板中输入参数(如{"a": 5, "b": 3}),然后点击“Call”按钮。如果一切正常,你会在下方看到返回结果:“计算结果:5 + 3 = 8”。

注意事项:使用Inspector时,确保你的服务器代码没有语法错误,并且已经正确导出了工具列表。如果Inspector页面显示“No tools”或者连接失败,请检查:

  1. 服务器进程是否成功启动并输出了等待连接的日志。
  2. 服务器的list_tools处理器是否正确返回了工具数组。
  3. 浏览器控制台(F12)是否有网络错误。
  4. Inspector终端是否有错误输出。

5.3 配置Claude Desktop集成(示例)

真正的价值在于让AI助手使用你的工具。以Anthropic推出的Claude Desktop应用为例,它支持通过MCP协议集成自定义服务器。配置通常通过一个JSON配置文件完成。

首先,找到Claude Desktop的配置目录:

  • macOS:~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows:%APPDATA%\Claude\claude_desktop_config.json
  • Linux:~/.config/Claude/claude_desktop_config.json

如果文件不存在,就创建一个。然后,编辑这个文件,添加mcpServers配置项。你需要告诉Claude如何启动你的服务器。假设你的项目在/Users/yourname/projects/mcp-tutorial,并且通过npm run dev启动:

{ "mcpServers": { "my-calculator-server": { "command": "node", "args": [ "/Users/yourname/projects/mcp-tutorial/build/server.js" ], "env": { "NODE_ENV": "production" } } } }

重要:这里args指向的是编译后的JavaScript文件(build/server.js),而不是TypeScript源文件。因此,你需要先运行npm run build来生成build/server.jscommandnode,意思是Claude Desktop会执行node /path/to/build/server.js这个命令来启动你的服务器进程。

保存配置文件后,完全重启Claude Desktop应用。重启后,Claude Desktop会在后台启动你配置的MCP服务器进程。当你打开Claude并开始一个新的对话时,Claude模型就应该“知道”它可以使用calculate_sum这个工具了。你可以尝试输入:“请帮我计算一下123和456的和。” Claude应该会识别出意图,并调用你的工具,最终返回结果。

踩坑记录:最常见的集成失败原因有三个。第一,路径错误,确保args中的文件路径是绝对路径,并且文件确实存在。第二,权限问题,确保Node.js有权限执行该文件。第三,服务器启动失败,可能是代码有运行时错误。一个调试技巧是,先手动在终端用配置中的命令(如node /path/to/build/server.js)运行一下,看服务器是否能正常启动并等待连接。如果手动运行都报错,那在Claude Desktop里也一定会失败。

6. 进阶开发:实现一个实用的天气查询工具

掌握了基础流程后,我们来构建一个更真实、更有用的工具:一个查询城市天气的MCP工具。这将涉及到调用外部API、处理异步请求和错误处理。

6.1 设计工具接口与选择天气API

首先,我们需要设计工具的输入输出。输入应该是一个城市名(字符串),输出应该是结构化的天气信息,比如温度、天气状况、湿度等。

接下来是选择天气API。有很多免费或付费的选项,比如 OpenWeatherMap、WeatherAPI、和风天气等。这里我们以 OpenWeatherMap 为例,因为它提供免费的层级(虽然需要注册获取API Key)。我们将实现一个get_weather工具。

工具定义如下:

const weatherTool = { name: "get_weather", description: "获取指定城市的当前天气信息。", inputSchema: { type: "object", properties: { city: { type: "string", description: "城市名称,例如:Beijing, London, New York。支持中文和英文城市名。", }, }, required: ["city"], }, };

6.2 集成第三方API与处理敏感信息

调用外部API通常需要密钥。绝对不要将API密钥硬编码在源代码中或提交到版本控制系统(如Git)。正确的做法是使用环境变量。

首先,在项目根目录创建一个.env文件(确保该文件已在.gitignore中):

OPENWEATHER_API_KEY=your_actual_api_key_here

然后,安装dotenv包来加载环境变量:

pnpm add dotenv

server.ts文件的开头(在所有其他导入之后),加载环境变量:

import * as dotenv from 'dotenv'; dotenv.config(); const OPENWEATHER_API_KEY = process.env.OPENWEATHER_API_KEY; if (!OPENWEATHER_API_KEY) { console.error("错误:未设置 OPENWEATHER_API_KEY 环境变量。请在 .env 文件中配置。"); // 可以选择退出进程,或者以“无天气功能”的模式运行 // process.exit(1); }

现在,实现天气查询的处理函数。我们需要使用node-fetchaxios来发起HTTP请求。Node.js 18+ 内置了fetch,所以我们可以直接使用:

async function handleGetWeather(params: { city: string }) { const { city } = params; const apiKey = OPENWEATHER_API_KEY; if (!apiKey) { return { content: [{ type: "text", text: "天气服务未正确配置。", }], isError: true, }; } // 构建请求URL。OpenWeatherMap的当前天气API端点 // 注意:这里使用了按城市名查询的接口。对于中文城市名,API支持吗?可能需要处理编码。 // 一个更健壮的做法是先用一个地理编码API将城市名转换为经纬度。 // 但为了示例简单,我们直接使用城市名。 const url = new URL("https://api.openweathermap.org/data/2.5/weather"); url.searchParams.append("q", city); url.searchParams.append("appid", apiKey); url.searchParams.append("units", "metric"); // 使用摄氏度 url.searchParams.append("lang", "zh_cn"); // 使用中文描述 try { const response = await fetch(url); if (!response.ok) { // 如果API返回错误(如城市未找到、密钥无效) const errorData = await response.json(); throw new Error(`天气API请求失败: ${response.status} - ${errorData.message || 'Unknown error'}`); } const data = await response.json(); // 解析API返回的JSON数据 const weatherDesc = data.weather[0]?.description || "未知"; const temp = data.main?.temp; const humidity = data.main?.humidity; const cityName = data.name; return { content: [{ type: "text", text: `城市:${cityName}\n天气状况:${weatherDesc}\n温度:${temp}°C\n湿度:${humidity}%`, }], }; } catch (error: any) { // 网络错误或JSON解析错误 console.error("获取天气失败:", error); return { content: [{ type: "text", text: `无法获取 ${city} 的天气信息:${error.message}`, }], isError: true, }; } }

6.3 工具注册与错误处理优化

现在,我们需要将这个新工具注册到服务器。更新list_tools处理器,将weatherTool也加入返回数组:

// 更新 list_tools 处理器 server.setRequestHandler(async (request) => { if (request.method === "tools/list") { return { tools: [calculatorTool, weatherTool], // 添加 weatherTool }; } return null; });

同样,更新tools/call处理器,增加对get_weather的路由:

// 在 tools/call 处理器中添加新的条件分支 if (toolName === "get_weather") { try { const result = await handleGetWeather(args as { city: string }); return { content: result.content, isError: result.isError, }; } catch (error: any) { return { content: [{ type: "text", text: `调用天气工具时出错:${error.message}`, }], isError: true, }; } }

实操心得:处理外部API调用时,错误处理必须细致。至少区分几种情况:1. 网络超时或不可达;2. API返回非200状态码(如404城市未找到、401密钥无效);3. API返回的数据格式不符合预期。为每种情况提供清晰的错误信息,不仅便于调试,也能让最终用户(或AI模型)理解发生了什么。另外,考虑给API请求加上超时设置(AbortSignal),避免服务器被慢响应拖死。

7. 调试技巧、常见问题与性能考量

7.1 服务器端调试

当你的MCP服务器行为不符合预期时,可以从以下几个层面排查:

  1. 日志输出:在关键位置添加console.error日志。例如,在main()函数开始、工具处理函数被调用时、API请求前后。因为MCP通信使用stdio,你的日志输出到stderr不会干扰协议通信,可以在启动服务器的终端窗口看到。

  2. 使用MCP Inspector:如前所述,Inspector是强大的调试工具。它不仅允许你手动调用工具,还能显示原始的协议消息交换。在Inspector的“Logs”或“Messages”标签页,你可以看到客户端和服务器之间发送的每一条JSON-RPC消息。这对于理解握手过程、检查请求和响应格式是否正确至关重要。

  3. 验证工具定义:确保你的工具inputSchema是有效的JSON Schema。一个常见的错误是属性类型写错(如“string”写成了String)。你可以使用在线的JSON Schema验证器来检查。

  4. 检查环境变量:确保.env文件已创建,变量名正确,并且在代码中能正确读取。可以在服务器启动后立即打印一下process.env.OPENWEATHER_API_KEY的前几个字符(不要打印完整的密钥)来确认。

7.2 客户端集成问题

  1. Claude Desktop不识别工具

    • 症状:在Claude对话中,模型完全不提你的工具。
    • 排查:首先确认Claude Desktop已重启。检查配置文件路径和格式是否正确。最有效的方法是查看Claude Desktop的日志。在macOS上,可以在终端运行log stream --process Claude来实时查看日志,搜索“MCP”或你的服务器名,看是否有错误信息。
  2. 工具调用失败

    • 症状:模型尝试调用工具,但返回错误,如“Tool call failed”。
    • 排查:在服务器日志中查看对应的错误信息。可能是工具处理函数抛出了未捕获的异常。确保你的处理函数有完整的try...catch,并将错误信息通过isError: true返回。在Inspector中手动用相同参数调用,看是否能复现错误。
  3. 服务器进程意外退出

    • 症状:第一次能调用,后来就不行了,或者Claude Desktop启动时报错。
    • 排查:可能是服务器代码中存在未处理的异常导致进程崩溃。用process.on('uncaughtException', ...)process.on('unhandledRejection', ...)全局捕获异常并记录日志。另外,检查服务器是否在处理完请求后错误地调用了process.exit()

7.3 性能与生产环境考量

目前的示例是一个简单的脚本,适合学习和原型开发。如果要用于生产环境,需要考虑更多:

  1. 进程模型:目前是每个客户端连接启动一个独立的服务器进程(Claude Desktop就是这样做的)。对于轻量级工具这没问题。但如果工具初始化很重(例如加载大模型),或者需要维护状态,你可能需要改为常驻的HTTP服务器,让多个客户端连接同一个进程。MCP SDK也支持HTTPServerTransport

  2. 资源管理与超时:为长时间运行的工具调用设置超时。可以使用Promise.raceAbortController。避免在工具处理函数中进行同步的、阻塞事件循环的操作。

  3. 安全性强化

    • 输入验证:虽然inputSchema定义了类型,但在处理函数内部仍要对输入进行业务逻辑验证(例如,城市名是否非空字符串)。
    • 输出净化:如果工具返回的内容可能包含用户输入(如从数据库查询的结果),要小心防范注入攻击(虽然MCP上下文下风险较低,但好习惯要保持)。
    • 权限控制:未来如果工具更复杂,可能需要根据客户端或用户身份进行权限校验。这可以在工具处理函数中实现,通过分析请求的上下文(如果协议支持传递此类信息)。
  4. 可观测性:添加更结构化的日志(使用Winston、Pino等库),记录工具调用的耗时、成功率。考虑集成监控和告警。

7.4 扩展方向:资源(Resources)与提示词模板

MCP协议除了工具(Tools),还定义了资源(Resources)。资源代表模型可以读取的静态或动态内容,比如一个文件、一个数据库查询的结果集、甚至一个网页的实时内容。你可以通过实现resources/listresources/read请求处理器来暴露资源。例如,创建一个工具来读取服务器上的一个配置文件内容,或者提供一个实时股票数据流。

另一个强大的功能是提示词模板(Prompts)。虽然MCP协议标准可能还在演进中,但核心思想是服务器可以提供预定义的提示词片段,客户端可以组合使用它们来构建更复杂的系统提示词。这有助于实现提示词的模块化和复用。

通过vivy-yi/mcp-tutorial这个项目入门后,你已经掌握了MCP最核心的工具调用机制。接下来,你可以参考官方的MCP协议文档和SDK源码,探索这些更高级的特性,将你的AI应用与内部系统更深度、更安全地集成起来。记住,关键是把复杂的、敏感的、动态的逻辑封装在MCP服务器后面,为AI模型提供一个干净、标准、安全的操作界面。

http://www.jsqmd.com/news/828423/

相关文章:

  • RP2040内置温度传感器开发指南:从原理到实践
  • 3步解锁闲置电视盒子:Amlogic S9xxx系列Armbian系统全攻略
  • Winhance中文版:5分钟快速优化Windows系统的终极指南
  • 基于跨平台转换引擎的智能图层传输系统:企业级动效工作流解决方案
  • 终极指南:使用Tinke轻松探索和修改NDS游戏资源
  • 人工智能的经济学 — 自动化对工人意味着什么?
  • 百度网盘Mac版终极加速方案:免费解锁SVIP级下载体验
  • 如何通过WebPShop插件在Photoshop中实现专业级WebP图像优化
  • 3步解决容器镜像拉取难题:DaoCloud公开镜像仓库加速实战指南
  • MonitorControl架构重构:基于DDC/CI协议的多显示器硬件控制方案
  • LSM6DS3TR-C与磁力计融合:Mahony算法实现高精度姿态解算
  • 别再只搭个单机版了!用CentOS 7和MinIO打造一个带域名访问的私有图床/文件分享服务
  • 在控制台中管理多项目API Key与设置访问权限
  • Agent Teams / Swarms(智能体团队/蜂群)
  • 5分钟掌握B站缓存视频转换:m4s-converter终极使用指南
  • Path of Building终极指南:流放之路Build规划完整教程
  • 如何3分钟完成漫画翻译:BallonsTranslator深度学习辅助工具终极指南
  • Noto Emoji终极指南:如何在5分钟内彻底解决表情符号乱码问题
  • Claude for Small Business发布:AI与传统软件结合,能否颠覆SaaS市场?
  • 如何快速掌握Sigil:开源EPUB编辑器的完整使用指南
  • 构建垂直领域RAG引擎:从检索增强生成到人才招聘智能问答实践
  • 图像质量评估新纪元:AI算法如何为百万图片精准打分
  • 新手避坑指南:在CCS v5/v6上为TMS320C6678创建第一个LED闪烁工程(附完整CMD文件配置)
  • 从零开始:如何用EasyOCR轻松实现多语言文字识别
  • 终结 Vibe Coding(Harness Engineering)!深度拆解 ralph:以交付所有 PRD 为生命周期的自主 AI Agent 闭环
  • 告别DDPG训练不稳定:手把手教你用TD3算法搞定连续控制任务(附PyTorch代码)
  • 终极JSXBIN解码器完整指南:如何快速恢复Adobe脚本源代码
  • 省90%成本!你还在为大模型调用费发愁吗?
  • Vue2项目里,用lodash的debounce给搜索框‘降降温’(附完整代码和常见坑点)
  • Midjourney黑白摄影风格权威测评:基于1,842组测试样本,验证哪3种--s参数区间真正适配银盐颗粒模拟