李春川

MCP プロトコル統合実践:AI Agent に任意のツールを呼び出させる

あなたが AI に航空券を予約してほしいと頼んだとします。AI は「私は言語モデルなので外部システムにアクセスできません」と答えます。別の Agent 製品に変えると、使えると言いながらパスワードや管理者権限を要求されます。あなたはためらいます。

LLM の次の 10 年の戦いはモデル自体ではなく、ツール呼び出しです。Anthropic は 2024 年末に Model Context Protocol (MCP) をオープンソース化し、「AI が外部ツールを呼び出す」ことを標準化しました。本稿では、IHUI AI が 8 端全スタック AI OS に MCP を統合し、Agent を実際に働かせる方法を解説します。


一、課題:AI は会話はできても仕事はできない

LLM アプリケーションの最も厄介な現状:モデルは賢いがガラスケースの中に閉じ込められている

課題 1:各 Agent フレームワークが独自のツールプロトコルを実装

  • OpenAI Function Calling:tools + tool_calls
  • Anthropic Tool Use:tool_use ブロック
  • LangChain Tools:BaseTool クラス
  • AutoGen Tools:@tool デコレータ
  • Coze / Dify:ビジュアルプラグイン

「注文照会」ツールを 1 つ書くのに、5 つのフレームワークに合わせて 5 つのコードを書く必要があります。

課題 2:ツール再利用率がほぼゼロ

A 社が「天気照会」ツールを書いても、自社の Agent でしか使えません。B 社が再利用しようとすると一から実装する必要があります。業界全体が車輪の再発明を繰り返しています。

課題 3:権限の喪失

Agent にデータベースを照会させるために DB アカウントを渡し、メールを送らせるために SMTP パスワードを渡し、GitHub を操作させるために PAT を渡す。prompt injection 攻撃 1 つで本番データベースが丸裸になります。

課題 4:サンドボックスなし

Agent にコードスニペットを実行させると、サーバー上で直接 eval() される?本番環境はあっという間に破壊されます。


二、解決策:MCP プロトコル+サンドボックス実行

2.1 MCP とは

Model Context Protocol は Anthropic が主導するオープンソースの AI ツール呼び出しプロトコルです。核心思想:ツール/リソース/Prompt を標準 server として公開し、MCP 対応クライアントなら誰でも呼び出せる

アナロジー:MCP を AI Agent に例えるなら、LSP をエディタに例えるのと同じです。LSP が 1 つの言語サーバーを VSCode/Vim/Emacs で共有できるように、MCP は 1 つのツールを Claude Desktop/Cursor/任意の Agent で共有できるようにします。

2.2 MCP の 3 要素

  1. Tools(ツール):実行可能な関数(例:query_order(orderId)send_email(to, subject, body)
  2. Resources(リソース):読み取り可能なデータ(例:file:///path/to/doc.mddb://users/123
  3. Prompts(プロンプトテンプレート):再利用可能な prompt(例:summarize_meeting(transcript)

2.3 IHUI AI の MCP アーキテクチャ

┌──────────────────────────────────────────┐
│  AI Agent (LangGraph 編成)               │
│  ↓ ツール呼び出し判断                    │
│  MCP Client (統一呼び出し層)             │
└──────────────┬───────────────────────────┘
               │ JSON-RPC over stdio/SSE
               ↓
┌──────────────────────────────────────────┐
│  MCP Server Registry(ツール登録センター)│
│  ↓ 権限検証+ルーティング                │
└──────────────┬───────────────────────────┘
               │
        ┌──────┼──────┬──────┬──────┐
        ↓      ↓      ↓      ↓      ↓
   組み込みツール  企業DB  GitHub Slack  サンドボックスコード実行
   (ローカル)   MCP    MCP    MCP   MCP

Enter fullscreen mode Exit fullscreen mode

各 MCP server は独立したプロセスで、Agent は標準プロトコルでのみ通信します。ツール実装と Agent は分離されています。


三、技術詳細

3.1 カスタム MCP Server

IHUI AI は公式 @modelcontextprotocol/sdk で MCP server を書いています。以下は「注文照会」server の例です。

// mcp-servers/order-query/index.ts
import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
import {
  CallToolRequestSchema,
  ListToolsRequestSchema,
} from '@modelcontextprotocol/sdk/types.js';
import { db } from './db.js';

const server = new Server(
  { name: 'order-query', version: '1.0.0' },
  { capabilities: { tools: {} } },
);

// ツール登録
server.setRequestHandler(ListToolsRequestSchema, async () => ({
  tools: [
    {
      name: 'query_order',
      description: '注文詳細を照会。ユーザー自身の注文 ID が必要',
      inputSchema: {
        type: 'object',
        properties: {
          orderId: { type: 'string', description: '注文 ID' },
        },
        required: ['orderId'],
      },
    },
    {
      name: 'list_recent_orders',
      description: '現在のユーザーの直近 10 件の注文を一覧表示',
      inputSchema: {
        type: 'object',
        properties: {
          userId: { type: 'string', description: 'ユーザー ID(認証コンテキストから取得)' },
        },
        required: ['userId'],
      },
    },
  ],
}));

// ツール実装
server.setRequestHandler(CallToolRequestSchema, async (request) => {
  const { name, arguments: args } = request.params;

  switch (name) {
    case 'query_order': {
      const order = await db.orders.findById(args.orderId);
      if (!order) {
        return { content: [{ type: 'text', text: '注文が存在しません' }] };
      }
      // 権限検証:自分の注文のみ照会可能
      if (order.userId !== args.userId) {
        return { content: [{ type: 'text', text: '他人の注文にはアクセスできません' }] };
      }
      return {
        content: [{
          type: 'text',
          text: JSON.stringify(order, null, 2),
        }],
      };
    }
    case 'list_recent_orders': {
      const orders = await db.orders.findRecentByUser(args.userId, 10);
      return {
        content: [{
          type: 'text',
          text: JSON.stringify(orders, null, 2),
        }],
      };
    }
  }
});

const transport = new StdioServerTransport();
await server.connect(transport);

Enter fullscreen mode Exit fullscreen mode

主要設計:

  • ツールは JSON Schema で入力パラメータを記述し、LLM は自動的に呼び出し方を理解します。
  • 権限検証はツール内部で行われ、Agent はバイパスできません。
  • 戻り値は構造化 JSON で、LLM がさらに処理できます。

3.2 MCP Client 統合

apps/ai-service/src/mcp/client.py:

from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client
from contextlib import asynccontextmanager
import json

class MCPRegistry:
    """MCP Server 登録センター"""

    def __init__(self):
        self.servers: dict[str, ClientSession] = {}
        self.tools_cache: dict[str, list] = {}

    @asynccontextmanager
    async def connect(self, server_name: str, command: str, args: list[str]):
        params = StdioServerParameters(command=command, args=args)
        async with stdio_client(params) as (read, write):
            async with ClientSession(read, write) as session:
                await session.initialize()
                self.servers[server_name] = session
                # ツール一覧をキャッシュ
                tools_resp = await session.list_tools()
                self.tools_cache[server_name] = tools_resp.tools
                yield session

    async def call_tool(
        self,
        server_name: str,
        tool_name: str,
        arguments: dict,
        user_context: dict,  # 認証コンテキスト
    ):
        if server_name not in self.servers:
            raise ValueError(f"未登録の MCP server: {server_name}")
        # ユーザーコンテキストを引数に注入(権限検証用)        enriched_args = {**arguments, **user_context}
        session = self.servers[server_name]
        result = await session.call_tool(tool_name, enriched_args)
        return json.loads(result.content[0].text)

    def all_tools_as_openai_format(self) -> list[dict]:
        """全 MCP ツールを OpenAI tools 形式に変換し、LLM が呼び出せるようにする"""
        all_tools = []
        for server_name, tools in self.tools_cache.items():
            for tool in tools:
                all_tools.append({
                    "type": "function",
                    "function": {
                        "name": f"{server_name}__{tool.name}",
                        "description": tool.description,
                        "parameters": tool.inputSchema,
                    },
                })
        return all_tools

Enter fullscreen mode Exit fullscreen mode

3.3 LangGraph ツール呼び出しループ

MCP ツールを LangGraph Agent に公開します。

from langgraph.graph import StateGraph, END
from langgraph.prebuilt import ToolNode
from litellm import acompletion

class AgentState(TypedDict):
    messages: list[dict]
    user_context: dict

async def call_llm(state: AgentState):
    tools = registry.all_tools_as_openai_format()
    response = await acompletion(
        model="gpt-4o",
        messages=state["messages"],
        tools=tools,
    )
    msg = response.choices[0].message
    return {"messages": state["messages"] + [msg.to_dict()]}

async def call_mcp_tool(state: AgentState):
    last_msg = state["messages"][-1]
    results = []
    for tool_call in last_msg.get("tool_calls", []):
        # ツール名形式:server_name__tool_name
        server_name, tool_name = tool_call["function"]["name"].split("__", 1)
        args = json.loads(tool_call["function"]["arguments"])
        result = await registry.call_tool(
            server_name, tool_name, args, state["user_context"],
        )
        results.append({
            "role": "tool",
            "tool_call_id": tool_call["id"],
            "content": json.dumps(result),
        })
    return {"messages": state["messages"] + results}

def should_continue(state: AgentState) -> str:
    last_msg = state["messages"][-1]
    if last_msg.get("tool_calls"):
        return "call_tool"
    return END

graph = StateGraph(AgentState)
graph.add_node("llm", call_llm)
graph.add_node("call_tool", call_mcp_tool)
graph.add_conditional_edges("llm", should_continue)
graph.add_edge("call_tool", "llm")  # ツール結果を LLM に戻す
graph.set_entry_point("llm")
agent = graph.compile()

Enter fullscreen mode Exit fullscreen mode

呼び出しループ全体:LLM が判断 → MCP ツールを呼び出し → 結果を LLM に返す → 次を判断 → 完了まで。

3.4 権限制御:3 層モデル

IHUI AI の MCP 権限は 3 層に分かれています。

  1. ユーザー層:ユーザーは自身がアクセス権を持つツールのみ呼び出せます(例:自分の注文のみ照会)。ツール実装内で userId を検証します。
  2. プラン層:Free ユーザーは組み込みツール 5 個のみ、Pro ユーザーは 50 個、Enterprise は全ツールを利用可能。MCP Client 層でツール一覧をフィルタリングします。
  3. セッション層:ユーザーは UI 上で明示的に「Agent に X ツールの呼び出しを許可」する必要があります。OAuth 認可に似ています。Agent は一時トークンを取得し、セッション終了時に無効化されます。
def filter_tools_by_permission(
    user_id: str,
    plan: str,
    session_grants: list[str],
    all_tools: list[dict],
) -> list[dict]:
    allowed = []
    for tool in all_tools:
        tool_id = tool["function"]["name"]
        # プラン層
        if tool_id in PLAN_TOOL_WHITELIST[plan]:
            allowed.append(tool)
        # セッション層
        elif tool_id in session_grants:
            allowed.append(tool)
    return allowed

Enter fullscreen mode Exit fullscreen mode

3.5 サンドボックス実行:コードインタープリタ MCP

最も危険なツールは「コード実行」です。IHUI AI は Docker+リソース制限に基づく独立したサンドボックス MCP server を実装しています。

// mcp-servers/code-sandbox/index.ts
import Docker from 'dockerode';
const docker = new Docker();

server.setRequestHandler(CallToolRequestSchema, async (req) => {
  if (req.params.name !== 'execute_code') {
    throw new Error(`未知のツール: ${req.params.name}`);
  }
  const { code, language = 'python' } = req.params.arguments;

  // 使い捨てコンテナを起動
  const container = await docker.createContainer({
    Image: 'ihui/sandbox-python:3.11-slim',
    Cmd: ['python', '-c', code],
    HostConfig: {
      Memory: 256 * 1024 * 1024,    // 256MB
      NanoCpus: 1e9,                // 1 CPU
      NetworkMode: 'none',          // ネットワーク禁止
      AutoRemove: true,
    },
  });

  await container.start();
  const output = await container.attach({
    stream: true, stdout: true, stderr: true,
  });
  // ... 出力を収集、5 秒でタイムアウトして kill
  return {
    content: [{ type: 'text', text: output.toString() }],
  };
});

Enter fullscreen mode Exit fullscreen mode

セキュリティ対策:ネットワーク禁止+メモリ/CPU 制限+5 秒タイムアウト+使い捨てコンテナ+非 root ユーザー。Agent がどんなコードを実行してもホストに影響しません。

3.6 A2A プロトコル:MCP 上の Agent 間相互呼び出し

MCP は「Agent がツールを呼び出す」を解決しますが、Agent 同士の相互呼び出しはどうするのでしょうか?IHUI AI は A2A(Agent-to-Agent)プロトコルも統合しています。

  • Agent A(計画)→ Agent B(検索)→ Agent C(生成)
  • 各 Agent は MCP server を公開し、他の Agent をツールとして呼び出します
  • 単一の Agent ではなく Agent ネットワークを形成します

これが IHUI AI の P3 深層です。ユーザーは Agent トポロジをドラッグ&ドロップで設定し、プラットフォームがスケジューリングを担当します。


四、IHUI AI 運用データ

指標
組み込み MCP server 数 28 個(注文/決済/メール/検索/データベース/コードサンドボックスなど)
サードパーティ MCP 互換性 100%(任意の MCP server をプラグ&プレイ)
平均ツール呼び出しレイテンシ 120ms(ローカル)/ 800ms(リモート)
サンドボックス最大実行時間 30 秒(タイムアウトで自動 kill)
権限レイヤー 3 層(ユーザー/プラン/セッション)
プロトコル対応 MCP+A2A デュアルプロトコル

実例:ある企業ユーザーが社内 ERP システムを MCP server としてカプセル化し、IHUI AI の Agent がワンクリックで接続。従業員は自然言語で在庫照会、注文作成、レポート生成が可能になりました。従来 3 週間かかっていた統合が 2 日で完了し、権限分離も明確です。


五、落とし穴まとめ

落とし穴 1:ツール記述が不十分だと LLM が誤ったツールを呼び出す

LLM は description フィールドでツールを判断します。記述が曖昧(「注文を照会」)だと LLM は誤ったツールを呼び出します。当社の規約:記述には①いつ使うか、②いつ使わないか、③入力パラメータの意味、④戻り値の構造を必ず含めます。例:「注文詳細を照会。ユーザーが注文状況を明示的に尋ねた場合のみ使用し、雑談中には呼び出さない。注文 JSON(id/items/total/status フィールドを含む)を返却する。」

落とし穴 2:ツール数が爆発し、LLM が選択に苦労する

ツールが 50 以上になると LLM の誤選択率が急上昇します。対策:シナリオごとにグループ化し、LangGraph ルーティングノードで「ツールサブセット」を先に選択してから LLM に渡します。

落とし穴 3:stdio vs SSE

MCP は 2 種類のトランスポートをサポート:stdio(ローカル子プロセス)と SSE(リモート HTTP)。stdio は高速ですがローカルのみ、SSE はネットワーク越えですが遅延があります。IHUI AI は組み込みツールに stdio、サードパーティツールに SSE を使用し、Client 層で透過的に切り替えます。

落とし穴 4:ツール結果のトークン爆発

あるツールが 10 万行の SQL 結果を返すと、コンテキストに収まらなくなります。MCP Client 層で切り詰め処理を実施:tool_result_truncator(result, max_tokens=2000)。長すぎる場合は要約+「完全な結果は保存済み。query_detail ツールで確認可能」と案内します。


六、MCP を導入すべきでないケース

  1. 1 つの LLM ベンダーにのみ依存する場合:ベンダー固有の Function Calling の方が軽量です。
  2. ツール数が 5 未満で外部共有しない場合:自前実装の方がシンプルです。
  3. Agent 間でのツール再利用が不要な場合:MCP の本質的な価値は「1 回書けばすべての Agent で使える」点にあります。自社 Agent 1 つでしか使わないならメリットは限定的です。

MCP の真の価値はエコシステムにあります。ツールを 1 回書けば、すべての MCP 対応クライアント(Claude Desktop/Cursor/Cline/IHUI AI)で利用可能です。これは LSP がエディタエコシステムを統一した再現です。


七、結び

MCP 統合の核心は以下の通りです。

  • プロトコル標準化:MCP によりツールを 1 回書けばすべての Agent で呼び出せ、5 つのフレームワークに 5 つのコードを書く必要がなくなります。
  • server 分離:各ツールは独立したプロセスで、LangGraph Agent は JSON-RPC で通信するのみです。
  • 3 層権限:ユーザー層/プラン層/セッション層。prompt injection 攻撃でも本番データベースは守られます。
  • サンドボックス実行:コード実行系ツールは Docker で隔離。ネットワーク禁止+リソース制限+タイムアウト kill。
  • A2A 拡張:MCP の上に Agent 間相互呼び出しを構築し、Agent ネットワーク(P3 深層)を形成します。

IHUI AI は MCP で 28 個の組み込みツール+任意のサードパーティ MCP server を接続し、Agent マーケットのツールを 1 回書けば全プラットフォームで通用するようにしました。Agent アプリケーションを開発中の方は、初日から MCP を採用することを強くおすすめします。後から移行するとツールプロトコル適応が大変です。


IHUI AI について

IHUI AI はオールインワンの 8 端全スタック AI OS で、Apache 2.0 でオープンソース化されています。

  • 🌐 公式サイト:https://aizhs.top
  • 💻 GitHub:https://github.com/IHUI-INF-AI/IHUI-AI(Star サポート ⭐)
  • 📦 8 端同一ソース:Web / API / CLI / Desktop / Extension / Mobile / Miniapp
  • 🤖 176 モデル:OpenAI / Claude / Gemini / 通義 / DeepSeek / 智譜 / 文心 / 豆包 / Kimi / Ollama
  • 💰 料金プラン:Free / Pro ¥49/月 / Team ¥199/人/月 / Enterprise ¥2999/月〜

5 分で Fork から本番稼働まで。ChatGPT Team+Claude Code+Notion AI の代替で、月 $60 以上節約。


この記事は元々 IHUI AI Blog で公開されました。AI エンジニアリングの最新情報は GitHub でフォローしてください。