MCPプロトコル実装の詳細解析

数値計算ツールMCPサーバーの技術解説

目次

  1. プロジェクト概要
  2. 全体アーキテクチャ
  3. 主要コンポーネント
  4. 通信プロトコル詳細
  5. math_evaluator.py 実装原理
  6. bridge_connector.py 実装原理
  7. MCPプロトコルメッセージ形式
  8. 実行フロー解析
  9. 設定駆動メカニズム

1. プロジェクト概要

1.1 MCPとは

MCP(Model Context Protocol)は、サーバーが言語モデルに公開可能な呼び出し可能ツールを提供するための標準化プロトコルです。これによりモデルは外部システムと連携できます:

  • データベースクエリ
  • 外部API呼び出し
  • 数値計算
  • メール送信
  • デバイス制御

1.2 プロジェクトの用途

MCP-Calculatorは、aiVoice(AI音声アシスタント)のMCP統合サンプルプロジェクトであり、以下を示しています:

  1. MCPサーバーを作成しツールを公開する方法
  2. bridge_connector.pyを介してローカルMCPサービスをaiVoiceクラウドに接続する方法
  3. 音声端末がこれらのツールを呼び出せるようにする方法

1.3 プロジェクトファイル構造

mcp-math-calculator/
├── math_evaluator.py    # MCPサーバー実装(計算ツール)
├── bridge_connector.py  # WebSocket <-> stdioブリッジプログラム
├── mcp_settings.json    # サーバー設定ファイル
├── dependencies.txt     # 依存関係リスト
└── README.md           # プロジェクト説明

2. 全体アーキテクチャ

2.1 システムアーキテクチャ図

┌─────────────────────────────────────────────────────────────────────────────┐
│                            aiVoice クラウド                                │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                      MCPクライアント (WebSocket)                      │   │
│  │         責務:ツール一覧照会、ツール呼び出しリクエスト、レスポンス返却   │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                    ▲                                     │
│                                    │ WebSocket (wss://api.aivoice.me/)   │
│                                    │                                     │
└────────────────────────────────────┼────────────────────────────────────┘
                                     │
┌────────────────────────────────────┼────────────────────────────────────┐
│              ローカルマシン           │                                     │
│                                    ▼                                     │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                        bridge_connector.py                          │   │
│  │                                                                      │   │
│  │  責務:                                                               │   │
│  │  1. aiVoiceクラウドWebSocket接続                                     │   │
│  │  2. math_evaluator.py子プロセス起動と管理                            │   │
│  │  3. WebSocketとstdio間でメッセージ転送                              │   │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                    ▲                                     │
│                                    │ stdio(標準入出力)                   │
│                                    │                                     │
│  ┌─────────────────────────────────────────────────────────────────────┐   │
│  │                      math_evaluator.py                              │   │
│  │                   (MCPサーバー / FastMCP)                           │   │
│  │                                                                      │   │
│  │  公開ツール:calculate(expression) -> dict                           │
│  └─────────────────────────────────────────────────────────────────────┘   │
│                                                                         │
└─────────────────────────────────────────────────────────────────────────┘

2.2 データフロー

音声端末                           aiVoice クラウド                     bridge_connector.py
   │                                    │                                     │
   │  "1004に328を掛けた結果は?"        │                                     │
   │ ──────────────────────────────────► │                                     │
   │                                    │ JSON-RPC: tools/call                │
   │                                    │ ─────────────────────────────────► │
   │                                    │                                     │
   │                                    │        JSON-RPC over WebSocket      │
   │                                    │ ◄─────────────────────────────────► │
   │                                    │                                     │
   │                                    │        JSON-RPC over stdio          │
   │                                    │ ─────────────────────────────────► │
   │                                    │                                     │
   │                                    │                                     │ ▼
   │                                    │                              math_evaluator.py
   │                                    │                                     │処理
   │                                    │                                     │▼
   │                                    │ ◄───────────────────────────────── │
   │  "計算結果は329312です"             │ JSON-RPC レスポンス                 │
   │ ◄───────────────────────────────── │                                     │

3. 主要コンポーネント

3.1 math_evaluator.py - MCPサーバー

役割:FastMCPフレームワークを使用してMCPサーバーを作成し、数学計算ツールを公開

主要コード

from fastmcp import FastMCP
import math
import random

mcp_server = FastMCP("MathCalculator")  # MathCalculatorという名前のMCPサーバーを作成

@mcp_server.tool()  # デコレータ:ツールを公開
def calculate(expression: str) -> dict:
    """数値計算には常にこのツールを使用してください..."""
    result = eval(expression, {"__builtins__": None}, {"math": math, "random": random})
    return {"status": "success", "value": result}

mcp_server.run(transport="stdio")  # stdio転送モードで実行

FastMCPフレームワークの責務

  1. MCPプロトコルのメッセージ解析を自動処理
  2. ツール登録テーブルを管理
  3. initializetools/listtools/callなどのメソッド処理
  4. stdio経由でリクエストを受信し、レスポンスを返信

3.2 bridge_connector.py - ブリッジプロキシ

役割:WebSocket接続とローカルstdioプロセスを接続

主要責務

  1. クラウド接続:aiVoiceにWebSocketクライアントとして接続
  2. 子プロセス起動math_evaluator.pyを子プロセスとして起動
  3. 双方向転送
  • WebSocket → 子プロセスstdin
  • 子プロセスstdout → WebSocket
  • 子プロセスstderr → 端末出力

3.3 mcp_settings.json - 設定センター

役割:複数のMCPサーバー設定を定義

{
  "mcpServers": {
    "local-math-calculator": {  // サーバー名
      "type": "stdio",          // 転送タイプ:stdio/sse/http
      "command": "python",      // 起動コマンド
      "args": ["-m", "math_evaluator"]  // コマンド引数
    },
    "remote-api-server": {     // 別のサーバー
      "type": "sse",
      "url": "https://api.example.com/mcp",
      "disabled": true         // 無効化
    }
  }
}

4. 通信プロトコル詳細

4.1 MCPプロトコル概要

MCPはJSON-RPC 2.0プロトコルをベースとしており、3つの核心メソッドがあります:

メソッド 方向 説明
initialize クライアント → サーバー 接続確立、能力交換
tools/list クライアント → サーバー ツール一覧取得
tools/call クライアント → サーバー 指定ツール呼び出し

4.2 JSON-RPCメッセージ形式

リクエスト形式

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "tools/call",
  "params": {
    "name": "calculate",
    "arguments": {
      "expression": "1004 * 328"
    }
  }
}

レスポンス形式

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"status\": true, \"value\": 329312}"
      }
    ],
    "isError": false
  }
}

エラー形式

{
  "jsonrpc": "2.0",
  "id": 1,
  "error": {
    "code": -32600,
    "message": "Invalid Request"
  }
}

5. math_evaluator.py 実装原理

5.1 コード逐行解析

# 1. FastMCPフレームワークのインポート
from fastmcp import FastMCP
import sys
import logging
import math
import random

logger = logging.getLogger('MathCalculator')  # ロガーの作成

# 2. Windowsエンコード修正(クロスプラットフォーム互換性)
if sys.platform == 'win32':
    sys.stderr.reconfigure(encoding='utf-8')
    sys.stdout.reconfigure(encoding='utf-8')

# 3. MCPサーバーインスタンスの作成
mcp_server = FastMCP("MathCalculator")

# 4. ツール定義(デコレータ使用)
@mcp_server.tool()
def calculate(expression: str) -> dict:
    """
    ツール説明ドキュメント(docstring)
    重要:AIはこの説明に基づいてツール呼び出しのタイミングを決定
    """
    # 5. ユーザー入力の数式を安全に実行
    # 重要:mathとrandomの2つのモジュールのみ注入し、悪意のあるコードを防止
    result = eval(expression, {"__builtins__": None}, {"math": math, "random": random})

    logger.info(f"計算式: {expression}, 結果: {result}")

    # 6. 構造化された結果を返す
    return {"status": "success", "value": result}

# 7. サーバー起動(stdioモード)
if __name__ == "__main__":
    mcp_server.run(transport="stdio")

5.2 @mcp_server.tool() デコレータ原理

ユーザーコード                    FastMCPフレームワーク
   │                            │
   ▼                            │
@mcp_server.tool() ──────────► ツールテーブルに登録
   │                  {
   │                    "calculate": {
   │                      "name": "calculate",
   │                      "description": "...",
   │                      "callback": func,
   │                      "schema": {...}
   │                    }
   │                  }
   │                            │
   ▼                            │
関数定義              フレームワークが自動的にJSON Schema生成

5.3 stdio転送モード原理

┌─────────────────────────────────────────────────────────────┐
│                      math_evaluator.py                      │
│                                                              │
│  stdin  ◄────────────────────────────────────────────────── │
│          JSON-RPCリクエスト (bridge_connector.pyから)        │
│                                                              │
│  フレームワーク処理                                          │
│    │                                                        │
│    ▼                                                        │
│  calculate()関数呼び出し                                    │
│    │                                                        │
│    ▼                                                        │
│  stdout ───────────────────────────────────────────────────► │
│          JSON-RPCレスポンス (bridge_connector.pyへ)        │
└─────────────────────────────────────────────────────────────┘

stdioを使用する理由

  1. シンプル:ネットワークプログラミング不要、子プロセスは標準入出力で通信
  2. 分離:各MCPサーバーは独立プロセスとして、互いに影響しない
  3. 安全:プロセスレベルの分離、障害が発生しても単一ツールにのみ影響

6. bridge_connector.py 実装原理

6.1 主要関数概観

bridge_connector.py
├── establish_connection_with_retry()  # リトライ付きWebSocket接続
├── link_to_service()                   # サービス接続と子プロセス管理
├── forward_websocket_to_process()      # WebSocket → stdin
├── forward_process_to_websocket()       # stdout → WebSocket
├── redirect_process_errors()            # stderr → 端末
├── construct_service_command()         # 子プロセス起動コマンド構築
├── load_configuration()                # 設定ファイル読み込み
└── handle_system_signals()             # シグナル処理(正常終了)

6.2 メインフロー図

┌──────────────────────────────────────────────────────────────────────────┐
│                           _main()関数                                    │
│                                                                          │
│  1. MCP_ENDPOINT環境変数読み取り                                          │
│  2. 起動モード判断:                                                     │
│     ├─ コマンドライン引数あり ───► 単一スクリプト実行 (math_evaluator.py) │
│     └─ 引数なし ──────────────► mcp_settings.json読み込み、全サービス起動│
│                                                                          │
└──────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────────────┐
│                   establish_connection_with_retry()                     │
│                                                                          │
│  while True:                                                            │
│    try:                                                                 │
│      link_to_service(uri, service) ───────────────────────────────────┐ │
│    except:                                                              │ │
│      retry_count++                    │                                │ │
│      backoff = min(backoff * 2, MAX_BACKOFF) │ 指数バックオフ         │ │
│      await asyncio.sleep(backoff) ─────────┴─────────────────────────┘ │
└──────────────────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────────────┐
│                      link_to_service()                                  │
│                                                                          │
│  1. await websockets.connect(uri)  ─── WebSocketクライアント             │
│                                                                          │
│  2. subprocess.Popen(cmd)  ─────────── 子プロセス起動                   │
│     ├─ stdin = PIPE                                                │
│     ├─ stdout = PIPE                                               │
│     └─ stderr = PIPE                                               │
│                                                                          │
│  3. asyncio.gather(                                                   │
│       forward_websocket_to_process(),  ─── WebSocket → stdin           │
│       forward_process_to_websocket(),   ─── stdout → WebSocket         │
│       redirect_process_errors()  ─── stderr → 端末                     │
│     )                                                                  │
└──────────────────────────────────────────────────────────────────────────┘

6.3 forward_websocket_to_process() 詳細

役割:WebSocketメッセージを読み取り、子プロセスのstdinに転送

async def forward_websocket_to_process(websocket, process, service_name):
    try:
        while True:
            # 1. WebSocketからメッセージ受信(ブロッキング)
            message = await websocket.recv()
            logger.debug(f"[{service_name}] << {message[:120]}...")

            # 2. バイトから文字列への変換(必要な場合)
            if isinstance(message, bytes):
                message = message.decode('utf-8')

            # 3. 子プロセスのstdinに書き込み
            process.stdin.write(message + '\n')  # JSON-RPCリクエスト
            process.stdin.flush()               # 直ちに送信
    except Exception as e:
        logger.error(f"[{service_name}] WebSocketからプロセスへの転送エラー: {e}")
        raise

データフロー

aiVoice クラウド                    bridge_connector.py              math_evaluator.py
   │                                    │                              │
   │ JSON-RPCリクエスト                  │                              │
   │ {"jsonrpc":"2.0",...}              │                              │
   │ ──────────────────────────────────► │                              │
   │                                    │                              │
   │                                    │ message = await websocket.recv()
   │                                    │ process.stdin.write(message)
   │                                    │ ────────────────────────────► │
   │                                    │                              │
   │                                    │                              │ JSON-RPCリクエスト
   │                                    │                              │ {"jsonrpc":"2.0",...}
   │                                    │                              │ ▼
   │                                    │                              │ FastMCPフレームワーク解析

6.4 forward_process_to_websocket() 詳細

役割:子プロセスのstdoutを読み取り、WebSocketに転送

async def forward_process_to_websocket(process, websocket, service_name):
    try:
        while True:
            # 1. 子プロセスのstdoutから1行読み取り(ブロッキング)
            data = await asyncio.to_thread(process.stdout.readline)

            if not data:  # EOFはプロセス終了を示す
                logger.info(f"[{service_name}] プロセス出力が終了しました")
                break

            # 2. WebSocketに送信
            logger.debug(f"[{service_name}] >> {data[:120]}...")
            await websocket.send(data)
    except Exception as e:
        logger.error(f"[{service_name}] プロセスからWebSocketへの転送エラー: {e}")
        raise

データフロー

math_evaluator.py                  bridge_connector.py              aiVoice クラウド
   │                                │                              │
   │ JSON-RPCレスポンス              │                              │
   │ {"jsonrpc":"2.0",...}           │                              │
   │ ◄────────────────────────────── │                              │
   │                                │                              │
   │                                │ data = process.stdout.readline()
   │                                │ ◄────────────────────────────── │
   │                                │                              │
   │                                │ await websocket.send(data)    │
   │                                │ ──────────────────────────────► │
   │                                │                              │
   │                                │                              │ JSON-RPCレスポンス

6.5 再接続メカニズム詳細

指数バックオフアルゴリズム

INITIAL_BACKOFF = 1    # 初期待機1秒
MAX_BACKOFF = 600      # 最大待機10分

# バックオフシーケンス:1s → 2s → 4s → 8s → 16s → 32s → 64s → 128s → 256s → 512s → 600s

トリガーシナリオ

  1. WebSocket接続が切断された
  2. 子プロセスが異常終了した
  3. ネットワークの不安定

7. MCPプロトコルメッセージ形式

7.1 initialize - 接続確立

クライアント送信

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {},
    "clientInfo": {
      "name": "aiVoice-agent",
      "version": "1.0.0"
    }
  }
}

サーバーレスポンス

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {"tools": {}},
    "serverInfo": {
      "name": "MathCalculator",
      "version": "3.2.4"
    }
  }
}

7.2 tools/list - ツール一覧取得

クライアント送信

{
  "jsonrpc": "2.0",
  "id": 2,
  "method": "tools/list",
  "params": {}
}

サーバーレスポンス

{
  "jsonrpc": "2.0",
  "id": 2,
  "result": {
    "tools": [
      {
        "name": "calculate",
        "description": "数値計算用ツール...",
        "inputSchema": {
          "type": "object",
          "properties": {
            "expression": {
              "type": "string",
              "description": "評価するPython式"
            }
          },
          "required": ["expression"]
        }
      }
    ]
  }
}

7.3 tools/call - ツール呼び出し

クライアント送信

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "tools/call",
  "params": {
    "name": "calculate",
    "arguments": {
      "expression": "1004 * 328"
    }
  }
}

サーバーレスポンス

{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "{\"status\": true, \"value\": 329312}"
      }
    ],
    "isError": false
  }
}

8. 実行フロー解析

8.1 完全な起動フロー

┌──────────────────────────────────────────────────────────────────────────┐
│                           ユーザーによるコマンド実行                      │
│                    python bridge_connector.py math_evaluator.py         │
└──────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌──────────────────────────────────────────────────────────────────────────┐
│  bridge_connector.py: main()関数                                        │
│                                                                          │
│  1. signal.signal(SIGINT, handle_system_signals)  ─── シグナルハンドラ登録│
│  2. endpoint_url = os.environ.get('MCP_ENDPOINT')                      │
│     └─ wss://api.aivoice.me/mcp/?token=xxx の取得                       │
│  3. service_arg = sys.argv[1]                                          │
│     └─ "math_evaluator.py"                                             │
│  4. asyncio.run(_main())                                               │
└──────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌──────────────────────────────────────────────────────────────────────────┐
│  _main()                                                                │
│                                                                          │
│  asyncio.run(establish_connection_with_retry(endpoint_url, "math_evaluator.py")) │
│                                                                          │
│  establish_connection_with_retry(uri, service):                         │
│    while True:                                                          │
│      try:                                                               │
│        link_to_service(uri, service)  ─── 接続試行                      │
│      except:                                                            │
│        retry_count++                                                    │
│        backoff = min(backoff * 2, MAX_BACKOFF)                          │
│        await asyncio.sleep(backoff)  ─── 待機後再試行                    │
└──────────────────────────────────────────────────────────────────────────┘
                                    │
                                    ▼
┌──────────────────────────────────────────────────────────────────────────┐
│  link_to_service(uri, service)                                         │
│                                                                          │
│  1. async with websockets.connect(uri)                                 │
│     ── WebSocket接続成功                                                │
│                                                                          │
│  2. cmd, env = construct_service_command("math_evaluator.py")           │
│     ── [sys.executable, "math_evaluator.py"], os.environ.copy()        │
│                                                                          │
│  3. subprocess.Popen(cmd, stdin=PIPE, stdout=PIPE, stderr=PIPE)        │
│     ── math_evaluator.py子プロセス起動                                   │
│                                                                          │
│  4. asyncio.gather(                                                   │
│       forward_websocket_to_process(...),  ─── 3つの非同期タスク起動      │
│       forward_process_to_websocket(...),   ───                          │
│       redirect_process_errors(...)  ───                                │
│     )                                                                  │
└──────────────────────────────────────────────────────────────────────────┘

8.2 ツール呼び出し完全フロー

┌─────────────────────────────────────────────────────────────────────────┐
│ ステップ1: aiVoiceクラウドがinitialize送信                              │
│                                                                         │
│ WebSocket ────────────────────────────────────────────────────────────► │
│            {"jsonrpc":"2.0","method":"initialize",...}                  │
│                                                                         │
│                      math_evaluator.py (子プロセス)                     │
│                              │                                          │
│                              ▼                                          │
│                      FastMCPフレームワーク処理                           │
│                              │                                          │
│                              ▼                                          │
│ stdout ────────────────────────────────────────────────────────────────► │
│         {"jsonrpc":"2.0","result":{"capabilities":{...}}}              │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ ステップ2: aiVoiceクラウドがtools/list送信                              │
│                                                                         │
│ WebSocket ────────────────────────────────────────────────────────────► │
│            {"jsonrpc":"2.0","method":"tools/list",...}                 │
│                                                                         │
│                      math_evaluator.py                                 │
│                              │                                          │
│                              ▼                                          │
│                      FastMCPがツール一覧返却                             │
│                              │                                          │
│                              ▼                                          │
│ stdout ────────────────────────────────────────────────────────────────► │
│         {"jsonrpc":"2.0","result":{"tools":[...]}}                    │
└─────────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────────┐
│ ステップ3: aiVoiceクラウドがtools/call送信(計算リクエスト)             │
│                                                                         │
│ WebSocket ────────────────────────────────────────────────────────────► │
│            {"jsonrpc":"2.0","method":"tools/call",                      │
│             "params":{"name":"calculate",                               │
│                       "arguments":{"expression":"1004*328"}}}          │
│                                                                         │
│                      math_evaluator.py                                 │
│                              │                                          │
│                              ▼                                          │
│                      @mcp_server.tool()デコレート関数                   │
│                              │                                          │
│                              ▼                                          │
│                      result = eval("1004*328", {...}) = 329312         │
│                              │                                          │
│                              ▼                                          │
│ stdout ────────────────────────────────────────────────────────────────► │
│         {"jsonrpc":"2.0","result":{"content":[{"text":"..."}]}}        │
└─────────────────────────────────────────────────────────────────────────┘

9. 設定駆動メカニズム

9.1 mcp_settings.json 詳細

{
  "mcpServers": {
    "local-math-calculator": {  // サーバー名(カスタマイズ可能)
      "type": "stdio",          // 転送タイプ
      "command": "python",      // 起動コマンド
      "args": ["-m", "math_evaluator"] // 引数(モジュールとして実行)
    },
    "remote-api-service": {
      "type": "sse",            // SSEタイプのリモートサービス
      "url": "https://api.example.com/mcp",
      "disabled": true          // 無効化(起動しない)
    },
    "remote-http-service": {
      "type": "http",           // HTTPタイプ
      "url": "https://api.example.com/mcp",
      "disabled": true
    }
  }
}

9.2 設定の優先順位

コマンドライン引数 ─────────────► 直接スクリプト実行
    │                         │
    │ (例: math_evaluator.py) │
    ▼                         ▼
   引数をパスとして使用       設定ファイルmcp_settings.jsonを使用
                              │
                              ├─► MCP_CONFIG環境変数が指すファイル
                              │
                              └─► ./mcp_settings.json

9.3 マルチサービス同時起動

bridge_connector.pyが引数なしで実行された場合:

cfg = load_configuration()
servers_cfg = cfg.get("mcpServers", {})

# 無効化されていない全サービスを起動
tasks = [asyncio.create_task(establish_connection_with_retry(endpoint_url, t))
         for t in enabled_services]

await asyncio.gather(*tasks)  // 全サービスを同時実行

付録:重要概念クイックリファレンス

概念 説明
**FastMCP** MCPプロトコルに基づくPythonフレームワーク、ツール開発を簡化
**stdio** 標準入出力、親子プロセス間通信に使用
**WebSocket** 双方向リアルタイム通信プロトコル
**JSON-RPC 2.0** 軽量リモートプロシージャコールプロトコル
**bridge_connector.py** WebSocket ↔ stdioブリッジプログラム
**@mcp_server.tool()** デコレータ、ツール関数を公開するために使用

参照リンク

  • MCPプロトコル仕様
  • FastMCPフレームワーク
  • aiVoice AI音声アシスタント
  • mcp-math-calculatorサンプルプロジェクト

タグ: MCPプロトコル json-rpc FastMCP websocket stdio通信

7月11日 16:44 投稿