CUBE SUGAR CONTAINER

技術系のこと書きます。

自分で MCP (Model Context Protocol) を喋ってみる

Model Context Protocol (MCP) は、LLM が外部のシステムから情報を得る、または操作するのに用いられるプロトコルのひとつ。 昨今の扱われ方からすると、今後のデファクトとなりつつあるように感じられる。

今回は、そんな MCP がどういったプロトコルなのか仕様を読みながら自分で喋ってみることにした。 詳しくは後述するものの、MCP はあくまで JSON-RPC 2.0 をベースにしたプロトコルに過ぎない。 そのため、LLM とは関係なく独立して扱うことができる。

使った環境は次のとおり。 MCP Server と Client の実装には MCP 公式の Python SDK を用いた。

$ sw_vers
ProductName:        macOS
ProductVersion:     15.6
BuildVersion:       24G84
$ uname -srm                      
Darwin 24.6.0 arm64
$ uv --version                    
uv 0.8.4 (Homebrew 2025-07-30)

もくじ

MCP について

本題に入る前に、まずは MCP の概要についてざっくりと説明していく。

なお、特に断りがない限りは執筆の時点で最新のバージョン (2025-06-18) のプロトコル仕様を元にしている。 プロトコルの仕様は、以下の Web サイトで確認できる。

modelcontextprotocol.io

アーキテクチャ

MCP のアーキテクチャには、次のようなコンポーネントが登場する。

  • MCP Server
  • MCP Client
  • MCP Host

まず、MCP Server は実際にツールを実行したりデータを提供したりするコンポーネントになっている。 そのため、MCP Server が MCP の主役といっていい。 そして MCP Client は MCP Server に対してツールの実行やデータの提供を依頼する。

MCP Host は LLM やそれを利用するアプリケーションとほぼイコールと考えて良い。 MCP Host には MCP Server (+Client) を登録する。 登録した MCP Server が提供する機能を、MCP Host の LLM が使えるようになる。 具体例を挙げると、Claude Desktop や LM Studio が MCP Host として動作する。 例として挙げた MCP Host には MCP Client の機能が同梱されている。

機能

MCP Server が提供する機能には、次のような種類がある。

  • Tools
  • Resources
  • Prompts

Tools は読んで字のごとくツールの呼び出しに使用する。 LLM はツールを呼び出すことで外部から情報を得たり、外部の世界に何らかの副作用を生じさせたりする。 たとえば外部の何らかの API の呼び出しや、データベースへのクエリの発行、計算の実行など。 これが MCP のユースケースとしてはイメージしやすいと思う。

参考: Tools - Model Context Protocol

Resources は URI を使って識別される何らかのリソースの読み取りに使用する。 LLM はリソースを読み取ることで後続の推論に必要な情報を得る。 分かりやすいところでいうとファイルシステムの読み取りがある。 たとえば file:///read/to/path のような URI を読み取ることで、そのファイルの内容が得られる。

参考: Resources - Model Context Protocol

Prompts は、LLM の推論に用いるプロンプトを得るのに使用する。 この機能は LLM が直接利用するのではなく、MCP Host やそれを操作するユーザが主に利用する機能らしい。 MCP Server から提供されたプロンプトを使って、ユーザが LLM に指示を出すイメージか。

参考: Prompts - Model Context Protocol

なお、 MCP Client も提供する機能がいくつかあるが、ここでは扱わない。

メッセージ

プロトコルとしての MCP は、メッセージングに JSON-RPC 2.0 を用いる。 つまり、MCP Server と Client の間では JSON-RPC 2.0 のメッセージがやり取りされる。

JSON-RPC 2.0 は、その名の通り JSON を表現に使った RPC (Remote Procedure Call) になっている。 仕様は以下の Web サイトで確認できる。

www.jsonrpc.org

メッセージとしては、次の 3 つが定義されている。

  • Requests
  • Responses
  • Notifications

上記で Requests と Responses は対になっている。 機能の要求が Requests で、それに対する応答が Responses になる。 Notifications は一方通行の通知で、Server と Client のいずれも他方に送信することがある。

参考: Overview - Model Context Protocol

トランスポート

MCP Server と Client の間で JSON-RPC 2.0 をやり取りするための通信路をトランスポートという。 仕様には次の 3 つのトランスポートが存在する。

  • stdio
  • Streamable HTTP
  • ​Custom

stdio は文字通り stdio (標準入出力) を使って MCP Server と Client が通信する。 典型的には MCP Server の機能がローカルホストで完結する場合や、開発の用途で用いられる。

Streamable HTTP も文字通り HTTP を使って MCP Server と Client が通信する。 この場合、MCP Server はイコール HTTP サーバになる。 MCP Server から Client に対して非同期にメッセージを送りたいときは、オプションで SSE (Server-Sent Events) を用いる。

Custom は独自に実装したトランスポートになる。 具体例を挙げると、WebSocket や UNIX ドメインソケットでメッセージをやり取りするような場合だろう。 その場合は MCP Server と Client の両方が、その独自のトランスポートに対応している必要がある。

参考: Transports - Model Context Protocol

なお、今回はトランスポートとして stdio だけを使う。

下準備

これで、必要な前提知識は大体説明できた。

次は実際に MCP Server と MCP Client を書いて動かしてみよう。 ただし、最初から自分で MCP を喋ることはしない。 まずは、Server と Client がそれぞれちゃんと動くことを確認する。

下準備として Homebrew で uv をインストールしておく。

$ brew install uv

Tools

まずは Tools から見ていこう。

MCP Server

公式の Python SDK を使って実装した MCP Server のサンプルコードを次に示す。 この MCP Server では add()server_time() という 2 つの Tools を実装している。 add は引数を取って足し算をする。 server_time は引数なしでサーバの時刻を返す。

# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Tools を実装した MCP サーバ"""

from datetime import datetime

from mcp.server.fastmcp import FastMCP

mcp = FastMCP()


@mcp.tool(description="足し算をするツールです")
def add(a: int, b: int) -> int:
    return a + b


@mcp.tool(description="サーバのシステム時刻を返すツールです")
def server_time() -> str:
    now = datetime.now()
    return now.isoformat(sep=" ", timespec="seconds")


if __name__ == "__main__":
    mcp.run(transport="stdio")

MCP Client

続いて、同じように公式の SDK で実装した MCP Client のサンプルコードを以下に示す。 まずは公式の SDK を使って振る舞いを確認する。 サンプルコードは次のような流れになっている。

  • セッションを初期化する
  • 使用できる Tools のリストを得る
  • Tool を呼び出す
# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Tools を実装した MCP サーバを呼び出す MCP クライアント"""

import asyncio

from mcp import ClientSession
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

server_params = StdioServerParameters(
    command="uv",
    args=[
        # MCP サーバのスクリプトが別のディレクトリにある場合は以下で指定する
        # "--directory",
        # "/path/to/server/script/directory",
        "run",
        "toolmcpserver.py",
    ],
)


async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # セッションを初期化する
            await session.initialize()

            # 利用できるツールの一覧を得る
            items = await session.list_tools()
            print("Available tools:", [item.name for item in items.tools])

            # ツール (add) を呼び出す
            response = await session.call_tool("add", {"a": 1, "b": 2})
            result = response.structuredContent.get("result")
            print("Tool (add) result:", result)

            # ツール (server_time) を呼び出す
            response = await session.call_tool("server_time")
            result = response.structuredContent.get("result")
            print("Tool (server_time) result:", result)


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

それでは、実装した MCP Client のスクリプトを uv で実行してみよう。

$ uv run --script toolmcpclient.py
[07/27/25 00:55:04] INFO     Processing request of type ListToolsRequest                                               server.py:625
Available tools: ['add', 'server_time']
                    INFO     Processing request of type CallToolRequest                                                server.py:625
Tool (add) result: 3
                    INFO     Processing request of type CallToolRequest                                                server.py:625
Tool (server_time) result: 2025-07-27 00:55:04

ちゃんと MCP Server から処理に対応した結果が得られている。

自分で喋ってみる

続いては今回の本題である MCP を自分で喋ってみることにしよう。 要するに、先ほどの MCP Client を自分が代わりにやる。

それには、実装した MCP Server を uv で実行する。

$ uv run --script toolmcpserver.py

これで MCP Server と stdio トランスポートで会話できる。 stdio トランスポートの場合、それぞれのメッセージは改行で区切られる。

まずはクライアント側から initialize メソッドを呼び出す。 このメッセージで、クライアント側が対応しているプロトコルのバージョンや機能などの情報をサーバへ伝える。 なお、id は単に Requests と Responses の対応を取るためのもので、別に連番になっている必要はない。

> { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "Human", "title": "Human Powered Client", "version": "0.0.1" }}}

また、実際に試す際には先頭の > は必要ない。 これは分かりやすさのためにつけている。 以降はクライアントからサーバへのメッセージには > をつける。 また、サーバからクライアントへのメッセージには < をつける。

すると、initialize メソッドに対する返答がサーバから返ってくる。 このメッセージには、サーバが対応しているプロトコルのバージョンや機能の情報が含まれている。

< {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"FastMCP","version":"1.12.2"}}}

なお、サーバ側が出力するログの情報も本来は一緒に表示されるがここでは省略する。

次にクライアントからサーバに対して初期化が完了した旨の通知を送る。 これは先述した Notifications なので、一方通行であってサーバからの返答は無い。

> { "jsonrpc": "2.0", "method": "notifications/initialized" }

ちなみに、初期化しないまま後続のメッセージをやり取りしようとすると次のようなエラーメッセージになる。

< {"jsonrpc":"2.0","id":1,"error":{"code":-32602,"message":"Invalid request parameters","data":""}}

次に MCP Server が提供する Tools の一覧を得る。 これには tools/list メソッドを呼び出す。

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

すると、利用できる Tools の一覧が返答として得られる。 ここには説明や、呼び出す際の引数の情報などが含まれている。

< {"jsonrpc":"2.0","id":2,"result":{"tools":[{"name":"add","description":"足し算をするツールです","inputSchema":{"properties":{"a":{"title":"A","type":"integer"},"b":{"title":"B","type":"integer"}},"required":["a","b"],"title":"addArguments","type":"object"},"outputSchema":{"properties":{"result":{"title":"Result","type":"integer"}},"required":["result"],"title":"addOutput","type":"object"}},{"name":"server_time","description":"サーバのシステム時刻を返すツールです","inputSchema":{"properties":{},"title":"server_timeArguments","type":"object"},"outputSchema":{"properties":{"result":{"title":"Result","type":"string"}},"required":["result"],"title":"server_timeOutput","type":"object"}}]}}

実際に Tools を呼び出すには tools/call メソッドを使う。 ここでは引数のない server_time を呼んでいる。

> { "jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": { "name": "server_time" }}

呼び出すと、次のようにサーバ側で処理された結果が返ってくる。

< {"jsonrpc":"2.0","id":3,"result":{"content":[{"type":"text","text":"2025-07-27 01:23:07"}],"structuredContent":{"result":"2025-07-27 01:23:07"},"isError":false}}

引数があるものについては次のように arguments をつけて呼び出す。

> { "jsonrpc": "2.0", "id": 4, "method": "tools/call", "params": { "name": "add", "arguments": { "a": 1, "b": 2 }}}

結果については先ほどと変わらない。

< {"jsonrpc":"2.0","id":4,"result":{"content":[{"type":"text","text":"3"}],"structuredContent":{"result":3},"isError":false}}

これで先ほどの MCP Client と同じ流れを自分で体験できた。

Resources

次は Resources についても同じ流れを試す。

MCP Server

次の MCP Server のコードでは greeting()echo() という 2 つの Resources を実装している。 greeting は定数の文字列を返す。 echo は URI に埋め込まれた内容をそのまま返す。

# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Resources を実装した MCP サーバ"""

from mcp.server.fastmcp import FastMCP

mcp = FastMCP()


@mcp.resource("greeting://", description="挨拶をするリソースです")
def greeting():
    return "Hello, World!"


@mcp.resource("echo://{message}", description="エコーを返すリソースです")
def echo(message: str) -> str:
    return message


if __name__ == "__main__":
    mcp.run(transport="stdio")

MCP Client

対応する MCP Client のサンプルコードを以下に示す。 Tools と同様に以下の流れになっている。

  • 初期化する
  • 利用できる Resources のリストを得る
  • Resources を読み取る

なお、URI に何らかの情報が含まれる Resources は Resource Templates として別物になっている。

# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Resources を実装した MCP サーバを呼び出す MCP クライアント"""

import asyncio

from mcp import ClientSession
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

server_params = StdioServerParameters(
    command="uv",
    args=[
        # "--directory",
        # "/path/to/server/script/directory",
        "run",
        "resourcemcpserver.py",
    ],
)


async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # セッションを初期化する
            await session.initialize()

            # 利用できるリソースの一覧を得る
            items = await session.list_resources()
            print("Available resources:", [item.name for item in items.resources])

            # 利用できるリソーステンプレートの一覧を得る
            items = await session.list_resource_templates()
            print("Available resource templates:", [item.name for item in items.resourceTemplates])

            # リソース (greeting) を呼び出す
            response = await session.read_resource("greeting://")
            print("Resource (greeting) response:", response)

            # リソーステンプレート (echo) を呼び出す
            response = await session.read_resource("echo://hello")
            print("Resource (echo) response:", response)


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

実装した MCP Client を実行してみよう。

$ uv run --script resourcemcpclient.py
[08/04/25 03:43:45] INFO     Processing request of type ListResourcesRequest                                           server.py:625
Available resources: ['greeting']
                    INFO     Processing request of type ListResourceTemplatesRequest                                   server.py:625
Available resource templates: ['echo']
                    INFO     Processing request of type ReadResourceRequest                                            server.py:625
Resource (greeting) response: meta=None contents=[TextResourceContents(uri=AnyUrl('greeting://'), mimeType='text/plain', meta=None, text='Hello, World!')]
                    INFO     Processing request of type ReadResourceRequest                                            server.py:625
Resource (echo) response: meta=None contents=[TextResourceContents(uri=AnyUrl('echo://hello'), mimeType='text/plain', meta=None, text='hello')]

ちゃんと Resources のリストの取得や読み取りができている。

自分で喋ってみる

次は、先ほどと同様に自分が MCP Client となって Server とやり取りしてみよう。

uv を使って MCP Server のスクリプトを起動する。

$ uv run --script resourcemcpserver.py

まずは Tools の場合と同様に初期化する。

> { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "Human", "title": "Human Powered Client", "version": "0.0.1" }}}
< {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"FastMCP","version":"1.12.2"}}}
> { "jsonrpc": "2.0", "method": "notifications/initialized" }

次に利用できる Resources の一覧を resources/list メソッドで得る。

> { "jsonrpc": "2.0", "id": 2, "method": "resources/list" }
< {"jsonrpc":"2.0","id":2,"result":{"resources":[{"name":"greeting","uri":"greeting://","description":"挨拶をするリソースです","mimeType":"text/plain"}]}}

同様に、利用できる Resource Templates の一覧を resources/templates/list で得る。

> { "jsonrpc": "2.0", "id": 3, "method": "resources/templates/list" }
< {"jsonrpc":"2.0","id":3,"result":{"resourceTemplates":[{"name":"echo","uriTemplate":"echo://{message}","description":"エコーを返すリソースです"}]}}

greeting:// の Resources を resources/read メソッドで読み取る。

> { "jsonrpc": "2.0", "id": 4, "method": "resources/read", "params": { "uri": "greeting://" }}
< {"jsonrpc":"2.0","id":4,"result":{"contents":[{"uri":"greeting://","mimeType":"text/plain","text":"Hello, World!"}]}}

結果として定数の文字列が返ってきた。

同様に echo:// の Resource Templates を読み取る。 読み取る場合のメソッドは Resources と Resource Templates で違いはないようだ。

> { "jsonrpc": "2.0", "id": 5, "method": "resources/read", "params": { "uri": "echo://hello" }}
< {"jsonrpc":"2.0","id":5,"result":{"contents":[{"uri":"echo://hello","mimeType":"text/plain","text":"hello"}]}}

Prompts

次は Prompts でも同じ流れを確認する。

MCP Server

以下に Prompts を実装した MCP Server のサンプルコードを示す。 サンプルコードでは summarize というプロンプトを実装している。

# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Prompts を実装した MCP サーバ"""

from mcp.server.fastmcp import FastMCP

mcp = FastMCP()


@mcp.prompt(description="要約に使うプロンプトです")
def summarize(topic: str) -> str:
    return f"{topic}について分かりやすく要約してください"


if __name__ == "__main__":
    mcp.run(transport="stdio")

MCP Client

対応する MCP Client のサンプルコードを以下に示す。 Tools や Resources と同様に以下の流れになっている。

  • 初期化する
  • 利用できる Prompts のリストを得る
  • Promts を得る
# /// script
# requires-python = ">=3.9"
# dependencies = [
#     "mcp[cli]",
# ]
# ///

"""サンプルの Prompts を実装した MCP サーバを呼び出す MCP クライアント"""

import asyncio

from mcp import ClientSession
from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

server_params = StdioServerParameters(
    command="uv",
    args=[
        # "--directory",
        # "/path/to/server/script/directory",
        "run",
        "promptmcpserver.py",
    ],
)


async def main():
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            # セッションを初期化する
            await session.initialize()

            # 利用できるプロンプトの一覧を得る
            items = await session.list_prompts()
            print("Available prompts:", [item.name for item in items.prompts])

            # プロンプト (summarize) を呼び出す
            response = await session.get_prompt("summarize", {"topic": "生命、宇宙、そして万物についての究極の疑問の答え"})
            print("Prompts (summarize) response:", response)


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

上記を uv で実行してみよう。

$ uv run --script promptmcpclient.py
[08/04/25 04:12:23] INFO     Processing request of type ListPromptsRequest                                                                               server.py:625
Available prompts: ['summarize']
                    INFO     Processing request of type GetPromptRequest                                                                                 server.py:625
Prompts (summarize) response: meta=None description='要約に使うプロンプトです' messages=[PromptMessage(role='user', content=TextContent(type='text', text='生命、宇宙、そして万物についての究極の疑問の答えについて分かりやすく要約してください', annotations=None, meta=None))]

クライアントからの入力に基づいてプロンプトが得られている。

自分で喋ってみる

次は先ほどと同様に自分が MCP Client となって Server とやり取りしてみよう。

uv を使って MCP Server のスクリプトを起動する。

$ uv run --script promptmcpserver.py

まずは初期化する。

> { "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-06-18", "capabilities": {}, "clientInfo": { "name": "Human", "title": "Human Powered Client", "version": "0.0.1" }}}
< {"jsonrpc":"2.0","id":1,"result":{"protocolVersion":"2025-06-18","capabilities":{"experimental":{},"prompts":{"listChanged":false},"resources":{"subscribe":false,"listChanged":false},"tools":{"listChanged":false}},"serverInfo":{"name":"FastMCP","version":"1.12.2"}}}
> { "jsonrpc": "2.0", "method": "notifications/initialized" }

利用できるプロンプトの一覧を得る。

> { "jsonrpc": "2.0", "id": 2, "method": "prompts/list" }
< {"jsonrpc":"2.0","id":2,"result":{"prompts":[{"name":"summarize","description":"要約に使うプロンプトです","arguments":[{"name":"topic","required":true}]}]}}

プロンプトの summarize を得る。

> { "jsonrpc": "2.0", "id": 3, "method": "prompts/get", "params": { "name": "summarize", "arguments": { "topic": "生命、宇宙、そして万物についての究極の疑問の答え" }}}
< {"jsonrpc":"2.0","id":3,"result":{"description":"要約に使うプロンプトです","messages":[{"role":"user","content":{"type":"text","text":"生命、宇宙、そして万物についての究極の疑問の答えについて分かりやすく要約してください"}}]}}

入力した内容が埋め込まれたプロンプトが得られた。

まとめ

今回は Model Context Protocol (MCP) をプロトコルという側面から扱った。 MCP は JSON-RPC 2.0 をベースにしたプロトコルで、Server / Client / Host という 3 つのコンポーネントが登場する。 今回はその中で Server と Client を実装した上で、Client になりきって自分で MCP を喋ってみた。 実際に自分でサーバとメッセージをやり取りすることで、どのようなプロトコルなのか理解を深めることができた。