Skip to content

Comments

feat(server): lifespan redesign with server-scoped and session-scoped lifetimes#2127

Closed
gspeter-max wants to merge 18 commits intomodelcontextprotocol:mainfrom
gspeter-max:python-sdk/issue_2113
Closed

feat(server): lifespan redesign with server-scoped and session-scoped lifetimes#2127
gspeter-max wants to merge 18 commits intomodelcontextprotocol:mainfrom
gspeter-max:python-sdk/issue_2113

Conversation

@gspeter-max
Copy link

Summary

This PR implements Option B (Breaking Change) for the lifespan redesign, separating server-level and session-level resource lifecycles. This fixes critical bugs #1300 and #1304 where database pools were being initialized on the first client connection instead of at server startup.

Key Changes

  • New server_lifespan parameter: Runs ONCE at server startup (for database pools, ML models, shared caches)
  • Renamed lifespan to session_lifespan: Runs PER-CLIENT connection (for user auth, session-specific state)
  • Context split: ctx.lifespan_contextctx.server_lifespan_context + ctx.session_lifespan_context
  • New ServerLifespanManager: Infrastructure for managing server-scoped resources via context variable
  • Starlette app integration: Server lifespan now runs at app startup, not in session manager

Migration Impact

Breaking Change: Users must update their code from:

# OLD (v1)
server = Server("name", lifespan=my_lifespan)
# Access: ctx.lifespan_context
# NEW (v2)
server = Server("name", 
    server_lifespan=my_server_lifespan,    # Runs once
    session_lifespan=my_session_lifespan)  # Per-client
# Access: ctx.server_lifespan_context, ctx.session_lifespan_context

See Migration Guide for detailed examples.

Test Plan

  • ✅ All 663 existing tests pass
  • ✅ 8 new lifespan-specific tests added
  • ✅ Integration tests with streamable-http pass
  • ✅ Client tests unaffected (182 passing)
  • ✅ Code formatting and linting pass

Fixes

Documentation

  • Migration guide added to docs/migration.md
  • README.v2.md updated with dual-scope examples
  • Lifespan example updated to show both scopes

Files Changed

  • Core: Server, ServerRequestContext, ServerLifespanManager
  • Tests: 8 new test files, all existing tests updated
  • Docs: Migration guide, README, examples
  • Total: 15 commits, 10 source files modified, 3 test files created

🤖 Generated with Claude Code

This clarifies that the default lifespan function is for session-scoped
resources. Server lifespan will be added separately.
This provides the infrastructure for managing server-level lifecycle
resources that live for the entire server process.

The server lifespan context is stored in a context variable, making
it accessible to all sessions without re-initializing.
CRITICAL FIX: Server lifespan now runs at Starlette app startup (once),
not in session manager. This is the correct fix for bugs modelcontextprotocol#1300 and modelcontextprotocol#1304.

The server lifespan runs BEFORE the session manager starts, ensuring
database pools and ML models are initialized once and shared across
all client sessions.

Note: Type errors will be fixed in subsequent commits when context access
is updated.
…contexts

This separates server-level resources (database pools, ML models)
from session-level resources (user data, auth context) for clarity.

BREAKING CHANGE: ctx.lifespan_context is now split into:
- ctx.server_lifespan_context
- ctx.session_lifespan_context
…texts

Request handlers now receive both server_lifespan_context and
session_lifespan_context. The server context is retrieved from
the context variable set by ServerLifespanManager.

Note: Type errors will be fixed in subsequent commits when tests are updated.
Tests now use session_lifespan parameter instead of lifespan for
low-level Server. MCPServer continues to use lifespan parameter
internally.

Also updated MCPServer Context class to use new type variables
(ServerLifespanContextT, SessionLifespanContextT) and updated
import references.
Tests verify:
- Server lifespan runs once at startup
- Context is accessible via manager and context variable
- Context persists across sessions
- Default server lifespan works
- Error handling when context not set
…le-http

Tests verify:
- Server lifespan runs at startup (not on client connect)
- Session lifespan runs per-client
- Handlers can access both contexts
- Proper lifecycle ordering
Example now demonstrates:
- Server lifespan (runs once, shared database)
- Session lifespan (runs per-client, session_id)
- How to access both contexts in handlers

Shows the clear separation between server-level and session-level
resources, which is the key improvement in the redesigned API.
Documents the breaking change from single lifespan to
server_lifespan + session_lifespan parameters.

Includes:
- Before/after code examples
- Key differences table
- When to use each lifespan type
- Explanation of the bug fix (modelcontextprotocol#1300, modelcontextprotocol#1304)
Updated two sections:
1. MCPServer lifespan example - now shows server_lifespan and session_lifespan
2. Request Context Properties - documents both server_lifespan_context and session_lifespan_context

Clarifies when to use each lifespan type and what resources belong in each scope.
Fixed imports in:
- MCPServer and all its submodules
- test_tool_manager.py

Changed from LifespanContextT to ServerLifespanContextT and SessionLifespanContextT
to match the updated context module.
The _create_app_lifespan method was causing issues when called as a bound
method from within the combined_lifespan lambda. Fixed by inlining the
logic directly in the lambda function.

This fixes integration tests that were failing with:
TypeError: Server._create_app_lifespan() takes 2 positional arguments but 3 were given
Fixed tests that were still using the old lifespan_context parameter:
- Updated to server_lifespan_context and session_lifespan_context
- Fixed experimental tasks tests
- Fixed shared streamable-http tests
- Fixed issues tests
Updated all type annotations from Context[LifespanContextT, RequestT]
to Context[ServerLifespanContextT, SessionLifespanContextT, RequestT]

This fixes type errors in:
- prompts/base.py, prompts/manager.py
- resources/resource_manager.py, resources/templates.py
- tools/base.py, tools/tool_manager.py
ROOT CAUSE:
The MCPServer lifespan example was showing the dual-lifespan API
(server_lifespan + session_lifespan) which is only for the lowlevel
Server API. MCPServer uses a single 'lifespan' parameter mapped to
session_lifespan internally.

CHANGES:
- Reverted lifespan_example.py to use single 'lifespan' parameter
- Updated context access to use session_lifespan_context (correct for MCPServer)
- Regenerated README.v2.md code snippets via update_readme_snippets.py

IMPACT:
- Fixes CI failure in README snippet validation
- Documents correct MCPServer API usage
- Lowlevel Server API (with dual lifespans) remains documented separately

Github-Issue:modelcontextprotocol#2113
ROOT CAUSE:
Pre-commit hook detected files with inconsistent formatting.

CHANGES:
- Collapsed multi-line function call arguments to single line
- Fixed type annotation line wrapping in resource_manager.py
- Applied consistent formatting across 4 files

IMPACT:
- Fixes pre-commit CI failure
- Ensures code style consistency

Github-Issue:modelcontextprotocol#2113
ROOT CAUSE:
The experimental task tests were using the old 'lifespan' parameter
which was renamed to 'session_lifespan' in the lifespan redesign.

CHANGES:
- Updated tests/experimental/tasks/server/test_integration.py
- Updated tests/experimental/tasks/client/test_tasks.py
- Changed Server() calls from lifespan= to session_lifespan=

IMPACT:
- Fixes test failures in experimental/tasks/ directory
- All 217 task tests now pass

Github-Issue:modelcontextprotocol#2113
@gspeter-max
Copy link
Author

Closing PR - will be deleted and resubmitted

@gspeter-max gspeter-max deleted the python-sdk/issue_2113 branch February 22, 2026 22:42
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.

Lifespan enters and exits for each request when stateless_http=True Lifespan does not execute on startup streamable-http

2 participants