1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

リモート MCPプロキシを Python FastMCP で簡単に実装できた

Last updated at Posted at 2025-07-09

Python の FastMCPパッケージ を用いてリモートMCPプロキシを作成するチュートリアルです。思いのほか簡単に実装できました。

システム構成

MCPクライアントは、Streamable HTTP でリモートの MCPプロキシに接続し、リモートの MCPサーバ群を呼び出します(次図)。

なお、本チュートリアルで作成するリモートの MCPプロキシは、便宜的に MCPクライアントと同一のノートPC 上に構築します。

環境

  • Windows11
  • npm 10.9.2
  • uv 0.7.15 (4ed9c5791 2025-06-25)
  • fastmcp 2.9.2

要件

MCPプロキシは次の要件を満たしたい。詳細は後述。

  1. URLパスで呼び出すMCPサーバを切り替えたい
  2. MCPサーバ毎に設定する環境変数を変更したい

準備

以下、PowerShell で作業します。

Node.js のパッケージマネージャ npm をインストールしていなければインストールする。

winget install -e --id OpenJS.NodeJS.LTS

Node.js をインストールすると npm もインストールされます。

Python のパッケージマネージャ uv をインストールしていなければインストールする。

winget install -e --id=astral-sh.uv

動作確認

npm --version

uv --version

Python プロジェクト作成

Python プロジェクト mcp-proxy を作成し、仮想環境に Python3.13 をインストールします。

# プロジェクト作成
uv init mcp-proxy

cd mcp-proxy

# 仮想環境を作成し Python3.13 をインストール
uv venv --python 3.13

# 仮想環境を有効化
.venv\Scripts\activate

# Pythonバージョン確認
python --version

仮想環境を無効化するときは deactivate

deactivate

この時点のフォルダ構成

mcp-proxy/
├── .gitignore
├── .python-version
├── .venv/
├── README.md
├── main.py
└── pyproject.toml

Pythonスクリプト main.py を実行できることを確認。

python main.py

# 又は

uv run main.py

出力結果

Hello from mcp-proxy!

仮想環境に FastMCP パッケージをインストール。

uv add fastmcp

MCPサーバ hello-world を作成

動作確認用の MCPサーバ hello-world を作成します。

hello_world.py
from fastmcp import FastMCP

mcp = FastMCP(name="Hello World")

@mcp.tool
def hello_world() -> str:
    """Hello World"""
    return "Hello World!"

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

実行してみます。

uv run hello_world.py

出力結果

[06/28/25 15:14:49] INFO  Starting MCP server 'Hello World' with transport 'stdio' 

停止するときは Ctrl+C

MCPインスペクターで動作確認

実行できることを確認したら次は、MCPインスペクター から MCPサーバを呼び出します。MCPインスペクターは、MCP サーバーのテストとデバッグを行うためのインタラクティブな開発者ツールです。

直接、MCPインスペクターを実行してもよいですが、次のように FastMCP CLI から実行することができます。

fastmcp dev hello_world.py

出力結果

Starting MCP inspector...
⚙️ Proxy server listening on 127.0.0.1:6277
🔑 Session token: XXXXXXXXXXXXXXXXXXX
Use this token to authenticate requests or set DANGEROUSLY_OMIT_AUTH=true to disable auth

🔗 Open inspector with token pre-filled:
   http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXX
   (Auto-open is disabled when authentication is enabled)

🔍 MCP Inspector is up and running at http://127.0.0.1:6274 🚀

ここで表示された URL http://localhost:6274/?MCP_PROXY_AUTH_TOKEN=XXXXXXXXXXXXXXXXXXX をブラウザで開くと次のようなページが表示されます。

image.png

サイドバーの設定が次のようになっていることを確認し、[Connect]ボタンをクリックする。

項目
Transport Type STDIO
Command uv
Arguments run --with fastmcp fastmcp run hello_world.py

ここで uv run は、FastMCP パッケージをインストールした後 FastMCP CLIの fastmcp コマンドで hello_world.py を実行する。

しばらく待つとステータスが Connected に変わり画面右側にタブが表示されるので、Toolsタブを選択し [List Tools] ボタンをクリックすると、次のように hello_worldツールが表示されるはず。

image.png

hello_worldツールを選択し [Run Tool]ボタンをクリックする。
"Hello World" と表示されれば成功。

image.png

MCPサーバ greeting を作成

最終的に複数の MCPサーバを使い分けたいのでもう一つ、動作確認用の MCPサーバを作成しておきます。

greeting.py
import os
from fastmcp import FastMCP

mcp = FastMCP(name="Greeting")

@mcp.tool
def greet(name: str) -> str:
    """Greet a user by name."""
    # 環境変数 GREET_MESSAGE を参照
    msg = os.getenv('GREET_MESSAGE', 'Hello')
    return f"{msg}, {name}!"

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

この MCPサーバは、環境変数 GREET_MESSAGE を参照します。

リモート MCP プロキシを作成

先の MCPサーバ hello-worldgreeting を呼び出す MCP設定ファイル remote.json を作成します。MCPサーバ greeting に環境変数 GREET_MESSAGE を設定します。

MCP設定ファイル

remote.json
{
  "mcpServers": {
    "hello-world": {
      "command": "uv",
      "args": [ "run", "./hello_world.py" ]
    },
    "greeting": {
      "command": "uv",
      "args": [ "run", "./greeting.py" ],
      "env": {
        "GREET_MESSAGE": "Bonjour"
      }      
    }
  }
}

MCPプロキシ remote.py を作成します。MCP設定ファイルをロードし FastMCP.as_proxy() するだけで MCPプロキシを実現できます!

remote.py
import logging
import json
import asyncio
from fastmcp import FastMCP
from fastmcp import settings

# ログ出力(省略可)
logging.basicConfig(level=logging.INFO)
logging.info("Remote MCP settings: %s", settings.model_dump_json(indent=2))

# MCP設定ファイルをロード
with open('remote.json', 'r', encoding='utf-8') as fp:
    mcp_config = json.load(fp)

# MCPプロキシ作成
mcp = FastMCP.as_proxy(mcp_config, name="MCP Proxy")

async def main():
    await mcp.run_async()

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

なお、この時点でファイル配置は次の通り。全てのファイルをプロジェクトフォルダの直下に配置しています。

mcp-proxy/
├── README.md
├── greeting.py
├── hello_world.py
├── main.py
├── pyproject.toml
├── remote.json
├── remote.py
└── uv.lock

※ 隠しファイルを除く

MCPプロキシの動作を確認します。

fastmcp dev コマンドは STDIO にしか対応していないとのことなので、fastmcp run コマンドに --transport http オプションを付けて実行します。

fastmcp run remote.py --transport http --host 127.0.0.1 --port 8000

URLパスについて
実行すると次のような内容が表示されますが、これは fastmcp.settings をログ出力したものです。

出力結果

INFO:root:Remote MCP settings: {
  "home": "C:\\Users\\<your-home-dir>\\.fastmcp",
  "test_mode": false,
  "log_level": "INFO",
  "enable_rich_tracebacks": true,
  "deprecation_warnings": true,
  "client_raise_first_exceptiongroup_error": true,
  "resource_prefix_format": "path",
  "tool_attempt_parse_json_args": false,
  "client_init_timeout": null,
  "host": "127.0.0.1",
  "port": 8000,
  "sse_path": "/sse/",
  "message_path": "/messages/",
  "streamable_http_path": "/mcp/",
  "debug": false,
  "mask_error_details": false,
  "server_dependencies": [],
  "json_response": false,
  "stateless_http": false,
  "default_auth_provider": null,
  "include_tags": null,
  "exclude_tags": null
}

ここで streamable_http_path は Streamable HTTP で接続する際のデフォルトの URLパスです。

fastmcp.settings の各項目は、接頭句 FASTMCP_ を付けた大文字の環境変数で変更できます。例えば、URLパスを変更したければ環境変数 FASTMCP_STREAMABLE_HTTP_PATH を設定します。

$env:FASTMCP_STREAMABLE_HTTP_PATH='/foo/'

もしくは FastMCP.as_proxy() の引数 streamable_http_path にパスを指定します。

remote.py
mcp = FastMCP.as_proxy(mcp_config, name="MCP Proxy", streamable_http_path='/bar')

引数の詳細は FastMCP のコードを参照。
https://github.com/jlowin/fastmcp/blob/v2.9.2/src/fastmcp/server/server.py#L153

MCPインスペクターで動作確認

MCPインスペクターを実行しブラウザで開きます。

npx @modelcontextprotocol/inspector

MCPインスペクターで [Connect] する際の設定。

項目
Transport Type Streamable HTTP
URL http://localhost:8000/mcp/

接続したら Toolsタブを選択し [List Tools] ボタンをクリックします。
次のように hello-world ツールと greeting ツールが混在して表示されるはずです。

image.png

これを異なる MCPサーバに分離したいというのが1つ目の要件です。つまり [Connect] する際に hello-worldgreeting のどちらに接続するかを選択できるようにしたい。

もう1つの要件は、リモートMCPサーバの起動時に指定した環境変数の値を選択的に MCPサーバへ引き継ぎたい。

ということで次節では、FastMCP の ASGI統合を用いてそれらの要件を実装します。

ASGI統合でリモート MCP プロキシを作成

MCP設定ファイル remote_asgi.json を作成します。env セクションに MCPプロキシの環境変数を参照する書式 ${環境変数名} を導入します。

remote_asgi.json
{
  "mcpServers": {
    "hello-world": {
      "command": "uv",
      "args": [ "run", "./hello_world.py" ]
    },
    "greeting": {
      "command": "uv",
      "args": [ "run", "./greeting.py" ],
      "env": {
        "GREET_MESSAGE": "${GREET_MESSAGE}"
      }
    }
  }
}

MCPプロキシ remote_asgi.py を作成します。FastMCP の ASGI統合を用いて各MCPサーバをそれぞれ異なる Starletteアプリとして作成し、それらのアプリを URLパス /servers/<MCPサーバ名>/mcp にマウントします(create_route)。また、env セクションに ${変数名} の値があればそれを環境変数の値に置換します(replace_envvars)。

remote_asgi.py
import os
import re
import json
from contextlib import asynccontextmanager, AsyncExitStack
from fastmcp import FastMCP
from starlette.applications import Starlette
from starlette.routing import Mount
import uvicorn

# MCP設定ファイルをロード
with open('remote_asgi.json', 'r', encoding='utf-8') as fp:
    mcp_config = json.load(fp)

def extract_varname(s: str) -> str | None:
    """文字列 s が "${XXX}" 形式なら変数名 XXX を返す. それ以外は None を返す."""
    match = re.search(r'^\$\{(.*?)\}$', s)
    if match:
        return match.group(1)
    return None

def replace_envvars(envvars: dict) -> dict:
    """MCP設定 env セクションの変数を環境変数の値で置換"""
    for k, v in envvars.items():
        if isinstance(v, str) and (name := extract_varname(v)):
            envvars[k] = os.getenv(name, '')
    return envvars

def create_route(name: str, config: dict):
    """各MCPツールへの Starlette ルートを作成
    ルートのパス形式 'http://localhost/servers/<ServerName>/mcp'
    """
    # 環境変数を置換
    if 'env' in config:
        config['env'] = replace_envvars(config['env'])

    mcp_servers = {"mcpServers": {name: config}}

    # MCPサーバ作成
    mcp = FastMCP.as_proxy(mcp_servers, name=name)

    # MCPサーバのASGIアプリを作成
    mcp_app = mcp.http_app(path='/mcp', stateless_http=False)

    # ASGIアプリをマウント
    return Mount(f"/servers/{name}", app=mcp_app)

# MCPサーバ毎に異なるルートにマウント
routes = [
    create_route(k, v)
    for k, v in mcp_config.get('mcpServers', {}).items()
]

# ライフスパン. 欄外の補足を参照
@asynccontextmanager
async def mcp_apps_lifespan(app: Starlette):
    async with AsyncExitStack() as stack:
        sessions = [
            await stack.enter_async_context(route.app.lifespan(route.app))
            for route in routes
        ]
        yield

app = Starlette(routes=routes, lifespan=mcp_apps_lifespan)

if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

補足:ライフスパン

Starlette アプリケーションでは、アプリケーションの起動前、またはアプリケーションのシャットダウン時に実行する必要があるコードを処理するためのライフスパン ハンドラーを登録できます。
https://www.starlette.io/lifespan/

Streamable HTTPトランスポートでは、ネストされたライフスパンが認識されないため、FastMCPアプリからStarletteアプリにライフスパンコンテキストを渡す必要があります。そうしないと、FastMCPサーバーのセッションマネージャーが正しく初期化されません。

環境変数 GREET_MESSAGE に値を設定し、MCPプロキシを起動します。ここでは ASGIサーバに uvicorn を用いていることに注意して下さい。このため FastMCP CLI から起動することはできません。

# 環境変数を設定
$env:GREET_MESSAGE="こんにちは"

# MCPプロキシを起動
uv run remote_asgi.py

# または
uvicorn remote_asgi:app --host 0.0.0.0 --port 8000

MCPインスペクターから MCPプロキシに接続します。

MCPサーバ hello-world を使用するときは次のように URL を指定します。

項目
Transport Type Streamable HTTP
URL http://localhost:8000/servers/hello-world/mcp/

MCPサーバ greeting を使用するときは次のように URL を指定します。

項目
Transport Type Streamable HTTP
URL http://localhost:8000/servers/greeting/mcp/

MCPサーバ greeting のツールを実行すると、環境変数 GREET_MESSAGE に設定した「こんにちは」が表示されます。
image.png

なお、uvicorn のワーカー数を複数にすると使用する MCPクライアントによっては動作しないことがあります。

Claude for Desktop で動作確認

本執筆時点で私の知る限りでは、Claude for Desktop はプライベートな MCPサーバに Streamable HTTP で接続できません。この問題を回避するためにローカル MCPプロキシを介してリモート MCPプロキシに接続します。ローカル MCPプロキシには FastMCP CLI を使用します。

FastMCP CLI は、fastmcp run コマンドの引数に http://server-url/path または https://server-url/path を指定するとリモートサーバーに接続してプロキシを作成します。

Claude for Desktop に MCP を設定します。

claude_desktop_config.json
{
  "mcpServers": {
    "hello-world": {
      "command": "uvx",
      "args": [
        "fastmcp",
        "run",
        "http://localhost:8000/servers/hello-world/mcp/"
      ]
    },
    "greeting": {
      "command": "uvx",
      "args": [
        "fastmcp",
        "run",
        "http://localhost:8000/servers/greeting/mcp/"
      ]
    }
  }
}

greeting ツールを呼び出します。

image.png

以上です。
最後までお読みいただきありがとうございます。

1
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?