ブログ
MCP

公開日: 更新日

ShouryamaanとArag

REST APIをMCPサーバーに変換する方法

Markdownで読む

REST APIの前に小さな変換レイヤーを置くことで、MCPサーバーにできます。このレイヤーはタスク単位のツールを登録し、入力を検証し、既存エンドポイントを呼び出して、範囲を限定した構造化結果を返します。REST APIとそのデータモデルを変更する必要はありません。

まずは読み取り専用タスクを一つ選びます。エンドポイントの境界がエージェントにとって適切か確認する前に、すべてのエンドポイントからツールを生成しないでください。

エンドポイント一覧ではなくタスクから始める

OpenAPIドキュメントは一覧として役立ちますが、エンドポイントとツールを機械的に1対1で変換すると、選択肢の多すぎるツールセットになりがちです。「注文1842はどこ?」に答えるエージェントには注文状態を調べるアクションが必要です。内部検索、配送情報の取得、状態コード変換を別々のツールで推論する必要はありません。

タスク、その完了に必要な最小限の入力、最小限の有用な結果を書き出します。その契約をhandler内の一つ以上のREST呼び出しへ対応付けます。信頼できるMCPツールの設計ガイドでは、命名とツール境界を詳しく扱っています。

型付きMCPツールを登録する

現在のMCP TypeScript SDKはMcpServer、registerTool、createMcpHandlerを公開しています。次の例では、GET /orders/:idを読み取り専用のget-order-statusツールとしてラップします。

import { createMcpHandler, McpServer } from '@modelcontextprotocol/server';
import * as z from 'zod/v4';

const OrderStatus = z.object({
  orderId: z.string(),
  status: z.enum(['processing', 'shipped', 'delivered', 'cancelled']),
  updatedAt: z.string(),
});

const handler = createMcpHandler(({ authInfo }) => {
  const server = new McpServer({ name: 'orders', version: '1.0.0' });

  server.registerTool(
    'get-order-status',
    {
      description: 'Get the current status of one order the caller can access.',
      inputSchema: z.object({
        orderId: z.string().min(1).describe('The order identifier, such as 1842'),
      }),
      outputSchema: OrderStatus,
    },
    async ({ orderId }) => {
      const response = await fetch(
        `${process.env.ORDERS_API_URL}/orders/${encodeURIComponent(orderId)}`,
        {
          headers: {
            Authorization: `Bearer ${process.env.ORDERS_API_TOKEN}`,
            'X-Caller-ID': authInfo?.clientId ?? 'unknown',
          },
        },
      );

      if (!response.ok) {
        return {
          isError: true,
          content: [{
            type: 'text',
            text: `Order lookup failed with HTTP ${response.status}`,
          }],
        };
      }

      const order = OrderStatus.parse(await response.json());
      return {
        structuredContent: order,
        content: [{ type: 'text', text: JSON.stringify(order) }],
      };
    },
  );

  return server;
});

export default handler;

出力スキーマにより、クライアントは安定した結果shapeを得られます。テキスト形式も返すことで、構造化ツール結果に未対応のクライアントとの互換性を維持します。MCPツール仕様は、構造化contentがある場合に両方を返すことを推奨しています。

handlerの前に認証を置く

createMcpHandlerはbearer tokenを自動検証しません。リクエストがhandlerへ届く前に呼び出し元を検証し、検証済みのIDをauthInfoから渡します。上流のRESTサービスでも、そのIDまたはさらに狭いサービス認証情報を認可する必要があります。呼び出し元IDのheaderは帰属の記録に役立ちますが、それだけでは認可になりません。

プロバイダーのtokenはサーバーに保持します。ツールの引数、結果、プロンプト、クライアントから見えるエラーへ含めないでください。書き込みツールでは許可されたアクションだけに認証情報を制限し、破壊的または金銭的な操作には承認を求めます。MCPインフラのセキュリティガイドで、この権限境界を説明しています。

エージェントが対処できるエラーを返す

HTTP statusだけでは、次に何をすべきかエージェントへ伝えられない場合がほとんどです。既知の失敗を、短く安定した理由へ変換します。

RESTの失敗 MCPの結果 エージェントのアクション
400 不正な注文ID invalid_order_id 再試行前に引数を修正
401または403 not_authorized 停止し、別のスコープを要求
404 order_not_found IDを確認するかユーザーへ質問
429 再試行時刻付きのrate_limited 一度待って再試行
5xx upstream_unavailable 操作が安全な場合だけ再試行

変更操作では冪等性キーを受け取り、REST APIへ転送します。サーバーが書き込みを完了した後にレスポンスだけが失われた場合、再試行ラッパーを追加しても冪等でないエンドポイントを安全にはできません。

MCPクライアントからツールをテストする

REST handlerを直接呼ぶのではなく、登録済みの契約をテストします。成功する呼び出し一つと、エージェントの動作を変える失敗分類を網羅します。

  • 欠落または不正な入力をスキーマが拒否する。
  • ある呼び出し元が別の呼び出し元の注文を取得できない。
  • 上流のタイムアウトがシークレットを含まない限定的なエラーを返す。
  • 一つの冪等性キーで書き込みを繰り返しても効果が一つになる。
  • 破壊的アクションが承認境界で停止する。

SDKのHTTP servingガイドは、DNS rebindingを防ぐため、ローカルサーバーにもHostとOriginの検証を求めています。SDKのframework factoryはlocalhost上でこれらのguardを有効にします。通常のNodeサーバーでは明示的に追加する必要があります。

デプロイ前に一つの読み取り専用ツールを検証する

よくあるユーザーの質問一つに答えるエンドポイントを選びます。入力と出力のスキーマを定義し、実際のMCPクライアントから呼び出し、ツール結果全体を調べます。認可と失敗のテストに合格してからデプロイしてください。観測したタスクを最初の契約だけでは適切に完了できないときに、次のツールを追加します。

最初のツールがこれらの確認に耐えたら、以下のプロンプトでエージェントへデプロイを任せます。今日は一つで十分です。