メインコンテンツへスキップ
見出し画像

FastMCP 入門 (4) - リソース

    「FastMCP」の「リソース」についてまとめました。

    ・Resources & Templates

    前回

    1. リソース

    「リソース」はMCPクライアントが読み取れるデータまたはファイルを表します。「リソーステンプレート」はこの概念を拡張し、クライアントがURIで渡されたパラメータに基づいて動的に生成されたリソースをリクエストできるようにします。

    FastMCPは、主に@mcp.resourceデコレータを使用して、静的リソースと動的リソースの両方の定義を簡素化します。

    ・FastMCPは対応するリソース定義を検索します。
    ・リソースが動的な場合 (関数によって定義されている場合)、その関数が実行されます。
    ・コンテンツ (テキスト、JSON、バイナリデータ) がクライアントに返されます。

    これにより、LLMはファイル、データベースコンテンツ、構成、会話に関連する動的生成された情報にアクセスできます。

    2. @mcp.resourceデコレータ

    リソースを定義する最も一般的な方法は、Python関数をデコレートすることです。デコレータには、リソースの一意のURIが必要です。

    import json
    from fastmcp import FastMCP
    
    mcp = FastMCP(name="DataServer")
    
    # 文字列を返す基本的な動的リソース
    @mcp.resource("resource://greeting")
    def get_greeting() -> str:
        """簡単な挨拶メッセージを提供"""
        return "FastMCP Resources からこんにちは!"
    
    # JSON データを返すリソース (dict は自動シリアライズ)
    @mcp.resource("data://config")
    def get_config() -> dict:
        """アプリケーション構成を JSONとして提供"""
        return {
            "theme": "dark",
            "version": "1.2.0",
            "features": ["tools", "resources"],
        }

    ・URI
    @resource の最初の引数は、クライアントがこのデータをリクエストするために使用する一意の URI (例: "resource://greeting") です。

    ・遅延読み込み
    デコレートされた関数 (get_greeting、get_config) は、クライアントが resources/read 経由でそのリソース URI を明示的にリクエストした場合にのみ実行されます。

    ・推論されたメタデータ
    デフォルトでは、以下のようになります。
     ・リソース名 : 関数名 (get_greeting) から取得されます。
     ・リソースの説明 : 関数のドキュメント文字列から取得されます。

    3. 戻り値

    FastMCPは、関数の戻り値を適切なMCPリソースコンテンツに自動的に変換します。

    ・str
    TextResourceContents として送信されます (デフォルトでは mime_type="text/plain")。

    ・dict、list、pydantic.BaseModel
    自動的に JSON 文字列にシリアル化され、TextResourceContents として送信されます (デフォルトでは mime_type="application/json")。

    ・bytes
    Base64エンコードされ、BlobResourceContents として送信されます。適切な mime_type (例: "image/png"、"application/octet-stream") を指定してください。

    ・None
    空のリソースコンテンツリストが返されます。

    4. リソースのメタデータ

    デコレータの引数を使用して、リソースのプロパティをカスタマイズできます。

    from fastmcp import FastMCP
    
    mcp = FastMCP(name="DataServer")
    
    # メタデータを指定する例
    @mcp.resource(
        uri="data://app-status",      # 明示的なURI (必須)
        name="ApplicationStatus",     # カスタム名
        description="アプリケーションの現在のステータスを提供", # カスタム説明
        mime_type="application/json", # 明示的なMIMEタイプ
        tags={"monitoring", "status"} # 分類タグ
    )
    def get_application_status() -> dict:
        """内部関数の説明 (上記に説明が提供されている場合は無視されます)"""
        return {"status": "ok", "uptime": 12345, "version": mcp.settings.version} # Example usage

    ・uri
    リソースの一意の識別子 (必須)

    ・name
    人間が読める名前 (デフォルトは関数名)

    ・description
    リソースの説明 (デフォルトはdocstring)

    ・mime_type
    コンテンツタイプを指定します (FastMCPはtext/plainやapplication/jsonなどのデフォルトを推測することが多いですが、テキスト以外のタイプの場合は明示的に指定する方が適切です)

    ・tags
    分類用の文字列のセット。クライアントがフィルタリングに使用する可能性があります。

    5. MCPコンテキストへのアクセス

    リソースとリソーステンプレートは、Contextオブジェクトを介してMCPの追加情報と機能にアクセスできます。これにアクセスするには、リソース関数にContext型アノテーションを付与したパラメータを追加します。

    from fastmcp import FastMCP, Context
    
    mcp = FastMCP(name="DataServer")
    
    @mcp.resource("resource://system-status")
    async def get_system_status(ctx: Context) -> dict:
        """システムステータス情報を提供"""
        return {
            "status": "operational",
            "request_id": ctx.request_id
        }
    
    @mcp.resource("resource://{name}/details")
    async def get_details(name: str, ctx: Context) -> dict:
        """特定の名前の詳細を取得"""
        return {
            "name": name,
            "accessed_at": ctx.request_id
        }

    詳しくは、Contextのドキュメントを参照してください。

    6. 非同期リソース

    サーバのブロックを回避するため、I/O操作 (データベースやネットワークからの読み取りなど) を実行するリソース関数には async def を使用します。

    import aiofiles
    from fastmcp import FastMCP
    
    mcp = FastMCP(name="DataServer")
    
    @mcp.resource("file:///app/data/important_log.txt", mime_type="text/plain")
    async def read_important_log() -> str:
        """特定のログファイルから非同期的にコンテンツを読み取る"""
        try:
            async with aiofiles.open("/app/data/important_log.txt", mode="r") as f:
                content = await f.read()
            return content
        except FileNotFoundError:
            return "ログファイルが見つかりません。"

    7. リソースクラス

    @mcp.resource は動的コンテンツに最適ですが、mcp.add_resource() と具体的な Resourceサブクラスを使用して、定義済みのリソース (静的ファイルや単純なテキストなど) を直接登録することもできます。

    from pathlib import Path
    from fastmcp import FastMCP
    from fastmcp.resources import FileResource, TextResource, DirectoryResource
    
    mcp = FastMCP(name="DataServer")
    
    # 1. 静的ファイルを直接公開
    readme_path = Path("./README.md").resolve()
    if readme_path.exists():
        # Use a file:// URI scheme
        readme_resource = FileResource(
            uri=f"file://{readme_path.as_posix()}",
            path=readme_path, # Path to the actual file
            name="README File",
            description="The project's README.",
            mime_type="text/markdown",
            tags={"documentation"}
        )
        mcp.add_resource(readme_resource)
    
    # 2. シンプルな定義済みテキストを公開
    notice_resource = TextResource(
        uri="resource://notice",
        name="Important Notice",
        text="日曜日にシステムメンテナンスが予定されています。",
        tags={"notification"}
    )
    mcp.add_resource(notice_resource)
    
    # 3. URIとは異なるカスタムキーを使用
    special_resource = TextResource(
        uri="resource://common-notice",
        name="Special Notice",
        text="これはカスタムストレージキーに関する特別な通知です。",
    )
    mcp.add_resource(special_resource, key="resource://custom-key")
    
    # 4. ディレクトリリストの公開
    data_dir_path = Path("./app_data").resolve()
    if data_dir_path.is_dir():
        data_listing_resource = DirectoryResource(
            uri="resource://data-files",
            path=data_dir_path, # ディレクトリへのパス
            name="Data Directory Listing",
            description="データディレクトリで使用可能なファイルを一覧表示します。",
            recursive=False # サブディレクトリを一覧表示するにはTrueに設定
        )
        mcp.add_resource(data_listing_resource) # ファイルのJSONリストを返す

    ・TextResource
    単純な文字列コンテンツ用

    ・BinaryResource
    生のバイト列コンテンツ用

    ・FileResource
    ローカルファイルパスからコンテンツを読み取ります。テキスト/バイナリモードと遅延読み取りに対応しています。

    ・HttpResource
    HTTP(S) URL からコンテンツを取得します(httpx が必要です)。

    ・DirectoryResource
    ローカルディレクトリ内のファイルを一覧表示します(JSON を返します)。
    (FunctionResource: @mcp.resource が使用する内部クラス)。

    コンテンツが静的であるか、ファイル/URLから直接取得される場合にこれらを使用して、専用のPython関数の必要性を回避します。

    8. カスタムリソースキー

    mcp.add_resource() を使用してリソースを直接追加する場合は、オプションでカスタムストレージキーを指定できます。

    # 標準URIをキーとしてリソースを作成
    resource = TextResource(uri="resource://data")
    mcp.add_resource(resource)  # "resource://data" を使用して保存およびアクセス
    
    # カスタムキーを使用してリソースを作成する
    special_resource = TextResource(uri="resource://special-data")
    mcp.add_resource(special_resource, key="internal://data-v2")  # "internal://data-v2"を使用して保存およびアクセス

    このパラメータは、デコレータを使用するときに URI が明示的に提供されるため、add_resource() を直接使用する場合にのみ使用可能であり、@resourceデコレータ経由は使用できません。

    9. リソーステンプレート

    リソーステンプレートを使用すると、クライアントはURIに埋め込まれたパラメータに応じてコンテンツが変化するリソースをリクエストできます。テンプレートは@mcp.resourceデコレータを使用して定義しますが、URI文字列に{parameter_name}プレースホルダを含め、対応する引数を関数シグネチャに追加します。

    リソーステンプレートは、通常のリソースとほとんどの設定オプション(名前、説明、MIMEタイプ、タグ)を共有しますが、関数パラメータにマッピングされるURIパラメータを定義する機能が追加されています。

    リソーステンプレートは、パラメータの一意のセットごとに新しいリソースを生成するため、リソースをオンデマンドで動的に作成できます。例えば、リソーステンプレート「user://profile/{name}」が登録されている場合、MCPクライアントは「user://profile/ford」または「user://profile/marvin」をリクエストすることで、これら2つのユーザープロファイルのいずれかをリソースとして取得できます。各リソースを個別に登録する必要はありません。

    2つのリソーステンプレートを定義する方法を示す完全な例を以下に示します。

    from fastmcp import FastMCP
    
    mcp = FastMCP(name="DataServer")
    
    # Template URI includes {city} placeholder
    @mcp.resource("weather://{city}/current")
    def get_weather(city: str) -> dict:
        """特定の都市の天気情報を提供"""
        # 実際の実装では、天気予報APIを呼び出す
        # ここでは例として簡略化したロジックを使用
        return {
            "city": city.capitalize(),
            "temperature": 22,
            "condition": "Sunny",
            "unit": "celsius"
        }
    
    # 複数のパラメータを持つテンプレート
    @mcp.resource("repos://{owner}/{repo}/info")
    def get_repo_info(owner: str, repo: str) -> dict:
        """GitHub リポジトリに関する情報を取得"""
        # 実際の実装では、GitHub APIを呼び出すことになる。
        return {
            "owner": owner,
            "name": repo,
            "full_name": f"{owner}/{repo}",
            "stars": 120,
            "forks": 48
        }

    ・weather://london/current → ロンドンの天気を返します
    ・weather://paris/current → パリの天気を返します
    ・repos://jlowin/fastmcp/info → jlowin/fastmcp リポジトリに関する情報を返します
    ・repos://prefecthq/prefect/info → prefecthq/prefect リポジトリに関する情報を返します

    10. ワイルドカードパラメータ

    リソーステンプレートは、複数のパスセグメントに一致するワイルドカードパラメータをサポートしています。標準パラメータ({param})は単一のパスセグメントにのみ一致し、「/」境界を越えることはありません。一方、ワイルドカードパラメータ({param*})は、スラッシュを含む複数のセグメントをキャプチャできます。ワイルドカードは、URIテンプレートの定義部分(リテラルまたは別のパラメータ)までの、後続のすべてのパスセグメントをキャプチャします。これにより、単一のURIテンプレートに複数のワイルドカードパラメータを含めることができます。

    from fastmcp import FastMCP
    
    mcp = FastMCP(name="DataServer")
    
    
    # 標準パラメータは1つのセグメントのみに一致
    @mcp.resource("files://{filename}")
    def get_file(filename: str) -> str:
        """名前でファイルを取得"""
        # files://<single-segment> のみに一致
        return f"File content for: {filename}"
    
    
    # ワイルドカードパラメータは複数のセグメントに一致できる
    @mcp.resource("path://{filepath*}")
    def get_path_content(filepath: str) -> str:
        """特定のパスにあるコンテンツを取得"""
        # path://docs/server/resources.mdx に一致
        return f"Content at path: {filepath}"
    
    
    # 標準パラメータとワイルドカードパラメータの混在
    @mcp.resource("repo://{owner}/{path*}/template.py")
    def get_template_file(owner: str, path: str) -> dict:
        """特定のリポジトリとパスからファイルを取得します
        ただし、リソースが `template.py` で終わる場合のみ"""
        # repo://jlowin/fastmcp/src/resources/template.py に一致
        return {
            "owner": owner,
            "path": path + "/template.py",
            "content": f"File at {path}/template.py in {owner}'s repository"
        }

    ワイルドカードパラメータは、次のような場合に役立ちます。

    ・ファイルパスまたは階層型データを扱う場合
    ・可変長のパスセグメントをキャプチャする必要がある API を作成する場合
    ・REST API に似た URL のようなパターンを構築する場合

    通常のパラメータと同様に、ワイルドカードパラメータは関数シグネチャ内で名前付きパラメータとして指定する必要があり、必要なすべての関数パラメータは URI テンプレートに記述する必要があることに注意してください。

    11. デフォルト値

    リソーステンプレートを作成する際、FastMCP は URI テンプレートパラメータと関数パラメータの関係について 2 つのルールを適用します。

    ・必須の関数パラメータ
    デフォルト値のないすべての関数パラメータ (必須パラメータ) は、URI テンプレートに記述する必要があります。

    ・URI パラメータ
    すべての URI テンプレートパラメータは、関数パラメータとして存在する必要があります。

    ただし、デフォルト値を持つ関数パラメータは、URIテンプレートに含める必要はありません。クライアントがリソースをリクエストすると、FastMCPは次の処理を行います。

    ・テンプレートに含まれるパラメータについては、URIからパラメータ値を抽出します。
    ・URIテンプレートに含まれない関数パラメータについては、デフォルト値を使用します。

    これにより、柔軟なAPI設計が可能になります。例えば、オプションパラメータ付きのシンプルな検索テンプレートは次のようになります。

    from fastmcp import FastMCP
    
    mcp = FastMCP(name="DataServer")
    
    @mcp.resource("search://{query}")
    def search_resources(query: str, max_results: int = 10, include_archived: bool = False) -> dict:
        """クエリ文字列に一致するリソースを検索"""
        # URIでは「クエリ」のみが必要で、他のパラメータはデフォルトを使用
        results = perform_search(query, limit=max_results, archived=include_archived)
        return {
            "query": query,
            "max_results": max_results,
            "include_archived": include_archived,
            "results": results
        }

    このテンプレートを使用すると、クライアントは search://python をリクエストでき、query="python"、max_results=10、include_archived=False で関数が呼び出されます。MCP 開発者は、より具体的なパラメータを指定して、基盤となる search_resources 関数を直接呼び出すこともできます。

    さらに強力なパターンとして、1つの関数を複数の URI テンプレートに登録し、同じデータに複数の方法でアクセスできるようにする方法があります。

    from fastmcp import FastMCP
    
    mcp = FastMCP(name="DataServer")
    
    # 異なる識別子でアクセスできるユーザー検索関数を定義
    @mcp.resource("users://email/{email}")
    @mcp.resource("users://name/{name}")
    def lookup_user(name: str | None = None, email: str | None = None) -> dict:
        """名前またはメールでユーザーを検索"""
        if email:
            return find_user_by_email(email) # 疑似コード
        elif name:
            return find_user_by_name(name) # 疑似コード
        else:
            return {"error": "ルックアップパラメータが指定されていません"}

    LLMまたはクライアントは、以下の2つの方法でユーザー情報を取得できます。

    ・users://email/alice@example.com → メールアドレス(name=None)でユーザーを検索
    ・users://name/Bob → メールアドレス(email=None)でユーザーを検索

    このスタックデコレータパターンでは、次のようになります。

    ・nameパラメータは、users://name/{name}テンプレートを使用する場合にのみ提供されます。
    ・emailパラメータは、users://email/{email}テンプレートを使用する場合にのみ提供されます。
    ・各パラメータは、URIに含まれていない場合、デフォルトでNoneになります。
    ・関数ロジックは、提供されたパラメータを処理します。

    ・テンプレートの動作

    (1) 定義
    FastMCP は、@resource URI 内の {...} プレースホルダとそれに一致する関数パラメータを検出すると、ResourceTemplate を登録します。

    (2) 検出
    クライアントは resources/listResourceTemplates 経由でテンプレートを一覧表示します。

    (3) リクエストとマッチング
    クライアントが特定の URI (例: weather://london/current) をリクエストすると、FastMCP はこれを weather://{city}/current テンプレートとマッチングします。

    (4) パラメータ抽出
    パラメータ値 (city="london") を抽出します。

    (5) 型変換と関数呼び出し
    抽出した値を関数内でヒントされた型に変換し、get_weather(city="london") を呼び出します。

    (6) デフォルト値
    URI テンプレートに含まれていないデフォルト値を持つ関数パラメータについては、FastMCP はデフォルト値を使用します。

    (7) レスポンス
    関数の戻り値はフォーマット (例: dict から JSON) され、リソースコンテンツとして返されます。

    テンプレートは、RESTのような原則に従ってパラメータ化されたデータアクセスポイントを公開する強力な方法を提供します。

    12. サーバの動作

    12-1. 重複リソース

    FastMCPサーバが同じURIに複数のリソースまたはテンプレートを登録しようとした場合の処理​​方法を設定できます。FastMCPの初期化時にon_duplicate_resources設定を使用してください。

    from fastmcp import FastMCP
    
    mcp = FastMCP(
        name="ResourceServer",
        on_duplicate_resources="error" # 重複がある場合はエラーを発生
    )
    
    @mcp.resource("data://config")
    def get_config_v1(): return {"version": 1}
    
    # "data://config" はすでに登録されており、動作は "error" であるため、
    # この登録の試行では ValueError が発生
    # @mcp.resource("data://config")
    # def get_config_v2(): return {"version": 2}

    ・warn (デフォルト)
    警告をログに記録し、新しいリソース/テンプレートが古いものを置き換えます。

    ・error
    ValueError を発生させ、重複登録を防止します。

    ・replace
    既存のリソース/テンプレートを新しいものに置き換えます。

    ・ignore
    元のリソース/テンプレートを保持し、新しい登録の試行を無視します。

    次回



     
     

    npaka

     
     
    プログラマー。iPhone / Android / Unity / ROS / AI / AR / VR / RasPi / ロボット / ガジェット。年2冊ペースで技術書を執筆。アニソン / カラオケ / ギター / 猫 twitter : @npaka123

    あなたへのおすすめ