MCP Server NestJS Module Library について
NestJS MCP Server は、NestJS で MCP サーバーを構築するためのモジュール式ライブラリです。デコレーターとモジュールによりリソース、ツール、プロンプトを登録し、公式の @modelcontextprotocol/sdk をラップしています。大規模な MCP サーバーを整理したい NestJS 開発者を想定しています。
主な機能
- 01デコレーターによるツール、リソース、プロンプトの登録
- 02名前空間を指定できる Resolver クラス
- 03forRoot と forFeature によるモジュール登録
- 04ガードと依存性注入のサポート
- 05ツールのアノテーション設定
活用シーン
- 01既存の NestJS サービスを MCP ツールとして公開
- 02大規模な MCP サーバーを機能別モジュールに分割
- 03ガードによる MCP 機能へのアクセス制御
設定
ローカルサーバーの設定はどのクライアントでも共通です。コピーしてクライアントの MCP 設定ファイルに貼り付けてください。
ツール
ローカルサーバーのツールはマシン上で起動して初めて取得できます。メンテナーはまだツール一覧を提供していません。
詳細
MCP サーバー NestJS モジュールライブラリ
概要
NestJS MCP Server は、NestJS を使用して Model Context Protocol (MCP) サーバーを構築するためのモジュール型ライブラリです。デコレーター、モジュール、統合パターンを提供し、MCP のリソース、ツール、プロンプトをスケーラブルかつ保守しやすい方法で公開できます。このプロジェクトは公式の @modelcontextprotocol/sdk のラッパーであり、その型と仕様との互換性を常に維持しています。
目次
- インストール
- クイックスタート
- MCP とは?
- コアコンセプト
- モジュール API
- モジュールの使用方法
- 機能
- 動的な機能
- ガード
- 認証と認可
- ステートレス性
- トランスポートオプション
- MCP 2026-07-28 の機能
1.xから2.xへの移行- Inspector プレイグラウンド
- 例
- 変更履歴
- ライセンス
- 貢献
インストール
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 とは?
Model Context Protocol(MCP) は、LLM を外部データ、ツール、プロンプトに接続するためのオープンプロトコルです。MCP サーバーは、標準化された方法でリソース(データ)、ツール(アクション)、プロンプト(会話フロー)を公開し、LLM を活用するクライアントとのシームレスな統合を実現します。
- 詳細については、Anthropic の発表をご覧ください。
コアコンセプト
サーバー
MCP サーバーは、LLM に機能を公開するための主要なエントリーポイントです。リソース、ツール、プロンプトの登録と検出を管理します。
リソース
リソースは、LLM がクエリまたは取得できる構造化データやドキュメントを表します。通常、リソースは読み取り専用で、一意の URI によって識別されます。
- 詳細はこちら:MCP リソースのドキュメント
ツール
ツールは、LLM が呼び出せるアクションまたは関数です。ツールには副作用がある場合があり、計算の実行や操作の開始に使用するパラメーターを受け取ることができます。
- 詳細はこちら:MCP Tools documentation
プロンプト
プロンプトは、LLM の会話フロー、テンプレート、またはインタラクションパターンを定義します。プロンプトは、特定のシナリオにおけるモデルの動作を導くのに役立ちます。
- 詳細はこちら:MCP Prompts documentation
実装の詳細とコード例については、Capabilities セクションをご覧ください。
モジュール 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:トランスポート設定(任意)です(Transport Options を参照)。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)。forRootAsyncはルートモジュール(AppModule)で一度だけ使用してください。- 利用可能なすべてのオプションについては、
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 機能を登録する主な方法を 2 つ提供しています。
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 フィールドでは、ツールの動作に関するプロトコルレベルのヒントを指定できます。たとえば、破壊的な操作か、読み取り専用か、冪等か、その他の特別な特性があるかを指定できます。これらのヒントは、クライアント、UI、またはプロトコル自体が警告を表示したり、呼び出しを最適化したり、ポリシーを適用したりするために使用できます。
一般的なアノテーションキー:
destructiveHint(boolean):ツールが破壊的な操作(データの削除など)を行うことを示します。readOnlyHint(boolean):ツールがデータを変更しないことを示します。idempotentHint(boolean):同じ効果を得るためにツールを安全に複数回呼び出せることを示します。openWorldHint(boolean):ツールが現在のシステムの外部に副作用をもたらす可能性があることを示します。
例:
@Tool({
name: 'reset_password',
paramsSchema: z.object({ userId: z.string() }),
annotations: { destructiveHint: true, idempotentHint: false }
})
resetPassword({ userId }: { userId: string }): CallToolResult {
// ...
}
ToolOptions のバリアント
| バリアント | 必須フィールド |
|---|---|
| ToolBaseOptions | name |
| ToolWithDescriptionOptions | name, description |
| ToolWithParamOrAnnotationsOptions | name, paramsSchemaOrAnnotations |
| ToolWithParamOrAnnotationsAndDescriptionOptions | name, paramsSchemaOrAnnotations, description |
| ToolWithParamAndAnnotationsOptions | name, paramsSchema, annotations |
| ToolWithParamAndAnnotationsAndDescriptionOptions | name, 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 IDmcpReq.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' }],
};
}
}
重要な注意事項:
@Resource、@Prompt、@Toolでデコレートされたメソッドでは、ctxは常に最後の引数です。- 2.0 で
headersの意味が変わりました。 2.0 より前は、_接続_を開始したリクエスト、つまりinitializePOST のヘッダーであり、その接続の存続期間中は固定されていました。現在はこの呼び出し自体のヘッダーなので、会話の途中で有効期限が切れたり、取り消されたりした認証情報も正しく反映されます。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;
}
2 つの形式で、オプションのキーは 1 つです。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 の処理中に sessionId を割り当てますが、その時点では登録はすでに完了しています。これを公開すると、同じゲートの動作がトランスポートごとに異なってしまいます。
クライアントから見える内容
無効な機能も、スキップされるのではなく、登録された後に無効化されます。そのため、tools/list/prompts/list/resources/list には表示されず、呼び出すと Tool <name> not found ではなく Tool <name> disabled が返されます。SDK ではこの 2 つは別の状態であり、存在するものの利用者に対して無効になっている機能には、前者ではなく後者が正しい応答です。
5 つのケースでフェイルクローズ
判定できないゲートが機能を公開したままにすることはありません。
| ケース | 結果 |
|---|---|
isEnabled が同期的に例外をスローする | 無効になり、エラーがログに記録されます |
isEnabled が拒否されるプロミスを返す | 無効になり、エラーがログに記録されます |
| ゲートクラスをコンテナから解決できない | 無効になり、エラーがログに記録されます。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 レイヤーはベアラートークンを検証し、ツールごとのチェックはハンドラー内で行います。なぜなら、「実行中のツールに関する権威ある情報源はハンドラーだからです。」
enabled は呼び出し元に何を表示するかを決めるために使い、ガードは呼び出しを許可するかどうかを決めるために使ってください。どちらも同じリクエストを参照するため、述語を共通化できます。しかし、機能を隠すことは強制ではありません。ツール名をすでに知っているクライアントは、そのツールを呼び出せます。それを止めるのはガードだけです。
RegistryService を直接呼び出す場合
RegistryService.registerAll は async であり、2 番目の引数である
登録コンテキストは必須です:
await registry.registerAll(server, { request, era: 'modern' });
影響を受けるのはレジストリ自体を操作するコードだけです。通常どおり McpModule を使用する場合は影響ありません。変更点の全一覧については、「1.x から 2.x への移行」を参照してください。
実行可能な例は
examples/dynamic/にあります —EXAMPLE=dynamic pnpm start:example。
ガード
1 つ以上のガードを Resolver、個々のメソッド、またはグローバルに適用できます。ガードは NestJS の CanActivate インターフェースを実装する必要があります。
グローバルレベルのガード
この方法では、標準の NestJS グローバルガードシステム(APP_GUARD)を使用します。グローバルガードは MCP エンドポイント(/mcp)を含む、すべての NestJS ルートを保護します。幅広い認証や、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 で適用されたガードには、Nest の
ExecutionContext ではなく McpExecutionContext が渡されます。
CanActivate ではなく McpGuard(型付きの ID を使用する場合は
McpGuard<YourExtra>)を実装してください。CanActivate のパラメーター型は
ExecutionContext であるため、McpExecutionContext パラメーターで
implements CanActivate を指定すると、厳格モードではコンパイルできません。
@UseGuards は両方を受け付けます。グローバルな
APP_GUARD ガードは通常の Nest ガードであり、引き続き CanActivate を使用します。
Resolver レベルのガード
これは、このライブラリ独自の機能です。Resolver レベルのガードは、
Resolver クラスに @UseGuards() デコレーター(@nestjs-mcp/server から
エクスポート)を使用して適用します。その 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
}
メソッドレベルのガード
これは、このライブラリ独自の機能です。メソッドレベルのガードは、MCP 機能のメソッド
(@Prompt、@Resource、@Tool)に直接 @UseGuards() デコレーターを
適用します。デコレーターを適用したメソッドのみが、これらのガードによって保護されます。
特定の機能に対するきめ細かなアクセス制御に使用してください。
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 も、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 の応答を書き込み、認証結果の ID を認可処理、
ゲート、ガード、ハンドラーに渡します。また、OAuth を使用する場合に限り、ディスカバリーメタデータを提供します。
認可は別のレイヤーであり、リクエスト認可処理、機能ごとの scopes、ゲート、ガードはご自身で用意します。
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
1 つのリクエストにおける評価順序は次のとおりです。認証戦略 → 認可処理 → ゲート(登録時の
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 {}
すべての認証戦略と認可処理は、起動時にコンテナーから解決されます。解決できないクラスがあると
アプリケーションの起動が停止します。このライブラリは、DI を迂回する new へのフォールバックを行いません。
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 を返す | 続行します。後続のストラテジーはスキップされます |
すべてのストラテジーが null を返す | 401 + 登録済みのすべてのチャレンジを含む 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) | そのまま書き込みます(例:Retry-After を含む 429) |
| その他すべて | 500。ヘッダーやトークンを含めずにログに記録されます |
AuthInfo は MCP SDK の ID 型です(ハンドラーが ctx.http.authInfo として受け取るもの)。認証方式にかかわらず、clientId は認証された主体の ID、token は提示された認証情報です。どちらも必須です。また、expiresAt はエポックからの秒数です。文字列の token、文字列の clientId、文字列の配列である scopes のいずれかがない AuthInfo、またはミリ秒と明らかに判断できる(1e11 を超える)expiresAt を持つ AuthInfo は、バグとして 500 で応答します。expiresAt がすでに過ぎている場合は、401 invalid_token で応答します。アプリケーションの ID(ユーザー ID、キー ID、ワークスペース)は AuthInfo.extra に格納してください。CORS のプリフライト(OPTIONS)では認証を行いません。エラーメッセージに認証情報を含めないでください。予期しないエラーは、クラス名とメッセージのみをログに記録します。
チャレンジ。 ストラテジーでは、クライアントに認証情報の提示方法を伝える RFC 9110 のチャレンジである readonly challenge を宣言できます。例:ApiKey header="x-api-key" や Basic realm="notes"。すべての 401 では、宣言されたすべてのチャレンジが列挙されます。protectedResource が設定されている場合は、resource_metadata を含む Bearer チャレンジが先頭になります(MCP クライアントがこれを探すためです)。チャレンジが宣言されておらず、protectedResource も設定されていない場合、ヘッダーは単独の Bearer になります。有効な RFC 9110 チャレンジではない challenge があると、起動時にアプリケーションが停止します。また、OAuth のスコープトークンでない scopesSupported またはデコレーターの scopes エントリー(スペース、引用符、バックスラッシュ、制御文字を含むもの)がある場合も同様です。
任意のヘッダーからの 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 から導出したパス(https://notes.example.com/mcp → /.well-known/oauth-protected-resource/mcp)で RFC 9728 Protected Resource Metadata ドキュメントを提供し、ライブラリが生成するチャレンジの resource_metadata としてそのドキュメントを通知します。対象となるのは、認証情報がない場合または invalid_token の場合の 401 と、ステップアップ認証の 403 insufficient_scope です。独自のチャレンジを持つ McpAccessDeniedError、McpHttpError、または McpUnauthorizedError は、スローされた内容のまま書き込まれます。これにより、OAuth 対応の MCP クライアント(claude.ai、ChatGPT)は認可サーバーを検出できます。ライブラリは認可サーバーのメタデータを提供しません。これは AS の担当です。
OAuth をエンドツーエンドで試す
examples/oauth では、Docker もプロバイダーアカウントも使わずに、実際のプロバイダーのように動作するモック認可サーバーと並べて、OAuth 2.1 で保護された MCP サーバーを実行できます。実行方法、デモ用のユーザーとクライアント、失敗シナリオ、モックを Auth0、Okta、または Keycloak に切り替える方法については、例の README をご覧ください。
リクエストオーソライザー(レイヤー 2a)
McpAuthorizer は認証後にリクエストごとに 1 回実行され、それまでの 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 の値を置き換え(マージされません)、機能の public は resolver の値を上書きし、機能または resolver の hideOutOfScope は auth.hideOutOfScope を上書きします。@Resolver({ public: true }) を指定したクラス内で、機能が独自の scopes を宣言した場合、その機能は公開ではなく保護されます。スコープトークンは、メソッドデコレーターと同様に、クラス定義時に検証されます。
公開機能
@Tool、@Resource、@Prompt(または @Resolver)に public: true を指定すると、どのストラテジーにも認識されない呼び出し元にも機能を公開できます。それ以外のすべての機能では、引き続き auth が必須です。
@Tool({ name: 'search_docs', public: true })
searchDocs() {}
optional: true を指定せずに auth が設定され、すべてのストラテジーが null を返した場合、通常はリクエストに 401 が返されます。モジュールで公開機能が1つ以上宣言されている場合は、解析済みの JSON-RPC 本文を調べ、本文内のすべてのメッセージ(単一メッセージまたはバッチ)が次のいずれかの場合に限り、匿名でリクエストを処理します。
- ハンドシェイクまたは生存確認メッセージ —
initialize、server/discover、ping、すべてのnotifications/*。 - 一覧取得 —
tools/list、prompts/list、resources/list、resources/templates/list。この場合、公開機能のみが一覧表示されます。 - 公開ツール、プロンプト、リソース URI、または公開リソーステンプレートに一致する URI を指定した
tools/call、prompts/get、resources/read。
それ以外の場合(別のメソッド、非公開の対象、本文がない、または不正な本文、GET)は、以前と同じチャレンジを伴う 401 が返されます。匿名リクエストでは、すべての非公開機能が無効になります(一覧に表示されず、直接呼び出された場合も「無効」)。そのため、到達できるのは公開機能だけです。
- 認証情報があっても変わりません。 有効な認証情報を持つリクエストは、
publicの指定がない場合とまったく同じように機能を参照・呼び出しできます。公開機能は誰でも利用できます。 publicとscopesは併用できません。 同じ機能(または同じ@Resolver)に両方を宣言すると、クラス定義時にエラーが発生します。「誰でも」と「X の権限を持つ人のみ」は矛盾します。どちらかを黙って選ぶと、誤って公開したり、利用できなくしたりするおそれがあります。- 公開機能がなければ、変更はありません。 公開機能がない場合、認証処理は従来とまったく同じです。本文が調べられることはありません。
optional: trueの意味は変わりません。 すべての匿名リクエストは本文を調べずに許可され、スコープのない機能は引き続き表示され、スコープのある機能は無効になります。- ハンドシェイクも公開されます。 公開機能が1つでもある場合、
initializeとserver/discover(サーバーのinstructionsを含む)は匿名で読み取れるため、そこに機密情報を含めないでください。 - 両方のプロトコル世代に対応します。 SDK がリクエストを受け取る前に、解析済みの本文をチェックするため、2025年世代のクライアント(例:
@modelcontextprotocol/sdkv1)も匿名で動作します。initialize→tools/list→ 公開tools/callの順に実行できます。
型付きアイデンティティ(getAuthInfo)
getAuthInfo<TExtra>(ctx) は、ハンドラーの McpContext またはガードの McpExecutionContext から、有効な AuthInfo(ストラテジーが生成し、認可処理が整形した情報)を返します。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 | リクエストごとに1回 | 401 | 呼び出し元の識別 |
| authorizer | 認証後、リクエストごとに1回 | 403 / 絞り込まれたAuthInfo | テナント/ワークスペースへの所属、ロール ∩ キーのスコープ |
scopes | ディスパッチ前、機能ごと | 403 insufficient_scope / 非表示 | 機能に必要な OAuth スコープ |
gate(enabled) | 登録時、機能ごと | 非表示+「無効」 | 任意のロジックに基づく検出レベルの可視性 |
guard(@UseGuards) | ハンドラーの実行前、呼び出しごと | ツールエラー | 引数や操作対象のリソースに対する呼び出しごとのチェック |
認可する場所を選ぶ
各レイヤーは、それぞれ異なる問いに答えます。ライブラリはデータを認可しません。 ライブラリが判断するのは、呼び出し元がある操作にアクセスできるかどうかであり、その操作がどのレコードにアクセスできるかではありません。
| 場所 | 答える内容 | 形式 | クライアントに見える失敗 |
|---|---|---|---|
scopes | 各操作について、認証情報/アプリに何が許可されているか | 宣言的。コントローラーのメソッドに付ける権限デコレーターのようなもの | 403 insufficient_scope(追加認可が必要)、または非表示 |
ガード(@UseGuards) | ある操作に対するアプリケーション固有のルール(引数、時間、機能フラグ) | コード。呼び出しごと | ツールエラー |
| オーソライザー | このリクエストにおいて呼び出し元が誰か:テナント/ワークスペース、ロール ∩ 認証情報のスコープ | コード。リクエストごとに実行され、ほかの処理が参照する AuthInfo を構成します | 403 access_denied/スコープの縮小 |
| アプリケーションサービス | データレベルの認可 — 「このレコードはこのワークスペースに属しているか?」— 信頼できる情報源 | ハンドラーが getAuthInfo(ctx) を使って呼び出すドメインコード | ハンドラーが返す内容 |
public | どの操作で認証が不要か | 宣言的。機能またはリゾルバーごと | それ以外はすべて 401 |
| リゾルバーのデフォルト | 操作グループに共通する scopes/public/hideOutOfScope の値 | 宣言的。@Resolver({ … }) に指定 | scopes/public と同様 |
| 型付き ID | ハンドラーとガードが呼び出し元を読み取る方法 | getAuthInfo<TExtra>(ctx) | — |
スコープの失敗が 403 で、ガードの失敗がツールエラーになる理由。 scopes
はディスパッチ前に判明するため、SDK は HTTP リクエスト自体を 403 とともに拒否できます。
その際、OAuth クライアントが対応方法を認識できる WWW-Authenticate チャレンジを返します
(より多くのスコープで再同意するなど)。ガードはディスパッチされた呼び出しの内部で実行されます。その時点では
MCP リクエストが受け付けられ、HTTP レスポンスは JSON-RPC の結果に確定しているため、
残されている正直な応答は、そのツール呼び出しのエラー結果だけです。再認可でクライアントが解決できる内容は
scopes(またはオーソライザー)に設定し、再同意では変更できないルールにはガードを使ってください。
知っておくべきこと
- グローバルな
APP_GUARDも/mcpで実行されます。 グローバルに登録された REST ガードは MCP のトラフィックも処理するため、REST ガードはコントローラーごとに適用してください。これらは MCP コントローラーがリクエストを認証する前に実行されるため、その時点ではreq.authはまだundefinedです。MCP の呼び出し元は、auth.authorizers、リゾルバーへの@UseGuards、または capability gates を使って認可してください。これらはすべて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()にフォールバックしていましたが、現在はゲートと同様にアクセスを拒否し、エラーログを記録します。
API キー戦略、JWT 戦略、テナント認可機能、スコープ付きツールを備えた、実行可能なサーバーについては、examples/auth を参照してください。
ステートレス性
2.0 以降、このライブラリはリクエスト間で状態を保持しません。セッションストアもセッション ID もなく、スティッキールーティングも不要です。
SDK の createMcpHandler は、このライブラリが提供するファクトリーを使い、各 HTTP リクエストに対して新しい McpServer を作成します。このファクトリーは、処理中のリクエストに対して RegistryService.registerAll を実行するため、すべての capability ハンドラーとガードはそのリクエストをクロージャーとして保持します。レスポンス後に残るものはありません。
これにより可能になること
- 通常のラウンドロビンロードバランサーの背後で、任意の数のインスタンスを実行できます。スティッキーセッションも共有セッションストアも、ゲートウェイでのセッションアフィニティも不要です。
- 会話の途中でインスタンスを再起動したり、自動スケーリングしたりしても、クライアントとの接続は途切れません。
- 接続を開いたままにできないサーバーレス環境やエッジランタイムにデプロイできます。
これは
#121 の解決策です。test/stateless-load-balancing.e2e-spec.ts では、厳密なラウンドロビンプロキシの背後にある、個別に構築された 2 つの Nest アプリケーションを 1 つの 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;
}
トランスポートオプション
このライブラリは、1 つの MCP エンドポイント ALL /mcp を公開します。
2.0 で変更されました。 1.x では
POST/GET/DELETE /mcpに加え、GET /sseとPOST /messagesを提供し、トランスポートごとにenabledを切り替えられました。HTTP+SSE トランスポートは削除されました。構造上、スティッキーセッションが必要であり、長時間接続するGET /sseは同じインスタンス上のPOST /messagesと組み合わせる必要があります。これは、ステートレス化によって解決されるまさにその問題です。/mcpに対するGETとDELETEは、2025 年当時のセッション操作でしたが、現在は405を返します。
後方互換性。 2025 年当時のクライアントも、SDK のステートレスなレガシーフォールバックを通じて、同じエンドポイントで引き続き利用できます。現在公開されているすべての MCP クライアント SDK(@modelcontextprotocol/sdk@1 と
@modelcontextprotocol/client@2 を含む)は、いずれもその時代のプロトコルを使用しており、両方とも E2E テストスイートで検証されています。
オプションは 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' です。 |
keepAliveMs | SSE コメントフレームのキープアライブ間隔です。デフォルトは 15000 です。 |
maxSubscriptions | 開いている subscriptions/listen ストリームの上限です。デフォルトは 1024 です。 |
bus | subscriptions/listen の変更イベントバスです。Redis に置き換えると、インスタンス間で通知を配信できます。 |
進捗と responseMode
ctx.reportProgress がクライアントに届くかどうかは、
リクエストのプロトコル世代と responseMode によって異なります。
| リクエストの世代 | 'auto'(デフォルト) | 'sse' | 'json' |
|---|---|---|---|
| モダン(2026-07-28) | 配信されます | 配信されます | 破棄されます — 警告が1回表示されます。下記を参照してください |
| レガシー(2025、SDK フォールバック) | SSE 経由で配信されます | SSE 経由で配信されます | SSE 経由で配信されます |
- モダン +
'json'。 SDK はこのモードでは呼び出し中の通知を破棄します。ハンドラーが初めてreportProgressを呼び出すと、@mcp:progressの下にwarnのログが1行記録されます。また、SDK は'json'エンドポイントごとに起動時に独自のconsole.warnを1回出力します。呼び出し自体は引き続き成功します。 - レガシー世代。 SDK のレガシーフォールバックは常に SSE で呼び出しに応答し、
responseModeを適用しないため、どのモードでも進捗が配信されます。 'auto'での SSE への切り替え。 進捗がなければ、モダンな呼び出しは単一のapplication/json本文で応答します。最初の進捗フレームの後、応答は200 text/event-streamとなり、進捗フレームの後に結果が続きます。それ以降にエラーが発生すると、HTTP ステータスではなく JSON-RPC エラーとしてストリーム内に送られます。ステータスコードを基準に判定するクライアントからは200に見えます。
サーバー全体のプロトコルオプションは server の下に配置し、そのまま SDK に渡します。
server.* | 用途 |
|---|---|
requestState.verify | MRTR の再試行で返される状態を検証します — 別のインスタンスでの再開を参照してください |
cacheHints | サーバーのキャッシュ可能なメソッド(tools/list、server/discover など)に対する ttlMs / cacheScope。リソースごとのヒントは @Resource に指定します。 |
inputRequired | MRTR の 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 でテストされています。
クライアントは、すべてのリクエストの capabilities に
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 はサーバー全体に対して1つの結果を返すため、機能ごとのヒントを設定する場所がありません。これらには 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.x | 2.x | 理由 |
|---|---|---|
RequestHandlerExtra | McpContext | SDK からこの型が削除されました。ctx.request は実際の Express リクエストになりました。 |
extra.sessionId | (削除) | 2026-07-28 でセッションが廃止されました。ctx.sessionId は存在しますが、最新のトラフィックでは undefined です。 |
extra.headers | ctx.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, … } | エンドポイントは1つです。オプションは createMcpHandler に渡されます。 |
session: { sessionTimeoutMs, … } | (削除) | タイムアウトさせるセッションはありません。 |
GET /sse + POST /messages | (削除) | HTTP+SSE は構造上、スティッキーセッションが必要です。 |
GET / DELETE /mcp | 405 | これらはセッション操作でした。 |
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 を使うと、ブラウザー UI で 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 をお読みください。
コメント
AI アプリケーションが外部のツールやデータソースに統一的な方法で接続するためのオープンなプロトコルです。クライアントはモデルとの対話を担い、MCP サーバーはツール・リソース・プロンプトテンプレートを提供します。標準メッセージでやり取りするため、1 つのサーバーを複数のクライアントで再利用できます。