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、uriauthInfo(clientId、scopes、expiresAt、extra)request(平台请求;在 stdio 上为undefined)requestInfotransport('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。
- JWT:使用
- 原语上缺少
scopes→ 返回 403 并带有升级挑战。auth.requiredScopes适用于每个请求。 - 使用
@nestjs/authentication≥ 0.1.3 和@nestjs/authorization≥ 0.1.1:- 它们的全局 Guard 验证每个调用,
@CurrentUser()和@Can(Policy, 'action')在工具中工作。 - 在端点上设置
decorators: [Public()],以便auth处理挑战。 - 没有
@Can()的工具会被拒绝。
- 它们的全局 Guard 验证每个调用,
- 仅限内部的服务器可以跳过 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-nest1.x/2.x 迁移。配置生产环境、编写测试或迁移时阅读。