Masayan tech blog.

  1. ブログ記事一覧>
  2. MCPサーバー開発の本質理解 - JSONRPCプロトコルから実践的デバッグテクニックまで

MCPサーバー開発の本質理解 - JSONRPCプロトコルから実践的デバッグテクニックまで

公開日

環境

  • OS: MacOS Apple M4 Max Sequoia 15.1
  • Python: 3.13
  • MCP: Model Context Protocol 2025-06-18
  • Tools: Claude Desktop, FastMCP, uvicorn, Google Cloud SDK

この記事を読むことで得られるメリット

  • JSONRPCプロトコルの本質を理解し、MCPの内部動作メカニズムを完全把握できる
  • 表面的なツールの使い方ではなく、通信フローから問題の根本原因を特定する力が身につく
  • 開発効率を劇的に向上させる実践的なデバッグワークフローを構築できる
  • セキュリティのベストプラクティスに沿ったMCPサーバー開発手法を習得できる
  • 他の開発者が見落としがちな、実運用で差がつく細かなTIPSとベストプラクティスを学べる
  • エラーハンドリングから進捗通知まで、ユーザー体験を向上させる実装テクニックが身につく

この記事を読むのにかかる時間

約25分

要約

MCPサーバー開発における真の生産性向上を実現するための実践ガイドである。単なるツールの使い方ではなく、JSONRPCプロトコルの本質的理解から始まり、MCPの通信フローを完全に把握することで、問題の根本原因を素早く特定できる開発者を育成する。表面的なデバッグ手法ではなく、プロトコルレベルでの深い理解に基づいた効率的なワークフロー、セキュリティを考慮した実装パターン、そして実運用で差がつく細かなTIPSまで、現場で即戦力となる知識を体系化した。

MCPとは何か

MCP(Model Context Protocol)は、AI助手が外部データソースやツールにアクセスするための標準化されたプロトコルである。Claude DesktopからMCPサーバーを経由して外部APIからデータを取得したり、様々なツールを実行したりすることが可能になる。

本記事では、ローカルでのMCPサーバーを試して、Claude Desktop等からツールを実行できている前提で、デバッグとトラブルシューティングに焦点を当てて解説を進める。

サーバーログファイルの場所

デバッグの第一歩として、ログファイルの場所を正確に把握することが重要である。MacOS環境では、以下の場所にMCP関連のログが出力される。

~/Library/Logs/Claude/
├── mcp.log                    # MCP接続および接続失敗に関する一般的なログ
└── mcp-server-SERVERNAME.log  # 特定のサーバーからの詳細なエラーログ

これらのファイルには、標準出力(stdout)に出力されるMCPプロトコルのメッセージに関するログが記録される。アプリケーション固有のログについては、Dockerで起動している場合は、通常通り docker logs -f --tail=500 などのコマンドで確認可能である。

MCPプロトコルの通信フロー

Claude Desktopで操作している内容とクライアント、サーバーの処理がどのようなやり取りで実行されているのかの解像度を上げることが、効果的なデバッグの鍵となる。MCPサーバーで実装したサンプルのツールをClaude Desktopから実行するという前提で、詳細な通信フローを解説する。

処理は大きく初期化フェーズ操作(実行)フェーズの流れで進行する。

1. 初期化フェーズ

ユーザーから見える処理の実行タイミング: Claude DesktopなどのMCPクライアントを起動したタイミング

Initialize Request

クライアント→サーバーに対して初期化リクエストを送信する。

  • 初期化リクエストのmethodはinitialize
  • clientInfoにはMCPクライアント側の情報が含まれている
  • jsonrpcという規格でメッセージが送信されている
{
  "method": "initialize",
  "params": {
    "protocolVersion": "2025-06-18",
    "capabilities": {},
    "clientInfo": {
      "name": "claude-ai",
      "version": "0.1.0"
    }
  },
  "jsonrpc": "2.0",
  "id": 0
}

Initialize Response

サーバー→クライアントに対して初期化レスポンスを送信する。

  • serverInfoにMCPサーバーの情報が含まれている
  • jsonrpcという規格でメッセージが受信されている
  • IDがリクエストと同じID(他のリクエストレスポンスも同様)
{
  "jsonrpc": "2.0",
  "id": 0,
  "result": {
    "protocolVersion": "2025-06-18",
    "capabilities": {
      "experimental": {},
      "prompts": {
        "listChanged": false
      },
      "resources": {
        "subscribe": false,
        "listChanged": false
      },
      "tools": {
        "listChanged": false
      }
    },
    "serverInfo": {
      "name": "sample",
      "version": "1.11.0"
    }
  }
}

Initialized Notification

クライアント→サーバーに初期化が完了したことを通知する。

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

2. 操作フェーズ

ツール一覧の取得

ユーザーから見える処理の実行タイミング: Claude DesktopなどのMCPクライアントを起動したタイミング(該当のツールの利用が有効化されていることが大前提)

クライアント→サーバーに対してサーバーに実装しているツールとリソースの一覧取得をリクエストする。

  • リクエストとレスポンスのメッセージにはidが含まれており、どのリクエストとレスポンスが関連しているかということがidを見ればわかるようになっている(id1のリクエストに対するレスポンスはid1のレスポンス)。
  • methodはツールならtools/list、リソースならresources/listとなっている。
{"method":"tools/list","params":{},"jsonrpc":"2.0","id":1}
{"method":"resources/list","params":{},"jsonrpc":"2.0","id":2}

サーバー→クライアントに対してサーバーに実装しているツールとリソースの一覧を返却する。

  • addというツールが1件存在する
  • ツールの説明や引数などの情報が含まれている
{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "add",
        "description": "Add two numbers",
        "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"
        }
      }
    ]
  }
}

{"jsonrpc":"2.0","id":2,"result":{"resources":[]}}

ツールの実行

ユーザーから見える処理の実行タイミング: Claude DesktopなどのMCPクライアントでツールを実行したタイミング

クライアント→サーバーに対してツールの実行をリクエストする。

  • ツールの実行はmethodがtools/callになる
  • addというツールを実行され、引数が2つ指定されている
{
  "method": "tools/call",
  "params": {
    "name": "add",
    "arguments": {
      "a": 1,
      "b": 2
    }
  },
  "jsonrpc": "2.0",
  "id": 8
}

サーバー→ツールを実行し結果をクライアントに返却する。

  • result.contentにデータ型と値が含まれている
{
  "jsonrpc": "2.0",
  "id": 8,
  "result": {
    "content": [
      {
        "type": "text",
        "text": "3"
      }
    ],
    "structuredContent": {
      "result": 3
    },
    "isError": false
  }
}

ツールの実装でいうと、このreturnで返却している値とデータ型に対応する:

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    logging.info(f"Adding {a} and {b}")
    return a + b

最終的に、MCPクライアントは、この情報を使用してLLMで回答を生成する流れになる。

ロギング

アプリケーションサーバーのログはloggingモジュールを利用することが重要である。print文は開発時のデバッグ用なのでそもそも推奨しないが、modelcontextprotocolの仕様によると、stdioトランスポートを利用している場合、標準入出力はjsonrpc形式のメッセージのやり取りに使用されるため、アプリケーションサーバーのログは標準エラー出力(stderr)に書き込む必要がある(printはデフォルトでstdoutに書き込んでしまう。loggingはデフォルトでstderr)。

"The server MUST NOT write anything to its stdout that is not a valid MCP message."

https://modelcontextprotocol.io/specification/2025-06-18/basic/transports

print関数: デフォルトでsys.stdout(標準出力)に出力され、
logging: デフォルトでsys.stderr(標準エラー出力)に出力される

公式リポジトリのログと通知で説明されているログは、jsonrpc形式でクライアントへのレスポンスメッセージとして記録したい場合の内容に該当するので、使用する場合はツールの引数で受け取ることができるcontextオブジェクトからinfowarnを呼び出して行う。

非同期関数として定義する必要があるので注意が必要

以下のように実装が可能である。これは冒頭でも触れたmcp-server-SERVERNAME.logに出力される:

@mcp.tool()
async def add(a: int, b: int, c: Optional[int], ctx: Context) -> Any:
    await ctx.debug("Debug: Starting processing")
    await ctx.info("Info: Starting processing")
    await ctx.warning("Warn: Processing may take a while")
    await ctx.error("Error: Processing failed")

なお、printでも以下のようにすればstderrに出力することは可能である:

import sys

print("debug message", file=sys.stderr)

トランスポート層の理解

Stdioトランスポート

標準入出力を使用した通信方式で、ローカルプロセスに最適である。クライアントとサーバーを同一マシン上で動作させ、サーバーをサブプロセスとして起動する。ネットワーク設定が不要でレイテンシが低いため、開発やデバッグに適している。

Streamable HTTPトランスポート(推奨)

2025年3月26日付の最新MCP仕様で導入された通信方式である。単一のHTTPエンドポイント(POST /mcp)でクライアントからのリクエスト送信とサーバーからのストリーミング配信を兼ねることができる。

Streamable HTTPの主な利点:

  • ステートレスサーバーの実現: セッション状態をサーバー側で保持する必要がない
  • 長時間接続が不要: リクエストごとに接続を確立・切断
  • オンデマンドのリソース割り当て: 必要な時のみリソースを使用
  • サーバーレス環境での実行が容易: AWSラムダやGoogle Cloud Functionsでの実行に適している
  • Mcp-Session-Idヘッダーによるセッション管理: ステートレスながらセッション追跡が可能

リモートMCPサーバーへのデプロイ

本記事では、Google CloudのCloud Runを使用する

事前準備

CIを構築したい場合は、Cloud BuildトリガーとGitHubリポジトリを連携、Artifact Registryの作成などを行っておくこと。

今回はgcloudコマンドで手動デプロイするため、Google Cloud SDKのインストールと認証を行う。CloudBuild等のCIを使う場合は不要である。

brew install --cask google-cloud-sdk
gcloud auth login

Dockerファイルの作成

FastMCPを使用したMCPサーバーのDockerファイル例:

FROM python:3.13-slim

COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

COPY . /app
WORKDIR /app

# 環境変数でシステムPythonを使用(venvを使わないように)
ENV UV_PROJECT_ENVIRONMENT="/usr/local/"

RUN uv sync --frozen

CMD uv run uvicorn server:app --host 0.0.0.0 --port $PORT

MCPサーバーの実装

server.py

from mcp.server.fastmcp import FastMCP

# ステートレスHTTP対応でMCPサーバーを作成
mcp = FastMCP(
    "sample",
    stateless_http=True,
)

# ASGIアプリとして公開
app = mcp.streamable_http_app()

@mcp.tool()
async def add(a: int, b: int) -> int:
    return a + b

手動デプロイコマンド

gcloud run deploy <service-name> \
  --source . \
  --platform managed \
  --region asia-northeast1 \
  --allow-unauthenticated \
  --project <project-id> \
  --set-env-vars "FOO=xxxx, BAR=yyyy"

セキュリティベストプラクティス

MCPサーバーを本番環境で運用する際は、以下のセキュリティ対策を必ず実装すること:

環境変数での機密情報管理

APIキーやトークンは環境変数で管理し、コードに直接記述しない。

HTTPS通信の強制

本番環境では必ずHTTPS通信を使用する。

適切なCORS設定

必要最小限のオリジンのみを許可する。

リクエストレート制限

DoS攻撃を防ぐためのレート制限を実装する。

詳細については、MCPセキュリティベストプラクティスを参照すること。

トラブルシューティング

一般的なエラーとコード

エラーコード一覧:

エラーコード

名称

説明

-32700

ParseError

JSONパースエラー

-32600

InvalidRequest

不正なリクエスト

-32601

MethodNotFound

メソッドが見つからない

-32602

InvalidParams

パラメータが不正

-32603

InternalError

内部エラー

例えば、適当に存在しないツールを実行してから、/Users/<username>/Library/Logs/Claude/mcp.logを確認すると、コード-32601のエラーが以下のように出力される:

2025-08-15T08:55:01.360Z [info] [microcms] Message from server: {"jsonrpc":"2.0","id":11,"error":{"code":-32601,"message":"Method not found"}}

307 Temporary Redirect

MCPサーバーのエンドポイントの最後にスラッシュがあると発生する可能性がある。(http://localhost:8080/mcp/の最後の/が不要)

INFO: 151.101.64.223:64761 - "POST /mcp/ HTTP/1.1" 307 Temporary Redirect

TIPS

リクエスト情報の取得

MCPサーバー側でリクエスト情報はctx.request_contextctx.request_idctx.client_idで取得できる

@mcp.tool()
async def foo(ctx: Context) -> str:
  ctx.request_context.request["headers"]
  ctx.request_id
  ctx.client_id

開発環境でのホットリロード設定

開発環境では効率を向上させるため、uvicornの--reloadオプションを使用したい。

公式ドキュメントの例だとserver.pyuv run mcp dev server.pyで直接実行する形式になっている。

# server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Demo")

# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

ホットリロードを利用するには、以下のようにMCPインスタンスからstreamable_http_appメソッドを呼び出してASGIアプリのインスタンスを取得し、これをmodule:appappとして指定することで実現できる。

# server.py
from mcp.server.fastmcp import FastMCP

mcp = FastMCP("Demo") 
app = mcp.streamable_http_app()

# Add an addition tool
@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers"""
    return a + b

サーバー起動時にリロードオプション(--reload)を指定:

# uv run uvicorn <module>:<ASGIアプリのインスタンス名>
uv run uvicorn server:app --host 0.0.0.0 --port 8080 --reload --reload-dir /app --timeout-graceful-shutdown 30

進捗通知の実装

長時間かかる処理では、progress tokenを使用してリアルタイムで進捗状況を報告できる。

ただし、ctx.report_progressは主に技術的な進捗追跡(mcp-server-SERVERNAME.logに出力される)のための機能で、ユーザーが直接目に見える進捗表示ではないので注意が必要である。

@mcp.tool()
async def long_running_task(ctx: Context) -> str:
    """時間のかかる処理で進捗を報告する例"""
    total_steps = 5
    
    for step in range(1, total_steps + 1):
        await ctx.report_progress(step, total_steps)
        # 処理実行
        await asyncio.sleep(1)
        await ctx.info(f"ステップ {step}/{total_steps} 完了")
    
    return "すべての処理が完了しました"

ユーザーフレンドリーなメッセージ

ツールを実行するのに必要な引数は必ずしも正確に受け取れるわけではない。例えば、複数の候補を選択肢として提示し、そこからユーザーに選択させる等の方がユーザーにとっては体験がよくなるケースもある。

そのような場合、以下のようにオプショナルな引数としてツールに定義し、入力されていない場合は再度ユーザーに柔軟に指示を出すことができる。

@mcp.tool()
def add(a: int, b: int, c: Optional[int], ctx: Context) -> Any:

    if c is None:
        return {
            "candidate": [
                {"label": "1", "value": 1},
                {"label": "2", "value": 2},
                {"label": "3", "value": 3},
            ],
            "message": "cが指定されていない場合、candidateからクライアントに選択させ、再度このツールを呼び出すように促す"
        }

このように、値に特定の形式や制限や範囲などがある場合はその内容を提示した方が、ユーザーにとっては正確に入力しやすい。

開発者モードの有効化

有効化される機能:

  • MCP接続ログファイルの場所を簡単に開けるようになる([「MCPログファイルを開く」)
  • Claudeのconfig設定ファイル(/Users/<username>/Library/Application Support/Claude/claude_desktop_config.json)をアプリを落とさずに反映できたり、ファイルの場所を簡単に開けるようになる
  • Claude for Desktop開発者モードを利用できる(Command+Option+I

有効化の手順:

ヘルプ→トラブルシューティングから可能

開発者のメニューが表示されていれば設定完了

MCPサーバーの検証方法

Inspectorでも実行できるが、curlを使用して動作確認を行うことも可能である。以下はツールの一覧を取得するリクエストの例:

  • POSTメソッドである必要がある
  • Content-Type: application/json、Accept: application/json, text/event-stream で指定すること(指定できていない場合エラーになる)
curl -X POST \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 1,
    "method": "tools/list",
    "params": {}
  }' \
  http://localhost:8080/mcp

レスポンス例:

event: message
data: {
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "tools": [
      {
        "name": "add",
        "description": "",
        "inputSchema": {
          "properties": {
            "a": {
              "title": "A",
              "type": "integer"
            },
            "b": {
              "title": "B",
              "type": "integer"
            },
            "c": {
              "anyOf": [
                {
                  "type": "integer"
                },
                {
                  "type": "null"
                }
              ],
              "title": "C"
            }
          },
          "required": [
            "a",
            "b",
            "c"
          ],
          "title": "addArguments",
          "type": "object"
        }
      }
      // ...割愛
    ]
  }
}

Content-Typeヘッダーを適切に指定できていない場合のエラー:

Invalid Content-Type header

Acceptヘッダーを適切に指定できていない場合のエラー(InvalidRequest (-32600): 不正なリクエスト):

{"jsonrpc":"2.0","id":"server-error","error":{"code":-32600,"message":"Not Acceptable: Client must accept both application/json and text/event-stream"}}

まとめ

MCPサーバー開発における真の生産性は、表面的なツールの使い方を覚えることではなく、JSONRPCプロトコルとMCPの通信フローを本質的に理解することから生まれる。

これらの知識は、単なる技術的なTIPSではなく、MCPサーバー開発における本質的な理解に基づいている。表面的な問題解決ではなく、根本的な設計思想を理解することで、将来的な仕様変更や新たな要件にも柔軟に対応できる開発者になることができる。

真のMCP開発者は、ツールを使うのではなく、プロトコルを理解し、システムを設計し、ユーザー体験を創造する。この記事で学んだ知識を基盤として、さらなる探求と実践を続けていただきたい。