Community编程与开发github.com

ruwanb/nestjs-mcp-server

将 NestJS 12 应用程序转换为模型上下文协议(MCP)服务器,使用 @nestjs/mcp 模块,支持 @Tool、@Resource、@Prompt 等装饰器,并集成了 OAuth 2.1 认证和 NestJS 管道拦截器体系。

nestjs-mcp-server 是什么?

该技能描述了如何将 NestJS 12 应用程序配置为符合 MCP(Model Context Protocol)标准的服务器。它利用 `@nestjs/mcp` 模块,通过 `McpModule.forRoot` 或 `McpModule.forRootAsync` 快速启用 MCP 端点。开发者可以在普通的 NestJS 提供者(Provider)上使用 `@Tool`、`@Resource`、`@ResourceTemplate` 和 `@Prompt` 装饰器来定义 AI 模型可调用的原语,输入输出模式支持 Zod 4.2+、ArkType、Valibot(通过标准 JSON Schema 桥接)以及直接使用 JSON Schema。该服务器同时支持 HTTP 流式传输和 stdio 传输,适用于云服务和本地进程两种场景。在授权方面,它作为 OAuth 2.1 资源服务器运行,通过 `McpTokenVerifier` 验证 JWT 或 opaque token,并支持与 `@nestjs/authentication` 和 `@nestjs/authorization` 的深度集成。此外,常规的 NestJS 增强器(Guard、Interceptor、Pipe、Filter)在 MCP 上下文中同样有效,`McpContext` 提供了信号(signal)、进度报告(reportProgress)、用户输入索取(inputRequired)以及认证信息(authInfo)等丰富的调用上下文。

兼容平台✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/ruwanb/nestjs-boost/tree/HEAD/skills/nestjs-mcp-server

Installed? Explore more 编程与开发 skills: steipete/bluebubbles, steipete/eightctl, steipete/blucli · View all 6 →

在你喜欢的 AI 中提问

打开一个已预加载此 Agent Skill 的新对话。

文档

NestJS MCP server

npm i @nestjs/mcp @modelcontextprotocol/server zod
npm i -D @modelcontextprotocol/client          # 内存测试客户端

Zod 4.2+ 和 ArkType 可直接使用;Valibot 通过 toStandardJsonSchema() 桥接;已有 JSON Schema 通过 fromJsonSchema() 导入。Zod 3 模式会在启动时拒绝。

McpModule.forRoot({
  name: 'cat-store', version: '1.0.0',
  instructions: '在更改订单前,先使用 find-order 查找订单。',   // 告诉模型如何使用该服务器
}),

app.listen(3000) 后在 /mcp 路径提供服务。使用 npx @modelcontextprotocol/inspector 检查。启动时模块会扫描所有提供者(所有模块,或仅 include 中指定的模块)中带有装饰器的方法。遇到重复名称、无效模式或声明了 scopes 但没有 auth 的情况时,会快速失败。

原语:提供者上的方法

原语装饰器使用者调用参数
工具(一个动作)@Tool({ name, description, inputSchema, outputSchema?, annotations?, scopes? })模型验证后的输入、ctx
资源(数据)@Resource({ uri, mimeType }) / @ResourceTemplate({ uriTemplate: 'orders://{orderId}' })客户端(通常由用户附加){} 或模板变量、ctx(ctx.uri)
提示(一个模板)@Prompt({ name, description, argsSchema })用户选择一个验证后的参数、ctx
@Injectable()                                          // 一个普通的提供者,列在模块的 providers 中
export class OrdersTools {
  constructor(private readonly orders: OrdersService) {}

  @Tool({
    name: 'find-order',
    description: '通过订单ID查找订单,或通过客户邮箱查找最新订单',
    inputSchema: z.object({
      orderId: z.string().optional().describe('订单ID,例如 ord_123'),
      email: z.email().optional().describe("客户的邮箱地址"),
    }),
    annotations: { readOnlyHint: true },               // 客户端可以在不询问用户的情况下运行它
  })
  async find({ orderId, email }: { orderId?: string; email?: string }) {
    const found = orderId ? [await this.orders.findById(orderId)] : await this.orders.latestFor(email!);
    if (!found[0]) throw new NotFoundException('没有该订单');   // → 返回 isError: true 的工具结果
    return found;
  }

  @Tool({
    name: 'change-address',
    description: '更改尚未发货的订单的配送地址',
    inputSchema: z.object({ orderId: z.string(), address: z.string().min(10) }),
    outputSchema: z.object({ orderId: z.string(), address: z.string(), status: z.string() }),
    scopes: ['orders:write'],
  })
  async changeAddress({ orderId, address }: { orderId: string; address: string }, ctx: McpContext) {
    const o = await this.orders.changeAddress(orderId, address, ctx.authInfo?.clientId);
    return { orderId: o.id, address: o.address, status: o.status };   // → structuredContent + JSON 文本
  }
}

返回值:工具的字符串成为文本块。如果存在 outputSchema,对象成为 structuredContent;否则成为 JSON 文本。自行返回 CallToolResult 以支持图片、音频或嵌入资源;undefined 返回空结果。资源的字符串作为文本发送,Buffer/Uint8Array 作为 blob 发送,对象作为 JSON 发送。提示的字符串成为一条用户消息;数组返回 PromptMessage[]。工具名称长度为 1–128 个字符,允许 A-Z a-z 0-9 _ - .,默认使用方法名。

请为模型编写名称、描述和 .describe() 文本。 这是模型了解的全部信息:说明工具做什么、何时使用、是否更改了某些内容,并为每个参数提供示例值。使用资源加载原样内容;使用工具进行带参数的查询。

调用上下文

ctx: McpContext 具有以下属性:

  • kind、name、uri
  • authInfo(clientId、scopes、expiresAt、extra)
  • request(平台请求;在 stdio 上为 undefined)
  • requestInfo
  • transport('http' | 'stdio')
  • era('modern' 2026-07-28 / 'legacy' 2025)
  • signal:传递给 fetch 和数据库调用;在循环中调用 throwIfAborted()
  • reportProgress(progress, total?, message?)
  • server(SDK 上下文)

要询问用户(确认、缺失值),返回 inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message, requestedSchema }) } })。在重试时,使用 acceptedContent(ctx.server.mcpReq.inputResponses, 'confirm', Schema) 读取答案。在 HTTP 上,这只适用于 2026-07-28 客户端,因为端点是无状态的。在 stdio 上,两种 era 都适用。

提供者保持单例。从 ctx 读取每次调用的数据,而不是从请求作用域的提供者。

增强器和错误

  • Guard、Interceptor、Pipe 和 Filter 与路由上的运行方式相同,context.getType() === 'mcp'。McpExecutionContext.create(context) 提供 getInput()、getContext()、getKind()、getName()、getRequest() 和 getAuthInfo()。
  • 全局增强器在 MCP 调用和承载的 HTTP 请求上都运行。 仅限 HTTP 的增强器必须在调用 switchToHttp() 之前检查 context.getType() === 'http'。
  • 全局 ValidationPipe({ transform: true }) 验证 DTO 类型的输入。自定义参数装饰器放在 (input, ctx, @CurrentUser() user) 之后。
  • 工具抛出的错误成为 isError: true 的结果,模型可以从中恢复。HttpException 保留其消息;任何其他错误变为 Internal server error 并记录日志。
  • 资源和提示以 JSON-RPC 错误响应:NotFoundException → 资源未找到,BadRequestException → 无效参数。
  • Filter:检查 host.getType<McpContextType>() === 'mcp'。Filter 返回的任何内容都成为结果;要发送 JSON-RPC 错误,抛出 ProtocolError。

授权(可网络访问的服务器)

McpModule.forRootAsync({
  imports: [ConfigModule, AuthModule],
  inject: [ConfigService, IdentityProviderTokenVerifier],
  useFactory: (config: ConfigService, verifier: IdentityProviderTokenVerifier) => ({
    name: 'cat-store', version: config.get('VERSION'),
    auth: {
      verifier,                                               // 实现 McpTokenVerifier.verifyAccessToken(token) → AuthInfo | null
      resource: config.get('MCP_RESOURCE'),                   // 规范的公共 URL = 令牌受众
      authorizationServers: [config.get('IDENTITY_PROVIDER_URL')],
      scopesSupported: ['orders:read', 'orders:write'],
    },
  }),
}),
  • 服务器是 OAuth 2.1 资源服务器:它验证令牌,从不颁发令牌。
  • 它在 /.well-known/oauth-protected-resource/mcp 提供 RFC 9728 元数据。没有令牌的请求会收到 401 状态码和 WWW-Authenticate: Bearer 挑战,从而启动客户端登录。
  • 验证器:
    • JWT:使用 @nestjs/authentication 中的 JwtVerifier(检查签名、签发者、受众和过期时间)。
    • 不透明令牌:调用 RFC 7662 内省端点。
    • 返回的 AuthInfo 必须携带 resource === auth.resource 和未来的 expiresAt,否则令牌将被拒绝并返回 401。
    • 抛出 401 错误以拒绝令牌;任何其他错误变为 500。
  • 原语上缺少 scopes → 返回 403 并带有升级挑战。auth.requiredScopes 适用于每个请求。
  • 使用 @nestjs/authentication ≥ 0.1.3 和 @nestjs/authorization ≥ 0.1.1:
    • 它们的全局 Guard 验证每个调用,@CurrentUser() 和 @Can(Policy, 'action') 在工具中工作。
    • 在端点上设置 decorators: [Public()],以便 auth 处理挑战。
    • 没有 @Can() 的工具会被拒绝。
  • 仅限内部的服务器可以跳过 OAuth,并在端点上使用 decorators: [UseGuards(ApiKeyGuard)]。

传输

  • 流式 HTTP 默认提供;path: false 禁用它。端点是一个普通的控制器,因此全局前缀、版本化(endpointVersion)、中间件和 CORS 都适用。它是无状态的:不需要粘性会话。两种协议 era 都提供服务(http.legacy: 'reject' 仅服务于 2026-07-28)。http.responseMode 可以是 'auto'、'json' 或 'sse'。
  • stdio,适用于启动本地进程的助手:创建 stdio.ts,使用 NestFactory.createApplicationContext(AppModule, { logger: new McpStdioLogger() }),然后 await app.get(McpStdioServer).listen(); await app.close();。任何其他写入 stdout 的内容都会破坏协议。凭证来自 process.env;不检查作用域,但 Guard 会运行。

有关完整的选项表、生产设置、stdio 客户端配置、测试模式和迁移映射,请参见 references/mcp-reference.md。

工作流程

  • 1. 安装;检查 Zod 4.2+;使用 name、version、instructions 配置 McpModule.forRoot。
  • 2. 在提供者上添加工具、资源和提示,使用面向模型的描述、为每个参数添加 .describe()、在只读工具上添加 readOnlyHint,并在结果结构化的地方添加 outputSchema。
  • 3. 确保仅限 HTTP 的全局增强器检查 getType();根据需要添加 MCP Guard 和 Filter。
  • 4. 可网络访问的服务器:添加 auth(验证器、资源、授权服务器、作用域)、allowedHosts/allowedOrigins、CORS 头和主体大小限制。本地服务器:使用 McpStdioLogger 添加 stdio.ts。
  • 5. 测试:直接对方法进行单元测试;使用 connectMcpClient(moduleRef, { token }) 针对测试验证器运行 e2e 测试;通过套接字测试 HTTP 路由。构建、lint、运行测试、启动应用,并使用 Inspector 列出工具。

参考文件

  • references/mcp-reference.md:模块选项、装饰器选项、HTTP 加固(Origin、Host、CORS 头、主体大小)、stdio 客户端配置、NestJS Observe 命名、生产检查清单、测试(令牌和作用域、协议 era、McpRegistry 检查、HTTP 和 stdio 测试),以及从 @rekog/mcp-nest 1.x/2.x 迁移。配置生产环境、编写测试或迁移时阅读。

来源:概述 · 原语 · 传输 · 测试 · 迁移

相关技能