FastMCP 3.xでPython MCPサーバーを30分で作る — @toolデコレーターひとつで十分だ

FastMCP 3.xでPython MCPサーバーを30分で作る — @toolデコレーターひとつで十分だ

FastMCP 3.2.4を実際にインストールして、@mcp.tool()・@mcp.resource()・@mcp.prompt()デコレーターで動くMCPサーバーを構築した。Claude DesktopとCursorが呼び出せるAIツールサーバーをPython30行で実装する実践ガイド。

MCP(Model Context Protocol)サーバーをゼロから実装しようとすると、思ったより手間がかかる。stdioトランスポートを処理し、JSON-RPC 2.0をシリアライズし、ハンドラーをひとつずつ登録する。Streamable HTTPでMCPサーバーを直接実装する過程を辿ってみると、「AIツールをひとつ追加したいだけなのに」なぜこんなにボイラープレートが必要なのかと嫌になる瞬間が来る。

FastMCPはその苦しさを解消するために作られたフレームワークだ。今日、サンドボックスでpipでインストールして、実際に動くMCPサーバーを30分以内に立ち上げた。

この記事はMCPプロトコル自体ではなく、FastMCPというツールを扱う。プロトコルの背景が気になるならModel Context Protocol公式サイトを、FastMCPのソースと変更履歴はjlowin/fastmcp GitHubリポジトリを一緒に開いておくと理解が早い。なお、同じMCPをTypeScriptで作る流れはMCPサーバーTypeScript SDKステップバイステップガイドに別途まとめてある。

FastMCPとは何かを先に確認した

FastMCPはMCP Python SDKの上に乗る高レベルレイヤーだ。Express.jsがNodeのhttpモジュールをラップしたような構造だ。公式説明は「The fast, Pythonic way to build MCP servers and clients」で、実際に使ってみるとその通りだと感じた。

バージョンを確認したら、予想よりずっと先に進んでいた。

$ fastmcp version

FastMCP version:   3.2.4
MCP version:       1.27.0
Python version:    3.12.8
Platform:          macOS-15.6-arm64

バックログには「v2.0」基準で書いていたが、すでに3.xまで上がっていた。MCPプロトコル自体も1.27.0だ。このバージョン番号が示すことがひとつある。FastMCPはかなり活発に開発されている。

正直に言えば、3.xになってAPIがどれほど変わったか直接確認する必要があった。ドキュメントだけを見るより実際に動かしてみるのが早い。

インストールと最初のサーバー: 本当にこれだけだ

pip install fastmcp

インストールは10秒で終わった。実際に動くサーバーを作ってみよう。私がサンドボックスで最初に作ったのは、天気関連ツール2つのサーバーだった。

from fastmcp import FastMCP
from datetime import datetime

mcp = FastMCP("weather-tools", version="1.0.0")

@mcp.tool()
def get_current_time(timezone: str = "UTC") -> str:
    """現在時刻を返します。"""
    return f"現在時刻 ({timezone}): {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"

@mcp.tool()
def calculate_temp(celsius: float) -> dict:
    """摂氏を華氏とケルビンに変換します。"""
    return {
        "celsius": celsius,
        "fahrenheit": round(celsius * 9/5 + 32, 2),
        "kelvin": round(celsius + 273.15, 2)
    }

@mcp.resource("data://server-info")
def server_info() -> str:
    """サーバー情報を返します。"""
    return "FastMCP 3.x 天気サーバー"

@mcp.prompt()
def weather_analysis(location: str) -> str:
    """天気分析プロンプトテンプレート"""
    return f"{location}の天気を分析して、服装のおすすめをしてください。"

if __name__ == "__main__":
    mcp.run()  # stdioモードで実行

これだけだ。Python関数にデコレーターをひとつ付けるとMCPツールになる。型ヒントが自動的にJSON Schemaに変換されてClaudeに渡される。

fastmcp inspectでサーバー構成を確認できる。

$ fastmcp inspect server.py

Server
  Name:         weather-tools
  Version:      1.0.0
  Generation:   2

Components
  Tools:        2
  Prompts:      1
  Resources:    1
  Templates:    0

3つのビルディングブロック: Tool、Resource、Prompt

FastMCPの核心概念は3つだ。これを区別することがサーバーをうまく設計する第一歩だ。

@mcp.tool(): Claudeが直接呼び出す関数だ。パラメーターを受け取って作業を実行し、結果を返す。検索、計算、ファイル操作、API呼び出しなどがここに入る。Claudeに私のファイルシステムやAPIを直接扱わせたいときに@mcp.tool()を使う。

@mcp.resource(): 読み取り専用データソースだ。data://file://https://のようなURIで登録すると、Claudeがコンテキストとして読み込む。ツールと違って「実行する」のではなく「読む」概念だ。データベーススキーマ、設定ファイル、ドキュメントなどをここに入れるとClaudeのコンテキストウィンドウに入る。

@mcp.prompt(): 再利用可能なプロンプトテンプレートだ。パラメーターを受け取って構成されたプロンプトメッセージを返す。Claude Desktopやclaude.aiでスラッシュコマンドのように使える。

MCPを初めて使う開発者がよく混乱するのがToolとResourceの違いだ。私の基準はシンプルだ。副作用(side effect)があればTool、なければResourceだ。

ContextでクライアントにProgress情報を送る

ツールが長い処理をするとき、進捗状況をリアルタイムで送れる。Contextパラメーターを追加するとFastMCPが自動で注入してくれる。

from fastmcp import FastMCP, Context

mcp = FastMCP("dev-tools")

@mcp.tool()
async def list_files(directory: str, ctx: Context) -> list[str]:
    """指定ディレクトリのファイル一覧を返します。"""
    import os
    await ctx.info(f"ディレクトリを読み込み中: {directory}")  # クライアントにログ送信
    
    try:
        files = os.listdir(directory)
        await ctx.report_progress(100, 100, "complete")  # 進捗報告
        return sorted(files)
    except FileNotFoundError:
        raise ValueError(f"ディレクトリが見つかりません: {directory}")

サンドボックスで実際に実行してみたら、ctx.info()がクライアント側にリアルタイムログを送信することを確認した。

INFO  Received INFO from server: {'msg': 'ディレクトリを読み込み中: /tmp', 'extra': None}

Claude Desktopで動作すると、ユーザーはツールがどの段階を実行しているかリアルタイムで確認できる。UX的にかなり重要な機能だ。

FastMCP Clientでテストする

実際のClaude Desktopなしにサーバーをテストできる。FastMCPはin-processクライアントを提供している。MCPエージェントワークフローパターンを実装するときもこの方式でテストがシンプルになる。

import asyncio
from fastmcp import FastMCP
from fastmcp.client import Client

mcp = FastMCP("dev-tools")

@mcp.tool()
def search_text(text: str, pattern: str) -> dict:
    """テキストからパターンを検索します。"""
    import re
    matches = re.findall(pattern, text)
    return {"pattern": pattern, "matches": matches, "count": len(matches)}

@mcp.tool()
def word_count(text: str) -> dict:
    """単語数・文字数・行数を返します。"""
    words = text.split()
    return {
        "words": len(words),
        "characters": len(text),
        "lines": len(text.splitlines())
    }

async def test():
    async with Client(mcp) as client:
        tools = await client.list_tools()
        print(f"登録されたツール {len(tools)}個:")
        for t in tools:
            print(f"  [{t.name}] {t.description}")
        
        result = await client.call_tool("search_text", {
            "text": "FastMCP is fast. FastMCP is easy.",
            "pattern": "FastMCP"
        })
        print(f"\nsearch_text結果: {result.data}")
        # → {'pattern': 'FastMCP', 'matches': ['FastMCP', 'FastMCP'], 'count': 2}
        
        result2 = await client.call_tool("word_count", {
            "text": "Hello World from FastMCP 3.x"
        })
        print(f"word_count結果: {result2.data}")
        # → {'words': 5, 'characters': 27, 'lines': 1}

asyncio.run(test())

result.dataで構造化された戻り値をそのまま使える。実際に動かしてみてエラーなく正常に動作した。

FastMCP CLI実行結果 — fastmcp version、inspect、ツール呼び出しテスト

HTTPサーバーとしてリモートデプロイする

ローカルstdioモード以外に、HTTPサーバーとして立ち上げることができる。CursorやリモートからMCPサーバーを共有したいときに使う。

# HTTPモードで実行(デフォルトポート8000)
if __name__ == "__main__":
    mcp.run(transport="http", host="0.0.0.0", port=8000)
# またはuvicornで直接実行
uvicorn server:mcp.http_app() --host 0.0.0.0 --port 8000

FastMCPのHTTPアプリはStarlette基盤だ。型を確認してみるとStarletteWithLifespanだった。つまり、FastAPIやStarletteアプリにマウントすることも可能だという意味だ。

# FastAPIとの統合
from fastapi import FastAPI
from fastmcp import FastMCP

app = FastAPI()
mcp = FastMCP("my-tools")

@mcp.tool()
def my_tool() -> str:
    return "result"

# FastAPIアプリにMCPサーバーをマウント
app.mount("/mcp", mcp.http_app())

Claude DesktopからHTTPサーバーに接続する設定はシンプルだ。

{
  "mcpServers": {
    "my-tools": {
      "url": "http://localhost:8000/mcp/"
    }
  }
}

fastmcp CLI: 開発ワークフローで役立つコマンド群

FastMCPはCLIも提供している。最初は知らなかったがfastmcp --helpを実行してみると、かなり充実していた。

Commands:
  inspect      — サーバーコンポーネントの要約出力
  list         — 登録されたツール一覧
  call         — ツールを直接呼び出す(デバッグ用)
  install      — Claude Desktop / Cursorに自動登録
  dev          — 開発サーバー起動(hot reload)
  discover     — エディターに設定されたMCPサーバーの探索
  run          — サーバー実行

fastmcp install server.py --client claudeを実行するとClaude Desktop configを自動修正してくれるそうだ。JSONファイルを手動で編集する手間がなくなる。ただし今の環境にはClaude Desktopが入っていないので、直接テストできなかった。--clientオプションがどのパスを変更するかは公式ドキュメントで確認しておくと安全だ。

正直、fastmcp devの方が実用的に見える。コードを修正するたびにサーバーを再起動する手間をなくしてくれる。

型ヒントがそのままAPIスキーマになる

FastMCPで最も印象に残った機能は、型ヒントからJSON Schemaへの自動変換だ。直接実装する場合は各ツールに手動でinputSchemaを書く必要がある。FastMCPはそれをPythonの型システムに委ねる。

from typing import Literal
from pydantic import BaseModel

class FileFilter(BaseModel):
    extension: str
    min_size_kb: int = 0
    exclude_hidden: bool = True

@mcp.tool()
def list_files_advanced(
    directory: str,
    filter: FileFilter | None = None,
    sort_by: Literal["name", "size", "modified"] = "name",
    limit: int = 50
) -> list[dict]:
    """フィルターとソートオプションでファイル一覧を返します。"""
    import os
    files = []
    for f in os.scandir(directory):
        if filter and filter.exclude_hidden and f.name.startswith("."):
            continue
        if filter and not f.name.endswith(f".{filter.extension}"):
            continue
        info = f.stat()
        size_kb = info.st_size / 1024
        if filter and size_kb < filter.min_size_kb:
            continue
        files.append({"name": f.name, "size_kb": round(size_kb, 2), "modified": info.st_mtime})
    key_map = {"name": "name", "size": "size_kb", "modified": "modified"}
    files.sort(key=lambda x: x[key_map[sort_by]])
    return files[:limit]

この関数を@mcp.tool()で登録すると、ClaudeはFileFilterの構造、sort_byの有効な値(name/size/modified)、limitのデフォルト値を自動的に把握できる。Pydanticモデルもサポートされるので複雑な入力構造もそのまま使える。

ドキュメント文字列は自動的にツールの説明として使われる。よく書かれたdocstringひとつがClaudeに送る使用マニュアルになる。

実践例:コード分析MCPサーバー

実際に使えるものを作ってみた。PythonファイルをASTで解析する開発ツールMCPサーバーだ。

from fastmcp import FastMCP, Context
import ast
import os

mcp = FastMCP("code-analyzer", version="1.0.0")

@mcp.tool()
async def analyze_python_file(filepath: str, ctx: Context) -> dict:
    """PythonファイルをASTで解析し、関数とクラスの一覧を返します。"""
    await ctx.info(f"解析中: {filepath}")
    if not os.path.exists(filepath):
        raise ValueError(f"ファイルが見つかりません: {filepath}")
    with open(filepath, "r", encoding="utf-8") as f:
        source = f.read()
    tree = ast.parse(source)
    functions, classes = [], []
    for node in ast.walk(tree):
        if isinstance(node, ast.FunctionDef):
            functions.append({
                "name": node.name, "line": node.lineno,
                "args": [a.arg for a in node.args.args],
                "docstring": ast.get_docstring(node)
            })
        elif isinstance(node, ast.ClassDef):
            classes.append({"name": node.name, "line": node.lineno})
    await ctx.report_progress(100, 100, "complete")
    return {"total_lines": source.count("\n") + 1, "functions": functions, "classes": classes}

@mcp.tool()
def count_todo_comments(filepath: str) -> dict:
    """ファイルのTODO/FIXME/HACKコメントを検索して返します。"""
    markers = ["TODO", "FIXME", "HACK", "XXX"]
    results = {m: [] for m in markers}
    with open(filepath, "r", encoding="utf-8") as f:
        for i, line in enumerate(f, 1):
            for marker in markers:
                if f"# {marker}" in line:
                    results[marker].append({"line": i, "text": line.strip()})
    return {k: v for k, v in results.items() if v}

@mcp.resource("data://project-structure")
def project_structure() -> str:
    """現在のディレクトリのPythonファイル一覧を返します。"""
    py_files = []
    for root, dirs, files in os.walk("."):
        dirs[:] = [d for d in dirs if not d.startswith(".")]
        for f in files:
            if f.endswith(".py"):
                py_files.append(os.path.join(root, f))
    return "\n".join(py_files[:50])

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

このサーバーをClaude Desktopに接続すると、「このファイルのクラス一覧を見せて」や「TODOコメントはいくつある?」を自然言語で聞ける。ユーザー側はPythonを一行も書かなくていい。それがMCPツールサーバーの要点だ。

既存のMCPサーバーと何が違うか

Streamable HTTP方式でMCPサーバーを直接実装したときと比較すると差は明確だ。

直接実装の場合:

  • Serverインスタンス作成
  • @server.list_tools() / @server.call_tool() を別々に登録
  • 入力パラメーターを自分でパース
  • anyio.run() + stdio_server() の組み合わせで実行

FastMCPの場合:

  • FastMCPインスタンスひとつ
  • @mcp.tool()で関数をそのままツールとして登録
  • 型ヒントからJSON Schemaが自動生成
  • mcp.run()一行

コード行数が半分以下になるのは副次的な問題だ。核心はビジネスロジックに集中できるという点だ。トランスポートレイヤーがどう動くか気にしなくていい。

ただし感じた限界もある。FastMCPは自由度をトレードオフとして支払う。低レベルMCPメッセージをカスタマイズしたり、非標準のトランスポートが必要な状況では、FastMCPが抽象化で隠したものを再び取り出す必要がある。そういった場合はMCP Python SDKを直接使うのが正しい。

実行可能性の判断: いつFastMCPを選ぶか

私の結論はこうだ:

FastMCPを使うとき: Claude、Cursor、VS Codeなど標準MCPクライアントと連携するサーバーを作るとき。特にチーム内のAIツールを素早くプロトタイピングしたり、既存のPython関数をMCPサーバーとして公開したいとき。

直接SDKを使うとき: カスタムトランスポート、非標準メッセージフォーマット、またはFastMCPがサポートしていないMCP機能が必要なとき。MCP Code Executionの実践事例のように低レベル制御が必要な場合だ。

FastMCPで惜しい点がひとつある。3.xになって文書がコード変化に完全についていけていない。get_tools()のようなメソッドが文書には存在するように見えるが実際にはlist_tools()に変わっていた。公式docsよりソースコードやdir(mcp)で直接確認する習慣が必要だ。

本番環境に上げる前にもうひとつ — MCP Gatewayでエージェントトラフィックを制御する方法も合わせて検討することを勧める。サーバーを公開したとき、どのツールがどう呼び出されるか制御するレイヤーが必要になる瞬間が来る。

いつ使い、いつ避けるべきか

ツールを勧める記事は多いが、「いつ使うべきでないか」を正直に書いた記事は少ない。実際に動かして整理した基準は次のとおりだ。

FastMCPが向いている場合

  • Claude Desktop、Cursor、VS Codeのような標準MCPクライアントに繋ぐサーバーを素早く作るとき。
  • 既存のPython関数をそのままAIツールとして公開したいとき。デコレーターを付けるだけで済む。
  • チーム内のプロトタイプのように「まず動くこと」が優先で、トランスポートの詳細に時間を使いたくないとき。
  • 入力構造が複雑でinputSchemaを手書きしたくないとき。型ヒントとPydanticが代わりにやってくれる。

FastMCPを避けたほうがいい場合

  • 低レベルのMCPメッセージを直接いじる必要があったり、非標準トランスポートが必要なとき。この場合は抽象化がむしろ邪魔になる。MCP Python SDKを直接使うほうが正しい。
  • Python以外のランタイムが主力のとき。Node/TypeScript環境ならMCPサーバーTypeScript SDKステップバイステップガイドのほうが自然だ。
  • サーバーを外部に公開せず、完全にローカル・プライベートで動かしたいとき。モデルまでローカルに束ねる構成はGemma 3とFastMCPで作るプライベートMCPサーバーで扱った。
  • フレームワークの抽象化の中で起きる挙動を100%追跡しなければならない規制・監査環境。こういう場合は依存を薄く保つほうが安全だ。

一行でまとめると、標準クライアントと素早く連携するならFastMCP、プロトコルの底を直接触るならSDKだ。

30分で動くサーバー、その次に考えること

FastMCP 3.xは、Python開発者がMCPサーバーを最速で作る方法だ。pip install fastmcp一行、@mcp.tool()デコレーターひとつ、mcp.run()一行。30分以内にClaude Desktopが呼び出すAIツールを作れる。

MCP生態系が急速に成熟している。私が使うMCPサーバーリストを見ると、すでに多様な統合が存在する。自分で作る前に既存のものを使うのが実用的だが、なければFastMCPで直接作るのはもうそれほど難しくない。

今日のサンドボックスで確認したバージョンはFastMCP 3.2.4、MCP 1.27.0だ。この分野が急変しているので、実際の適用前にFastMCP公式ドキュメントで最新APIを確認してほしい。

よくある質問

FastMCPでMCPサーバーはどう作りますか?
pip install fastmcpでインストールし、FastMCPインスタンスを作ってPython関数に@mcp.tool()デコレーターを付けるだけです。最後にmcp.run()を一行呼べばstdioモードでサーバーが起動します。記事の例のように30行以内で動くサーバーを作れます。
@mcp.toolと@mcp.resourceの違いは何ですか?
@mcp.tool()はClaudeが直接呼び出して検索・計算・ファイル操作などを実行する関数です。@mcp.resource()はdata://などのURIで登録する読み取り専用データソースで、Claudeがコンテキストとして読み込みます。記事の基準はシンプルで、副作用があればTool、なければResourceです。
型ヒントはなぜ重要なのですか?
FastMCPは関数の型ヒントを自動的にJSON Schemaに変換してClaudeに渡します。PydanticモデルやLiteral型もサポートするため、複雑な入力構造のinputSchemaを手で書く必要がありません。docstringはツールの説明として自動的に使われます。
いつFastMCPではなくMCP Python SDKを直接使うべきですか?
Claude、Cursor、VS Codeなど標準MCPクライアントと連携したり、既存のPython関数を素早く公開する場合はFastMCPが適しています。低レベルMCPメッセージのカスタマイズや非標準トランスポートが必要な場合は、抽象化の下を扱い直すことになるためMCP Python SDKを直接使うのが正しいです。

他の言語で読む

この記事は役に立ちましたか?

より良いコンテンツを作成するための力になります。コーヒー一杯で応援してください。

著者について

jw

Kim Jangwook

AI/LLM専門フルスタック開発者

10年以上のWeb開発経験を活かし、AIエージェントシステム、LLMアプリケーション、自動化ソリューションを構築しています。Claude Code、MCP、RAGシステムの実践的な知見を共有します。

ブログリストへ