0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

MCPとAPI、どっちを使う? 既存APIをMCPにする意味

0
Posted at

MCPとAPI、どっちを使う? 既存APIをMCPにする意味

はじめに:「APIがあるのに、MCPって要るの?」

AIエージェントの話になると、必ずと言っていいほど MCP(Model Context Protocol) が出てきます。GitHubもNotionもSlackも「MCPサーバーを公開しました」と発表していて、社内でも「うちのAPIもMCP対応したほうがいいのでは?」という声が上がり始めているかもしれません。

一方で、こんな疑問を持つ人も多いはずです。

  • システム同士をつなぐなら、もうAPIがある。MCPは何が違うのか
  • 既存のAPIをわざわざMCPに作り直す意味はあるのか
  • 全部MCPに寄せるべきなのか、使い分けるべきなのか

先に結論を書きます。MCPはAPIの置き換えではありません。 既存のAPIの手前に「AIが使うための入口」を1枚足す仕組みです。そのため判断の軸は「APIかMCPか」の二択ではなく、そのシステムを誰が呼ぶのかになります。決まった手順で動くプログラムが呼ぶならAPI、その場で考えながら動くAIが呼ぶならMCP、というのが基本線です。

次の図で、この違いを先に見ておきましょう。左はプログラムがAPIを呼ぶ流れ、右はAIがMCPを通して同じAPIを呼ぶ流れです。

左はプログラムが決まった順番で在庫APIを呼ぶ流れ。右はユーザーの質問を受けたAIがツールの説明を読んでMCPサーバーを選び、MCPサーバーが同じ在庫APIを呼ぶ流れ

図1:どちらも最後は同じAPIに届きます。違うのは「呼ぶ順番を、開発時に人が決めるか、実行時にAIが決めるか」です。(概念図/静止画)

この記事では、MCP公式サイトとAnthropicの公式ドキュメント・ブログを2026年9月25日時点で読み直し、次の順で整理します。

  1. APIとMCPの違いを、比較表で整理する
  2. 既存APIをMCP化すると「得るもの」と「払うもの」
  3. どちらを選ぶかの判断フローと、12の具体ケース
  4. 同じAPIを「直接」「Claude Code経由」「Codex経由」で呼んだ実測
  5. 既存APIをMCP化するときの設計のコツと、セキュリティの注意

想定読者は、APIという言葉は知っているけれどMCPはまだ触ったことがない方から、社内でMCP対応を検討しているエンジニアまでです。実験のコードはNode.jsだけで動く短いものを載せているので、手元で再現もできます。

この記事の確認範囲

種類 内容
公式情報 modelcontextprotocol.io(仕様の最新版は 2026-07-28)、Anthropicの公式ドキュメント・エンジニアリングブログ・claude.comブログ。出典は記事末尾にまとめています
実測 筆者のMac(macOS)で、Claude Code 2.1.280(モデルはOpus 5.5)と Codex CLI 0.156.1 を使用。2026年9月25日に実施
画像 「概念図」は説明のための図、「実際の出力を再描画」は端末に出た文字をそのまま画像に描き直したもの(OSのスクリーンショットではありません)

そもそもAPIとMCPは何か

API:プログラム同士の「窓口」

API(Application Programming Interface) は、あるシステムの機能を外から使うための窓口です。Webの世界では、GET /api/products?q=ノート のようなURLにリクエストを送ると、決まった形のJSONが返ってくる REST API が主流です。

APIを使うときは、開発者が仕様書(OpenAPIの定義やドキュメント)を読み、「まず商品を検索して、そのIDで在庫を聞く」といった手順をコードに書きます。いったん書けば、プログラムは毎回同じ順番で、同じ形のデータを、ミリ秒単位で処理します。この 速さ・安さ・再現性 がAPIの強みです。

MCP:AIアプリと外部システムをつなぐ「共通規格」

MCP は、Anthropicが2024年11月に公開した、AIアプリと外部のデータ・ツールをつなぐためのオープンな規格です。公式サイトでは「AIアプリにとってのUSB-Cポート」とたとえられています。発表時の問題意識は、データ源を1つ足すたびに個別の連携実装が必要で、規模を広げにくいことでした。2025年12月にはLinux Foundation傘下の Agentic AI Foundation に寄贈され、現在は特定の1社ではなく中立の団体のもとで仕様が決められています。

登場人物は3つです。

役割 何をするか 例
ホスト AIが動いているアプリ。サーバーごとに接続係(クライアント)を持つ Claude、ChatGPT、Claude Code、Codex、VS Code
MCPサーバー 外部システムの機能を「ツール」としてAIに見せる窓口 在庫MCP、GitHub MCP、Notion MCP
外部システム 実際のデータや機能 在庫API、データベース、SaaS

MCPサーバーがAIに渡すものの中心は ツール(tools) です。ツールには「名前」「何をするかの説明」「入力の型(JSON Schema)」が付いていて、AIはこれを読んで、どのツールをどんな引数で呼ぶかを自分で決めます。

ここで大事なのは、MCPサーバーの中身の多くは、既存のAPIを呼ぶコードだということです。MCP公式のチュートリアルからして、米国気象局の公開APIを包んで「警報を取る」「天気予報を取る」の2つのツールにする、という題材になっています。つまりMCPは、APIの「上」に乗る層です。

既存のREST APIを中心に、左上からは自社アプリ・夜間バッチ・取引先システムが直接呼び、右上からはClaude・ChatGPT・VS CodeなどのAIアプリがMCPサーバーを経由して呼ぶ構成図

図2:既存の利用者はこれまでどおりAPIを直接呼び、AIアプリだけがMCPサーバーを通ります。認証や業務ルールはAPI側に残すのが基本です。(概念図/静止画)

APIとMCPの違いを表で整理する

では、具体的に何が違うのでしょうか。いちばん本質的な違いは、使い方を「いつ」「誰が」知るかです。

APIでは、開発者が開発時に仕様書を読み、呼ぶ手順をコードに固定します。MCPでは、AIアプリが実行時にサーバーへ「どんなツールがありますか」と問い合わせ(tools/list)、返ってきた説明を読んで、その場で使い方を判断します。サーバー側がツールを足したり説明を直したりしても、AIアプリ側を作り直す必要はありません。

上段のAPIは、人が仕様書を読む・コードを書く・テストして配布・実行の4段階で、開発時に手順が決まる。下段のMCPは、サーバーに接続・ツール一覧を受け取る・AIが選ぶ・呼び出すの4段階で、実行時に決まる

図3:APIは「開発時に決める」、MCPは「実行時に決まる」。この差が、柔軟さと再現性のトレードオフになります。(概念図/静止画)

この違いを軸に、主な観点を並べると次のようになります。

観点 API(プログラムから直接) MCP(AIアプリから)
主な呼び手 人が書いたプログラム AIモデル(ユーザーの依頼を受けて)
使い方を知るタイミング 開発時(人が仕様書を読む) 実行時(tools/list でAIが説明を読む)
呼ぶ順番を決めるのは コード(毎回同じ) AI(依頼や途中結果で変わる)
結果の受け取り方 決まった形のJSONをプログラムが処理 テキストなどをAIが読み、要約・判断に使う
1回の速さと費用(本記事の実測) 約0.035秒・0円 約6.3秒・約0.013ドル(AIの利用料)
再現性 高い(同じ入力なら同じ処理) 中程度(結論は同じでも手順や言い回しが揺れる)
規格が決めている範囲 各社の設計次第(RESTやOpenAPIは書き方の慣習・記述形式) AIアプリとサーバーの接続方法そのもの(JSON-RPC 2.0、stdio/HTTP)
認証 APIキー、OAuthなど各社バラバラ ローカルは環境変数など、リモートはOAuth 2.1ベースの方式を仕様で規定
得意な作業 大量・定期・高速・確実な処理 探索的な調べもの、複数システムをまたぐ判断、自然言語での操作
追加で必要な運用 なし(API自体の運用のみ) MCPサーバーの運用、ツール設計、AI経由の操作への安全対策

表の「速さと費用」は、後半の実験で測った値です。もちろんAIの料金やモデルによって変わりますが、桁が2つ以上違うことは覚えておくとよいでしょう。この差を払ってでも「AIがその場で判断してくれる」価値がある作業かどうか。これが使い分けの出発点です。

既存APIをMCPにすると「何がうれしいのか」

ここからが本題です。既存のAPIがあるのに、わざわざMCPサーバーを用意するメリットは何でしょうか。公式情報と実験から、大きく4つに整理できます。

メリット1:一度作れば、対応するどのAIアプリからも使える

MCPがない世界では、自社の在庫システムをClaudeから使いたければClaude向けに、ChatGPTから使いたければChatGPT向けに、自社のAIアプリから使いたければそれ向けに、別々に連携コードを書くことになります。AIアプリが3つ、つなぎたいサービスが4つなら、組み合わせは12通りです。

MCPなら、サービス側はMCPサーバーを1回作るだけ、AIアプリ側もMCPに1回対応するだけで、全部がつながります。掛け算が足し算になるわけです。

左はClaude・ChatGPT・自社AIアプリの3つが、在庫API・顧客CRM・チケット管理・社内Wikiの4つと個別に12本の線でつながる図。右は3つのAIアプリがMCPという共通の規格を通して4つのMCPサーバーにつながり、実装は7つで済む図

図4:MCPなしだと3×4=12通り、MCPありなら3+4=7つ。サービスやAIアプリが増えるほど差が開きます。(概念図/静止画)

2026年9月時点で、MCP公式サイトやAnthropicの発表に名前が挙がっている対応アプリには、Claude(チャット・Desktop・Claude Code)、ChatGPT、Codex、VS Code(GitHub Copilot)、Cursor、Gemini、Microsoft Copilot などがあります。後半の実験でも、Claude Code用に作ったMCPサーバーを、1行も変えずにCodex CLIから使えました。

メリット2:使い方をAIが自分で読み取る

APIをAIに使わせるには、本来「このエンドポイントはこういう意味で、この順番で呼んで、エラーのときはこうして」という説明を、誰かがAIに教え込む必要があります。MCPでは、その説明をサーバー側がツールの description と入力の型として持ち、AIアプリに実行時に渡します。

つまり、APIの「使い方の説明書」をAI向けに書き直して、API本体と一緒に配れるのがMCPです。サーバーを更新すれば、次にAIアプリが一覧を取得したときに新しい説明が反映されます。

メリット3:自然な言葉で、画面を知らない人も使える

APIは開発者向けの窓口です。営業やサポートの担当者が「今月、品切れになっている商品は?」を知りたくても、APIを直接叩くことはできません。これまでは、そのための管理画面を別に作る必要がありました。

MCPでAIアプリにつないでおけば、ClaudeやChatGPTのチャット欄で、普段の言葉で聞くだけで済みます。さらにAIは複数のツールを組み合わせられるので、「品切れの商品を調べて、仕入れ担当向けのチケットを起票して」のような、複数システムをまたぐ作業もひと続きで進められます。

メリット4:権限と「人の承認」をはさめる

AIにシステムを触らせるとき、いちばん怖いのは勝手な書き込みです。MCPのツールには「読み取り専用」「破壊的な操作」といった目印(annotations)を付けられ、Claude Codeなどのアプリ側では「どのツールを自動で許可するか」を設定できます。実験でも、読み取りツールだけを許可した状態で注文を頼むと、AIは注文ツールを呼ぼうとして止められ、理由を説明して終わりました(後述)。

払うもの:速さ・費用・再現性・運用

もちろん、タダではありません。MCP経由でAIに作業させると、1回ごとにAIの推論が入るので数秒かかり、利用料もかかります。手順や言い回しは毎回少しずつ変わります。

AnthropicとMCP公式は、ツールの定義をすべて最初にAIへ読み込ませると、それだけで作業メモリ(コンテキスト)を大量に消費する点も繰り返し注意しています。Anthropicのエンジニアリングブログでは、ツールをコードのAPIとして見せ、必要な定義だけを読む方式に変えた例で、約15万トークンが約2千トークン(98.7%減) になったと紹介されています。Claude APIのドキュメントでも、ツールが30〜50個を超えると正しいツールを選ぶ精度が落ちると書かれています。

左の「得るもの」には、どのAIからも使える、使い方をAIが読み取る、自然な言葉で使える、他の道具と組み合わせられるの4つ。右の「払うもの」には、1回ごとに数秒と利用料、手順と言い回しが毎回同じとは限らない、ツール定義が作業メモリを使う、運用とセキュリティの対象が増えるの4つ

図5:得るものと払うものを並べると、「人やAIがその場で考えながら使う作業」ほどMCP化の価値が大きいことが分かります。(静止画)

どちらを選ぶ? 判断フローと12の具体ケース

4つの選択肢から選ぶ

実は、選択肢は「APIかMCPか」の2つではありません。AnthropicのClaude公式ブログ(2026年4月)は、エージェントを外部システムにつなぐ方法を「直接API呼び出し」「CLI」「MCP」に分けて整理しています。要点は次のとおりです。

  • 直接API呼び出し:1つのエージェントが1つのサービスとやり取りする程度で、使い回す予定がない場合
  • CLI:ローカルやサンドボックスでは有効。ただしクラウドやスマホのアプリからは使いにくい
  • MCP:クラウドで動く本番のエージェント向け。複数のクライアントで使い回せ、OAuthによる認証も標準化される

さらにClaude Codeのドキュメントは、gh(GitHub)や aws、gcloud のようなCLIがあるなら、ツール一覧を読み込まない分コンテキスト効率が良いのでCLIを優先するよう勧めています。

これを踏まえ、「APIを直接呼ぶ」「AIのツール定義に直接書く」「CLIを使わせる」「MCPサーバーにする」の4つから選ぶフローにまとめました。

判断フロー。Q1で呼ぶ順番と処理が事前に決まっていればAPIを直接呼ぶ。いいえならQ2で、AIに使わせるのが自分のアプリだけでツールも数個ならツール定義の直書き。いいえならQ3で、手元の開発環境だけで使い良いCLIがあるならCLI。それ以外はMCPサーバーにする

図6:上から順に3つの質問に答えると、4つの選択肢のどれが合うかが分かります。1つに絞る必要はなく、併用が普通です。(静止画)

2番目の「ツール定義を直書き」は見落とされがちですが、MCPを使わずにAIにAPIを使わせる正攻法です。Claude APIのMessages APIでは、tools にツールの名前・説明・入力の型を書けば、Claudeがそれを呼ぶべきタイミングで tool_use を返してくれます。実行するのは自分のアプリです。

次の例は、在庫APIをClaude APIのツールとして直接定義する場合のイメージです(この記事では実行していません。書式はClaude APIのツール利用ドキュメントに基づきます)。

{
  "name": "find_products",
  "description": "商品名のキーワードで商品を探し、価格と現在の在庫数をまとめて返す。",
  "input_schema": {
    "type": "object",
    "properties": {
      "keyword": { "type": "string", "description": "商品名に含まれる語" }
    },
    "required": ["keyword"]
  }
}

見比べると分かるとおり、中身はMCPサーバーが tools/list で返す情報とほぼ同じです。違いは その定義を誰が持ち、どこで配るか です。自分のアプリの中だけで使うならアプリに直書きすれば十分で、複数のAIアプリやチームで共有したくなった時点でMCPサーバーに切り出す、という順番で考えると無駄がありません。

具体ケースで考える

判断フローを、よくある12の場面に当てはめてみます。

# ケース おすすめ 理由
1 ECサイトの決済・在庫の引き当て API ミリ秒単位の応答と確実な処理が必要。AIの判断を挟む理由がない
2 毎晩の売上集計・システム間のデータ同期 API(バッチ) 手順が固定で件数が多い。AI経由だと時間と費用が桁違いになる
3 決済サービスからの通知(Webhook)を受けて処理 API イベントに反応して決まった処理をするだけ
4 自社アプリに「問い合わせの要約と分類」機能を1つ足す API+ツール直書き 使うのは自社アプリだけで、ツールも数個。MCPサーバーを別に運用するほどではない
5 社内ヘルプデスクが、チケット・社内Wiki・在庫を横断して調べる MCP 質問ごとに調べる順番が変わる。複数システムをまたぐ
6 営業が「今月の大口顧客で返信待ちは?」とCRMに聞く MCP(リモート+OAuth) 非エンジニアがチャットから使う。利用者ごとに見える範囲を分けたい
7 自社SaaSを、顧客のClaudeやChatGPTから使ってもらいたい MCPサーバーを提供(APIも維持) 1回作れば、顧客がどのAIアプリを使っていても届く
8 開発者がClaude CodeでGitHubのIssueやPRを扱う CLI(gh)が第一候補 ローカルで完結し、ツール一覧を読み込まない分軽い。チームでPRのコメント連携などを共有したいならMCPも候補
9 数千件の注文データをAIに分析させたい AIにスクリプトを書かせてAPIを直接呼ぶ 途中結果を全部AIに読ませると重い。集計はコードに任せ、AIは結果の解釈に使う
10 IoTセンサーの値を1秒ごとに取り込む API(またはストリーミング) 高頻度・大量で、AIが1件ずつ判断する必要がない
11 障害対応で、ログ・監視・チケットを見比べながら原因を探す MCP 探索的で、次に何を見るかが途中結果で変わる
12 同じ社内システム連携を、複数チームのAIで使い回す MCP(リモート、またはプロジェクト共有の設定) 連携の実装と説明を1か所で管理できる

表を眺めると、APIが向いているのは 「手順が先に決まっていて、速さ・量・確実さが大事な処理」、MCPが向いているのは 「人やAIが、その場で考えながら進める作業」 だと分かります。そして実際のシステムでは、同じAPIに対してケース1〜3のような処理と、ケース5〜7のようなAI活用が同居します。だからこそ「APIを残したまま、MCPを足す」形になるわけです。

実験:同じ在庫APIを3通りで呼んでみる

ここまでの話を、手元で確かめてみました。架空の文房具店の「在庫API」を既存のAPIに見立て、それを次の3通りで呼びます。

  • A. プログラムから直接:Node.jsのスクリプトで、検索→在庫の順にAPIを呼ぶ
  • B. Claude Codeから:APIを包んだMCPサーバーを登録し、「ノートの品切れは?」と日本語で聞く
  • C. Codex CLIから:Bと 同じMCPサーバーのファイル を、コードを変えずに使う

実験の構成図。A・B・Cの3つの呼び出し方が、同じshop-apiに届く。BとCは同じshop-mcpサーバーを経由する。結果はAが約0.035秒、Bが約6.3秒・約0.013ドル、Cはコード変更なしで動作

図7:APIもMCPサーバーも共通。変えたのは「誰が呼ぶか」だけです。(筆者のMacで実測/静止画)

準備:既存APIのつもりの在庫API

まずは、どこにでもありそうなREST APIを用意します。商品検索、在庫確認、注文の3つのエンドポイントを持ち、注文だけはトークン認証が必要です。Node.js 18以降なら追加のパッケージなしで動きます(筆者はNode.js 22で確認)。

// shop-api/server.mjs : 実験用の「既存のREST API」(架空の文房具店)
import { createServer } from "node:http";

const PORT = Number(process.env.PORT || 8787);
const TOKEN = process.env.SHOP_API_TOKEN || "demo-token";

const products = [
  { id: "P001", name: "方眼ノート A5", price: 380 },
  { id: "P002", name: "方眼ノート B5", price: 420 },
  { id: "P003", name: "無地ノート A5", price: 350 },
  { id: "P004", name: "ゲルインクボールペン 0.5 黒", price: 150 },
  { id: "P005", name: "ゲルインクボールペン 0.5 青", price: 150 },
  { id: "P006", name: "付箋 75mm 4色", price: 290 },
];
const stock = { P001: 42, P002: 0, P003: 17, P004: 120, P005: 8, P006: 64 };
const orders = [];

const json = (res, status, body) => {
  res.writeHead(status, { "content-type": "application/json; charset=utf-8" });
  res.end(JSON.stringify(body));
};

createServer(async (req, res) => {
  const url = new URL(req.url, `http://${req.headers.host}`);
  let m;
  // 商品検索: GET /api/products?q=キーワード
  if (req.method === "GET" && url.pathname === "/api/products") {
    const q = url.searchParams.get("q") || "";
    return json(res, 200, products.filter((p) => p.name.includes(q)));
  }
  // 在庫: GET /api/products/:id/stock
  if (req.method === "GET" && (m = url.pathname.match(/^\/api\/products\/(\w+)\/stock$/))) {
    if (!(m[1] in stock)) return json(res, 404, { error: "product_not_found" });
    return json(res, 200, { id: m[1], stock: stock[m[1]] });
  }
  // 注文: POST /api/orders (Bearerトークン必須)
  if (req.method === "POST" && url.pathname === "/api/orders") {
    if (req.headers.authorization !== `Bearer ${TOKEN}`) return json(res, 401, { error: "unauthorized" });
    let raw = "";
    for await (const chunk of req) raw += chunk;
    const { productId, quantity } = JSON.parse(raw || "{}");
    if (!(productId in stock)) return json(res, 404, { error: "product_not_found" });
    if (!Number.isInteger(quantity) || quantity < 1) return json(res, 400, { error: "invalid_quantity" });
    if (stock[productId] < quantity) return json(res, 409, { error: "out_of_stock", stock: stock[productId] });
    stock[productId] -= quantity;
    const order = { orderId: `O${String(orders.length + 1).padStart(4, "0")}`, productId, quantity };
    orders.push(order);
    return json(res, 201, order);
  }
  json(res, 404, { error: "not_found" });
}).listen(PORT, () => console.error(`shop-api: http://localhost:${PORT}`));

作業用のフォルダ(ここでは api-mcp-lab)に shop-api/server.mjs として保存し、別のターミナルで起動しておきます。

# 作業フォルダを作って、在庫APIを起動する(このターミナルは開いたままにする)
mkdir -p ~/api-mcp-lab/shop-api ~/api-mcp-lab/shop-mcp
cd ~/api-mcp-lab
SHOP_API_TOKEN=demo-token node shop-api/server.mjs

A. プログラムから直接呼ぶ

直接呼ぶ側は、呼ぶ順番(検索→在庫)を開発者がコードに書きます。

// shop-api/client.mjs : APIをプログラムから直接呼ぶ例
const BASE = process.env.SHOP_API_URL || "http://localhost:8787";

async function stockList(keyword) {
  // ① 検索 → ② 見つかった商品ごとに在庫、という順番をコードで決めている
  const items = await (await fetch(`${BASE}/api/products?q=${encodeURIComponent(keyword)}`)).json();
  for (const p of items) {
    const { stock } = await (await fetch(`${BASE}/api/products/${p.id}/stock`)).json();
    console.log(`${p.id}  ${p.name}  ${p.price}円  在庫${stock}`);
  }
}

async function bench(n) {
  const t0 = performance.now();
  for (let i = 0; i < n; i++) await (await fetch(`${BASE}/api/products/P001/stock`)).json();
  const ms = performance.now() - t0;
  console.log(`${n}回: 合計 ${ms.toFixed(0)} ms / 1回あたり ${(ms / n).toFixed(2)} ms`);
}

const [arg, value] = process.argv.slice(2);
if (arg === "--bench") await bench(Number(value || 1000));
else {
  const t0 = performance.now();
  await stockList(arg || "ノート");
  console.log(`(所要 ${(performance.now() - t0).toFixed(0)} ms)`);
}
cd ~/api-mcp-lab
node shop-api/client.mjs ノート        # 「ノート」を含む商品の在庫一覧
node shop-api/client.mjs --bench 1000  # 在庫APIを1000回呼んで時間を測る

端末の出力。node shop-api/client.mjs ノート で3商品の在庫が表示され、所要44ミリ秒、続けて3回実行すると37・35・35ミリ秒。1000回呼ぶベンチマークは合計1430ミリ秒、1回あたり1.43ミリ秒

画面1:APIを直接呼ぶと、検索1回+在庫3回の処理が約0.035秒。1000回呼んでも1.4秒ほどです(別の回では1.7秒)。(実際の出力を再描画)

同じ処理を何度実行しても、結果の形は変わりません。後続のプログラムがこの出力を読んで何かをする、という使い方に向いているのがよく分かります。なお、この測定は同じPCの中(localhost)で行っているので、実際のネットワーク越しのAPIではもう少し遅くなります。

B. MCPサーバーで包んで、Claude Codeから呼ぶ

次に、同じAPIの手前にMCPサーバーを置きます。ここでは仕組みが見えるように、外部パッケージを使わずに約100行で書きました。ポイントは次の3つです。

  • APIの2つのエンドポイント(検索・在庫)を、「在庫を確かめたい」という用途単位の1ツール find_products にまとめる
  • 注文ツール create_order には、読み取り専用ではないことを示す目印(annotations)を付ける
  • APIのトークンはMCPの設定(環境変数)から受け取り、AIには見せない
#!/usr/bin/env node
// shop-mcp/server.mjs : 既存の shop-api を包む、依存パッケージなしの最小MCPサーバー(stdio)
// 1行 = 1つのJSON-RPCメッセージ。ログは stderr へ(stdout はプロトコル専用)
import { createInterface } from "node:readline";

const BASE = process.env.SHOP_API_URL || "http://localhost:8787";
const TOKEN = process.env.SHOP_API_TOKEN; // 認証情報は設定(env)から受け取り、AIには見せない

const TOOLS = [
  {
    // APIの2つのエンドポイント(検索・在庫)を、「やりたいこと」単位の1ツールにまとめる
    name: "find_products",
    description:
      "商品名のキーワードで商品を探し、価格と現在の在庫数をまとめて返す。在庫確認や品切れチェックに使う。",
    inputSchema: {
      type: "object",
      properties: { keyword: { type: "string", description: "商品名に含まれる語(例: ノート、ボールペン)" } },
      required: ["keyword"],
    },
    annotations: { title: "商品と在庫を探す", readOnlyHint: true, openWorldHint: false },
  },
  {
    name: "create_order",
    description: "商品IDと数量を指定して注文を確定する(在庫が減る)。実行前に必ず find_products で在庫を確認すること。",
    inputSchema: {
      type: "object",
      properties: {
        product_id: { type: "string", description: "find_products が返した商品ID(例: P001)" },
        quantity: { type: "integer", minimum: 1, description: "注文する数量" },
      },
      required: ["product_id", "quantity"],
    },
    annotations: { title: "注文する", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
  },
];

async function findProducts({ keyword }) {
  const items = await (await fetch(`${BASE}/api/products?q=${encodeURIComponent(keyword)}`)).json();
  if (items.length === 0) return `「${keyword}」を含む商品は見つかりませんでした。別の語で試してください。`;
  const lines = [];
  for (const p of items) {
    const { stock } = await (await fetch(`${BASE}/api/products/${p.id}/stock`)).json();
    lines.push(`${p.id} | ${p.name} | ${p.price}円 | 在庫 ${stock}${stock === 0 ? "(品切れ)" : ""}`);
  }
  return lines.join("\n");
}

async function createOrder({ product_id, quantity }) {
  const res = await fetch(`${BASE}/api/orders`, {
    method: "POST",
    headers: { "content-type": "application/json", authorization: `Bearer ${TOKEN}` },
    body: JSON.stringify({ productId: product_id, quantity }),
  });
  const body = await res.json();
  // APIのエラーコードを、AIが次の行動を決められる説明文に言い換える
  if (res.status === 409) return { error: true, text: `在庫不足です(現在の在庫 ${body.stock})。数量を減らすか、別の商品を提案してください。` };
  if (res.status === 404) return { error: true, text: `商品ID ${product_id} は存在しません。find_products でIDを確認してください。` };
  if (!res.ok) return { error: true, text: `注文に失敗しました(HTTP ${res.status}: ${body.error})。` };
  return { error: false, text: `注文を受け付けました。注文番号 ${body.orderId}(${body.productId} × ${body.quantity})` };
}

const send = (msg) => process.stdout.write(JSON.stringify(msg) + "\n");
const reply = (id, result) => send({ jsonrpc: "2.0", id, result });

async function handle({ id, method, params }) {
  if (method === "initialize") {
    return reply(id, {
      protocolVersion: params.protocolVersion,
      capabilities: { tools: {} },
      serverInfo: { name: "shop-mcp", version: "0.1.0" },
    });
  }
  if (method === "ping") return reply(id, {});
  if (method === "tools/list") return reply(id, { tools: TOOLS });
  if (method === "tools/call") {
    try {
      if (params.name === "find_products") {
        return reply(id, { content: [{ type: "text", text: await findProducts(params.arguments) }] });
      }
      if (params.name === "create_order") {
        const r = await createOrder(params.arguments);
        return reply(id, { content: [{ type: "text", text: r.text }], isError: r.error });
      }
    } catch (e) {
      return reply(id, { content: [{ type: "text", text: `APIに接続できませんでした: ${e.message}` }], isError: true });
    }
  }
  if (id !== undefined) send({ jsonrpc: "2.0", id, error: { code: -32601, message: `Unknown method: ${method}` } });
}

createInterface({ input: process.stdin }).on("line", (line) => {
  if (line.trim()) handle(JSON.parse(line));
});
console.error("shop-mcp: ready");

これを shop-mcp/server.mjs として保存し、Claude Codeに読み込ませる設定ファイル mcp.json を作ります。<作業フォルダの絶対パス> は、自分の環境のパス(例: /Users/yourname/api-mcp-lab)に置き換えてください。

{
  "mcpServers": {
    "shop": {
      "command": "node",
      "args": ["<作業フォルダの絶対パス>/shop-mcp/server.mjs"],
      "env": { "SHOP_API_URL": "http://localhost:8787", "SHOP_API_TOKEN": "demo-token" }
    }
  }
}

そして、Claude Codeを1回だけ実行するヘッドレスモード(-p)で聞いてみます。今回は計測のため、MCPツール以外の道具を外し(--tools "")、読み取りツールだけを自動許可しています。--setting-sources project は、筆者の個人設定が結果に混ざらないようにするための指定です。

cd ~/api-mcp-lab
claude -p "ノートの在庫を確認して、品切れのものがあれば教えて。" \
  --setting-sources project --mcp-config mcp.json --strict-mcp-config \
  --tools "" --allowedTools "mcp__shop__find_products" --output-format json

端末の出力。Claude Codeが「品切れは方眼ノート B5(P002)の1点です」と答え、3商品の在庫を表にまとめている。計測値は duration_ms 6521、num_turns 2、total_cost_usd 0.0132

画面2:日本語で聞くだけで、AIが find_products を選んで呼び、表にまとめて答えました。3回実行して、所要時間の中央値は約6.3秒、費用は1回約0.013ドル(Claude Codeが報告するAPI換算額)。(実際の出力を再描画)

3回とも「方眼ノート B5 が品切れ」という結論は同じでしたが、前置きの文や締めの一文は毎回少しずつ違いました。人が読むには問題ありませんが、この出力を別のプログラムが読み取る用途には向きません。これが「再現性は中程度」の中身です。

時間と費用の差も見ておきましょう。直接呼び出しの約0.035秒に対して、MCP経由は約6.3秒。もし「1000回の在庫確認」を同じやり方でAIにさせたら、単純計算で約1時間45分・約13ドルかかります(実測ではなく1回の値を掛けた概算です)。決まった処理を大量に回すならAPIを直接、という判断はこの数字だけでも納得できるはずです。

書き込みは「人の許可」で止められる

次に、読み取りツールだけを許可したまま「注文して」と頼んでみました。

端末の出力。読み取りツールだけ許可した状態で「方眼ノート A5 を3冊注文して」と頼むと、Claudeは在庫39冊を確認したうえで、create_order の実行許可がないため注文していないと説明し、permission_denials に create_order が記録された。注文ツールも許可した別の実行では「無地ノート A5 を5冊」の注文が完了し、注文番号O0002が返った

画面3:上は注文ツールを許可していない状態。AIは注文ツールを呼ぼうとしましたが、Claude Codeが止め、記録(permission_denials)に残りました。下は注文ツールも許可した実行で、実際にAPIへ注文が入りました。(実際の出力を再描画)

ここで押さえておきたいのは、止めたのはAIの良心ではなく、アプリ側の許可設定だという点です。MCPの仕様でも、ツールの目印(annotations)はあくまで「ヒント」で、信頼できないサーバーから来たものを鵜呑みにしてはいけないとされています。本当に守りたいものは、AIアプリの許可設定と、API側の認可(トークンの権限)の二重で守ります。

フロー図。ユーザーの注文依頼を受け、AIは読み取りツール find_products を自動で実行し、次に書き込みツール create_order を呼ぼうとする。AIアプリの許可チェックで、未許可なら実行せず理由を説明、許可ありならAPIに注文して注文番号が返る

図8:書き込み系のツールは、AIアプリの許可チェックを通るまで実行されません。対話で使うClaude Codeなら、ここで画面に確認が出ます。(実験をもとにした概念図/静止画)

C. 同じMCPサーバーをCodex CLIから使う

最後に、メリット1の「一度作れば、どのAIアプリからも使える」を確かめます。Codex CLIに、Claude Codeで使ったのと同じ shop-mcp/server.mjs を渡しました。Codexの設定ファイルは書き換えず、-c オプションでその実行のときだけ指定しています。

cd ~/api-mcp-lab
# Gitリポジトリの外で実行するため --skip-git-repo-check を付ける
codex exec --skip-git-repo-check \
  -c 'mcp_servers.shop.command="node"' \
  -c "mcp_servers.shop.args=[\"$PWD/shop-mcp/server.mjs\"]" \
  -c 'mcp_servers.shop.env={SHOP_API_TOKEN="demo-token"}' \
  "ノートの在庫を確認して、品切れのものがあれば教えて。shopのMCPツールを使うこと。"

端末の出力。Codex CLIが shop の find_products を呼び出して完了し、「方眼ノート B5(P002)が品切れ」と回答。サーバーが受け取った initialize の protocolVersion は、Claude Code 2.1.280 が 2025-11-25、Codex CLI 0.156.1 が 2025-06-18

画面4:サーバーのコードは1行も変えずに、Codexでも同じツールが呼ばれ、同じ答えが返りました。(実際の出力を再描画。筆者の個人設定に由来する出力は省略)

面白いのは画面4の下の行です。サーバーが受け取った最初のメッセージを記録したところ、Claude Code 2.1.280 は仕様の版 2025-11-25 を、Codex CLI 0.156.1 は 2025-06-18 を名乗ってきました。アプリによって対応している版が違っても、今回のサンプルは「相手の版をそのまま返す」作りなので両方で動きました。

ただし、これは学習用の割り切りです。MCPの最新仕様 2026-07-28 では、接続時のあいさつ(initialize)をやめて毎回のリクエストを独立させる「ステートレス化」など、大きな変更が入っています。実際に運用するサーバーは、公式のSDK(TypeScript・Pythonなど)で作り、版の交渉や新しい仕様への追従はSDKに任せてください。

既存APIをMCPにするときの設計のコツ

実験では、MCPサーバーの作り方で結果が変わることも確かめました。

エンドポイントを「1対1」で包まない

OpenAPIの定義からMCPサーバーを自動生成するツールもありますが、エンドポイントをそのままツールにするのが最善とは限りません。Anthropicのエンジニアリングブログ「Writing effective tools for agents」は、既存の機能やAPIエンドポイントをただ包んだだけのツールを、よくある失敗として挙げています。代わりに勧めているのは、find_availability と create_event を schedule_event にまとめるような、AIがやりたいこと単位のツールです。

そこで、同じ在庫APIを「1対1」で包んだ比較用のサーバー(search_products と get_stock の2ツール)も作り、同じ質問を3回ずつ投げてみました。

左は1対1で包んだ場合で、search_products で検索したあと get_stock を3回呼び、AIが自分で突き合わせる。実測5ターン・約8.9秒。右はやりたいこと単位でまとめた find_products で、サーバーの中で検索と在庫を済ませて1回で返す。実測2ターン・約6.3秒

図9:答えの正しさは同じでも、1対1だとAIとの往復が増え、時間も延びました(3回の中央値)。(静止画)

設計 ツール ターン数 所要時間(中央値) 費用(2〜3回目)
1対1で包む search_products / get_stock 5 約8.9秒 約0.020ドル
用途単位でまとめる find_products 2 約6.3秒 約0.013ドル

1対1の1回目は作業メモリへの書き込み(キャッシュ作成)が入り約0.042ドルだったため、費用は2〜3回目の値を載せています。商品が3件なので差は数秒ですが、件数が増えるほど往復も増えます。

設計チェックリスト

既存APIをMCP化するときに、筆者が実験と公式情報から押さえておきたいと考えた点を表にまとめます。

観点 やること 理由
ツールの単位 「在庫を確かめる」「注文する」など、利用者の意図ごとにまとめる 往復が減り、AIが選びやすい
ツールの数 必要なものに絞る。多い場合は名前に接頭辞を付ける 数が多いと選択精度が落ち、作業メモリも使う
説明文 何に使うか、いつ使うか、前提(例:注文前に在庫確認)を書く AIが読むのは説明文だけ
返す内容 生のJSONより、AIが読みやすい要約(ID・名前・状態)を返す。量は絞る 作業メモリの節約と誤読の防止
エラー 409 を「在庫不足。数量を減らすか別の商品を」のように、次の行動が分かる文にする AIが自分で立て直せる
読み書きの分離 読み取りと書き込みを別ツールにし、目印(annotations)を付ける アプリ側で「読み取りだけ自動許可」ができる
業務ルール 認証・権限・入力チェックはAPI側に残す MCPを通らない利用者にも同じルールが効く

セキュリティ:MCPにしたから安全、ではない

MCPは便利な分、AIに「手」を渡す仕組みでもあります。公式の注意点のうち、既存APIを包むときに特に関係するものを整理します。

リスク どういうことか 対策
認証情報の漏えい APIキーをツールの引数や返り値に含めると、AIの会話に残る ローカル(stdio)のサーバーは環境変数から読む。今回のサンプルもAIにトークンを見せていない
トークンの素通し 利用者から受け取ったトークンを、そのまま下流のAPIに渡す MCP仕様で禁止(Token Passthrough)。リモートのサーバーはOAuth 2.1ベースの方式で、自分宛てのトークンだけを受け付ける
権限の取り違え 代理で動くサーバーが、本来の利用者以上の権限でAPIを操作してしまう(Confused Deputy) 利用者ごとの同意と、最小限の権限から始めて必要なときに広げる
プロンプトインジェクション APIが返した外部の文章(問い合わせ本文など)に、AIへの指示が紛れ込む 書き込み系は人の承認を必須にする。信頼できるサーバーだけをつなぐ(Claude Codeのドキュメントも警告)
目印の過信 readOnlyHint などの目印を、安全の保証だと思い込む 目印はヒント。実際の制御はアプリの許可設定とAPIの認可で行う

特に「社内向けだから大丈夫」と考えがちな点に注意が必要です。社内のWikiやチケットにも、外部から届いたメールや問い合わせの文面が入っています。AIがそれを読む以上、書き込みを伴うツールには人の確認をはさむ設計にしておくと安心です。

よくある誤解

最後に、APIとMCPの関係でよく見かける誤解を整理します。

誤解 実際
MCPに移行すればAPIは要らなくなる MCPサーバーの中身の多くはAPI呼び出し。アプリやバッチは引き続きAPIを直接使う
MCPはAPIの新しい書き方(RESTの後継) MCPは「AIアプリと道具をつなぐ規格」。RESTのようなシステム間連携の置き換えを目指したものではない
OpenAPIがあれば、そのまま自動変換すれば十分 動くが最善とは限らない。実験でも1対1だと往復が増えた。用途単位に整えるのがおすすめ
ツールは多いほど便利 数十個を超えると選び間違いが増える。必要なものに絞るか、必要なときだけ読み込む仕組みを使う
MCPを使うならAIは何でも自動でやってくれる どのツールを自動で許可するかはアプリ側の設定次第。書き込みは承認制にできる
AIにAPIを使わせるには必ずMCPが必要 自分のアプリ1つで使うだけなら、Claude APIなどのツール定義に直接書けば足りる

まとめ

APIとMCPの使い分けは、「どちらが新しいか」ではなく「誰が呼ぶか」で決まります。

  • APIは、人が書いたプログラムが決まった手順で呼ぶための窓口。速く・安く・結果が毎回同じなので、決済やバッチ、Webhookなど「手順が先に決まっている処理」に向いています
  • MCPは、AIがその場で使い方を読み取って呼ぶための入口。既存のAPIの手前に足すもので、置き換えるものではありません
  • 既存APIをMCP化するメリットは、①一度作ればどのAIアプリからも使える、②使い方の説明をAPIと一緒に配れる、③自然な言葉で複数システムをまたいで使える、④権限と人の承認をはさめる、の4つです
  • 代わりに、1回ごとの数秒と利用料、手順の揺れ、ツール定義の重さ、運用とセキュリティの手間を払います。実測では直接呼び出し約0.035秒に対し、MCP経由は約6.3秒・約0.013ドルでした
  • 自分のアプリだけで使うならツール定義の直書き、手元の開発ならCLI、という選択肢も忘れずに。MCPは「複数のAIアプリやチームで使い回す」「非エンジニアがチャットから使う」ときに真価を発揮します

次の一歩としては、まず読み取り専用のツールを1つだけMCPサーバーにして、自分のClaude CodeやClaudeのアプリから使ってみるのがおすすめです。便利さと、数秒の待ち時間の両方を体感すると、どの業務をMCPに任せ、どこをAPIのまま残すかの判断がぐっと具体的になります。

参考リソース

MCP公式

Anthropic公式

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?