MCP (Model Context Protocol) の仕組みを知りたい！

[Model Context Protocol]:https://www.anthropic.com/news/model-context-protocol
[Claude for Desktop]:https://claude.ai/download
[Cursor]:https://www.cursor.com/
[Cline]:https://github.com/cline/cline

[tmux]:https://formulae.brew.sh/formula/tmux
[@modelcontextprotocol/server-filesystem]:https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem
[JSON-RPC]:https://www.jsonrpc.org/

[Supabase]:https://supabase.com/


# この記事は何？

この記事は MCP ([Model Context Protocol]) の仕組みについて僕が調べたことを書いています。

MCP って何？ や使い方については既に Anthropic を始め、公式の記事が充実しています。

なので、内部の仕組みを知らなくても普通に使えはするのですが、僕はどうしても **「MCP ってどういう仕組み（内部が〜・仕様が〜）で動いてるの？」** が気になってしまうので調べました... 😅

# MCP について

MCP の概要については、昨年11月に Anthropic がとても親切な記事を書いてくれています。

https://www.anthropic.com/news/model-context-protocol

実際の使い方 (Client) だけなら、次の記事を読めばすぐに始められます。

https://modelcontextprotocol.io/introduction

また、MCP Server を自作するのも簡単です。

https://qiita.com/megmogmog1965/items/8bbcb6fb856ab2eba446

## 仕組み概要

簡単に説明をすると、MCP の仕組みを用いると AI Agent (MCP Client) が MCP Server を介して外部の様々なリソースに自律的にアクセス（情報取得・変更処理）を行えるようになります。

例えば...

* [Claude for Desktop] の AI が、MCP Server を介してローカルのファイルを読み書きできたり
* [Cursor] や [Cline] がエラーの解消の目的で、MCP Server を介してデータベースの中を見ることができたり

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/9ff47aca-746a-5a41-0534-5c66856d2135.png)

https://modelcontextprotocol.io/examples

# 実際に動かしてみる

ということで、実際に動かしてみて仕組みを探ります。
公式のこれ ↓ に従うと Python (& Anthropic API) で MCP Client コードが書けます。

https://modelcontextprotocol.io/quickstart/client

実際に私が書いたものが ↓ です。少し改変して `npx` で提供されている MCP Server を自由に使えるようにしました。

https://github.com/megmogmog1965/example-mcp-client

:::note
自分でコードを書いたほうが、MCP Client ⇔ Server 間の動作の解析はしやすいので今回はわざとそうしています。
例としては [Claude for Desktop], [Cursor] での利用の方が分かりやすい為それも後述します。
:::

## 動作例 1️⃣ (自作クライアント)

実際に Client コード (Anthropic API) を動かしてみます。

一緒に立ち上げる MCP Server は [@modelcontextprotocol/server-filesystem] にしました。この MCP Server を使うと、AI がローカルのファイルを参照できるようになります。

```bash
uv run client.py npx -y @modelcontextprotocol/server-filesystem "/Users/{username}/Desktop/"
```

:::note
client.py スクリプトに引数で渡しているコマンド ↓ が、MCP Server 単体の起動コマンドです。

```bash
npx -y @modelcontextprotocol/server-filesystem "/Users/{username}/Desktop/"
```
:::

AI (Anthropic API) に「私のデスクトップ上のファイルを一覧して！」とお願いしてみました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/392c6425-9140-d0fe-8b7e-324e56a4a90a.png)

すると、次の通りデスクトップのファイル一覧を参照した上で回答をしくれます。

```bash
はい、デスクトップのファイルを一覧表示するために`list_directory`関数を使用します。指定されたパスで実行してみましょう。
[Calling tool list_directory with args {'path': '/Users/yuusuke-kawatsu/Desktop'}]
デスクトップ上のファイルとディレクトリを以下のように整理してお伝えします：

主なファイル:
1. PDFファイル:
   - xxx.pdf

2. 画像ファイル:
   - 各種PNG/JPG/GIFファイル (GREAT.png, LGTM.gif など)
   - スクリーンショット画像
   - プロフィール関連画像 (face.jpg, face_circle.png)

3. 開発関連ファイル:
   - index.html
   - index.js

4. 機密/業務関連ファイル:
   - xxx

システムファイル:
- .DS_Store
- .localized
```

## 動作例 2️⃣ ([Claude for Desktop])

まず [Claude for Desktop] を起動して、設定を開きましょう。

* ` Claude` >> `Settings...` >> `Developer` >> `Edit Config`

すると `claude_desktop_config.json` が表示されるので、開いて下記の通り編集します。

```json:claude_desktop_config.json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/{username}/Desktop/",
        "/Users/{username}/Downloads/"
      ]
    }
  }
}
```

:::note warn
zsh 経由ではないので `npx` はパスが通っていない可能性が高いです。
その場合は `/Users/{username}/.volta/bin/npx` など絶対パス指定をすると良いですね。
:::

実際に問い合わせてみる。

![Pasted_Image_2025_02_07__10_44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/a2939345-2b48-a14d-293d-86b3cd38a061.png)

## 動作例 3️⃣ ([Cursor])

[Cursor] の場合も設定から MCP Server を登録できます。

* ` Cursor` >> `Settings...` >> `Cursor Settings` >> `MCP` >> `+ Add new global MCP server`

![Pasted_Image_2025_04_07__9_49.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/0fbc52b2-704e-49c2-8338-c3380cc08cf0.png)

設定ファイル `mcp.json` が表示されるので、以下の様に編集します。

```json:mcp.json
{
  "mcpServers": {
    "PostgREST": {
      "command": "npx",
      "args": [
        "-y",
        "@supabase/mcp-server-postgrest",
        "--apiUrl",
        "https://{supabase のプロジェクトID}.supabase.co/rest/v1",
        "--apiKey",
        "{supabase のサービスロール API Key}",
        "--schema",
        "public"
      ]
    }
  }
}
```

:::note
[Cursor] はそもそもファイルを読み書きする tools を内包しているので、ここではデータベースのクエリ検索ができる PostgREST ([@supabase/mcp-server-postgrest](https://github.com/supabase-community/mcp-supabase/tree/main/packages/mcp-server-postgrest)) を試しに設定しています。
:::

:::note
ちなみにこの例ではデータベースは [Supabase] (PostgreSQL) を使っています。 
:::

:::note warn
zsh 経由ではないので `npx` はパスが通っていない可能性が高いです。
その場合は `/Users/{username}/.volta/bin/npx` など絶対パス指定をすると良いですね。
:::

実際に Cursor Composer で問い合わせをしてみるとこう。

![cursor_mcp_usage.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/743d18f7-3e6d-75a0-34e3-48e399b71dd8.png)


# Client ⇔ Server 間の通信

MCP は Client ⇔ Server 間の通信プロトコルです。

MCP 自体は [JSON-RPC] 仕様に基づいています。
そのため、プロトコル下層の Client ⇔ Server 間のネットワーク経路は様々な手段を取れます。

たとえば前章の動かしてみたケースは「標準入出力を用いたプロセス間通信」でした。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/60131/864d6da4-6668-9fe2-45d7-d8d62f877e9e.png)

MCP の具体的なスキーマ仕様は以下の Github repo で定義管理されています。

https://github.com/modelcontextprotocol/specification/tree/main/schema

# 実際に MCP 通信の本文 (req/res) を覗いてみる！

MCP Server との通信は [JSON-RPC] メッセージを標準入出力でやりとりするだけです。
実際に Mac の Terminal 上で試してみましょう！

## [tmux] の使い方

今回、MCP Server プロセスと標準入出力で通信をするために [tmux] を用います。

```bash
brew install tmux
```

[tmux] セッションを経由して、MCP Server ([@modelcontextprotocol/server-filesystem]) のプロセスを起動します。

```bash
tmux new-session -d -s my_session \
  "npx -y @modelcontextprotocol/server-filesystem /Users/yuusuke-kawatsu/Desktop/"
```

早速、MCP Server へ標準入力で [JSON-RPC] リクエストを送信します。

```bash
tmux send-keys -t my_session \
  '{"method": "initialize", "params": {"protocolVersion": "2024-11-05", "capabilities": {"roots": {"listChanged": true}}, "clientInfo": {"name": "mcp", "version": "0.1.0"}}, "jsonrpc": "2.0", "id": 0}' \
  C-m
```

:::note
↑ メッセージの意味は次章で説明します。
:::

すると、MCP Server から標準出力で応答が返ります。

```bash
tmux capture-pane -t my_session -p
```

```bash
{"jsonrpc": "2.0", "id": 0, "result": {"protocolVersion": "2024-11-05", "capabilities": {"tools": {}}, "serverInfo": {"name": "secure-filesystem-server", "version": "0.2.0"}}}
```

(ちゃんと tmux session を片付けておく)

```bash
tmux kill-session -t my_session
```

## 通信の中身

ここからは、Client ⇔ Server 間で具体的にどのようなメッセージ内容のやり取りが行われているか？ を見ていきましょう。

MCP Server は引き続き [@modelcontextprotocol/server-filesystem] を例にしています。

```mermaid
sequenceDiagram
    activate Client
    Note over Client, Server: Initialization Phase
    Client ->> Server: 1. 初期化メッセージの送信
    activate Server
    Server -->> Client: 応答
    Client -->> Server: 2. クライアント側の準備OK通知

    Note over Client, Server: Operation Phase
    Client ->> Server: 3. 利用可能な tools を問い合わせする
    Server -->> Client: ツールの一覧
    Client ->> Server: 4. tools の使用を要求
    Server -->> Client: ツール実行結果
    deactivate Server
    deactivate Client
```


:::note
前述のサンプルコードで動かした際の Client / Server 間の標準入出力を覗き見て解析しました。

https://qiita.com/megmogmog1965/items/9d4597b42eba18b5e233
:::

### 1. 初期化メッセージの送信

Client から MCP Server へ、初期化の指示を送っています。

```json:Request
{
  "method": "initialize",
  "params": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "roots": {
        "listChanged": true
      }
    },
    "clientInfo": {
      "name": "mcp",
      "version": "0.1.0"
    }
  },
  "jsonrpc": "2.0",
  "id": 0
}
```

完了したよ！の応答。

```json:Response
{
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "protocolVersion": "2024-11-05",
    "capabilities": {
      "tools": {}
    },
    "serverInfo": {
      "name": "secure-filesystem-server",
      "version": "0.2.0"
    }
  }
}
```

### 2. クライアント側の準備OK通知

Client 側の準備が完了したことを MCP Server へ通知しています。

```json:Request
{
  "method": "notifications/initialized",
  "jsonrpc": "2.0",
  "params": null
}
```

method が `notifications` の場合は MCP Server からの応答はありません。

### 3. Client が MCP Server へ、利用可能な tools を問い合わせする

Client が MCP Server へ、どのような tools が利用可能か？ を問い合わせしています。

これによって AI は、状況に応じて tools の利用可否を自律的に判断出来るようになります。

```json:Request
{
  "method": "tools/list",
  "params": null,
  "jsonrpc": "2.0",
  "id": 1
}
```

(長いので大分省略しましたが) tools に関する説明とインターフェース仕様についての応答ですね。

```json:Response
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "read_file",
        "description": "Read the complete contents of a file from the file system. Handles various text encodings and provides detailed error messages if the file cannot be read. Use this tool when you need to examine the contents of a single file. Only works within allowed directories.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "path": {
              "type": "string"
            }
          },
          "required": [
            "path"
          ],
          "additionalProperties": false,
          "$schema": "http://json-schema.org/draft-07/schema#"
        }
      },
      {
        "name": "read_multiple_files",
        "description": "Read the contents of multiple files simultaneously. This is more efficient than reading files one by one when you need to analyze or compare multiple files. Each file's content is returned with its path as a reference. Failed reads for individual files won't stop the entire operation. Only works within allowed directories.",
        "inputSchema": { ... }
      },
      {
        "name": "write_file",
        "description": "Create a new file or completely overwrite an existing file with new content. Use with caution as it will overwrite existing files without warning. Handles text content with proper encoding. Only works within allowed directories.",
        "inputSchema": { ... }
      },
      {
        "name": "edit_file",
        "description": "Make line-based edits to a text file. Each edit replaces exact line sequences with new content. Returns a git-style diff showing the changes made. Only works within allowed directories.",
        "inputSchema": { ... }
      },
      {
        "name": "create_directory",
        "description": "Create a new directory or ensure a directory exists. Can create multiple nested directories in one operation. If the directory already exists, this operation will succeed silently. Perfect for setting up directory structures for projects or ensuring required paths exist. Only works within allowed directories.",
        "inputSchema": { ... }
      },
      {
        "name": "list_directory",
        "description": "Get a detailed listing of all files and directories in a specified path. Results clearly distinguish between files and directories with [FILE] and [DIR] prefixes. This tool is essential for understanding directory structure and finding specific files within a directory. Only works within allowed directories.",
        "inputSchema": {
          "type": "object",
          "properties": {
            "path": {
              "type": "string"
            }
          },
          "required": [
            "path"
          ],
          "additionalProperties": false,
          "$schema": "http://json-schema.org/draft-07/schema#"
        }
      },
      {
        "name": "directory_tree",
        "description": "Get a recursive tree view of files and directories as a JSON structure. Each entry includes 'name', 'type' (file/directory), and 'children' for directories. Files have no children array, while directories always have a children array (which may be empty). The output is formatted with 2-space indentation for readability. Only works within allowed directories.",
        "inputSchema": { ... }
      },
      {
        "name": "move_file",
        "description": "Move or rename files and directories. Can move files between directories and rename them in a single operation. If the destination exists, the operation will fail. Works across different directories and can be used for simple renaming within the same directory. Both source and destination must be within allowed directories.",
        "inputSchema": { ... }
      },
      {
        "name": "search_files",
        "description": "Recursively search for files and directories matching a pattern. Searches through all subdirectories from the starting path. The search is case-insensitive and matches partial names. Returns full paths to all matching items. Great for finding files when you don't know their exact location. Only searches within allowed directories.",
        "inputSchema": { ... }
      },
      {
        "name": "get_file_info",
        "description": "Retrieve detailed metadata about a file or directory. Returns comprehensive information including size, creation time, last modified time, permissions, and type. This tool is perfect for understanding file characteristics without reading the actual content. Only works within allowed directories.",
        "inputSchema": { ... }
      },
      {
        "name": "list_allowed_directories",
        "description": "Returns the list of directories that this server is allowed to access. Use this to understand which directories are available before trying to access files.",
        "inputSchema": { ... }
      }
    ]
  }
}
```

### 4. Client が MCP Server へ tools の使用を要求

tool `list_directory` の使用を要求しています。`arguments` で対象のディレクトリを指定していますね。

```json:Request
{
  "method": "tools/call",
  "params": {
    "name": "list_directory",
    "arguments": {
      "path": "/Users/yuusuke-kawatsu/Desktop"
    }
  },
  "jsonrpc": "2.0",
  "id": 3
}
```

(恥ずかしいのでふせましたが...😅) 私のデスクトップ上のファイル一覧が返ってきています。

```json:Response
{
  "jsonrpc": "2.0",
  "id": 3,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "[FILE] .DS_Store\n[FILE] 20240705_aaaaa.pdf\n[DIR] images\n"
      }
    ]
  }
}
```