1
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?

【2026年版】MCP SDK 2.0で壊れる5箇所まとめ。移行は3行

1
Last updated at Posted at 2026-09-02

MCP Python SDK 1.x から 2.0 への移行で壊れる箇所と直し方

pyproject.toml に mcp>=1.2.0 と書いてあるなら、今日 pip install すると 2.1.1 が入って import の時点で落ちる。

実際に直すコードは3行。デコレータの書き方は1.x のまま通る。
ただし黙って値が変わる ctx.client_id が残る。落ちないぶん、こっちのほうが厄介だった。

自分の手元にある自作MCPサーバー3本が全部 mcp>=1.2.0 の上限なしで止まっていたので、venv を2つ立てて 1.27.1 と 2.1.1 の両方で同じコードを動かし、どこがどう壊れるかを1つずつ数えた。その記録。

先行記事リスペクト — この記事を読む前に

本記事の差分: 先行記事は仕様の変更点とクライアント側の互換性を扱っている。本記事はサーバー側に寄せて、(1) 上限なしの依存指定が今日どのバージョンに解決されるかを実測し、(2) 1.27.1 と 2.1.1 に同じサーバーコードを食わせて差分行数を数え、(3) 二次情報で「消えた」と書かれている mcp.types が実際にはまだ通ることと、代わりに黙って壊れる箇所を特定した。

想定読者

  • Python で MCP サーバーを自分で書いて、pyproject.toml で誰かに配っている
  • 依存を mcp>=1.x と書いたまま、上限を付けた記憶がない
  • Claude Code や他のホストに自作サーバーを繋いで日常的に使っている

MCPサーバーを書いたことがない場合は、上の @yonaka15 の記事のほうが先。ここは移行の話だけ書く。

pyproject.toml の上限なしが、今日いちばん危ない

自作サーバー3本の依存指定を見たら、こうなっていた。

# 3本とも上限を書いていない。書いた当時は 1.2 系が最新だった
"mcp>=1.2.0",
"mcp>=1.2.0",
"mcp>=1.0.0",

この状態で新しい環境に入れ直すと何が来るか。空の venv を作って確かめた。

# 上限なしのまま、今日インストールする
python3 -m venv v3
./v3/bin/pip install "mcp>=1.2.0"
./v3/bin/python -c "import importlib.metadata as m; print(m.version('mcp'))"
# => 2.1.1

>=1.2.0 と書いてあるのに 2.1.1 が入る。バージョン範囲の指定として当然の挙動で、SDK側は何も悪くない。悪いのは上限を書かなかった過去の自分。

で、この状態で既存のサーバーを import するとこうなる。

ModuleNotFoundError: No module named 'mcp.server.fastmcp'. This is mcp 2.x,
where FastMCP was renamed to MCPServer (from mcp.server.mcpserver import MCPServer)
and other APIs changed; see the migration guide at
https://py.sdk.modelcontextprotocol.io/v2/migration/#fastmcp-renamed-to-mcpserver
or pin 'mcp<2' to keep running v1 code.

このエラーメッセージは親切な部類で、移行先のモジュール名と移行ガイドのURLと退避策が全部書いてある。SDK側は src/mcp/server/fastmcp.py をわざと残して、そこで ModuleNotFoundError を投げている。消して素の import エラーにするより、読んだ人が次に何をすればいいか分かる。

同じことは他のプロジェクトでも起きていて、jupyter-mcp-server には mcp 2.0.0 breaks imports: FastMCP renamed to MCPServer, module moved というissueが立っている。自分だけが踏んだ穴ではない。

上限を1文字足すと、止まる場所が変わる

先に対処法を出しておく。,<2 を足すだけ。

# 上限を付けて、同じことをやる
python3 -m venv v4
./v4/bin/pip install "mcp>=1.2.0,<2"
./v4/bin/python -c "import importlib.metadata as m; print(m.version('mcp'))"
# => 1.29.1

2.1.1 ではなく 1.29.1 で止まった。1.x 系の最新まで上がって、そこで踏みとどまる。この状態なら既存コードは1文字も直さずに動く。

公式のエラーメッセージが案内しているのも mcp<2 で、v1系は v1.x ブランチに残って重大なバグ修正とセキュリティパッチを受け取り続けるとリリースノートに書いてある。今すぐ2.xに行く理由がないなら、上限を足して寝るのが正しい。

要は、選択肢は2つある。

やること 変更量 結果
,<2 を足す 1文字 1.29.1 で止まる。コードはそのまま
2.xへ移行する 3行(後述) 2.1.1 で動く。新機能が使える

どちらも短い。困るのは「どちらもやっていない」状態のまま、CI が新しい環境を作った日に落ちるパターン。

venvを2つ立てて、同じコードを両方に食わせた

以降の実測は全部この構成でやった。

# macOS 15.6 (Darwin 24.6.0) / Python 3.14.5
python3 -m venv v1 && ./v1/bin/pip install "mcp==1.27.1"   # 移行前
python3 -m venv v2 && ./v2/bin/pip install "mcp==2.1.1"    # 移行後(現時点の最新)

移行元にした 1.27.1 は、自分のサーバーに実際に入っていたバージョン。2.1.1 は今日 PyPI で最新のもの。2.0.0 が 2026-07-28 の MCP 仕様改訂に対応したリリースで、そこからパッチが積まれて 2.1.1 に至っている。

食わせるサーバーは、1.x でよく見る書き方をひと通り入れた36行。下の引用では末尾の if __name__ == "__main__": mcp_app.run() を省いている。

# server_v1.py : mcp 1.x でごく普通に書いたサーバー
from mcp.server.fastmcp import FastMCP
from mcp.server.fastmcp import Context
import mcp.types as types

mcp_app = FastMCP("memo")

# サーバー側に状態を持つ。1.x のセッション前提だとこれで通ってしまう
_MEMO: dict[str, list[str]] = {}


@mcp_app.tool()
def add_memo(text: str, ctx: Context) -> str:
    """メモを1件足す"""
    key = getattr(ctx, "client_id", "anon")
    _MEMO.setdefault(key, []).append(text)
    return f"{key}: {len(_MEMO[key])}件"


@mcp_app.tool()
def list_memo(ctx: Context) -> list[str]:
    """メモを全部返す"""
    return _MEMO.get(getattr(ctx, "client_id", "anon"), [])


@mcp_app.resource("memo://all")
def all_memo() -> str:
    return "\n".join(sum(_MEMO.values(), []))

getattr でフォールバックを付けているのがポイントで、後半でここが効いてくる。

どのAPIが生きているかを、機械的に数える

手で試すと見落とすので、import できるかどうかを列挙するスクリプトを書いた。

# probe.py : どのAPIが生きているかをバージョン別に数える
import importlib

CHECKS = [
    ("mcp.server.fastmcp:FastMCP", "mcp.server.fastmcp", "FastMCP"),
    ("mcp.types(module)", "mcp.types", None),
    ("mcp_types(module)", "mcp_types", None),
    ("mcp:ClientSession", "mcp", "ClientSession"),
    ("mcp:Client", "mcp", "Client"),
    ("mcp.server:MCPServer", "mcp.server", "MCPServer"),
]

for label, mod, attr in CHECKS:
    try:
        m = importlib.import_module(mod)
        if attr:
            getattr(m, attr)
        print(f"  OK   {label}")
    except Exception as e:
        print(f"  FAIL {label}: {type(e).__name__}")

両方で走らせた結果を並べる。

API 1.27.1 2.1.1
mcp.server.fastmcp:FastMCP OK FAIL
mcp.types(モジュール) OK OK
mcp_types(モジュール) FAIL OK
mcp:ClientSession OK OK
mcp:Client FAIL OK
mcp.server:MCPServer FAIL OK
mcp.server.lowlevel:Server OK OK

落ちるのは mcp.server.fastmcp だけ。ClientSession も mcp.server.lowlevel.Server も両方で生きている。移行の入口が1本道になっているのは助かる。

mcp.types は消えていない。二次情報を信じて書き換えると無駄になる

上の表で太字にした行が、今回いちばん引っかかったところ。

リリースノートの要約系の情報を読むと「型は mcp-types という独立パッケージに分離された」と書いてある。それ自体は本当で、実体は mcp_types に移っている。ただ「だから mcp.types は消えた」と読むと間違う。2.1.1 で確かめると、まだ通る。

./v2/bin/python -c "
import mcp.types as t
print('file:', t.__file__)
print('TextContent の実体:', t.TextContent.__module__)
"
# file: .../site-packages/mcp/types/__init__.py
# TextContent の実体: mcp_types._types

mcp.types は残っていて、中身を mcp_types から再輸出している。つまり import mcp.types as types と書いてある行は、2.1.1 では直さなくていい。

正直ここは、最初 mcp_types に一括置換しようとして手が止まった。二次情報の「削除された」を鵜呑みにしていたら、動いているコードを触って壊すところだった。移行作業では、消えたと書いてあるAPIを1個ずつ import して確かめたほうが早い。将来 shim が外れる可能性はあるので、新規に書くなら mcp_types を見るほうがいい。ただ今日の移行で急いで直す対象ではない。

直すのは3行。デコレータの書き方は変わっていない

実際に移植した。差分はこれだけ。

# server_v2.py : 変わるのは冒頭のimportとクラス名だけ
from mcp.server.mcpserver import MCPServer   # was: from mcp.server.fastmcp import FastMCP
from mcp.server.mcpserver import Context     # Context も同じモジュールに移った
import mcp.types as types                    # ここは 2.1.1 でもそのまま通る

mcp_app = MCPServer("memo")                  # was: FastMCP("memo")

# ここから下は1文字も変えていない
@mcp_app.tool()
def add_memo(text: str, ctx: Context) -> str:
    """メモを1件足す"""
    ...

diff -u で数えると、コメント行を除いた実質の変更は3行。@mcp_app.tool() も @mcp_app.resource("memo://all") も docstring からの説明生成も、1.x のまま動く。

ツールがちゃんと登録されたかも確認した。

./v2/bin/python -c "
import asyncio, server_v2
ts = asyncio.run(server_v2.mcp_app.list_tools())
print('登録ツール数:', len(ts))
for t in ts: print(' -', t.name, '|', t.description)
"
# 登録ツール数: 2
#  - add_memo | メモを1件足す
#  - list_memo | メモを全部返す

デコレータAPIを据え置いたまま名前空間だけ動かした形で、移行の重さの大半は import 文の機械的な置換で消える。

ctx.client_id は消えた。しかも1.xの時点で None だった

ここが今回いちばん効いた発見。Context の公開属性をバージョン別に数えると、増えているのに1個消えている。

# ctxcheck.py : Context に client_id が生えているかを見る
names = [n for n in dir(Context) if not n.startswith("_")]
print("client_id ある?:", "client_id" in names)
print("公開属性数:", len(names))
1.27.1 2.1.1
公開属性数 44 52
client_id ある ない
増えたもの — headers / request_state / protocol_version / client_capabilities / notify_tools_changed ほか

公開属性は 44件 → 52件 に増えて、その中で client_id だけが落ちた。移行ガイドの案内は get_access_token().client_id を使えというもの。

問題は、冒頭のサーバーが getattr(ctx, "client_id", "anon") と書いてあること。属性が消えても例外にならず、黙って "anon" が返る。実際にクライアントを繋いでツールを2回叩き、メモがどのキーに入るかを見た。

# real_v2.py : 2.x では Client にサーバーを直接渡せる
async with Client(server_v2.mcp_app) as client:
    await client.call_tool("add_memo", {"text": "牛乳"})
    await client.call_tool("add_memo", {"text": "卵"})
print(server_v2._MEMO)

結果を並べる。

mcp==1.27.1 / 本物のリクエスト経由
  _MEMO のキー: [None]
  中身: {None: ['牛乳', '卵']}

mcp==2.1.1 / 本物のリクエスト経由
  _MEMO のキー: ['anon']
  中身: {'anon': ['牛乳', '卵']}

キーが None から "anon" に変わった。エラーは出ない。ログにも何も出ない。

そして、よく見ると 1.27.1 の時点でキーが None になっている。つまり client_id による利用者の切り分けは、移行で壊れたのではなく最初から動いていなかった。2.x が壊したのは「動いていた機能」ではなく「動いていないことに気づけない書き方」のほう。getattr のフォールバックが、1.x でも 2.x でも事実を隠していた。

ちなみに getattr を外して直接 ctx.client_id に触ると、2.1.1 ではツール実行時に例外になる。

UnexpectedToolError: Error executing tool boom

握り潰さずに書いてあれば、移行した瞬間に気づけた。ユーザーごとに状態を分けているつもりのコードがあるなら、移行の前にキーの実物を1回 print したほうがいい。

自作3サーバー1669行のうち、直すのは5箇所だった

正規表現で移行対象を数えるスクリプトを書いて、自作サーバー3本に当てた。

# scan.py : 2.x で壊れる箇所を数える
RULES = [
    ("mcp.server.fastmcp から import", r"from\s+mcp\.server\.fastmcp"),
    ("FastMCP( を呼んでいる", r"\bFastMCP\s*\("),
    ("ctx.client_id を読んでいる", r"\.client_id\b"),
    ("import mcp.types", r"(from\s+mcp\.types|import\s+mcp\.types)"),
    ("ClientSession( を使っている", r"\bClientSession\s*\("),
    ("RFC7523OAuthClientProvider", r"RFC7523OAuthClientProvider"),
    ("FileResource(is_binary=)", r"is_binary\s*="),
]

結果。

壊れる書き方 件数 2.x での直し方
mcp.server.fastmcp から import 2 mcp.server.mcpserver に変える
FastMCP( を呼んでいる 2 MCPServer( に変える
import mcp.types 1 2.1.1 では直さなくていい
ctx.client_id 0 該当なし
ClientSession( 0 該当なし
RFC7523OAuthClientProvider 0 ClientCredentialsOAuthProvider へ
FileResource(is_binary=) 0 encoding= へ
合計 5

対象は21ファイル・1669行。そのうち手を入れるのは5箇所で、しかも1箇所は直さなくていい mcp.types。実質の移行対象は 5件 → 4件 に減り、1669行に対する率は 0.3% を切る。

重いのは変更量じゃない。どこが壊れるか分からない時間だ。20行の正規表現を当てれば、それは終わる。

デメリットと、まだ移行しなくていい理由

ここまで移行が軽い話を書いたが、今すぐやるべきかは別。実際に引っかかりそうなところを挙げる。

  1. 上の実測は「素直に書いたサーバー」の話。 認証まわりを触っていると話が変わる。RFC7523OAuthClientProvider と JWTParameters は削除され、ClientCredentialsOAuthProvider に寄せられた。ミドルウェアも ServerMiddleware.__call__ の引数が (ctx, method, params, call_next) から (ctx, call_next) に変わっている。自作サーバーには該当が0件だったので、この2つは自分では踏んでいない。踏む人は移行ガイドと src/mcp/server/mcpserver の実装を先に読んだほうがいい。
  2. mcp.types の shim がいつ外れるか分からない。 今日通ることは確かめたが、将来の保証ではない。
  3. ホスト側の対応状況を確認していない。 2.xで書いたサーバーを繋ぐ相手が古いプロトコルしか喋らない場合の挙動は、今回の検証範囲の外。
  4. 状態を持つサーバーは、移行より先に設計の話になる。 2026-07-28 の仕様でプロトコルレベルのセッションと初期化ハンドシェイクが無くなり、ロードマップには「サーバーが状態を抱えずに水平スケールできる」と書いてある。冒頭のサンプルのようにグローバル辞書に状態を置いているサーバーは、import を直しても設計としては前提が変わっている。

3と4が残っているので、この記事は「移行は3行で終わる」までしか言えない。本番に繋いだ話はしていない。

今日/今週/今月やること

今日(5分) — 依存指定に上限があるか見る。無ければ足す。

# 手元の全プロジェクトで、上限なしの mcp 指定を洗い出す
grep -rn 'mcp>=' ~/projects/*/pyproject.toml | grep -v '<2'

出てきた行を "mcp>=1.2.0,<2" に直してコミットすれば、今日のところは終わり。

今週(30分) — 上の scan.py を自分のリポジトリに当てて、移行対象の件数を出す。件数が10行を切っているなら、venv を2つ立てて実際に移植まで通す。

python3 -m venv v2 && ./v2/bin/pip install "mcp==2.1.1"
./v2/bin/python -c "import your_server"   # エラー全文が移行先を教えてくれる

今月 — 状態を持っているサーバーがあるなら、ctx 由来のキーが実際に何を返しているか print で確認する。ついでに移行ガイドと、下の参考リンクに置いたロードマップに目を通しておくと、次の破壊的変更(エージェント認証まわり)の予告が読める。

参考リンク

1
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
1
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?