nestjs-mcp について
nestjs-mcpは、実行中のNestJSアプリにMCPインスペクターを組み込み、コーディングエージェントから実行時の構造や動作を確認できるようにします。独立した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 インスペクターです。
モジュールを 1 つ追加すると、実行中のアプリが専用ポートで MCP エンドポイントを開き、エージェント(Claude Code、Cursor、
任意の MCP クライアント)に、Nest が実際に構築したものを伝えます。モジュールとプロバイダーのグラフ、各ルートが通るガード、
インターセプター、パイプ、フィルター、アプリの Logger が現在出力している内容、指定したリクエストが通過したエンハンサーと各処理にかかった時間、
イベントリスナー、スケジュール済みジョブを確認できます。エージェント側にインストールするものはありません。http://127.0.0.1:7777/ を指定するだけです。
目次
- インストール:devDependency ではなく、通常の依存関係として追加します
- クイックスタート、独自のエンドポイントで単独利用するモジュール、 設定ソースからオプションを指定する
- エージェントから接続する
- オプション
- ツール:グラフ、ルートとエンハンサー、ログ と独自のロガー、リクエストトレース、 デバッグ、イベント、スケジュール済みジョブ、 実行
- 仕組み、セキュリティ、制限事項
- コントリビューション、ライセンス
インストール
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 モジュールのアプリでは、同じ条件のもとで 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 サブモジュール)をホストモノレポのワークスペースとして登録する方法はサポートされていません。このリポジトリには独自の yarn.lock があり、CI と単独での開発に必要なため、Yarn はそのフォルダーを別プロジェクトとして扱います。その結果、フォルダー内で実行するスクリプトは、個別にインストールするまで失敗します。また、そのインストールやワークスペースのインストールによってパッケージの devDependencies が取り込まれ、ホスト側の依存性注入が認識しない Nest のコピーが 2 つ目として追加されます(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 {}
graph_* ツールと routes_* ツールが読み取るグラフを Nest に保持させるため、snapshot: true を指定してアプリを作成します。
const app = await NestFactory.create(AppModule, { snapshot: true });
起動時に、このモジュールはアプリのサーバーとは別に、独立したインスペクター専用の HTTP リスナーを開き、コンテキスト NestMcp とともにエンドポイントをログに出力します。
[NestMcp] Inspector listening at http://127.0.0.1:7777/
アプリの処理は何も介在しません。グローバルプレフィックス、バージョニング、ミドルウェア、グローバルガード、インターセプター、パイプ、フィルターは適用されません。認証ガードがインスペクターへの呼び出しを受け取ることはなく、リクエストロガーにも記録されず、OpenAPI ドキュメントにも含まれません。また、インスペクターへの呼び出しがインスペクター自身のリクエストトレースに表示されることもありません。リスナーはアプリとともに閉じます(app.close())。
専用エンドポイントで単一モジュールを公開する
forFeature は、インポート元のモジュールと、そのモジュールがインポートするすべてのモジュールをスコープとするインスタンスをマウントします。path は必須です。同じリスナー上に、独自の 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 の戻り値ではなく、useFactory と並べて静的に指定します。そのため、path を指定せずに forFeatureAsync を使うと、直ちにエラーになります。それ以外の listen や authorize を含むすべてのオプションは、ファクトリーから取得できます。enabled はどちらの方法でも指定できます。静的に false を指定すると何も登録されません。ファクトリーから false が返されると、そのインスタンスはリスナーに参加せず、ほかにインスタンスがなければリスナーも開かれません。また、それ以外のものもインストールされません。
エージェントを接続する
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 はアプリのポート、プレフィックス、バージョニングに依存しません。同じマシン上で 2 つのアプリを動かすには、2 つのポートが必要です。それぞれに独自の listen.port を指定してください。
オプション
forFeature の path を除き、すべて省略可能です。
| オプション | デフォルト | 説明 |
|---|---|---|
enabled | process.env.NODE_ENV !== 'production' | 有効・無効を切り替えます。false の場合、インスタンスは何も登録せず、何も開きません。これはデフォルト値であり、ルールではありません。インスペクターを使いたい場所では true に設定し、authorize で保護してください。 |
listen | { host: '127.0.0.1', port: 7777 } | アプリのすべてのインスタンスで共有される、インスペクター独自の HTTP リスナーです。1つのインスタンスで設定するか、すべてで同じ設定にしてください(異なる設定が2つあると起動時に失敗します)。port: 0 を指定すると空いているポートが選ばれます。起動ログと app_info.mcpUrl にポートが表示されます。使用中のポートを指定すると、listen.port を示すエラーが発生して起動に失敗します。 |
path | forRoot では '/'、forFeature では必須 | リスナー上のインスタンスのルートです。プレフィックスやバージョニングは適用されません。同じパスに2つのインスタンスを設定すると、起動時に失敗します。 |
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' | エージェントがリクエストのトレースを検索するために設定する相関ヘッダーです。読み取られますが、応答には含まれません。 |
requests.headers | 'names' | トレースに保持するリクエストヘッダーです。'none'、'names'、または 'full' を指定します。ハンドラーが例外をスローしたときに変数に保持している値を除き、ボディは記録されません。debugを参照してください。 |
runs.bufferSize | 500 | ジョブとリスナーの実行を保持する数です。アプリごとに1つのバッファがあり、最初にインストールしたインスタンスがサイズを設定します。 |
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 ワーカーの状態)、インスタンスのツール切り替え、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 を返します。エラーは isError: true とともに { "error": "<code>", "hint": "…" } として返され、再試行に役立つ情報(たとえば candidates)も含まれます。
グラフ
ソースではなく、Nest が snapshot: true で保持するシリアライズ済みグラフを読み取ります。エラーを含むすべての回答には scope が含まれ、これはインスタンスが認識する境界を示します。forFeature インスタンスは、そのクロージャー内のモジュールと、それらの間のプロバイダー、依存関係、循環のみを一覧表示します。
| ツール | 入力 | 回答 |
|---|---|---|
graph_modules | includeInternal? | 各モジュール:global、dynamic、プロバイダー数とコントローラー数、(宣言された)imports |
graph_module | name、includeInternal? | (宣言された)imports、Nest がリンクした globals、exports、プロバイダー(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:モジュールのインポート循環(宣言されたインポートのみ)とクラスのインジェクション循環。循環がない場合は [] |
- トークンはテキストです。クラスは名前で、文字列またはシンボルのトークンは説明で表されます。インスタンスは返されません。
- Nest 自身のモジュールやプロバイダー(
InternalCoreModule、ModuleRef、ApplicationConfig、…)は、includeInternal: trueを指定しない限り非表示です。 importsはモジュールが宣言する内容です。Nest はすべてのモジュールをすべてのグローバルモジュールにもリンクし、それらのリンクはglobalsに含まれます(includeInternal: trueの場合はInternalCoreModuleも含まれます)。モジュールが宣言するグローバルモジュールはimportsに残ります。graph_cyclesはグローバルリンクを除外します。これらを含めると、すべてのグローバルモジュールが、そのモジュールがインポートするあらゆるものと循環することになるためです。- 同じクラス名を持つモジュール(たとえば、同一モジュールの 2 つの動的インスタンス)は、Nest に登録された順に
Name#1、Name#2として一覧表示されます。入力にはこれらの名前を使います。 dependsOnとdependentsは、コンストラクターとプロパティのインジェクションを数えます。@UseGuardsなどでアタッチされたエンハンサーは依存関係ではありません。これらはroutesツールが報告します。- 不明な名前に対しては、最大 5 つの類似した
candidatesとともにnot_foundが返されます。複数のモジュールで宣言されたトークンに対しては、それぞれの{ token, module }とともにambiguousが返され、moduleで 1 つを選択できます。
ルートとエンハンサー
インスタンスのスコープ内にあるコントローラーを持ち、Nest が登録したすべての HTTP ルートです。インスペクターはコントローラーではないため、対象に含まれません。RPC レイヤーがコントローラーハンドラーとして実装するルートは通常のルートですが、独自のミドルウェアが処理するものはここでのルートには含まれません。
| ツール | 入力 | 応答 |
|---|---|---|
routes_list | filter?、method?、controller?、module? | ルートごとに 1 件。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はどちらのパスも受け付けます。複数のバージョンで共有されている raw path を指定すると、完全なパスを含む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のパスとメソッドがルートの raw path に一致するミドルウェアです。通常のパスは完全一致し、ワイルドカードはその前のプレフィックスで一致します。exclude(...)は読み取らないため、リストは概算です(route_getはmiddlewaresMatchでその旨を示します)。 -
関数ミドルウェア(
consumer.apply((req, res, next) => next())、またはnestjs-pinoの 2 つ)は、適用したconfigureを持つモジュールの後に<functional middleware from LoggerModule>と表示されます。Nest は関数をランダムな名前のクラスでラップし、関数自体はクロージャー内にのみ保持するため、関数名は読み取れません。それぞれ 1 回だけ表示されます。
ログ
起動時に、モジュールはアプリが設定したロガー(app.useLogger、NestFactory.create の logger オプション、または Nest のデフォルトの ConsoleLogger)を透過的なティーでラップします。Nest の Logger へのすべての呼び出しは、元のとおりにロガーへ届き、そのコピーが logs.bufferSize 件のエントリーを保持するメモリー内リングバッファーに送られます。マスク、フィルタリング、変更は一切行われず、永続化もされません。サービスが独自のロガー(PinoLogger、winston など)を使ってログを記録する場合は、ログシンクを使ってその出力を同じバッファーに渡してください。ティーとバッファーはプロセスごとに 1 回だけインストールされ、すべてのインスタンスで共有されます。最初にインストールするインスタンスによって 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? }。次回 sinceSeq に渡す lastSeq。sinceSeq 以降のページが limit 件で切られた場合は more。sinceSeq 以降のエントリーがすでにバッファから消えている場合は dropped |
logs_levels | — | Nest のロガーで有効な levels(isLevelEnabled がない場合、または capture: 'sink' の場合は null)、capture、buffer(size、length、lastSeq)、sink(connected、received、errors、lastError?。存在しない場合は null) |
- レベルは Nest のものです。
verbose、debug、log、warn、error、fatalです。minLevelを指定すると、そのレベルと、より重大度の高いレベルが対象になります。 sourceはエントリーの発生元を示します。Nest のLoggerの場合は'nest'、シンクを介した独自ロガーの場合は'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の場合は従来どおり何も出力されませんが、バッファには引き続き記録されます。 - インスペクター自身の行(コンテキスト
NestMcp)もバッファに含まれます。 requestIdは、エントリーの記録時にトレースされているリクエストの ID です(相関ヘッダーの値、またはrequests_listに表示される生成済み ID)。logs_tail({ requestId })で単一のリクエストの行を読み取れ、request_traceでそれらを一覧表示できます。これは tee またはシンクが行を受け取った時点で、リクエストの非同期コンテキストから読み取られます。そのため、リクエスト外で書き込まれたエントリーには ID がなく、コンテキスト外で配信されたシンクの行にも ID がありません。後から独自ロガーが渡す行(非同期トランスポートやpino.transportなどのワーカースレッドの宛先)、または接続前にシンクが保持していた行も該当します。非同期コンテキストを失ったミドルウェアの後、最初のガードが実行される前に動作するミドルウェアがログに記録した行にも ID はありません。また、コンテキストを失ったリクエストでレスポンス送信後に書き込まれた行(pino-http のrequest completed行などのres.on('finish')リスナー)にも ID はありません。Node は、Nest のルーターがレスポンスを終了した場所でそのリスナーを呼び出すためです。ID を付けるにはrequestsグループを有効にする必要があります。有効でない場合、どのエントリーにも ID は付きません。runIdも同様に、エントリーの記録時に実行中のジョブまたはリスナーの実行の ID です。logs_tail({ runId })とrun_getで、その実行の行を読み取れます。リスナーがリクエスト内で実行中に書き込んだ行には、両方の ID が付きます。ID を付けるにはrunsグループを有効にする必要があります。
独自のロガーを読み取る
tee が確認するのは、Nest の Logger を経由するものだけです。独自のロガーでログを出力するサービスは tee を通らずに書き込みます。nestjs-pino の場合、logs_tail に届く内容は次のとおりです。
| 出力元 | sink なし | sink あり |
|---|---|---|
Nest の Logger(new Logger(ctx)、および app.useLogger で nestjs-pino がインストールする 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 はアプリケーションのプロセス内で動作する必要があります。pino.transport のターゲットとしてではなく、multistream(または通常の stream)に追加してください。pino.transport はワーカースレッドで動作します。
nest-winston の有無を問わない 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エントリーになります。
1 回の書き込みに複数行が含まれる場合、各行が 1 つのエントリーになります。それ以外は 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ごとに 1 回ずつ、計 2 回記録されます。tee はインストールされないため、logs_levels.levelsはnullです。'nest':sink は無視されます。
アプリケーションの起動前に出力された内容は sink に保持され(最大 1000 エントリー)、インスペクターが接続すると配信されます。アプリケーションが終了すると 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 | 2 回記録されます:intercept の呼び出し時に phase: 'enter'、返された Observable が応答したときに phase: 'exit'。next.handle() を呼び出さずに応答した場合、exit に shortCircuited: true が付きます | ok または threw |
pipe | 引数ごとに記録されます。param は route_get と同様に表示されます('id'、または名前のない引数の場合は #n。同じ型の名前のない引数を区別できない場合は省略)。paramType は param、query、body、custom のいずれかです。値は記録されません | ok または threw |
handler | メソッドのプロパティキーを使って routes_list と同様に Controller.handler と名付けられます(デコレーターがメソッドをラッパーに置き換えた場合も同様)。実行された位置、つまりパイプの後、インターセプターの exit の前に記録されます。パイプまたはインターセプターが先にリクエストを停止した場合は記録されません | 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)はルートのエンハンサー連鎖から取得されるため、moduleと同様にsnapshot: trueが必要です。これがない場合、ステップは表示されますが、これらのフィールドは表示されません。forFeatureインスタンスには、そのスコープ内にハンドラーがあるトレースだけが表示されます。どのルートにも一致しなかったリクエストにはハンドラーがないため、アプリスコープからのみ確認できます。scope: 'app'を設定すると、このフィルターが解除されます。- インスペクター自身への呼び出しはトレースされません。Nest を経由せず、インスペクターのリスナーに届くためです。アプリごとに
requests.bufferSize件のトレースを保持するリングバッファーが 1 つあり、各インスタンスで共有されます。requestsグループを有効にした最初のインスタンスがrequests.*を決定します。 REQUESTまたはTRANSIENTスコープのエンハンサーには、監視対象となる長期間存続するインスタンスがありません。トレースでは、ルート上にあるそれらのエンハンサーがnotTracedに表示されます。logs(logsグループが有効な場合)は、このリクエスト中に書き込まれ、ログバッファーに残っているログエントリーです。logs_tailと同じように、古い順に表示されます。Nest 独自の例外ハンドラーはリクエスト中に未処理エラーをログに記録するため、500 の場合はその行も表示されます。表示件数は最新の最大 200 件です。一致するログがそれより多い場合はlogsTruncated: true、バッファーがすでに一部を破棄している場合はlogsDropped: trueとなり、リストが全件ではないことを示します。同じ ID を再利用した以前のリクエストの行は表示されません。トレース自体がすでにスコープ内にあるため、インスタンスのスコープに関係なく行が表示されます。行はrequestIdの指定に従い、その制限が適用されたうえで照合されます。
デバッグ:失敗したステップのスタック
debug: true の場合、トレース内の各 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)。Node 自体のソースマップが使われるのは、プロセスを--enable-source-maps付きで実行するか、process.setSourceMapsEnabled(true)を呼び出した場合です(その場合、スタックはすでにマッピングされているため、そのまま読み取られます)。それ以外の場合は、コンパイル済みファイルのsourceMappingURLコメントが指すソースマップ(tscや Nest CLI がsourceMap: trueで書き出すような、隣接する.js.mapファイルまたはインラインマップ)が使われます。ソースマップがない場合、フレームはコンパイル済みファイルの位置を保持し、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)を示すため、エージェントは自分のコードまでスキップできます。これらのフレームは保持され、マッピングされません。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]と表示し、呼び出されることはありません。アプリ内のコードは一切評価されません。 - 上限:オブジェクトごとのプロパティ数(スコープごとの変数数、配列ごとの要素数にも適用)に上限があり、超過分は
"…": "[N more]"と表示します。文字列はstringLength文字で切り詰め、長さも表示します。エラーごとの JSON は最大bytesバイトで、超過すると読み取りを停止し、エラーにlocalsTruncated: trueを設定します。取得は最大perMinute回で、超過するとエラーにlocalsSkipped: 'rate'を設定し、何も読み取りません。 - 対象となる例外:トレース対象のリクエストまたは実行の進行中に、自分のコードがスローした例外です。進行中の処理がないときにスローされた例外(起動時や独自のタイマーなど)は読み取りません。また、
node_modules内でスローされた例外(パイプのBadRequestExceptionなど)や、catchブロック内で捕捉したエラーを自分のコードが再スローした場合も読み取りません(最初のスロー時のローカル変数が保持されます)。リクエスト中に自分のコードがスローし、自分で捕捉した例外も読み取り対象となり、取得回数にカウントされます。 - すべてのステップではなく、1 つのステップのみ:
request_traceでは、例外を記録した最初のステップ、つまり例外をスローしたステップにローカル変数が含まれます。例外が通過したインターセプターと、例外を捕捉したフィルターにはstack(および該当する場合はlocalsSkipped)が含まれますが、ローカル変数は含まれません。 - 値が取得されない場合:同じスタックテキストを持つ例外が同時に 2 つ保留中の場合(同じコードが 2 つの並行リクエストで失敗した場合など)、両者を区別できません。その場合、別のリクエストの値を表示する代わりに、エラーに
localsSkipped: 'ambiguous'を設定します。スロー後にコードがstackを置き換えた例外や、Errorではない例外には、ローカル変数は含まれません。 - 負荷への影響:ワーカーが判断する間、例外が発生するたびにメインスレッドが停止します。M シリーズの Mac で Node 20 および 22 を使って測定したところ、読み取らない例外 1 件あたり約 0.13 ms、読み取る例外 1 件あたり約 1.8 ms です(数値は CHANGELOG を参照してください)。これは開発用ツールです。高負荷時は無効にしてください。
- スコープ内にあったあらゆる情報が露出する可能性があります:ハンドラーが比較したパスワード、解析したリクエストボディ、クロージャ内のトークンなどです。
debugは開発時のみ有効にし、Security セクションの説明に従ってエンドポイントを保護してください。 - 独自のデバッガーを接続している場合:ワーカーは追加のデバッガーセッションとして動作します。ワーカーが原因ではない一時停止(ブレークポイントなど)はそのままにしますが、例外での一時停止は再開されます。デバッガーで指定した一時停止(「捕捉された例外で一時停止」など)も対象です。手動でデバッグする際は
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)が記録されます。ペイロードは一切保存されません。ラッパーは元の呼び出しとまったく同じもの(ブール値、同じプロミス)を返し、元の呼び出しと同じ例外をスローします。eventは部分一致、またはエミッターを介したパターン(cat.*)で絞り込みます。 forFeatureインスタンスに表示されるのは、そのスコープで宣言されたリスナーだけです。イベントの発行にはモジュール情報がないため、すべてのインスタンスに表示されます。エミッターごとに最新 500 件を保持するバッファーが 1 つあり、インスタンス間で共有されます。
スケジュール済みジョブ
@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 ジョブ(
CronJobが認識するexpression、running、lastFiredAt、および実行中のnextRun)、インターバル(everyMs)、タイムアウト(発火またはクリア後はrunning: false)が対象となり、アプリがaddCronJob/addInterval/addTimeoutで追加したジョブも含まれます。timeZoneは、プロセス自体のタイムゾーンと異なる場合に表示されます。 - クラス、メソッド、モジュールは
@Cron/@Interval/@Timeoutメタデータから取得され、名前でレジストリーの項目と照合されます。名前なしのデコレーターは、レジストリーでは生成された ID で認識されますが、該当するジョブが 1 つだけの場合は、その式または期間で照合されます。 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)がログに記録するエラー行は、実行記録の外部に書き込まれます。そのため、runIdなしでlogs_tailに含まれます。実行記録のerrorには、スローされた内容が含まれます。 forFeatureインスタンスには、そのスコープ内のクラスの実行だけが表示されます。アプリごとに最新のruns.bufferSize件を保持するバッファーは 1 つで、各インスタンス間で共有されます。実行中のものはoutcome: 'running'になります。
仕組み
- ステートレスな Streamable HTTP。 各
POSTは専用の MCP サーバーとトランスポートを起動し、応答後に終了します。GETとDELETEはAllow: POSTを付けて 405 を返します。ストリーミング先や終了対象となるセッションはありません。 - 独自のリスナー。 アプリごとに 1 つの通常の
node:httpサーバーを使用します。最初のインスタンスがonApplicationBootstrapで起動し、アプリの終了時に閉じます。各リクエストをパスに基づいて該当インスタンスへルーティングし、そのインスタンスのauthorizeを実行して、生のリクエストを MCP トランスポートに渡します。コントローラーは登録されないため、アプリの設定(プレフィックス、バージョニング、ミドルウェア、エンハンサー、OpenAPI 生成)の影響を受けず、listenを一度も呼び出さないアプリでも動作します。 - エンハンサーインスタンスをラップしてトレースします。
requestsグループが有効な場合、モジュールは Nest が構築したガード、インターセプター、パイプ、フィルターのインスタンス(グローバル、コントローラー単位、ルート単位)のcanActivate、intercept、transform、catchをインプレースでラップしてから、Nest がルートを構築します。各ラッパーは、ミドルウェアが開くAsyncLocalStorageに保持された現在のリクエストのトレースにステップを記録し、元のメソッドとまったく同じ値を、同じthisと同じ例外で返します。後続のミドルウェアがその非同期コンテキストを失わせた場合(multer が 2.3 より前のバージョンで行うように、ストリームのコールバックからnextを呼び出す場合など)、ガード、インターセプター、フィルターはリクエストオブジェクトを使ってトレースを見つけ、リクエストの残りをそのコンテキスト内で実行します。インターセプターが返す Observable もそのコンテキスト内でサブスクライブされるため、終了フェーズ(next.handle()の後に実行される処理)でもコンテキストが維持されます。また、ハンドラーの名前を記録するグローバルインターセプターを 1 つ追加します。このグループが無効な場合、ラップもインストールも行われません。 - エミッターをラップしてイベントを記録します。
eventsグループが有効で、アプリのEventEmitter2がコンテナーにある場合、そのインスタンスのemitとemitAsyncを一度だけ、イベント名とリスナー数を記録してから元のメソッドを呼び出す関数に置き換えます。エミッターのその他の部分は変更しません。 - レジストリーを読み取ってスケジュール済みジョブを取得します。
scheduleグループが有効な場合、モジュールはアプリのSchedulerRegistryを見つけ、呼び出しごとに読み取ります。ラップ、起動、停止は行いません。 - ジョブとリスナーのメソッドをラップして実行を記録します。
runsグループが有効な場合、いずれかの@Cron、@Interval、@Timeout、@OnEventメソッドを持つ長寿命のプロバイダーまたはコントローラーに対して、onModuleInitより前に、元のメタデータを保持したラッパーを独自プロパティとして定義します。これにより、スケジューラーとエミッターは引き続きデコレーターを見つけられます。スケジューラーは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)。ワーカーは最初のインスタンスとともに起動し、最後のインスタンスが破棄されると停止します。 - プロセス外へ出るものはありません。 サーバーはアプリ内で動作します。ネットワーク通信は、エージェントがインスペクターのリスナーを呼び出す場合のみです。
セキュリティ
インスペクターにはアプリの内部が表示されます。デフォルトでは本番環境で無効になっており、リスナーはループバック(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によってフラッシュされるまで保持するログや、inspector より先に Nest が初期化するモジュールのonModuleInitフックによるログも含まれます。アプリの起動後にuseLoggerを呼び出すと tee が置き換えられ、キャプチャが停止します。ログシンクには影響しません。ログシンクは inspector が接続する前に到着したログも保持します。 - Nest のデフォルトロガーでは、すべての
Loggerが単一の共有ConsoleLoggerを通じて出力します。インスタンスごとのnew Logger(context, { timestamp: true })では、経過時間のサフィックスが失われます。 - リクエストトレースには、モジュールの設定後にアプリのルートが構築される必要があります。これは通常の
NestFactory.create→listen(またはinit)による起動時の動作です。app.init()の後に追加されたグローバルエンハンサーはトレースされません(Nest は既存のルートに対しても実行しません)。インスタンスが凍結されているエンハンサーはラップされません。Nest 組み込みの例外ハンドラーはステップとして記録されません。独自のフィルターで捕捉されなかった例外は、例外をスローしたステップとステータスに表示されます。 - Nest のミドルウェアを通過しないリクエスト(グローバルプレフィックスの外部、または Nest のルーターより前に処理されるリクエスト)はトレースされません。
- イベントの発行は、モジュールの設定時点(
onModuleInitの前)から記録されます。アプリがEventEmitterModuleの外部で独自に作成したエミッターは監視されません。emitter.on(...)で手動追加されたリスナーはlistenersにはカウントされますが、@OnEventメタデータを読み取るevents_listenersには含まれません。 schedule_jobsに一覧表示されるのは、SchedulerRegistryに登録されている内容です。デコレートされたジョブがScheduleModuleによって登録されなかった場合(リクエストスコープのプロバイダーや、forRootで無効化された種類など)、一覧には表示されません。インターバルとタイムアウトの期間および保留状態は、Node のタイマーオブジェクトから読み取られます。- リクエストスコープのリスナー(エミッターが発行ごとにインスタンスを生成するもの)や、アプリが独自に
SchedulerRegistryに追加したジョブ(独自のコールバックを実行するもの)について、実行は記録されません。ジョブとリスナーの両方であるメソッドは、発行の外部でスケジューラーが呼び出した場合にジョブの実行として記録されます。 debugを有効にした場合、例外がErrorではない(スローされた文字列やプレーンオブジェクトである)とstackはありません。フレームの深さは、V8 が取得できるError.stackTraceLimitの範囲内です。別のライブラリによって JavaScript から JavaScript へすでにマッピングされているスタック(カスタムError.prepareStackTraceがバンドルのマップを適用したもの)は、コンパイル済みコードとして読み取られます。プロセスの実行中に再ビルドされたコンパイル済みファイルは、最初に読み取られたマップを保持します。debug.localsを有効にした場合、inspector を利用できないプロセス(inspector なしでビルドされた 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
devDependencies では Nest 11 でテストスイートを実行します。Nest 10 で実行するには、4 つの Nest パッケージをまとめてインストールし(依存関係ツリーに同じメジャーバージョンが 1 つだけ含まれるようにします)、テストスイートを実行してから、変更を破棄してください。
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 で、フォーマット、リント、型チェック、ビルド、テストスイートを実行します。main のブランチ保護では、4 つの ci チェックの合格が必須です。これはメンテナーがリポジトリの設定で行うもので、このリポジトリには含まれていません。
リリースはメンテナーが作成し、ワークフローが作成することはありません。CHANGELOG.md の先頭に ## x.y.z セクションを追加し、npm version patch|minor|major を実行して(コミットと vx.y.z タグを作成)、その後 git push --follow-tags を実行します。このタグによって .github/workflows/release.yml が開始され、CI の全マトリックスを実行した後、npm publish --provenance --access public(リポジトリシークレットの NPM_TOKEN を使用)を実行し、該当する CHANGELOG.md セクションをリリースノートとする GitHub リリースを作成します。ワークフローを手動で実行した場合、または -test で終わるタグを使用した場合は、すべての処理で npm publish --dry-run が実行され、リリースは作成されません。
ライセンス
MIT
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。