本文内容
NestJS MCP:如何接入现有应用?
现有 NestJS 后端已经定义了谁能读取发票、哪些操作能够修改发票。接入 AI 助手时,应复用这些规则,避免再维护一套权限与业务逻辑。
在现有应用中接入官方 NestJS MCP 模块,应先核对依赖兼容性,再注册 McpModule,并只开放选定的提供者方法。记录权限和业务操作仍由应用服务负责。通过 MCP 客户端验证输入输出、守卫和返回结果,再测试 HTTP 端点,最后连接远程助手。
核查及测试日期:2026年11月4日。我们使用 @nestjs/mcp 0.0.2 编译了一个发票状态查询示例,并执行了十项行为检查,其中两项通过真实 HTTP 请求完成。下载包使用模拟数据和测试令牌。
NestJS 官方 MCP 模块提供了什么?
官方 NestJS MCP 模块可将选定的提供者方法开放为工具、资源和提示模板。模块负责协议处理,并将这些方法接入 Nest 的依赖注入与执行机制。具体开放哪些能力由应用决定,其余服务方法不会进入 MCP 接口。
该版本的 README 说明了 Express、Fastify 和 Node 适配器上的 Streamable HTTP 支持,以及用于本地进程通信的 stdio。@nestjs/mcp 是官方集成包,@rekog/mcp-nest 则是独立的社区项目。NestJS Observe 还提供了用于访问其可观测性数据的 MCP 服务端。
客服助手可以调用管理后台已在使用的发票服务,让事务规则和租户检查继续集中维护。我们建议先开放一项只读操作,逐项确认调用者可访问哪些记录、哪些字段会离开应用,以及请求失败时助手会收到什么。
现有 NestJS 应用满足这个版本的要求吗?
@nestjs/mcp 0.0.2 声明的对等依赖范围为 NestJS 12 和 MCP SDK ^2.4.0。NestJS 10、11 不在这一范围内。先检查依赖树;如果需要升级框架,应单独实施并执行回归检查。
下表依据 0.0.2 的包配置列出依赖范围。我们的示例只验证了其中一组固定版本组合。
| 依赖或运行环境 | 声明的版本要求或文档中的 Schema 支持 | 实际检查项 |
|---|---|---|
@nestjs/common 与 @nestjs/core | ^12.0.0 | 框架、HTTP 适配器和测试包保持版本一致。 |
@modelcontextprotocol/server | ^2.4.0 | 使用 v2 服务端包,区别于旧版合并 SDK 的导入。 |
| Node.js | 本模块要求 >=20.19.0 | 还要检查其他依赖各自的运行时要求。 |
| Zod | 4.2 或更高版本 | 已有 Zod 3 依赖时,先评估大版本升级影响。 |
@modelcontextprotocol/client | 可选对等依赖 ^2.4.0 | 内存客户端测试需要安装。 |
| 包格式 | ESM 导出 | 验证应用实际使用的构建方式及运行时互操作。 |
npm ls @nestjs/core @nestjs/common @modelcontextprotocol/server zod
npm install --save-exact @nestjs/[email protected] \
@modelcontextprotocol/[email protected] [email protected]
npm install --save-dev --save-exact @modelcontextprotocol/[email protected]示例将这些版本与 NestJS 12.3.3、Node.js 24.22.1 配套使用。引入 ESM 依赖不一定要重写整个 CommonJS 应用,但必须验证实际的导入方式和构建流程。强行覆盖对等依赖约束,并不能证明版本兼容。
升级时还要核对 Schema 预处理源码。Zod 3 会被拒绝,ArkType 可以使用。
Valibot 需要 @valibot/to-json-schema 提供的 toStandardJsonSchema();已有 JSON Schema 可以用 MCP 服务端包的 fromJsonSchema() 包装。工具参数必须使用能够转换为 JSON Schema 的对象结构。
如何在现有应用中接入 NestJS MCP?
在现有应用中接入 NestJS MCP,需要注册 McpModule 和工具提供者,并由提供者注入所需的应用服务。将发现范围限定到对应功能模块。HTTP 端点若要开放敏感工具,应先配置真正的令牌验证器。
下面是集成配置示意,其中的 InvoicesModule、IdentityModule 和 IdentityTokenVerifier 需要由你的应用提供。
McpModule.forRoot({
name: 'billing-assistant',
version: '1.0.0',
include: [InvoicesModule],
imports: [IdentityModule],
auth: {
verifier: IdentityTokenVerifier,
resource: 'https://api.example.com/mcp',
authorizationServers: ['https://login.example.com'],
},
})还要在应用中导入 InvoicesModule,并把 InvoiceTools 加入该模块的 providers。MCP 模块的 imports 选项让验证器能够使用导出的依赖。提供者注册表在启动时发现带装饰器的方法,include 决定扫描哪些模块。
以下提供者代码摘自已经运行的示例。下载包还包含它依赖的服务、权限辅助类、守卫和测试模块。
export const InvoiceInput = z.object({
invoiceId: z.string().regex(/^inv_[a-z0-9]+$/).max(64),
}).strict();
export const InvoiceStatus = z.object({
invoiceId: z.string(),
status: z.enum(['open', 'paid']),
});
@Injectable()
export class InvoiceTools {
constructor(
private readonly invoices: InvoicesService,
private readonly access: InvoiceAccess,
) {}
@Tool({
name: 'get-invoice-status',
description: 'Read the status of one invoice available to the signed-in user. Does not modify an invoice.',
inputSchema: InvoiceInput,
outputSchema: InvoiceStatus,
scopes: ['invoices:read'],
annotations: { readOnlyHint: true },
})
@UseGuards(SupportTeamGuard)
async status(input: z.infer<typeof InvoiceInput>, ctx: McpContext) {
const actor = this.access.requirePrincipal(ctx);
return this.invoices.statusForActor(actor, input.invoiceId, ctx.signal);
}
}InvoiceAccess 从已经验证的上下文读取用户主体和租户;InvoicesService 检查该主体能否读取发票。模型只提交发票标识。额外添加租户字段不会获得权限,严格的输入结构也会拒绝未声明字段。
示例只返回标识和状态。直接返回 ORM 发票实体,可能额外暴露地址、税务信息或内部备注,因此应明确约定助手能够接收的输出字段。
找不到端点时,检查全局路径前缀和版本配置。工具没有出现在发现结果中时,检查提供者注册、include 和启动错误。仅添加装饰器,无法让未注册的类被发现。
哪些方法适合做工具、资源或提示模板?
有参数的操作用 NestJS MCP 工具,按 URI 读取的数据用资源,可复用的消息模板用提示模板。每个协议原语都应有清晰的用途,方便审查者一起核对权限和失败行为。
官方协议原语指南说明了装饰器选项及返回结果。对于发票客服应用,我们会这样划分能力。
| 客户端任务 | 协议原语 | 应用侧设计 |
|---|---|---|
| 查询有权访问的一张发票的状态 | @Tool() 配合 inputSchema | 通过已有权限策略完成范围明确的查询。 |
| 读取已发布的计费政策 | @Resource() 配合 uri | 明确受众并管理版本的参考数据。 |
| 通过 URI 读取已授权的发票 | @ResourceTemplate() 配合 uriTemplate | 执行与查询工具相同的记录级权限检查。 |
| 准备客服回复模板 | @Prompt() 配合 argsSchema | 返回供客户端使用的消息;真正发送回复需要独立授权。 |
通用的 run-sql 或 call-any-service 工具更难审查。我们倾向于按业务能力命名工具,并限制输入范围,这样更容易确认它能访问哪些记录、产生哪些变更。
已有 NestJS 守卫能直接用于 MCP 调用吗?
已有守卫可以用于 NestJS MCP 调用,前提是正确处理 mcp 执行上下文。拦截器、管道和异常过滤器也要检查同样的假设。即使守卫注册无误,只适用于 HTTP 的上下文访问方式仍可能读到错误的参数。
MCP 执行上下文辅助类提供输入、调用上下文和认证信息。示例中的工具守卫读取验证器提供的部门声明:
@Injectable()
export class SupportTeamGuard implements CanActivate {
canActivate(context: ExecutionContext): boolean {
const ctx = McpExecutionContext.create(context).getContext();
return ctx.authInfo?.extra?.department === 'support';
}
}把这个守卫应用到 MCP 提供者或方法上。如果全局守卫同时服务 HTTP 路由,应按上下文类型分支,并在每个分支执行相应策略。遇到不认识的上下文就返回 true,会让调用绕过策略。
用户和租户状态应放在参数或调用上下文中。共享提供者上的 this.currentTenant 可能被另一个请求覆盖。注册表会拒绝包含请求作用域依赖的树,因此,注入了请求作用域租户服务的提供者需要先调整设计。
在 0.0.2 中,McpContext 是客户端上下文与应用内部智能体上下文的联合类型。使用客户端专属的 server、era 等字段前,先检查 ctx.transport。内部智能体可选的 idempotencyKey 不会自动为远程 MCP 写入去重。
拦截器应记录操作、结果、关联标识和耗时,避免记录 Bearer 令牌或完整发票内容。也要审查异常消息:主动抛出的 HttpException 可能把消息传给调用者。错误描述应帮助客户端处理失败,同时避免泄露内部记录或凭据。
OAuth scope 与用户权限策略如何配合?
在 NestJS MCP 中,OAuth 权限范围(scope)决定是否允许调用某项能力,应用策略决定调用者可以访问哪些对象。HTTP 模块验证身份提供方签发的令牌。即使令牌有效且包含发票读取权限,仍须检查调用者能否读取指定发票。
授权检查入口将验证器连接到 OAuth 认证质询流程。验证器要检查签发方、签名或令牌内省结果、受众和有效期。仅解码 JWT 后返回配置中的资源 URL,不能证明令牌确实为该资源签发。
invoices:read 允许调用查询能力后,服务仍会核对用户是否有权访问该发票。authInfo.clientId 标识的是 OAuth 客户端,用户身份和租户信息需要另外获取。更完整的设计见企业 MCP 授权架构。
集成 @CurrentUser() 和 @Can(),需要配置 @nestjs/authentication 0.1.3+ 与 @nestjs/authorization 0.1.1+。上游集成测试展示了两个模块如何在工具调用中完成用户注入和策略检查。
文档中的 decorators: [Public()] 让端点路由跳过认证守卫,由 MCP 返回自己的 OAuth 发现质询。它必须配合 MCP auth 和预期的提供者授权一起使用。把 @Public() 普遍加到工具提供者上,并不是同一种配置。
HTTP 端点声明了工具 scope,却没有配置负责验证的 auth,启动时就会被拒绝。stdio 使用另一套安全模型,仅在装饰器中声明 OAuth scope,不能保护本地 stdio 调用。
HTTP、stdio 和旧版客户端有哪些区别?
NestJS MCP 通过 Streamable HTTP 服务远程客户端,通过 stdio 连接本地进程。HTTP 请求相互独立,stdio 则维持进程连接。应结合传输方式和协议版本核对交互要求,尤其是需要用户补充输入的工具。
这些选择的具体行为以官方传输文档为准:
| 连接方式 | 可用能力 | 需要核对的边界 |
|---|---|---|
| HTTP,2026-07-28 客户端 | 独立请求、JSON 或 SSE 响应,以及受支持的用户补充输入流程。 | OAuth、代理路由、允许的来源、请求体限制及客户端实际支持的功能。 |
| HTTP,旧版 2025 客户端 | 通过同一个无状态端点执行普通调用。 | 不支持需要服务端到客户端会话的交互式输入请求。 |
| stdio | 客户端启动服务端进程,支持两代协议。 | 没有 HTTP 请求和 HTTP OAuth scope 检查,需要明确建立本地身份与权限。 |
无状态 HTTP 让模块不再依赖粘性传输会话。数据库、任务队列、限流和业务幂等仍须满足各自的一致性要求。协议层面的整体变化,见 MCP 无状态服务端迁移指南。
浏览器客户端要区分两个配置:模块的 allowedOrigins 接收主机名,Nest CORS 使用来源配置。不带 Origin 头的请求可以通过来源检查,所以它不能替代认证。上游 HTTP 请求体解析器也可能在模块检查大小之前拒绝请求。
使用 stdio 时,通过 McpStdioLogger 输出应用日志,因为 stdout 用来传输协议消息。本地连接成功,不能证明远程部署的 OAuth 发现、TLS 或代理配置正确。
从 mcp-nest 迁移需要改哪些地方?
从 @rekog/mcp-nest 迁移时,需要按当前社区包版本,对照官方模块的提供者、Schema 和上下文 API。替换依赖前,先盘点正在使用的功能。装饰器名称相似,并不代表授权或传输行为相同。
按面向社区包 1.x 与 2.x API 的官方迁移指南核对以下变化。
| 现有集成方式 | 官方模块中的变化 | 迁移时要检查什么 |
|---|---|---|
@modelcontextprotocol/sdk 导入 | 改用 v2 @modelcontextprotocol/server API。 | 删除旧依赖前,检查类型和其他使用方。 |
parameters | 工具改用 inputSchema,提示模板改用 argsSchema。 | 验证公开的结构定义及非法参数的拒绝行为。 |
mcpEndpoint 与模块 guards | 改用 path 与端点 decorators。 | 区分路由守卫和提供者级授权。 |
2.x 的 @McpController() 与策略启动方式 | 改为普通可注入提供者和应用启动流程。 | 将类移入 providers;删除微服务启动代码前,检查其他微服务是否仍在使用。 |
| RPC 上下文读取 | 改用 McpExecutionContext。 | 更新共享守卫、拦截器和过滤器。 |
| 内置授权服务器 | 改为外部身份提供方和验证器。 | 明确迁移令牌签发与客户端登录流程。 |
| 请求作用域提供者或运行时注册 | 当前版本不支持。 | 调整设计,或暂时保留现有集成。 |
该版本还不支持旧的 2024 HTTP+SSE 传输,也不支持在一个应用内运行多个 MCP 服务端。启用 HTTP auth 后,社区模块中针对个别工具允许匿名访问的配置不能直接沿用。
先迁移一个只读工具,尽可能保留名称和调用约定。将工具发现、返回结果和拒绝行为与原有集成逐项比较,再处理写操作及其重放行为。
怎样测试助手实际收到的 MCP 行为?
使用 connectMcpClient() 验证 NestJS MCP 行为,再通过 HTTP 路由检查中间件和部署配置。只直接调用提供者的方法,会跳过协议处理流程,包括参数校验、scope 检查、守卫和结果序列化。
内存测试辅助函数无需打开套接字,就能调用 HTTP 服务及其 Host、Origin 检查。它会绕过端点中间件,以及通过 decorators 指定的路由守卫,并在 http://localhost/mcp 使用简化的请求对象。
| 测试层次 | 应断言的行为 | 仍需单独测试的部分 |
|---|---|---|
| 直接测试服务或提供者 | 业务判断、返回字段和事务行为。 | 协议处理流程及 HTTP 路由。 |
| 内存 MCP 客户端 | 工具发现、输入输出约定、令牌检查、scope、提供者守卫及结果格式。 | HTTP 中间件、端点守卫、真实 CORS 预检、代理和请求体解析器。 |
| 真实 HTTP 与目标客户端 | 认证发现质询、路由、浏览器行为、连接处理和客户端兼容性。 | 每项业务变更仍需明确的状态断言。 |
下面的代码验证了非法输入不会进入发票服务。完整示例负责创建测试模块和已认证客户端,并在测试结束后关闭两者。
const service = moduleRef.get(InvoicesService);
const before = service.calls;
const result = await client.callTool({
name: 'get-invoice-status',
arguments: { invoiceId: 123 },
});
assert.equal(result.isError, true);
assert.equal(service.calls, before);测试指南区分了工具错误与认证失败:非法参数可能返回 isError: true,缺少 scope 会抛出 InsufficientScopeError,缺少必需的令牌则无法建立连接。若认定所有失败都会让 Promise 拒绝,就会漏掉以工具结果返回的错误。
生产环境如果设置了 allowedHosts,应在仅供测试使用的配置中允许辅助函数的 localhost URL,保留生产环境原有的主机限制。
这个集成测试示例验证了什么?
我们的 NestJS MCP 示例通过了 8 项内存检查和 2 项真实 Express HTTP 检查。测试使用一张模拟发票、按租户检查权限的服务替身,以及固定的测试令牌。结果说明了该示例在这些条件下如何处理成功调用和拒绝请求。
测试版本为 @nestjs/mcp 0.0.2、NestJS 12.3.3、MCP 客户端及服务端 2.4.0、Zod 4.6.7、TypeScript 7.0.2 和 Node.js 24.22.1。源码、依赖锁文件和记录的运行输出都在可下载的 NestJS MCP 集成测试示例中。
| 已执行的检查 | 观察结果 |
|---|---|
| 工具发现 | 列出了目标工具及其输入、输出结构。 |
| 已授权的查询 | 结构化内容包含 inv_demo 和 open。 |
| 非法输入 | 返回工具错误,发票服务的调用次数不变。 |
| 另一个租户持有有效令牌 | 无法读取请求中的发票。 |
| 缺少 scope | 抛出 InsufficientScopeError,未调用业务服务。 |
| 部门不符 | 提供者守卫拒绝调用,未调用业务服务。 |
| 缺少令牌 | 客户端连接被拒绝。 |
| 旧版协议客户端 | 已授权的读取成功返回。 |
| 未认证的真实 HTTP 请求 | 返回 401,并携带包含 OAuth 资源元数据地址的认证质询。 |
| HTTP 请求来自未允许的浏览器来源 | 返回 403。 |
要复现测试,解压下载包,在其目录中运行:
npm ci
npm testNode 报告 12 项测试通过,因为它把两个父测试也计入了十项行为检查之外。模拟验证器仅供测试使用。本示例没有验证 Fastify、Node 适配器、生产身份提供方、并发写入或负载均衡行为。
开放写工具之前还要完成哪些设计?
开放 NestJS MCP 写工具之前,要定义授权规则、持久化操作标识、并发规则和恢复方式。请求超时后再次调用会发生什么,应由应用服务决定。装饰器无法替你决定是否再次付款。
以发票更正为例,应记录获批的操作及参数,在执行时重新检查权限,并在重试中保持同一操作标识。同一标识对应的参数发生变化时,要拒绝执行。如果首次操作已经提交,但响应丢失,应返回已记录的结果,或核对并恢复其状态。
这是建议采用的设计,与已经执行的只读测试示例分开。它需要针对实际数据存储进行事务和并发测试。如果模型每次尝试都生成一个新的幂等键,基于键的重复检查就会失效。
MCP 维护者明确说明,工具注解只是行为提示。readOnlyHint 和 idempotentHint 告诉客户端工具的预期行为,但不负责强制执行。同样,在提示模板里要求确认,也不能据此获得执行权限。
首次上线写操作时,我们会要求验证以下可观察结果:
- 调用者无权执行,或无权访问目标对象时,在任何持久化变更发生前拒绝请求。
- 同一项已批准操作的两个并发尝试,只产生预期的一次业务变更。
- 同一操作标识下的参数发生变化时,拒绝执行。
- 提交后的超时可以通过核对记录恢复结果,无需盲目重复变更。
- 审计记录能够说明执行主体、操作、授权结果和最终状态,同时不保存令牌。
无论操作来自 REST、队列任务还是 MCP,都应执行同一套数据授权。智能体数据访问控制指南进一步解释了这个边界。
什么情况下适合使用官方 NestJS MCP 模块?
如果现有应用满足依赖要求,并且已经在服务层执行业务权限,我们会选择官方 NestJS MCP 模块。先实现一个权限、输出和失败路径都能检查清楚的提供者适配层,再通过客户端协议验证。
对于 NestJS 10、11 项目,先确定框架升级方案,再承诺接入 MCP。如果系统依赖请求作用域提供者、运行时工具注册,或同一进程内的多个独立 MCP 服务端,应先解决设计不匹配,再替换当前可用的集成。
Wavect 的 NestJS 开发服务涵盖底层应用改造。MCP 服务端开发服务可帮助你确定工具范围、接入授权并制定验收检查。PromptID 案例介绍了相关的 AI 应用开发工作。
用软件上线前 QA 检查清单将工具测试纳入整体发布决策。先开放一个权限和输出都能说清楚的只读能力,验证拒绝路径,再按业务操作逐步扩大范围。
