コンテンツへスキップ
Integration

OpenAPI から MCP サーバーへ: 150 エンドポイント、49 AI ツール

| 9 min read

OpenAPI 仕様を 49 のツールを備えた厳選された MCP サーバーにどのように変換したか。 スキーマ変換、ツールの説明、注釈、およびステートレス HTTP トランスポート。

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

Botoi の REST API には 150 以上のエンドポイントがあります。 MCPサーバーを構築した際に、49個をツールとして登録しました。 残りが機能しないからではありません。 なぜなら、AI モデルに 150 のツールを与えることは、誰かに 200 ページのメニューを渡すようなものだからです。 彼らは何かを選ぶでしょうが、それは正しいことではありません。

この投稿では、ツールリストのキュレーション、OpenAPI スキーマから Zod オブジェクトへの変換、AI モデルが適切に解析する記述の作成、MCP アノテーションの追加、ステートレスな Cloudflare Worker として全体の実行というプロセス全体を説明します。 API を保守し、そこから MCP サーバーを構築したい場合は、これがプレイブックになります。

なぜ 150 ではなく 49 のツールなのか

MCP サーバーに登録したすべてのツールは、モデルのコンテキスト ウィンドウにシリアル化されます。 ツール名、説明、および完全な入力スキーマはすべてトークンを消費します。 150 個のツールのマニフェストは、ユーザーが 1 つの単語を入力する前に 30,000 以上のトークンを書き込むことができます。

これにより、次の 2 つの問題が生じます。

  • 会話自体に残っているトークンが少なくなります
  • リストが長い場合、モデルは間違ったツールを選択することが多くなります

これをテストしました。 150 以上のエンドポイントがすべて登録されているため、クロードは最初の試行で約 72% の確率で正しいツールを選択しました。 49 の厳選されたツールを使用すると、その数は 94% に跳ね上がりました。 より小さく、焦点を絞ったリストにより、モデルの機能が向上しました。

キュレーション基準はシンプルでした。

  • AI エージェントは会話中にこれを必要としますか? (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;
  };
}

2 つのエントリが実際にどのように見えるかは次のとおりです。

// 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
};

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

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

この関数は、サーバーの作成時にツールごとに 1 回実行されます。 アパートを返します Record<string, z.ZodTypeAny> MCP SDK がツール マニフェストの JSON スキーマにシリアル化すること。

ツールの説明を書く AI モデルはうまく解析する

ツールの説明は、ツールを正しく選択するための最も重要なフィールドです。 モデルはそれを読み取って、ツールがユーザーの意図と一致するかどうかを判断します。 曖昧な説明は間違ったツールの選択につながります。

私たちは次の 2 つの文のパターンに落ち着きました。

  1. 最初の文: ツールが何をするのか、動詞で始まります。 処理する特定のデータ型または形式を含めます。
  2. 2 番目の文: いつ使用するか、「いつ使用するか」から始まります。 これにより、モデルにトリガー条件が与えられます。

同じ DNS ルックアップ ツールに関する次の 2 つの説明を比較してください。

バージョン 説明 問題
悪い 「調べるための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 },
}

4 つのアノテーションとそれらが示す内容:

注釈 信号
readOnlyHint このツールはデータを読み取りますが、何も変更しません DNSルックアップ、WHOIS、SSLチェック
destructiveHint このツールはデータを削除または上書きします Webhook 受信箱の削除 (厳選されたセットには含まれていません)
idempotentHint 同じ入力でこのツールを 2 回呼び出すと、同じ結果が生成されます AES暗号化、AES復号化
openWorldHint このツールは外部ネットワーク要求を行います IP ルックアップ、URL メタデータ、技術検出

当社の 49 個のツールのうち、44 個はキャリーです readOnlyHint: true。 12 個のルックアップ ツールには次の機能もあります。 openWorldHint: true 外部の DNS サーバー、WHOIS レジストリを呼び出したり、ライブ Web ページを取得したりするためです。 暗号化/復号化ツールは、 idempotentHint: true それは決定論的な変換だからです。

私たちの厳選されたツールには、 destructiveHint。 それは意図的な選択でした。 AI モデルは強力なガードレールなしではユーザー データを削除すべきではないため、Webhook 受信トレイの削除やペースト削除などのツールを厳選されたセットから除外しました。

MCPサーバーへのツールの登録

登録ループはすべてを結び付けます。 厳選されたツール リストを反復処理し、OpenAPI 仕様から Zod スキーマを構築し、各ツールをその説明と注釈とともに登録します。

// 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) }],
  };
}

appFetcher パターンを使用すると、MCP サーバーは外部 HTTP リクエストを作成する代わりに、内部関数参照を通じて API ルートを呼び出すことができます。 これにより、ネットワークの往復が回避されます。 MCP ハンドラーと API ルートは同じ Cloudflare Worker で実行されるため、内部ルーティングは HTTP ホップではなく関数呼び出しになります。

Cloudflareワーカー上のステートレスHTTPトランスポート

MCP は、stdio (ローカル プロセス用) と Streamable HTTP (リモート サーバー用) の 2 つのトランスポートをサポートします。 サーバーは長時間実行プロセスをサポートしない Cloudflare ワーカー上で実行されるため、ストリーミング可能な HTTP を選択しました。

// 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 実例。 リクエスト間ではセッション状態は保持されません。 すべてのツール呼び出しは自己完結型であるため、これは問題ありません。 モデルはツール名と引数を送信し、サーバーは結果を返します。 複数ステップのトランザクションはありません。

ステートレス設計には 3 つの利点があります。

  • セッションストレージは不要(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. 2 文の説明を書きます。 文 1: 動詞で始まるツールの動作。 文 2: 「いつ使用するか」 + トリガー条件。 データ型と形式については具体的に指定してください。
  4. 注釈を追加します。 読み取り専用ツールにマークを付けます。 外部ネットワーク呼び出しを行うツールにフラグを立てます。 冪等な操作を特定します。 確認フローがない限り、破壊的なツールを除外します。
  5. 交通手段を選択してください。 リモート サーバーにはストリーミング可能な HTTP を使用し、ローカル CLI ツールには stdio を使用します。 MCP SDK は両方を提供します。
  6. ツール呼び出しを既存の API にルーティングします。 ビジネス ロジックを書き換えないでください。 独自のルートを内部で呼び出します。 MCP サーバーは、薄いアダプター層です。

Botoi の MCP サーバーは 4 つのファイルです。 curated-tools.ts (49 のツール定義)、 schema-builder.ts (OpenAPI から Zod へのコンバーター)、 server.ts (登録とルーティング)、および tools.ts (パブリックマニフェストエンドポイント)。 全体として、既存の API に約 400 行の TypeScript が追加されます。

試してみてください

Botoi MCP サーバーは次の場所で稼働しています。 https://api.botoi.com/mcp。 1 分以内に Claude Desktop、Claude Code、Cursor、または VS Code に接続します。 を参照してください。 MCP セットアップ ドキュメント サポートされているすべてのクライアントの構成スニペット用。

閲覧する 完全なツールマニフェスト 49 個すべてのツール定義とそのスキーマおよび注釈を表示します。 の APIドキュメント MCP サーバーの背後にある 150 以上の REST エンドポイントの完全なセットをカバーします。

FAQ

OpenAPI 仕様から MCP サーバーを構築するにはどうすればよいですか?
OpenAPI パス定義を解析し、リクエスト本文スキーマ (POST の場合) またはクエリ パラメーター (GET の場合) を抽出し、各プロパティを Zod タイプに変換して、Zod スキーマを inputSchema として使用して McpServer インスタンスに各ツールを登録します。 MCP SDK は、JSON-RPC トランスポートとツール検出を処理します。
すべての API エンドポイントを MCP ツールとして公開してはどうでしょうか?
AI モデルにはコンテキスト ウィンドウの制限があります。 各ツール定義はトークンを消費します。 150 個のツールのマニフェストは、会話が始まる前に 30,000 以上のトークンを消費する可能性があります。 価値の高いツールを 49 個まで厳選することで、マニフェストを 8,000 トークン未満に抑え、ツール選択の精度を向上させます。
MCP ツールのアノテーションとは何ですか?なぜ重要ですか?
注釈は、readOnlyHint、destructiveHint、idempotentHint、openWorldHint などのメタデータ ヒントです。 これらは、ツールがデータを読み書きするかどうか、外部サービスに接続するかどうか、再試行しても安全かどうかを AI モデルに伝えます。 モデルはこれらのヒントを使用して複数ステップのワークフローを計画し、確認なしに破壊的なアクションを回避します。
MCPサーバーはCloudflare Workers上で実行できますか?
はい。 MCP SDK の WebStandardStreamableHTTPServerTransport を使用します。 これは、Web 標準リクエスト/レスポンス API をサポートする任意のランタイムで動作します。 Cloudflare Workers、Deno Deploy、Vercel Edge Functions はすべて対象となります。 リクエストごとに新しい McpServer インスタンスが作成されるため、セッション状態は必要ありません。
AI モデルの MCP ツールの説明はどのように記述すればよいですか?
動詞から始めます。 ツールの機能を 1 文で説明します。 トリガー条件を説明する「Use when」で始まる 2 番目の文を追加します。 実装の詳細はスキップします。 例: 「ドメインの DNS レコード (A、AAAA、MX、TXT、CNAME、NS) をクエリします。DNS 構成を確認するか、ドメイン解決のトラブルシューティングを行う必要がある場合に使用します。」

botoiで開発を始めよう

150以上のAPIエンドポイント。検索、テキスト処理、画像生成、開発者ユーティリティに対応。無料プラン、クレジットカード不要。