NestJS MCPサーバー
npm i @nestjs/mcp @modelcontextprotocol/server zod
npm i -D @modelcontextprotocol/client # インメモリテストクライアント
Zod 4.2+ と ArkType はそのまま使用可能。Valibot は toStandardJsonSchema() を介して、既存の JSON Schema は fromJsonSchema() を介して使用します。Zod 3 のスキーマは起動時に拒否されます。
McpModule.forRoot({
name: 'cat-store', version: '1.0.0',
instructions: '注文を変更する前に、find-orderで注文を検索してください。', // モデルにサーバーの使い方を指示
}),
app.listen(3000) を実行すると、エンドポイントが /mcp で提供されます。npx @modelcontextprotocol/inspector で検査できます。起動時にモジュールはプロバイダ(すべてのモジュール、または include で指定されたもの)をスキャンし、デコレートされたメソッドを検出します。重複した名前、無効なスキーマ、または auth なしで宣言された scopes がある場合は、起動時にエラーで停止します。
プリミティブ:プロバイダ上のメソッド
| プリミティブ | デコレータ | 使用者 | 呼び出し時の引数 |
|---|---|---|---|
| ツール(アクション) | @Tool({ name, description, inputSchema, outputSchema?, annotations?, scopes? }) | モデル | 検証済みの入力、ctx |
| リソース(データ) | @Resource({ uri, mimeType }) / @ResourceTemplate({ uriTemplate: 'orders://{orderId}' }) | クライアント(多くの場合ユーザーが添付) | {} またはテンプレート変数、ctx(ctx.uri) |
| プロンプト(テンプレート) | @Prompt({ name, description, argsSchema }) | ユーザーが選択 | 検証済みの引数、ctx |
@Injectable() // モジュールの providers にリストされた通常のプロバイダ
@Injectable()
export class OrdersTools {
constructor(private readonly orders: OrdersService) {}
@Tool({
name: 'find-order',
description: '注文IDで注文を検索するか、メールアドレスで顧客の最新注文を検索します',
inputSchema: z.object({
orderId: z.string().optional().describe('注文ID(例:ord_123)'),
email: z.email().optional().describe('顧客のメールアドレス'),
}),
annotations: { readOnlyHint: true }, // クライアントはユーザーに確認せず実行可能
})
async find({ orderId, email }: { orderId?: string; email?: string }) {
const found = orderId ? [await this.orders.findById(orderId)] : await this.orders.latestFor(email!);
if (!found[0]) throw new NotFoundException('注文が見つかりません'); // → isError: true のツール結果
return found;
}
@Tool({
name: 'change-address',
description: 'まだ発送されていない注文の配送先住所を変更します',
inputSchema: z.object({ orderId: z.string(), address: z.string().min(10) }),
outputSchema: z.object({ orderId: z.string(), address: z.string(), status: z.string() }),
scopes: ['orders:write'],
})
async changeAddress({ orderId, address }: { orderId: string; address: string }, ctx: McpContext) {
const o = await this.orders.changeAddress(orderId, address, ctx.authInfo?.clientId);
return { orderId: o.id, address: o.address, status: o.status }; // → structuredContent + JSONテキスト
}
}
戻り値:ツールの文字列はテキストブロックになります。オブジェクトは、outputSchema がある場合は structuredContent に、それ以外の場合はJSONテキストになります。画像、オーディオ、埋め込みリソースの場合は自分で CallToolResult を返します。undefined は空の結果になります。リソースの文字列はテキストとして送信され、Buffer/Uint8Array はblobとして、オブジェクトはJSONとして送信されます。プロンプトの文字列は1つのユーザーメッセージになり、配列は PromptMessage[] を返します。ツール名は1〜128文字で、A-Z a-z 0-9 _ - . から構成され、デフォルトではメソッド名になります。
名前、説明、および .describe() テキストはモデルのために書きましょう。 モデルが知っているのはこれだけです。ツールの機能、使用すべきタイミング、何かを変更するかどうか、各引数のサンプル値を記述してください。そのままロードされるコンテンツにはリソースを使用し、引数を取る検索にはツールを使用してください。
呼び出しコンテキスト
ctx: McpContext には以下のプロパティがあります:
kind、name、uriauthInfo(clientId、scopes、expiresAt、extra)request(プラットフォームリクエスト。stdioではundefined)requestInfotransport('http' | 'stdio')era('modern'2026-07-28 /'legacy'2025)signal:fetchやDB呼び出しに渡します。ループ内でthrowIfAborted()を呼び出します。reportProgress(progress, total?, message?)server(SDKコンテキスト)
ユーザーに確認や不足値を尋ねるには、inputRequired({ inputRequests: { confirm: inputRequired.elicit({ message, requestedSchema }) } }) を返します。リトライ時には、acceptedContent(ctx.server.mcpReq.inputResponses, 'confirm', Schema) で回答を読み取ります。HTTPではエンドポイントがステートレスなため、これは2026-07-28クライアントでのみ機能します。stdioでは両方のエラで機能します。
プロバイダはシングルトンのままです。呼び出しごとのデータはリクエストスコープのプロバイダではなく、ctx から読み取ってください。
エンハンサーとエラー
- ガード、インターセプター、パイプ、フィルターはルートと同様に動作し、
context.getType() === 'mcp'で判定します。McpExecutionContext.create(context)はgetInput()、getContext()、getKind()、getName()、getRequest()、getAuthInfo()を提供します。 - グローバルエンハンサーはMCP呼び出し と それを運ぶHTTPリクエストの両方で実行されます。 HTTP専用のエンハンサーは、
switchToHttp()を呼び出す前にcontext.getType() === 'http'をチェックする必要があります。 - グローバルな
ValidationPipe({ transform: true })はDTO型の入力を検証します。カスタムパラメータデコレータは(input, ctx, @CurrentUser() user)の後に配置します。 - ツールがスローするエラーは
isError: trueの結果となり、モデルが回復可能です。HttpExceptionはそのメッセージを保持します。その他のエラーはInternal server errorになり、ログに記録されます。 - リソースとプロンプトはJSON-RPCエラーで応答します:
NotFoundException→ リソース未検出、BadRequestException→ 無効なパラメータ。 - フィルター:
host.getType<McpContextType>() === 'mcp'をチェックします。フィルターが返すものは何でも結果になります。JSON-RPCエラーを送信するには、ProtocolErrorをスローします。
認可(ネットワーク接続可能なサーバー)
McpModule.forRootAsync({
imports: [ConfigModule, AuthModule],
inject: [ConfigService, IdentityProviderTokenVerifier],
useFactory: (config: ConfigService, verifier: IdentityProviderTokenVerifier) => ({
name: 'cat-store', version: config.get('VERSION'),
auth: {
verifier, // McpTokenVerifier.verifyAccessToken(token) → AuthInfo | null を実装
resource: config.get('MCP_RESOURCE'), // 正規の公開URL = トークンのaudience
authorizationServers: [config.get('IDENTITY_PROVIDER_URL')],
scopesSupported: ['orders:read', 'orders:write'],
},
}),
}),
- サーバーはOAuth 2.1 リソースサーバーです。トークンを検証し、トークンを発行することはありません。
- RFC 9728メタデータを
/.well-known/oauth-protected-resource/mcpで提供します。トークンがないリクエストにはWWW-Authenticate: Bearerチャレンジを含む401が返され、クライアントのサインインが開始されます。 - 検証ツール:
- JWT:
@nestjs/authenticationのJwtVerifierを使用します(署名、発行者、audience、有効期限をチェック)。 - 不透明トークン:RFC 7662イントロスペクションエンドポイントを呼び出します。
- 返される
AuthInfoはresource === auth.resourceを満たし、将来のexpiresAtを持つ必要があります。そうでない場合、トークンは401で拒否されます。 - トークンを拒否するには401エラーをスローします。その他のエラーは500になります。
- JWT:
- プリミティブに
scopesがない場合は403とステップアップチャレンジが返されます。auth.requiredScopesはすべてのリクエストに適用されます。 @nestjs/authentication≥ 0.1.3 および@nestjs/authorization≥ 0.1.1 と一緒に使用する場合:- それらのグローバルガードが各呼び出しを認証し、
@CurrentUser()と@Can(Policy, 'action')がツール内で機能します。 authがチャレンジを処理できるように、エンドポイントにdecorators: [Public()]を設定します。@Can()がないツールは拒否されます。
- それらのグローバルガードが各呼び出しを認証し、
- 内部専用サーバーはOAuthをスキップし、エンドポイントに
decorators: [UseGuards(ApiKeyGuard)]を使用できます。
トランスポート
- Streamable HTTP がデフォルトで提供されます。
path: falseで無効化できます。エンドポイントは通常のコントローラーであるため、グローバルプレフィックス、バージョニング(endpointVersion)、ミドルウェア、CORSがすべて適用されます。ステートレスであるため、スティッキーセッションは不要です。両方のプロトコルエラに対応します(http.legacy: 'reject'は2026-07-28のみ提供)。http.responseModeは'auto'、'json'、'sse'から選択できます。 - stdio:アシスタントがローカルプロセスを起動する場合に使用します。
NestFactory.createApplicationContext(AppModule, { logger: new McpStdioLogger() })でstdio.tsを作成し、await app.get(McpStdioServer).listen(); await app.close();を実行します。stdoutに他のものを書き出すとプロトコルが壊れます。認証情報はprocess.envから取得されます。スコープはチェックされませんが、ガードは実行されます。
完全なオプションテーブル、本番設定、stdioクライアント設定、テストパターン、移行マップについては、references/mcp-reference.md を参照してください。
ワークフロー
- 1. インストール。Zod 4.2+ を確認。
McpModule.forRootにname、version、instructionsを設定。 - 2. プロバイダにツール、リソース、プロンプトを追加。モデル向けの説明、すべての引数に
.describe()、読み取り専用ツールにはreadOnlyHint、構造化された結果にはoutputSchemaを設定。 - 3. HTTP専用のグローバルエンハンサーが
getType()をチェックすることを確認。必要に応じてMCPガードとフィルターを追加。 - 4. ネットワーク接続可能なサーバー:
auth(verifier、resource、authorization servers、scopes)、allowedHosts/allowedOrigins、CORSヘッダー、ボディサイズ制限を追加。ローカルサーバー:McpStdioLoggerを使用したstdio.tsを追加。 - 5. テスト:メソッドを直接ユニットテスト。
connectMcpClient(moduleRef, { token })を使用してテスト用verifierに対してe2eテストを実行。ソケット経由でHTTPルートをテスト。ビルド、リント、テスト実行、アプリ起動、インスペクターでツール一覧表示。
参考ファイル
references/mcp-reference.md:モジュールオプション、デコレータオプション、HTTP強化(Origin、Host、CORSヘッダー、ボディサイズ)、stdioクライアント設定、NestJS Observe命名、本番チェックリスト、テスト(トークンとスコープ、プロトコルエラ、McpRegistryチェック、HTTPおよびstdioテスト)、@rekog/mcp-nest1.x/2.xからの移行。本番設定、テスト作成、移行時に参照してください。