Skip to content

docs: fill migration guide gaps surfaced by automated upgrade eval#2412

Merged
maxisbey merged 2 commits intomainfrom
docs/migration-eval-gaps
Apr 9, 2026
Merged

docs: fill migration guide gaps surfaced by automated upgrade eval#2412
maxisbey merged 2 commits intomainfrom
docs/migration-eval-gaps

Conversation

@maxisbey
Copy link
Copy Markdown
Contributor

@maxisbey maxisbey commented Apr 9, 2026

Fills gaps in docs/migration.md identified by an automated migration eval: 24 workers × 8 v1 codebases, each tasked with upgrading to v2 using only the migration guide. The eval surfaced 139 gap reports (20/24 successful migrations, 4 partial), aggregated into 10 priority findings and 10 minor friction points.

Motivation and Context

The eval showed real migration friction the doc didn't address — workers hit ImportError/ModuleNotFoundError/AttributeError and had to read SDK source to recover. These gaps would translate directly to support burden and failed upgrades for end users.

Priority gaps fixed (commit 1):

  • Context import path never shown (highest-frequency gap, ~9 reports)
  • camelCase → snake_case field rename never stated as a rule
  • Complete on_* handler reference table for lowlevel Server
  • mcp.server.fastmcp.* submodule rename note
  • create_connected_server_and_client_session removal — point to Client(server) instead
  • MCPError raise-side constructor change
  • mcp.shared.context module removal stated explicitly
  • _meta example fixed to avoid progressToken alias confusion
  • Documented _add_request_handler workaround for handlers MCPServer doesn't expose

Minor gaps fixed (commit 2):

  • sse_client params unchanged note
  • Iterable[ReadResourceContents] added to read_resource wrapping example
  • mcp.settings no longer holds transport fields; debug/log_level remain on constructor
  • Context generic params 3→2 with before/after
  • No public handler-introspection API note
  • RootModel construction-side example (drop wrapper)
  • follow_redirects=True added to primary streamable_http_client example with rationale

Two gaps were skipped as not-migration-guide-territory (AnyUrl.path parsing, timedelta conversion). One gap (dependencies= removal) was skipped because the parameter was restored in #2358.

How Has This Been Tested?

All 34 from mcp... import statements in the doc's v2 code blocks were extracted and verified to resolve against the rebased code. Behavioral claims (camelCase/snake_case construction vs attribute access, MCPError constructor, _add_request_handler workaround, Context generics) were verified by running them against the installed package.

Breaking Changes

None — documentation only.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

Additional context

The RequestParamsMeta TypedDict has an asymmetric key-transformation behavior (only progress_token gets aliased; extra keys pass through untouched) that this PR works around in the doc but doesn't fix in code — flagged for separate follow-up.

AI Disclaimer

maxisbey added 2 commits April 9, 2026 10:54
Adds missing migration coverage identified by running 24 automated upgrade
workers across 8 v1 codebases using only docs/migration.md as reference.

- Show Context import path in FastMCP→MCPServer section and get_context() example
- Document camelCase→snake_case field rename with common-field table
- Add complete on_* handler reference table (decorator → kwarg → params → return)
- Note that all mcp.server.fastmcp.* submodules moved to mcp.server.mcpserver.*
- Document create_connected_server_and_client_session removal (use Client instead)
- Add raise-side example for MCPError constructor signature change
- State explicitly that mcp.shared.context module was removed
- Fix _meta example to use arbitrary keys (avoid progressToken alias confusion)
- Document private _add_request_handler workaround for handlers MCPServer
  doesn't expose (subscribe/unsubscribe/set_logging_level)
Follow-up to the previous commit covering the lower-severity gaps that
caused hesitation but not failures during the automated upgrade eval.

- Note that sse_client retains headers/timeout/auth params (only
  streamable HTTP changed)
- Add Iterable[ReadResourceContents] to the read_resource wrapping-removed
  example (was the recommended v1 return type)
- Note that mcp.settings no longer holds transport fields; debug and
  log_level remain on the constructor
- Document Context generic params change (3→2, drop ServerSessionT) with
  before/after
- Note there is currently no public handler introspection API
- Show RootModel construction direction (drop wrapper call), not just
  parsing direction
- Add follow_redirects=True to the primary streamable_http_client example
  and explain v1 set it internally
Copy link
Copy Markdown
Contributor

@felixweinberger felixweinberger left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚢

Copy link
Copy Markdown
Contributor

@claude claude bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM — comprehensive doc improvements, the inline comment flags a minor type-precision nit in the Context generics example.

Extended reasoning...

Overview

Documentation-only PR modifying docs/migration.md. Adds 17 new sections/clarifications covering migration gaps surfaced by an automated eval: Context import paths, camelCase→snake_case rename rule, lowlevel Server handler reference table, mcp.server.fastmcp.* rename, create_connected_server_and_client_session removal, MCPError constructor change, mcp.shared.context removal, _add_request_handler workaround, RootModel construction change, and several minor friction points.

Security risks

None — documentation only, no code changes.

Level of scrutiny

Low. All changes are prose and code examples in a migration guide. The PR description notes that all 34 from mcp... import statements were verified to resolve, and behavioral claims were validated against the installed package. The sole bug found is a nit (inline comment flagged) affecting only static type checkers: Context[ServerSession, None] → bare Context is not type-preserving; Context[None] would be the precise equivalent. Runtime behavior is unaffected.

Other factors

No prior reviews on this PR. The PR is well-scoped (single file), clearly motivated, and the fixes are independently verifiable against the SDK source. The nit is minor enough that it does not block approval — it is already surfaced via inline comment.

Comment on lines 600 to 626
server_ctx: ServerRequestContext[LifespanContextT, RequestT]
```

The high-level `Context` class (injected into `@mcp.tool()` etc.) similarly dropped its `ServerSessionT` parameter: `Context[ServerSessionT, LifespanContextT, RequestT]` → `Context[LifespanContextT, RequestT]`. Both remaining parameters have defaults, so bare `Context` is usually sufficient:

**Before (v1):**

```python
async def my_tool(ctx: Context[ServerSession, None]) -> str: ...
```

**After (v2):**

```python
async def my_tool(ctx: Context) -> str: ...
# or, with an explicit lifespan type:
async def my_tool(ctx: Context[MyLifespanState]) -> str: ...
```

### `ProgressContext` and `progress()` context manager removed

The `mcp.shared.progress` module (`ProgressContext`, `Progress`, and the `progress()` context manager) has been removed. This module had no real-world adoption — all users send progress notifications via `Context.report_progress()` or `session.send_progress_notification()` directly.

**Before:**
**Before (v1):**

```python
from mcp.shared.progress import progress
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 The migration guide shows Context[ServerSession, None] → bare Context, but this silently changes LifespanContextT from None to dict[str, Any] for type-checker users. The type-accurate v2 equivalent of Context[ServerSession, None] is Context[None], not bare Context.

Extended reasoning...

What the bug is: In the migration guide's "Context generic params" section (lines 600–626), the before example is Context[ServerSession, None] where the second type param is LifespanContextT=None. The after example shows bare Context, which in v2 resolves to Context[dict[str, Any], Any] because LifespanContextT has a default of dict[str, Any].

The specific code path: In src/mcp/server/context.py, LifespanContextT is defined as TypeVar('LifespanContextT', default=dict[str, Any]). So bare Context expands to Context[dict[str, Any], Any] under a type checker, not Context[None, Any].

Why existing doc text doesn't prevent it: The guide says bare Context is "usually sufficient", which partially mitigates the concern, but the direct before/after pairing of Context[ServerSession, None]Context implies they are type-equivalent when they are not. Users following this one-to-one mapping will silently change LifespanContextT from None to dict[str, Any].

Impact: Runtime behavior is completely unaffected — TypeVar defaults only matter to static type checkers. However, users who had Context[ServerSession, None] specifically to signal "no lifespan" and follow this guide will end up with Context (i.e., Context[dict[str, Any], Any]), which could cause false type-checker positives or negatives if they try to access ctx.request_context.lifespan_context and expect it to be None.

How to fix: Change the after example to show Context[None] as the direct type-preserving equivalent, and note bare Context separately as a convenience when lifespan type precision is not required:

# After (v2) — type-preserving equivalent:
async def my_tool(ctx: Context[None]) -> str: ...
# or, if lifespan type precision is not needed:
async def my_tool(ctx: Context) -> str: ...

Step-by-step proof:

  1. v1 code: async def my_tool(ctx: Context[ServerSession, None]) -> str: ... — here LifespanContextT = None
  2. Migration guide says: migrate to async def my_tool(ctx: Context) -> str: ...
  3. In v2, Context without type params uses TypeVar defaults: LifespanContextT = TypeVar('LifespanContextT', default=dict[str, Any])
  4. So bare Context = Context[dict[str, Any], Any], not Context[None, Any]
  5. The type-preserving equivalent is Context[None] (keeping LifespanContextT=None), which the guide does not show as an option

@maxisbey maxisbey merged commit f27d2aa into main Apr 9, 2026
32 checks passed
@maxisbey maxisbey deleted the docs/migration-eval-gaps branch April 9, 2026 12:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants