AI アシスタントが外部のツールやデータにアクセスする仕組みとして、Model Context Protocol(MCP)が急速に普及しています。Claude Code、Cursor、Windsurf といった AI 開発ツールがこぞって対応し、MCP サーバーのエコシステムは日々拡大しています。
筆者は最近、Common Lisp(SBCL)で MCP サーバーを自作しました。実装を進める中で、MCP がどんな技術の上に成り立っているのかを体系的に整理したくなったのが、この記事を書いたきっかけです。
この記事では、MCP の公式オープン仕様に基づいて、プロトコルの全体像と構成技術を整理します。各構成技術(JSON-RPC 2.0 など)の詳細は別の記事で掘り下げる予定です。ここではMCPの中で各技術がどんな役割を果たしているかに焦点を当てます。
MCPとは
MCP(Model Context Protocol)は、LLM アプリケーションと外部システムを接続するためのオープンプロトコルです。2024年11月に Anthropic が公開し、現在は Linux Foundation がホストするオープンな仕様として管理されています。
公式サイトでは「AI アプリケーションの USB-C」と表現されており、デバイスを接続する標準規格のように、AI アプリケーションと外部システムの接続方法を標準化する役割を担っています。
- 公式サイト: modelcontextprotocol.io
- 仕様: MCP Specification 2025-11-25(最新版)
- GitHub: github.com/modelcontextprotocol
MCPが解決する問題
MCP が登場する以前は、LLM を外部ツールやデータベースに接続するには、ツールごとに個別のインテグレーションを実装する必要がありました。ツールが10個あれば10通りの接続方法を書くことになり、AI アプリケーション側の負担が大きくなっていました。
MCPはこの「M×N問題」を解決します。すべてのツールがMCPという共通プロトコルで通信すれば、AI アプリケーション側はMCPクライアントを1つ実装するだけで、任意の MCP サーバーに接続できます。
MCPのアーキテクチャ
MCP は、Host・Client・Server の3つのコンポーネントで構成されています。
Host(例: Claude Code, Cursor)├── Client 1 ←→ Server A(ファイルシステム)├── Client 2 ←→ Server B(データベース)└── Client 3 ←→ Server C(外部API)Host
Host は、ユーザーが直接操作するアプリケーションです。Claude Code や Cursor などの AI 開発ツールが該当します。Host は Client インスタンスの生成・管理を行い、セキュリティポリシーの適用やユーザーの許可判断を制御します。
Client
Client は、1つの Server との通信セッションを管理するコンポーネントです。Host の内部で動作し、プロトコルバージョンの交渉(ネゴシエーション)やメッセージのルーティングを担当します。Client と Server は 1:1 の関係で接続されます。
Server
Server は、外部のリソースやツールを MCP プロトコルで公開するプロセスです。ファイルシステムへのアクセス、データベースへのクエリ実行、外部 API の呼び出しなど、様々な機能を MCP の統一されたインターフェースで提供します。
MCPを支える構成技術
MCP は複数の既存技術を組み合わせて設計されています。ここでは各技術がMCPの中でどのような役割を果たしているかを概観します。
JSON-RPC 2.0 — メッセージングの基盤
MCP のすべてのメッセージは、JSON-RPC 2.0 仕様に従います。JSON-RPC は、JSON 形式でリモートプロシージャコール(RPC)を行うための軽量なプロトコルです。
MCP では以下の4種類のメッセージが使われます。
| メッセージ種別 | 説明 | IDフィールド |
|---|---|---|
| リクエスト | 操作を開始する(レスポンスを期待する) | 必須 |
| 成功レスポンス | リクエストの結果を返す | リクエストと同じID |
| エラーレスポンス | エラーコードとメッセージを返す | リクエストと同じID |
| 通知 | 一方向のメッセージ(レスポンス不要) | なし |
実際のメッセージの例を見てみましょう。Client が Server に対して利用可能なツールの一覧を要求する場合、以下のようなやり取りになります。
{ "jsonrpc": "2.0", "id": 1, "method": "tools/list", "params": {}}{ "jsonrpc": "2.0", "id": 1, "result": { "tools": [ { "name": "read_file", "description": "ファイルの内容を読み取る", "inputSchema": { "type": "object", "properties": { "path": { "type": "string" } }, "required": ["path"] } } ] }}JSON-RPC 2.0 を採用したことで、MCPサーバーの実装は言語やフレームワークに依存しません。JSON のパースと RPC のディスパッチを実装すれば、どの言語でもMCPサーバーを構築できます。
トランスポート層 — stdio と Streamable HTTP
JSON-RPC のメッセージをどうやって運ぶか、それがトランスポート層の役割です。MCP は2つの標準トランスポートを定義しています。
stdio トランスポート
stdin/stdout を使った最もシンプルなトランスポートです。
Client(親プロセス) │ ├── stdin → Server(子プロセス)に JSON-RPC メッセージを送信 └── stdout ← Server から JSON-RPC メッセージを受信Client が Server をサブプロセスとして起動し、標準入出力でメッセージをやり取りします。各メッセージは改行(\n)で区切られた1行の JSON です。stderr はログ出力に使用できます。
stdio は設定が簡単で、ローカル環境での開発に適しています。Claude Code や Cursor で MCP サーバーを使う場合、ほとんどがこの方式です。
Streamable HTTP トランスポート
HTTP ベースのトランスポートで、リモートサーバーとの通信に適しています。2025年3月の仕様改訂で、それまでの HTTP+SSE 方式に代わって導入されました。
- Client → Server: HTTP POST でリクエストを送信
- Server → Client: レスポンスを直接返すか、Server-Sent Events(SSE)でストリーミング
- Server → Client(サーバー起点): HTTP GET で SSE ストリームを確立
セッション管理には Mcp-Session-Id ヘッダーが使われ、プロトコルバージョンは Mcp-Protocol-Version ヘッダーで指定します。
Server-Sent Events(SSE)
Server-Sent Events は、サーバーからクライアントへの一方向のリアルタイムデータ配信を実現する Web 標準技術です。
MCP の Streamable HTTP トランスポートでは、サーバーがクライアントに対して非同期にメッセージを送信する手段として SSE を利用しています。長時間実行されるツールの進捗通知や、サーバー起点の通知配信に活用されます。
OAuth 2.1 — 認証・認可
リモートの MCP サーバーに接続する場合、認証と認可が必要です。MCP は HTTP ベースのトランスポートに対して、OAuth 2.1 に基づく認可フレームワークを定義しています。
関連する仕様として以下が参照されています。
- RFC 8414 — OAuth 2.0 Authorization Server Metadata
- RFC 7591 — OAuth 2.0 Dynamic Client Registration
- RFC 9728 — OAuth 2.0 Protected Resource Metadata
- RFC 8707 — Resource Indicators for OAuth 2.0
stdio トランスポートの場合は、プロセス間通信のためOAuthは通常不要です。認証が必要なリモートサーバーを公開する場合に関係します。
JSON Schema — ツールの入力定義
MCP のツールは、入力パラメータの構造を JSON Schema で定義します。これにより、LLM がツールの使い方を理解し、正しい形式で引数を渡せるようになります。
{ "name": "search_files", "description": "パターンに一致するファイルを検索する", "inputSchema": { "type": "object", "properties": { "pattern": { "type": "string", "description": "検索するglobパターン" }, "directory": { "type": "string", "description": "検索対象のディレクトリ" } }, "required": ["pattern"] }}JSON Schema による型定義は、LLM がツールを正しく呼び出すための重要な手がかりになります。description フィールドは LLM が引数の意味を理解するためのものなので、明確に記述することが推奨されます。
Capability Negotiation — 機能交渉
MCP では、接続の初期化時に Client と Server が互いのサポート機能を宣言します。これを Capability Negotiation(機能交渉)と呼びます。
{ "jsonrpc": "2.0", "id": 1, "method": "initialize", "params": { "protocolVersion": "2025-11-25", "capabilities": { "roots": { "listChanged": true } }, "clientInfo": { "name": "claude-code", "version": "1.0.0" } }}{ "jsonrpc": "2.0", "id": 1, "result": { "protocolVersion": "2025-11-25", "capabilities": { "tools": { "listChanged": true } }, "serverInfo": { "name": "my-mcp-server", "version": "0.1.0" } }}この仕組みにより、Client と Server のバージョンが異なっていても、共通してサポートする機能の範囲で通信できます。プロトコルの後方互換性と段階的な機能拡張を両立する設計です。
MCPの3つの主要機能
MCP サーバーは、以下の3つの機能カテゴリを通じて LLM にコンテキストを提供します。
Resources — データの提供
Resources は、サーバーが保持するデータやコンテンツを LLM に提供する機能です。ファイルの内容、データベースのスキーマ、API のレスポンスなどが該当します。
Resources はアプリケーションが制御する機能です。ユーザーやアプリケーションが必要なリソースを選択し、LLM のコンテキストに追加します。
Tools — アクションの実行
Tools は、LLM が呼び出せる関数です。ファイルの作成、データベースへのクエリ、外部 API の呼び出しなど、実際のアクションを実行します。
Tools はモデルが制御する機能です。LLM が会話の文脈に応じて、どのツールをどの引数で呼び出すかを判断します。ただし、実行前にユーザーの承認を求めることが推奨されています。
Prompts — テンプレートの提供
Prompts は、再利用可能なプロンプトテンプレートやワークフローです。特定のタスクに最適化された指示をパッケージ化し、ユーザーが選択して利用できます。
Prompts はユーザーが制御する機能です。スラッシュコマンドやメニューから選択する形で利用されることが一般的です。
3つの機能の比較
| 機能 | 制御主体 | 用途 | 例 |
|---|---|---|---|
| Resources | アプリケーション | データの参照 | ファイル内容、DBスキーマ |
| Tools | LLM(モデル) | アクションの実行 | ファイル作成、API呼び出し |
| Prompts | ユーザー | テンプレート利用 | コードレビュー手順 |
MCPサーバーを自作してみて
筆者は Common Lisp(SBCL)で swank-mcp という MCPサーバーを実装しました。実行中の Lisp 環境(Swank サーバー)に接続し、Claude から直接 Common Lisp のコードを評価・補完・検索できるようにするツールです。
きっかけは、Claude Code で Common Lisp のコードを書く際に「REPL で試しながら開発したい」と思ったことでした。Emacs の SLIME が Swank プロトコルで Lisp 環境と対話するように、Claude にも同じことができないかと考え、MCP サーバーとして Swank クライアントをラップする方針で開発を始めました。
stdio ループの実装は驚くほどシンプル
MCP サーバーの核心は、stdin から JSON を読み、処理して stdout に返すループです。実際の Common Lisp による実装は以下のようになりました。
(defun stdio-loop () (loop for line = (read-line *standard-input* nil nil) while line do (unless (string= line "") (let* ((request (parse-json-rpc line)) (response (dispatch-method request))) (when response (write-string (encode-json-rpc response) *standard-output*) (terpri *standard-output*) (force-output *standard-output*))))))stdin から1行ずつ読み、JSON-RPC としてパースし、メソッド名に応じてハンドラーにディスパッチし、結果を stdout に書き出す。HTTP サーバーのセットアップも WebSocket も不要で、標準入出力だけで完結します。
実装が必要なメソッドは3つだけ
MCP の仕様は広範ですが、最小限のツールサーバーに必要なメソッドハンドラーは initialize、tools/list、tools/call の3つだけでした。
(register-handler "initialize" (lambda (params id) (declare (ignore params)) (make-response id `((:protocol-version . "2025-03-26") (:capabilities . ((:tools . ((:list-changed . t))))) (:server-info . ((:name . "sbcl-mcp-server") (:version . "0.1.0")))))))initialize でプロトコルバージョンとサポート機能を返し、tools/list で登録済みツールの一覧を返し、tools/call でツールを実行する。この3つを実装した時点で Claude Code から接続でき、ツールが呼び出せるようになりました。
ツール定義の description が LLM の挙動を左右する
ツール登録では、description と inputSchema を JSON Schema 形式で定義します。
(register-tool "swank-eval" "Evaluate a Common Lisp expression on the connected Swank server and return the result" '((:type . "object") (:properties . ((:expression . ((:type . "string") (:description . "Common Lisp expression to evaluate"))) (:package . ((:type . "string") (:description . "Package context for evaluation (default: CL-USER)"))))) (:required . #("expression"))) #'swank-eval-handler)開発中に気づいたのは、description の書き方で LLM の挙動が大きく変わることです。当初は簡素な説明にしていましたが、「何をするツールか」「どんな値を渡すべきか」を具体的に書くほど、Claude が適切にツールを選択・呼び出すようになりました。
cl-json のキャメルケース自動変換に注意
Common Lisp で JSON を扱う際に使った cl-json ライブラリには、キーの命名規則を自動変換する機能があります。Lisp 側のキーワードシンボル :protocol-version が JSON では protocolVersion にキャメルケース変換されます。MCP の仕様ではキーがキャメルケース(protocolVersion、inputSchema)なので、この自動変換がちょうどうまく噛み合いました。ただし、ツール名のように変換されたくない文字列もあるため、変換ルールの把握は必要です。
構成技術マップ
最後に、この記事で取り上げた構成技術の関係を整理します。
┌───────────────────────────────────────────┐│ MCP(Model Context Protocol) ││ ││ ┌─────────────────────────────────────┐ ││ │ 機能層 │ ││ │ Resources / Tools / Prompts │ ││ │ Capability Negotiation │ ││ │ JSON Schema(ツール入力定義) │ ││ └─────────────────────────────────────┘ ││ ││ ┌─────────────────────────────────────┐ ││ │ メッセージング層 │ ││ │ JSON-RPC 2.0 │ ││ └─────────────────────────────────────┘ ││ ││ ┌─────────────────────────────────────┐ ││ │ トランスポート層 │ ││ │ stdio / Streamable HTTP(SSE) │ ││ └─────────────────────────────────────┘ ││ ││ ┌─────────────────────────────────────┐ ││ │ 認証・認可層 │ ││ │ OAuth 2.1 │ ││ └─────────────────────────────────────┘ │└───────────────────────────────────────────┘| 構成技術 | MCPにおける役割 |
|---|---|
| JSON-RPC 2.0 | Client-Server 間のメッセージフォーマットと RPC の基盤 |
| stdio | ローカル環境でのプロセス間通信トランスポート |
| Streamable HTTP | リモート環境での HTTP ベーストランスポート |
| Server-Sent Events | サーバーからクライアントへの非同期メッセージ配信 |
| OAuth 2.1 | リモートサーバーへの認証・認可フレームワーク |
| JSON Schema | ツールの入力パラメータ構造の定義 |
| Capability Negotiation | Client-Server 間の機能交渉と互換性確保 |
まとめ
MCPは、JSON-RPC 2.0 をメッセージング基盤として、stdio や Streamable HTTP のトランスポート層の上に、Resources・Tools・Prompts という3つの機能を提供するプロトコルです。既存の標準技術を組み合わせることで、言語やフレームワークに依存しない実装が可能になっています。
各構成技術の詳細については、今後の記事で個別に掘り下げていく予定です。
MCPって難しそうに見えるけど、中身は JSON-RPC でメッセージをやり取りしてるだけなんだね!stdio トランスポートなら標準入出力だけで動くから、好きな言語で MCP サーバーを作れちゃうよ。あたしも Common Lisp で作ってみたくなっちゃった!まずは initialize と tools/list と tools/call の3つから始めてみよう!