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プロキシは次の要件を満たしたい。詳細は後述。
- URLパスで呼び出すMCPサーバを切り替えたい
- 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 を作成します。
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
をブラウザで開くと次のようなページが表示されます。
サイドバーの設定が次のようになっていることを確認し、[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
ツールが表示されるはず。
hello_world
ツールを選択し [Run Tool]ボタンをクリックする。
"Hello World" と表示されれば成功。
MCPサーバ greeting
を作成
最終的に複数の MCPサーバを使い分けたいのでもう一つ、動作確認用の MCPサーバを作成しておきます。
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-world
と greeting
を呼び出す MCP設定ファイル remote.json
を作成します。MCPサーバ greeting
に環境変数 GREET_MESSAGE
を設定します。
MCP設定ファイル
{
"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プロキシを実現できます!
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
にパスを指定します。
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 ツールが混在して表示されるはずです。
これを異なる MCPサーバに分離したいというのが1つ目の要件です。つまり [Connect] する際に hello-world
と greeting
のどちらに接続するかを選択できるようにしたい。
もう1つの要件は、リモートMCPサーバの起動時に指定した環境変数の値を選択的に MCPサーバへ引き継ぎたい。
ということで次節では、FastMCP の ASGI統合を用いてそれらの要件を実装します。
ASGI統合でリモート MCP プロキシを作成
MCP設定ファイル remote_asgi.json
を作成します。env
セクションに MCPプロキシの環境変数を参照する書式 ${環境変数名}
を導入します。
{
"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
)。
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 に設定した「こんにちは」が表示されます。
なお、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 を設定します。
{
"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
ツールを呼び出します。
以上です。
最後までお読みいただきありがとうございます。