見出し画像

FastMCP での MCPサーバ と MCPクライアント の構築を試す

「FastMCP」での「MCPサーバ」と「MCPクライアント」の構築を試したので、まとめました。


1. FastMCP

FastMCP」は、「MCPサーバ」や「MCPクライアント」をシンプルかつ直感的に構築できるパッケージです。

「MCP」の実装には、サーバのセットアップ、プロトコルハンドラ、コンテンツタイプ、エラー管理など、多くの定型処理が必要です。「FastMCP」は複雑なプロトコルとサーバ管理をすべて処理するため、開発者はツールの構築に集中できます。

「FastMCP」の目標は次のとおりです。

・高速
高水準インターフェースによりコードが少なくなり、開発が高速化
・シンプル
最小限の定型文で MCP サーバを構築
・Pythonic
Python開発者にとって自然に感じられる
・完全
コアMCP仕様の完全な実装を提供することを目指している

FastMCP v1」は、 MCPサーバを公開するための最も一般的なボイラープレートの抽象化に重点を置いており、現在では公式「MCP Python SDK」に含まれています。「FastMCP v2」では、その基盤を拡張し、柔軟なクライアント、プロキシとコンポジション、デプロイメントなど、主にサーバインタラクションの簡素化に重点を置いた新しい機能を導入しています。

2. MCP

MCP」 (Model Context Protocol) を使用すると、LLMアプリケーションにデータと機能を安全かつ標準化された方法で公開するサーバを構築できます。これは「AI用のUSB-Cポート」とも呼ばれ、LLMを利用可能なリソースに接続するための統一された方法を提供します。APIと考えると分かりやすいかもしれませんが、LLMとのやり取りに特化して設計されています。

MCPサーバ」は以下のことが可能です。

・Resource
データを公開します。GETエンドポイントのようなものです。LLMのコンテキストに情報をロードするために使用します。
・Tool
関数を提供します。POSTエンドポイントのようなものです。コードを実行したりするために使用します。
Prompt
LLMインタラクションの再利用可能なテンプレートを通じてインタラクションパターンを定義します。

3. FastMCPでのMCPサーバの構築

FastMCPでのMCPサーバの構築手順は、次のとおりです。

(1) uvのインストール。

(2) プロジェクトの作成。

uv init -p 3.10 letter_counter
cd letter_counter

(3) パッケージのインストール。

uv add fastmcp

(4) コードを作成。
単語やフレーズの文字数を数えるシンプルなMCPサーバを作成します。

・letter_counter.py

from fastmcp import FastMCP

mcp = FastMCP("Letter Counter")

@mcp.tool()
def letter_counter(word: str, letter: str) -> int:
    """
    単語の中に文字が何回現れるかを数える。

    Args:
        word: 分析する単語またはフレーズ
        letter: 出現回数を数える文字

    Returns:
        単語中にその文字が現れる回数
    """
    return word.lower().count(letter.lower())

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

4. CursorでのMCPサーバの利用

CursorでのMCPサーバの利用手順は、次のとおりです。

(1) 「Cursor」をインストールして起動。

(2) メニュー「Cursor → 基本設定 → Cursor Settings → MCP」で「Add new global MCP server」をクリックし、「mcp.json」を以下のように編集。
--directoryには自分のプロジェクトの絶対パスを指定してください。

{
  "mcpServers": {
    "letter-counter": {
      "command": "uv",
      "args": [
        "--directory",
        "/Users/npaka/Documents/letter_counter",
        "run",
        "letter_counter.py"
      ]
    }
  }
}

(3) 「AI Pane」で「Strawberryに含まれるrの数は?」と質問。
gpt-4oでも3つと答えることができることを確認します。

5. FastMCPでMCPクライアントの構築

(1) コードの作成。

・letter_counter_client.py

import asyncio
from fastmcp import Client
from fastmcp.client.transports import PythonStdioTransport

async def main():
    # サーバースクリプトのパス
    server_script = "/Users/npaka/Documents/letter_counter/letter_counter.py"

    # PythonStdioTransportを使用してサーバを起動
    transport = PythonStdioTransport(script_path=server_script)

    # クライアントを作成し、サーバと通信
    async with Client(transport) as client:
        # 利用可能なツールを取得
        tools = await client.list_tools()
        print("ツール:" ,tools)

        # ツールの呼び出し
        result = await client.call_tool("letter_counter", {"word": "Strawberry", "letter": "r"})
        print("ツール呼び出し結果:", result)

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

(2) コードの実行。

uv run letter_counter_client.py
ツール: [
  Tool(
    name='letter_counter',
    description='''
    単語の中に文字が何回現れるかを数える。

    Args:
        word: 分析する単語またはフレーズ
        letter: 出現回数を数える文字

    Returns:
        単語中にその文字が現れる回数
    ''',
    inputSchema={
      'properties': {
        'word': {'title': 'Word', 'type': 'string'},
        'letter': {'title': 'Letter', 'type': 'string'}
      },
      'required': ['word', 'letter'],
      'title': 'letter_counterArguments',
      'type': 'object'
    },
    annotations=None
  )
]

ツール呼び出し結果: [
  TextContent(
    type='text',
    text='3',
    annotations=None
  )
]

6. FastMCPでのMCPホストの構築

OpenAIのFunction Callingで、MCPクライアントを呼び出してみます。

(1) 環境変数「OPENAI_API_KEY」に「OpenAI APIキー」を設定。

(2) コードの作成

・letter_counter_host.py

import asyncio
import json
import os
from openai import OpenAI
from fastmcp import Client
from fastmcp.client.transports import PythonStdioTransport

# OpenAIのクライアントの準備
openai_api_key = os.getenv("OPENAI_API_KEY")
client = OpenAI(api_key=openai_api_key)

# サーバースクリプトのパス
SERVER_SCRIPT = "/Users/npaka/Documents/letter_counter/letter_counter.py"

# MCPサーバからツールスキーマを取得
async def get_tools():
    transport = PythonStdioTransport(script_path=SERVER_SCRIPT)
    async with Client(transport) as client:
        tools = await client.list_tools()
        return tools

# MCPサーバのツールを呼び出す
async def call_tool(tool_name, tool_args):
    transport = PythonStdioTransport(script_path=SERVER_SCRIPT)
    async with Client(transport) as client:
        result = await client.call_tool(tool_name, tool_args)
        return result

def main():
    # メッセージリストの準備
    messages = [
        {"role": "user", "content": "Strawberryに含まれるrの数は?"}
    ]    

    # ツールの準備
    tools = [
        {
            "type": "function",
            "name": tool.name,
            "description": tool.description,
            "parameters": tool.inputSchema
        }
        for tool in asyncio.run(get_tools())
    ]
    print("ツール:", tools)

    # 推論の実行
    response = client.responses.create(
        model="gpt-4o",
        input=messages,
        tools=tools
    )

    if response.output[0].type == "function_call":
        # 関数呼び出し
        tool_call = response.output[0]
        tool_name = tool_call.name
        tool_args = json.loads(tool_call.arguments)
        result = asyncio.run(call_tool(tool_name, tool_args))
        print("関数呼び出し結果:", result)


        # メッセージリストの準備
        messages.append(tool_call)
        messages.append({
            "type": "function_call_output",
            "call_id": tool_call.call_id,
            "output": str(result)
        })

        # 推論の実行
        response2 = client.responses.create(
            model="gpt-4o",
            input=messages,
            tools=tools,
        )
        print("応答:", response2.output_text)        
    else:
        print("関数呼び出しが見つかりませんでした。")

if __name__ == "__main__":
    main()

(3) コードの実行。

uv run letter_counter_host.py
ツール: [
  {
    'type': 'function',
    'name': 'letter_counter',
    'description': '''
    単語の中に文字が何回現れるかを数える。

    Args:
        word: 分析する単語またはフレーズ
        letter: 出現回数を数える文字

    Returns:
        単語中にその文字が現れる回数
    ''',
    'parameters': {
      'properties': {
        'word': {'title': 'Word', 'type': 'string'},
        'letter': {'title': 'Letter', 'type': 'string'}
      },
      'required': ['word', 'letter'],
      'title': 'letter_counterArguments',
      'type': 'object'
    }
  }
]

関数呼び出し結果: [
    TextContent(type='text', text='3', annotations=None)
]

応答: 「Strawberry」には「r」が3回含まれています

次回



いいなと思ったら応援しよう!