MCPModel Context ProtocolAI Tools

MCP(Model Context Protocol)統合ガイド:API開発者向け

約1分

MCP登場以前、すべてのAIツールは独自のプラグインシステムを持っていました。Claude Codeにもありました。Cursorにも別のものがありました。データベースツールはClaude Codeでは動くのにCursorでは動かず、2回書く羽目になりました。ツールを切り替えるたびに、すべての統合を書き直す必要がありました。これが2024年のAIツールの状況でした——それぞれのツールが孤島であり、それぞれの統合がカスタムだったのです。

MCP(Model Context Protocol)は2025年にこれを変え、2026年にはあらゆる場所で使われるようになりました。Claude CodeはMCPを使います。Cursorも対応しています。LangChain、LiteLLM、そして主要なAIプラットフォームすべてがMCPサポートを追加しました。公式MCP仕様がプロトコルを定義しています——これはAIモデルがツールを発見・呼び出しする方法を標準化するオープン標準(JSON-RPCベース)です。MCPサーバーを1つ書けば、MCP互換のクライアントならどれでも使えます。

MCPとは何か——そしてなぜ重要なのか

プロトコルであり、マーケティングではない。 MCPはJSON-RPC 2.0ベースのプロトコルです。MCPサーバーはtool、resource、promptを公開します。MCPクライアント(AIアプリケーションに組み込まれます)はサーバーに接続し、利用可能なものを発見し、LLMに代わってツールを呼び出します。トランスポート層は差し替え可能です——ローカルツールにはstdio、リモートサービスにはHTTP+SSEです。

MCP vs function calling——補完的であり、競合ではない。 function calling:LLMは単一のAPIリクエスト内でツールを呼び出します。ツール定義はAPI呼び出しで送信されます。MCP:LLMは標準化されたプロトコルを通じて利用可能なツールを発見し、その後それらを呼び出します——複数のセッションやツールにまたがる可能性もあります。MCPはツールの発見と記述を標準化します。function callingはそれらを実行します。MCPでツールカタログを管理し、function callingでそれらを呼び出すことができます——両者は連携して動作します。OpenAI、Anthropic、Google、DeepSeekのfunction calling実装の詳細な比較は、function callingとtool useガイドをご覧ください。

API開発者にとってMCPが重要な理由。 MCP以前:データベースクエリツールを書いたとします。Claude Codeで使うにはClaude Code専用のプラグインを書きました。Cursorで使うにはCursor専用の統合を書きました。自作アプリで使うにはカスタムコードを書きました。MCP以降:MCPサーバーを1つ書くだけです。MCP互換のクライアントならどれでも使えます。これは「AIツールのUSB-C」という比喩です——完璧ではありませんが、勝ち残った標準です。

MCPアーキテクチャ:サーバー、クライアント、トランスポート

MCPサーバー。 tool、resource、promptを公開します。Python、Node.js、Goなど——お好みの言語で書けます。ローカル(stdioトランスポート)またはリモート(HTTP+SSEトランスポート)で実行されます。サーバーはプログラムです。起動し、接続を待ち受け、ツール呼び出しリクエストに応答し、クライアントが切断すると停止します。

MCPクライアント。 MCPサーバーに接続し、利用可能なツールを発見し、LLMからのツール呼び出しリクエストを送信し、結果を返します。Claude Code、Cursor、自作アプリなどのAIアプリケーションに組み込まれます。クライアントは、LLMの決定(「データベースをクエリしたい」)とツールの実行(SELECT * FROM users)の間の橋渡し役です。

トランスポート。 stdio:サーバーがクライアントのサブプロセスとして実行されます。通信には標準入出力を使用します。ローカル開発ツールに最適です——ネットワーク設定ゼロ、レイテンシーゼロ。stdioトランスポートは、最新のマシンではメッセージ1往復あたり1ms未満のオーバーヘッドしか追加しません。HTTP+SSE:サーバーがリモートサービスとして実行されます。リクエストにはHTTP POST、ストリーミングレスポンスにはServer-Sent Eventsを使用します。本番サービスに最適です——複数のクライアントが接続でき、開発者全員のIDEを再起動せずにサーバーを独立して更新できます。

アーキテクチャ図:

LLM  → MCP Client  → Transport (stdio/HTTP)  → MCP Server  → External Resources (DB, API, Filesystem)

最初のMCPサーバーを構築する

天気+株価のMCPサーバー。15分。ツールは2つ。

# mcp_server.py
import json
import sys
from typing import Any

class MCPServer:
    def __init__(self):
        self.tools = {
            "get_weather": {
                "description": "Get current weather for a city. Returns temperature in Celsius.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "city": {"type": "string", "description": "City name, e.g. 'Tokyo'"}
                    },
                    "required": ["city"]
                }
            },
            "get_stock_price": {
                "description": "Get current stock price for a ticker symbol.",
                "inputSchema": {
                    "type": "object",
                    "properties": {
                        "symbol": {"type": "string", "description": "Stock ticker, e.g. AAPL"}
                    },
                    "required": ["symbol"]
                }
            }
        }

    def handle_request(self, request: dict) -> dict:
        method = request.get("method")

        if method == "tools/list":
            return {"tools": list(self.tools.values())}

        elif method == "tools/call":
            tool_name = request["params"]["name"]
            tool_args = request["params"]["arguments"]
            result = self._execute_tool(tool_name, tool_args)
            return {"content": [{"type": "text", "text": str(result)}]}

        else:
            return {"error": f"Unknown method: {method}"}

    def _execute_tool(self, name: str, args: dict) -> Any:
        if name == "get_weather":
            return f"Weather in {args['city']}: 22°C, partly cloudy"
        elif name == "get_stock_price":
            return f"{args['symbol']}: $185.50"
        else:
            return f"Unknown tool: {name}"

# stdio transport —reads JSON-RPC from stdin, writes to stdout
if __name__ == "__main__":
    server = MCPServer()
    for line in sys.stdin:
        request = json.loads(line)
        response = server.handle_request(request)
        sys.stdout.write(json.dumps(response) + "\n")
        sys.stdout.flush()

Claude Desktopに接続する。 AnthropicのMCPクイックスタートがセットアップ全体を案内します。これをclaude_desktop_config.jsonに追加します:

{
  "mcpServers": {
    "my-tools": {
      "command": "python",
      "args": ["mcp_server.py"]
    }
  }
}

Claude Desktopを再起動します。2つのツール——get_weatherget_stock_price——が利用可能になります。Claudeが自動的にそれらを発見します。試してみましょう:「東京の天気とAppleの株価は?」Claudeは両方のツールを並列に呼び出し、回答を合成します。

Claude APIの完全な解説——認証、ストリーミング、ツール使用、エラー処理——は、Claude API開発者ガイドをご覧ください。

本格的なMCPサーバーを構築する:データベースクエリツール

天気サーバーは、MCPがPython 50行で動作することを示しました。本番ツールにはさらに必要です:パラメータ化クエリ、コネクションプーリング、LLMが自己修正できるエラーメッセージ。ここでは、公式MCP Python SDK(pip install mcp)で構築した、SQLiteデータベースをクエリする完全なMCPサーバーを紹介します。ツールは3つ——query_usersget_user_by_idcreate_user。コピーして、適用し、デプロイしてください。

# db_mcp_server.py —production-grade MCP database server
import sqlite3
import asyncio
from mcp.server import Server, NotificationOptions
from mcp.server.models import InitializationCapabilities
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent

server = Server("database-tools")

# Database connection —created once, reused across all tool calls
_connection: sqlite3.Connection | None = None

def get_db():
    global _connection
    if _connection is None:
        _connection = sqlite3.connect("app.db")
        _connection.row_factory = sqlite3.Row
        # WAL mode: concurrent reads without locking. Without this,
        # two simultaneous query_users calls serialize —the second
        # blocks until the first releases the read lock.
        _connection.execute("PRAGMA journal_mode=WAL")
    return _connection

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [
        Tool(
            name="query_users",
            description="Run a SELECT query against the users table. "
                        "Use for searching, filtering, or counting users. "
                        "Returns results as JSON array. "
                        "Column names: id, name, email, role, status, created_at.",
            inputSchema={
                "type": "object",
                "properties": {
                    "where_clause": {
                        "type": "string",
                        "description": "SQL WHERE clause without 'WHERE' keyword. "
                                       "Example: \"age > 25 AND status = 'active'\". "
                                       "Leave empty for all users."
                    },
                    "limit": {
                        "type": "integer",
                        "description": "Max rows to return. Default 50.",
                        "default": 50
                    }
                }
            }
        ),
        Tool(
            name="get_user_by_id",
            description="Fetch a single user by primary key. Returns user object or null.",
            inputSchema={
                "type": "object",
                "properties": {
                    "user_id": {
                        "type": "integer",
                        "description": "The user's ID in the database."
                    }
                },
                "required": ["user_id"]
            }
        ),
        Tool(
            name="create_user",
            description="Insert a new user into the database. "
                        "Returns the created user with their assigned ID.",
            inputSchema={
                "type": "object",
                "properties": {
                    "name": {"type": "string", "description": "User's full name."},
                    "email": {"type": "string", "description": "Valid email address."},
                    "role": {
                        "type": "string",
                        "enum": ["admin", "editor", "viewer"],
                        "description": "User role. Default: viewer."
                    }
                },
                "required": ["name", "email"]
            }
        )
    ]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    db = get_db()

    if name == "query_users":
        where = arguments.get("where_clause", "")
        limit = arguments.get("limit", 50)

        query = "SELECT * FROM users"
        params = []
        if where.strip():
            query += " WHERE " + where
        query += " LIMIT ?"
        params.append(limit)

        try:
            rows = db.execute(query, params).fetchall()
        except sqlite3.OperationalError as e:
            # Return column names so the LLM can self-correct bad WHERE clauses
            return [TextContent(
                type="text",
                text=f"Query failed: {e}. Valid columns: id, name, email, role, status, created_at."
            )]

        return [TextContent(
            type="text",
            text=str([dict(r) for r in rows])
        )]

    elif name == "get_user_by_id":
        user_id = arguments["user_id"]
        row = db.execute(
            "SELECT * FROM users WHERE id = ?", [user_id]
        ).fetchone()
        if row is None:
            return [TextContent(type="text", text=f"No user found with id={user_id}")]
        return [TextContent(type="text", text=str(dict(row)))]

    elif name == "create_user":
        name_val = arguments["name"]
        email = arguments["email"]
        role = arguments.get("role", "viewer")

        try:
            cursor = db.execute(
                "INSERT INTO users (name, email, role) VALUES (?, ?, ?)",
                [name_val, email, role]
            )
            db.commit()
            new_id = cursor.lastrowid
        except sqlite3.IntegrityError as e:
            return [TextContent(
                type="text",
                text=f"Insert failed: {e}. Email may already exist."
            )]

        new_user = db.execute(
            "SELECT * FROM users WHERE id = ?", [new_id]
        ).fetchone()
        return [TextContent(type="text", text=str(dict(new_user)))]

    return [TextContent(type="text", text=f"Unknown tool: {name}")]

async def main():
    async with stdio_server() as (read_stream, write_stream):
        await server.run(
            read_stream,
            write_stream,
            InitializationCapabilities(
                sampling={},
                experimental={},
                roots={}
            ),
            notification_options=NotificationOptions()
        )

if __name__ == "__main__":
    asyncio.run(main())

このコードには、午前3時のオンコールページャーを防ぐ4つの判断があります。

例を含むツールの説明。 LLMは、どのツールを呼ぶかを決めるためにツールの説明を読みます。素の"description": "Query the database"では、モデルはツールを区別する手がかりを得られません——SELECT COUNT(*)クエリでは、代わりにget_user_by_idを選ぶことがよくあります。明示的な指示がなければ、複数のツールの説明が似ているとき、モデルは間違ったツールを選ぶことがよくあります。上記の説明には、ツールが何をするか、何を返すか、正確なカラム名が含まれています。その具体性が曖昧さを排除します。

パラメータ化クエリ——f-string禁止。 db.execute("SELECT * FROM users WHERE id = ?", [user_id])は決してdb.execute(f"SELECT * FROM users WHERE id = {user_id}")になってはいけません。データベースMCPサーバーにf-stringが1つあると、user_id = "1 OR 1=1"を生成するLLMがusersテーブルの全行を漏洩させます。LLMはそのような入力を生成します。本番で実際に起きています。パラメータ化クエリは必須要件であり、おまけではありません。

回復ヒント付きのエラーメッセージ。 query_usersが、LLMが存在しないカラム名を推測したために失敗した場合、エラーには有効なカラムのリストが含まれます:"Valid columns: id, name, email, role, status, created_at." LLMはこれを読み、クエリを修正し、ツール呼び出しを再試行します——人間の介入はゼロです。素の"OperationalError: no such column"では、Claudeは肩をすくめるだけで、ユーザーは行き詰まります。

WALジャーナルモードによる接続の再利用。 PRAGMA journal_mode=WALは、ロックなしでの同時読み取りを可能にします。これがないと、2つの同時query_users呼び出しが直列化されます——2つ目は1つ目が読み取りロックを解放するまで50ms待ちます。Claudeがquery_usersを呼び、結果を読み、各行についてget_user_by_idを呼ぶツールチェーンでは、そのロック待ちが5回の連続呼び出しにわたって累積し、ユーザーから見える250msのレイテンシーになります。WALモードはこのボトルネックを排除します。

LLMプロバイダー横断でのMCP

MCPサポートはプロバイダーによって大きく異なります。Anthropicがプロトコルを生み出し、最も深い統合を持っています。他の企業はそれぞれ異なる速度で追い上げています。2026年半ば時点で各主要プロバイダーがどこにいるかを示します。

プロバイダーMCPサポートレベルトランスポート対応本番対応度SDK品質
Anthropicネイティブ —発明元stdio + HTTP/SSE両トランスポートとも本番対応公式のPython・TypeScript SDKで仕様を完全網羅
OpenAIAgents SDK経由+コミュニティ製アダプターstdio(Agents SDK)、HTTPはアダプター経由Agents SDK:本番対応。アダプター:ベータ品質コミュニティ保守;公式MCP SDKはなし
Googleコミュニティ製アダプターのみstdio、HTTP/SSEは限定対応開発のみ初期段階のコミュニティ製パッケージ;ドキュメントは乏しい
DeepSeekOpenAI互換の経路OpenAIアダプターのツールチェーン経由動作するが非サポート専用MCP SDKなし;OpenAIアダプターの癖を引き継ぐ
集約プラットフォーム(インフラ層 —以下のすべてのプロバイダーにルーティング)MCPゲートウェイ —一度接続すればどこへでもルーティングHTTP/SSE+統合認証本番対応。認証・レートリミット・ログを管理Python・JavaScript・LangChain向けプラットフォームSDK

MCPゲートウェイパターン。 集約プラットフォームはMCPクライアントとして動作し、あなたのMCPサーバーに接続し、単一のAPIエンドポイントを通じて任意のLLMにツールを公開します。MCPサーバーを1つ書きます。それをGPT-5.5、Claude Opus 4、Gemini 3、DeepSeek V3で使います——すべてPOST https://api.tokspan.com/v1/chat/completionsと同一のAPIキー経由です。プラットフォームがプロトコル変換を処理するため、どのモデルがアクティブでもMCPツールは動作します。1つのサーバー定義が6つ以上のモデルに供給します。5分のセットアップはTokSpanクイックスタートをご覧ください。

MCP vs function calling:どちらを使うべきか

判断は1つの問いに帰着します:あなたのツールは複数のLLMクライアントで動作する必要がありますか?

function calling単独を使うべき場合。 OpenAI、Anthropic、Googleなど1つのLLM APIを直接呼び、ツールが単一アプリケーション固有である場合です。ツール定義はchat completionsリクエストのtools配列に直接渡します。インフラはゼロです:別個のサーバープロセスも、トランスポート層も、プロトコルネゴシエーションもありません。GET /orders/:idという1つの内部RESTエンドポイントで注文ステータスを調べるカスタマーサポートボットには、MCPではなくfunction callingが必要です。過剰設計はやめましょう——クライアントが1つの場合、MCPはサーバープロセス、トランスポート、プロトコルハンドシェイクを追加するだけで、利益はゼロです。

MCP+function callingを併用すべき場合。 同じツールを必要とする複数のAIクライアント——ノートPCのClaude Code、同僚のCursor、ステージングサーバーのカスタムダッシュボード——がある場合です。データベースクエリツール、デプロイツール、ログビューアをMCPサーバーに一度だけ定義します。各クライアントはtools/listでそれらを発見し、実行時にはfunction callingで呼び出します。

ツールカタログが一元化されます。ツールの説明やスキーマの更新は、再デプロイなしで即座にすべてのクライアントに伝わります。このパターンを使った200人規模のエンジニアリング組織の内部プラットフォームチームは、新しいツールあたりの統合メンテナンスを8時間から30分に短縮しました。

MCPをゲートウェイとして使うべき場合。 集約プラットフォームを通じてツール呼び出しを複数のLLMプロバイダーにルーティングする場合です。1つのMCPサーバー定義が、GPT-5.5、Claude Opus 4、Gemini 3に同時にツールを供給します。プラットフォームがMCPのツール発見プロトコルと各プロバイダーのfunction calling APIの間を変換します。プロバイダー固有のツール定義を書くことはありません。ツールがClaudeでは動くのにGPT-5.5では黙って失敗する理由をデバッグすることもありません。1つのサーバー定義が単一エンドポイントを通じて6つ以上のモデルに供給します——プロバイダーごとのツール設定は不要です。

間違った選択——経験から。 典型的なシナリオを考えてみましょう:小さなスタートアップの開発者が、Claude Code専用に使う3つの内部ツールのために、HTTP/SSEトランスポート、認証ミドルウェア、コネクションプーリングを備えたMCPサーバーのセットアップに2週間を費やしました。function callingなら2時間で済んだでしょう。ちょうど1つのクライアントにしか使われないインフラのために2週間のエンジニアリング時間を費やしたのです。function callingから始めましょう。2つ目のクライアントが必要になったとき、MCPに移行してください——それより前ではありません。

本番でのMCP:認証、レートリミット、エラー処理

stdioは開発用。HTTP+SSEは本番用。 stdioトランスポートはMCPサーバーをサブプロセスとして実行します——認証なし、ネットワークセキュリティなし、サーバープロセスごとに1クライアントです。ノートPCのclaude_desktop_config.jsonには問題ありません。組織全体の50人の開発者にサービスを提供するケースでは間違いです。HTTP+SSEトランスポートなら、ロードバランサーの背後にスタンドアロンサービスとしてデプロイでき、適切な認証、モニタリング、独立したデプロイサイクルを実現できます。

認証ミドルウェア。 MCP HTTPトランスポート仕様は、2026年半ば時点で標準の認証メカニズムをまだ定義していません。自分で追加します。一般的なパターン:MCPメッセージが処理される前に、Authorization: Bearer <key>ヘッダーのAPIキーを検証します。TokSpanベースのデプロイでは、プラットフォームがAPIゲートウェイ層で認証を処理します——MCPサーバーはhttps://api.tokspan.com/v1/で認証済みのリクエストを受け取ります。セルフホスティングの場合は、MCPハンドラーの前に認証ミドルウェアを取り付けます。

tool_atool_cを呼ぶtool_bを呼ぶツールチェーンの深さ3で期限切れのトークンがあると、謎めいたJSON-RPCエラーが発生し、認証層まで遡るのに45分かかります。何よりも先に認証を正しくしましょう。完全なセキュリティチェックリストは、本番セキュリティチェックリストをご覧ください。

# Auth middleware for MCP HTTP server (FastAPI pattern)
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
VALID_TOKENS = {"mcp-secret-abc123", "mcp-prod-xyz789"}  # Replace with DB lookup

@app.middleware("http")
async def auth_middleware(request: Request, call_next):
    if request.url.path.startswith("/mcp/"):
        token = request.headers.get("Authorization", "").removeprefix("Bearer ")
        if token not in VALID_TOKENS:
            raise HTTPException(status_code=401, detail="Invalid MCP API key")
    return await call_next(request)

レートリミットがLLM増幅型インシデントを防ぐ。 説明が緩いツールが1つ——LIMIT強制のないrun_sqlツール——あるだけで、ユーザーが「全部見せて」と頼んだため、LLMは午前2時に1,000万行をスキャンするクエリを生成します。サーバー側のトークンバケットレートリミッターで、query_usersを毎分60回、create_userを毎分10回に制限します。複数サーバーインスタンスにまたがる分散レートリミットにはRedisを使います——APIキーごと・ツール名ごとにINCREXPIREです。

レートリミットがなければ、1つの熱心なLLMプロンプトが本番データベース障害の根本原因になります。午前3時にページャーが鳴ります。15分で書いて6スプリント前に忘れたツール定義に遡ることになります。

LLMの回復を助けるエラー処理。 "Error: something went wrong."を返さないでください。LLMはエラーメッセージを読んで再試行します。3つのことを返します:エラーの種類、失敗した特定のパラメータ、有効な代替案。このエラーは無価値です:"Invalid input." こちらはClaudeが1回の再試行で自己修正できます:"create_user failed: role must be one of [admin, editor, viewer]. Received: 'superadmin'." この2つのエラーメッセージの違いは、自己修復するツールチェーンと、スピナーを見つめ続けて最終的に諦めてチケットを発行するユーザーの違いです。

よくあるMCPの落とし穴:実際に何が壊れるのか

MCP開発者が最初の1か月以内にぶつかる4つの障害モードを紹介します。どれもデプロイ前に知っていれば回避できます。

曖昧なツールの説明。 これは誤ったツール選択バグの第一位の原因です。"description": "Fetch data"ではLLMに何も伝わりません。Claude Opus 4はどのツールを使うか推測しますが、複数のツールの説明が重複していると頻繁に誤ります。あなたのコードベースを見たことがない開発者にツールを説明するように説明を書きましょう。ツールが何をするか、何を返すか、有効な入力の例、いつ使うべきでないかを含めます。

上記のデータベースサーバーのquery_usersの説明は冗長ではありません——曖昧さを排除するのにちょうど正確です。

stdioバッファ制限。 stdioトランスポートはOSのパイプを使用します。Linuxのデフォルトのパイプバッファは64KBです。query_large_datasetツールが2MBのJSONを返すと、write()呼び出しがブロックします。クライアントがハングします。30秒スピナーを見つめてから、kill -9でプロセスを殺します。

64KBを超える可能性のあるツールには、レスポンスのチャンキングを実装します——pagepage_sizeパラメータで大きな結果をページに分割します。または、サーバーフレームワークレベルでレスポンスサイズ制限を設定できるHTTPトランスポートに切り替えます。すべてのツールに512KBのハードなレスポンス上限を設定しましょう。MCPサーバーのツールは、LLMが単一のコンテキストウィンドウで有用に処理できる量を超えるデータを決して返すべきではありません。

サーバー間のツール名衝突。 2つのMCPサーバーを接続するとします:データベースチームのもの(get_status——レプリケーションラグ)と運用チームのもの(get_status——デプロイヘルス)。両方がget_statusというツールを公開します。Claude Codeはサーバー名のプレフィックスを付加します:database-tools_get_statusops-tools_get_status。しかしCursorの最近のバージョンを含む一部のMCPクライアントは、tools/listに最初に応答したサーバーを黙って選びます。修正は恥ずかしいほど簡単で、普遍的に無視されています:最初からすべてのツール名をネームスペース化する。db_query_usersops_deploy_servicelogs_search_errors。2文字のプレフィックスが、サイレント障害のカテゴリー全体を防ぎます。

スキーマ検証のギャップ。 スキーマが"type": "integer"と言っているのに、LLMは"user_id": "42"(文字列)を送ります。スキーマが"minimum": 1と言っているのに、"limit": -5を送ります。スキーマが"enum": ["admin", "editor", "viewer"]と言っているのに、"role": "superadmin"を送ります。call_toolハンドラーはすべての引数を検証し、安全な場合は型を強制し(int("42") —42)、その他すべてについては具体的な検証エラーを返す必要があります。LLMがJSON Schemaを尊重すると思ってはいけません。それはしません。あなたのサーバーは、幻覚的なツール引数と本番データベースの間の最後の防衛線です。

FAQ

すでにfunction callingを使っている場合、MCPは必要ですか?

両者は補完的であり、二者択一ではありません。function callingはAPI呼び出し内でツールを実行します。MCPはアプリケーション横断でツールの発見と記述を標準化します。MCPでツールカタログ——どのツールが存在し、どう呼ぶかに関する単一の真実源——を定義します。実行時にはfunction callingで実際にそれらのツールを呼び出します。MCPはインターフェース層です。function callingは実行層です。クライアントアプリケーションがちょうど1つでツールが3つなら、MCPを飛ばしてfunction callingを直接使ってください。MCPが必要になる時が来たら分かります:Claude Codeの設定ファイルと自作アプリのtools配列の間でツール定義をコピーペーストしている自分に気づいた瞬間です。

OpenAIモデルでMCPを使えますか?

はい——アダプターまたはOpenAI Agents SDK経由で使えます。ネイティブサポートはAnthropicほど成熟していませんが、一般的なツールパターンには機能します。MCPゲートウェイ対応の集約プラットフォームを使えば、プラットフォームが変換を処理します:MCPサーバーは、OpenAI固有のMCPコードを書くことなくGPT-5.5とGPT-5.5-miniで動作します。トレードオフ:コミュニティアダプターは、新しいMCP仕様機能についてAnthropicの公式SDKよりおよそ3〜6か月遅れています。

MCPは2026年に本番利用可能ですか?

stdioトランスポート経由のローカルツール:はい。Claude Codeのstdioクライアントは、何百万時間もの開発者の利用で戦場を経験済みです。HTTP経由のリモートサービス:はい。ただし、自分で認証ミドルウェアを追加する必要があるという注意点付きです——HTTPトランスポートの認証標準はまだ進化中です。プロトコルの中核(JSON-RPCメッセージ形式、ツール発見、ツール実行)は安定しており、執筆時点では2025-03-26の仕様更新以来、破壊的な変更はありません。HTTP MCPサーバーをセルフホストする場合は、認証、レートリミット、ログ設定に2〜3日を確保してください。

MCPとAPI集約プラットフォームの関係は?

集約プラットフォームはMCPゲートウェイとして機能できます:MCPサーバーをプラットフォームに一度接続します。プラットフォームはツール呼び出しを任意のLLM——GPT-5.5、Claude Opus 4、Gemini 3、DeepSeek V3——にルーティングします。プロバイダーごとのMCP設定なしでマルチモデルのツール使用が得られます。MCPサーバー1つ。全モデル。集約プラットフォームが実際にMCPゲートウェイルーティングをどう実装するかの詳細は、TokSpanのMCP統合ドキュメントをご覧ください。

複数のMCPサーバーを1つのクライアントに接続できますか?

はい。Claude Code、Cursor、そしてほとんどのMCPクライアントは、複数のサーバーへの同時接続をサポートしています——それぞれのサーバーを一意の名前で設定ファイルに追加すれば、全サーバーのすべてのツールがLLMの利用可能なツールリストに表示されます。鋭い刃の部分:サーバー間のツール名衝突。最初からネームスペースプレフィックス(db_queryops_deploy)でツール名を付けましょう。クライアントによる曖昧さの解消に頼ってはいけません——すべてのクライアントが行うわけではなく、行うものでも一貫性がありません。

2つのMCPサーバーが同じ名前のツールを公開するとどうなりますか?

クライアントによって異なります——それが問題です。Claude Codeはサーバー名を付加して一意の識別子を作成します:database-tools_get_statusops-tools_get_status。Cursorは黙って重複排除します——tools/listに最初に応答したサーバーが勝ちです。クライアント側の曖昧さ解消にまったく頼ってはいけません。すべてのツール名にサーバー固有のネームスペースを前置しましょう。db_get_statusops_get_statusは、発生源で曖昧さを除去します。

何か問題が起きたとき、MCPツール呼び出しをどうデバッグしますか?

順番に3つの場所を確認します。1つ目、クライアントのログ——Claude CodeはMCPメッセージをログディレクトリに書き込みます。2つ目、サーバーのstderr——stdioトランスポートはすべてのstderr出力をクライアントのコンソールに送ります(stdinとstdoutはMCPプロトコルメッセージ用に予約されていますが、stderrはログ用に自由に使えます)。3つ目、サーバーに構造化JSONログを追加します:{"event": "tool_call", "tool": "query_users", "args": {...}, "duration_ms": 45, "error": null}。構造化ログは、オンコール中に発生し、インシデントに15分入ってもどのツール呼び出しがなぜ失敗したのか分からない、というMCPバグの80%を捕捉します。

MCPはツールからのストリーミングレスポンスをサポートしていますか?

現在の仕様では対応していません。MCPツール呼び出しは単一のcontent配列を返します——部分レスポンスも、進捗更新も、ツール実行からのServer-Sent Eventsもありません。データベースマイグレーションやレポート生成のような長時間実行の操作では、すぐにジョブIDを返し、別のget_job_statusツールを公開します。LLMがそれをポーリングします。このジョブID+ポーリングパターンは、2026年半ば時点ですべての主要な本番MCPサーバーが使うデファクト標準です。直接のストリーミングサポートはMCP仕様のロードマップにありますが、確定したリリース日はありません。

MCPは2025–2026年にAIツール統合を統一しました——サーバー1つ、どのクライアントでも、あらゆるモデル。しかし標準化は競争を招きます。GoogleのAgent-to-Agent Protocol(A2A)は、単純なツール呼び出しを超えたエージェント間通信を必要とするエンタープライズチームの間で勢いを増しています。OpenAIはより深いプラットフォーム統合を備えた独自のエージェントSDKエコシステムを構築しています。2027年まで注目に値する問いは、MCPが生き残るかどうかではありません——プロトコル自体は堅牢で仕様も安定しています——「すべてを支配する単一のプロトコル」が、より緊密な結合とより良いパフォーマンスをロックインの代償として提供するプラットフォーム固有の統合に対する業界の欲求より長く続くかどうかです。

この記事の最初のセクションで構築したMCPサーバーは、1つのクライアント——Claude Desktop——で動作します。MCPゲートウェイはその同じサーバーをスタック内のすべてのLLMに接続するため、一度書いたツール定義が使用するすべてのモデルに供給されます。TokSpanのMCP統合ドキュメントは、ゲートウェイのセットアップ、認証設定、上記のセクションの本番チェックリストをカバーしています——50行のstdioサーバーを、ツールコードに1行も触れずにプロバイダー切り替えを乗り切るマルチモデルツール層に変えます。