0
0

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 SDKで環境変数が子に渡らない理由

0
Posted at

MCP Python SDKのstdio接続を手元で試していて、親に置いたダミーのトークンが子では消えた。同じPythonを普通のsubprocessで起動すると見えている。

MCPサーバーへ渡す環境変数は、名前を決めて明示する。自分はこの方針で組みたい。親の環境を丸ごと渡すと、使わない秘密情報まで渡してしまうんだよね。

認証先への通信やLLMの呼び出しは使わず、ローカルの子へ環境変数が届くかを切り出した。

親で見えても、stdioの子には届かない

macOS、CPython 3.12.13、mcp==1.27.1で実行した。依存はanyio==4.13.0、pydantic==2.13.4。対象はmcp.client.stdio.stdio_clientだ。

親へダミー変数を2個置く。MCP_DEMO_TOKENが必要な設定、MCP_DEMO_OTHERが不要な設定だ。実際のAPIキーは使わない。

env_probe.pyに保存し、上記の環境でpython env_probe.pyを実行する。

import asyncio
import json
import os
import subprocess
import sys

from mcp import StdioServerParameters
from mcp.client.stdio import stdio_client

os.environ["MCP_DEMO_TOKEN"] = "dummy"
os.environ["MCP_DEMO_OTHER"] = "unrelated"

child = '''import json, os
params = {"token": "MCP_DEMO_TOKEN" in os.environ,
          "other": "MCP_DEMO_OTHER" in os.environ,
          "path": "PATH" in os.environ}
print(json.dumps({"jsonrpc": "2.0", "method": "probe", "params": params}), flush=True)
'''

def show(name, values):
    print(name, " ".join(f"{k}={v}" for k, v in values.items()))

async def main():
    raw = subprocess.check_output([sys.executable, "-c", child], text=True)
    show("subprocess", json.loads(raw)["params"])
    cases = [
        ("SDK default", None),
        ("SDK empty", {}),
        ("SDK explicit", {"MCP_DEMO_TOKEN": os.environ["MCP_DEMO_TOKEN"]}),
        ("SDK all", dict(os.environ)),
    ]
    for name, env in cases:
        server = StdioServerParameters(
            command=sys.executable, args=["-c", child], env=env
        )
        async with stdio_client(server) as (read, _):
            message = await read.receive()
            if isinstance(message, Exception):
                raise message
            show(name, message.message.root.params)

asyncio.run(main())

子は環境変数の有無をJSON-RPC形式で1行返す。プロセス起動と読み取りの検証用で、MCPの初期化やツール呼び出しは省いている。トークンの値は出力しない。

手元の出力は次のとおり。

subprocess token=True other=True path=True
SDK default token=False other=False path=True
SDK empty token=False other=False path=True
SDK explicit token=True other=False path=True
SDK all token=True other=True path=True

通常のsubprocessでは2個とも届いた。SDKでenvを省略すると、どちらも届かない。

最初に気になったのは、その次のpath=Trueだった。

env={}でも空の環境にはならない

SDKのenvへ空の辞書を渡しても、PATHは残った。

インストール済みの1.27.1で、mcp/client/stdio/__init__.pyを読んだ。同バージョンのソースでは、stdio_client()がget_default_environment()の辞書へserver.envを足して、子を起動している。

このmacOS環境で既定の引き継ぎ対象に入っていた名前は、HOME、LOGNAME、PATH、SHELL、TERM、USER。親に値がある変数だけを拾い、値が()で始まるものは除く。Windows向けの一覧は別にある。

MCP_DEMO_TOKENは一覧にないので、親で設定しただけでは拾われない。env={}も、既定の辞書へ何も追加しない指定になる。今回の3通りを並べるとこうだ。

SDKに渡すenv 子へ渡す辞書の作り方
省略またはNone SDKが選んだ既定の変数
空の辞書 既定の変数をそのまま使う
名前と値を指定 既定の変数へ追加し、同名なら上書き

SDKのenvで、既定の変数を一括削除する指定はできない。標準ライブラリへ直接渡すenvと混同しやすいところだ。

コードのsys.executableは同じPythonを使うために固定した。pythonという名前だけだと、どの仮想環境を見ていたか調べる手間も増える。

全部渡せば直る。そのとき何が増えるか

SDK allでは不要な変数も届いた。dict(os.environ)で接続エラーが解消しても、渡す範囲は絞れていない。

自分なら起動設定に、必要な変数名を列挙する。SDK explicitがその形だ。実際の名前でも、親からはos.environ[名前]で読む。設定が欠けたら、子を起動する前に止まる。get()でNoneを渡して別のエラーになるより、設定漏れを追いやすい。

この制限だけでは子プロセスを隔離できない。同じOSユーザーの権限で動き、読めるファイルから別の情報を取得し得る。other=Falseは、この起動時にダミー変数を環境経由で渡さなかったことだけを示す。外部製サーバーのファイル権限や実行環境の分離は、別に決める必要がある。

確認したのはローカルの起動まで。各AIアプリの設定処理やHTTP接続、実サービスの認証は対象外だ。

MCPサーバーで設定が見つからないときは、親のシェルで見えるかに加えて、SDKが子へ渡す名前を確認する。必要な変数を1個ずつ明示すれば、今回の例ではトークンを渡しつつ、不要な変数は渡さずに済んだ。起動確認には値を出さない小さなプローブを使える。接続を直す変更と、サーバーへ渡す情報を増やす変更を、同じ行で見直せるようにしておきたい。

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?