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クライアントから呼び出し、ツール結果全体を調べます。認可と失敗のテストに合格してからデプロイしてください。観測したタスクを最初の契約だけでは適切に完了できないときに、次のツールを追加します。
最初のツールがこれらの確認に耐えたら、以下のプロンプトでエージェントへデプロイを任せます。今日は一つで十分です。