Model Context Protocolサーバーにおけるjson Schemaの利用パターン

mcpjson-schematypescriptzodvalidation
By sko X opus 4.19/19/202510 min read

Model Context Protocol(MCP)サーバーは、ツールパラメータの定義、入力値の検証、クライアント・サーバー間通信における型安全性の確保において、JSON Schemaを広範囲にわたって活用しています。この包括的ガイドでは、本番環境のサーバーと公式仕様書から得られた実用的な実装パターンを検証し、堅牢なMCP実装を構築するための実践的な洞察を開発者に提供します。

MCPのTypeScript優先アーキテクチャ

Model Context ProtocolはTypeScript優先のアプローチを採用しており、権威あるスキーマ定義はTypeScriptコードから発生し、幅広い互換性のためにJSON Schemaが自動生成されます。この設計哲学により、TypeScript実装における型安全性を確保しつつ、他言語向けの汎用的なJSON Schemaバリデーションを提供しています。

公式MCP仕様において、JSON Schemaは4つの重要な領域で使用されています。ツールは受け入れるパラメータを定義するためにinputSchemaを、構造化されたレスポンスを検証するためにoutputSchema(2025-06-18の仕様で導入)を使用します。リソースはテンプレート定義とコンテンツ検証にスキーマを使用します。プロンプトは引数構造とメッセージフォーマットを検証します。プロトコルメッセージ構造全体がJSON Schemaを使用して定義されており、リクエスト/レスポンスフォーマット、通知、エラーハンドリング、機能ネゴシエーションをカバーしています。

現在の仕様には注目すべき制限があります:structuredContentはオブジェクト型でなければならず、JSON Schemaの幅広い機能にもかかわらず、ツールが配列やプリミティブ型を返すことができません。コミュニティ提案#834は、完全なJSON Schema 2020-12サポートを確立することでこの問題に対処し、オブジェクトのみの制限を取り除き、高度な検証組み合わせを可能にすることを目指しています。

ZodとネイティブJSON Schemaアプローチの比較

最新のMCP実装では、優れた開発者体験と自動型推論により、ネイティブJSON SchemaよりもZodを圧倒的に選択しています。実用的な比較をご覧ください:

// Zodアプローチ - TypeScript環境で推奨
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const weatherSchema = z.object({
  location: z.string().describe("都市名または郵便番号"),
  units: z.enum(["celsius", "fahrenheit"]).default("celsius"),
  includeForcast: z.boolean().optional()
});

server.registerTool("get_weather", {
  description: "現在の天気データを取得",
  inputSchema: weatherSchema
}, async (args) => {
  // TypeScriptが自動的にパラメータ型を推論
  // args.locationはstring、args.unitsは"celsius" | "fahrenheit"
  const validated = weatherSchema.parse(args);
  // 完全な型安全性を持つ実装
});

ネイティブJSON Schemaアプローチは、より冗長ですが、言語に依存しない互換性を提供します:

// ネイティブJSON Schemaアプローチ
server.setRequestHandler(ListToolsRequestSchema, async () => {
  return {
    tools: [{
      name: "get_weather",
      description: "現在の天気データを取得",
      inputSchema: {
        type: "object",
        properties: {
          location: {
            type: "string",
            description: "都市名または郵便番号"
          },
          units: {
            type: "string",
            enum: ["celsius", "fahrenheit"],
            default: "celsius"
          }
        },
        required: ["location"],
        additionalProperties: false
      }
    }]
  };
});

Zodアプローチは自動TypeScript型推論、詳細なエラーメッセージを含む組み込みランタイム検証、優れたIntelliSenseサポート、外部依存関係ゼロを提供します。ただし、TypeScript環境が必要で学習コストがあります。ネイティブJSON Schemaはプロトコルネイティブな互換性、すべてのプログラミング言語での動作、既存のJSON Schemaツールとの統合を提供しますが、型安全性がなく手動検証実装が必要です。

MCPツールの一般的なスキーマパターン

本番環境のMCPサーバーは、柔軟性と検証の厳密さのバランスを取る複数のスキーマパターンを一貫して使用しています。最も普及しているパターンはデフォルト値付きオプションパラメータの処理です:

const toolSchema = z.object({
  // 検証付き必須パラメータ
  query: z.string().min(1).describe("検索クエリ"),

  // デフォルト値と制約付きオプション
  limit: z.number().min(1).max(100).default(10)
    .describe("返す結果の最大数"),

  // デフォルト値なしオプション
  category: z.string().optional()
    .describe("カテゴリでフィルタ"),

  // 混在要件のネストオブジェクト
  options: z.object({
    includeMetadata: z.boolean().default(false),
    sortBy: z.enum(["relevance", "date", "popularity"]).default("relevance")
  }).optional()
});

複雑なネスト構造では、MCPツールは階層データをしばしばモデル化します:

const addressSchema = z.object({
  street: z.string(),
  city: z.string(),
  zipCode: z.string().regex(/^\d{5}(-\d{4})?$/),
  country: z.string().default("US")
});

const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  addresses: z.array(addressSchema).min(1),
  preferences: z.record(z.string(), z.any()).optional()
});

ツールパラメータ定義のベストプラクティス

効果的なパラメータ定義には、検証、ドキュメント、エラーハンドリングへの注意深い配慮が必要です。入力サニタイゼーションはセキュリティ脆弱性を防ぎます:

const secureInputSchema = z.object({
  filename: z.string()
    .regex(/^[a-zA-Z0-9._-]+$/, "無効なファイル名文字")
    .refine(name => !name.startsWith('.'), "隠しファイルは許可されていません"),

  sqlQuery: z.string()
    .refine(query => {
      const dangerous = ['drop', 'delete', 'truncate'];
      return !dangerous.some(word =>
        query.toLowerCase().includes(word)
      );
    }, "クエリに危険な操作が含まれています"),

  userInput: z.string()
    .trim()
    .min(1, "入力は空にできません")
    .max(1000, "入力が最大長を超えています")
    .transform(input => input.replace(/[<>]/g, ""))
});

詳細な説明はLLMの理解を向上させます:

const documentedSchema = z.object({
  query: z.string()
    .describe("検索クエリ - ワイルドカード(*)と引用符内の完全一致フレーズをサポート"),

  dateRange: z.object({
    start: z.string().datetime()
      .describe("ISO 8601形式の開始日(YYYY-MM-DDTHH:mm:ssZ)"),
    end: z.string().datetime()
      .describe("ISO 8601形式の終了日(YYYY-MM-DDTHH:mm:ssZ)")
  }).optional()
    .describe("日付範囲で結果をフィルタ - 両方の日付を提供する必要があります"),

  sortBy: z.enum(["relevance", "date", "popularity"])
    .default("relevance")
    .describe("ソート順序: relevance(最適一致)、date(新しい順)、popularity(最多閲覧)")
});

MCPでの高度なJSON Schema機能

基本的なスキーマはほとんどのツールに十分ですが、複雑なMCPサーバーは高度なJSON Schema機能を活用します。ただし、クライアントの互換性は大きく異なります。

allOfを使用したスキーマ合成

allOfキーワードにより、アドレスツールで国固有の検証が可能になります:

{
  "type": "object",
  "properties": {
    "street_address": {"type": "string"},
    "country": {"enum": ["US", "Canada", "Netherlands"]}
  },
  "allOf": [
    {
      "if": {"properties": {"country": {"const": "US"}}},
      "then": {"properties": {"postal_code": {"pattern": "[0-9]{5}(-[0-9]{4})?"}}
    },
    {
      "if": {"properties": {"country": {"const": "Canada"}}},
      "then": {"properties": {"postal_code": {"pattern": "[A-Z][0-9][A-Z] [0-9][A-Z][0-9]"}}}
    }
  ]
}

anyOfを使用したユニオン型

データベース接続ツールは柔軟な設定のためにanyOfを使用します:

const connectionSchema = z.union([
  z.object({
    type: z.literal("postgresql"),
    host: z.string(),
    port: z.number(),
    database: z.string(),
    username: z.string(),
    password: z.string()
  }),
  z.object({
    type: z.literal("sqlite"),
    file_path: z.string()
  }),
  z.object({
    type: z.literal("connection_string"),
    connection_string: z.string()
  })
]);

// ### クライアント制限の回避策

異なるMCPクライアントは様々なJSON Schemaサポートレベルを持ちます。Googleエージェントは`anyOf`のみをサポートし(`allOf``oneOf`は非対応)、Gemini APIは共用体型を全くサポートせず、Cursorはサーバーを40ツール、合計60文字の名前に制限します。本番環境のサーバーは機能検出を実装します:

```javascript
function transformSchemaForClient(schema, clientType) {
  const capabilities = {
    "claude-desktop": ["refs", "unions", "formats"],
    "cursor": ["basic"],
    "google-agents": ["anyof-only"],
    "gemini": ["basic", "no-unions"]
  };

  if (!capabilities[clientType].includes("unions")) {
    // anyOf/oneOfを削除、最初の変種のみ使用
    schema = removeUnions(schema);
  }

  if (!capabilities[clientType].includes("refs")) {
    // すべての$ref参照をインライン化
    schema = inlineReferences(schema);
  }

  return schema;
}

検証戦略と実装

MCPサーバーは、AJVなどの従来のJSON Schemaバリデータではなく、ランタイム検証にZodを主に使用します。このパターンは公式SDKと本番実装から現れています:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { z } from "zod";

const UserFilterSchema = z.object({
  userId: z.string().describe("フィルタするユーザーID"),
  page: z.number().int().positive().optional().default(1),
  limit: z.number().int().positive().optional().default(10)
});

server.registerTool("getUserData", {
  inputSchema: UserFilterSchema
}, async (args) => {
  try {
    const validated = UserFilterSchema.parse(args);
    // 型安全性で処理
    return { content: [{ type: "text", text: JSON.stringify(result) }] };
  } catch (error) {
    if (error instanceof z.ZodError) {
      const details = error.errors.map(e =>
        `${e.path.join('.')}: ${e.message}`
      ).join(', ');

      return {
        content: [{ type: "text", text: `検証に失敗しました: ${details}` }],
        isError: true
      };
    }
    throw error;
  }
});

パフォーマンス最適化のため、コンパイル済みバリデータをキャッシュします:

const validatorCache = new Map<string, z.ZodSchema>();

function getValidator(toolName: string, schema: z.ZodSchema) {
  if (!validatorCache.has(toolName)) {
    validatorCache.set(toolName, schema);
  }
  return validatorCache.get(toolName)!;
}

JSON SchemaからのTypeScript型生成

既存のJSON Schema仕様を使用する場合、MCPサーバーはTypeScriptインターフェース生成にjson-schema-to-typescriptを使用します:

import { compile } from 'json-schema-to-typescript';

const schema = {
  title: "MCPToolInput",
  type: "object",
  properties: {
    query: { type: "string" },
    filters: {
      type: "array",
      items: { type: "string" }
    },
    options: {
      type: "object",
      properties: {
        timeout: { type: "number" },
        retry: { type: "boolean" }
      }
    }
  },
  required: ["query"],
  additionalProperties: false
};

const typescript = await compile(schema, 'MCPToolInput', {
  bannerComment: '/* Auto-generated MCP tool types */',
  unreachableDefinitions: true // $refsを持つスキーマに重要
});

// 生成結果:
// export interface MCPToolInput {
//   query: string;
//   filters?: string[];
//   options?: {
//     timeout?: number;
//     retry?: boolean;
//   };
// }

双方向型安全性のため、型を最初に定義してからスキーマを導出します:

interface CreateUserRequest {
  name: string;
  email: string;
  role: 'admin' | 'user' | 'guest';
}

const createUserSchema = z.object({
  name: z.string().min(1).max(100),
  email: z.string().email(),
  role: z.enum(['admin', 'user', 'guest'])
}) satisfies z.ZodType<CreateUserRequest>;

// コンパイル時に型互換性を検証
type SchemaType = z.infer<typeof createUserSchema>;
const _typeCheck: SchemaType extends CreateUserRequest ? true : false = true;

JSON Schemaバージョン互換性

MCP仕様は現在、明示的なJSON Schemaバージョン宣言を欠いており、クライアント間で検証の不整合を引き起こしています。異なる実装が異なるバージョンを想定しており、一部はDraft-07を使用し、他は2020-12サポートを試みています。このバージョンの曖昧さは検証失敗とランタイムエラーをもたらします。

コミュニティ提案#834は、明示的な$schemaフィールドサポートを持つJSON Schema 2020-12をデフォルトダイアレクトとして確立することを目指しています。これにより、高度な検証組み合わせ(anyOf、oneOf、allOf)、より厳密な検証のためのunevaluatedProperties、構造化コンテンツのオブジェクトのみ制限の削除が可能になります。

公式の2020-12サポートが到着するまで、開発者はすべてのクライアントで動作する保守的なスキーマ機能を使用し、複数のMCPクライアント実装に対してスキーマをテストし、将来の互換性のための移行パスを準備する必要があります。

エラーハンドリングと検証パターン

MCPはすべてのツールが従うべき構造化されたエラーレスポンスフォーマットを定義しています:

interface MCPErrorResponse {
  isError: true;
  content: Array<{
    type: "text";
    text: string;
  }>;
}

function handleValidationError(error: ZodError): MCPErrorResponse {
  const errorDetails = error.errors.map(err => ({
    field: err.path.join('.'),
    message: err.message,
    code: err.code
  }));

  const userFriendlyMessage = `${errorDetails.length}個のフィールドで検証に失敗しました:\n${
    errorDetails.map(detail => `- ${detail.field}: ${detail.message}`).join('\n')
  }`;

  return {
    isError: true,
    content: [{
      type: "text",
      text: userFriendlyMessage
    }]
  };
}

本番環境の堅牢性のため、外部依存関係に対してサーキットブレーカーを実装します:

class CircuitBreaker {
  private failures = 0;
  private state: 'closed' | 'open' | 'half-open' = 'closed';

  async execute<T>(operation: () => Promise<T>): Promise<T> {
    if (this.state === 'open') {
      throw new Error('サービスが一時的に利用できません');
    }

    try {
      const result = await operation();
      this.onSuccess();
      return result;
    } catch (error) {
      this.onFailure();
      throw error;
    }
  }

  private onSuccess() {
    this.failures = 0;
    this.state = 'closed';
  }

  private onFailure() {
    this.failures++;
    if (this.failures >= 3) {
      this.state = 'open';
      setTimeout(() => this.state = 'half-open', 60000);
    }
  }
}

実際の実装例

本番環境のMCPサーバーは洗練されたスキーマパターンを実証しています。GitHub MCPサーバーはユニオン型を持つ複雑なネストスキーマを使用します:

{
  "name": "create_pull_request",
  "inputSchema": {
    "type": "object",
    "properties": {
      "repo": {"type": "string"},
      "title": {"type": "string"},
      "body": {"type": "string"},
      "reviewers": {
        "anyOf": [
          {"type": "array", "items": {"type": "string"}},
          {"type": "null"}
        ]
      }
    },
    "required": ["repo", "title"]
  }
}

Ultimate MCPサーバーは動的スキーマを持つマルチプロバイダールーティングを実装します:

const multiProviderSchema = z.object({
  prompt: z.string(),
  providers: z.array(z.object({
    provider: z.enum(["google", "anthropic", "gemini", "deepseek"]),
    model: z.string(),
    temperature: z.number().min(0).max(2).optional()
  })).min(1),
  commonParameters: z.record(z.string(), z.any()).optional()
});

開発者向けヒントと推奨事項

スキーマ設計について:TypeScriptプロジェクトでは自動型推論を活用するためZodから始めましょう。説明的なフィールド名と包括的な説明を使用してLLMがパラメータの目的を理解できるようにします。意味のあるエラーメッセージを持つ適切な検証を実装します。適切な場合は合理的なデフォルト値を提供します。

検証について:より良いTypeScript統合のためAJVよりもZodを選択します。包括的なエラーレポートを持つリクエスト時検証を実装します。パフォーマンスのためコンパイル済みバリデータをキャッシュします。コンテキスト対応検証のため動的スキーマインジェクションを使用します。

型安全性について:スキーマを最初に定義し、その後TypeScript型を導出します。双方向型互換性を確保するためsatisfies演算子を使用します。型生成をビルドパイプラインに統合します。コンパイル時安全性と並行してランタイム型チェックを実装します。

エラーハンドリングについて:常にisErrorフラグを持つ構造化エラーレスポンスを返します。明確で実行可能なエラーメッセージを提供します。外部依存関係に対してサーキットブレーカーを実装します。部分的な失敗を優雅に処理します。サーバーサイドでは完全なエラー詳細をログに記録し、クライアントメッセージをサニタイズします。

本番デプロイについて:複数のMCPクライアントに対してスキーマをテストします。クライアント機能検出を実装します。制限されたクライアントのためのフォールバックスキーマを提供します。検証失敗率を監視します。スキーマ要件を明確に文書化します。

Model Context ProtocolのJSON Schema統合は、型安全で検証されたツールインターフェースを構築するための堅牢な基盤を提供しています。バージョン互換性とクライアントサポートに関する現在の制限は課題を提示しますが、エコシステムは急速に成熟しています。これらのパターンとベストプラクティスに従うことで、開発者はプロトコルの将来の機能強化に対応できる、強力で回復力のあるMCPサーバーを作成できます。