Closed18

今更ながら Model Context Protocol(MCP) を試す

kun432kun432

公式ドキュメント

https://modelcontextprotocol.io/introduction

Get Started: 最初に

Model Context Protocol (MCP) の利用を開始しよう。

MCP は、アプリケーションが LLM にコンテキストを提供する方法を標準化するオープンプロトコルです。MCP を AI アプリケーション向けの USB-C ポートのように考えてください。USB-C が様々な周辺機器やアクセサリに接続するための標準的な方法を提供するのと同様に、MCP は AI モデルを異なるデータソースやツールに接続するための標準的な方法を提供します。

なぜMCPなのか?

MCP は、LLM 上にエージェントや複雑なワークフローを構築するのに役立ちます。LLM はしばしばデータやツールと統合する必要があり、MCP は以下を提供します:

  • LLM が直接接続できる事前構築済みの統合機能のリストが拡大しています
  • LLM プロバイダーやベンダー間を柔軟に切り替えられる柔軟性があります
  • インフラ内でデータを安全に保護するためのベストプラクティスが提供されています

一般的なアーキテクチャ

MCP の基本は、ホストアプリケーションが複数のサーバーに接続できるクライアント・サーバーアーキテクチャに基づいています:

  • MCP ホスト: Claude Desktop、IDE、または MCP を介してデータにアクセスしたい AI ツールなどのプログラム
  • MCP クライアント: サーバーと 1:1 の接続を維持するプロトコルクライアント
  • MCP サーバー: 標準化された Model Context Protocol を通じて特定の機能を公開する軽量プログラム
  • ローカルデータソース: MCP サーバーが安全にアクセスできる、コンピューター内のファイル、データベース、サービス
  • リモートサービス: MCP サーバーが接続できる、インターネット経由で利用可能な外部システム(例:API を通じて)

始めよう

自分のニーズに最適なパスを選んでください:

Quickstarts

サンプル

チュートリアル

  • LLM と連携した MCP の構築
    Claude のような LLM を活用して MCP 開発を加速する方法を学ぶ
  • デバッグガイド
    MCP サーバーや統合の効果的なデバッグ方法を学ぶ
  • MCP Inspector
    インタラクティブなデバッグツールで MCP サーバーをテストおよび検査する
  • MCP ワークショップ (動画・2時間)
    YouTube

MCP を探る

MCP の基本概念と機能をより深く理解する:

  • コアアーキテクチャ
    MCP がどのようにクライアント、サーバー、LLM を接続するかを理解する
  • リソース
    サーバーから LLM にデータやコンテンツを公開する
  • プロンプト
    再利用可能なプロンプトテンプレートやワークフローを作成する
  • ツール
    サーバーを通じて LLM にアクションを実行させる
    - サンプリング
    サーバーが LLM に対して補完リクエストを送る
  • トランスポート
    MCP の通信メカニズムについて学ぶ
kun432kun432

Quickstart: Claude Desktop

まずは一番手っ取り早く試せそうなClaude Desktopで試してみる。環境はMac。

https://modelcontextprotocol.io/quickstart/user

HomebrewでClaude Desktopをインストール

brew install cluade

バージョン

Claude Desktopの設定を開く

「開発者」→「構成を編集」

Finderで設定ファイル、~/Library/Application Support/Claude/claude_desktop_config.json が表示されるので、これをエディタで編集する。

Filesystem MCP Serverをインストールして、ローカルのファイルにアクセスできるようにする。設定ファイル内のパスの部分にアクセスを許可するディレクトリを指定する。

claude_desktop_config.json
{
  "mcpServers": {
    "filesystem": {
      "command": "npx",
      "args": [
        "-y",
        "@modelcontextprotocol/server-filesystem",
        "/Users/kun432/Desktop/mcp-work",
        "/Users/kun432/Desktop/mcp-work2"
      ]
    }
  }
}

今回は許可するディレクトリを新規に用意して、それぞれのディレクトリに、神戸市が公開している観光に関する統計・調査資料のPDFファイルを配置した。

  • ~/Desktop/mcp-work
    • r5_irikomi.pdf (「令和5年観光入込客数について」)
  • ~/Desktop/mcp-work2
    • r5_doukou.pdf (「令和5年度 神戸市観光動向調査結果について」)

で、設定ファイルにもある通り、Node.jsが必要になるが、これグローバルにインストールされてる必要があるってことよなぁ・・・今回はHomebrewでインストールする。

brew install node
node -v
出力
v23.10.0

Claude Desktopを再起動すると、金づちアイコンに数字が増えている。

クリックするとこんな感じで、Filesystem MCP Serverが使用できるツールというか機能が表示される。

ざっと見た感じ、以下が可能になっている様子。

  • ディレクトリの作成
  • ディテクトリツリーの参照
  • ファイルの編集
  • ファイル・ディレクトリのメタデータ参照
  • アクセス可能なディレクトリのリスト
  • ディレクトリ・ファイルのリスト
  • ファイルの移動
  • ファイルの読み取り
  • 複数ファイルの読み取り
  • ファイルの検索
  • ファイルの書き込み

では試してみる。

ディレクトリにアクセスできる?

こんな感じでツールからのアクセスを許可するかを確認してくるので、許可する。

許可したディレクトリが表示される。

あとはこんな感じでチャットで指示を出していくと、エージェントライクにいろいろトライしてくれる。OS側の許可ダイアログが出る場合もあるので適宜許可。なお、PDFファイルは読み込めなかった・・・

最終的に読み込めなかったりした場合とかエラーが起きた場合、何もなかったことになる様子。

kun432kun432

コンセプトを少し理解しておく。自分はPythonメインなので、サンプルコードはPythonのみ。

kun432kun432

コンセプト: コアアーキテクチャ

https://modelcontextprotocol.io/docs/concepts/architecture

公式ドキュメントの日本語訳

コアアーキテクチャ"

MCP がどのようにクライアント、サーバー、そして LLM を接続するかを理解する

Model Context Protocol (MCP) は、LLM アプリケーションと統合との間でシームレスな通信を可能にする、柔軟かつ拡張性のあるアーキテクチャ上に構築されています。本ドキュメントでは、コアとなるアーキテクチャのコンポーネントと概念について説明します。

概要

MCP はクライアント・サーバーアーキテクチャに従っています。具体的には:

  • ホスト: 接続を開始する LLM アプリケーション(例: Claude Desktop や IDE)
  • クライアント: ホストアプリケーション内でサーバーと 1:1 の接続を維持する
  • サーバー: クライアントにコンテキスト、ツール、プロンプトを提供する

コアコンポーネント

プロトコル層

プロトコル層は、メッセージのフレーミング、リクエスト/レスポンスの連携、および高レベルな通信パターンを処理します。

class Session(BaseSession[RequestT, NotificationT, ResultT]):
   async def send_request(
       self,
       request: RequestT,
       result_type: type[Result]
   ) -> Result:
       """
       リクエストを送信し、レスポンスを待ちます。レスポンスにエラーが含まれている場合は McpError を発生させます。
       """
       # リクエスト処理の実装

   async def send_notification(
       self,
       notification: NotificationT
   ) -> None:
       """レスポンスを期待しない一方向の通知を送信します。"""
       # 通知処理の実装

   async def _received_request(
       self,
       responder: RequestResponder[ReceiveRequestT, ResultT]
   ) -> None:
       """相手側からのリクエストを処理します。"""
       # リクエスト処理の実装

   async def _received_notification(
       self,
       notification: ReceiveNotificationT
   ) -> None:
       """相手側からの通知を処理します。"""
       # 通知処理の実装

主なクラスには以下が含まれます:

  • Protocol
  • Client
  • Server

トランスポート層

トランスポート層は、クライアントとサーバー間の実際の通信を処理します。MCP は複数のトランスポート機構をサポートしています:

  1. Stdio トランスポート
    • 標準入力/出力を使用して通信します
    • ローカルプロセス間での通信に最適です
  2. HTTP と SSE トランスポート
    • サーバーからクライアントへのメッセージに Server-Sent Events を使用
    • クライアントからサーバーへのメッセージに HTTP POST を使用

すべてのトランスポートは、メッセージ交換に JSON-RPC 2.0 を使用します。Model Context Protocol のメッセージフォーマットの詳細については、仕様 を参照してください。

メッセージタイプ

MCP には主に以下のメッセージタイプがあります:

  1. リクエスト: 相手側からのレスポンスを期待します:

    interface Request {
      method: string;
      params?: { ... };
    }
    
  2. 結果: リクエストに対する成功レスポンスです:

    interface Result {
      [key: string]: unknown;
    }
    
  3. エラー: リクエストが失敗したことを示します:

    interface Error {
      code: number;
      message: string;
      data?: unknown;
    }
    
  4. 通知: レスポンスを期待しない一方向のメッセージです:

    interface Notification {
      method: string;
      params?: { ... };
    }
    

接続ライフサイクル

1. 初期化

  1. クライアントは、プロトコルバージョンと能力を含む initialize リクエストを送信します
  2. サーバーは、そのプロトコルバージョンと能力を含むレスポンスを返します
  3. クライアントは、確認のために initialized 通知を送信します
  4. 通常のメッセージ交換が開始されます

2. メッセージ交換

初期化後、以下のパターンがサポートされます:

  • リクエスト-レスポンス: クライアントまたはサーバーがリクエストを送信し、相手側がレスポンスを返す
  • 通知: どちらの当事者も一方向のメッセージを送信可能

3. 終了

どちらの当事者も接続を終了することができます:

  • close() を使用したクリーンなシャットダウン
  • トランスポートの切断
  • エラー条件

エラー処理

MCP は以下の標準エラーコードを定義しています:

enum ErrorCode {
  // 標準 JSON-RPC エラーコード
  ParseError = -32700,
  InvalidRequest = -32600,
  MethodNotFound = -32601,
  InvalidParams = -32602,
  InternalError = -32603
}

SDK やアプリケーションは、-32000 以上の独自エラーコードを定義できます。

エラーは以下の方法で伝播されます:

  • リクエストに対するエラーレスポンス
  • トランスポート上のエラーイベント
  • プロトコルレベルのエラーハンドラ

実装例

以下は MCP サーバーを実装する基本的な例です:

import asyncio
import mcp.types as types
from mcp.server import Server
from mcp.server.stdio import stdio_server

app = Server("example-server")

@app.list_resources()
async def list_resources() -> list[types.Resource]:
   return [
       types.Resource(
           uri="example://resource",
           name="Example Resource"
       )
   ]

async def main():
   async with stdio_server() as streams:
       await app.run(
           streams[0],
           streams[1],
           app.create_initialization_options()
       )

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

ベストプラクティス

トランスポートの選択

  1. ローカル通信
    • ローカルプロセス間の通信には stdio トランスポートを使用
    • 同一マシン上の通信に効率的
    • プロセス管理が簡単
  2. リモート通信
    • HTTP 互換性が必要なシナリオには SSE を使用
    • 認証や認可などのセキュリティ上の考慮事項を検討

メッセージ処理

  1. リクエスト処理
    • 入力を十分に検証する
    • 型安全なスキーマを使用する
    • エラーは適切に処理する
    • タイムアウトを実装する
  2. 進捗報告
    • 長時間の操作には進捗トークンを使用する
    • 進捗を段階的に報告する
    • 既知の場合は全体の進捗を含める
  3. エラー管理
    • 適切なエラーコードを使用する
    • 有用なエラーメッセージを含める
    • エラー発生時にリソースを適切にクリーンアップする

セキュリティ上の考慮事項

  1. トランスポートのセキュリティ
    • リモート接続には TLS を使用する
    • 接続元を検証する
    • 必要に応じて認証を実装する
  2. メッセージの検証
    • すべての受信メッセージを検証する
    • 入力をサニタイズする
    • メッセージサイズの制限を確認する
    • JSON-RPC のフォーマットを検証する
  3. リソースの保護
    • アクセス制御を実装する
    • リソースパスを検証する
    • リソース使用状況を監視する
    • リクエストのレート制限を行う
  4. エラー処理
    • 機密情報を漏洩させない
    • セキュリティに関するエラーをログに記録する
    • 適切なクリーンアップを実施する
    • DoS シナリオに対応する

デバッグとモニタリング

  1. ログ記録
    • プロトコルイベントをログに記録する
    • メッセージの流れを追跡する
    • パフォーマンスを監視する
    • エラーを記録する
  2. 診断
    • ヘルスチェックを実装する
    • 接続状態を監視する
    • リソース使用状況を追跡する
    • パフォーマンスのプロファイリングを行う
  3. テスト
    • 異なるトランスポートをテストする
    • エラー処理を検証する
    • エッジケースを確認する
    • サーバーのロードテストを実施する
kun432kun432

コンセプト: リソース

https://modelcontextprotocol.io/docs/concepts/resources

硬式ドキュメントの日本語訳

リソース

サーバーから LLM にデータやコンテンツを公開する

リソースは、Model Context Protocol (MCP) の中核となるプリミティブであり、サーバーがクライアントに読み取られ、LLM との相互作用のコンテキストとして利用されるデータやコンテンツを公開できるようにします。

概要

リソースは、MCP サーバーがクライアントに提供したいと考えるあらゆる種類のデータを表します。これには以下が含まれます:

  • ファイルの内容
  • データベースのレコード
  • API レスポンス
  • ライブシステムデータ
  • スクリーンショットや画像
  • ログファイル
  • その他多数

各リソースは一意の URI によって識別され、テキストまたはバイナリデータのいずれかを含むことができます。

リソース URI

リソースは、以下の形式に従う URI を使用して識別されます:

[protocol]://[host]/[path]

例えば:

  • file:///home/user/documents/report.pdf
  • postgres://database/customers/schema
  • screen://localhost/display1

プロトコルおよびパスの構造は MCP サーバーの実装によって定義され、サーバーは独自のカスタム URI スキームを定義できます。

リソースの種類

リソースは、2 種類のコンテンツを含むことができます:

テキストリソース

テキストリソースは、UTF-8 エンコードされたテキストデータを含みます。以下に適しています:

  • ソースコード
  • 設定ファイル
  • ログファイル
  • JSON/XML データ
  • プレーンテキスト

バイナリリソース

バイナリリソースは、base64 エンコードされた生のバイナリデータを含みます。以下に適しています:

  • 画像
  • PDF
  • オーディオファイル
  • ビデオファイル
  • その他テキスト以外のフォーマット

リソースの発見

クライアントは、以下の 2 つの主要な方法で利用可能なリソースを発見できます:

直接リソース

サーバーは、resources/list エンドポイントを通じて具体的なリソースのリストを公開します。各リソースは以下の情報を含みます:

{
  uri: string;           // リソースの一意識別子
  name: string;          // 人間に読みやすい名称
  description?: string;  // オプションの説明
  mimeType?: string;     // オプションの MIME タイプ
}

リソーステンプレート

動的なリソースの場合、サーバーはクライアントが有効なリソース URI を構築するために使用できる URI テンプレート を公開できます:

{
  uriTemplate: string;   // RFC 6570 に準拠した URI テンプレート
  name: string;          // このタイプの人間に読みやすい名称
  description?: string;  // オプションの説明
  mimeType?: string;     // 一致するすべてのリソースに対するオプションの MIME タイプ
}

リソースの読み取り

リソースを読み取るために、クライアントはリソース URI を指定して resources/read リクエストを送信します。

サーバーは、リソースの内容のリストで応答します:

{
  contents: [
    {
      uri: string;        // リソースの URI
      mimeType?: string;  // オプションの MIME タイプ

      // 以下のいずれか:
      text?: string;      // テキストリソースの場合
      blob?: string;      // バイナリリソースの場合 (base64 エンコード)
    }
  ]
}

リソースの更新

MCP は、以下の 2 つのメカニズムを通じてリソースのリアルタイム更新をサポートします:

リストの変更

サーバーは、利用可能なリソースのリストが変更されたときに、notifications/resources/list_changed 通知を通じてクライアントに通知できます。

コンテンツの変更

クライアントは、特定のリソースの更新を購読できます:

  1. クライアントはリソース URI を指定して resources/subscribe を送信します
  2. サーバーは、リソースが変更された際に notifications/resources/updated を送信します
  3. クライアントは resources/read を使用して最新の内容を取得できます
  4. クライアントは resources/unsubscribe で購読を解除できます

実装例

以下は、MCP サーバーにおけるリソースサポートの実装例です:

app = Server("example-server")

@app.list_resources()
async def list_resources() -> list[types.Resource]:
   return [
       types.Resource(
           uri="file:///logs/app.log",
           name="Application Logs",
           mimeType="text/plain"
       )
   ]

@app.read_resource()
async def read_resource(uri: AnyUrl) -> str:
   if str(uri) == "file:///logs/app.log":
       log_contents = await read_log_file()
       return log_contents

   raise ValueError("Resource not found")

# サーバーの起動
async with stdio_server() as streams:
   await app.run(
       streams[0],
       streams[1],
       app.create_initialization_options()
   )

ベストプラクティス

リソースサポートを実装する際には、以下の点に留意してください:

  1. 明確で説明的なリソース名および URI を使用する
  2. LLM の理解を促すために有用な説明を含める
  3. 知られている場合は適切な MIME タイプを設定する
  4. 動的コンテンツにはリソーステンプレートを実装する
  5. 頻繁に変更されるリソースには購読機能を使用する
  6. 明確なエラーメッセージでエラーを適切に処理する
  7. 大規模なリソースリストにはページネーションを検討する
  8. 適切な場合はリソースの内容をキャッシュする
  9. 処理前に URI の検証を行う
  10. カスタム URI スキームについてドキュメント化する

セキュリティ上の考慮事項

リソースを公開する際には、以下の点に注意してください:

  • すべてのリソース URI を検証する
  • 適切なアクセス制御を実装する
  • ディレクトリトラバーサルを防止するためにファイルパスをサニタイズする
  • バイナリデータの取り扱いには注意する
  • リソースの読み取りに対してレート制限を検討する
  • リソースアクセスを監査する
  • 転送中の機密データを暗号化する
  • MIME タイプを検証する
  • 長時間実行される読み取りにタイムアウトを実装する
  • リソースのクリーンアップを適切に処理する
kun432kun432

コンセプト: プロンプト

https://modelcontextprotocol.io/docs/concepts/prompts

公式ドキュメントの日本語訳

プロンプト

再利用可能なプロンプトテンプレートとワークフローを作成する

プロンプトは、サーバーが再利用可能なプロンプトテンプレートやワークフローを定義し、クライアントがユーザーや LLM に対して容易に提示できるようにするものです。これにより、一般的な LLM とのやり取りを標準化し、共有する強力な方法が提供されます。

概要

MCP におけるプロンプトは、事前に定義されたテンプレートであり、以下の機能を提供します:

  • 動的な引数の受け入れ
  • リソースからのコンテキストの組み込み
  • 複数のやり取りのチェーン
  • 特定のワークフローの指針付け
  • UI 要素としての提示(例: スラッシュコマンド)

プロンプトの構造

各プロンプトは以下の形式で定義されます:

{
  name: string;              // プロンプトの一意識別子
  description?: string;      // 人間に読みやすい説明
  arguments?: [              // オプションの引数リスト
    {
      name: string;          // 引数の識別子
      description?: string;  // 引数の説明
      required?: boolean;    // 引数が必須かどうか
    }
  ]
}

プロンプトの発見

クライアントは、prompts/list エンドポイントを通じて利用可能なプロンプトを発見できます:

// リクエスト
{
  method: "prompts/list"
}

// レスポンス
{
  prompts: [
    {
      name: "analyze-code",
      description: "コードを解析して潜在的な改善点を見つける",
      arguments: [
        {
          name: "language",
          description: "プログラミング言語",
          required: true
        }
      ]
    }
  ]
}

プロンプトの利用

プロンプトを利用するために、クライアントは prompts/get リクエストを送信します:

// リクエスト
{
  method: "prompts/get",
  params: {
    name: "analyze-code",
    arguments: {
      language: "python"
    }
  }
}

// レスポンス
{
  description: "Pythonコードを解析して潜在的な改善点を見つける",
  messages: [
    {
      role: "user",
      content: {
        type: "text",
        text: "以下のPythonコードを改善の余地がないか分析してください。:\n\n```python\ndef calculate_sum(numbers):\n    total = 0\n    for num in numbers:\n        total = total + num\n    return total\n\nresult = calculate_sum([1, 2, 3, 4, 5])\nprint(result)\n```"
      }
    }
  ]
}

動的プロンプト

プロンプトは動的であり、以下を含むことができます:

組み込みリソースコンテキスト

{
  "name": "analyze-project",
  "description": "プロジェクトのログとコードを解析する",
  "arguments": [
    {
      "name": "timeframe",
      "description": "解析するログの期間",
      "required": true
    },
    {
      "name": "fileUri",
      "description": "レビューするコードのファイルのURI",
      "required": true
    }
  ]
}

prompts/get リクエストの処理例:

{
  "messages": [
    {
      "role": "user",
      "content": {
        "type": "text",
        "text": "これらのシステムログとコードファイルを分析し、問題がないか確認してください:"
      }
    },
    {
      "role": "user",
      "content": {
        "type": "resource",
        "resource": {
          "uri": "logs://recent?timeframe=1h",
          "text": "[2024-03-14 15:32:11] ERROR: Connection timeout in network.py:127\n[2024-03-14 15:32:15] WARN: Retrying connection (attempt 2/3)\n[2024-03-14 15:32:20] ERROR: Max retries exceeded",
          "mimeType": "text/plain"
        }
      }
    },
    {
      "role": "user",
      "content": {
        "type": "resource",
        "resource": {
          "uri": "file:///path/to/code.py",
          "text": "def connect_to_service(timeout=30):\n    retries = 3\n    for attempt in range(retries):\n        try:\n            return establish_connection(timeout)\n        except TimeoutError:\n            if attempt == retries - 1:\n                raise\n            time.sleep(5)\n\ndef establish_connection(timeout):\n    # Connection implementation\n    pass",
          "mimeType": "text/x-python"
        }
      }
    }
  ]
}

マルチステップワークフロー

const debugWorkflow = {
  name: "debug-error",
  async getMessages(error: string) {
    return [
      {
        role: "user",
        content: {
          type: "text",
          text: `以下が私が見つけたエラーです: ${error}`
        }
      },
      {
        role: "assistant",
        content: {
          type: "text",
          text: "このエラーの解析をお手伝いします。これまでに何を試しましたか?"
        }
      },
      {
        role: "user",
        content: {
          type: "text",
          text: "サービスを再起動してみましたが、エラーは依然として発生しています。"
        }
      }
    ];
  }
};

実装例

以下は、MCP サーバーにおけるプロンプトサポートの完全な実装例です:

from mcp.server import Server
import mcp.types as types

# 定義されたプロンプト
PROMPTS = {
   "git-commit": types.Prompt(
       name="git-commit",
       description="Gitのコミットメッセージを生成する",
       arguments=[
           types.PromptArgument(
               name="changes",
               description="Git diff または変更内容の説明",
               required=True
           )
       ],
   ),
   "explain-code": types.Prompt(
       name="explain-code",
       description="コードの仕組みを説明する",
       arguments=[
           types.PromptArgument(
               name="code",
               description="説明すべきコード",
               required=True
           ),
           types.PromptArgument(
               name="language",
               description="プログラミング言語",
               required=False
           )
       ],
   )
}

# サーバーの初期化
app = Server("example-prompts-server")

@app.list_prompts()
async def list_prompts() -> list[types.Prompt]:
   return list(PROMPTS.values())

@app.get_prompt()
async def get_prompt(
   name: str, arguments: dict[str, str] | None = None
) -> types.GetPromptResult:
   if name not in PROMPTS:
       raise ValueError(f"Prompt not found: {name}")

   if name == "git-commit":
       changes = arguments.get("changes") if arguments else ""
       return types.GetPromptResult(
           messages=[
               types.PromptMessage(
                   role="user",
                   content=types.TextContent(
                       type="text",
                       text=f"これらの変更について、簡潔かつ説明的なコミットメッセージを作成してください。\n\n{changes}"
                   )
               )
           ]
       )

   if name == "explain-code":
       code = arguments.get("code") if arguments else ""
       language = arguments.get("language", "Unknown") if arguments else "Unknown"
       return types.GetPromptResult(
           messages=[
               types.PromptMessage(
                   role="user",
                   content=types.TextContent(
                       type="text",
                       text=f"この{language}コードの仕組みを説明してください。:\n\n{code}"
                   )
               )
           ]
       )

   raise ValueError("Prompt implementation not found")

ベストプラクティス

プロンプトを実装する際には以下を考慮してください:

  1. 明確で説明的なプロンプト名を使用する
  2. プロンプトおよび引数の詳細な説明を提供する
  3. 必須の引数をすべて検証する
  4. 引数が不足している場合は適切に処理する
  5. プロンプトテンプレートのバージョン管理を検討する
  6. 適切な場合は動的コンテンツをキャッシュする
  7. エラー処理を実装する
  8. 期待される引数のフォーマットを文書化する
  9. プロンプトの組み合わせ可能性を検討する
  10. 様々な入力でプロンプトをテストする

UI 統合

プロンプトはクライアントの UI に以下のように提示できます:

  • スラッシュコマンド
  • クイックアクション
  • コンテキストメニュー項目
  • コマンドパレットのエントリ
  • ガイド付きワークフロー
  • インタラクティブフォーム

更新と変更

サーバーは、以下の方法でプロンプトの変更をクライアントに通知できます:

  1. サーバーの機能: prompts.listChanged
  2. 通知: notifications/prompts/list_changed
  3. クライアントがプロンプト一覧を再取得する

セキュリティ上の考慮事項

プロンプトを実装する際には、以下に注意してください:

  • すべての引数を検証する
  • ユーザー入力をサニタイズする
  • レート制限を検討する
  • アクセス制御を実装する
  • プロンプトの利用状況を監査する
  • 機微なデータを適切に取り扱う
  • 生成されたコンテンツを検証する
  • タイムアウトを実装する
  • プロンプトインジェクションのリスクを考慮する
  • セキュリティ要件を文書化する
kun432kun432

コンセプト: ツール

https://modelcontextprotocol.io/docs/concepts/tools

公式ドキュメントの日本語訳

ツール

サーバーを通じて LLM にアクションを実行させる

ツールは、Model Context Protocol (MCP) の強力なプリミティブであり、サーバーがクライアントに実行可能な機能を公開できるようにします。ツールを通じて、LLM は外部システムと連携したり、計算を実行したり、現実世界でアクションを起こすことが可能になります。

:message
ツールは モデル制御 を前提として設計されており、ツールはサーバーからクライアントに公開され、AI モデルが自動的に呼び出すことを意図しています(ただし、承認のために人間が介在する場合もあります)。
:::

概要

MCP におけるツールは、サーバーがクライアントに対して実行可能な関数を公開し、それを LLM がアクションの実行に利用できるようにするものです。ツールの主な特徴は以下の通りです:

  • 発見: クライアントは tools/list エンドポイントを通じて利用可能なツールを一覧できます
  • 呼び出し: ツールは tools/call エンドポイントを使用して呼び出され、サーバーは要求された操作を実行して結果を返します
  • 柔軟性: ツールは単純な計算から複雑な API 連携まで、さまざまな操作をカバーできます

resources と同様に、ツールは一意の名前で識別され、使用方法をガイドするための説明が含まれる場合があります。しかし、リソースと異なり、ツールは状態を変更したり、外部システムと連携する動的な操作を表します。

ツール定義の構造

各ツールは以下の構造で定義されます:

{
  name: string;          // ツールの一意識別子
  description?: string;  // 人間に読みやすい説明
  inputSchema: {         // ツールのパラメータに対する JSON Schema
    type: "object",
    properties: { ... }  // ツール固有のパラメータ
  }
}

ツールの実装

以下は、MCP サーバーにおける基本的なツールの実装例です:

app = Server("example-server")

@app.list_tools()
async def list_tools() -> list[types.Tool]:
   return [
       types.Tool(
           name="calculate_sum",
           description="Add two numbers together",
           inputSchema={
               "type": "object",
               "properties": {
                   "a": {"type": "number"},
                   "b": {"type": "number"}
               },
               "required": ["a", "b"]
           }
       )
   ]

@app.call_tool()
async def call_tool(
   name: str,
   arguments: dict
) -> list[types.TextContent | types.ImageContent | types.EmbeddedResource]:
   if name == "calculate_sum":
       a = arguments["a"]
       b = arguments["b"]
       result = a + b
       return [types.TextContent(type="text", text=str(result))]
   raise ValueError(f"Tool not found: {name}")

ツールの使用例パターン

サーバーが提供可能なツールの例をいくつか示します:

システム操作

ローカルシステムと連携するツール:

{
  name: "execute_command",
  description: "Run a shell command",
  inputSchema: {
    type: "object",
    properties: {
      command: { type: "string" },
      args: { type: "array", items: { type: "string" } }
    }
  }
}

API 連携

外部 API をラップするツール:

{
  name: "github_create_issue",
  description: "Create a GitHub issue",
  inputSchema: {
    type: "object",
    properties: {
      title: { type: "string" },
      body: { type: "string" },
      labels: { type: "array", items: { type: "string" } }
    }
  }
}

データ処理

データを変換または解析するツール:

{
  name: "analyze_csv",
  description: "Analyze a CSV file",
  inputSchema: {
    type: "object",
    properties: {
      filepath: { type: "string" },
      operations: {
        type: "array",
        items: {
          enum: ["sum", "average", "count"]
        }
      }
    }
  }
}

ベストプラクティス

ツールを実装する際には以下を考慮してください:

  1. 明確で説明的な名前と説明を提供する
  2. パラメータのために詳細な JSON Schema 定義を使用する
  3. モデルがどのように利用するかを示す例をツールの説明に含める
  4. 適切なエラー処理と検証を実装する
  5. 長時間の操作には進捗報告を実装する
  6. ツールの操作は集中化され、原子的に保つ
  7. 期待される戻り値の構造を文書化する
  8. 適切なタイムアウトを実装する
  9. リソースを多く消費する操作にはレート制限を検討する
  10. デバッグやモニタリングのためにツール使用状況をログに記録する

セキュリティ上の考慮事項

ツールを公開する際には:

入力検証

  • すべてのパラメータをスキーマに対して検証する
  • ファイルパスやシステムコマンドをサニタイズする
  • URL や外部識別子を検証する
  • パラメータのサイズや範囲を確認する
  • コマンドインジェクションを防止する

アクセス制御

  • 必要に応じて認証を実装する
  • 適切な認可チェックを行う
  • ツールの利用状況を監査する
  • リクエストに対してレート制限を行う
  • 悪用を監視する

エラー処理

  • 内部エラーをクライアントに公開しない
  • セキュリティに関連するエラーをログに記録する
  • タイムアウトを適切に処理する
  • エラー発生後にリソースをクリーンアップする
  • 戻り値の検証を行う

ツールの発見と更新

MCP は動的なツール発見をサポートしています:

  1. クライアントはいつでも利用可能なツールを一覧できます
  2. サーバーは notifications/tools/list_changed を使用して、ツールの変更をクライアントに通知できます
  3. ツールはランタイム中に追加または削除できます
  4. ツール定義は更新可能ですが、慎重に行う必要があります

エラー処理

ツールのエラーは、MCP プロトコルレベルのエラーではなく、結果オブジェクト内で報告されるべきです。これにより、LLM はエラーを確認し、適切な対応(修正や人間による介入の要求など)を行うことができます。ツールでエラーが発生した場合は:

  1. 結果オブジェクト内の isErrortrue に設定する
  2. content 配列にエラーの詳細を含める

以下は、ツールの適切なエラー処理の例です:

try:
   # ツールの操作
   result = perform_operation()
   return types.CallToolResult(
       content=[
           types.TextContent(
               type="text",
               text=f"Operation successful: {result}"
           )
       ]
   )
except Exception as error:
   return types.CallToolResult(
       isError=True,
       content=[
           types.TextContent(
               type="text",
               text=f"Error: {str(error)}"
           )
       ]
   )

この方法により、LLM はエラーが発生したことを認識し、必要に応じて修正行動や人間の介入を求めることができます。

ツールのテスト

MCP ツールの包括的なテスト戦略は、以下をカバーすべきです:

  • 機能テスト: 有効な入力に対してツールが正しく実行され、無効な入力に対して適切に処理されることを確認する
  • 統合テスト: 実際およびモックされた依存関係を使用して、外部システムとのツールの連携をテストする
  • セキュリティテスト: 認証、認可、入力のサニタイズ、およびレート制限を検証する
  • パフォーマンステスト: 負荷下での動作、タイムアウトの処理、リソースのクリーンアップを確認する
  • エラー処理: ツールが MCP プロトコルを通じて適切にエラーを報告し、リソースをクリーンアップすることを確認する
kun432kun432

コンセプト: サンプリング

https://modelcontextprotocol.io/docs/concepts/sampling

公式ドキュメントの日本語訳

サンプリング

サーバーが LLM に補完リクエストを送る

サンプリングは、サーバーがクライアントを通じて LLM の補完をリクエストできる強力な MCP 機能であり、セキュリティとプライバシーを保ちながら高度なエージェント的動作を実現します。

サンプリングの仕組み

サンプリングのフローは以下のステップに従います:

  1. サーバーがクライアントに対して sampling/createMessage リクエストを送信する
  2. クライアントはリクエストを確認し、必要に応じて修正する
  3. クライアントが LLM から補完をサンプリングする
  4. クライアントが補完内容を確認する
  5. クライアントが結果をサーバーに返す

この「ヒューマン・イン・ザ・ループ」設計により、ユーザーは LLM が閲覧および生成する内容を制御できます。

メッセージフォーマット

サンプリングリクエストは、標準化されたメッセージフォーマットを使用します:

{
  messages: [
    {
      role: "user" | "assistant",
      content: {
        type: "text" | "image",

        // テキストの場合:
        text?: string,

        // 画像の場合:
        data?: string,             // base64 エンコードされたデータ
        mimeType?: string
      }
    }
  ],
  modelPreferences?: {
    hints?: [{
      name?: string                // 推奨されるモデル名/ファミリー
    }],
    costPriority?: number,         // 0-1, コスト最小化の重要度
    speedPriority?: number,        // 0-1, レイテンシ低減の重要度
    intelligencePriority?: number  // 0-1, 高度な能力の重要度
  },
  systemPrompt?: string,
  includeContext?: "none" | "thisServer" | "allServers",
  temperature?: number,
  maxTokens: number,
  stopSequences?: string[],
  metadata?: Record<string, unknown>
}

リクエストパラメータ

メッセージ

messages 配列には、LLM に送信する会話履歴が含まれます。各メッセージは以下の要素を持ちます:

  • role: "user" または "assistant"
  • content: メッセージの内容。これは以下のいずれかです:
    • text フィールドを持つテキストコンテンツ
    • data (base64) および mimeType フィールドを持つ画像コンテンツ

モデルの選好

modelPreferences オブジェクトにより、サーバーは利用可能なモデルから適切なモデルを選択するための指針を指定できます:

  • hints: クライアントが適切なモデルを選択するためのモデル名の推奨リスト:
    • name: 完全または部分的なモデル名にマッチする文字列 (例: "claude-3", "sonnet")
    • 複数のヒントが優先順に評価されます
  • 優先度 (0-1 の正規化値):
    • costPriority: コスト最小化の重要度
    • speedPriority: 低レイテンシ応答の重要度
    • intelligencePriority: 高度なモデル能力の重要度

クライアントは、これらの選好と利用可能なモデルに基づいて最終的なモデル選択を行います。

システムプロンプト

オプションの systemPrompt フィールドにより、サーバーは特定のシステムプロンプトをリクエストできます。クライアントはこれを修正または無視することができます。

コンテキストの包含

includeContext パラメータは、どの MCP コンテキストを含むかを指定します:

  • "none": 追加のコンテキストは含まない
  • "thisServer": リクエスト元サーバーのコンテキストを含む
  • "allServers": 接続されているすべての MCP サーバーのコンテキストを含む

クライアントは実際にどのコンテキストを含むかを制御します。

サンプリングパラメータ

LLM のサンプリングを微調整するために、以下のパラメータを指定できます:

  • temperature: ランダム性を制御 (0.0 から 1.0)
  • maxTokens: 生成する最大トークン数
  • stopSequences: 補完の停止を示すシーケンスの配列
  • metadata: その他のプロバイダー固有の追加パラメータ

レスポンスフォーマット

クライアントは、以下の形式で補完結果を返します:

{
  model: string,  // 使用されたモデルの名前
  stopReason?: "endTurn" | "stopSequence" | "maxTokens" | string,
  role: "user" | "assistant",
  content: {
    type: "text" | "image",
    text?: string,
    data?: string,
    mimeType?: string
  }
}

リクエスト例

以下は、クライアントに対してサンプリングをリクエストする例です:

{
  "method": "sampling/createMessage",
  "params": {
    "messages": [
      {
        "role": "user",
        "content": {
          "type": "text",
          "text": "What files are in the current directory?"
        }
      }
    ],
    "systemPrompt": "You are a helpful file system assistant.",
    "includeContext": "thisServer",
    "maxTokens": 100
  }
}

ベストプラクティス

サンプリングを実装する際には以下を考慮してください:

  1. 明確で構造化されたプロンプトを必ず提供する
  2. テキストと画像の両方のコンテンツを適切に扱う
  3. 適切なトークン制限を設定する
  4. includeContext を通じて関連するコンテキストを含める
  5. 応答を使用する前に必ず検証する
  6. エラーを適切に処理する
  7. サンプリングリクエストに対してレート制限を考慮する
  8. 期待されるサンプリング動作を文書化する
  9. 様々なモデルパラメータでテストする
  10. サンプリングコストを監視する

ヒューマン・イン・ザ・ループ制御

サンプリングは人間による監視を前提としています:

プロンプトの場合

  • クライアントはユーザーに提案されたプロンプトを表示すべきです
  • ユーザーはプロンプトを修正または拒否できるべきです
  • システムプロンプトはフィルタリングまたは修正可能です
  • コンテキストの包含はクライアント側で制御されます

補完の場合

  • クライアントはユーザーに補完結果を表示すべきです
  • ユーザーは補完結果を修正または拒否できるべきです
  • クライアントは補完結果をフィルタリングまたは修正可能です
  • 使用するモデルはユーザーが制御します

セキュリティ上の考慮事項

サンプリングを実装する際には、以下に注意してください:

  • すべてのメッセージコンテンツを検証する
  • 機密情報をサニタイズする
  • 適切なレート制限を実装する
  • サンプリングの使用状況を監視する
  • 転送中のデータを暗号化する
  • ユーザーデータのプライバシーを確保する
  • サンプリングリクエストを監査する
  • コストの露出を制御する
  • タイムアウトを実装する
  • モデルエラーを適切に処理する

一般的なパターン

エージェント的ワークフロー

サンプリングは、以下のようなエージェント的なパターンを可能にします:

  • リソースの読み取りと解析
  • コンテキストに基づいた意思決定
  • 構造化データの生成
  • マルチステップタスクの処理
  • インタラクティブな支援の提供

コンテキスト管理

コンテキスト管理のベストプラクティス:

  • 必要最小限のコンテキストをリクエストする
  • コンテキストを明確に構造化する
  • コンテキストサイズの制限に対処する
  • 必要に応じてコンテキストを更新する
  • 古いコンテキストをクリーンアップする

エラー処理

強固なエラー処理では以下を行うべきです:

  • サンプリングの失敗をキャッチする
  • タイムアウトエラーを処理する
  • レート制限を管理する
  • 応答を検証する
  • フォールバック動作を提供する
  • エラーを適切にログに記録する

制限事項

以下の制限事項に注意してください:

  • サンプリングはクライアントの機能に依存します
  • ユーザーがサンプリングの動作を制御します
  • コンテキストサイズには制限があります
  • レート制限が適用される場合があります
  • コストに留意する必要があります
  • モデルの利用可能性は変動します
  • 応答時間は状況により異なります
  • すべてのコンテンツタイプがサポートされるわけではありません
kun432kun432

コンセプト: ルート

https://modelcontextprotocol.io/docs/concepts/roots

公式ドキュメントの日本語訳

ルート

MCP におけるルートの理解

ルート は、サーバーが動作できる境界を定義する MCP の概念です。これにより、クライアントはサーバーに対して、関連するリソースやその位置について情報を提供することができます。

ルートとは?

ルートは、クライアントがサーバーに注目すべき URI を提案するものです。クライアントがサーバーに接続する際、どのルートでサーバーが作業すべきかを宣言します。主にファイルシステムのパスに使用されますが、ルートは HTTP URL を含む任意の有効な URI である可能性があります。

例えば、ルートは次のようになります:

file:///home/user/projects/myapp
https://api.example.com/v1

なぜ ルート を使用するのか?

ルート はいくつかの重要な目的を果たします:

  1. ガイダンス: ルートは、サーバーに関連するリソースや位置についての情報を提供します
  2. 明確性: ルートにより、どのリソースがワークスペースの一部であるかが明確になります
  3. 整理: 複数のルートを使用することで、異なるリソースを同時に扱うことができます

ルート の仕組み

クライアントがルートをサポートする場合、以下のことを行います:

  1. 接続時に roots 機能を宣言する
  2. サーバーに対して、提案されたルートのリストを提供する
  3. (サポートされている場合)ルートが変更された際にサーバーに通知する

ルートは情報提供的なものであり、厳密に強制されるものではありませんが、サーバーは以下のことを行うべきです:

  1. 提供されたルートを尊重する
  2. ルート URI を使用してリソースを特定し、アクセスする
  3. ルートの境界内での操作を優先する

一般的な使用例

ルートは一般的に以下を定義するために使用されます:

  • プロジェクトディレクトリ
  • リポジトリの場所
  • API エンドポイント
  • 設定の場所
  • リソースの境界

ベストプラクティス

ルート を使用する際には、以下の点に注意してください:

  1. 必要なリソースのみを提案する
  2. ルートに対して明確で説明的な名前を使用する
  3. ルートのアクセス性を監視する
  4. ルートの変更を適切に処理する

以下は、典型的な MCP クライアントがルートを公開する方法の例です:

{
  "roots": [
    {
      "uri": "file:///home/user/projects/frontend",
      "name": "Frontend Repository"
    },
    {
      "uri": "https://api.example.com/v1",
      "name": "API Endpoint"
    }
  ]
}

この構成は、サーバーがローカルのリポジトリと API エンドポイントの両方に注目するよう提案し、論理的に分離されていることを示しています。

kun432kun432

コンセプト: トランスポート

https://modelcontextprotocol.io/docs/concepts/transports

公式ドキュメントの日本語訳

トランスポート

MCP の通信メカニズムについて学ぶ

Model Context Protocol (MCP) におけるトランスポートは、クライアントとサーバー間の通信の基盤を提供します。トランスポートは、メッセージの送受信の基本的な仕組みを処理します。

メッセージフォーマット

MCP は、そのワイヤフォーマットとして JSON-RPC 2.0 を使用します。トランスポート層は、MCP プロトコルメッセージを送信のために JSON-RPC フォーマットに変換し、受信した JSON-RPC メッセージを MCP プロトコルメッセージに戻す役割を担います。

使用される JSON-RPC メッセージのタイプは以下の 3 種類です:

リクエスト

{
  jsonrpc: "2.0",
  id: number | string,
  method: string,
  params?: object
}

レスポンス

{
  jsonrpc: "2.0",
  id: number | string,
  result?: object,
  error?: {
    code: number,
    message: string,
    data?: unknown
  }
}

通知

{
  jsonrpc: "2.0",
  method: string,
  params?: object
}

標準トランスポートタイプ

MCP には、2 つの標準トランスポート実装が含まれています:

標準入力/出力 (stdio)

stdio トランスポートは、標準入力および出力ストリームを通じた通信を可能にします。これは、ローカル統合やコマンドラインツールに特に有用です。

stdio を使用する場面:

  • コマンドラインツールの構築
  • ローカル統合の実装
  • シンプルなプロセス間通信が必要な場合
  • シェルスクリプトとの連携
サーバー
app = Server("example-server")

async with stdio_server() as streams:
   await app.run(
       streams[0],
       streams[1],
       app.create_initialization_options()
   )
クライアント
params = StdioServerParameters(
   command="./server",
   args=["--option", "value"]
)

async with stdio_client(params) as streams:
   async with ClientSession(streams[0], streams[1]) as session:
       await session.initialize()

Server-Sent Events (SSE)

SSE トランスポートは、HTTP POST リクエストを使用したクライアントからサーバーへの通信と、サーバーからクライアントへのストリーミングを可能にします。

SSE を使用する場面:

  • サーバーからクライアントへのストリーミングのみが必要な場合
  • 制限されたネットワーク環境での利用
  • シンプルな更新通知の実装
サーバー
from mcp.server.sse import SseServerTransport
from starlette.applications import Starlette
from starlette.routing import Route

app = Server("example-server")
sse = SseServerTransport("/messages")

async def handle_sse(scope, receive, send):
   async with sse.connect_sse(scope, receive, send) as streams:
       await app.run(streams[0], streams[1], app.create_initialization_options())

async def handle_messages(scope, receive, send):
   await sse.handle_post_message(scope, receive, send)

starlette_app = Starlette(
   routes=[
       Route("/sse", endpoint=handle_sse),
       Route("/messages", endpoint=handle_messages, methods=["POST"]),
   ]
)
クライアント
async with sse_client("http://localhost:8000/sse") as streams:
   async with ClientSession(streams[0], streams[1]) as session:
       await session.initialize()

カスタムトランスポート

MCP は、特定のニーズに合わせたカスタムトランスポートの実装を容易にします。任意のトランスポート実装は、Transport インターフェースに準拠する必要があります:

カスタムトランスポートは以下の用途に利用できます:

  • カスタムネットワークプロトコル
  • 専用の通信チャネル
  • 既存システムとの統合
  • パフォーマンス最適化

注意: MCP サーバーは通常 asyncio を用いて実装されますが、より広い互換性を得るために、トランスポートなどの低レベルインターフェースは anyio を使用して実装することを推奨します。

@contextmanager
async def create_transport(
   read_stream: MemoryObjectReceiveStream[JSONRPCMessage | Exception],
   write_stream: MemoryObjectSendStream[JSONRPCMessage]
):
   """
   MCP のトランスポートインターフェース。

   Args:
       read_stream: 受信メッセージを読み取るためのストリーム
       write_stream: 送信メッセージを書き込むためのストリーム
   """
   async with anyio.create_task_group() as tg:
       try:
           # メッセージ処理の開始
           tg.start_soon(lambda: process_messages(read_stream))

           # メッセージの送信
           async with write_stream:
               yield write_stream

       except Exception as exc:
           # エラー処理
           raise exc
       finally:
           # クリーンアップ
           tg.cancel_scope.cancel()
           await write_stream.aclose()
           await read_stream.aclose()

エラー処理

トランスポート実装は、以下のエラーシナリオを処理する必要があります:

  1. 接続エラー
  2. メッセージ解析エラー
  3. プロトコルエラー
  4. ネットワークタイムアウト
  5. リソースのクリーンアップ

エラー処理の例:

注意: MCP サーバーは通常 asyncio を用いて実装されますが、より広い互換性を得るために、トランスポートなどの低レベルインターフェースは anyio を使用して実装することを推奨します。

@contextmanager
async def example_transport(scope: Scope, receive: Receive, send: Send):
   try:
       # 双方向通信のためのストリームを作成
       read_stream_writer, read_stream = anyio.create_memory_object_stream(0)
       write_stream, write_stream_reader = anyio.create_memory_object_stream(0)

       async def message_handler():
           try:
               async with read_stream_writer:
                   # メッセージ処理ロジック
                   pass
           except Exception as exc:
               logger.error(f"Failed to handle message: {exc}")
               raise exc

       async with anyio.create_task_group() as tg:
           tg.start_soon(message_handler)
           try:
               # 通信のためにストリームを yield
               yield read_stream, write_stream
           except Exception as exc:
               logger.error(f"Transport error: {exc}")
               raise exc
           finally:
               tg.cancel_scope.cancel()
               await write_stream.aclose()
               await read_stream.aclose()
   except Exception as exc:
       logger.error(f"Failed to initialize transport: {exc}")
       raise exc

ベストプラクティス

MCP トランスポートの実装または利用においては、以下を考慮してください:

  1. 接続ライフサイクルを適切に処理する
  2. 適切なエラー処理を実装する
  3. 接続終了時にリソースをクリーンアップする
  4. 適切なタイムアウトを使用する
  5. 送信前にメッセージを検証する
  6. デバッグのためにトランスポートイベントをログに記録する
  7. 必要に応じて再接続ロジックを実装する
  8. メッセージキューのバックプレッシャーに対応する
  9. 接続の健全性を監視する
  10. 適切なセキュリティ対策を実装する

セキュリティ上の考慮事項

トランスポート実装においては、以下を考慮してください:

認証と認可

  • 適切な認証機構を実装する
  • クライアントの資格情報を検証する
  • セキュアなトークン管理を行う
  • 認可チェックを実装する

データセキュリティ

  • ネットワークトランスポートには TLS を使用する
  • 機密データを暗号化する
  • メッセージの整合性を検証する
  • メッセージサイズの制限を実装する
  • 入力データをサニタイズする

ネットワークセキュリティ

  • レート制限を実装する
  • 適切なタイムアウトを設定する
  • サービス拒否攻撃 (DoS) に対応する
  • 異常なパターンを監視する
  • 適切なファイアウォールルールを実装する

デバッグトランスポート

トランスポートの問題をデバッグするためのヒント:

  1. デバッグログを有効にする
  2. メッセージの流れを監視する
  3. 接続状態を確認する
  4. メッセージフォーマットを検証する
  5. エラーシナリオをテストする
  6. ネットワーク解析ツールを使用する
  7. ヘルスチェックを実装する
  8. リソース使用状況を監視する
  9. エッジケースをテストする
  10. 適切なエラー追跡を実装する
kun432kun432

ざっとコンセプトを見てみたけど、まだ良くわかってない。とりあえず、MCPサーバ向けのQuickstartを進めてみる。

kun432kun432

Quickstart: サーバ開発者向け

https://modelcontextprotocol.io/quickstart/server

お題は以下となっている。

  • お天気MCPサーバ
    • 天気予報や気象警報を取得するMCPサーバ
    • サーバには以下の2つのツールを実装
      • get-alerts
      • get-forecast
    • Claude Desktopから接続する

なお、

なぜClaude for DesktopでClaude.aiではないのでしょうか?

サーバーはローカルで実行されるため、MCPは現在、デスクトップホストのみをサポートしています。リモートホストは現在開発中です。

らしい。

MCPサーバが提供する機能については、コアコンセプトにも色々書かれていたが、

MCPサーバーは主に3つの機能を提供します。

  • Resources: クライアントが読み取ることのできるファイルのようなデータ(APIのレスポンスやファイルの内容など)
  • Tools: (ユーザーの承認を受けて)LLMから呼び出すことのできる機能
  • Prompt: ユーザーが特定のタスクを達成するのを助ける、あらかじめ作成されたテンプレート

このチュートリアルでは、主にツールに焦点を当てます。

ということで、まあできることの幅を広げるという意味ではツールが中心にはなりそう。

今回はローカルのMacで進める。

uvでプロジェクトを作成する。

uv init weather && cd weather

main.pyは使用しないので削除。

rm main.py

Python仮想環境を作成

uv venv -p 3.12.9

パッケージインストール

uv add "mcp[cli]" httpx

メインのスクリプト(weather.py)を以下の内容で作成。FastMCPを使うと型ヒントとdocstringgから自動的にツール定義を生成してくれるらしい。

weather.py
from typing import Any
import httpx
from mcp.server.fastmcp import FastMCP

# FastMCP serverの初期化
mcp = FastMCP("weather")

# 定数
NWS_API_BASE = "https://api.weather.gov"
USER_AGENT = "weather-app/1.0"

使用されている天気APIはこれか。

https://www.weather.gov/

余談だが、以下で実際にAPIを試しているので、気になる場合は参照。

National Weather Service APIを使用した気象警報・天気予報の取得

APIのページはこちら

https://www.weather.gov/documentation/services-web-api

まず警報。2文字の州コードを指定する。以下はテキサス州(TX)の場合で、警報はリストで複数入っているので1つだけ抜粋した。

curl https://api.weather.gov/alerts/active/area/TX
    -H "User-Agent: weather-app/1.0" \
    -H "Accept: application/geo+json"  | jq -r .features[0].properties
出力
{
  "@id": "https://api.weather.gov/alerts/urn:oid:2.49.0.1.840.0.f6e51155e2067acf2bf0d9b755c6384ae0cc3558.001.1",
  "@type": "wx:Alert",
  "id": "urn:oid:2.49.0.1.840.0.f6e51155e2067acf2bf0d9b755c6384ae0cc3558.001.1",
  "areaDesc": "Archer; Clay",
  "geocode": {
    "SAME": [
      "048009",
      "048077"
    ],
    "UGC": [
      "TXZ089",
      "TXZ090"
    ]
  },
  "affectedZones": [
    "https://api.weather.gov/zones/forecast/TXZ089",
    "https://api.weather.gov/zones/forecast/TXZ090"
  ],
  "references": [],
  "sent": "2025-04-02T06:44:00-05:00",
  "effective": "2025-04-02T06:44:00-05:00",
  "onset": "2025-04-02T06:44:00-05:00",
  "expires": "2025-04-02T07:15:00-05:00",
  "ends": null,
  "status": "Actual",
  "messageType": "Alert",
  "category": "Met",
  "severity": "Moderate",
  "certainty": "Observed",
  "urgency": "Expected",
  "event": "Special Weather Statement",
  "sender": "w-nws.webmaster@noaa.gov",
  "senderName": "NWS Norman OK",
  "headline": "Special Weather Statement issued April 2 at 6:44AM CDT by NWS Norman OK",
  "description": "At 643 AM CDT, Doppler radar was tracking a strong thunderstorm 10\nmiles east of Olney, moving northeast at 60 mph.\n\nHAZARD...Wind gusts up to 50 mph and penny size hail.\n\nSOURCE...Radar indicated.\n\nIMPACT...Gusty winds could knock down tree limbs and blow around\nunsecured objects. Minor damage to outdoor objects is\npossible.\n\nLocations impacted include...\nShannon, Joy, and Windthorst.",
  "instruction": "If outdoors, consider seeking shelter inside a building.",
  "response": "Execute",
  "parameters": {
    "AWIPSidentifier": [
      "SPSOUN"
    ],
    "WMOidentifier": [
      "WWUS84 KOUN 021144"
    ],
    "NWSheadline": [
      "A strong thunderstorm will impact portions of southwestern Clay and southeastern Archer Counties through 715 AM CDT"
    ],
    "eventMotionDescription": [
      "2025-04-02T11:43:00-00:00...storm...241DEG...53KT...33.4,-98.59"
    ],
    "maxWindGust": [
      "50 MPH"
    ],
    "maxHailSize": [
      "0.75"
    ],
    "BLOCKCHANNEL": [
      "EAS",
      "NWEM",
      "CMAS"
    ],
    "EAS-ORG": [
      "WXR"
    ]
  }
}

次に天気予報。こちらは緯度経度で指定する。以下はテキサス州オースチンのテキサス大学の緯度経度を渡している。

curl https://api.weather.gov/points/30.2851,-97.733 \
    -H "User-Agent: weather-app/1.0" \
    -H "Accept: application/geo+json" | jq -r .properties
出力
{
  "@id": "https://api.weather.gov/points/30.2851,-97.733",
  "@type": "wx:Point",
  "cwa": "EWX",
  "forecastOffice": "https://api.weather.gov/offices/EWX",
  "gridId": "EWX",
  "gridX": 156,
  "gridY": 92,
  "forecast": "https://api.weather.gov/gridpoints/EWX/156,92/forecast",
  "forecastHourly": "https://api.weather.gov/gridpoints/EWX/156,92/forecast/hourly",
  "forecastGridData": "https://api.weather.gov/gridpoints/EWX/156,92",
  "observationStations": "https://api.weather.gov/gridpoints/EWX/156,92/stations",
  "relativeLocation": {
    "type": "Feature",
    "geometry": {
      "type": "Point",
      "coordinates": [
        -97.754358,
        30.30394
      ]
    },
    "properties": {
      "city": "Austin",
      "state": "TX",
      "distance": {
        "unitCode": "wmoUnit:m",
        "value": 2931.4871636478
      },
      "bearing": {
        "unitCode": "wmoUnit:degree_(angle)",
        "value": 135
      }
    }
  },
  "forecastZone": "https://api.weather.gov/zones/forecast/TXZ192",
  "county": "https://api.weather.gov/zones/county/TXC453",
  "fireWeatherZone": "https://api.weather.gov/zones/fire/TXZ192",
  "timeZone": "America/Chicago",
  "radarStation": "KGRK"
}

結果に天気予報のURL(forecast)が含まれるので、これを再度リクエスト。こちらも時間ごとに複数の結果がリストに入ってくるので、1つだけピックアップ。

curl https://api.weather.gov/gridpoints/EWX/156,92/forecast \
    -H "User-Agent: weather-app/1.0" \
    -H "Accept: application/geo+json" | jq -r .properties.periods[0]
出力
{
  "number": 1,
  "name": "Today",
  "startTime": "2025-04-02T08:00:00-05:00",
  "endTime": "2025-04-02T18:00:00-05:00",
  "isDaytime": true,
  "temperature": 88,
  "temperatureUnit": "F",
  "temperatureTrend": "",
  "probabilityOfPrecipitation": {
    "unitCode": "wmoUnit:percent",
    "value": 20
  },
  "windSpeed": "5 to 10 mph",
  "windDirection": "SSE",
  "icon": "https://api.weather.gov/icons/land/day/tsra,20/bkn?size=medium",
  "shortForecast": "Slight Chance Showers And Thunderstorms then Mostly Cloudy",
  "detailedForecast": "A slight chance of showers and thunderstorms before 10am. Mostly cloudy, with a high near 88. South southeast wind 5 to 10 mph, with gusts as high as 25 mph. Chance of precipitation is 20%."
}

このAPIにリクエストを送信してレスポンスをフォーマットするためのヘルパー関数を追加する。これは気象警報も天気予報も共通。

weather.py
(snip)

async def make_nws_request(url: str) -> dict[str, Any] | None:
    """National Weather Service APIに、適切なエラー処理を行いつつ、リクエストを送信する。"""
    headers = {
        "User-Agent": USER_AGENT,
        "Accept": "application/geo+json"
    }
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(url, headers=headers, timeout=30.0)
            response.raise_for_status()
            return response.json()
        except Exception:
            return None

気象警報の方は出力を読みやすいフォーマットに変換するヘルパー関数も追加。

weather.py
(snip)

def format_alert(feature: dict) -> str:
    """National Weather Service APIの警報を人間の読みやすい文字列にフォーマット変換する。"""
    props = feature["properties"]
    return (
        f"Event: {props.get('event', '不明')}\n"
        f"Area: {props.get('areaDesc', '不明')}\n"
        f"Severity: {props.get('severity', '不明')}\n"
        f"Description: {props.get('description', '説明はありません。')}\n"
        f"Instructions: {props.get('instruction', '指示は出ていません。')}\n"
    )

では気象警報・天気予報を取得する関数を定義して、@mcp.toolデコレータでツールとして定義する。

weather.py
(snip)

@mcp.tool()
async def get_alerts(state: str) -> str:
    """米国の州に対する天気警報を取得する

    Args:
        state: 米国の2文字の州コード (例: CA, NY)
    """
    url = f"{NWS_API_BASE}/alerts/active/area/{state}"
    data = await make_nws_request(url)

    if not data or "features" not in data:
        return "警報の取得に失敗したか、警報が見つかりませんでした。"

    if not data["features"]:
        return "この州には現在有効な警報はありません。"

    alerts = [format_alert(feature) for feature in data["features"]]
    return "\n---\n".join(alerts)


@mcp.tool()
async def get_forecast(latitude: float, longitude: float) -> str:
    """特定の場所の天気予報を取得する

    Args:
        latitude: 場所の緯度
        longitude: 場所の経度
    """
    # 予報グリッドのエンドポイントを取得
    points_url = f"{NWS_API_BASE}/points/{latitude},{longitude}"
    points_data = await make_nws_request(points_url)

    if not points_data:
        return "この場所の予報データの取得に失敗しました。"

    # points のレスポンスから予報の URL を取得
    forecast_url = points_data["properties"]["forecast"]
    forecast_data = await make_nws_request(forecast_url)

    if not forecast_data:
        return "詳細な予報の取得に失敗しました。"

    # 期間ごとに整形して読みやすい予報に変換
    periods = forecast_data["properties"]["periods"]
    forecasts = []
    for period in periods[:5]:  # 次の5期間のみ表示
        forecast = (
            f"{period['name']}:\n"
            f"気温: {period['temperature']}°{period['temperatureUnit']}\n"
            f"風: {period['windSpeed']} {period['windDirection']}\n"
            f"予報: {period['detailedForecast']}\n"
        )
        forecasts.append(forecast)

    return "\n---\n".join(forecasts)

最後にサーバを初期化して起動

(snip)

if __name__ == "__main__":
    # サーバを初期化して起動
    mcp.run(transport='stdio')

ではClaude Desktopでこの天気MCPサーバを使用するようにする。パスは各自の環境に合わせて。

以下のように設定してClaude Desktopを再起動。

claude_desktop_config.json
{
    "mcpServers": {
        "weather": {
            "command": "uv",
            "args": [
                "--directory",
                "/Users/kun432/work/mcp-work/weather",
                "run",
                "weather.py"
            ]
        }
    }
}

読み込まれていればOK

では試しに聞いてみる。ツールごとに都度実行許可を求めてくるので適宜許可。

動作しているのがわかる。実際の動きは

内部では何が起こっているのか

あなたが質問をすると:

  1. クライアントがあなたの質問を Claude に送信します
  2. Claude は利用可能なツールを分析し、どのツールを使用するかを決定します
  3. クライアントは MCP サーバーを通じて選ばれたツールを実行します
  4. 結果は Claude に返されます
  5. Claude は自然言語で応答を作成します
  6. 応答があなたに表示されます!

という感じ。

kun432kun432

Quickstart: クライアント開発者向け

https://modelcontextprotocol.io/quickstart/client

次はクライアント。Claude Desktopでやっていたことを、チャットアプリでやる、Function Callingの呼び出し側のイメージ。

uvでプロジェクト作成。プロジェクトはMCPサーバと同じ階層に作成した。

uv init mcp-client && cd mcp-client

main.pyは使用しないので削除。

rm main.py

Python仮想環境も作成。

uv venv -p 3.12.9

パッケージ追加

uv add mcp anthropic python-dotenv

.envを作成して、AnthropicのAPIキーをセット

.env
ANTHROPIC_API_KEY=XXXXXXXXXX

一応、.env.gitignoreに追加

echo ".env" >> .gitignore

ではクライアント用スクリプトをclient.pyとして書いていく。

まず、MCPクライアントクラスを作成する。自分はPythonメインなのでJS向けの処理は消した。

client.py
import asyncio
from typing import Optional
from contextlib import AsyncExitStack

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

from anthropic import Anthropic
from dotenv import load_dotenv

load_dotenv()  # .env から環境変数を読み込む

class MCPClient:
    def __init__(self):
        # セッションとクライアントオブジェクトを初期化する
        self.session: Optional[ClientSession] = None
        self.exit_stack = AsyncExitStack()
        self.anthropic = Anthropic()

MCPサーバに接続するメソッドを追加。

client.py
(snip)

    async def connect_to_server(self, server_script_path: str):
        """MCP サーバーに接続する

        Args:
            server_script_path: サーバースクリプトのパス (.py ファイル)
        """
        if not server_script_path.endswith('.py'):
            raise ValueError("サーバースクリプトは .py ファイルでなければなりません")

        command = "python"
        server_params = StdioServerParameters(
            command=command,
            args=[server_script_path],
            env=None
        )

        stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
        self.stdio, self.write = stdio_transport
        self.session = await self.exit_stack.enter_async_context(ClientSession(self.stdio, self.write))

        await self.session.initialize()

        # 利用可能なツールを一覧表示する
        response = await self.session.list_tools()
        tools = response.tools
        print("\nサーバーに接続しました。利用可能なツール:", [tool.name for tool in tools])

ユーザからのクエリを受けて、必要ならMCPサーバから取得したツールをFunction Callingで呼び出して、回答を行う。なお、モデルはHaikuにした。

client.py
(snip)

    async def process_query(self, query: str) -> str:
        """Claude と利用可能なツールを使用してクエリを処理する"""
        messages = [
            {
                "role": "user",
                "content": query
            }
        ]

        response = await self.session.list_tools()
        available_tools = [{
            "name": tool.name,
            "description": tool.description,
            "input_schema": tool.inputSchema
        } for tool in response.tools]

        # 最初の Claude API 呼び出し
        response = self.anthropic.messages.create(
            model="claude-3-haiku-20240307",
            max_tokens=1000,
            messages=messages,
            tools=available_tools
        )

        # レスポンスを処理し、ツール呼び出しを処理する
        final_text = []

        assistant_message_content = []
        for content in response.content:
            if content.type == 'text':
                final_text.append(content.text)
                assistant_message_content.append(content)
            elif content.type == 'tool_use':
                tool_name = content.name
                tool_args = content.input

                # ツール呼び出しを実行する
                result = await self.session.call_tool(tool_name, tool_args)
                final_text.append(f"[ツール {tool_name} を引数 {tool_args} で呼び出し中]")

                assistant_message_content.append(content)
                messages.append({
                    "role": "assistant",
                    "content": assistant_message_content
                })
                messages.append({
                    "role": "user",
                    "content": [
                        {
                            "type": "tool_result",
                            "tool_use_id": content.id,
                            "content": result.content
                        }
                    ]
                })

                # Claude から次のレスポンスを取得する
                response = self.anthropic.messages.create(
                    model="claude-3-5-haiku-20241022",
                    max_tokens=1000,
                    messages=messages,
                    tools=available_tools
                )

                final_text.append(response.content[0].text)

        return "\n".join(final_text)

チャットをループで処理するメソッドと終了時のメソッドを追加。

client.py
(snip)

    async def chat_loop(self):
        """対話型チャットループを実行する"""
        print("\nMCP クライアントが起動しました!")
        print("クエリを入力するか、終了するには 'quit' と入力してください。")

        while True:
            try:
                query = input("\nクエリ: ").strip()

                if query.lower() == 'quit':
                    break

                response = await self.process_query(query)
                print("\n" + response)

            except Exception as e:
                print(f"\nエラー: {str(e)}")

    async def cleanup(self):
        """リソースをクリーンアップする"""
        await self.exit_stack.aclose()

メイン

```python:client.py
(snip)

async def main():
    if len(sys.argv) < 2:
        print("使用法: python client.py <server_scriptのパス>")
        sys.exit(1)

    client = MCPClient()
    try:
        await client.connect_to_server(sys.argv[1])
        await client.chat_loop()
    finally:
        await client.cleanup()

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

MCPサーバも含めて、ディレクトリ構成はこんな感じになった。

tree -a -I .venv -I .git ..
出力
..
├── mcp-client
│   ├── .env
│   ├── .gitignore
│   ├── .python-version
│   ├── README.md
│   ├── client.py
│   ├── pyproject.toml
│   └── uv.lock
└── weather
    ├── .gitignore
    ├── .python-version
    ├── README.md
    ├── pyproject.toml
    ├── uv.lock
    └── weather.py

3 directories, 13 files

実行。client.pyの引数にMCPサーバのスクリプトのパスを渡す。

uv run client.py ../weather/weather.py
出力
Processing request of type ListToolsRequest

サーバーに接続しました。利用可能なツール: ['get_alerts', 'get_forecast']

MCP クライアントが起動しました!
クエリを入力するか、終了するには 'quit' と入力してください。

クエリ: おはよう!
Processing request of type ListToolsRequest

おはようございます!
天気予報や気象警報について知りたいことがありましたら、お手伝いさせていただきます。

具体的には以下のようなことができます:
1. アメリカの各州の気象警報の確認 (州コードを指定していただく必要があります)
2. 特定の場所の天気予報の取得 (緯度・経度を指定していただく必要があります)

ご質問がありましたら、お気軽にどうぞ!

クエリ: ニューヨークの気象警報を教えて。
Processing request of type ListToolsRequest
Processing request of type CallToolRequest
HTTP Request: GET https://api.weather.gov/alerts/active/area/NY "HTTP/1.1 200 OK"

ニューヨーク州の気象警報を確認するために、get_alerts関数を使用します。ニューヨーク州の州コードは"NY"です。
[ツール get_alerts を引数 {'state': 'NY'} で呼び出し中]
ニューヨーク州全体で、主に強風と雪氷混合が予想されています。強風による倒木や停電の可能性がありますので、外出時は十分注意する必要があります。また、道路状況が悪化する可能性もあるため、移動時は慎重に運転するようにしてください。

クエリ: テキサス大学オースチン高の緯度経度を教えて。
Processing request of type ListToolsRequest

テキサス大学オースティン校のメインキャンパス(チン高)の緯度・経度を取得する必要がありますが、正確な値は以下の通りです:

緯度: 30.2862
経度: -97.7394

これらの座標を使って天気予報を取得することも可能です。天気予報が必要な場合はお知らせください。

クエリ: 緯度: 30.2862、経度: -97.7394の天気を教えて。
Processing request of type ListToolsRequest
Processing request of type CallToolRequest
HTTP Request: GET https://api.weather.gov/points/30.2862,-97.7394 "HTTP/1.1 200 OK"
HTTP Request: GET https://api.weather.gov/gridpoints/EWX/156,92/forecast "HTTP/1.1 200 OK"

承知しました。指定された座標(緯度30.2862、経度-97.7394)の天気予報を取得します。この座標はテキサス州オースチン付近のようですね。
[ツール get_forecast を引数 {'latitude': 30.2862, 'longitude': -97.7394} で呼び出し中]
オースチン付近の天気予報をお知らせしました。明日にかけては曇りの日が続き、時折雨や雷雨の可能性がありそうです。気温は高めに推移する見込みです。ご参考にしていただければ幸いです。

クエリ: quit

会話履歴を維持していないので恣意的なクエリにしているが、MCPサーバの2つのツールを使って会話ができていることがわかる。

処理の流れは以下。

クエリを送信すると、以下のように処理されます:

  1. クライアントはサーバーから利用可能なツールの一覧を取得します
  2. あなたのクエリは、ツールの説明とともに Claude に送られます
  3. Claude は、使用すべきツール(ある場合)を決定します
  4. クライアントは、要求されたツール呼び出しをサーバーを通じて実行します
  5. 結果は Claude に返されます
  6. Claude は自然言語で応答を生成します
  7. その応答があなたに表示されます

またベストプラクティスが記載されている

ベストプラクティス

  1. エラー処理
    • ツール呼び出しは常に try-catch ブロックで囲む
    • 意味のあるエラーメッセージを提供する
    • 接続の問題は適切に処理する
  2. リソース管理
    • 適切なクリーンアップのために AsyncExitStack を使用する
    • 終了時には接続を閉じる
    • サーバーの切断に対処する
  3. セキュリティ
    • API キーは .env に安全に保存する
    • サーバーのレスポンスを検証する
    • ツールの権限には十分注意する
kun432kun432

MCPに対応しているサーバ&クライアントは以下に記載がある

https://modelcontextprotocol.io/examples

https://modelcontextprotocol.io/clients

あと、サーバについては以下にも掲載されているし、他にもリスティングしてるものがいろいろありそう。

https://github.com/modelcontextprotocol/servers

何かしらMCP対応クライアントを使っていて(例えばClaude DesktopとかCursorとかWindsurfとか)、MCPサーバを便利に使いたいだけなら、適当に探して使えば良い。

ただ、自分でMCPサーバ or クライアントを作ろうとした場合、Quickstartの内容だけ見ると、個人的にはFunction Callingがちょっと便利になった、ぐらいの印象しかない(まあそれでも便利だとは思うけども)。コンセプトのところをもっとしっかり読んだり、あとはリファレンスの実装コードを読む、などが必要になりそう。

kun432kun432

OpenAI SDKでのクライアント実装例。

https://www.ai-shift.co.jp/techblog/5226

MCP Serverは様々な機能をMCP Clientに提供しますが、今回扱ったToolsにおいて実現できることはFunction Callingとそれほど変わりません。

そうだよね。

しかし、MCPはサーバーとクライアントの責任を明確に分離することで、開発のスピードを一層加速させるように設計されており、これによってLLMが利用できるツールやリソースの選択肢が増えたことは非常に大きなインパクトがあります。今後はMCPを活用して様々なツールと連携するAIエージェントを作成してみたいと考えています。

まあそうだよね、ここがMCPという仕組みで結合度が下がったってのが大きいところではある。

とはいえ、もうちょっとありそうなんだよな・・・やっぱもう少しドキュメント読む。

kun432kun432

クライアントとして良さそう。

https://github.com/OpenAgentPlatform/Dive

Dive AI Agent 🤿 🤖

Diveは、関数呼び出し機能をサポートするあらゆるLLM(大規模言語モデル)とシームレスに統合できる、オープンソースのMCPホストデスクトップアプリケーションです。✨

特徴 🎯

  • 🌐 ユニバーサルLLM対応:ChatGPT、Anthropic、Ollama、およびOpenAI互換モデルに対応
  • 💻 クロスプラットフォーム:Windows、MacOS、Linuxで利用可能
  • 🔄 モデルコンテキストプロトコル(MCP):stdioモードとSSEモードの両方でMCP AIエージェントとのシームレスな統合を実現
  • 🌍 多言語対応:繁体字中国語、簡体字中国語、英語、スペイン語に対応(今後さらに追加予定)
  • ⚙️ 高度なAPI管理:複数のAPIキーとモデルの切り替えをサポート
  • 💡 カスタム命令:パーソナライズされたシステムプロンプトでAIの挙動を最適化
  • 🔄 自動更新機能:アプリケーションの最新アップデートを自動でチェック&インストール
このスクラップは5ヶ月前にクローズされました