Перейти к содержимому
Integration

OpenAPI для сервера MCP: 150 конечных точек, 49 инструментов искусственного интеллекта.

| 9 min read

Как мы преобразовали спецификацию OpenAPI в курируемый сервер MCP с 49 инструментами. Преобразование схемы, описания инструментов, аннотации и HTTP-транспорт без сохранения состояния.

Laptop showing code in a dark development environment
Photo by Clement Helardot on Unsplash

REST API Botoi имеет более 150 конечных точек. Когда мы создавали сервер MCP, мы зарегистрировали 49 из них как инструменты. Не потому, что остальные не работают. Потому что дать модели ИИ 150 инструментов — это все равно, что дать кому-то 200-страничное меню; они что-то выберут, но это будет не то.

В этом посте описывается весь процесс: составление списка инструментов, преобразование схем OpenAPI в объекты Zod, написание описаний, которые хорошо анализируются моделями AI, добавление аннотаций MCP и запуск всего этого в качестве Cloudflare Worker без сохранения состояния. Если вы поддерживаете API и хотите создать на его основе сервер MCP, это инструкция.

Почему 49 инструментов, а не 150

Каждый инструмент, который вы регистрируете на сервере MCP, сериализуется в контекстное окно модели. Имя инструмента, описание и полная входная схема используют токены. Манифест из 150 инструментов может сжечь более 30 000 токенов, прежде чем пользователь введет хоть одно слово.

Это создает две проблемы:

  • Меньше токенов осталось на сам разговор
  • Модель чаще выбирает неправильный инструмент, если список длинный.

Мы проверили это. Зарегистрировав более 150 конечных точек, Клод выбрал правильный инструмент с первой попытки примерно в 72% случаев. С учетом 49 курируемых инструментов это число выросло до 94%. Меньший и сфокусированный список позволил модели лучше выполнять свою работу.

Критерии курирования были просты:

  • Нужен ли ИИ-агенту этот разговор в середине разговора? (Поиск DNS: да. Генерация PDF: редко.)
  • Возвращает ли инструмент структурированные данные, которые может анализировать модель? (JSON: да. Двоичное изображение: нет.)
  • Может ли модель заполнить необходимые параметры из естественного языка? (Доменное имя: да. Сложные вложенные объекты конфигурации: нет.)

Структура манифеста инструмента

Каждый курируемый инструмент сопоставляет имя инструмента MCP с путем API, методом HTTP, описанием и аннотациями. Вот интерфейс TypeScript:

export interface CuratedTool {
  path: string;
  method: 'get' | 'post';
  title: string;
  description: string;
  annotations: {
    readOnlyHint?: boolean;
    destructiveHint?: boolean;
    idempotentHint?: boolean;
    openWorldHint?: boolean;
  };
}

А вот как на практике выглядят две записи:

// curated-tools.ts
export const CURATED_TOOLS: Record<string, CuratedTool> = {
  lookup_dns: {
    path: '/v1/dns/lookup',
    method: 'post',
    title: 'DNS Lookup',
    description:
      'Query DNS records (A, AAAA, MX, TXT, CNAME, NS) for a domain. ' +
      'Use when you need to check DNS configuration or troubleshoot domain resolution.',
    annotations: { readOnlyHint: true, openWorldHint: true },
  },
  dev_hash: {
    path: '/v1/hash',
    method: 'post',
    title: 'Hash Text',
    description:
      'Generate a hash (MD5, SHA-1, SHA-256, SHA-512) of input text. ' +
      'Use for checksums, data integrity, or fingerprinting.',
    annotations: { readOnlyHint: true },
  },
  // ... 47 more tools
};

The path и method поля указывают на существующую конечную точку REST. description сообщает модели, когда использовать инструмент. annotations сообщите модели, как ведет себя инструмент.

Преобразование схем OpenAPI в Zod

MCP SDK ожидает, что входные схемы инструментов будут объектами Zod. В нашем API уже есть спецификация OpenAPI 3.1 с полными определениями тела запроса для каждой конечной точки. Построитель схем считывает эти определения и генерирует типы Zod при запуске сервера.

Основная функция преобразования сопоставляет каждый тип свойства OpenAPI с его эквивалентом Zod:

// schema-builder.ts
import { z } from 'zod';
import { paths } from '../../openapi-paths';

function mapPropertyToZod(
  prop: OpenApiProperty,
  isRequired: boolean
): z.ZodTypeAny {
  let schema: z.ZodTypeAny;

  if (prop.enum && prop.enum.length > 0) {
    schema = z.enum(prop.enum as [string, ...string[]]);
  } else {
    switch (prop.type) {
      case 'number':
      case 'integer':
        schema = z.number();
        break;
      case 'boolean':
        schema = z.boolean();
        break;
      case 'array':
        schema = z.array(z.string());
        break;
      case 'object':
        schema = z.record(z.unknown());
        break;
      default:
        schema = z.string();
        break;
    }
  }

  if (prop.description) {
    schema = schema.describe(prop.description);
  }
  if (prop.default !== undefined) {
    schema = schema.default(prop.default);
  }
  if (!isRequired) {
    schema = schema.optional();
  }

  return schema;
}

Ключевые решения в этой функции:

  • enum ценности становятся z.enum(), предоставляя модели фиксированный набор допустимых опций
  • Обязательные поля остаются обязательными; необязательные поля получают .optional()
  • OpenAPI description переносится через .describe(), который MCP SDK включает в манифест инструмента.
  • Значения по умолчанию распространяются через .default()

The buildZodSchema функция обрабатывает конечные точки как POST (тело запроса), так и GET (параметры запроса):

export function buildZodSchema(
  apiPath: string,
  method: 'get' | 'post'
): Record<string, z.ZodTypeAny> {
  const operation = getOperation(apiPath, method);
  if (!operation) return {};

  // POST: read from requestBody schema
  if (method === 'post') {
    const schema = operation.requestBody
      ?.content?.['application/json']?.schema;
    if (!schema?.properties) return {};

    const required = new Set(schema.required ?? []);
    const result: Record<string, z.ZodTypeAny> = {};

    for (const [key, prop] of Object.entries(schema.properties)) {
      result[key] = mapPropertyToZod(prop, required.has(key));
    }
    return result;
  }

  // GET: read from query parameters
  const params = operation.parameters;
  if (!params || params.length === 0) return {};

  const result: Record<string, z.ZodTypeAny> = {};
  for (const param of params) {
    if (param.in !== 'query') continue;
    const prop: OpenApiProperty = {
      type: param.schema?.type ?? 'string',
      description: param.description ?? param.schema?.description,
      default: param.schema?.default,
      enum: param.schema?.enum,
    };
    result[param.name] = mapPropertyToZod(prop, param.required === true);
  }
  return result;
}

Эта функция запускается один раз для каждого инструмента во время создания сервера. Возвращает квартиру Record<string, z.ZodTypeAny> что MCP SDK сериализует в схему JSON для манифеста инструмента.

Написание описаний инструментов. Модели ИИ хорошо анализируются.

Описание инструмента — это самое важное поле для правильного выбора инструмента. Модели читают его, чтобы решить, соответствует ли инструмент намерениям пользователя. Расплывчатые описания приводят к неправильному выбору инструментов.

Мы остановились на шаблоне из двух предложений:

  1. Первое предложение: что делает инструмент, начинающийся с глагола. Включите конкретные типы данных или форматы, которые он обрабатывает.
  2. Второе предложение: когда его использовать, начиная с «Использовать когда». Это дает модели условие триггера.

Сравните эти два описания одного и того же инструмента поиска DNS:

Версия Описание Проблема
Плохой «DNS-инструмент для поиска информации» Типы записей не указаны, условия триггера отсутствуют, неясно
Хороший «Запрос записей DNS (A, AAAA, MX, TXT, CNAME, NS) для домена. Используйте, когда вам нужно проверить конфигурацию DNS или устранить неполадки разрешения домена». Никто

Хорошая версия сообщает модели, какие именно типы записей она может запрашивать (чтобы она знала, что этот инструмент обрабатывает поиск MX) и ситуации, которые должны его активировать (проверка конфигурации DNS, устранение неполадок). Модель сопоставляет намерения пользователя с этими ключевыми словами.

Аннотации MCP: сообщают моделям, как ведут себя инструменты

Аннотации — это флаги метаданных каждого инструмента. Они не влияют на исполнение. Они сообщают модели, каких побочных эффектов следует ожидать.

// Read-only tool that hits an external service
lookup_dns: {
  annotations: { readOnlyHint: true, openWorldHint: true },
}

// Encryption tool: no external calls, same input = same output
security_encrypt: {
  annotations: { idempotentHint: true },
}

Четыре аннотации и что они сигнализируют:

Аннотация Сигнал Пример
readOnlyHint Этот инструмент читает данные, но никогда ничего не изменяет Поиск DNS, WHOIS, проверка SSL
destructiveHint Этот инструмент удаляет или перезаписывает данные Удаление входящих сообщений Webhook (нет в нашем тщательно подобранном наборе)
idempotentHint Вызов этого инструмента дважды с одним и тем же вводом дает тот же результат. Шифрование AES, расшифровка AES
openWorldHint Этот инструмент выполняет запросы к внешней сети Поиск IP-адресов, метаданные URL-адресов, техническое обнаружение

Из наших 49 инструментов 44 переносят readOnlyHint: true. 12 инструментов поиска также содержат openWorldHint: true потому что они вызывают внешние DNS-серверы, реестры WHOIS или получают действующие веб-страницы. Инструменты шифрования/дешифрования несут idempotentHint: true потому что это детерминированные преобразования.

Ни один из наших тщательно подобранных инструментов не несет в себе destructiveHint. Это был осознанный выбор. Мы исключили такие инструменты, как удаление входящих сообщений веб-перехватчика и удаление вставки, из курируемого набора, поскольку модели ИИ не должны удалять пользовательские данные без надежных ограничений.

Регистрация инструментов на сервере MCP

Цикл регистрации связывает все воедино. Он перебирает список тщательно подобранных инструментов, строит схему Zod на основе спецификации OpenAPI и регистрирует каждый инструмент с его описанием и аннотациями:

// server.ts
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { CURATED_TOOLS } from './curated-tools';
import { buildZodSchema } from './schema-builder';

function createMcpServer(apiKey: string | undefined, env: Env) {
  const server = new McpServer(
    { name: 'botoi', version: '1.0.0' },
    { jsonSchemaValidator: new CfWorkerJsonSchemaValidator() }
  );

  for (const [toolName, tool] of Object.entries(CURATED_TOOLS)) {
    const zodSchema = buildZodSchema(tool.path, tool.method);

    server.registerTool(toolName, {
      title: tool.title,
      description: tool.description,
      inputSchema: zodSchema,
      annotations: tool.annotations,
    }, async (args: Record&lt;string, unknown&gt;) =&gt; {
      return callApi(tool.path, tool.method, args, apiKey, env);
    });
  }

  return server;
}

Когда модель вызывает инструмент, функция-обработчик получает проанализированные аргументы и перенаправляет их на внутренний маршрут API. callApi Функция создает внутренний HTTP-запрос и возвращает ответ в виде содержимого в формате MCP:

async function callApi(
  path: string,
  method: string,
  body: unknown,
  apiKey: string | undefined,
  env: Env
) {
  const headers: Record&lt;string, string&gt; = {
    'Content-Type': 'application/json',
  };
  if (apiKey) headers['X-API-Key'] = apiKey;

  const req = new Request(\`http://internal\$\{path}\`, {
    method: method.toUpperCase(),
    headers,
    body: method === 'post' ? JSON.stringify(body) : undefined,
  });

  const res = appFetcher
    ? await appFetcher(req, env)
    : await fetch(req);

  const json = await res.json();
  if (!json.success) {
    return {
      content: [{ type: 'text', text: JSON.stringify(json.error, null, 2) }],
      isError: true,
    };
  }
  return {
    content: [{ type: 'text', text: JSON.stringify(json.data, null, 2) }],
  };
}

The appFetcher Шаблон позволяет серверу MCP вызывать маршруты API через внутреннюю ссылку на функцию вместо выполнения внешнего HTTP-запроса. Это позволяет избежать сетевых обходов. Обработчик MCP и маршруты API выполняются в одном и том же Cloudflare Worker, поэтому внутренняя маршрутизация — это вызов функции, а не переход HTTP.

HTTP-транспорт без сохранения состояния в Cloudflare Workers

MCP поддерживает два транспорта: stdio (для локальных процессов) и Streamable HTTP (для удаленных серверов). Мы выбрали Streamable HTTP, потому что сервер работает на Cloudflare Worker, который не поддерживает длительные процессы.

// Hono route handler
import { WebStandardStreamableHTTPServerTransport }
  from '@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js';

app.all('/mcp', async (c) =&gt; {
  const apiKey =
    c.req.header('X-API-Key') ||
    c.req.header('Authorization')?.replace('Bearer ', '');

  const server = createMcpServer(apiKey, c.env);
  const transport = new WebStandardStreamableHTTPServerTransport();
  await server.connect(transport);
  return transport.handleRequest(c.req.raw);
});

Каждый запрос создает новый McpServer пример. Между запросами состояние сеанса не сохраняется. Это нормально, поскольку каждый вызов инструмента является автономным; модель отправляет имя инструмента и аргументы, а сервер возвращает результат. Никаких многошаговых транзакций.

Дизайн без сохранения состояния имеет три преимущества:

  • Не требуется хранилище сеансов (ни Redis, ни KV, ни база данных)
  • Масштабируется до нуля в режиме ожидания, масштабируется горизонтально под нагрузкой
  • Развертывание в более чем 300 периферийных местоположениях Cloudflare без настройки.

Обработка ключей API происходит на уровне MCP. Клиент отправляет ключ через X-API-Key или Authorization: Bearer заголовок. Маршрут MCP извлекает его и передает внутреннему вызову API. На самом маршруте MCP нет отдельного промежуточного программного обеспечения для аутентификации.

Руководство для вашего собственного API

Если у вас есть спецификация OpenAPI и вы хотите создать сервер MCP, вот сокращенная версия:

  1. Составьте свой список инструментов. Выберите 20–80 конечных точек, которые возвращают структурированные данные и принимают простые входные данные. Пропускайте конечные точки, которые возвращают двоичные данные, требуют загрузки файлов или имеют глубоко вложенные входные схемы.
  2. Напишите преобразователь схем. Сопоставьте типы свойств OpenAPI с Zod. Перенесите описания, значения по умолчанию и значения перечисления. Обработка шаблонов как тела запроса (POST), так и параметров запроса (GET).
  3. Напишите описание из двух предложений. Предложение первое: что делает инструмент, начиная с глагола. Второе предложение: «Использовать, когда» + условие срабатывания. Будьте конкретны в отношении типов и форматов данных.
  4. Добавьте аннотации. Отметьте инструменты, доступные только для чтения. Отметьте инструменты, которые совершают вызовы во внешнюю сеть. Назовите идемпотентные операции. Исключите деструктивные инструменты, если у вас нет потоков подтверждения.
  5. Выберите свой транспорт. Используйте Streamable HTTP для удаленных серверов, stdio для локальных инструментов CLI. MCP SDK предоставляет и то, и другое.
  6. Направляйте вызовы инструментов к существующему API. Не переписывайте бизнес-логику. Вызовите свои собственные маршруты внутри компании. Сервер MCP представляет собой тонкий уровень адаптера.

Сервер MCP Ботоя состоит из 4 файлов: curated-tools.ts (49 определений инструментов), schema-builder.ts (конвертер OpenAPI в Zod), server.ts (регистрация и маршрутизация), и tools.ts (конечная точка общедоступного манифеста). Все это добавляет около 400 строк TypeScript к существующему API.

Попробуйте это

Сервер Botoi MCP работает по адресу: https://api.botoi.com/mcp. Подключите его к Claude Desktop, Claude Code, Cursor или VS Code менее чем за минуту. См. Документация по настройке MCP для фрагментов конфигурации для каждого поддерживаемого клиента.

Просмотрите полный манифест инструмента чтобы увидеть все 49 определений инструментов с их схемами и аннотациями. Документация по API охватить полный набор из более чем 150 конечных точек REST за сервером MCP.

FAQ

Как создать сервер MCP на основе спецификации OpenAPI?
Проанализируйте определения путей OpenAPI, извлеките схему тела запроса (для POST) или параметры запроса (для GET), преобразуйте каждое свойство в тип Zod, а затем зарегистрируйте каждый инструмент на экземпляре McpServer со схемой Zod как inputSchema. MCP SDK управляет транспортировкой JSON-RPC и обнаружением инструментов.
Почему бы не предоставить все конечные точки API как инструменты MCP?
Модели AI имеют ограничение контекстного окна. Каждое определение инструмента использует токены. Манифест из 150 инструментов может съесть более 30 000 токенов до начала разговора. Сокращение до 49 ценных инструментов позволяет сохранить манифест менее 8000 токенов и повысить точность выбора инструментов.
Что такое аннотации инструмента MCP и почему они имеют значение?
Аннотации — это подсказки метаданных, такие как readOnlyHint, DestructiveHint, IdempotentHint и openWorldHint. Они сообщают моделям ИИ, читает ли инструмент или записывает данные, связывается ли он с внешними службами и безопасно ли повторять попытку. Модели используют эти подсказки, чтобы планировать многоэтапные рабочие процессы и избегать деструктивных действий без подтверждения.
Может ли сервер MCP работать на рабочих Cloudflare?
Да. Используйте WebStandardStreamableHTTPServerTransport из MCP SDK. Он работает с любой средой выполнения, которая поддерживает API запросов и ответов веб-стандартов. Cloudflare Workers, Deno Deploy и Vercel Edge Functions соответствуют требованиям. Каждый запрос создает новый экземпляр McpServer, поэтому состояние сеанса не требуется.
Как мне написать описания инструментов MCP для моделей ИИ?
Начните с глагола. Скажите, что делает инструмент, в одном предложении. Добавьте второе предложение, начинающееся с «Использовать когда», описывающее условие триггера. Пропустить детали реализации. Пример: «Запросить DNS-записи (A, AAAA, MX, TXT, CNAME, NS) для домена. Используйте, когда вам нужно проверить конфигурацию DNS или устранить неполадки разрешения домена».

Начните разработку с botoi

150+ API-эндпоинтов для поиска, обработки текста, генерации изображений и утилит для разработчиков. Бесплатный тариф, без банковской карты.