mcpField
M
MCP Server NestJS Module Library

MCP Server NestJS Module Library

本地服务
github.com/adrian-d-hidalgo开发工具404 周前更新

关于 MCP Server NestJS Module Library

NestJS MCP Server 是一个用于基于 NestJS 构建 MCP 服务的模块化库。它通过装饰器和模块提供 resource、tool 与 prompt 的注册方式,并封装官方 @modelcontextprotocol/sdk。适合需要组织大型 MCP 服务的 NestJS 开发者。

主要功能

  1. 01基于装饰器注册 Tool、Resource 与 Prompt
  2. 02Resolver 类分组并支持命名空间
  3. 03forRoot 与 forFeature 模块化注册
  4. 04支持 Guard 与依赖注入
  5. 05提供工具注解(annotations)配置

适用场景

  1. 01在 NestJS 应用中暴露业务接口为 MCP 工具
  2. 02将大型 MCP 服务按功能拆分为多个模块
  3. 03为 MCP 能力添加访问控制与守卫

配置

本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。

mcp.json · 通用配置
{
"mcpServers": {
"nestjs": {
"command": "npx",
"args": [
"-y",
"@modelcontextprotocol/inspector"
]
}
}
}
启动命令npx -y @modelcontextprotocol/inspector

本地服务需要在你的电脑上安装运行环境(如 Node.js 18+)。各客户端的配置文件位置不同,请参考客户端文档。

安装包
npm@modelcontextprotocol/inspector

工具

还没有获取到工具列表

本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。

详细介绍

MCP Server NestJS 模块库

NPM Version Semantic Release Downloads CI Pipeline codecov Known Vulnerabilities MIT License PRs Welcome Contributor Covenant


概述

NestJS MCP Server 是一个用于借助 NestJS 构建 Model Context Protocol (MCP) 服务的模块化库。它提供装饰器、模块和集成模式,以可扩展且易于维护的方式提供 MCP 资源、工具和提示词。本项目是官方 @modelcontextprotocol/sdk 的封装,并始终确保与其类型和规范兼容。


目录


安装

npm install @nestjs-mcp/server @modelcontextprotocol/sdk zod
# or
yarn add @nestjs-mcp/server @modelcontextprotocol/sdk zod
# or
pnpm add @nestjs-mcp/server @modelcontextprotocol/sdk zod

快速入门

在 NestJS 应用中注册 MCP 模块,并公开一个简单的工具:

import { Module } from '@nestjs/common';

import { CallToolResult } from '@modelcontextprotocol/sdk/types';

import { Resolver, Tool, McpModule } from '@nestjs-mcp/server';

@Resolver()
export class HealthResolver {
  /**
   * Simple health check tool
   */
  @Tool({ name: 'server_health_check' })
  healthCheck(): CallToolResult {
    return {
      content: [
        {
          type: 'text',
          text: 'Server is operational. All systems running normally.',
        },
      ],
    };
  }
}

@Module({
  imports: [
    McpModule.forRoot({
      name: 'My MCP Server',
      version: '1.0.0',
    }),
  ],
  providers: [HealthResolver],
})
export class AppModule {}

什么是 MCP?

模型上下文协议(MCP) 是一种开放协议,用于将大模型连接到外部数据、工具和提示词。MCP 服务以标准化方式公开资源(数据)、工具(操作)和提示词(对话流程),从而实现与大模型驱动的客户端无缝集成。


核心概念

服务端

MCP 服务端是向大模型公开功能的主要入口。它负责管理资源、工具和提示词的注册与发现。

资源

资源表示大模型可以查询或检索的结构化数据或文档。资源通常为只读,并通过唯一 URI 标识。

工具

工具是 LLM 可以调用的操作或函数。工具可能产生副作用,也可以接受参数来执行计算或触发操作。

提示词

提示词定义了适用于 LLM 的对话流程、模板或交互模式。提示词有助于引导模型在特定场景中的行为。

实现详情和代码示例,请参阅功能部分。


模块 API

McpModule.forRoot

在 NestJS 应用中全局注册 MCP 服务。

参数:

  • options: McpModuleOptions — 主要的服务配置对象:
    • name: string:MCP 服务的名称。
    • version: string:MCP 服务的版本。
    • instructions?: string:可选的 MCP 服务描述,供客户端使用。
    • capabilities?: Record<string, unknown>:可选的其他功能元数据。
    • providers?: Provider[]:要包含在模块中的可选 NestJS 提供者数组。
    • imports?: any[]:要导入的可选 NestJS 模块数组。
    • logging?: McpLoggingOptions:可选的日志配置:
      • enabled?: boolean(默认值:true):启用或禁用日志记录。
      • level?: 'error' | 'warn' | 'log' | 'debug' | 'verbose'(默认值:'verbose'):设置日志级别。
    • transports?: McpModuleTransportOptions:可选的传输配置(请参阅传输选项)。
    • protocolOptions?: Record<string, unknown>:直接传递给底层 @modelcontextprotocol/sdk 服务实例的可选参数。

返回值:

  • 一个注册了所有 MCP 提供者的动态 NestJS 模块。

示例:

import { Module } from '@nestjs/common';
import { McpModule } from '@nestjs-mcp/server';

@Module({
  imports: [
    McpModule.forRoot({
      name: 'My Server',
      version: '1.0.0',
      instructions: 'A server providing utility tools and data.',
      logging: { level: 'log' },
      transport: { legacy: 'reject' }, // Modern-era clients only
      // ...other MCP options
    }),
  ],
})
export class AppModule {}
McpModule.forRootAsync

使用异步选项全局注册 MCP 服务,适用于与 @nestjs/config 等配置模块集成。

注意:

  • imports 数组应包含为 useFactory 提供所需依赖项的任何模块(例如,注入 ConfigService 时需要包含 ConfigModule)。
  • 仅在根模块(AppModule)中使用一次 forRootAsync。
  • 有关所有可用选项,请参阅 McpModuleAsyncOptions。

参数:

  • options: McpModuleAsyncOptions — 异步配置对象:
    • imports?: any[]:在工厂运行前要导入的可选模块。
    • useFactory: (...args: any[]) => Promise<McpModuleOptions> | McpModuleOptions:返回 McpModuleOptions 的工厂函数。
    • inject?: any[]:要注入 useFactory 的可选提供者。

返回值:

  • 一个动态 NestJS 模块。

示例(与 ConfigModule 配合使用):

import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { McpModule } from '@nestjs-mcp/server';

@Module({
  imports: [
    ConfigModule.forRoot(), // Make sure ConfigModule is imported
    McpModule.forRootAsync({
      imports: [ConfigModule], // Import ConfigModule here too
      useFactory: (configService: ConfigService) => ({
        name: configService.get<string>('MCP_SERVER_NAME', 'Default Server'),
        version: configService.get<string>('MCP_SERVER_VERSION', '1.0.0'),
        instructions: configService.get<string>('MCP_SERVER_DESC'),
        logging: {
          level: configService.get('MCP_LOG_LEVEL', 'verbose'),
        },
        // ... other options from configService
      }),
      inject: [ConfigService], // Inject ConfigService into the factory
    }),
  ],
})
export class AppModule {}
McpModule.forFeature

在功能模块中注册其他 MCP 资源、工具或提示词。使用此方法可将大型服务组织到多个模块中。包含 MCP 功能的解析器必须加入功能模块的 providers 数组。

参数:

  • options?: McpFeatureOptions(目前未使用,为未来增强功能保留)。

返回值:

  • 一个动态模块。

示例:

// src/status/status.resolver.ts
import { Resolver, Tool } from '@nestjs-mcp/server';
import { CallToolResult } from '@modelcontextprotocol/sdk/types';

@Resolver('status')
export class StatusResolver {
  @Tool({ name: 'health_check' })
  healthCheck(): CallToolResult {
    return { content: [{ type: 'text', text: 'OK' }] };
  }
}

// src/status/status.module.ts
import { Module } from '@nestjs/common';
import { McpModule } from '@nestjs-mcp/server';
import { StatusResolver } from './status.resolver';

@Module({
  imports: [McpModule.forFeature()], // Import forFeature here
  providers: [StatusResolver], // Register your resolver
})
export class StatusModule {}

模块用法

此库提供两种在 NestJS 应用中注册 MCP 功能的主要方式:

1. 使用 McpModule.forRoot 进行全局注册

在根应用模块中使用 McpModule.forRoot,以全局配置并注册 MCP 服务。每个 MCP 服务应用都必须进行此操作。

import { Module } from '@nestjs/common';
import { McpModule } from '@nestjs-mcp/server';
import { PromptsResolver } from './prompts.resolver';

@Module({
  imports: [
    McpModule.forRoot({
      name: 'My MCP Server',
      version: '1.0.0',
      // ...other MCP options
    }),
  ],
  providers: [PromptsResolver],
})
export class AppModule {}
2. 使用 McpModule.forFeature 注册功能模块

在功能模块中使用 McpModule.forFeature 注册额外的解析器、工具或资源。这有助于将大型服务拆分为多个模块。

import { Module } from '@nestjs/common';
import { McpModule } from '@nestjs-mcp/server';

import { ToolsResolver } from './tools.resolver';

@Module({
  imports: [McpModule.forFeature()],
  providers: [ToolsResolver],
})
export class ToolsModule {}
  • 在根模块(AppModule)中,仅使用一次 forRoot 或 forRootAsync。
  • 在定义 MCP 功能(@Resolver 类)的任何功能模块中使用 forFeature。
  • 确保在各自模块的 providers 数组中列出所有解析器。

功能

此库提供了一组装饰器,用于定义 MCP 功能并应用守卫等横切关注点。装饰器既可用于解析器(类)级别,也可用于方法级别。

解析器装饰器

解析器是一个用于归类相关 MCP 功能的类。所有 MCP 功能方法(@Prompt、@Resource、@Tool)都必须属于使用 @Resolver 装饰的类。

  • **无需使用 @Injectable():**MCP 模块会自动将解析器类视为提供者,无需使用 @Injectable() 装饰器。
  • **依赖注入:**标准的 NestJS 依赖注入可在解析器构造函数中使用。
  • **命名空间:**可以选择为 @Resolver('my_namespace') 提供字符串参数,以设置该解析器中各项功能的命名空间。
  • 访问默认值:@Resolver({ name, scopes, public, hideOutOfScope }) 会为该类的每项功能设置默认值——请参阅解析器默认值。
  • **守卫:**可使用 @UseGuards() 在类级别应用守卫。

示例:

import { Resolver, Prompt, Resource, Tool } from '@nestjs-mcp/server';
// Import any services you need to inject
import { SomeService } from '../some.service';

@Resolver('workspace') // No @Injectable()
export class MyResolver {
  // Inject dependencies as usual
  constructor(private readonly someService: SomeService) {}

  @Prompt({ name: 'greet_user' }) // Capabilities must be inside a Resolver
  greetPrompt(/*...args...*/) {
    const greeting = this.someService.getGreeting();
    /* ... */
  }

  @Resource({ name: 'user_profile', uri: 'user://{id}' })
  getUserResource(/*...args...*/) {
    /* ... */
  }

  @Tool({ name: 'calculate_sum' })
  sumTool(/*...args...*/) {
    /* ... */
  }
}

也可以在解析器级别应用守卫:

import { UseGuards, Resolver } from '@nestjs-mcp/server';
import { MyGuard } from './guards/my.guard';

@UseGuards(MyGuard) // Applied to all capabilities in this Resolver
@Resolver('secure') // No @Injectable()
export class SecureResolver {
  // All capabilities in this resolver will use MyGuard
}
提示词装饰器

在解析器类中装饰方法,将其公开为 MCP 提示词。接受与 @modelcontextprotocol/sdk 中的 server.prompt() 兼容的选项。name 应使用 snake_case。

import { Prompt, Resolver } from '@nestjs-mcp/server';
import { McpContext } from '@nestjs-mcp/server'; // Handler context type
import { z } from 'zod'; // Example if using Zod schema

// Optional: Define schema if needed
// const SummaryArgs = z.object({ topic: z.string() });

@Resolver('prompts') // Must be in a Resolver class
export class MyPrompts {
  @Prompt({
    name: 'generate_summary',
    description: 'Generates a summary for the given text.',
    // argsSchema: SummaryArgs
  })
  generateSummaryPrompt(
    // params: z.infer<typeof SummaryArgs>, // Arguments based on argsSchema (if defined)
    ctx: McpContext, // The request this call arrived on
  ) {
    console.log(`Generating summary for ${ctx.mcpReq.method}`);
    /* ... return CallPromptResult ... */
    return { content: [{ type: 'text', text: 'Summary generated.' }] };
  }
}
资源装饰器

在解析器类中装饰方法,将其公开为 MCP 资源。接受与 @modelcontextprotocol/sdk 中的 server.resource() 兼容的选项。name 应使用 snake_case。

import { Resource, Resolver } from '@nestjs-mcp/server';
import { McpContext } from '@nestjs-mcp/server'; // Handler context type
import { URL } from 'url'; // Type for URI resource
import { z } from 'zod'; // Example if using Zod template

// Optional: Define template schema if needed
// const DocQueryTemplate = z.object({ query: z.string() });

@Resolver('data') // Must be in a Resolver class
export class MyResources {
  @Resource({
    name: 'user_profile',
    uri: 'user://profiles/{userId}',
    // metadata: { description: '...' } // Optional
  })
  getUserProfile(
    uri: URL, // First argument is the parsed URI
    // metadata: Record<string, any> // Second argument if is defined
    ctx: McpContext, // The request this call arrived on
  ) {
    const userId = uri.pathname.split('/').pop(); // Example: Extract ID from URI
    console.log(`Fetching profile for ${userId}`);
    /* ... return CallResourceResult ... */
    return { content: [{ type: 'text', text: `Profile data for ${userId}` }] };
  }

  @Resource({
    name: 'document_list',
    template: { type: 'string', description: 'Document content query' }, // Simple template example
    // metadata: { list: true } // Optional
  })
  findDocuments(
    uri: URL, // First arg based on simple template type
    variables: Record<string, string>, // Second arg is path params (if any)
    ctx: McpContext, // The request this call arrived on
  ) {
    console.log(`Finding documents matching '${query}'`);
    /* ... return CallResourceResult ... */
    return { content: [{ type: 'text', text: 'List of documents.' }] };
  }
}
工具装饰器

在解析器类中装饰方法,将其公开为 MCP 工具。接受与 @modelcontextprotocol/sdk 中的 server.tool() 兼容的选项。name 应使用 snake_case。

import { Tool, Resolver } from '@nestjs-mcp/server';
import { McpContext } from '@nestjs-mcp/server';
import { z } from 'zod';
import { CallToolResult } from '@modelcontextprotocol/sdk/types';

@Resolver('user_tools')
export class UserToolsResolver {
  @Tool({
    name: 'delete_user',
    description: 'Deletes a user by ID',
    paramsSchema: z.object({ userId: z.string() }),
    annotations: { destructiveHint: true, readOnlyHint: false },
  })
  deleteUser({ userId }: { userId: string }, ctx: McpContext): CallToolResult {
    // ...logic...
    return { content: [{ type: 'text', text: `User ${userId} deleted.` }] };
  }
}
工具注释

annotations 字段允许提供有关工具行为的协议级提示,例如工具是否具有破坏性、是否为只读、是否幂等,或是否具有其他特殊属性。客户端、用户界面或协议本身可使用这些提示来显示警告、优化调用或强制执行策略。

常见注释键:

  • destructiveHint(布尔值):表示该工具会执行破坏性操作(例如删除数据)。
  • readOnlyHint(布尔值):表示该工具不会修改任何数据。
  • idempotentHint(布尔值):表示多次调用该工具都能安全地产生相同效果。
  • openWorldHint(布尔值):表示该工具可能会在当前系统之外产生副作用。

示例:

@Tool({
  name: 'reset_password',
  paramsSchema: z.object({ userId: z.string() }),
  annotations: { destructiveHint: true, idempotentHint: false }
})
resetPassword({ userId }: { userId: string }): CallToolResult {
  // ...
}
ToolOptions 变体
变体必填字段
ToolBaseOptionsname
ToolWithDescriptionOptionsname, description
ToolWithParamOrAnnotationsOptionsname, paramsSchemaOrAnnotations
ToolWithParamOrAnnotationsAndDescriptionOptionsname, paramsSchemaOrAnnotations, description
ToolWithParamAndAnnotationsOptionsname, paramsSchema, annotations
ToolWithParamAndAnnotationsAndDescriptionOptionsname, paramsSchema, annotations, description
  • paramsSchema 和 paramsSchemaOrAnnotations 可以是用于输入验证的 Zod 架构。
  • annotations 是一个包含上述协议级提示的对象。
McpContext 参数

所有 MCP 能力方法(@Prompt、@Resource、@Tool)都会将 McpContext 对象作为最后一个参数。它扩展了 SDK 自带的 ServerContext,并包含调用到达时的 Express 请求。

在 2.0 中更名。 该类型原名为 RequestHandlerExtra,MCP SDK 已在 v2 中移除此类型。请参阅从 1.x 迁移到 2.x。

来自 SDK 的属性:

  • mcpReq.id — 此请求的 JSON-RPC id
  • mcpReq.method — 正在处理的方法,例如 tools/call
  • mcpReq._meta / mcpReq.envelope — 请求元数据和 2026-07-28 信封
  • http.authInfo — 经过验证的访问令牌信息(如果运行了身份验证中间件)
  • sessionId — 在 2026-07-28 流量中为 undefined。 协议修订版 2026-07-28 废弃了会话。通过旧版回退机制服务的 2025 年客户端仍可能填充此字段。 切勿根据此字段执行授权分支判断。

由 @nestjs-mcp/server 添加:

  • request — 此调用对应的实时 Express 请求
  • headers — request.headers 的简写
  • reportProgress(progress, total?, message?) — 向调用方发送进度更新;请参阅报告进度

用法示例:

import { McpContext, Resolver, Tool } from '@nestjs-mcp/server';
import { CallToolResult } from '@modelcontextprotocol/server';
import { z } from 'zod';

@Resolver('auth')
export class AuthResolver {
  @Tool({
    name: 'authenticate_user',
    description: 'Authenticates a user with credentials',
    paramsSchema: z.object({ username: z.string(), password: z.string() }),
  })
  authenticateUser(
    params: { username: string; password: string },
    ctx: McpContext, // Always the last parameter
  ): CallToolResult {
    // Headers of THIS call, not of the connection handshake
    const authHeader = ctx.headers.authorization;
    const userAgent = ctx.headers['user-agent'];
    console.log(`Serving ${ctx.mcpReq.method} for: ${String(userAgent)}`);

    // Anything Nest middleware attached upstream is reachable too
    console.log(ctx.request.ip);

    return {
      content: [{ type: 'text', text: 'Authentication successful' }],
    };
  }
}

重要说明:

  • ctx 始终是任何使用 @Resource、 @Prompt 或 @Tool 装饰的方法的最后一个参数
  • headers 在 2.0 中改变了含义。 在 2.0 之前,它们是打开_连接_的请求 (initialize POST)的标头,并在连接的生命周期内保持不变。现在它们是本次调用自身的标头,因此可以正确获取在对话期间过期或被撤销的凭据。针对 1.x 编译的代码仍可正常编译,但开始接收的值会不同(且正确)。
  • ctx 无法进行 JSON 序列化:request 是包含循环引用的 Express 对象,因此 JSON.stringify(ctx) 会抛出异常。请读取所需的字段。
报告进度

长时间运行的工具、提示词或资源可以告知调用方当前进度:

@Tool({
  name: 'analyze_files',
  paramsSchema: z.object({ files: z.array(z.string()) }),
})
async analyzeFiles(
  params: { files: string[] },
  ctx: McpContext,
): Promise<CallToolResult> {
  const total = params.files.length;

  for (let i = 0; i < total; i++) {
    await this.analyzer.analyze(params.files[i]);
    await ctx.reportProgress(i + 1, total, `analyzed ${i + 1}/${total}`);
  }

  return { content: [{ type: 'text', text: `analyzed ${total} files` }] };
}

reportProgress 会发送与客户端在请求 _meta 中提供的 progressToken 相关联的 notifications/progress:

  • 没有令牌则不执行任何操作。 未请求进度的客户端(大多数客户端都不会请求) 不会收到任何内容,调用的响应与未使用此功能时完全相同。
  • 永不拒绝。 无法再发送的更新(交换过程已经结束)会记录为 debug 并丢弃。调用 void ctx.reportProgress(…) 是安全的。
  • 可以脱离上下文使用。 它从不读取 this,因此可作为普通回调传给服务:await importer.run(rows, ctx.reportProgress)。
  • 是否发送取决于 responseMode — 请参阅 进度与 responseMode。
  • 不进行验证或限流:请让 progress 递增,并将更新频率控制在合理范围内。

⚠️ message 会原样发送给调用方。请勿在进度消息中放入机密、凭据或 其他租户的数据。


动态能力

@Tool、@Prompt 和 @Resource 都接受可选的 enabled 选项,用于决定每次请求是否公布并允许调用相应能力。

type McpCapabilityToggle = boolean | Type<McpCapabilityGate>;

interface McpCapabilityGate {
  isEnabled(context: McpRegistrationContext): boolean | Promise<boolean>;
}

interface McpRegistrationContext {
  /** The HTTP request whose capability set is being assembled. */
  request: Request; // express
  /** The effective AuthInfo produced by `auth.strategies` / `auth.authorizers`. */
  authInfo?: AuthInfo; // import type { AuthInfo } from '@nestjs-mcp/server'
  /** 'modern' (2026-07-28) or 'legacy' (2025-era clients). */
  era: ProtocolEra;
}

两种形式,共用一个选项键。不设置 enabled 时,默认行为与该选项出现之前完全相同:始终注册、始终启用,且不会增加请求开销。

静态切换

适用于只需关闭的能力。它不需要类、提供程序,也不需要 await。

@Tool({
  name: 'legacy_tool',
  description: 'Kept for compatibility, not offered to clients',
  enabled: false,
})
legacyTool(): CallToolResult {
  /* ... */
}
能力门控

适用于需要由服务端做出判断的情况。门控是一个类,会像守卫一样从 Nest 容器中解析,因此可以注入服务;其判断结果会通过 await 等待,因此可以执行真正的异步操作。

import { Injectable } from '@nestjs/common';
import {
  McpCapabilityGate,
  McpRegistrationContext,
  Resolver,
  Tool,
} from '@nestjs-mcp/server';

@Injectable()
export class AdminGate implements McpCapabilityGate {
  constructor(private readonly permissions: PermissionsService) {}

  // `authInfo` may be undefined — always optional-chain it.
  async isEnabled(context: McpRegistrationContext): Promise<boolean> {
    if (context.request.headers['x-role'] === 'admin') return true;

    return this.permissions.isAdmin(context.authInfo?.clientId);
  }
}

@Resolver('admin')
export class AdminResolver {
  @Tool({
    name: 'rotate_keys',
    description: 'Only advertised to admin connections',
    enabled: AdminGate,
  })
  rotateKeys(): CallToolResult {
    /* ... */
  }
}

在拥有解析器的模块中将门控注册为提供程序:

@Module({
  imports: [McpModule.forRoot({ name: 'admin', version: '1.0.0' })],
  providers: [PermissionsService, AdminGate, AdminResolver],
})
export class AppModule {}

传入类,绝不要传入实例——在模块作用域中创建的实例无法访问容器,这正是门控必须是类的原因。如果检查只涉及廉价的内存操作,门控也可以同步返回结果(isEnabled(): boolean);boolean | Promise<boolean> 两种形式都适用。

请注意,McpRegistrationContext 不包含 sessionId,这是有意为之。它在 SSE 注册时存在,但在流式 HTTP 中不存在,因为传输层会在处理 initialize POST 时分配该 ID,而此时注册已经完成。若将其暴露出来,同一个门控在不同传输方式下就会表现不同。

客户端看到的内容

禁用的能力是先注册再禁用,绝不会跳过。因此,它不会出现在 tools/list / prompts/list / resources/list 中;调用它时会返回 Tool <name> disabled,而不是 Tool <name> not found——在 SDK 中两者是不同的,对于确实存在但对你关闭的能力,前者才是正确的响应。

五种情况下均故障关闭

无法作出判断的门控绝不会让能力继续暴露:

情况结果
isEnabled 同步抛出异常禁用,记录错误
isEnabled 返回被拒绝的 Promise禁用,记录错误
无法从容器中解析门控类禁用,记录错误——绝不会通过 new 静默创建,因为此类门控的依赖项为 undefined,可能会意外返回真值
声明了门控,但不存在注册上下文禁用,记录错误
disable() 本身抛出异常能力保持启用,并在日志中说明——这是唯一残留的故障开放情况,因为没有其他方式可以禁用能力

门控失败绝不会影响其旁边的能力,也绝不会让请求变成 500。日志行只会注明能力和失败原因——绝不会记录请求、请求头或令牌。

使用前必须了解的规则
  • 每个请求评估一次。 每个 HTTP 请求都会创建一个全新的 MCP 服务,这正是服务保持无状态的原因;enabled 会在服务填充过程中解析。更改门控所读取的任何内容,都会在客户端的下一次调用时生效;无需重新连接。(在 2.0 之前,判定结果在整个连接期间都不会改变。)
  • 不会发出 listChanged 通知。 每个切换操作都会在服务连接到传输层之前应用,而 SDK 会在此处控制通知的分发。本库绝不会发送 notifications/tools/list_changed 或对应的提示词/资源通知。不要指望已连接的客户端会收到变更通知——会话中途不会发生任何变化。
  • 保持门控开销低——它们处于请求处理路径上。 在为客户端提供服务之前,每个 tools/list 和每个 tools/call 都会等待所有门控完成,因此,如果门控查询数据库,就意味着每个请求都会查询一次数据库。对于已经在使用此选项的人来说,这是 2.0 中最重要的行为变化。本库会尽可能限制开销:注册仍是同步的;所有门控会在一轮并发中完成(增加的延迟取决于最慢的单个门控,而非所有门控延迟之和);每种不同的门控类,每个请求只会从容器解析并_请求评估_一次,无论有多少功能共享它。至于门控本身,本库无法限制其开销——请在注入的服务中、提供方作用域内进行缓存。没有内置超时:如果门控始终无法完成,请求就会一直得不到响应。
  • 没有注册上下文的门控会默认拒绝。 两种内置传输方式都会传递上下文。如果你自行通过单个参数调用 RegistryService.registerAll(server),那么任何声明了 门控 的功能都会被禁用,并记录原因。静态的 true/false 以及未设置 enabled 选项的功能不受影响。
  • authInfo 可能是 undefined。 只有当 auth 策略对调用方完成身份验证时(参见身份验证与授权),或者上游中间件设置了 req.auth,才会填充此字段。请写成 context.authInfo?.scopes——如果门控未加保护地直接访问该字段,就会抛出异常,随后默认拒绝,从而静默移除该功能。
  • 不支持请求作用域的门控提供方。 门控通过 moduleRef.get(..., { strict: false }) 解析,失败时回退到 moduleRef.create。这两种方式都无法妥善处理请求作用域的提供方。请使用单例门控,从注册上下文中读取所需内容。
这不能替代守卫

enabled 是发现层面的纵深防御,不是授权机制。守卫会针对每次功能调用,使用该次调用自己的上下文运行。

原因在 2.0 中发生了变化,但结论依然成立。在 2.0 之前,理由很直接:注册上下文只根据 initialize POST 构建,不包含其他内容,因此后续调用不会重新检查。现在情况不同了——门控会看到每次调用各自的请求和 req.auth。不变的是二者在_类型_上的区别:门控会在列表中隐藏某项功能,而守卫会拒绝一次调用。隐藏不等于拒绝。客户端如果已经知道某个工具的名称,就可以不读取列表直接调用;只有守卫能阻止这种调用。

这与 MCP SDK 对其自身的逐请求工厂所采用的界限一致:HTTP 层验证 bearer token,而针对各工具的检查放在处理器中,因为_"处理器是执行该工具时的权威来源。"_。

使用 enabled 决定调用方应该看到哪些内容;使用守卫决定是否允许调用。现在二者看到的是同一个请求,因此可以在二者之间提取共用的判断逻辑——但隐藏功能并不等于强制执行:已经知道工具名称的客户端仍然可以调用它,只有守卫能阻止这种调用。

如果直接调用 RegistryService

RegistryService.registerAll 是 async 的,其第二个参数——注册上下文——必填:

await registry.registerAll(server, { request, era: 'modern' });

只有直接操作注册表本身的代码会受到影响;通常使用 McpModule 不受影响。完整的 2.0 变更列表,请参阅从 1.x 迁移到 2.x。

可运行示例位于 examples/dynamic/——EXAMPLE=dynamic pnpm start:example。


守卫

可将一个或多个守卫应用于 Resolver、单个方法或全局。守卫必须实现 NestJS 的 CanActivate 接口。

全局级守卫

此方法使用标准的 NestJS 全局守卫系统(APP_GUARD)。全局守卫会保护 所有 NestJS 路由,包括 MCP 端点(/mcp)。适用于广泛的身份验证,或适用于任何 MCP 专属逻辑运行前的检查。

// src/guards/global-auth.guard.ts
import { CanActivate, ExecutionContext, Injectable } from '@nestjs/common';
import { Request } from 'express';

@Injectable()
export class GlobalAuthGuard implements CanActivate {
  canActivate(context: ExecutionContext): boolean {
    const request = context.switchToHttp().getRequest<Request>();
    const apiKey = request.headers['x-api-key'];
    // Example: Check for a valid API key
    return !!apiKey && apiKey === 'EXPECTED_KEY';
  }
}

在主模块中全局注册守卫:

// src/app.module.ts
import { Module } from '@nestjs/common';
import { APP_GUARD } from '@nestjs/core';
import { McpModule } from '@nestjs-mcp/server';
import { GlobalAuthGuard } from './guards/global-auth.guard';

@Module({
  imports: [McpModule.forRoot(/*...*/)],
  providers: [
    {
      provide: APP_GUARD,
      useClass: GlobalAuthGuard,
    },
  ],
})
export class AppModule {}

通过 @UseGuards 附加的守卫接收的是 McpExecutionContext,而非 Nest 的 ExecutionContext。应实现 McpGuard(可选用 McpGuard<YourExtra> 指定 类型化身份),而不是 CanActivate:CanActivate 将其参数类型设为 ExecutionContext,因此在严格模式下,若参数类型为 McpExecutionContext, 则 implements CanActivate 无法编译。@UseGuards 接受这两种类型。全局 APP_GUARD 守卫是普通的 Nest 守卫,仍使用 CanActivate。

Resolver 级守卫

这是此库的自定义功能。Resolver 级守卫通过导出的 @UseGuards() 装饰器 (来自 @nestjs-mcp/server)应用于 Resolver 类。该 Resolver 中的所有 MCP 方法(@Prompt、@Resource、@Tool)都会受到这些守卫的保护。可用它为一组相关功能强制执行逻辑(例如角色检查)。

import { UseGuards, Resolver, Prompt } from '@nestjs-mcp/server';
import { RoleGuard } from './guards/role.guard';

@UseGuards(RoleGuard)
@Resolver('admin')
export class AdminResolver {
  @Prompt({ name: 'admin_action' })
  adminAction(/*...*/) {
    /* ... */
  }
  // ... other admin capabilities
}
方法级守卫

这是此库的自定义功能。方法级守卫通过将 @UseGuards() 装饰器直接应用于 MCP 功能方法(@Prompt、@Resource、@Tool)来实现。只有被装饰的方法会受到这些守卫的保护。可用它对特定功能进行细粒度访问控制。

import { UseGuards, Resolver, Prompt, Tool } from '@nestjs-mcp/server';
import { SpecificCheckGuard } from './guards/specific-check.guard';

@Resolver('mixed')
export class MixedResolver {
  @Prompt({ name: 'public_prompt' })
  publicPrompt() {
    /* Publicly accessible */
  }

  @UseGuards(SpecificCheckGuard)
  @Tool({ name: 'protected_tool' })
  protectedTool(/*...*/) {
    /* Requires SpecificCheckGuard to pass */
  }
}

重要: Resolver 级和方法级守卫 仅在调用 MCP 功能时运行,不会在由全局守卫处理的初始连接建立阶段运行。它们使用自定义的 McpExecutionContext。

守卫示例

用于 Resolver 级或方法级保护的守卫:

// src/guards/my-mcp.guard.ts
import { Injectable } from '@nestjs/common';
import { McpExecutionContext, McpGuard } from '@nestjs-mcp/server';

@Injectable()
export class MyMcpGuard implements McpGuard {
  canActivate(context: McpExecutionContext): boolean {
    const request = context.getRequest();
    const userAgent = request.headers['user-agent'];

    const handlerArgs = context.getArgs();

    console.log(
      `Guard activated for ${context.getContext().mcpReq.method} from ${String(userAgent)}`,
    );
    console.log('Handler args:', handlerArgs);

    return true;
  }
}
MCP 执行上下文

使用此库的 @UseGuards() 实现 Resolver 级 或 方法级 守卫时, canActivate 方法接收的是 McpExecutionContext。它提供对 MCP 专属信息的访问:

import { Injectable } from '@nestjs/common';
import { McpExecutionContext, McpGuard } from '@nestjs-mcp/server';

@Injectable()
export class McpAuthGuard implements McpGuard {
  canActivate(context: McpExecutionContext): boolean {
    // The request this capability was INVOKED on — not the connection
    // handshake, as it was before 2.0.
    const request = context.getRequest();

    const authHeader = request.headers.authorization;
    if (!authHeader?.startsWith('Bearer ')) {
      console.log('Guard Denied: Missing or invalid Bearer token.');
      return false;
    }

    const token = authHeader.split(' ')[1];
    return token === 'VALID_TOKEN';
  }
}

McpExecutionContext 的要点:

  • getRequest() — 调用此功能时使用的 Express 请求。从 2.0 开始,它是调用本身的请求,因此过期或已撤销的凭据会被正确识别。在 1.x 中,它是连接握手请求,在整个连接生命周期内保持不变。
  • getContext() — 完整的 McpContext:mcpReq(id、method、_meta、信封)和 http.authInfo。
  • getArgs() — 传递给正在调用的 MCP 处理程序的参数。其结构取决于功能类型;可根据 type 判别属性缩小类型范围。
  • getClass() / getHandler() — Resolver 类和方法。
  • getAuthInfo?.() — 有效的 AuthInfo(经 auth.authorizers 处理后),匿名请求则为 undefined。此接口上将其设为可选,以确保现有测试替身仍能通过编译。
  • getSessionId() 已在 2.0 中移除。协议修订版 2026-07-28 废除了会话,因此没有可返回的 ID,也没有可查询的存储。
  • 这不是 Nest 的 ExecutionContext:没有 switchToHttp()。请直接使用 getRequest()。
依赖注入的守卫

守卫可以注入 NestJS 提供者。使用 @Injectable() 并将守卫注册为提供者:

@Injectable()
export class AuthGuard implements McpGuard {
  constructor(private readonly tokens: TokenService) {}

  async canActivate(context: McpExecutionContext): Promise<boolean> {
    const header = context.getRequest().headers.authorization;
    if (!header) return false;

    // Revocation works now: this is the token sent with THIS call.
    return this.tokens.isValid(header.replace('Bearer ', ''));
  }
}

@Module({
  imports: [McpModule.forRoot({ name: 'my-server', version: '1.0.0' })],
  providers: [AuthGuard, TokenService, MyResolver],
})
export class AppModule {}

容器无法解析或创建的守卫类会拒绝调用并记录错误(自 2.1 起)。请将带有依赖项的守卫注册为提供者。


身份验证与授权

身份验证由你自行实现。 此库不会决定调用方如何进行身份验证,也不会决定凭据通过何种方式传递。你的应用会注册身份验证策略——例如,在你选择的请求头、查询参数或 Cookie 中使用 API 密钥,或使用 Basic 身份验证、会话 Cookie、客户端证书、JWT、不透明 OAuth 令牌:任何可从 HTTP 请求中读取的信息都可以。此库提供相关基础设施:它会在 MCP 处理程序运行前执行这些策略,使用策略声明的质询返回 401/403/429 响应,将身份信息传递给授权器、gate、守卫和处理程序,并且——仅当你使用 OAuth 时——提供发现元数据。授权是你负责的独立层:请求授权器、每项功能的 scopes、gate 和守卫。

HTTP request ─► Layer 1  authentication (strategies, once per request)     ─► req.auth: AuthInfo | 401
             ─► Layer 2a request authorization (authorizers, once per request) ─► req.auth narrowed | 403
             ─► Layer 2b capability authorization (scopes, gates, guards)   ─► handler | 403 / hidden / denied

单个请求的执行顺序:策略 → 授权器 → gate(注册时的 enabled)→ SDK 的 scopes 质询 → 守卫(@UseGuards)→ 处理程序。 未配置 auth 时,端点的行为与之前完全相同。

配置 auth
import { Module } from '@nestjs/common';
import { McpModule } from '@nestjs-mcp/server';

@Module({
  imports: [
    McpModule.forRoot({
      name: 'notes',
      version: '1.0.0',
      auth: {
        strategies: [ApiKeyStrategy, JwtStrategy], // in order; first AuthInfo wins
        authorizers: [WorkspaceAuthorizer], // optional, see below
        optional: false, // default: unrecognized caller → 401
        hideOutOfScope: false, // default: see "Per-capability scopes"
        protectedResource: {
          // optional: OAuth discovery, see below
          resource: 'https://notes.example.com/mcp',
          authorizationServers: ['https://auth.example.com'],
          scopesSupported: ['notes:read', 'notes:write'],
          resourceName: 'Notes',
        },
      },
    }),
  ],
  // Strategies and authorizers are ordinary providers, so they can inject
  // services. They may be declared in any module.
  providers: [ApiKeyStrategy, JwtStrategy, WorkspaceAuthorizer, NotesResolver],
})
export class AppModule {}

每个策略和授权器都会在启动时从容器中解析。无法解析的类会阻止应用启动——此库绝不会回退到 new,因为这会绕过 DI。

使用 forRootAsync 时,请在模块旁边的 providers 中声明这些类;如果工厂返回 protectedResource,还需传入静态标志 protectedResourceMetadata: true(Nest 会在工厂运行前确定控制器):

McpModule.forRootAsync({
  imports: [ConfigModule],
  inject: [ConfigService],
  providers: [JwtStrategy],
  protectedResourceMetadata: true,
  useFactory: (config: ConfigService) => ({
    name: 'notes',
    version: '1.0.0',
    auth: {
      strategies: [JwtStrategy],
      protectedResource: {
        resource: config.getOrThrow('MCP_RESOURCE'),
        authorizationServers: [config.getOrThrow('OAUTH_ISSUER')],
      },
    },
  }),
});
编写策略

策略会返回 SDK 的 AuthInfo(由此包重新导出)、null (“我没有凭据,请尝试下一个”)或抛出异常:

结果HTTP 响应
返回 AuthInfo继续处理;跳过后续策略
所有策略都返回 null401 + WWW-Authenticate,包含所有已注册的质询(不含 error=)
throw new McpUnauthorizedError(description)401 {"error":"unauthorized"} + 已注册的质询;停止处理
throw new OAuthError(OAuthErrorCode.InvalidToken)401 + Bearer error="invalid_token" 质询(OAuth 策略);停止处理
throw new McpHttpError(status, body, headers)原样写入(例如 429,并带有 Retry-After)
其他任何情况500,记录日志时不包含标头或令牌

AuthInfo 是 MCP SDK 的身份类型(处理程序通过 ctx.http.authInfo 接收该类型),与使用的方法无关:clientId 是已认证主体的 ID,token 是所提供的凭据。两者都是必填项。expiresAt 以纪元时间起算的秒为单位。如果返回的 AuthInfo 没有字符串类型的 token、字符串类型的 clientId 和由字符串组成的 scopes 数组,或者其 expiresAt 明显以毫秒为单位(大于 1e11),则这是一个错误,响应为 500;如果其 expiresAt 已经过期,则响应为 401 invalid_token。请将应用身份信息(用户 ID、密钥 ID、工作区)放入 AuthInfo.extra。CORS 预检请求(OPTIONS)永远不会进行身份验证。切勿在错误消息中放入凭据:意外错误只会按其类名和消息记录。

质询。 策略可以声明 readonly challenge,即告知客户端如何提供凭据的 RFC 9110 质询,例如 ApiKey header="x-api-key" 或 Basic realm="notes"。每个 401 都会列出所有已声明的质询;设置了 protectedResource 时,带有 resource_metadata 的 Bearer 质询会排在首位(MCP 客户端会查找它)。如果没有已声明的质询,也没有 protectedResource,则标头为裸 Bearer。如果 challenge 不是一个有效的 RFC 9110 质询,应用会在启动时停止;scopesSupported 或装饰器的 scopes 条目不是 OAuth scope-token(不得包含空格、引号、反斜杠或控制字符)时,也会如此。

从自选标头读取 API 密钥(非 OAuth;无需使用 OAuth 术语):

import { Injectable } from '@nestjs/common';
import {
  AuthenticatedRequest,
  AuthInfo,
  McpAuthStrategy,
  McpUnauthorizedError,
} from '@nestjs-mcp/server';

@Injectable()
export class ApiKeyStrategy implements McpAuthStrategy {
  readonly challenge = 'ApiKey header="x-api-key"';

  constructor(private readonly keys: ApiKeyService) {}

  async authenticate(req: AuthenticatedRequest): Promise<AuthInfo | null> {
    const key = req.headers['x-api-key'];
    if (typeof key !== 'string') return null; // not mine

    const record = await this.keys.find(key);
    if (!record) throw new McpUnauthorizedError('Unknown API key');

    return { token: key, clientId: record.id, scopes: record.scopes };
  }
}

凭据的位置完全由策略决定:读取 req.query.api_key、req.cookies.session、req.socket.getPeerCertificate() 或任意标头。优先使用标头:查询字符串会进入访问日志和代理,而 MCP 授权规范禁止将其用于 OAuth 访问令牌。

通过 Bearer 传递 API 密钥的策略与上述策略相同,只是读取 Authorization。如果 Bearer 值不是你的密钥之一(例如,它的格式符合 JWT 的 xxx.yyy.zzz),则返回 null,以便列表中后续的 JWT 策略仍能处理它。

使用 jose 验证 JWT——检查受众是策略的职责(RFC 8707):不得接受为其他资源签发的令牌。

import { Injectable } from '@nestjs/common';
import { createRemoteJWKSet, jwtVerify } from 'jose';
import {
  AuthenticatedRequest,
  AuthInfo,
  McpAuthStrategy,
  OAuthError,
  OAuthErrorCode,
} from '@nestjs-mcp/server';

const JWKS = createRemoteJWKSet(
  new URL('https://auth.example.com/.well-known/jwks.json'),
);

@Injectable()
export class JwtStrategy implements McpAuthStrategy {
  async authenticate(req: AuthenticatedRequest): Promise<AuthInfo | null> {
    // The auth-scheme is case-insensitive (RFC 9110 §11.1).
    const match = /^Bearer +(\S+)$/i.exec(req.headers.authorization ?? '');
    if (!match) return null;
    const token = match[1];

    try {
      const { payload } = await jwtVerify(token, JWKS, {
        issuer: 'https://auth.example.com',
        audience: 'https://notes.example.com/mcp',
        requiredClaims: ['exp'], // a token without expiry never stops working
      });
      const clientId = [payload.client_id, payload.sub].find(
        (claim): claim is string => typeof claim === 'string',
      );
      if (!clientId) {
        throw new OAuthError(OAuthErrorCode.InvalidToken, 'No client id');
      }
      return {
        token,
        clientId,
        scopes: String(payload.scope ?? '')
          .split(' ')
          .filter(Boolean),
        expiresAt: payload.exp, // seconds
        extra: { sub: payload.sub },
      };
    } catch {
      throw new OAuthError(OAuthErrorCode.InvalidToken, 'Invalid access token');
    }
  }
}
OAuth 发现(protectedResource)

设置 protectedResource 后,库会在由 resource 推导出的路径提供 RFC 9728 受保护资源元数据文档 (https://notes.example.com/mcp → /.well-known/oauth-protected-resource/mcp), 并在库构建的质询中将其作为 resource_metadata 公布: 包括缺少凭据或凭据为 invalid_token 时的 401,以及 insufficient_scope 提权流程中的 403。携带自身质询的 McpAccessDeniedError、McpHttpError 或 McpUnauthorizedError 会按抛出时的内容写入。这样, 支持 OAuth 的 MCP 客户端(claude.ai、ChatGPT)就能找到你的授权 服务器。库不会提供授权服务器元数据——那属于 AS。

端到端试用 OAuth

examples/oauth 会运行一个受 OAuth 2.1 保护的 MCP 服务, 并在旁边启动一个行为类似真实提供商的模拟授权服务器,无需 Docker,也无需提供商账号。如何运行、演示用户和客户端、 失败场景,以及如何将模拟服务器换成 Auth0、Okta 或 Keycloak,详见 示例的 README。

请求授权器(第 2a 层)

McpAuthorizer 在每个请求通过身份验证后运行一次,接收当前的 AuthInfo 并返回用于后续处理的 AuthInfo。凡是会改变调用方_有效_作用域的情况,都应使用它——它必须在 SDK 看到 请求之前运行,因为 SDK 会在任何守卫之前评估 scopes:

@Injectable()
export class WorkspaceAuthorizer implements McpAuthorizer {
  constructor(private readonly members: MembershipService) {}

  async authorize(
    req: AuthenticatedRequest,
    auth: AuthInfo,
  ): Promise<AuthInfo> {
    const workspace = String(req.headers['x-workspace']);
    const role = await this.members.roleOf(auth.clientId, workspace);
    if (!role) throw new McpAccessDeniedError('Not a member of this workspace'); // 403 access_denied

    return {
      ...auth,
      scopes: auth.scopes.filter((scope) => role.allows(scope)), // key ∩ role
      extra: { ...auth.extra, workspace },
    };
  }
}

对于 403 { "error": "access_denied" }(重新授权也无法解决的情况),抛出 McpAccessDeniedError; 对于 403 提权质询,抛出 new OAuthError(OAuthErrorCode.InsufficientScope, …)。 返回的 AuthInfo 将用于门控;scopes、守卫(context.getAuthInfo())和处理器(ctx.http.authInfo) 都会使用它——两者都通过 getAuthInfo(ctx) 读取。

按功能配置作用域(第 2b 层)

@Tool、@Resource 和 @Prompt 接受 scopes:

@Tool({ name: 'add_note', paramsSchema: AddNote, scopes: ['notes:write'] })

SDK 会在分派之前,使用有效的 AuthInfo 检查这些作用域。 auth.scopeSatisfies(granted, required) 可覆盖默认检查(每个必需作用域都已获授权)。未持有这些作用域时的处理方式取决于 hideOutOfScope:

hideOutOfScope是否列出?调用时的行为适用场景
false(默认)是403 insufficient_scope,scope="<required scopes>"授权缺少用户可同意的作用域(OAuth 提权)
true否“已禁用”限制源于重新授权无法解决的因素,例如工作区角色

未配置 auth 时,没有 AuthInfo 的请求不会收到质询 (与 SDK 的 requireScopes 行为一致)。配置了 auth 且设为 optional: true 时,匿名请求会发现带作用域的功能已禁用。

配置 auth 后,tools/list、prompts/list、resources/list 和 resources/templates/list 默认使用私有、TTL 为 0 的缓存提示,因为这些列表因调用方而异。 显式设置的 server.cacheHints 优先。

Resolver 默认值

@Resolver 也接受一个选项对象,其中的 scopes、public 和 hideOutOfScope 会成为该类所有能力的默认值。字符串形式 @Resolver('notes') 保持不变。

@Resolver({ name: 'notes', scopes: ['notes:read'] })
export class NotesResolver {
  @Tool({ name: 'list_notes' }) // requires notes:read (inherited)
  listNotes() {}

  @Tool({ name: 'add_note', scopes: ['notes:write'] }) // requires notes:write only
  addNote() {}

  @Tool({ name: 'about', public: true }) // no scopes: public drops them
  about() {}
}

优先级为 能力 > resolver > 模块:能力的 scopes 会替换 resolver 的 scopes(而不是合并),能力的 public 会覆盖 resolver 的 public,而能力或 resolver 上的 hideOutOfScope 会覆盖 auth.hideOutOfScope。在 @Resolver({ public: true }) 类中声明自身 scopes 的能力受保护,而不是公开的。与方法装饰器一样,作用域令牌会在类定义时进行验证。

公开能力

在 @Tool、@Resource 或 @Prompt(或 @Resolver)上设置 public: true,即可让调用方在没有任何已识别策略的情况下访问该能力, 而 auth 对其他所有情况仍然是必需的:

@Tool({ name: 'search_docs', public: true })
searchDocs() {}

当配置了 auth,但未设置 optional: true,且所有策略都返回 null 时,通常会以 401 响应请求。如果模块声明了至少一个公开能力,则会改为检查已解析的 JSON-RPC 请求体;仅当其中的每条消息(单条消息或批处理)都符合以下条件时,才会匿名处理请求:

  • 握手或存活状态消息——initialize、server/discover、ping、任何 notifications/*;
  • 列表请求——tools/list、prompts/list、resources/list、 resources/templates/list——此时只会列出公开能力;
  • tools/call、prompts/get 或 resources/read,且目标为公开工具、提示词、 资源 URI,或匹配公开资源模板的 URI。

其他情况——其他方法、非公开目标、缺失或格式错误的请求体、GET——都会像以前一样返回相同的 401 和相同的质询。对于匿名请求,所有非公开能力都会被禁用(不会列出,直接调用时也会被“禁用”),因此只有公开能力可以访问。

  • 凭据不会改变任何行为。 带有效凭据的请求看到和调用能力的方式与没有 public 时完全相同;公开能力对所有人开放。
  • public 和 scopes 不能同时使用。 在同一能力(或同一 @Resolver)上同时声明二者会在类定义时抛出错误:“任何人”与“仅持有 X 授权的主体”相互矛盾,默默选用其中之一会意外暴露或锁定某项内容。
  • 没有公开能力,行为不变。 如果没有公开能力,身份验证流程与以前完全相同——不会检查请求体。
  • optional: true 的含义不变。 所有匿名请求都会放行,不检查请求体;未限定作用域的能力仍对其可见,带作用域的能力则会被禁用。
  • 握手也是公开的。 只要有任何公开能力,initialize 和 server/discover(包括服务器的 instructions)就可以匿名读取,因此不要在其中放置机密信息。
  • 支持两个协议时代。 检查会在 SDK 收到请求前对已解析的请求体执行,因此 2025 年代的客户端(例如 @modelcontextprotocol/sdk v1)也可以匿名使用:initialize → tools/list → 公开的 tools/call。
类型化身份(getAuthInfo)

getAuthInfo<TExtra>(ctx) 会返回有效的 AuthInfo——即策略生成并经授权器处理后的信息——可从处理程序的 McpContext 或守卫的 McpExecutionContext 中获取,其中 extra 的类型为 TExtra (McpAuthInfo<TExtra>)。匿名请求的返回值为 undefined。

type WorkspaceExtra = { workspace: string }; // a type alias, not an interface

@Tool({ name: 'list_notes', scopes: ['notes:read'] })
listNotes(ctx: McpContext) {
  const workspace = getAuthInfo<WorkspaceExtra>(ctx)?.extra?.workspace;
  return this.notes.listFor(workspace); // the service decides which rows
}

@Injectable()
export class OwnerGuard implements McpGuard<WorkspaceExtra> {
  canActivate(context: McpExecutionContext<WorkspaceExtra>) {
    return getAuthInfo<WorkspaceExtra>(context)?.extra?.workspace !== undefined;
  }
}

TExtra 是对你的策略和授权器放入 extra 的内容所作的类型断言,而不是运行时检查。McpExecutionContext 接受相同的可选类型参数;不提供该参数时,getAuthInfo() 会像以前一样返回 SDK 的 AuthInfo。请为 TExtra 使用类型别名:接口需要索引签名,而索引签名会允许任意键。

每项工作该使用哪种工具
机制运行时机响应适用场景
strategy每次请求执行一次401识别调用方
authorizer每次请求执行一次,认证后执行403 / 范围缩小后的 AuthInfo租户/工作区成员资格、角色 ∩ 密钥范围
scopes每项功能执行一次,分发前403 insufficient_scope / 隐藏功能所需的 OAuth 范围
gate (enabled)每项功能执行一次,注册时隐藏 +“已禁用”根据任意逻辑控制发现层级的可见性
guard (@UseGuards)每次调用执行一次,处理程序运行前工具错误检查每次调用的参数或正在访问的资源
选择在哪里进行授权

每一层回答不同的问题。该库绝不会授权数据: 它决定调用方是否可以调用某项操作,但绝不决定该操作可以访问哪些记录。

位置回答的问题形式客户端看到的失败
scopes凭证/应用对每项操作获准执行什么声明式,类似于控制器方法上的权限装饰器403 insufficient_scope(需要提升权限),或隐藏
guards (@UseGuards)应用针对某项操作的规则(参数、时间、功能标志)代码,针对每次调用工具错误
authorizers对于此请求,调用方是谁:租户/工作区、角色 ∩ 凭证 scopes代码,每个请求执行一次;用于构造其他所有内容都会读取的 AuthInfo403 access_denied/范围缩窄
application services数据级授权——“这条记录是否属于这个工作区?”——这是事实来源你的领域代码,由处理器通过 getAuthInfo(ctx) 调用处理器返回的任何结果
public哪些操作完全不需要身份声明式,按能力或解析器配置其他所有操作均返回 401
resolver defaults一组操作共用的 scopes/public/hideOutOfScope 默认值声明式,配置在 @Resolver({ … }) 上与 scopes/public 相同
typed identity处理器和 guards 如何读取调用方getAuthInfo<TExtra>(ctx)—

为什么范围检查失败会返回 403,而 guard 检查失败会返回工具错误。 scopes 在分派之前即可确定,因此 SDK 可以直接拒绝 HTTP 请求,返回 403 和 OAuth 客户端知道如何处理的 WWW-Authenticate 挑战(重新同意并授予更多范围)。Guard 在已分派的调用内部运行:此时 MCP 请求已被接受,HTTP 响应也已提交为 JSON-RPC 结果,因此唯一坦诚的处理方式就是为该工具调用 返回错误结果。任何客户端可以通过重新授权来解决的问题,都应放在 scopes(或 authorizer)中;guards 则用于处理重新同意无法改变的规则。

需要了解的事项
  • 全局 APP_GUARD 也会在 /mcp 上运行。 全局注册的 REST 守卫也会看到 MCP 流量;请改为按控制器应用 REST 守卫。这些守卫会在 MCP 控制器验证请求之前运行,因此此时 req.auth 仍为 undefined:请使用 auth.authorizers、在解析器上使用 @UseGuards 或使用能力门来授权 MCP 调用者;这些方式都能看到 AuthInfo。
  • MCP 端点和元数据路由不进行版本控制。 /mcp 和 /.well-known/oauth-protected-resource… 以 VERSION_NEUTRAL 提供服务,因此即使应用启用了 URI 版本控制,它们仍会保留在这些路径上。
  • 全局前缀。 如果调用 app.setGlobalPrefix(),请排除 MCP 路由:app.setGlobalPrefix('api', { exclude: ['mcp', '.well-known/{*path}'] })。
  • 无法解析的守卫现在会拒绝请求(自 2.1 起)。过去,容器无法解析的 @UseGuards 类会回退到 new Guard();现在则会拒绝请求并记录错误日志,与能力门的行为一致。

请参阅 examples/auth,了解一个可运行的服务,其中包含 API 密钥策略、JWT 策略、租户授权器和作用域工具。


无状态

自 2.0 起,此库不会在请求之间保存任何状态。没有会话存储、没有会话 ID,也不需要粘性路由。

SDK 的 createMcpHandler 会根据此库提供的工厂,为每个 HTTP 请求创建一个全新的 McpServer。该工厂会针对正在处理的请求运行 RegistryService.registerAll,因此每个能力处理器和每个守卫都会闭包捕获该请求。响应结束后不会保留任何内容。

这样做的好处

  • 可在普通轮询负载均衡器后运行任意数量的实例。无需粘性会话、共享会话存储,也无需在网关进行会话固定。
  • 实例可在对话过程中重启或自动扩缩容,而不会导致客户端断开。
  • 可部署到无法保持连接开启的无服务器和边缘运行时。

这是对 #121 的解决方案,并通过 test/stateless-load-balancing.e2e-spec.ts 进行了端到端验证:两个独立构建的 Nest 应用位于严格轮询代理后方,由一个 MCP 客户端驱动,并断言 Mcp-Session-Id 从未通过网络传输。

获取请求数据

过去需要通过 SessionManager 查找来获取的所有内容,现在都会直接交给你,而且描述的是_当前_调用,而不是连接握手:

// In a capability handler
handler(params: Params, ctx: McpContext) {
  ctx.headers.authorization; // this call's credential
  ctx.request.ip;            // the live Express request
}

// In a guard
canActivate(context: McpExecutionContext) {
  context.getRequest().headers.authorization;
}

传输选项

该库只公开一个 MCP 端点:ALL /mcp。

在 2.0 中变更。 1.x 提供了 POST/GET/DELETE /mcp,以及 GET /sse 和 POST /messages,并支持按传输方式设置 enabled 开关。HTTP+SSE 传输已移除:它在结构上依赖粘性会话——必须在同一个实例上配对一个长期运行的 GET /sse 和 POST /messages——而这正是无状态设计所要解决的问题。GET 和 DELETE 对 /mcp 的请求属于 2025 年时期的会话操作,现在会返回 405。

向后兼容。 2025 年时期的客户端仍可通过 SDK 的无状态旧版回退机制,在同一端点上使用。所有当前已发布的 MCP 客户端 SDK——包括 @modelcontextprotocol/sdk@1 和 @modelcontextprotocol/client@2——都支持该时期的协议,且二者均已纳入端到端测试套件。

选项会直接传递给 SDK 的 createMcpHandler:

McpModule.forRoot({
  name: 'my-server',
  version: '1.0.0',
  transport: {
    // 'stateless' (default) — serve 2025-era clients per request.
    // 'reject'              — modern-only; refuses 2025-era traffic.
    legacy: 'stateless',

    // 'auto' (default) — JSON, upgrading to SSE if the handler streams
    // 'json'           — never stream (drops mid-call notifications)
    // 'sse'            — always stream
    responseMode: 'auto',

    // SSE keepalive interval; 0 disables. Default 15000.
    keepAliveMs: 15000,
  },
});
选项用途
legacy如何处理 2025 年时期的流量。默认值为 'stateless'。
responseMode现代交互的响应格式。默认值为 'auto'。
keepAliveMsSSE 注释帧的保活间隔。默认值为 15000。
maxSubscriptionssubscriptions/listen 流的最大数量。默认值为 1024。
bussubscriptions/listen 的变更事件总线。可替换为 Redis,以便在多个实例间广播通知。
进度与 responseMode

ctx.reportProgress 是否会传达到客户端,取决于请求所属的协议时代以及 responseMode:

请求时代'auto'(默认)'sse''json'
Modern(2026-07-28)已传达已传达已丢弃 — 发出一条警告,见下文
Legacy(2025,SDK 回退)通过 SSE 传达通过 SSE 传达通过 SSE 传达
  • Modern + 'json'。 SDK 会在此模式下丢弃调用过程中的通知。处理程序首次调用 reportProgress 时,会在 @mcp:progress 下记录一条 warn 日志;SDK 还会在启动时针对任何 'json' 端点打印一次自己的 console.warn。调用本身仍会成功。
  • Legacy 时代。 SDK 的 Legacy 回退始终通过 SSE 响应调用,且不应用 responseMode,因此每种模式下都会传达进度。
  • 'auto' 下的 SSE 升级。 如果没有进度,Modern 调用会返回一个 application/json 正文。首个进度帧发出后,响应变为 200 text/event-stream — 先是进度帧,然后是结果。从此之后发生的错误会作为 JSON-RPC 错误随流发送,而不是作为 HTTP 状态码发送:依赖状态码进行判断的客户端会看到 200。

服务器级协议选项位于 server 下,并会原样传递给 SDK:

server.*用途
requestState.verify验证 MRTR 重试请求中回传的状态 — 见在不同实例上恢复
cacheHints服务器可缓存方法(tools/list、server/discover 等)的 ttlMs / cacheScope。每项资源的提示应放在 @Resource 上。
inputRequiredMRTR 的 maxRounds、roundTimeoutMs
jsonSchemaValidator替换 JSON Schema 验证引擎

⚠️ legacy: 'reject' 会拒绝当前发布的所有客户端 SDK,因为它们都还不支持 2026-07-28 时代。只有在你能控制所有调用方,且它们会发送原始请求时,才选择此项。


MCP 2026-07-28 功能

多轮往返请求(MRTR)

2026-07-28 规范以重试握手取代了由服务器发起的 elicitation/create 和 sampling/createMessage — 它们需要保持双向流打开,因此需要会话。需要输入的处理程序会返回 inputRequired(...);客户端会附上答案,重新发送相同的调用,然后处理程序完成处理。

import {
  acceptedContent,
  CallToolResult,
  inputRequired,
  InputRequiredResult,
  McpContext,
  Resolver,
  Tool,
} from '@nestjs-mcp/server';

@Resolver('ops')
export class OpsResolver {
  @Tool({ name: 'deploy', paramsSchema: z.object({ env: z.string() }) })
  deploy(
    params: { env: string },
    ctx: McpContext,
  ): CallToolResult | InputRequiredResult {
    const answer = acceptedContent<{ confirm: boolean }>(
      ctx.mcpReq.inputResponses,
      'confirm',
    );

    if (!answer) {
      return inputRequired({
        inputRequests: {
          confirm: inputRequired.elicit({
            message: `Deploy to ${params.env}?`,
            requestedSchema: {
              type: 'object',
              properties: { confirm: { type: 'boolean' } },
              required: ['confirm'],
            },
          }),
        },
      });
    }

    return { content: [{ type: 'text', text: `deployed to ${params.env}` }] };
  }
}
在不同实例上恢复

上面的处理程序会根据参数重新推导所有内容,因此任何实例都可以处理重试请求。当往返过程必须携带状态时,请将其封装到 requestState 中 — 状态会经由客户端往返传递,因此它是由攻击者控制的输入,而 SDK 默认不会提供完整性保护。

import { createRequestStateCodec } from '@nestjs-mcp/server';

// The HMAC key must be identical on every instance that might receive the
// echoed value. That shared key is what lets the retry land on another pod.
const codec = createRequestStateCodec<{ item: string }>({
  key: process.env.MCP_STATE_KEY!, // ≥ 32 bytes
});

McpModule.forRoot({
  name: 'shop',
  version: '1.0.0',
  server: { requestState: { verify: (...a) => codec.verify(...a) } },
});
// in the handler
return inputRequired({
  inputRequests: { confirm: inputRequired.elicit({/* … */}) },
  requestState: await codec.mint({ item: params.item }),
});

// on the retry, decoded and verified by whichever instance served it
const state = ctx.mcpReq.requestState<{ item: string }>();

如果没有 verify 钩子,回传的状态会在未经验证的情况下被接受。启用该钩子后,遭到篡改的值会被拒绝 — 这两种情况都涵盖在 test/mcp-features.e2e-spec.ts 中。

客户端必须在每个请求的能力中声明 elicitation: { form: {} },否则服务器会拒绝发出请求(-32021)。

ctx.mcpReq.elicitInput 和 ctx.mcpReq.requestSampling 仍然存在,但已弃用,并且在 2026-07-28 请求中会抛出异常。MRTR 是唯一可用的路径。

结构化输出和显示元数据
@Tool({
  name: 'measure',
  title: 'Measure Something',              // display name
  paramsSchema: z.object({ subject: z.string() }),
  outputSchema: z.object({ subject: z.string(), value: z.number() }),
  icons: [{ src: 'https://example.com/icon.png', mimeType: 'image/png' }],
  _meta: { 'com.example/category': 'diagnostics' },
})
measure(params: { subject: string }): CallToolResult {
  return {
    content: [{ type: 'text', text: `measured ${params.subject}` }],
    structuredContent: { subject: params.subject, value: 42 },
  };
}

title、icons 和 _meta 也可用于 @Prompt。

资源缓存提示
@Resource({
  name: 'catalog',
  uri: 'catalog://items',
  cacheHint: { ttlMs: 60_000, cacheScope: 'public' },
})

ttlMs 和 cacheScope 随 resources/read 结果一起返回,使客户端可以缓存,而不是重新获取。此设计仅适用于资源:tools/list 和 prompts/list 为整个服务返回一个结果,因此没有地方可放置针对单项能力的提示——这些情况请使用 server.cacheHints。

发出 list_changed

注入 McpHttpService 并向其通知器发布。这是该库首次能够发出变更通知。

@Injectable()
export class PermissionsService {
  constructor(private readonly mcp: McpHttpService) {}

  onRoleChanged(): void {
    this.mcp.notify.toolsChanged();
  }
}

通知会发送给持有 subscriptions/listen 流的客户端。在多实例部署中,请提供由 Redis pub/sub 支持的 transport.bus,以便某个实例上的变更能够广播给其他实例上的订阅者。


从 1.x 迁移到 2.x

2.0 采用 MCP 规范 2026-07-28,该规范废弃了会话,并改用 MCP SDK 的 v2 包系列。现有 MCP 客户端仍可正常使用——它们通过 SDK 的无状态旧版回退机制提供服务——但服务端 API 已有变化。

安装
# 1.x
pnpm add @nestjs-mcp/server @modelcontextprotocol/sdk

# 2.x — the SDK split into scoped packages; the server package is what you import types from
pnpm add @nestjs-mcp/server @modelcontextprotocol/server
破坏性变更
1.x2.x原因
RequestHandlerExtraMcpContextSDK 移除了此类型。ctx.request 现在是实时的 Express 请求。
extra.sessionId(已移除)2026-07-28 废弃了会话。ctx.sessionId 仍存在,但在现代流量中为 undefined。
extra.headersctx.headers含义已变更——详见下文。
extra.body(未记录)ctx.request.body现在已声明,且来自发起请求。
SessionManager(已移除)不再有会话存储。请使用 ctx.request 或 context.getRequest()。
McpExecutionContext.getSessionId()(已移除)没有可返回的内容。请使用 getRequest() 或 getContext()。
paramsSchema: { a: z.string() }paramsSchema: z.object({ a: z.string() })v2 使用 Standard Schema;不再支持原始 Zod 结构。
argsSchema: { … }argsSchema: z.object({ … })同上。
transports: { sse, streamable }transport: { legacy, responseMode, … }使用单一端点;选项会传递给 createMcpHandler。
session: { sessionTimeoutMs, … }(已移除)不再有需要超时的会话。
GET /sse + POST /messages(已移除)HTTP+SSE 在结构上依赖粘性会话。
GET / DELETE /mcp405这些是会话操作。
McpModule.forFeature()仍存在未变(仍为空操作)。
沉默的那个——请阅读

extra.headers 在 1.x 中可以编译,在 2.x 中也可以编译,但返回的内容有所不同。

在 1.x 中,它是打开连接的请求所带的标头——initialize POST 请求,或 GET /sse 握手请求的标头。这些标头从会话存储中恢复,并在连接的整个生命周期内保持不变。在 2.x 中,它是当前所处理调用的标头。

这是一个错误修复,也是此次发布的重点:现在可以看到会话进行期间过期或被撤销的 Authorization 标头。但如果你依赖连接级别的值,它们将不再跨调用持久保留。请从工具中创建一个显式句柄,并让模型将其作为参数传回。

处理程序签名
- import { RequestHandlerExtra, Resolver, Tool } from '@nestjs-mcp/server';
- import { CallToolResult } from '@modelcontextprotocol/sdk/types';
+ import { McpContext, Resolver, Tool } from '@nestjs-mcp/server';
+ import { CallToolResult } from '@modelcontextprotocol/server';

  @Resolver('example')
  export class ExampleResolver {
    @Tool({
      name: 'my_tool',
-     paramsSchema: { id: z.string() },
+     paramsSchema: z.object({ id: z.string() }),
    })
-   myTool(params: { id: string }, extra: RequestHandlerExtra): CallToolResult {
-     const auth = extra.headers.authorization;
+   myTool(params: { id: string }, ctx: McpContext): CallToolResult {
+     const auth = ctx.headers.authorization;
      return { content: [{ type: 'text', text: params.id }] };
    }
  }
防护措施
  @Injectable()
  export class AuthGuard implements McpGuard {
-   constructor(private readonly sessionManager: SessionManager) {}
-
    canActivate(context: McpExecutionContext): boolean {
-     const session = this.sessionManager.getSession(context.getSessionId());
-     return !!session?.request.headers.authorization;
+     return !!context.getRequest().headers.authorization;
    }
  }
客户端兼容性

无需更改客户端。legacy: 'stateless' 是默认设置,可在同一个 /mcp 端点为 2025 年的客户端提供服务。只有在你控制所有调用方的情况下,才设置 transport.legacy = 'reject'——目前还没有已发布的客户端 SDK 支持 2026-07-28 年代的协议。


Inspector Playground

使用 Inspector Playground 在浏览器界面中交互式测试和调试 MCP 服务端点。此工具由 @modelcontextprotocol/inspector 提供支持,可用于:

  • 探索可用的资源、工具和提示词
  • 调用端点并实时查看响应
  • 根据 MCP 规范验证服务端实现

要启动 Inspector Playground(请确保 NestJS MCP 服务正在运行):

npx @modelcontextprotocol/inspector

它通常会默认连接到 http://localhost:3000,你也可以指定其他目标 URL。


示例

examples/ 目录包含可直接使用的场景,演示如何注册和公开 MCP 功能。

每个示例都是独立的,并遵循最佳实践。如需了解高级用法,请参阅每个示例中的代码和文档。


更新日志

请参阅 CHANGELOG.md 了解发行说明。


许可证

MIT — 详情请参阅 LICENSE。


贡献

欢迎贡献!请参阅 CONTRIBUTING.md,了解指南、问题报告和拉取请求规则。

贡献前,请阅读我们的 Code of Conduct,了解我们社区对行为的期望。

评论

支持 Markdown

第一次接触 MCP?

从协议是什么,到在客户端里装上第一个服务,这几条回答覆盖了大多数入门问题。

一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。