MCP Server の仕組みを少し調べてみる

LLM をチャットの応答だけでなく、Agent として使うために直接外部アプリケーションを呼び出せるような拡張が行われています。特に MCP と呼ばれるプロトコルではツールとのインターフェースが標準化されており、SDK を使うと簡単に追加機能を作成することが可能です。

PC 上で動いているクライアントアプリケーションは、人間の入力を LLM が動いているサーバーに送信して返答を受け取ります。Agent ではその一連のやり取りにツールが割り込みます。LLM からの要請に応じてクライアント側がツールのソフトウエアを呼び出し、人間の代わりに LLM に対して返答するわけです。これらの Function Calling と呼ばれる機能拡張の仕組みを標準化したものが MCP に相当します。

MCP に対応したツールはサーバーと呼ばれるため、クラウド上に用意したり、LLM サーバー側が直接通信しているような印象を受けますが実際は異なります。簡単に言えば PC 上で動いている LLM アプリケーションに追加するプラグインのことです。

Claude Desktop や Cline などのソフトウエアは、プラグインのロードと同じように組み込まれている MCP のソフトウエアを別プロセスとして起動します。このときプロセス間通信として、PIPE (標準入出力) もしくは TCP (HTTP/SSE) が使われています。

MCP サーバーはクライアント毎に別プロセスが立ち上がります。例えば Claude Desktop と Cline のどちらも同じ MCP のツールを参照していた場合、それぞれが別プロセスとして MCP サーバーを起動します。サーバーは共有されません。やはり構造的にプラグインの方がイメージしやすいように思います。

Python SDK MCP コマンド

PIPE や TCP で渡されるデータは Json です。Python SDK で使われている mcp コマンドは、この Json と Python Script の関数呼び出しの相互変換を行っています。

例えば以下の例では mcp コマンド (mcp.exe) が標準入力から json フォーマットの文字列を受け取り、関数呼び出しの形に変換して server.py 内部の関数呼び出します。また関数の戻り値を json 化して標準出力に出力します。

{
  "mcpServers": {
    "demo-app": {
      "command": "mcp",
      "args": [
        "run",
        "C:\\mcptest\\server.py"
      ]
    },
  }
}

mcp.exe は Python の実行ファイルで、引数で渡されている server.py を内部で import しています。

実際にやり取りされているデータ

本当に標準入出力が使われているのかどうか、間に別のプロセスを挟み込んで通信内容をキャプチャしてみました。引数で渡した mcp サーバーを起動し、標準入出力をそのまま渡します。同時にログファイルに記録します。このコマンド (command-capture.py) は Calude 3.7 Sonnet が作りました。

{
  "mcpServers": {
    "demo-app": {
      "command": "C:\\mcptest\\venv\\Scripts\\python.exe",
      "args": [
        "C:\\mcptest\\command-capture.py",
            "--quiet",
            "--log-dir",
                "C:/mcptest/logs",
            "C:\\mcptest\\venv\\Scripts\\mcp.exe",
                "run",
                "C:\\mcptest\\server.py"
      ]
    }
  }
}

以下は Claude Desktop による実際の入出力を記録したものです。tool/list など method の呼び出しが行われている様子がわかります。

[2025-04-06 15:09:35.419] IN: {"method":"initialize","params":{"protocolVersion":"2024-11-05","capabilities":{},"clientInfo":{"name":"claude-ai","version":"0.1.0"}},"jsonrpc":"2.0","id":0}
[2025-04-06 15:09:36.981] OUT: {"jsonrpc":"2.0","id":0,"result":{"protocolVersion":"2024-11-05","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"demo-app","version":"1.5.0"}}}
[2025-04-06 15:09:36.988] IN: {"method":"notifications/initialized","jsonrpc":"2.0"}
[2025-04-06 15:09:36.989] IN: {"method":"resources/list","params":{},"jsonrpc":"2.0","id":1}
[2025-04-06 15:09:36.990] IN: {"method":"tools/list","params":{},"jsonrpc":"2.0","id":2}
[2025-04-06 15:09:36.992] OUT: {"jsonrpc":"2.0","id":1,"result":{"resources":[]}}
[2025-04-06 15:09:36.992] OUT: {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"calc_add","description":"Add two numbers","inputSchema":{"properties":{"a":{"title":"a","type":"string"},"b":{"title":"b","type":"string"}},"required":["a","b"],"title":"calc_addArguments","type":"object"}},{"name":"exec_shell_command","description":"Execute a command in the bash shell","inputSchema":{"properties":{"command":{"title":"command","type":"string"}},"required":["command"],"title":"exec_shell_commandArguments","type":"object"}}]}}

[2025-04-06 15:10:53.116] IN: {"method":"tools/call","params":{"name":"calc_add","arguments":{"a":"2838414","b":"8294241"}},"jsonrpc":"2.0","id":36}
[2025-04-06 15:10:53.118] OUT: {"jsonrpc":"2.0","id":36,"result":{"content":[{"type":"text","text":"11132655"}],"isError":false}}

なおアプリケーション側でも同様のログが記録されているため、デバッグに利用する場合はこのようなツールは不要です。Claude Desktop の Windows 版の場合 AppData\Roaming\Claude\logs にあります。

Json 部分を整形すると以下のようになります。

[2025-04-06 15:09:35.419] IN: {
    "method": "initialize",
    "params": {
      "protocolVersion": "2024-11-05",
      "capabilities": {},
      "clientInfo": {
        "name": "claude-ai",
        "version": "0.1.0"
      }
    },
    "jsonrpc": "2.0",
    "id": 0
  }
[2025-04-06 15:09:36.981] OUT: {
    "jsonrpc": "2.0",
    "id": 0,
    "result": {
      "protocolVersion": "2024-11-05",
      "capabilities": {
        "experimental": {},
        "prompts": {
          "listChanged": false
        },
        "resources": {
          "subscribe": false,
          "listChanged": false
        },
        "tools": {
          "listChanged": false
        }
      },
      "serverInfo": {
        "name": "demo-app",
        "version": "1.5.0"
      }
    }
  }
[2025-04-06 15:09:36.988] IN: {
    "method": "notifications/initialized",
    "jsonrpc": "2.0"
  }
[2025-04-06 15:09:36.989] IN: {
    "method": "resources/list",
    "params": {},
    "jsonrpc": "2.0",
    "id": 1
  }
[2025-04-06 15:09:36.990] IN: {
    "method": "tools/list",
    "params": {},
    "jsonrpc": "2.0",
    "id": 2
  }
[2025-04-06 15:09:36.992] OUT: {
    "jsonrpc": "2.0",
    "id": 1,
    "result": {
      "resources": []
    }
  }
[2025-04-06 15:09:36.992] OUT: {
    "jsonrpc": "2.0",
    "id": 2,
    "result": {
      "tools": [
        {
          "name": "calc_add",
          "description": "Add two numbers",
          "inputSchema": {
            "properties": {
              "a": {
                "title": "a",
                "type": "string"
              },
              "b": {
                "title": "b",
                "type": "string"
              }
            },
            "required": [
              "a",
              "b"
            ],
            "title": "calc_addArguments",
            "type": "object"
          }
        },
        {
          "name": "exec_shell_command",
          "description": "Execute a command in the bash shell",
          "inputSchema": {
            "properties": {
              "command": {
                "title": "command",
                "type": "string"
              }
            },
            "required": [
              "command"
            ],
            "title": "exec_shell_commandArguments",
            "type": "object"
          }
        }
      ]
    }
  }



[2025-04-06 15:10:53.116] IN: {
    "method": "tools/call",
    "params": {
      "name": "calc_add",
      "arguments": {
        "a": "2838414",
        "b": "8294241"
      }
    },
    "jsonrpc": "2.0",
    "id": 36
  }
[2025-04-06 15:10:53.118] OUT: {
    "jsonrpc": "2.0",
    "id": 36,
    "result": {
      "content": [
        {
          "type": "text",
          "text": "11132655"
        }
      ],
      "isError": false
    }
  }

標準入出力ができれば良いだけなので、SDK やライブラリがない他の言語でも比較的容易に作ることができる仕組みです。