見出し画像

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
元のリソース/テンプレートを保持し、新しい登録の試行を無視します。

次回



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