关于 nestjs-mcp
nestjs-mcp 将 MCP 检查器嵌入运行中的 NestJS 应用,让编码 Agent 检查应用的运行时结构与活动。它通过独立 HTTP 监听器提供模块和提供者图、路由、日志、请求跟踪、事件及定时任务信息。
主要功能
- 01检查模块与提供者关系图
- 02列出路由及执行增强器
- 03读取缓冲的应用日志
- 04跟踪请求及处理步骤
- 05检查事件监听器与定时任务
- 06记录任务和监听器运行情况
适用场景
- 01探索运行中 NestJS 应用的模块图
- 02检查路由守卫与拦截器
- 03依据跟踪信息诊断请求
- 04查看日志、事件和定时任务
MCP 相关
该软件包在 NestJS 应用内通过独立 HTTP 监听器启动 MCP 服务。Claude Code 和 Cursor 等 MCP 客户端可连接其端点并使用检查工具。
配置
本地服务的配置在各客户端中通用,复制后粘贴到客户端的 MCP 配置文件即可。
工具
本地服务需要在你的电脑上运行后才能读取工具,维护者暂未提供工具列表。
详细介绍
@jsirp/nestjs-mcp
嵌入 NestJS 应用中的运行时 MCP 检查器,供编码 Agent 使用。
添加一个模块后,正在运行的应用便会在独立端口上开放一个 MCP 端点,向 Agent(Claude Code、Cursor、
任何 MCP 客户端)说明 Nest 实际构建了什么:模块和提供者图、每条路由经过的守卫、
拦截器、管道和过滤器、应用的 Logger 当前正在写入什么、给定请求经过了哪些增强器以及各自耗时多少、事件监听器和定时任务。
Agent 端无需安装任何内容:将其指向 http://127.0.0.1:7777/ 即可。
目录
- 安装:常规依赖项,而非 devDependency
- 快速开始、独立端点上的单个模块、 从配置源读取选项
- 将 Agent 指向该服务
- 选项
- 工具:图、路由和增强器、日志 和自定义 Logger、请求跟踪、 调试、事件、定时任务、 运行记录
- 工作原理、安全性、限制
- 参与贡献、许可证
安装
npm install @jsirp/nestjs-mcp
将其作为常规依赖项安装,而不是 devDependency。你的 AppModule 会导入它,因此省略 devDependencies 的生产环境安装(npm ci --omit=dev、yarn workspaces focus --production,以及大多数 Docker 镜像的最后阶段)会导致应用启动失败,报错 Cannot find module '@jsirp/nestjs-mcp',而且发生在读取任何选项之前。在生产环境中,它除了占用磁盘空间外不产生任何成本:此时 enabled
默认为 false,模块不会打开监听器、复制日志或包装任何内容。
如果你希望完全从生产构建中移除它,请将其保留在 devDependencies 中,并且只在安装了它的环境中导入,这样生产包就不会解析它:
import { DynamicModule, Module } from '@nestjs/common';
const inspector: DynamicModule[] =
process.env.NODE_ENV === 'production'
? []
: // eslint-disable-next-line @typescript-eslint/no-require-imports
[require('@jsirp/nestjs-mcp').NestMcpModule.forRoot()];
@Module({ imports: [...inspector] })
export class AppModule {}
在 ES module 应用中,可在相同条件下使用 await import('@jsirp/nestjs-mcp')。
Nest 应用已有的所有内容都是对等依赖项:@nestjs/common 和 @nestjs/core 10 或 11、
reflect-metadata、rxjs 和 zod(3.25+ 或 4,MCP SDK 接受的版本)。
@nestjs/event-emitter 和 @nestjs/schedule 是可选对等依赖项:只有应用使用它们时,相应工具才会出现。唯一的运行时依赖项是 @modelcontextprotocol/sdk。
使用此包的受支持方式是安装已发布的 npm 包。不支持将此仓库的检出副本(克隆或 git 子模块)列为宿主 monorepo 的工作区:该仓库保留了自己的
yarn.lock,供其 CI 和独立开发使用,因此 Yarn 会将该文件夹视为单独项目;在其中运行的脚本会失败,直到为其单独安装依赖项为止;而这次安装或任何工作区安装都会引入该包的 devDependencies,其中包含宿主依赖注入无法识别的第二份 Nest(Nest can't resolve dependencies of the NestMcpModule)。已发布的 tarball 仅包含 dist/、此 README、更新日志和许可证;npm 也不会安装包的 devDependencies,因此应用自己的 Nest 是唯一的一份。若要在应用中试用尚未发布的更改,请在此处构建,然后在应用中安装 npm pack 的结果。
快速开始
import { Module } from '@nestjs/common';
import { NestMcpModule } from '@jsirp/nestjs-mcp';
@Module({
imports: [NestMcpModule.forRoot()],
})
export class AppModule {}
创建应用时设置 snapshot: true,这样 Nest 才会保留 graph_* 和 routes_* 工具读取的图:
const app = await NestFactory.create(AppModule, { snapshot: true });
启动时,该模块会在应用服务器旁边开启 Inspector 自己的 HTTP 监听器,该监听器独立于应用服务器,并使用上下文 NestMcp 记录端点:
[NestMcp] Inspector listening at http://127.0.0.1:7777/
应用的任何功能都不会在其前面运行:没有全局前缀、版本控制、中间件、全局守卫、拦截器、管道或过滤器。你的身份验证守卫不会处理 Inspector 调用,你的请求日志记录器不会记录这些调用,OpenAPI 文档不会列出它,而且 Inspector 自己的请求跟踪中也不会出现 Inspector 调用。监听器会随应用关闭(app.close())。
在独立端点上挂载一个模块
forFeature 会挂载一个实例,其作用域为导入它的模块及该模块导入的所有模块。path 是必填项。可以共存任意数量的实例,每个实例都有自己的路径、authorize 和开关,且共用同一个监听器:forRoot 在 / 响应,每个 forFeature 在自己的 path 响应(下例为 http://127.0.0.1:7777/cats)。
首次调用工具时,会从序列化后的图中解析作用域,因此也需要设置 snapshot: true:此时 app_info.scope 会读取 { "kind": "module", "module": "CatsModule" }。如果不设置 snapshot,则会读取 { "kind": "module", "module": null, "error": "scope_unresolved", "fallback": "app" }。闭包会排除 Nest 的内部模块和实例自身的模块,并纳入每个全局模块(@Global()、isGlobal: true)及其导入的模块:该模块无需声明即可注入这些模块导出的内容。
@Module({
imports: [NestMcpModule.forFeature({ path: 'cats' })],
})
export class CatsModule {}
从配置源获取选项
forRootAsync 和 forFeatureAsync 遵循 Nest 的可配置模块模式,因此可以使用任意配置源。以下以 @nestjs/config 为例:
NestMcpModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
enabled: config.get('INSPECTOR_ENABLED') === 'true',
}),
});
path 必须静态指定,与 useFactory 并列,而不是由它返回,因此未指定 path 的 forFeatureAsync 会立即失败。其他所有选项(包括 listen 和 authorize)都可以由工厂提供。enabled 可以通过任一方式指定:静态指定为 false 时,不会注册任何内容;由工厂返回 false 时,该实例不会加入监听器(如果没有其他实例,则不会打开监听器),也不会安装其他内容。
将 Agent 指向它
Claude Code:
claude mcp add --transport http my-nest-app http://127.0.0.1:7777/
或者使用任何能够读取 mcpServers 文件的客户端:
{
"mcpServers": {
"my-nest-app": { "type": "http", "url": "http://127.0.0.1:7777/" }
}
}
URL 不受应用端口、前缀或版本控制影响。同一台机器上的两个应用需要使用两个端口:分别为它们设置自己的 listen.port。
选项
除 forFeature 中的 path 外,均为可选项。
| 选项 | 默认值 | 作用 |
|---|---|---|
enabled | process.env.NODE_ENV !== 'production' | 开关。设为 false 时,实例不会注册任何内容,也不会打开任何内容。这只是默认值,不是规则:可在任何需要的位置将其设为 true,并用 authorize 加以保护。 |
listen | { host: '127.0.0.1', port: 7777 } | 检查器自己的 HTTP 监听器,由应用的所有实例共用:在一个实例上设置,或在所有实例上设置相同的值(两个不同的值会导致启动失败)。port: 0 会选择一个空闲端口;启动日志和 app_info.mcpUrl 会显示所选端口。端口被占用时,启动会失败,并显示包含 listen.port 的错误。 |
path | forRoot 为 '/';forFeature 中必填 | 此实例在监听器上的路由。不应用任何前缀或版本号。两个实例使用相同路径时,启动会失败。 |
authorize | 无 | (req) => boolean | Promise<boolean>,在响应此实例收到的每个请求前,使用原始 Node IncomingMessage 调用。返回 false 或抛出异常(会记录日志)时,响应为 403。 |
tools | 全部为 true | 每个工具组对应一个开关:{ graph, routes, logs, requests, events, schedule, runs }。关闭某个组后,不会注册任何工具,也不会安装任何内容。 |
logs.bufferSize | 2000 | logs_tail 读取的环形缓冲区大小。 |
logs.sink | 无 | createLogSink() 返回的内容,会作为输出添加到你自己的日志记录器中:该日志记录器的日志行也会进入缓冲区。参见读取你自己的日志记录器。 |
logs.capture | 'both' | 缓冲区的填充方式:'nest'(通过 tee 捕获 Nest 的 Logger)、'sink'(仅捕获 logs.sink;不安装 tee)或 'both'。'sink' 要求设置 logs.sink。 |
logs.normalize | 无 | (record) => entry | undefined:将内置映射无法读取的格式的 sink 记录转换为所需格式。 |
requests.bufferSize | 500 | 保留的请求跟踪数量。 |
requests.idHeader | 'x-nest-mcp-request-id' | Agent 设置的关联请求头,用于查找请求的跟踪信息。只读取,不会在响应中回显。 |
requests.headers | 'names' | 跟踪信息保留哪些请求头:'none'、'names' 或 'full'。不会保留请求体,但处理程序抛出异常时存放在变量中的内容除外,需使用 debug。 |
runs.bufferSize | 500 | 保留的任务运行和监听器运行数量。每个应用使用一个缓冲区:第一个安装该缓冲区的实例决定其大小。 |
debug | false | true 或 { stackFrames?, locals? }:跟踪步骤或运行记录的每个错误都会包含其 stack(映射到你的源代码的栈帧)以及抛出异常的栈帧中的 locals。默认关闭,不受 enabled 的设置影响。此设置作用于整个进程:由第一个实例决定,因此所有实例应使用相同的值。参见调试:失败步骤的堆栈。 |
debug.stackFrames | 30 | 每个错误保留的栈帧数,从抛出异常的位置向外最多保留 30 帧。 |
debug.locals | true(启用 debug 时) | 抛出异常时的局部变量值,由工作线程的检查器会话读取(ADR-0005)。设为 false 时会保留堆栈,但不包含局部变量,且不会启动工作线程。对象可设置各项限制:{ frames: 5, depth: 2, properties: 20, stringLength: 200, bytes: 16384, perMinute: 10 }(默认值)。参见抛出异常时的局部变量。 |
serverInfo | 应用的 package.json | 服务器在 MCP initialize 握手中报告的 { name, version }。 |
authorize 是普通函数,不是 Nest 守卫:没有 ExecutionContext。可以通过闭包访问所需内容,或使用注入的服务,在 forRootAsync 工厂中创建:
NestMcpModule.forRootAsync({
imports: [ConfigModule],
inject: [ConfigService],
useFactory: (config: ConfigService) => ({
listen: { host: '0.0.0.0', port: 7777 },
authorize: (req) => req.headers['x-inspector-key'] === config.get('INSPECTOR_KEY'),
}),
});
工具
| 分组 | 工具 | 状态 |
|---|---|---|
app | app_info:Nest 和 Node 版本、运行时间、全局前缀、版本控制、端点路径和 URL(mcpPath、mcpUrl)、实例范围、snapshot 是否开启、哪些分组处于活动状态、debug(是否开启、堆栈如何映射以及 locals worker 的状态),以及实例的工具开关、requests.headers 策略和 debug | ✅ |
graph | graph_modules、graph_module、graph_providers、graph_provider、graph_cycles | ✅ |
routes | routes_list、route_get | ✅ |
logs | logs_tail、logs_levels | ✅ |
requests | requests_list、request_trace | ✅ |
events | events_listeners、events_recent(仅适用于已安装 @nestjs/event-emitter 及其模块的情况) | ✅ |
schedule | schedule_jobs(仅适用于已安装 @nestjs/schedule 及其模块的情况) | ✅ |
runs | runs_list、run_get(仅适用于已安装 @nestjs/schedule 或 @nestjs/event-emitter 及其模块的情况) | ✅ |
每个工具都以单个文本块返回 JSON。错误会以 { "error": "<code>", "hint": "…" }
的形式返回,并带有 isError: true,以及有助于重试的信息(例如 candidates)。
图
从 Nest 在 snapshot: true 时保留的序列化图中读取,绝不从源代码读取。每个回答(包括
错误)都包含 scope,即实例所观察到的边界:forFeature 实例只列出其闭包中的模块,以及这些模块之间的提供者、依赖项和循环。
| 工具 | 输入 | 回答内容 |
|---|---|---|
graph_modules | includeInternal? | 每个模块:global、dynamic、提供者和控制器数量、imports(已声明) |
graph_module | name、includeInternal? | imports(已声明)、globals(由 Nest 关联)、导出项、提供者(token、scope、exported)、控制器、importedBy(声明导入该模块的模块) |
graph_providers | filter?、module?、scope?、includeInternal? | 提供者和可注入项;增强器包含 subtype(guard、pipe 等),若全局注册,还包含 global: true |
graph_provider | token、module?、includeInternal? | 模块、注入范围、dependsOn、dependents、initTimeMs |
graph_cycles | — | cycles:模块导入循环(仅已声明的导入)和类注入循环;如果没有,则为 [] |
- Token 是文本:类使用其名称,字符串或符号 token 使用其描述。绝不返回实例。
- 除非设置
includeInternal: true,否则会隐藏 Nest 自身的模块和提供者(InternalCoreModule、ModuleRef、ApplicationConfig等)。 imports是模块声明的内容。Nest 还会将每个模块关联到每个全局模块,这些关联会列在globals中(设置includeInternal: true时还包括InternalCoreModule);模块声明的全局模块仍会保留在imports中。graph_cycles不包含全局关联:否则每个全局模块都会与其导入的模块形成循环。- 如果多个模块具有相同的类名(例如同一模块的两个动态实例),会按照 Nest 注册的顺序列为
Name#1、Name#2;请使用这些名称作为输入。 dependsOn和dependents统计构造函数注入和属性注入。通过@UseGuards等方式附加的增强器不属于依赖项;routes工具会报告它们。- 未知名称会返回
not_found,并提供最多五个相似的candidates;若某个 token 在多个模块中声明,则会返回ambiguous,并列出各项{ token, module },而module可用于选定其中一个。
路由和增强器
Nest 注册的每个 HTTP 路由,且其控制器属于此实例的作用域。检查器不属于其中之一:它不是控制器。由 RPC 层实现为控制器处理程序的路由是普通路由;由独立中间件提供服务的路由则不在此列。
| 工具 | 输入 | 返回内容 |
|---|---|---|
routes_list | filter?、method?、controller?、module? | 每个路由一条记录:method、path、Controller.handler,以及该路由上运行的守卫、拦截器、管道和过滤器的数量 |
route_get | method、path | 路由:path、rawPath、version、controller、handler、module、guards、interceptors、pipes、filters、middlewares |
-
path是应用实际提供服务的路径,由 Nest 计算得出:全局前缀(路由在exclude列表中时除外)和 URI 版本。rawPath仅包含控制器和方法的路径,是使用标头或媒体类型版本控制时应使用的路径。route_get两种路径都接受;如果某个原始路径对应多个版本,则返回ambiguous和所有完整路径。 -
version是处理程序的版本,或控制器的版本,或defaultVersion;VERSION_NEUTRAL为'neutral'。未启用版本控制时则没有此项。 -
每个增强器的格式为
{ name, level },其中包含类名而非实例。每个列表的顺序与 Nest 执行顺序相同:级别 来源 globalapp.useGlobalGuards(...)等方法,以及APP_GUARD/APP_INTERCEPTOR/APP_PIPE/APP_FILTER提供程序,顺序与 Nest 注册顺序一致controller控制器类上的 @UseGuards、@UseInterceptors、@UsePipes、@UseFiltersmethod处理程序上的相同装饰器;之后是参数管道,其中带有 param(@Param('id', ParseIntPipe)对应'id';无名称的参数对应#n) -
过滤器按相同顺序列出,但解析顺序相反:Nest 先尝试方法的过滤器,再尝试控制器的过滤器,最后尝试全局过滤器,并由第一个
@Catch匹配的过滤器处理异常。 -
middlewares是forRoutes路径和方法与路由原始路径匹配的中间件。普通路径要求完全匹配;通配符按其前缀匹配,并且不会读取exclude(...),因此该列表仅供参考(route_get会在middlewaresMatch中说明这一点)。 -
函数中间件(
consumer.apply((req, res, next) => next()),或nestjs-pino的两个中间件)会显示为<functional middleware from LoggerModule>,位于调用其configure的模块之后。Nest 会将函数包装在一个随机命名的类中,并仅在闭包中保留该函数,因此无法读取函数本身的名称。每个中间件只列出一次。
日志
启动时,模块会将应用设置的日志记录器(通过 app.useLogger、NestFactory.create 的 logger 选项或 Nest 默认的 ConsoleLogger 设置)包装在一个透明的分流器中:每次调用 Nest 的 Logger 时,都会原样传递给你的日志记录器,并复制一份到内存环形缓冲区中,该缓冲区最多保存 logs.bufferSize 条记录。内容不会被遮蔽、过滤或更改,也不会持久化保存。如果你的服务使用自己的日志记录器(PinoLogger、winston 等)记录日志,请通过日志接收器将其输出写入同一缓冲区。分流器和缓冲区在每个进程中只安装一次,并由所有实例共享:第一个安装它们的实例决定 logs.bufferSize;而 logs.capture 不为 'sink' 的实例会安装分流器,因此同一 logs 组中的每个实例都应使用相同的 logs 设置。如果所有实例都关闭 logs 组,则不会安装任何内容。
| 工具 | 输入 | 返回内容 |
|---|---|---|
logs_tail | limit?(≤ 500,默认值为 100)、level?、minLevel?、context?、contains?、source?('nest' 或 'sink')、requestId?、runId?、sinceSeq?、scope?('instance' 或 'app') | entries,按时间从早到晚排列,每项为 { seq, source, timestamp, level, context, message, stack?, requestId?, runId? };lastSeq,可作为下次调用的 sinceSeq;当 sinceSeq 之后的一页结果因达到 limit 而被截断时返回 more;当 sinceSeq 之后的条目已从缓冲区移除时返回 dropped |
logs_levels | — | Nest Logger 当前启用的 levels(如果没有 isLevelEnabled,或使用 capture: 'sink',则为 null)、capture、buffer(size、length、lastSeq)和 sink(connected、received、errors、lastError?;没有 sink 时为 null) |
- 日志级别采用 Nest 的级别:
verbose、debug、log、warn、error、fatal。minLevel会保留该级别及严重程度更高的级别。 source表示条目的来源:'nest'表示来自 Nest 的Logger,'sink'表示通过 sink 传入的日志记录器。context是末尾的字符串参数,按ConsoleLogger的读取方式处理:对于new Logger(MyService.name),它就是类名。在error中,前面的多行字符串是stack。非字符串消息会以 JSON 格式存储,最多 2000 个字符。seq每个条目递增 1,且绝不重复。不提供sinceSeq时,返回最新的limit条目;提供时,返回其后的最早limit条目,因此分页不会跳过或重复任何条目。forFeature实例只保留其作用域内类(控制器、提供者、可注入项)的名称作为context的条目,并通过scopeFilter说明这一点。这只是近似判断:如果context不是类名,或多个模块中存在同名类,则仅按名称判断。scope: 'app'可取消此过滤。- 如果日志记录器报告某个级别已禁用(
isLevelEnabled),该级别仍会照常转发,但不会被复制,因此缓冲区只保存日志记录器保留的内容。与之前一样,设置logger: false时不会打印任何内容,但缓冲区仍会填充。 - 检查器自身的日志行(
context为NestMcp)也会保存在缓冲区中。 requestId是捕获条目时正在跟踪的请求 ID(关联请求头的值,或requests_list显示的生成 ID);logs_tail({ requestId })可读取某个请求的日志行,而request_trace会列出这些日志行。该 ID 会在 tee 或 sink 收到日志行的那一刻从请求的异步上下文中读取,因此,在请求之外写入的条目没有此 ID;在该上下文之外送达的 sink 日志行也没有此 ID,例如日志记录器稍后传递的日志行(异步传输、worker 线程目标,例如pino.transport),或 sink 连接前暂存的日志行。失去异步上下文的中间件之后、首个 guard 之前运行的中间件所记录的日志行也没有此 ID。对于上下文已丢失的请求,响应发送后写入的日志行(例如res.on('finish')监听器中的日志行,如 pino-http 的request completed日志行)也没有此 ID:Node 会在 Nest 的路由器结束响应时调用该监听器。必须启用requests组,条目才会携带此 ID。runId同样是捕获条目时正在执行的作业或监听器 运行 的 ID;logs_tail({ runId })和run_get可读取某次运行的日志行。监听器在请求中运行时写入的日志行会同时携带两个 ID。必须启用runs组。
读取自己的日志记录器
tee 只能看到通过 Nest 的 Logger 输出的内容。通过自己日志记录器记录日志的服务会绕过它:使用 nestjs-pino 时,logs_tail 会收到以下内容:
| 写入方式 | 不使用 sink | 使用 sink |
|---|---|---|
Nest 的 Logger(new Logger(ctx),以及 nestjs-pino 通过 app.useLogger 安装的 Logger) | 是(source: 'nest') | 是(根据 logs.capture,为 'nest'、'sink' 或两者) |
PinoLogger、@InjectPinoLogger() | 否 | 是(source: 'sink') |
pino-http 请求日志行 | 否 | 是(source: 'sink') |
createLogSink() 会返回一个日志 sink:一个可写流,你的日志记录器可以像写入其他输出一样写入它;此外,它还提供 record(entry) 函数,供直接传递对象的日志记录器使用。请在创建日志记录器之前创建它,将它添加到日志记录器的输出中,并将其作为 logs.sink 传入(通过 forRoot,或由 forRootAsync 工厂返回)。sink 只负责读取:其他输出收到的内容与之前完全相同,而且它绝不会向日志记录器抛出错误(错误会被吞掉,并在 logs_levels.sink 中计数)。
pino 与 nestjs-pino。 使用 pino.multistream 同时写入 stdout 和 sink:
import { Module } from '@nestjs/common';
import { createLogSink, NestMcpModule } from '@jsirp/nestjs-mcp';
import { LoggerModule } from 'nestjs-pino';
import pino from 'pino';
const logSink = createLogSink();
@Module({
imports: [
LoggerModule.forRoot({
pinoHttp: [
{ level: 'debug' },
pino.multistream([
{ stream: process.stdout, level: 'debug' },
{ stream: logSink, level: 'debug' },
]),
],
}),
// app.useLogger(app.get(Logger)) routes Nest's Logger through pino too: read it once, from the sink.
NestMcpModule.forRoot({ logs: { sink: logSink, capture: 'sink' } }),
],
})
export class AppModule {}
sink 必须在你的进程中运行:请将它添加到 multistream(或作为普通的 stream),而不是作为 pino.transport 目标;后者会在工作线程中运行。
winston,可与 nest-winston 搭配使用,也可单独使用。 使用一个将每条记录传给 record 的传输器:
import { createLogSink, NestMcpModule } from '@jsirp/nestjs-mcp';
import * as winston from 'winston';
import Transport from 'winston-transport';
const logSink = createLogSink();
class InspectorTransport extends Transport {
override log(info: unknown, next: () => void): void {
logSink.record(info);
next();
}
}
export const logger = winston.createLogger({
transports: [new winston.transports.Console(), new InspectorTransport()],
});
// In AppModule. With WinstonModule as the app logger, Nest's Logger goes through winston too:
NestMcpModule.forRoot({ logs: { sink: logSink, capture: 'sink' } });
sink 开箱即用时读取的内容:
- pino 和 bunyan JSON 行:数值型
level(10 →verbose、20 →debug、30 →log、40 →warn、50 →error、60 →fatal)、msg、time、context(由 nestjs-pino 和 nest-winston 设置),否则使用name,以及err的堆栈。 - 传给
record的对象,例如 winston 的对象:级别名称(info→log、warning→warn、trace和silly→verbose……;忽略颜色代码)、message、timestamp、context。 - 纯文本:每行都作为一条
log记录,行内容作为消息,且不带上下文。
一次写入可以包含多行;每行都会成为一条记录。其他内容会通过 logs.normalize 进行映射。它接收解析后的 JSON 行、作为字符串的纯文本行,或你传给 record 的内容,并返回 { level?, message?, context?, timestamp?, stack? };也可以返回 undefined,以使用内置映射:
NestMcpModule.forRoot({
logs: {
sink: logSink,
normalize: (record) =>
typeof record === 'object' && record !== null && 'severity' in record
? { level: String(record.severity), message: (record as { text?: string }).text }
: undefined,
},
});
如何选择 logs.capture:
'both'(默认值):你的服务通过自己的日志记录器输出日志,而 Nest 的Logger会在其他地方打印(例如 Nest 的ConsoleLogger)。不会产生重复日志。'sink':Nest 的Logger也会通过 sink 所连接的日志记录器输出(即通过app.useLogger设置的 nestjs-pino 或 nest-winston 日志记录器)。如果使用'both',这些日志行会各出现两次,每个source各一次。不安装 tee,因此logs_levels.levels为null。'nest':忽略 sink。
应用启动前到达的内容会由 sink 暂存(最多 1000 条记录),并在检查器连接 sink 时发送;应用关闭时,sink 会断开连接并再次开始暂存。禁用检查器时,sink 永远不会连接,因此只会保留最后 1000 行;如果你从不启用检查器,请勿将它添加到日志记录器的输出中。
请求跟踪
向应用发送请求,然后查询请求的去向。将关联标头(x-nest-mcp-request-id 或 requests.idHeader)设为你选择的 ID,并按该 ID 读取跟踪记录;如果不带标头,请求会获得一个自动生成的 ID,此时可通过 requests_list 查找。
curl -H 'x-nest-mcp-request-id: try-1' http://localhost:3000/api/cats/abc
# then, from the agent: request_trace { "id": "try-1" }
| 工具 | 输入 | 返回内容 |
|---|---|---|
requests_list | limit?(≤ 200,默认值为 50)、method?、path?(URL 的子字符串)、status?、minStatus?、scope?('instance' 或 'app') | requests,按时间从早到晚排列,每项为 { id, method, url, startedAt, statusCode, durationMs, handler, module, steps }(请求运行期间包含 inFlight: true);matched |
request_trace | id、scope? | 跟踪信息:method、url、route、statusCode、durationMs、controller、handler、module、headers、按顺序排列的 steps,以及启用 logs 组时的 logs;如果有多条跟踪记录使用同一 id,则返回最新的一条 |
每个步骤的格式为 { kind, name, level?, outcome, error?, startedAtMs, durationMs },时间从请求开始时计算:
kind | 记录时机 | outcome |
|---|---|---|
guard | canActivate 返回时(或其 Promise 或 Observable 完成时) | allowed、denied(返回 false,Nest 返回 403)或 threw |
interceptor | 记录两次:调用 intercept 时记录 phase: 'enter',其返回的 Observable 响应时记录 phase: 'exit';如果该拦截器在未调用 next.handle() 的情况下就已响应,则在退出记录中包含 shortCircuited: true | ok 或 threw |
pipe | 每个参数记录一次,param 的值与 route_get 显示的一致('id',或无名称参数对应的 #n;如果两个同类型的无名称参数无法区分,则省略);paramType 为 param、query、body 或 custom;绝不记录参数值 | ok 或 threw |
handler | 根据方法的属性键命名为 Controller.handler,与 routes_list 中的命名一致(即使装饰器将方法替换为包装器也如此);在实际运行的位置记录:管道之后、拦截器退出之前。如果管道或拦截器先行中断请求,则不记录 | ok 或 threw |
filter | 你的异常过滤器处理异常时记录,error 中包含该异常 | caught;如果过滤器本身抛出异常,则为 threw |
例如,路由配置了 API 密钥守卫、计时拦截器和 ParseIntPipe 时,GET /api/cats/abc 的记录为 ApiKeyGuard allowed → TimingInterceptor enter → ParseIntPipe(id) threw BadRequestException → TimingInterceptor exit threw,状态码为 400。
- 跟踪记录存储的内容:方法、带查询字符串的 URL、路由模式、状态、耗时、控制器、处理器和步骤;根据
requests.headers记录请求标头:'none'、'names'(默认,仅记录名称)或'full'(记录名称和值,包括凭据:请谨慎选择)。 绝不存储请求或响应的正文。 - 关联标头只读取,不会回传:响应不会携带原本没有的内容。若要复用应用已在传递的 ID(例如代理或请求日志记录器提供的
x-request-id),请设置requests.idHeader: 'x-request-id'。 level(global、controller、method)来自路由的增强器链,因此需要设置snapshot: true,与module一样。未设置时会有步骤,但不会有这些字段。forFeature实例只列出处理器位于其作用域内的跟踪记录;未匹配到任何路由的请求没有处理器,只能从应用作用域查看。scope: 'app'会取消此筛选。- 检查器自身的调用绝不会被跟踪:它们通过自己的监听器接收,而不是经过 Nest。每个应用有一个环形缓冲区,最多存储
requests.bufferSize条跟踪记录,由该应用的各个实例共享;第一个启用requests组的实例决定requests.*。 - 作用域为
REQUEST或TRANSIENT的增强器没有可供观察的长期实例;跟踪记录会在notTraced中列出其路由上未跟踪的增强器。 logs(启用logs组时)是此请求期间写入且仍保留在日志缓冲区中的日志条目,按时间从早到晚排列,与logs_tail返回的条目相同:Nest 自带的异常处理器会在请求期间记录未处理的错误,因此 500 响应也会列出该行。最多列出最新的 200 条;匹配条目超过 200 条时,logsTruncated: true;缓冲区已丢弃其中一些条目时,logsDropped: true,此时列表并非完整记录。不会列出之前使用相同 ID 的请求所产生的行。无论实例的作用域如何,都会列出这些行,因为跟踪记录本身已在作用域内。日志行按照requestId所述方式匹配,并受其限制约束。
调试:失败步骤的调用栈
启用 debug: true 后,跟踪中的每个 error(包括抛出异常的步骤上的 error,以及捕获该异常的筛选步骤上的 error)和 运行 的 error 都会包含 stack:异常从抛出位置向外展开的调用帧,最多为 debug.stackFrames(30)个。
"error": {
"name": "NotFoundException",
"message": "Cat 999 not found",
"stack": [
{ "function": "CatsController.findOne", "file": "/app/src/cats/cats.controller.ts", "line": 31, "column": 21, "mapped": true },
{ "function": "<anonymous>", "file": "/app/node_modules/@nestjs/core/router/router-execution-context.js", "line": 38, "column": 29, "mapped": false, "external": true }
]
}
- 如果找到源映射,
file、line和column就是原始源文件中的位置(mapped: true):如果进程使用--enable-source-maps或process.setSourceMapsEnabled(true)运行,就采用 Node 自身提供的位置(此时调用栈已映射,直接读取即可);否则,采用编译文件通过其sourceMappingURL注释指向的位置(例如,与文件同目录的.js.map,tsc和 Nest CLI 在使用sourceMap: true时会写入此文件;也可以是内联映射)。如果没有源映射,调用帧会保留编译后的位置,并标记为mapped: false。对于非 JavaScript 文件(.ts)中的位置,mapped: true:表示已有内容将其映射,或进程直接运行该文件。检查器不会在进程中启用源映射;app_info.debug.sourceMaps会说明 Node 是否应用源映射(process),以及检查器读取了多少个编译文件自身的映射(files)。 function是 V8 写入的名称(CatsController.find、new Thing、async Promise.all);没有名称时为<anonymous>。匿名异步调用帧(at async /app/dist/main.js:3:7)的值为async <anonymous>,file中仅包含路径,映射方式与其他调用帧相同。external: true表示调用帧位于node_modules、node:内部模块中,或没有文件的调用帧(<anonymous>、native),方便 Agent 跳过这些帧,直接定位到你的代码。这些调用帧会保留,不进行映射。stack仅包含位置信息:函数名称、文件、行号和列号。不会包含源代码文本(读取映射时会丢弃映射中的sourcesContent)。作用域中的值见下方的locals。- 关闭时不作任何更改:错误仍与之前一样表示为
{ name, message },不会读取调用栈,也不会创建工作进程或检查器会话。 - 记录错误时会从
error.stack读取调用栈,与任何日志记录器的读取方式相同;编译文件的映射只读取一次并保留。调用帧数量由 V8 的Error.stackTraceLimit决定(除非进程提高该值,否则为 10);检查器不会更改此值。
抛出时的局部变量
启用 debug(且 debug.locals 不为 false)时,抛出异常的步骤以及失败的运行所产生的错误也会携带 locals:抛出时顶层栈帧中作用域内的变量,并与 stack 对齐(通常,locals[0] 对应错误在抛出位置创建时 stack[0] 指明的栈帧)。
"error": {
"name": "Error",
"message": "failed with 42",
"stack": [{ "function": "OrdersController.total", "file": "/app/src/orders.controller.ts", "line": 18, "column": 11, "mapped": true }, "…"],
"locals": [
{
"function": "OrdersController.total",
"file": "/app/src/orders.controller.ts",
"line": 18,
"scopes": {
"local": { "total": 42, "order": { "[[Class]]": "Order", "id": 7, "lines": "[Array(2)]" }, "note": "nnnn…[500 chars]" },
"closure": { "TAX": 0.21 }
}
},
{ "function": "<anonymous>", "file": "/app/node_modules/@nestjs/core/router/router-execution-context.js", "line": 38, "external": true }
]
}
- 读取内容:顶层
debug.locals.frames个栈帧的local作用域(合并块级作用域和catch作用域)及closure作用域;绝不读取模块、脚本或全局作用域。代码不属于你的栈帧(node_modules、node:)会标记为external: true,且不会读取。 - 值的呈现方式:字符串、数字、布尔值和
null保持原样;普通对象和数组最多展开depth层,然后显示为[Object]、[Array(3)]或类名;类实例会显示"[[Class]]";undefined、NaN、42n和符号会显示为方括号括起的字符串;函数显示名称([Function: find]),绝不显示源码;Map、Set、Date、Promise或代理会显示其描述,绝不展开。仅读取自有的可枚举数据属性:getter 显示为[Getter],且绝不会调用。不会执行应用中的任何代码。 - 限制:每个对象的
properties数量(以及每个作用域的变量数、每个数组的元素数)均有限制,超出部分以"…": "[N more]"表示;字符串达到stringLength后会截断,并附上其长度;每个错误最多读取bytes字节的 JSON,达到上限后停止读取,并在错误中标记localsTruncated: true;每分钟最多捕获perMinute次,达到上限后,错误会标记localsSkipped: 'rate',且不会读取任何内容。 - 捕获哪些异常:在跟踪请求或运行进行期间,由你自己的代码抛出的异常。没有进行中的请求或运行时抛出的异常(启动期间、你设置的计时器触发时)绝不会读取;
node_modules内部抛出的异常(管道的BadRequestException)也不会读取;你的代码在catch块中重新抛出所捕获的错误时也不会读取(首次抛出时会保留其局部变量)。请求期间由你的代码抛出并自行捕获的异常也会读取,并计入速率限制。 - 只针对一个步骤,不是全部步骤:在
request_trace中,局部变量会附加到第一个记录该异常的步骤,也就是抛出异常的步骤;异常经过的拦截器以及捕获异常的筛选器会携带其stack(如有,也会携带localsSkipped),但不会携带局部变量。 - 无法提供局部变量的情况:如果两个具有相同栈文本的异常同时待处理(同一段代码在两个并发请求中失败),就无法区分它们:错误会标记
localsSkipped: 'ambiguous',而不会显示另一个请求的值。如果你的代码在异常抛出后替换了其stack,或抛出的对象不是Error,则不会提供局部变量。 - 性能开销:工作线程作出判断时,主线程会在每次异常发生时暂停;在 M 系列 Mac、Node 20 和 22 上测得,每个未读取的异常约耗时 0.13 ms,每个读取的异常约耗时 1.8 ms(具体数字见 CHANGELOG)。这是开发工具;负载较高时请关闭。
- 它可能暴露作用域内的任何内容:处理程序比对过的密码、解析过的请求正文、闭包中的令牌。请仅在开发环境中启用
debug,并按照安全一节的说明保护端点。 - 连接了你自己的调试器时:工作线程会成为另一个调试器会话。它不会干预自己未触发的暂停(你的断点),但会恢复异常触发的暂停,包括你要求调试器暂停的异常(“在已捕获的异常处暂停”)。手动调试时请设置
debug.locals: false。
事件
安装 @nestjs/event-emitter 并导入 EventEmitterModule 后,检查器会列出各个事件的监听者,并记录触发的事件。如果未安装该程序包或未导入其模块,就不会有 events_* 工具,且 app_info.tools.events 为 false。
| 工具 | 输入 | 返回内容 |
|---|---|---|
events_listeners | event?、scope?('instance' 或 'app') | listeners,每项为 { event, className, methodName, module, options?, requestScoped?, lastRun? };wildcard(事件发射器是否会解析通配符) |
events_recent | limit?(≤ 500,默认值为 50)、event? | entries,按时间从早到晚排列,每项为 { seq, event, method, timestamp, listeners };emissions: 'app-wide' |
- 监听者来自每个提供者和控制器的
@OnEvent元数据,包括请求作用域的监听者。指定event时,如果监听者的模式包含该文本,或者事件发射器本身会因该事件调用该监听者,则视为匹配:当wildcard: true时,events_listeners { "event": "cat.adopted" }也会返回cat.*监听者。 - 通过包装应用的
EventEmitter2上的emit和emitAsync来记录事件触发情况:事件名称、方法、时间,以及当时的listenerCount(event)。**绝不保存负载。**包装器会原样返回原方法的返回值(布尔值或同一个 Promise),并原样抛出其抛出的错误。event可按子字符串筛选,也可作为模式通过事件发射器进行筛选(cat.*)。 forFeature实例只会列出其作用域中声明的监听者。事件触发记录不关联模块,因此每个实例都会看到所有记录;每个事件发射器仅保留一个最新 500 条记录的缓冲区,供各实例共享。
定时任务
安装 @nestjs/schedule 并导入 ScheduleModule 后,检查器会列出应用的 SchedulerRegistry 中保存的内容。如果未安装该程序包或未导入其模块,就不会有 schedule_jobs,且 app_info.tools.schedule 为 false。
| 工具 | 输入 | 返回内容 |
|---|---|---|
schedule_jobs | kind?('cron'、'interval' 或 'timeout')、scope?('instance' 或 'app') | jobs,每项为 { kind, name, className?, methodName?, module?, expression?, timeZone?, everyMs?, delayMs?, running?, lastFiredAt?, nextRun?, lastRun? } |
// schedule_jobs { "kind": "cron" }, for @Cron('0 3 * * *', { name: 'cleanup' }) on CatsService
{ "scope": { "kind": "app" }, "jobs": [{ "kind": "cron", "name": "cleanup", "className": "CatsService",
"methodName": "cleanup", "module": "CatsModule", "expression": "0 3 * * *", "running": true,
"nextRun": "2026-10-26T03:00:00.000+01:00" }] }
- 每次调用都会读取注册表:cron 任务(
expression、running、lastFiredAt由CronJob提供;运行期间还包括nextRun)、间隔任务(everyMs)和超时任务(delayMs;触发或清除后为running: false),也包括应用通过addCronJob/addInterval/addTimeout添加的任务。当timeZone与进程本身的时区不同时,才会显示该字段。 - 类、方法和模块信息来自
@Cron/@Interval/@Timeout元数据,并按名称与注册表中的任务匹配;没有名称的装饰器任务会以生成的 id 记录,如果注册表中恰有一个任务与其表达式或周期匹配,则会与之匹配。 forFeature实例只会列出其作用域中类声明的任务。应用手动添加到注册表的任务没有关联类,因此只有scope: 'app'(或forRoot实例)会列出它。- 启用
runs组后,带装饰器的任务会包含lastRun({ id, startedAt, outcome, durationMs? }),即其最近一次运行;events_listeners的每个监听者也会包含该字段。
执行记录
安装 @nestjs/schedule 或 @nestjs/event-emitter 并导入相应模块后,检查器会记录装饰任务和 @OnEvent 监听器的每次执行(一条执行记录):执行了什么、何时执行、耗时多久,以及是否抛出错误和错误内容;即使调度器和事件发射器会捕获错误并仅将其记录到日志中也是如此。若未安装这两个包,或设置了 tools.runs: false,则不会包装任何内容,也不会提供 runs_list 或 run_get,并且 app_info.tools.runs 为 false。
| 工具 | 输入 | 返回内容 |
|---|---|---|
runs_list | kind?('cron'、'interval'、'timeout' 或 'listener')、name?、event?、outcome?('running'、'ok' 或 'threw')、limit?(≤ 500,默认值为 50)、scope?('instance' 或 'app') | runs,按时间从早到晚排列,每项包含 { id, kind, className, methodName, module, name?, event?, startedAt, durationMs?, outcome, error?, requestId? } |
run_get | id、scope? | run:包含相同字段,以及 logs(最多 200 条,按时间从新到旧排列)、logsTruncated?、logsDropped?;需启用 logs 组 |
// runs_list { "outcome": "threw" }, for @Cron('0 5 * * *', { name: 'purge' }) that threw
{ "scope": { "kind": "app" }, "runs": [{ "id": "…", "kind": "cron", "className": "JobsService",
"methodName": "purge", "module": "JobsModule", "name": "purge",
"startedAt": "2026-10-26T05:00:00.002Z", "durationMs": 1.42, "outcome": "threw",
"error": { "name": "Error", "message": "purge failed" } }] }
- 只有由调度器或事件发射器启动的执行才会被记录。 代码直接调用方法不会被记录。
name匹配任务的装饰器名称或Class.method;event匹配已发射事件的子字符串,或事件发射器会匹配的模式(cat.*)。 - 绝不会记录事件载荷或任务参数。 执行记录会保存类、方法、模块、任务名称或事件、时间、结果,以及错误的名称和消息(启用
debug后还会保存错误的stack,以及locals,其中包含方法参数在作用域内时的值);若执行开始时处于经过跟踪的请求中,还会保存requestId。 - 调度器(
Scheduler)或事件发射器(Event)在方法抛出错误后记录的错误行不属于执行记录,因此会出现在logs_tail中,但不带runId;执行记录的error包含抛出的错误。 forFeature实例只会列出其作用域内类的执行记录。每个应用只有一个缓冲区,保存最新的runs.bufferSize条执行记录,供各实例共享;正在进行的执行记录的outcome: 'running'。
工作原理
- 无状态 Streamable HTTP。 每个
POST都会创建各自的 MCP 服务和传输,并在响应后关闭。GET和DELETE返回 405,并带有Allow: POST:没有可供流式传输或拆除的会话。 - 独立监听器。 每个应用各有一个原生
node:http服务器,由第一个实例在onApplicationBootstrap时启动,并在应用关闭时关闭。它按路径将每个请求路由到相应实例,运行该实例的authorize,并将原始请求交给 MCP 传输。它不会注册控制器,因此应用的任何设置(前缀、版本控制、中间件、增强器、OpenAPI 生成)都不会影响它;即使应用从未调用listen,它也能正常工作。 - 通过包装增强器实例记录跟踪信息。 启用
requests组后,模块会在 Nest 构建路由前,原地包装 Nest 创建的守卫、拦截器、管道和过滤器实例(全局、控制器级和路由级)上的canActivate、intercept、transform和catch。每个包装器都会将一个步骤记录到当前请求的跟踪信息中;该信息保存在由中间件开启的AsyncLocalStorage中。包装器会原样返回原方法的返回值,并保留相同的this和异常。如果后续中间件丢失了该异步上下文(例如在流回调中调用next的中间件,multer 2.3 之前的版本就是如此),守卫、拦截器和过滤器会通过请求对象找到跟踪信息,并在其中继续处理请求;拦截器返回的 Observable 也会在该上下文中订阅,因此其退出阶段(即next.handle()之后运行的部分)也会保留该上下文。模块还会添加一个用于命名处理程序的全局拦截器。关闭该组时,不会进行任何包装,也不会安装任何拦截器。 - 通过包装发射器记录事件。 启用
events组且容器中存在应用的EventEmitter2时,模块会在该实例上将emit和emitAsync替换为函数(仅替换一次),这些函数会记录事件名称和监听器数量,然后调用原方法。发射器的其他部分不会改变。 - 通过读取注册表获取计划任务。 启用
schedule组后,模块会查找应用的SchedulerRegistry,并在每次调用时读取它。不会包装、启动或停止任何内容。 - 通过包装任务和监听器方法记录运行信息。 启用
runs组后,在任何onModuleInit之前,模块会为每个带有@Cron、@Interval、@Timeout或@OnEvent方法的长生命周期提供程序或控制器定义一个自有属性来包装该方法,并携带原方法的元数据,以便调度器和发射器仍能找到这些装饰器。在其onModuleInit中,调度器会绑定任务包装器;在onApplicationBootstrap时,模块会将属性还原,因此你自己的调用会转到原方法。监听器包装器会保留,并且只会在emit/emitAsync(包装器会据此识别事件)内部被调用,且事件为该方法所监听时记录信息。每个包装器都会返回原方法的返回值(同一个 promise),并抛出原方法抛出的异常 (ADR-0006)。 - 通过读取
error.stack获取调用栈。 启用debug后,步骤或运行记录的错误会根据 V8 格式的调用栈进行解析,并通过编译文件的源映射查找每一帧(使用node:module的findSourceMap和SourceMap;无依赖)。不会安装任何内容:不会设置Error.prepareStackTrace,不会更改Error.stackTraceLimit,也不会更改进程的源映射。 - 通过工作线程的检查器会话读取局部变量。 启用
debug.locals后,一个工作线程会在主线程上打开node:inspector会话(connectToMainThread),并保持断点未激活、在每次异常时暂停。暂停时,只有在跟踪或运行正在进行中(通过共享内存中的计数器判断),且栈帧属于你的代码时,它才会使用Runtime.getProperties读取作用域,将其发送到主线程,然后恢复执行;主线程会在记录错误时通过错误的栈文本将捕获结果与错误配对。不会调用Runtime.evaluate或callFunctionOn,也不会设置任何断点 (ADR-0005)。工作线程会在第一个实例创建时启动,并在最后一个实例销毁时停止。 - 没有任何数据会离开进程。 服务器在你的应用内运行;唯一的网络通信是 Agent 调用检查器的监听器。
安全
检查器可以查看应用内部情况。默认情况下,它在生产环境中处于关闭状态,且监听器绑定到环回地址(127.0.0.1),因此只有本机能够访问。如果要在环回地址之外开放访问,请使用 authorize 检查每个请求:
NestMcpModule.forRoot({
enabled: true,
listen: { host: '0.0.0.0' },
authorize: (req) => req.headers.authorization === `Bearer ${process.env.INSPECTOR_TOKEN}`,
});
在环回地址之外监听而不使用 authorize 也能正常工作,但每次启动时都会记录一条警告。应用自身的防护不会作用于检查器:若要在检查器前启用现有身份验证,请在 authorize 中调用它,或在监听器前放置反向代理。
debug 会带来更高风险:启用后,跟踪记录和运行记录会包含每次抛出时的局部变量值(抛出时的局部变量),其中可能包含机密信息和请求正文。无论 enabled 的值如何,默认情况下它都是关闭的。
限制
- 已通过 Express 验证。v1 不支持 Fastify。
graph_*和routes_*需要在NestFactory.create中设置snapshot: true;否则模块会在启动时发出警告,这些工具会返回snapshot_disabled,并指出需要设置的确切选项。- 如果没有日志接收器,日志捕获只能看到通过 Nest 的
Logger输出的内容:使用nestjs-pino时,通过PinoLogger/@InjectPinoLogger()写入的日志行和pino-http请求日志行不会被捕获。使用接收器时,只会保留接收器映射的字段(level、message、context、time、stack);记录的其他字段(req、res、bindings)不会保留。 - tee 会在
app.init()期间(在onModuleInit中)安装。此前写入的日志,包括bufferLogs: true缓存并等待useLogger刷新的日志,以及 Nest 在检查器之前初始化的模块的onModuleInit钩子所写入的日志,都不会进入缓冲区。应用启动后调用useLogger会替换 tee,并停止捕获。日志接收器不受影响:它会保留检查器连接它之前到达的内容。 - 使用 Nest 的默认日志记录器时,每个
Logger都通过同一个共享的ConsoleLogger输出;new Logger(context, { timestamp: true })的独立实例会丢失其耗时后缀。 - 请求跟踪要求在配置模块后构建应用的路由,这也是常规的
NestFactory.create→listen(或init)启动流程。app.init()后添加的全局增强器不会被跟踪(Nest 也不会在现有路由上运行它们)。被冻结的增强器实例不会被包装。Nest 内置的异常处理器不算一个步骤:未被你的过滤器捕获的异常会显示在抛出异常的步骤和状态中。 - 未经过 Nest 中间件的请求(位于全局前缀之外,或在 Nest 路由器启动前由其他组件处理)不会被跟踪。
- 从模块配置时起就会记录事件发射(早于任何
onModuleInit);应用在EventEmitterModule之外自行创建的发射器不会被观察到。通过emitter.on(...)手动添加的监听器会计入listeners,但不会计入events_listeners,后者读取的是@OnEvent元数据。 schedule_jobs会列出SchedulerRegistry中的内容:如果带装饰器的任务未被ScheduleModule注册(例如,请求作用域的 provider,或在forRoot中关闭了对应类型),则不会列出。间隔和超时的周期及待处理状态会从 Node 的计时器对象中读取。- 不会记录请求作用域监听器的运行情况(发射器每次发射都会创建一个实例),也不会记录应用自行添加到
SchedulerRegistry中的任务运行情况,因为这些任务会运行自己的回调。既是任务又是监听器的方法,在调度器于事件发射之外调用时,会记录为一次任务运行。 - 使用
debug时,非Error异常(抛出的字符串或普通对象)没有stack。堆栈帧深度受 V8 的Error.stackTraceLimit限制。若其他库已将 JavaScript 映射后的堆栈从 JavaScript 映射回 JavaScript(例如通过自定义的Error.prepareStackTrace应用了捆绑文件的映射),则会将其读取为编译后的代码。进程运行期间重新构建的编译文件会继续使用首次读取的映射。 - 使用
debug.locals时,如果进程无法使用检查器(Node 构建时未包含检查器),则会记录警告,并记录不含局部变量的堆栈;app_info.debug.locals会说明状态为failed及原因。 - 仅支持 stdio 的客户端无法连接:v1 没有 stdio 桥接。
app_info会报告该包解析出的 Nest 版本,以及工作目录中package.json的应用名称和版本;如果应用从其他位置启动,请设置serverInfo。
贡献
此软件包是独立的 Yarn 4 项目(packageManager 会固定版本;只需运行一次 corepack enable)。
yarn install --immutable
yarn format-check && yarn lint && yarn type-check
yarn build && yarn test # the suite imports dist/index.cjs: build first
开发依赖项会在 Nest 11 上运行测试套件。要在 Nest 10 上运行,请一次性安装四个 Nest 软件包(确保依赖树中只有一个主版本),运行测试套件,然后丢弃这些更改:
yarn add -D @nestjs/common@^10 @nestjs/core@^10 @nestjs/platform-express@^10 @nestjs/testing@^10
yarn build && yarn test
git checkout package.json yarn.lock && yarn install
CI(.github/workflows/ci.yml)会在每次向 main 推送以及每个拉取请求中,在 Nest 10 和 11 上分别使用 Node 20 和 22,运行格式检查、lint、类型检查、构建和测试套件。main 分支保护要求四项 ci 检查全部通过;由维护者在仓库设置中配置,不属于此仓库。
由维护者发布版本,绝不通过工作流发布:在 CHANGELOG.md 顶部添加 ## x.y.z 章节,运行 npm version patch|minor|major(创建提交和 vx.y.z 标签),然后运行 git push --follow-tags。该标签会启动 .github/workflows/release.yml:先运行完整的 CI 矩阵,然后使用仓库密钥 NPM_TOKEN 执行 npm publish --provenance --access public,并创建一个 GitHub 发布,其说明采用对应的 CHANGELOG.md 章节。手动运行该工作流,或使用以 -test 结尾的标签,会完成所有步骤,但使用 npm publish --dry-run,且不会创建发布。
许可证
MIT
评论
一个开放协议,规定了 AI 应用如何以统一方式连接外部工具与数据源。客户端负责与模型对话,MCP 服务负责提供工具、资源和提示模板,两者通过标准消息通信,同一个服务可以被不同客户端复用。