MCPサーバーレスポンスの検証:公式スキーマを活用した堅牢な実装

Model Context Protocol (MCP)の公式JSON Schemaを活用したサーバーレスポンスの検証実装について解説します。TypeScriptとAjvを使用した具体的な実装方法と、パフォーマンスを考慮した最適化手法を紹介します。

MCPサーバーの開発において、レスポンスの検証は重要な要素です。MCPは公式にJSON Schemaを提供しており、これを活用することで堅牢な実装が可能です。本記事では、公式スキーマを使用したバリデーション実装の詳細と、実践的な最適化手法について解説します。

1. MCPとJSON Schemaの関係

Model Context Protocol (MCP)は、LLMアプリケーションと外部データソース・ツールを統合するための標準プロトコルです。このプロトコルの仕様は、TypeScriptの型定義を基に作られており、それらの型定義からJSON Schemaが生成されています。この公式スキーマは、プロトコルの正確な実装と検証を可能にする重要なリソースです。

mcp-types.ts
1// MCPの基本的なメッセージ型
2type JSONRPCMessage =
3  | JSONRPCRequest     // リクエスト
4  | JSONRPCNotification // 通知
5  | JSONRPCResponse     // 成功レスポンス
6  | JSONRPCError;       // エラーレスポンス
7
8// JSON-RPC 2.0のレスポンス型
9interface JSONRPCResponse {
10  jsonrpc: '2.0';
11  id: string | number;
12  result: {
13    _meta?: { [key: string]: unknown };
14    [key: string]: unknown;
15  };
16}

MCPはJSON-RPC 2.0に基づいており、すべてのメッセージはこの形式に従います。サーバーのレスポンスを検証する際は、基本的なJSON-RPCの構造に加えて、MCPで定義された各メソッド固有のレスポンス形式も考慮する必要があります。

公式のJSON Schemaは、TypeScript定義から生成されているため、型定義とスキーマの整合性が常に保たれています。これにより、開発者は型システムとランタイムバリデーションの両方で一貫した検証を実装できます。

2. 公式スキーマの構造と特徴

MCPの公式JSON Schemaは、プロトコルのすべての要素を網羅的に定義しています。スキーマは大きく以下のような構造に分かれています。

schema.json
1{
2  "$schema": "http://json-schema.org/draft-07/schema#",
3  "definitions": {
4    // 基本メッセージ型
5    "JSONRPCMessage": { ... },
6    "JSONRPCRequest": { ... },
7    "JSONRPCResponse": { ... },
8    "JSONRPCError": { ... },
9    
10    // MCP固有の型
11    "ServerCapabilities": { ... },
12    "ClientCapabilities": { ... },
13    "Resource": { ... },
14    "Tool": { ... },
15    
16    // メソッド固有の型
17    "InitializeResult": { ... },
18    "ListResourcesResult": { ... },
19    "GetPromptResult": { ... }
20  }
21}

スキーマの主要な特徴として以下が挙げられます:

JSON Schema Draft 7を使用しており、高度な検証機能を活用できます。例えば、文字列フォーマットの検証(URI、バイト列など)や、数値の範囲チェック、必須フィールドの検証などが可能です。特に重要なのは、MCPの各メソッドに対応するレスポンススキーマが厳密に定義されている点です。

initialize-result-schema.json
1{
2  "InitializeResult": {
3    "description": "After receiving an initialize request from the client, the server sends this response.",
4    "properties": {
5      "protocolVersion": {
6        "description": "The version of the Model Context Protocol that the server wants to use.",
7        "type": "string"
8      },
9      "capabilities": {
10        "$ref": "#/definitions/ServerCapabilities"
11      },
12      "serverInfo": {
13        "$ref": "#/definitions/Implementation"
14      },
15      "instructions": {
16        "description": "Instructions describing how to use the server and its features.",
17        "type": "string"
18      }
19    },
20    "required": [
21      "capabilities",
22      "protocolVersion",
23      "serverInfo"
24    ]
25  }
26}

スキーマはメソッドごとに必要なフィールドを定義しており、オプショナルなフィールドも明確に区別されています。これにより、レスポンスの構造が仕様に準拠しているかを正確に検証できます。

3. TypeScriptによる実装

TypeScriptとAjvを使用して、MCPレスポンスのバリデーション機能を実装していきます。実装方法には主に以下の2つのアプローチがあります:

  1. 公式TypeScript SDKの活用: MCPは公式のTypeScript SDKを提供しており、これを使用することで最も直接的な実装が可能です。

  2. カスタム実装: 特定の要件や制約がある場合は、公式のJSON Schemaを使用して独自のバリデーション実装を行うことができます。

本記事では、独自実装のアプローチを詳しく解説します。これにより、バリデーションの仕組みをより深く理解し、必要に応じてカスタマイズできるようになります。公式SDKの使用方法については、公式ドキュメントを参照してください。

カスタム実装は以下の4つの部分に分かれます:

  1. バリデータクラスの基本構造
  2. メソッドごとのスキーマ定義
  3. バリデーション機能の実装
  4. エラーハンドリング

まず、必要なパッケージをインストールします:

terminal
1npm install ajv ajv-formats

バリデータクラスの基本構造

バリデーションを行うクラスの基本構造を実装します。このクラスは公式のJSON Schemaを読み込み、メソッドごとのバリデータを初期化します:

mcp-validator.ts
1import { Ajv, ErrorObject } from 'ajv';
2import addFormats from 'ajv-formats';
3import schema from './schema.json';
4
5export type MCPMethod = 'initialize' | 'resources/list' | 'prompts/get' | 'tools/call';
6
7export class MCPValidator {
8  private ajv: Ajv;
9  private validators: Map<MCPMethod, ReturnType<Ajv['compile']>>;
10
11  constructor() {
12    // Ajvインスタンスの初期化
13    this.ajv = new Ajv({
14      allErrors: true,    // すべてのエラーを収集
15      verbose: true,      // 詳細なエラー情報
16      strict: true        // 厳密モード
17    });
18    addFormats(this.ajv); // URI等のフォーマット検証を追加
19
20    // バリデータの初期化
21    this.validators = new Map();
22    this.initializeValidators();
23  }
24
25  private initializeValidators() {
26    // 各メソッドに対応するバリデータを初期化
27    const methodSchemas = {
28      'initialize': schema.definitions.InitializeResult,
29      'resources/list': schema.definitions.ListResourcesResult,
30      'prompts/get': schema.definitions.GetPromptResult,
31      'tools/call': schema.definitions.CallToolResult
32    };
33
34    for (const [method, methodSchema] of Object.entries(methodSchemas)) {
35      this.validators.set(method as MCPMethod, this.ajv.compile(methodSchema));
36    }
37  }
38}

バリデーション機能の実装

次に、実際のバリデーション機能を実装します。レスポンスの検証とエラーハンドリングを行うメソッドを追加します:

mcp-validator.ts
1export class MCPValidator {
2  // ... 前述のコード ...
3
4  /**
5   * MCPレスポンスをバリデートします
6   * @param response バリデーション対象のレスポンス
7   * @param method MCPメソッド名
8   * @returns バリデーション結果とエラー情報のタプル
9   */
10  validate(response: unknown, method: MCPMethod): ValidationResult {
11    // JSON-RPC 2.0の基本構造を検証
12    if (!this.isValidJSONRPCResponse(response)) {
13      return {
14        isValid: false,
15        errors: [{ message: 'Invalid JSON-RPC 2.0 response structure' }]
16      };
17    }
18
19    // メソッド固有のバリデータを取得
20    const validator = this.validators.get(method);
21    if (!validator) {
22      throw new Error(`Unknown method: ${method}`);
23    }
24
25    // レスポンスの検証
26    const isValid = validator(response.result);
27    return {
28      isValid,
29      errors: validator.errors ?? []
30    };
31  }
32
33  private isValidJSONRPCResponse(response: unknown): response is JSONRPCResponse {
34    return (
35      typeof response === 'object' &&
36      response !== null &&
37      'jsonrpc' in response &&
38      response.jsonrpc === '2.0' &&
39      'id' in response &&
40      (typeof response.id === 'string' || typeof response.id === 'number') &&
41      'result' in response &&
42      typeof response.result === 'object'
43    );
44  }
45}

エラーハンドリングとフォーマット

バリデーションエラーを人間が読みやすい形式に整形する機能を追加します:

mcp-validator.ts
1export class MCPValidator {
2  // ... 前述のコード ...
3
4  /**
5   * バリデーションエラーを人間が読みやすい形式に変換します
6   */
7  formatErrors(errors: ErrorObject[]): string[] {
8    return errors.map(error => {
9      const path = error.instancePath || 'response';
10      const message = error.message || 'Unknown error';
11
12      // 必須フィールドのエラー
13      if (error.keyword === 'required') {
14        const missing = error.params.missingProperty;
15        return `${path}: Missing required property '${missing}'`;
16      }
17
18      // 型のエラー
19      if (error.keyword === 'type') {
20        const expected = error.params.type;
21        return `${path}: Expected type '${expected}'`;
22      }
23
24      // その他のエラー
25      return `${path}: ${message}`;
26    });
27  }
28}

使用例

実際にバリデータを使用する例を見てみましょう:

usage-example.ts
1const validator = new MCPValidator();
2
3// リソース一覧のレスポンスを検証する例
4const response = {
5  jsonrpc: '2.0',
6  id: 1,
7  result: {
8    resources: [
9      {
10        uri: 'file:///path/to/resource',
11        name: 'Example Resource',
12        description: 'An example resource',
13        mimeType: 'text/plain'
14      }
15    ]
16  }
17};
18
19const result = validator.validate(response, 'resources/list');
20
21if (!result.isValid) {
22  console.error('Validation errors:');
23  validator.formatErrors(result.errors).forEach(error => {
24    console.error(`- ${error}`);
25  });
26} else {
27  console.log('Response is valid');
28}

このような実装により、以下のような利点が得られます:

  1. 型安全性: TypeScriptの型システムとAjvのランタイムバリデーションの組み合わせにより、開発時とランタイム時の両方で型安全性を確保できます。

  2. カスタマイズ性: エラーメッセージの形式やバリデーション条件をプロジェクトの要件に合わせて調整できます。

  3. デバッグ容易性: 詳細なエラー情報とカスタマイズ可能なエラーメッセージにより、問題の特定と修正が容易になります。

この実装はTypeScriptで記述していますが、同様のアプローチは他の言語でも適用可能です。MCPはPython SDKなども提供しています。

4. パフォーマンス最適化

MCPの公式スキーマは非常に大きく(約87KB)、多くのメソッドと型定義を含んでいます。実運用環境でパフォーマンスを最適化するためには、いくつかの重要な考慮点があります。

スキーマの分割とキャッシュ

最初の最適化アプローチは、必要なメソッドのスキーマだけを抽出して使用することです。buildスクリプトを使用して、メソッドごとにスキーマを分割します:

build-schemas.ts
1import schema from './schema.json';
2import { writeFileSync, mkdirSync } from 'fs';
3import { join } from 'path';
4
5// メソッドごとのスキーマを抽出
6const methodSchemas = {
7  'initialize': schema.definitions.InitializeResult,
8  'resources/list': schema.definitions.ListResourcesResult,
9  'prompts/get': schema.definitions.GetPromptResult,
10  'tools/call': schema.definitions.CallToolResult
11};
12
13// スキーマを個別のファイルとして保存
14const outputDir = join(__dirname, 'schemas');
15mkdirSync(outputDir, { recursive: true });
16
17for (const [method, methodSchema] of Object.entries(methodSchemas)) {
18  const fileName = method.replace('/', '-') + '.schema.json';
19  const filePath = join(outputDir, fileName);
20  
21  writeFileSync(filePath, JSON.stringify(methodSchema, null, 2));
22  console.log(`Generated schema for ${method} at ${filePath}`);
23}

動的インポートによる遅延ローディング

分割したスキーマを効率的に利用するために、動的インポートを活用します。これにより、必要なスキーマだけを必要なタイミングでロードできます:

optimized-validator.ts
1export class OptimizedMCPValidator {
2  private ajv: Ajv;
3  private cachedValidators: Map<MCPMethod, ReturnType<Ajv['compile']>>;
4  private loadingValidators: Map<MCPMethod, Promise<void>>;
5
6  constructor() {
7    this.ajv = new Ajv({ allErrors: true, strict: true });
8    addFormats(this.ajv);
9    
10    this.cachedValidators = new Map();
11    this.loadingValidators = new Map();
12  }
13
14  /**
15   * メソッド用のバリデータを非同期でロード
16   */
17  private async loadValidator(method: MCPMethod): Promise<void> {
18    // すでにロード済みの場合はスキップ
19    if (this.cachedValidators.has(method)) {
20      return;
21    }
22
23    // ロード中の場合は待機
24    const loading = this.loadingValidators.get(method);
25    if (loading) {
26      return loading;
27    }
28
29    // 新しくロードを開始
30    const loadPromise = (async () => {
31      const schemaFileName = method.replace('/', '-') + '.schema.json';
32      const schema = await import(`./schemas/${schemaFileName}`);
33      this.cachedValidators.set(method, this.ajv.compile(schema));
34    })();
35
36    this.loadingValidators.set(method, loadPromise);
37    await loadPromise;
38    this.loadingValidators.delete(method);
39  }
40
41  /**
42   * 非同期バリデーション
43   */
44  async validate(response: unknown, method: MCPMethod): Promise<ValidationResult> {
45    await this.loadValidator(method);
46    const validator = this.cachedValidators.get(method);
47    
48    if (!validator) {
49      throw new Error(`Failed to load validator for method: ${method}`);
50    }
51
52    const isValid = validator(response.result);
53    return {
54      isValid,
55      errors: validator.errors ?? []
56    };
57  }
58}

バリデーション処理の最適化

バリデーション自体のパフォーマンスを向上させるため、以下の最適化を適用します:

optimized-validator.ts
1export class OptimizedMCPValidator {
2  constructor() {
3    this.ajv = new Ajv({
4      allErrors: false,      // 最初のエラーで停止
5      strict: true,          // 厳密モード
6      validateFormats: false // パフォーマンスのためフォーマット検証を無効化
7    });
8
9    // 必須の形式のみ追加
10    addFormats(this.ajv, [
11      'uri',        // リソースURIに必要
12      'uri-template' // テンプレートに必要
13    ]);
14  }
15
16  /**
17   * パフォーマンス重視のバリデーション
18   */
19  async validateFast(response: unknown, method: MCPMethod): Promise<boolean> {
20    // 基本構造の高速チェック
21    if (
22      typeof response !== 'object' ||
23      response === null ||
24      response.jsonrpc !== '2.0' ||
25      !('result' in response)
26    ) {
27      return false;
28    }
29
30    // キャッシュされたバリデータを使用
31    const validator = this.cachedValidators.get(method);
32    if (!validator) {
33      await this.loadValidator(method);
34      return this.validateFast(response, method);
35    }
36
37    return validator(response.result);
38  }
39}

最適化の選択

実装する最適化は、以下のような要件とトレードオフを考慮して選択します:

  1. 開発環境 vs 本番環境:開発時は詳細なエラー情報が必要ですが、本番環境ではパフォーマンスが優先されます。

  2. メモリ使用量 vs 応答速度:全スキーマを事前にロードするか、必要に応じて動的にロードするかを検討します。

  3. バリデーション精度 vs パフォーマンス:完全な検証と高速な検証のバランスを取ります。

パフォーマンス最適化を適用する際は、validateFormatsやallErrorsの無効化によって失われる機能と、得られるパフォーマンスの向上を慎重に比較検討する必要があります。特に開発環境では完全な検証を維持し、本番環境でのみ最適化を適用することを推奨します。

まとめ:公式スキーマ活用のメリット

公式のJSON Schemaを活用したMCPレスポンスの検証実装について、主要なポイントを振り返ってみましょう。公式スキーマを使用することで、以下のような利点が得られます:

  1. 仕様との整合性の保証: 公式スキーマは仕様から直接生成されているため、実装の正確性が保証されます。TypeScriptの型定義とも一貫性が保たれており、開発時の型安全性も確保できます。

  2. メンテナンスの容易さ: プロトコルのアップデートに伴うスキーマの変更は、公式リポジトリで管理されています。アップデート時は新しいスキーマを取り込むだけで、最新の仕様に追従できます。

  3. 最適化の柔軟性: スキーマの分割や動的読み込み、検証オプションの調整など、様々な最適化手法を適用できます。開発環境と本番環境で異なる設定を使い分けることも可能です。

実装時に注意すべき点としては、以下が重要です:

  • スキーマの更新管理を適切に行うこと
  • 検証エラーの適切なハンドリングとログ記録
  • 開発環境と本番環境での設定の使い分け
  • パフォーマンス最適化と機能のバランス

MCPは活発に開発が進められているプロトコルです。今後、新しいメソッドや機能が追加される可能性があります。スキーマのバージョン管理と更新の仕組みを整備しておくことで、円滑な機能拡張が可能になります。

今後の展望

MCPの進化に伴い、バリデーション実装にも以下のような発展が期待されます:

  1. スキーマ管理の自動化: CI/CDパイプラインと連携した自動更新の仕組みの構築

  2. 高度な型安全性: TypeScriptの型定義とJSON Schemaの自動同期による、より強力な型チェック

  3. パフォーマンス最適化: WebAssemblyを活用した高速なバリデーションの実現

  4. 開発者体験の向上: バリデーションエラーの視覚化や、より詳細なデバッグ情報の提供

このように、公式スキーマを活用したバリデーション実装は、MCPを利用したアプリケーション開発の重要な基盤となります。スキーマ駆動開発のアプローチは、プロトコルの進化に柔軟に対応しつつ、堅牢なシステムを構築する手法として、今後さらに重要性を増していくでしょう。

参考資料

この記事の内容は、以下の資料やツールを参考にしています:

公式リソース:

検証ツール:

MCPはアクティブに開発が進められているプロジェクトです。最新の情報については公式ドキュメントを参照してください。