资讯动态

使用 NestJS 与 @rekog/mcp-nest 构建企业级 MCP 服务器

发布时间:2026/9/10 0:50:07 来源:尧图企业网站定制
1. 项目概述当 NestJS 遇见 MCP如果你正在用 NestJS 构建后端服务同时又想让你的服务能力比如查询数据库、调用内部 API、处理文件能够被 AI 智能体比如 Claude Desktop、Cursor、Windsurf直接调用那么rekog/mcp-nest这个库就是你一直在找的“桥梁”。简单来说它让你能用最熟悉的 NestJS 开发范式——依赖注入、装饰器、模块——来快速构建一个符合Model Context Protocol (MCP)标准的服务器。MCP 是什么你可以把它理解成 AI 世界的“USB 协议”。过去每个 AI 应用如 IDE 插件要接入你的服务都得写一套特定的适配代码繁琐且不通用。MCP 定义了一套标准让 AI 工具能以统一的方式发现、调用外部工具、读取资源和获取提示词。而rekog/mcp-nest则让你在 NestJS 这个强大的企业级框架内轻松实现这套协议把现有的业务逻辑直接暴露给 AI。我最初接触这个项目是因为团队内部有大量用 NestJS 写的微服务我们想探索如何让 AI 助手能安全、可控地调用这些服务来完成一些自动化任务比如生成周报数据、查询项目状态。手动为每个服务写 MCP 适配层太痛苦了而这个库的出现让我们几乎零成本地将现有服务“MCP 化”。接下来我会结合自己的实践拆解如何从零开始用它构建一个功能完备、安全可靠的 MCP 服务器。2. 核心设计思路为何选择rekog/mcp-nest在决定使用rekog/mcp-nest之前我也评估过直接使用官方的modelcontextprotocol/sdk或者用 Express/Fastify 从头搭建。最终选择它主要是基于以下几个在真实企业级开发中无法回避的考量点2.1 与现有 NestJS 生态无缝融合这是最大的优势。如果你的团队已经在使用 NestJS那么代码库中必然充斥着大量的Injectable()服务类、复杂的依赖关系以及模块化的架构。rekog/mcp-nest允许你直接在现有的服务类上通过添加Tool()装饰器就将其方法暴露为 MCP 工具。这意味着零重构成本你不需要为了适配 MCP 而重写业务逻辑。依赖注入DI完整可用你的工具方法内部可以像平常一样注入其他服务、仓库Repository、配置模块等。这对于调用数据库、消息队列、外部 API 等场景至关重要。统一的架构风格团队不需要学习一套新的开发模式维护成本低。2.2 开箱即用的企业级特性这个库不是简单的协议包装它考虑了很多生产环境的需求多传输协议支持它同时支持 HTTPSSE、Streamable HTTP 和 STDIO 三种通信方式。这在部署时给你极大的灵活性。例如在 Docker 容器或 Serverless 环境中HTTP 更合适而在本地开发或 CLI 工具集成时STDIO 模式更方便。内置安全框架它深度集成了 NestJS 的 Guards守卫和 Passport 策略可以轻松实现基于 OAuth2、JWT 甚至自定义 Token 的认证和授权。你甚至可以做到“按工具授权”即不同的 AI 用户或角色只能调用特定的工具集。完善的资源与提示词系统MCP 不仅仅是工具调用还包括资源提供只读数据如文档、配置文件和提示词模板。该库提供了清晰的方式来定义这两者特别是资源模板功能能让你创建动态的、带参数的资源 URI非常强大。2.3 开发体验与可维护性基于 Zod 的声明式参数验证工具的参数定义使用 Zod Schema这比手动写 JSON Schema 要直观和类型安全得多。NestJS 本身也推荐使用类验证器Zod 是当前社区更流行的选择类型推断完美。自动发现与手动注册并存工具可以通过装饰器自动被发现也支持在模块层面动态注册。这为插件化架构或根据配置动态启用工具提供了可能。完善的进度报告与上下文工具方法可以接收到一个Context对象用于报告执行进度、访问原始的 HTTP 请求信息如 headers这对于实现长任务和审计日志非常有用。注意选择这个库也意味着你接受了 NestJS 的“重量级”和一定的学习曲线。如果你的项目非常轻量且没有使用 NestJS那么直接使用底层 SDK 可能更简单。但对于任何有一定复杂度的、已有 NestJS 基础的项目这个库的收益是巨大的。3. 从零开始构建你的第一个 MCP 工具服务器理论说再多不如动手试。我们从一个最简单的例子开始构建一个能通过 AI 调用的“问候工具”和“待办事项管理工具”。3.1 环境准备与项目初始化首先确保你有一个 NestJS 项目。如果没有可以用 CLI 快速创建一个# 全局安装 NestJS CLI (如果尚未安装) npm i -g nestjs/cli # 创建新项目 nest new my-mcp-server cd my-mcp-server # 安装核心依赖 npm install rekog/mcp-nest modelcontextprotocol/sdk zod这里安装的三个包分别是rekog/mcp-nest: 我们的主角NestJS 模块。modelcontextprotocol/sdk: MCP 协议的核心 SDK是前者的 peer dependency。zod: 用于定义工具参数的模式验证库。3.2 核心模块配置接下来在根模块app.module.ts中导入并配置McpModule。// src/app.module.ts import { Module } from nestjs/common; import { McpModule } from rekog/mcp-nest; Module({ imports: [ McpModule.forRoot({ // 服务器元信息会在 MCP 初始化时告知客户端 name: my-todo-mcp-server, version: 1.0.0, // 可选指定传输方式默认为 stdio。这里我们先使用 stdio 便于本地测试。 // transport: stdio, // 如果使用 HTTP需要配置更多选项后面会讲到。 }), ], }) export class AppModule {}forRoot方法是配置的入口。最基本的配置就是name和version。至此一个最基础的 MCP 服务器框架就搭好了但它还没有任何可用的工具。3.3 创建你的第一个工具在 NestJS 中工具本身就是一个普通的、可注入的服务类。我们创建一个greeting.tool.ts// src/tools/greeting.tool.ts import { Injectable } from nestjs/common; import { Tool, Context } from rekog/mcp-nest; import { z } from zod; Injectable() // 标准的 NestJS 可注入服务 export class GreetingTool { /** * 使用 Tool() 装饰器将一个方法声明为 MCP 工具。 * 装饰器接收一个配置对象其中 * - name: 工具的唯一标识符AI 客户端通过这个名称调用。 * - description: 工具的描述AI 会根据这个描述决定何时使用该工具。务必写清楚 * - parameters: 用 Zod Schema 定义工具的参数和类型。 */ Tool({ name: get_greeting, description: 根据提供的姓名生成一句问候语。如果未提供姓名则使用默认值。, parameters: z.object({ // 定义 name 参数为字符串类型并设置默认值。 // Zod 的验证规则如 .min(1)也会被转换成 MCP 的 JSON Schema。 name: z.string().min(1, 姓名不能为空).default(World), }), }) async sayHello( // 第一个参数是工具调用时传入的参数对象其类型会根据上面的 Zod Schema 自动推断。 params: { name: string }, // 第二个参数是 Context 对象提供了额外的运行时信息和控制能力。 context: Context ): Promisestring { // 工具可以返回任何可 JSON 序列化的值或者一个 Promise。 // 使用 context.reportProgress 可以向客户端报告执行进度。 // 这对于执行时间较长的工具非常有用能让 AI 界面显示进度条。 await context.reportProgress({ progress: 30, total: 100, message: 正在生成问候语... }); // 模拟一些工作 await new Promise(resolve setTimeout(resolve, 100)); await context.reportProgress({ progress: 100, total: 100, message: 完成 }); // 返回值就是工具的执行结果会被发送回 AI 客户端。 return Hello, ${params.name}! Welcome to the MCP world.; } }创建好工具后需要将它作为提供者Provider添加到模块中这样 NestJS 的依赖注入容器才能管理它并且rekog/mcp-nest才能自动发现它上面的Tool装饰器。// src/app.module.ts import { Module } from nestjs/common; import { McpModule } from rekog/mcp-nest; import { GreetingTool } from ./tools/greeting.tool; Module({ imports: [ McpModule.forRoot({ name: my-todo-mcp-server, version: 1.0.0, }), ], providers: [GreetingTool], // 在这里注册工具服务 }) export class AppModule {}3.4 运行与测试现在我们可以运行这个服务器了。由于我们在配置中使用了默认的stdio传输服务器会通过标准输入输出进行通信。我们需要一个 MCP 客户端来测试它。最方便的方法是使用modelcontextprotocol/sdk包中自带的mcp命令行工具或者使用像Claude Desktop这样的已经集成了 MCP 客户端的应用。方法一使用mcpCLI 工具测试首先全局安装 MCP SDK 的 CLInpm install -g modelcontextprotocol/sdk然后我们需要创建一个 MCP 客户端的配置文件。在与你的 NestJS 项目同级的目录下创建一个client-test.mjs文件// client-test.mjs import { Client } from modelcontextprotocol/sdk/client/index.js; import { StdioClientTransport } from modelcontextprotocol/sdk/client/stdio.js; async function main() { // 1. 创建客户端 const client new Client( { name: test-client, version: 1.0.0 }, { capabilities: {} } ); // 2. 创建传输层这里启动我们 NestJS 应用的进程 const transport new StdioClientTransport({ command: node, // 命令 args: [dist/main.js], // 参数指向我们编译后的 NestJS 主文件 }); // 3. 连接 await client.connect(transport); console.log(Connected to MCP server.); // 4. 列出可用的工具 const { tools } await client.listTools(); console.log(Available tools:, tools.map(t t.name)); // 5. 调用我们定义的 get_greeting 工具 const result await client.callTool({ name: get_greeting, arguments: { name: Developer }, // 传入参数 }); console.log(Tool call result:, result.content?.[0]?.text); // 6. 断开连接 await client.close(); } main().catch(console.error);运行这个测试脚本之前需要先编译并运行你的 NestJS 应用# 在项目根目录下 npm run build # 编译 TypeScript node client-test.mjs # 运行测试客户端如果一切正常你会在控制台看到 “Available tools: [ ‘get_greeting’ ]” 和 “Tool call result: Hello, Developer! Welcome to the MCP world.” 的输出。方法二集成到 Claude Desktop更直观找到 Claude Desktop 的 MCP 配置文件位置通常在~/Library/Application Support/Claude/claude_desktop_config.json或%APPDATA%\Claude\claude_desktop_config.json。在mcpServers部分添加你的服务器配置{ mcpServers: { my-nestjs-server: { command: node, args: [/ABSOLUTE/PATH/TO/YOUR/PROJECT/dist/main.js] } } }重启 Claude Desktop。在聊天框中Claude 现在应该能“知道”它有一个名为get_greeting的工具可以使用了。你可以尝试对它说“请用我的名字 ‘Alex’ 打个招呼。” Claude 应该会调用这个工具并返回结果。实操心得在开发初期强烈建议使用stdio模式配合 CLI 测试因为日志输出和错误信息更直接。等核心逻辑稳定后再切换到 HTTP 模式进行集成测试。另外注意args中的路径必须是绝对路径否则 Claude Desktop 可能找不到你的可执行文件。4. 进阶实战构建一个带数据库的待办事项 MCP 服务现在我们来点更实际的一个具有增删改查功能的待办事项TodoMCP 服务。这将涉及更复杂的工具交互、依赖注入和错误处理。4.1 定义数据模型与服务层首先我们创建一个简单的 Todo 实体和对应的 TypeORM 服务假设项目已配置 TypeORM。// src/todos/todo.entity.ts import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from typeorm; Entity() export class Todo { PrimaryGeneratedColumn() id: number; Column() title: string; Column({ default: false }) completed: boolean; CreateDateColumn() createdAt: Date; UpdateDateColumn() updatedAt: Date; }// src/todos/todos.service.ts import { Injectable, NotFoundException } from nestjs/common; import { InjectRepository } from nestjs/typeorm; import { Repository } from typeorm; import { Todo } from ./todo.entity; Injectable() export class TodosService { constructor( InjectRepository(Todo) private todosRepository: RepositoryTodo, ) {} async findAll(): PromiseTodo[] { return this.todosRepository.find(); } async findOne(id: number): PromiseTodo { const todo await this.todosRepository.findOneBy({ id }); if (!todo) { throw new NotFoundException(Todo with ID ${id} not found); } return todo; } async create(title: string): PromiseTodo { const todo this.todosRepository.create({ title }); return this.todosRepository.save(todo); } async update(id: number, updates: PartialTodo): PromiseTodo { const todo await this.findOne(id); // 复用 findOne会触发 NotFoundException Object.assign(todo, updates); return this.todosRepository.save(todo); } async remove(id: number): Promisevoid { const result await this.todosRepository.delete(id); if (result.affected 0) { throw new NotFoundException(Todo with ID ${id} not found); } } }4.2 将服务方法暴露为 MCP 工具接下来我们创建一个TodosTool类它不直接操作数据库而是注入TodosService并将业务方法包装成 MCP 工具。这样做的好处是保持了业务逻辑的纯净工具层只负责协议适配。// src/tools/todos.tool.ts import { Injectable } from nestjs/common; import { Tool, Context } from rekog/mcp-nest; import { z } from zod; import { TodosService } from ../todos/todos.service; Injectable() export class TodosTool { constructor(private readonly todosService: TodosService) {} // 依赖注入 Tool({ name: list_todos, description: 获取所有的待办事项列表。, parameters: z.object({}), // 这个工具不需要参数 }) async listTodos(): Promise{ todos: any[] } { // 返回一个对象结构更清晰 const todos await this.todosService.findAll(); // 注意直接返回 Entity 实例可能包含循环引用或敏感字段。 // 最佳实践是映射到一个纯数据对象或 DTO。 return { todos: todos.map(t ({ id: t.id, title: t.title, completed: t.completed, createdAt: t.createdAt.toISOString(), })) }; } Tool({ name: get_todo, description: 根据 ID 获取单个待办事项的详细信息。, parameters: z.object({ id: z.number().int().positive(ID 必须是正整数), }), }) async getTodo(params: { id: number }) { const todo await this.todosService.findOne(params.id); return { id: todo.id, title: todo.title, completed: todo.completed, createdAt: todo.createdAt.toISOString(), updatedAt: todo.updatedAt.toISOString(), }; } Tool({ name: create_todo, description: 创建一个新的待办事项。, parameters: z.object({ title: z.string().min(1, 标题不能为空).max(200, 标题过长), }), }) async createTodo(params: { title: string }, context: Context) { await context.reportProgress({ progress: 0, total: 100, message: 开始创建任务... }); const newTodo await this.todosService.create(params.title); await context.reportProgress({ progress: 100, total: 100, message: 任务创建成功 }); return { message: 待办事项创建成功ID: ${newTodo.id}, todo: { id: newTodo.id, title: newTodo.title, completed: newTodo.completed, } }; } Tool({ name: update_todo, description: 更新一个待办事项例如标记为完成或修改标题。, parameters: z.object({ id: z.number().int().positive(ID 必须是正整数), title: z.string().max(200, 标题过长).optional(), completed: z.boolean().optional(), }).refine(data data.title ! undefined || data.completed ! undefined, { message: 必须提供 title 或 completed 至少一个字段进行更新, }), }) async updateTodo(params: { id: number; title?: string; completed?: boolean }) { const updates: any {}; if (params.title ! undefined) updates.title params.title; if (params.completed ! undefined) updates.completed params.completed; const updatedTodo await this.todosService.update(params.id, updates); return { message: 待办事项 ${params.id} 更新成功, todo: { id: updatedTodo.id, title: updatedTodo.title, completed: updatedTodo.completed, } }; } Tool({ name: delete_todo, description: 删除一个待办事项。, parameters: z.object({ id: z.number().int().positive(ID 必须是正整数), }), }) async deleteTodo(params: { id: number }) { await this.todosService.remove(params.id); return { message: 待办事项 ${params.id} 已删除 }; } }4.3 配置模块与依赖现在我们需要更新模块引入 TypeORM、TodosService和TodosTool。// src/app.module.ts import { Module } from nestjs/common; import { TypeOrmModule } from nestjs/typeorm; import { McpModule } from rekog/mcp-nest; import { Todo } from ./todos/todo.entity; import { TodosService } from ./todos/todos.service; import { GreetingTool } from ./tools/greeting.tool; import { TodosTool } from ./tools/todos.tool; Module({ imports: [ TypeOrmModule.forRoot({ type: sqlite, // 示例使用 SQLite生产环境请换成 PostgreSQL/MySQL database: database.sqlite, entities: [Todo], synchronize: true, // 开发环境用生产环境请使用迁移 }), TypeOrmModule.forFeature([Todo]), // 为 Todo 实体注册 Repository McpModule.forRoot({ name: todo-mcp-server, version: 1.0.0, }), ], providers: [GreetingTool, TodosService, TodosTool], // 注册所有服务 }) export class AppModule {}4.4 测试复杂的工具交互重新编译并运行服务器后AI 客户端现在可以看到list_todos,get_todo,create_todo,update_todo,delete_todo这一系列工具。你可以尝试让 Claude 执行一个复杂的工作流例如“请先帮我创建一个标题为‘学习 MCP 协议’的待办事项然后列出所有待办事项最后把刚才创建的那个标记为完成。”Claude 应该能理解并依次调用create_todo-list_todos-update_todo这三个工具并返回最终的结果。这展示了 MCP 如何让 AI 协调多个工具来完成一个复杂任务。注意事项错误处理我们的TodosService抛出了 NestJS 的NotFoundException。rekog/mcp-nest会自动捕获这些异常并将其转换为 MCP 协议规定的错误响应格式AI 客户端能接收到清晰的错误信息。你也可以在工具方法内部用try...catch进行更精细的错误处理和用户友好的提示。参数验证Zod Schema 不仅定义了类型还定义了验证规则如.min(1)。如果 AI 传入无效参数如空字符串或负数 ID库会在调用你的工具方法之前就返回验证错误保证了工具内部逻辑的纯净。返回格式尽量返回结构化的对象而不是简单的字符串。这有助于 AI 更好地解析和利用返回的数据。例如list_todos返回{ todos: [...] }比直接返回数组更清晰。5. 部署与生产配置HTTP 传输与安全认证在本地开发使用stdio很方便但生产环境通常需要以 HTTP 服务的形式部署。同时安全是重中之重我们不能让任何人都能调用我们的工具。5.1 配置 HTTP SSE 传输rekog/mcp-nest支持多种传输方式。我们将配置一个使用 HTTP 和 Server-Sent Events (SSE) 的服务器。首先安装必要的包如果你使用 Fastify 作为底层 HTTP 引擎需要额外安装npm install nestjs/platform-express # 如果使用 Express (默认) # 或 npm install nestjs/platform-fastify然后修改main.ts来创建并配置一个专门的 MCP HTTP 服务器与你的主 NestJS 应用分离这是推荐做法因为 MCP 协议与普通的 REST API 不同。// src/main.mcp.ts - 这是一个独立的入口文件 import { NestFactory } from nestjs/core; import { AppModule } from ./app.module; import { createMcpServer } from rekog/mcp-nest; async function bootstrap() { // 创建 NestJS 应用实例但不监听端口 const app await NestFactory.createApplicationContext(AppModule); // 从应用中获取 McpService const mcpService app.get(McpService); // 使用字符串 token 获取 // 创建 MCP HTTP 服务器 const mcpServer await createMcpServer(mcpService, { transport: http, // 指定使用 HTTP 传输 http: { port: 3001, // MCP 服务监听的端口与主应用端口区分开 endpoint: /mcp, // SSE 连接端点 // 可选配置 CORS 等 // cors: { origin: https://your-client.com } }, }); console.log(MCP HTTP server is running on http://localhost:3001); // 注意这里没有调用 app.listen()因为我们只运行 MCP 服务器 } bootstrap();同时我们需要修改app.module.ts中的McpModule配置移除全局的transport设置因为现在我们在入口文件里指定。// src/app.module.ts (部分) imports: [ McpModule.forRoot({ name: todo-mcp-server, version: 1.0.0, // 不再在这里设置 transport由入口文件决定 }), ],最后在package.json中添加一个新的脚本命令{ scripts: { start:mcp: nest build node dist/main.mcp } }运行npm run start:mcp你的 MCP 服务器就会在http://localhost:3001/mcp上提供 SSE 连接。5.2 集成认证与授权使用 Guards未经认证的 MCP 服务器是危险的。rekog/mcp-nest完美支持 NestJS 的 Guard 机制。我们可以创建一个简单的 API Key Guard。首先创建一个 Guard// src/auth/api-key.guard.ts import { Injectable, CanActivate, ExecutionContext, UnauthorizedException } from nestjs/common; import { Observable } from rxjs; import { Request } from express; Injectable() export class ApiKeyGuard implements CanActivate { private readonly validApiKey process.env.MCP_API_KEY || your-secret-key-here; canActivate(context: ExecutionContext): boolean | Promiseboolean | Observableboolean { // 对于 MCP over HTTP上下文是 HTTP 请求 const request context.switchToHttp().getRequestRequest(); const authHeader request.headers[authorization]; if (!authHeader || !authHeader.startsWith(Bearer )) { throw new UnauthorizedException(Missing or invalid Authorization header); } const apiKey authHeader.substring(7); // 去掉 Bearer 前缀 if (apiKey ! this.validApiKey) { throw new UnauthorizedException(Invalid API key); } return true; } }然后我们需要告诉McpModule在初始化 MCP 服务器时使用这个 Guard。这需要在forRoot配置中完成。// src/app.module.ts import { Module } from nestjs/common; import { McpModule } from rekog/mcp-nest; import { ApiKeyGuard } from ./auth/api-key.guard; Module({ imports: [ McpModule.forRoot({ name: todo-mcp-server, version: 1.0.0, // 配置全局 Guard useGuards: [ApiKeyGuard], // 所有 MCP 请求都必须通过此 Guard }), // ... 其他导入 ], providers: [ApiKeyGuard], }) export class AppModule {}现在任何连接到http://localhost:3001/mcp的客户端都必须在请求头中携带Authorization: Bearer your-secret-key-here。Claude Desktop 等客户端通常支持在服务器配置中设置自定义请求头。更细粒度的授权Per-Tool Authorization有时你可能希望不同的 API Key 拥有不同的工具调用权限。这可以通过工具级别的自定义装饰器或元数据来实现。rekog/mcp-nest的Tool装饰器支持传入一个guard选项。// src/tools/admin.tool.ts import { UseGuards } from nestjs/common; import { Tool } from rekog/mcp-nest; import { AdminGuard } from ../auth/admin.guard; export class AdminTool { Tool({ name: delete_all_todos, description: 【管理员专用】删除所有待办事项。, // 为这个特定的工具指定一个更严格的 Guard guard: AdminGuard, // AdminGuard 会检查更高级别的权限 }) async deleteAllTodos() { // ... 实现 } }5.3 使用内置授权服务器Beta对于需要完整 OAuth2 流程的复杂场景rekog/mcp-nest还提供了一个内置授权服务器Beta。这特别适合当你希望用户通过标准的 OAuth2 流程如授权码模式来授权 AI 客户端访问你的 MCP 服务器时使用。配置相对复杂涉及客户端注册、重定向 URI 等但为构建面向多用户的 MCP 服务提供了强大支持。具体配置请参考其官方文档的Built-in Authorization Server部分。部署心得端口与路径确保生产环境的防火墙和安全组规则允许访问你配置的 MCP 端口如 3001。同时考虑使用 Nginx 等反向代理为/mcp路径配置 SSL/TLS。API Key 管理永远不要将 API Key 硬编码在代码中。使用环境变量或秘密管理服务如 AWS Secrets Manager, HashiCorp Vault。为不同的客户端如开发、测试、生产环境下的 Claude Desktop颁发不同的 Key并做好轮换计划。监控与日志MCP 服务器的调用日志非常重要。你可以在全局 Guard 或自定义的拦截器Interceptor中记录所有的工具调用请求包括调用者通过 API Key 标识、工具名、参数和时间用于审计和故障排查。性能考虑AI 可能会频繁调用工具。确保你的工具方法是高效的并考虑对数据库查询或外部 API 调用进行适当的缓存。6. 常见问题与排查技巧实录在实际开发和集成过程中我踩过不少坑。这里总结一些最常见的问题和解决方法。6.1 工具未出现在客户端列表中症状运行了服务器但 AI 客户端如 Claude Desktop连接后看不到任何工具或者只看到部分工具。排查步骤检查工具类是否被注册为 Provider确保你的工具类如GreetingTool被添加到了所属模块的providers数组中。如果工具在特性模块中确保该模块被根模块或一个已导入的模块所导入。检查Tool()装饰器确认装饰器被正确地应用在类的方法上而不是类的属性上。方法必须是async或返回Promise。检查传输模式如果你配置的是 HTTP 传输确保客户端连接的是正确的 URL例如http://localhost:3001/mcp。对于stdio模式确保客户端启动命令的路径正确。查看服务器日志在McpModule.forRoot配置中启用调试日志{ debug: true }。启动服务器时你应该能看到类似[MCP] Registered tool: get_greeting的日志输出。如果没有说明工具注册环节有问题。依赖注入问题如果工具类依赖于其他服务如TodosService确保这些依赖服务也被正确注册并且没有循环依赖。NestJS 启动时的错误日志通常会给出提示。6.2 工具调用失败返回验证错误症状AI 尝试调用工具但返回错误提示参数无效。排查步骤仔细阅读错误信息MCP 协议会返回详细的错误信息其中会包含是哪个字段违反了哪条 Zod 规则。例如“name”: “必须为字符串类型”。检查 Zod Schema确认你为工具定义的 Zod Schema 与 AI 实际可能传入的数据类型匹配。AI 有时可能会推断出数字类型但你的 Schema 定义的是字符串。使用.coerce方法如z.coerce.number()可以尝试进行类型转换。检查默认值如果参数是可选的且有默认值确保 AI 在调用时可以不传该参数。有时 AI 会显式传递null而你的 Schema 可能不接受null需要使用.nullable()或.optional()。6.3 进度报告context.reportProgress不工作症状在工具方法中调用了context.reportProgress但客户端如 Claude Desktop没有显示进度条。排查步骤确认客户端支持并非所有 MCP 客户端都支持进度报告。Claude Desktop 目前支持。请查阅你所用客户端的文档。确认传输协议进度报告功能可能在某些传输模式下工作得更好。确保你使用的传输协议如 HTTPSSE支持服务器向客户端推送消息。检查调用时机context.reportProgress返回一个 Promise。确保你await它或者在它后面进行await操作以保证消息在函数返回前被发送出去。6.4 在 Docker 或 Serverless 环境中部署问题症状在本地运行正常但在容器化或无服务器环境中失败。排查步骤stdio模式不适用在 Docker 或 Serverless如 AWS Lambda中stdio传输模式通常无法工作因为没有一个长期运行的父进程来管理标准流。必须切换到 HTTP 传输模式。HTTP 端口绑定在 Docker 中确保容器内的应用监听的是0.0.0.0而不是127.0.0.1localhost。在createMcpServer的 HTTP 配置中可以设置host: 0.0.0.0。冷启动延迟在 Serverless 环境中冷启动可能导致连接超时。你需要调整客户端的连接超时设置或者使用提供性环境来保持实例温暖。环境变量确保生产环境所需的环境变量如数据库连接字符串、API Key已正确注入到容器或函数环境中。6.5 与特定 AI 客户端集成问题症状与测试 CLI 工作正常但与 Claude Desktop、Cursor 等集成时出现问题。排查步骤检查客户端配置格式每个客户端对claude_desktop_config.json的格式要求可能略有不同。确保 JSON 格式正确路径是绝对路径并且命令和参数无误。查看客户端日志Claude Desktop 通常有应用日志文件。在 macOS 上可以在~/Library/Logs/Claude/找到在 Windows 上可以在%APPDATA%\Claude\logs找到。查看日志中是否有连接错误或进程启动失败的信息。权限问题确保 Claude Desktop 有权限执行你指定的node命令和访问你的项目目录。版本兼容性确保你使用的rekog/mcp-nest和modelcontextprotocol/sdk版本与客户端兼容。有时需要尝试更新到最新版本。终极调试技巧当你遇到难以定位的问题时一个非常有效的方法是暂时绕开复杂的客户端直接用最原始的curl命令或 Postman 来测试你的 HTTP MCP 服务器。你可以手动构造一个 SSE 连接请求并发送 JSON-RPC 格式的工具列表请求和工具调用请求。这能帮你快速判断问题是出在服务器端还是客户端集成上。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价