Model Context Protocol 服务器中的 JSON Schema 使用模式

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

Model Context Protocol (MCP) 服务器广泛使用 JSON Schema 来定义工具参数、验证输入并确保客户端-服务器通信中的类型安全。本综合指南通过分析生产服务器和官方规范中的实际实现模式,为开发者提供构建健壮 MCP 实现的可操作见解。

MCP 的 TypeScript 优先架构

Model Context Protocol 采用TypeScript 优先的方法,其中权威的 Schema 定义源于 TypeScript 代码,然后自动生成 JSON Schema 以确保更广泛的兼容性。这种设计理念确保了 TypeScript 实现中的类型安全,同时为其他语言提供通用的 JSON Schema 验证。

在官方 MCP 规范中,JSON Schema 出现在四个关键领域。工具使用 inputSchema 定义接受的参数,使用 outputSchema(在规范 2025-06-18 中引入)验证结构化响应。资源使用 Schema 进行模板定义和内容验证。提示验证参数结构和消息格式。整个协议消息结构本身使用 JSON Schema 定义,涵盖请求/响应格式、通知、错误处理和能力协商。

当前规范有一个值得注意的限制:structuredContent 必须是对象类型,尽管 JSON Schema 具有更广泛的能力,但阻止工具返回数组或原始类型。社区提案 #834 旨在通过建立完整的 JSON Schema 2020-12 支持来解决这个问题,这将消除仅限对象的限制并启用高级验证组合。

Zod 与原生 JSON Schema 方法对比

现代 MCP 实现绝大多数倾向于使用 Zod 而不是原生 JSON Schema 进行 Schema 定义,这主要是由于更优秀的开发者体验和自动类型推断。以下是实际对比:

// 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 是字符串,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 工具中的常见 Schema 模式

生产 MCP 服务器一致采用几种平衡灵活性与验证严格性的 Schema 模式。最普遍的模式处理带默认值的可选参数

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 功能

虽然基本 Schema 足以满足大多数工具,但复杂的 MCP 服务器利用高级 JSON Schema 功能。但是,客户端兼容性存在显著差异。

使用 allOf 进行 Schema 组合

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(不支持 allOfoneOf),Gemini API 完全不支持联合类型,而 Cursor 将服务器限制为 40 个工具,且名称总长度为 60 个字符。生产服务器实现能力检测:

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 服务器主要使用 Zod 进行运行时验证,而不是传统的 JSON Schema 验证器如 AJV。这种模式来自官方 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 生成类型

在处理现有 JSON Schema 规范时,MCP 服务器使用 json-schema-to-typescript 生成 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: '/* 自动生成的 MCP 工具类型 */',
  unreachableDefinitions: true // 对于具有 $refs 的 Schema 很重要
});

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

对于双向类型安全,首先定义类型然后派生 Schema:

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 旨在建立 JSON Schema 2020-12 作为默认方言,并支持明确的 $schema 字段。这将启用高级验证组合(anyOf、oneOf、allOf)、unevaluatedProperties 进行更严格的验证,以及移除结构化内容的仅限对象限制。

在官方 2020-12 支持到来之前,开发者应该使用在所有客户端中都能工作的保守 Schema 功能,针对多个 MCP 客户端实现测试 Schema,并为未来兼容性准备迁移路径。

错误处理和验证模式

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 服务器展示了复杂的 Schema 模式。GitHub MCP 服务器使用带联合类型的复杂嵌套 Schema:

{
  "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 服务器实现带动态 Schema 的多提供商路由:

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()
});

开发者提示和建议

对于 Schema 设计:在 TypeScript 项目中从 Zod 开始,以利用自动类型推断。使用描述性字段名称和全面的描述来帮助 LLM 理解参数目的。实现带有有意义错误消息的适当验证。在适当的地方提供合理的默认值。

对于验证:优先选择 Zod 而不是 AJV 以获得更好的 TypeScript 集成。实现带有全面错误报告的请求时验证。缓存编译的验证器以提高性能。使用动态 Schema 注入进行上下文感知验证。

对于类型安全:首先定义 Schema,然后派生 TypeScript 类型。使用 satisfies 操作符确保双向类型兼容性。将类型生成集成到构建管道中。在编译时安全之外实现运行时类型检查。

对于错误处理:始终返回带有 isError 标志的结构化错误响应。提供清晰、可操作的错误消息。为外部依赖实现断路器。优雅地处理部分失败。在服务器端记录完整的错误详细信息,同时清理客户端消息。

对于生产部署:针对多个 MCP 客户端测试 Schema。实现客户端能力检测。为有限的客户端提供回退 Schema。监控验证失败率。清楚地记录 Schema 要求。

Model Context Protocol 的 JSON Schema 集成为构建类型安全、验证的工具接口提供了强大的基础。虽然当前版本兼容性和客户端支持方面的限制带来了挑战,但生态系统正在快速成熟。通过遵循这些模式和最佳实践,开发者可以创建既强大又有弹性的 MCP 服务器,准备随着协议的未来增强而发展。