---
title: "MCP Python SDK v2 移行：FastMCPからMCPServerへの書き換え手順と据え置き判断"
url: "https://www.issoh.co.jp/tech/details/18273/"
published: 2026-10-11
updated: 2026-10-11
categories: ["AI"]
publisher: "株式会社一創"
---

# MCP Python SDK v2 移行：FastMCPからMCPServerへの書き換え手順と据え置き判断

MCP Python SDK v2は、PyPIのmcpパッケージ2.x系です。2026年7月28日に2.0.0が公開され、10月11日時点の最新は2.3.0です。v2では`FastMCP`が`MCPServer`へ改名され、依存ライブラリ、トランスポート設定、型の属性名、テストの既定動作まで変わりました。この記事では、v1からの移行で手を入れる箇所を作業順に並べ、黙って壊れる変更と、mcp<2で据え置くほうがよい条件を整理します。MCPそのものの仕組みは[MCP（Model Context Protocol）の標準規格としての役割](https://www.issoh.co.jp/tech/details/5736/)で確認できます。

## まとめ｜v2移行の本体はimportより依存とバックチャネルの見直し

importの書き換えは数分で終わります。時間を取られるのは、httpxからhttpx2への置き換え、コンストラクタの位置引数、Streamable HTTPの4MiB上限、そしてサーバーからクライアントへの問いかけが2026-07-28仕様の接続で使えなくなる点です。最後の点はテストの既定動作も変えるため、CIが落ちて初めて気づく形になりがちです。

v1は保守モードに入りましたが、1.30.0（2026年9月7日）まで修正版が出ています。新仕様のクライアントへの対応や水平スケールの要件がなければ、mcp<2で固定して計画的に移る判断で問題ありません。逆に、`pip install mcp`を上限なしで書いている環境は、次のビルドでv2が入って壊れます。まず上限の有無を確認してください。

## v2.0.0の公開時点とv1保守モードの範囲を確かめる版番号と依存条件

移行計画は、どの版がいつ出たかと、v1がいつまで直るかの把握から始めます。

### PyPIの履歴で見るv2.0.0の公開日とv1.30.0までの保守系列

[PyPIのmcpプロジェクト](https://pypi.org/project/mcp/)のリリース履歴を2026年10月11日に確認した結果です。v2系とv1系が並行して出ています。

| 系列     | 版                | 公開日         |
| ------ | ---------------- | ----------- |
| v2 試験版 | 2.0.0a1〜2.0.0rc1 | 6月11日〜7月27日 |
| v2 正式版 | 2.0.0            | 7月28日       |
| v2 更新  | 2.1.0／2.2.0      | 8月24日／9月7日  |
| v2 最新  | 2.3.0            | 10月2日       |
| v1 保守  | 1.29.0／1.30.0    | 7月28日／9月7日  |

[v2.0.0のリリースノート](https://github.com/modelcontextprotocol/python-sdk/releases/tag/v2.0.0)は、v1.xを保守モードとし、今後はセキュリティ修正のみと明記しています。新機能はv2にしか入りません。2.3.0の要求Pythonは3.10以上です。

### pydantic 2.12・httpx2・mcp-typesに変わった依存関係の差分

[公式の移行ガイド](https://py.sdk.modelcontextprotocol.io/v2/migration/)が挙げる依存の変化のうち、解決エラーや挙動差につながるものを抜き出しました。

| 依存            | v1（1.28.1）      | v2                  |
| ------------- | --------------- | ------------------- |
| pydantic      | 2.11以上3未満       | 2.12以上              |
| HTTPクライアント    | httpx・httpx-sse | httpx2              |
| sse-starlette | 1.6.1以上         | 3.0.0以上             |
| 型定義           | mcp本体に同梱        | mcp-types（同版固定）     |
| OpenTelemetry | なし              | opentelemetry-api必須 |
| wsエクストラ       | websockets      | 削除                  |

一番効くのはhttpx2です。APIは互換ですが別パッケージなので、`except httpx.ConnectError:`のような例外処理は一致しなくなり、エラーで落ちずに素通りします。`httpx.AsyncClient(auth=provider)`のように旧httpxのクライアントへSDKの認証プロバイダを渡す書き方は、`TypeError`で止まります。grepで`httpx.`を洗い出し、SDKに渡す箇所から先に置き換えてください。

### v1に留まる場合の上限指定とpip install mcpの既定挙動

上限なしの`mcp`指定は、いまインストールすると2.x系を引きます。移行を後回しにするなら、上限を入れることが最初の作業です。

```
# pyproject.toml（v1に留まる場合）
[project]
dependencies = [
    "mcp>=1.28,<2",
]

# pyproject.toml（v2へ移る場合・移行ガイドの推奨）
[project]
dependencies = [
    "mcp>=2,<3",
    "sse-starlette>=3",
]
```

v1のドキュメントは公式サイトの`/v1/`配下に残っています。

## FastMCPからMCPServerへ書き換えるサーバー側コードの移行手順

v1の高水準API（FastMCP）で書いたサーバーは、デコレータの書き方がそのまま使えます。直すのは入口と設定の渡し方です。

### importパスとコンストラクタ引数順を直すBefore／Afterのコード例

旧パス`mcp.server.fastmcp`は非推奨ではなく削除済みで、`ModuleNotFoundError`になります。気づきにくいのはコンストラクタです。v2の位置引数はname、title、description、instructions、website\_url、icons、versionの順に変わりました。v1で`FastMCP("Demo", "説明文")`と書いていた2番目の値は、エラーにならずにtitleへ入ります。name以外はキーワード引数で渡します。

```
# v1（mcp 1.x）
from mcp.server.fastmcp import FastMCP, Context

mcp = FastMCP("inventory", "在庫数を返すサーバー", port=9000)

@mcp.tool()
async def stock(sku: str, ctx: Context) -> str:
    await ctx.info(f"lookup {sku}")
    return "12"

mcp.run(transport="streamable-http")

# v2（mcp 2.x）
from mcp.server.mcpserver import MCPServer, Context

mcp = MCPServer("inventory", instructions="在庫数を返すサーバー", version="1.4.0")

@mcp.tool()
async def stock(sku: str, ctx: Context) -> str:
    await ctx.info(f"lookup {sku}")
    return "12"

# ポートなどのトランスポート設定は run() へ渡す
mcp.run(transport="streamable-http", host="127.0.0.1", port=9000)
```

versionを省くと、v2では空文字になります。v1はSDKの版が自動で入っていたため、server\_infoを監視やログで使っているならversionを明示します。既定のサーバー名も”FastMCP”から”mcp-server”へ変わりました。`ctx.fastmcp`は`ctx.mcp_server`、`FastMCPError`は`MCPServerError`です。v1時代にFastMCPで作る手順は[FastMCPでPythonのMCPサーバーを作る手順](https://www.issoh.co.jp/tech/details/6507/)にまとめています。

### host・portなどをrun()へ移すトランスポート設定の書き換え手順

v1でコンストラクタに渡していたhost、port、sse\_path、json\_response、stateless\_httpは、`run()`、`sse_app()`、`streamable_http_app()`の引数へ移りました。`streamable_http_app(port=...)`は`TypeError`、`mcp.settings.port = 9000`のような代入は`ValueError`です。`mount_path`は削除されています。

ASGIアプリへマウントしている構成では、ホスト側のlifespanで`mcp.session_manager.run()`を開始する必要があります。lifespanが走るのはセッションごとではなく、マネージャーの起動時に限った1回だけです。v1でセッション単位の資源をlifespanで開いていたなら、ハンドラの中か`exit_stack`へ移します。

環境変数も読まれなくなりました。v1は`MCP_`で始まる環境変数と.envファイルから設定を拾っていましたが、v2ではどちらも無視されます。コンテナの環境変数でポートを渡していた場合、エラーは出ず既定値で起動します。

### get\_contextとclient\_idの削除などContextで黙って壊れる変更

Contextの扱いは、型チェックで拾えるものと拾えないものが混ざります。移行ガイドから影響の大きい順に並べました。

- `get_context()`は削除。ツール関数の引数に`ctx: Context`を宣言して受け取る
- `Context.client_id`は削除。`_meta`か`get_access_token()`から取る
- 同期関数のツールはワーカースレッドで動き、`asyncio.get_running_loop()`が`RuntimeError`になる
- ツール内で`MCPError`を投げるとJSON-RPCエラーになる。ツールの失敗は`ToolError`か`is_error=True`の結果で返す
- `ProgressContext`は削除。`ctx.report_progress()`に絶対値を渡す

3つ目と4つ目は型では検出できません。v1では例外の種類を問わずツールエラーとして扱われていたため、エラー時の応答をテストで固定しておくと差分に気づけます。

### Streamable HTTPの4MiB上限で413を返す仕様と上限の引き上げ方

v2のStreamable HTTPサーバーは、リクエスト本文が4MiBを超えるとHTTP 413で拒否します。移行ガイドはこの上限をv2の変更点に挙げており、画像やPDFをbase64でツール引数に載せる設計は、移行後に大きなファイルだけ失敗します。上限を変更するための設定項目は、`run()`の`max_request_body_size`です。

```
# 上限を8MiBへ引き上げる例
mcp.run(transport="streamable-http", max_request_body_size=8 * 1024 * 1024)
```

ただし、引き上げは対症療法です。数MBのファイルを毎回JSONで運ぶなら、オブジェクトストレージのURLを引数に渡す設計へ寄せたほうが、メモリとタイムアウトの両面で扱いやすくなります。同じくローカルの127.0.0.1などで待ち受ける場合はDNSリバインディング保護が自動で有効になり、ポートなしのHostヘッダは421で拒否されます。リバースプロキシがHostを書き換える構成では、この挙動の確認が必要です。

## 低水準Serverとクライアント側で手作業が残る変更点の洗い出し

FastMCPを使わず`mcp.server.Server`で直接書いたサーバーと、SDKをクライアントとして使うコードは、書き換え量が一段多くなります。

### デコレータからon\_\*引数へ移る低水準Serverのハンドラ登録

v1の`@server.list_tools()`のようなデコレータは廃止され、コンストラクタの`on_*`キーワード引数で渡します。ハンドラは`(ctx, params)`を受け、結果型をそのまま返します。

```
from mcp.server import Server, ServerRequestContext
from mcp.types import CallToolRequestParams, CallToolResult, TextContent

async def handle_call_tool(ctx: ServerRequestContext, params: CallToolRequestParams) -> CallToolResult:
    # v1 の戻り値自動ラップは無い。例外も自分で is_error=True に変換する
    try:
        text = f"Called {params.name}"
        return CallToolResult(content=[TextContent(type="text", text=text)], is_error=False)
    except Exception as e:
        return CallToolResult(content=[TextContent(type="text", text=str(e))], is_error=True)

server = Server("my-server", on_call_tool=handle_call_tool)
```

v1ではリストや文字列を返すとSDKが結果型に包み、例外も`isError`の結果へ変えていました。v2はどちらも行いません。`params.arguments`は`None`になりうるので、v1と同じ挙動にするには`params.arguments or {}`と書きます。

### streamablehttp\_client削除とhttpx2への接続の書き換え

クライアント側では`streamablehttp_client`が削除され、`streamable_http_client`へ変わりました。ヘッダ、タイムアウト、認証は引数ではなく、渡す`httpx2.AsyncClient`側に設定します。戻り値は2要素のタプルになり、`get_session_id`はなくなりました。

新しく入った`Client`クラスを使うと、接続、版の交渉、サーバー情報の取得を1つのオブジェクトで扱えます。既定は`mode="auto"`で、まず2026-07-28仕様の`server/discover`を試し、応じないサーバーには従来の初期化ハンドシェイクで接続します。タイムアウトの指定は`timedelta`ではなく秒数のfloatになり、タイムアウト時のエラーはJSON-RPCの-32001（`REQUEST_TIMEOUT`）です。v1のHTTP 408を条件にしたリトライ処理は書き直します。

### camelCaseからsnake\_caseへ変わる型属性とby\_alias出力の落とし穴

プロトコル型は`mcp-types`パッケージへ分かれ、属性名がsnake\_caseになりました。`inputSchema`は`input_schema`、`isError`は`is_error`、`nextCursor`は`next_cursor`です。`import mcp.types`は別名として残るので、import文は直さずに済みます。

見落としやすいのは自前のシリアライズです。`tool.model_dump()`はsnake\_caseで出力され、ワイヤ形式のcamelCaseを得るには`model_dump(by_alias=True, mode="json")`が必要です。ツール一覧をJSONでキャッシュや管理画面へ流している処理は、エラーなしでキー名だけが変わります。未知のフィールドも保持されなくなったので、独自データは`_meta`へ移します。

## 2026-07-28仕様と旧クライアントを1つのサーバーで両対応させる構成

v2のリリースノートは、2026-07-28仕様に対応し、それ以前の版のクライアントにも同じサーバーで応じると説明しています。仕様側の変更点は[MCP仕様2026-07-28版の変更点と移行手順](https://www.issoh.co.jp/tech/details/18245/)で整理しています。ここで説明する対象は、Python SDKでの具体的な動き方です。

### MCP-Protocol-Versionヘッダで自動で振り分けられる2世代の経路

[旧クライアント対応のドキュメント](https://py.sdk.modelcontextprotocol.io/v2/run/legacy-clients/)によると、`streamable_http_app()`はリクエストごとにMCP-Protocol-Versionヘッダを見て振り分けます。2026-07-28を名乗るリクエストは新しい経路へ、2025-11-25以前かヘッダなしの最初のinitializeはセッションを持つ従来の経路へ進みます。設定は要りません。

裏返すと、旧世代を拒否するオプションはありません。旧経路のセッションはプロセス内のdictに置かれるため、複数ワーカーで動かすならスティッキールーティングが必要です。外すと別ワーカーが404（Session not found）を返します。アイドルセッションは既定1800秒で閉じ、上限の既定10,000セッションを超えると503です。トランスポート自体の仕組みは[Streamable HTTPとSSEの違い](https://www.issoh.co.jp/tech/details/6580/)で確認できます。

### ctx.elicitのNoBackChannelErrorとResolveへの置き換え

2026-07-28仕様の接続では、サーバーからクライアントへ要求を送るチャネルがありません。ツールの途中で`ctx.elicit()`やサンプリングを呼ぶと`NoBackChannelError`になります。v2の答えが`Resolve`で、ツール引数に付けた関数がユーザーへの問いかけを担います。[v2の新機能紹介](https://py.sdk.modelcontextprotocol.io/whats-new/)の例を日本語の説明に置き換えたものです。

```
from typing import Annotated
from pydantic import BaseModel
from mcp.server.mcpserver import (
    MCPServer, AcceptedElicitation, Elicit, ElicitationResult, Resolve,
)

mcp = MCPServer("Bookshop")

class Quantity(BaseModel):
    copies: int

async def ask_quantity() -> Elicit[Quantity]:
    """取り置く冊数をユーザーに尋ねる"""
    return Elicit("How many copies?", Quantity)

@mcp.tool()
async def reserve(title: str, quantity: Annotated[ElicitationResult[Quantity], Resolve(ask_quantity)]) -> str:
    """書籍を取り置く（冊数はResolveで取得）"""
    if isinstance(quantity, AcceptedElicitation):
        return f"Reserved {quantity.data.copies} of {title!r}."
    return "Nothing reserved."
```

同じツールが、新世代の接続では入力要求の結果を返す往復で、旧世代の接続では従来の`elicitation/create`で動きます。ただし旧経路を`stateless_http=True`で動かしている場合は、旧世代でも問いかけが送れず失敗します。`ctx.elicit()`を多用するサーバーは、移行の工数をここに見積もってください。

### テストのClient(server)をmode=legacyで旧世代に固定する場面

v1のテストヘルパー`create_connected_server_and_client_session`は削除され、`Client(server)`でインメモリ接続を作ります。注意点は、このClientが既定で2026-07-28を交渉することです。v1のテストで`ctx.elicit()`や`list_roots()`を通していた箇所は`NoBackChannelError`になります。

```
from mcp.client import Client

async def test_stock_legacy() -> None:
    # mcp は前掲の MCPServer。旧世代の接続に固定して v1 時代と同じ経路で検証する
    async with Client(mcp, mode="legacy") as client:
        result = await client.call_tool("stock", {"sku": "A-1"})
        assert not result.is_error
```

新旧両方の経路をテストで通すなら、同じテストを`mode="legacy"`と既定の2本で回します。pytestで`filterwarnings = ["error"]`を使っている場合、非推奨のRootsやSamplingの呼び出しが例外になり、表面に現れるのはツールエラーです。`"default::mcp.MCPDeprecationWarning"`を足すと、警告を出しつつテストは通せます。

## v2へ今すぐ移行する案件とmcp<2で据え置く案件を分ける判断基準

v1が保守モードでも、全案件を今月中に上げる必要はありません。条件で分けます。

### 移行を先送りしない条件と新仕様クライアントや水平スケールの要件

次のどれかに当たるなら、移行を先送りしないでください。1つ目は、利用者のクライアントが2026-07-28仕様へ移りつつある場合です。v2なら1つのエンドポイントで新旧に応じられ、v1のままだと新仕様のみを話すクライアントから接続できません。2つ目は、Streamable HTTPのサーバーを複数インスタンスで動かしたい場合です。新世代の経路はセッションに依存しないため、スティッキールーティングの制約は旧クライアントの分だけに縮みます。3つ目は、mcp本体を上限なしで依存している社内パッケージを配っている場合です。利用者の環境で勝手にv2が入るので、先にv2対応版を出すか上限を入れた版を出します。

### mcp<2で据え置くほうが正解になる条件と見送りの判断基準

次の条件では、据え置きを選びます。第1に、stdioでローカル起動するツールを社内に配るだけの用途です。クライアントがv1系のプロトコルで話す限り、v2の利得はほぼありません。第2に、`ctx.elicit()`やサンプリングを多くのツールで使い、`Resolve`への書き換えとテスト整備の工数を今期に取れない場合です。第3に、httpxの例外やクライアントを自前のリトライ処理やOAuth処理と深く結んでいる場合です。httpx2への置き換えは素通りの不具合を生むため、まとまった検証期間を確保してから着手します。

据え置く場合でも、v1は新機能を受け取りません。四半期以内の移行計画を立て、セキュリティ修正の版だけを取り込みます。なお、独立系のFastMCP（PyPIのfastmcp）は公式SDKとは別のパッケージです。そちらの移行は[FastMCP 4のsessionless対応と3系からの移行判断](https://www.issoh.co.jp/tech/details/15907/)で扱っています。TypeScriptと並行して移すなら[MCP TypeScript SDK v2の移行要点](https://www.issoh.co.jp/tech/details/15905/)と比べてください。

### MCPサーバー移行を外部へ任せる前にelicit利用数とデプロイ構成を棚卸しする範囲

移行を外部へ出すとき、見積りがぶれる原因は2つです。1つは`ctx.elicit()`、サンプリング、rootsを使うツールの本数で、ここが`Resolve`の書き換え量を決めます。もう1つはデプロイ構成で、複数ワーカーか、マウント型か、リバースプロキシがHostを書き換えるかによって確認項目が変わります。作業範囲を固定するには、この2点を棚卸ししてから依頼する方法が有効です。エージェント基盤ごと見直すなら、[AIエージェント開発](https://www.issoh.co.jp/service/ai/agent/)のように、MCPサーバーの設計から運用まで任せられる体制と社内で持つ範囲を先に分けておきます。

## よくある質問

MCP Python SDK v2への移行でよく問われる点を、PyPIのメタデータと公式ドキュメントに沿って答えます。

### MCP Python SDK v1はいつまで使えますか？

終了日は示されていません。v2.0.0のリリースノートでv1.xは保守モードとされ、今後はセキュリティ修正のみを受けます。1.30.0がリリースされた日付は2026年9月7日です。使い続けるなら依存を`mcp<2`で固定し、新機能や2026-07-28仕様への対応が必要になった時点でv2へ移ります。

### FastMCPのコードはそのまま動きますか？

そのままでは動きません。`mcp.server.fastmcp`は削除され、`ModuleNotFoundError`になります。`from mcp.server.mcpserver import MCPServer`へ直せば、`@mcp.tool()`などのデコレータは同じ書き方で使えます。コンストラクタの2番目以降の位置引数と、host・portの渡し先は直してください。

### v2はPythonのどのバージョンで動きますか？

2026年10月11日時点の最新版2.3.0は、PyPIのメタデータで要求PythonがPython 3.10以上です。Python 3.14ではanyioとstarletteの下限が一段上がります。依存にはpydantic 2.12以上とhttpx2が入るため、Pythonの版よりも、既存のpydanticやhttpxとの共存のほうが先に問題になります。

### v2に上げると旧クライアントは接続できなくなりますか？

接続できます。`streamable_http_app()`はリクエストのMCP-Protocol-Versionヘッダで振り分け、ヘッダがないinitializeは従来の経路で処理します。旧世代だけを拒否する設定はありません。旧経路のセッションはプロセス内に保持されるので、複数ワーカーではスティッキールーティングが必要です。

### 移行後にテストが落ちる原因で多いものは何ですか？

既定の交渉先が変わったことです。`Client(server)`は既定で2026-07-28仕様を話すため、`ctx.elicit()`やroots、サンプリングを通すテストが`NoBackChannelError`で失敗します。旧動作を確かめるテストは`mode="legacy"`で固定し、新仕様側は`Resolve`へ書き換えて別に検証します。

## 関連記事

- [MCP仕様2026-07-28版とは：ステートレス化の変更点と2025-11-25版からの移行手順](https://www.issoh.co.jp/tech/details/18245/)：SDKの背後にある仕様の差分
- [FastMCPとは？PythonでMCPサーバーを作る手順とFastMCP 4の使い方](https://www.issoh.co.jp/tech/details/6507/)：PythonでMCPサーバーを作る基本
- [MCP TypeScript SDK v2とは？パッケージ分割とステートレス化の移行要点](https://www.issoh.co.jp/tech/details/15905/)：同じ仕様に対応したTypeScript版
- [Streamable HTTPとは？MCPのトランスポートとSSEの違い](https://www.issoh.co.jp/tech/details/6580/)：4MiB上限や両世代対応の土台
- [MCPとは？AIと外部ツールをつなぐ標準規格とMCPサーバーの仕組み](https://www.issoh.co.jp/column/details/12966/)：導入判断の前段

---

出典: [MCP Python SDK v2 移行：FastMCPからMCPServerへの書き換え手順と据え置き判断](<https://www.issoh.co.jp/tech/details/18273/>)（株式会社一創）
