|
| 1 | +# Python MCP SDK Custom Context Analysis |
| 2 | + |
| 3 | +## Executive Summary |
| 4 | + |
| 5 | +The Python MCP SDK **lacks built-in support for custom context injection** similar to what was added to the TypeScript SDK. While it does provide access to the raw HTTP request object in handlers, there's no clean mechanism to inject processed custom context (e.g., user authentication data, permissions, tenant information) that can be accessed by tool/prompt/resource handlers. |
| 6 | + |
| 7 | +## Current State of Python SDK |
| 8 | + |
| 9 | +### Context Architecture |
| 10 | + |
| 11 | +1. **RequestContext Class** (`src/mcp/shared/context.py`): |
| 12 | +```python |
| 13 | +@dataclass |
| 14 | +class RequestContext(Generic[SessionT, LifespanContextT, RequestT]): |
| 15 | + request_id: RequestId |
| 16 | + meta: RequestParams.Meta | None |
| 17 | + session: SessionT |
| 18 | + lifespan_context: LifespanContextT |
| 19 | + request: RequestT | None = None # This is where custom data could go |
| 20 | +``` |
| 21 | + |
| 22 | +2. **Context Access in Handlers**: |
| 23 | +```python |
| 24 | +@app.call_tool() |
| 25 | +async def my_tool(name: str, arguments: dict) -> list[types.ContentBlock]: |
| 26 | + ctx = app.request_context # Access context |
| 27 | + # ctx.request contains the Starlette Request object |
| 28 | + # ctx.session, ctx.request_id, ctx.lifespan_context are available |
| 29 | +``` |
| 30 | + |
| 31 | +3. **Transport Layer** (`src/mcp/server/streamable_http.py`): |
| 32 | + - Line 385-386: Creates `ServerMessageMetadata` with `request_context=request` |
| 33 | + - The raw Starlette Request object is passed as the context |
| 34 | + - No mechanism to inject processed custom data |
| 35 | + |
| 36 | +### Key Differences from TypeScript SDK |
| 37 | + |
| 38 | +| Aspect | TypeScript SDK | Python SDK | |
| 39 | +|--------|---------------|------------| |
| 40 | +| Custom Context Method | `transport.setCustomContext()` | None | |
| 41 | +| Context Access | `extra.customContext` | `app.request_context.request` | |
| 42 | +| Context Type | Arbitrary object | Starlette Request object | |
| 43 | +| Processing | Transport can inject processed data | Only raw HTTP request available | |
| 44 | +| Type Safety | Can define custom types | Limited to Request type | |
| 45 | + |
| 46 | +## Problems with Current Python SDK |
| 47 | + |
| 48 | +1. **No Clean Context Injection**: Handlers receive the raw HTTP request but there's no way to inject processed context |
| 49 | +2. **Authentication Complexity**: Every handler would need to extract and validate authentication from headers |
| 50 | +3. **No Abstraction**: Tight coupling to HTTP transport (Starlette Request) |
| 51 | +4. **Repeated Logic**: Authentication/authorization logic must be duplicated in each handler |
| 52 | +5. **Limited Flexibility**: Can't easily inject tenant data, user permissions, or other contextual information |
| 53 | + |
| 54 | +## Proposed Fix Plan |
| 55 | + |
| 56 | +### Option 1: Minimal Change - Add Custom Context Field (Recommended) |
| 57 | + |
| 58 | +Add a `custom_context` field to `RequestContext` and provide a way for transports to set it: |
| 59 | + |
| 60 | +#### 1. Update RequestContext (`src/mcp/shared/context.py`): |
| 61 | +```python |
| 62 | +@dataclass |
| 63 | +class RequestContext(Generic[SessionT, LifespanContextT, RequestT]): |
| 64 | + request_id: RequestId |
| 65 | + meta: RequestParams.Meta | None |
| 66 | + session: SessionT |
| 67 | + lifespan_context: LifespanContextT |
| 68 | + request: RequestT | None = None |
| 69 | + custom_context: Any | None = None # NEW: Custom context field |
| 70 | +``` |
| 71 | + |
| 72 | +#### 2. Update ServerMessageMetadata (`src/mcp/shared/message.py`): |
| 73 | +```python |
| 74 | +@dataclass |
| 75 | +class ServerMessageMetadata: |
| 76 | + related_request_id: RequestId | None = None |
| 77 | + request_context: Any | None = None |
| 78 | + custom_context: Any | None = None # NEW: Custom context field |
| 79 | +``` |
| 80 | + |
| 81 | +#### 3. Update StreamableHTTPServerTransport (`src/mcp/server/streamable_http.py`): |
| 82 | +Add a method to set custom context and use it in request handling: |
| 83 | + |
| 84 | +```python |
| 85 | +class StreamableHTTPServerTransport: |
| 86 | + def __init__(self, ...): |
| 87 | + # ... existing code ... |
| 88 | + self._custom_context: Any | None = None |
| 89 | + |
| 90 | + def set_custom_context(self, context: Any) -> None: |
| 91 | + """Set custom context to be passed to handlers.""" |
| 92 | + self._custom_context = context |
| 93 | + |
| 94 | + async def _handle_post_request(self, ...): |
| 95 | + # ... existing code ... |
| 96 | + # Line ~385, update metadata creation: |
| 97 | + metadata = ServerMessageMetadata( |
| 98 | + request_context=request, |
| 99 | + custom_context=self._custom_context # NEW: Include custom context |
| 100 | + ) |
| 101 | +``` |
| 102 | + |
| 103 | +#### 4. Update Server (`src/mcp/server/lowlevel/server.py`): |
| 104 | +Pass custom context to RequestContext: |
| 105 | + |
| 106 | +```python |
| 107 | +async def _handle_request(self, ...): |
| 108 | + # ... existing code ... |
| 109 | + # Extract custom context from metadata |
| 110 | + custom_context = None |
| 111 | + if message.message_metadata and isinstance(message.message_metadata, ServerMessageMetadata): |
| 112 | + request_data = message.message_metadata.request_context |
| 113 | + custom_context = message.message_metadata.custom_context # NEW |
| 114 | + |
| 115 | + # Set context with custom data |
| 116 | + token = request_ctx.set( |
| 117 | + RequestContext( |
| 118 | + message.request_id, |
| 119 | + message.request_meta, |
| 120 | + session, |
| 121 | + lifespan_context, |
| 122 | + request=request_data, |
| 123 | + custom_context=custom_context # NEW: Pass custom context |
| 124 | + ) |
| 125 | + ) |
| 126 | +``` |
| 127 | + |
| 128 | +#### 5. Add Middleware Support in StreamableHTTPSessionManager: |
| 129 | +```python |
| 130 | +class StreamableHTTPSessionManager: |
| 131 | + def __init__(self, ..., context_middleware: Callable[[Request], Awaitable[Any]] | None = None): |
| 132 | + self.context_middleware = context_middleware |
| 133 | + |
| 134 | + async def _handle_stateful_request(self, ...): |
| 135 | + # ... existing code ... |
| 136 | + # Before creating transport, process context |
| 137 | + custom_context = None |
| 138 | + if self.context_middleware: |
| 139 | + custom_context = await self.context_middleware(request) |
| 140 | + |
| 141 | + # Pass custom context to transport |
| 142 | + http_transport = StreamableHTTPServerTransport(...) |
| 143 | + if custom_context: |
| 144 | + http_transport.set_custom_context(custom_context) |
| 145 | +``` |
| 146 | + |
| 147 | +### Option 2: Full Middleware Architecture |
| 148 | + |
| 149 | +Create a more comprehensive middleware system similar to Express.js or FastAPI: |
| 150 | + |
| 151 | +1. Define middleware interface |
| 152 | +2. Allow chaining of middleware functions |
| 153 | +3. Support both sync and async middleware |
| 154 | +4. Provide built-in authentication middleware |
| 155 | + |
| 156 | +This is more complex but provides greater flexibility. |
| 157 | + |
| 158 | +### Option 3: Subclass-Based Approach |
| 159 | + |
| 160 | +Allow users to subclass `StreamableHTTPServerTransport` and override context extraction: |
| 161 | + |
| 162 | +```python |
| 163 | +class CustomHTTPTransport(StreamableHTTPServerTransport): |
| 164 | + async def extract_context(self, request: Request) -> Any: |
| 165 | + # Custom logic to extract and process context |
| 166 | + api_key = request.headers.get("X-API-Key") |
| 167 | + return await fetch_user_context(api_key) |
| 168 | +``` |
| 169 | + |
| 170 | +## Implementation Priority |
| 171 | + |
| 172 | +1. **Phase 1**: Implement Option 1 (Minimal Change) - Adds basic custom context support |
| 173 | +2. **Phase 2**: Add helper utilities for common patterns (auth extraction, validation) |
| 174 | +3. **Phase 3**: Consider full middleware architecture if needed |
| 175 | + |
| 176 | +## Example Usage After Fix |
| 177 | + |
| 178 | +```python |
| 179 | +# Server setup with custom context |
| 180 | +async def context_middleware(request: Request) -> dict: |
| 181 | + """Extract and validate user context from request.""" |
| 182 | + api_key = request.headers.get("X-API-Key") |
| 183 | + if not api_key: |
| 184 | + return None |
| 185 | + |
| 186 | + # Fetch user data from database |
| 187 | + user_data = await fetch_user_by_api_key(api_key) |
| 188 | + return { |
| 189 | + "user_id": user_data["id"], |
| 190 | + "email": user_data["email"], |
| 191 | + "permissions": user_data["permissions"], |
| 192 | + "organization_id": user_data["org_id"] |
| 193 | + } |
| 194 | + |
| 195 | +# Initialize session manager with middleware |
| 196 | +session_manager = StreamableHTTPSessionManager( |
| 197 | + app=app, |
| 198 | + context_middleware=context_middleware |
| 199 | +) |
| 200 | + |
| 201 | +# In tool handlers |
| 202 | +@app.call_tool() |
| 203 | +async def my_tool(name: str, arguments: dict) -> list[types.ContentBlock]: |
| 204 | + ctx = app.request_context |
| 205 | + user_context = ctx.custom_context # Access custom context |
| 206 | + |
| 207 | + if not user_context: |
| 208 | + return [types.TextContent(type="text", text="Not authenticated")] |
| 209 | + |
| 210 | + if "admin" not in user_context.get("permissions", []): |
| 211 | + return [types.TextContent(type="text", text="Permission denied")] |
| 212 | + |
| 213 | + # Proceed with tool logic |
| 214 | + return [types.TextContent( |
| 215 | + type="text", |
| 216 | + text=f"Hello {user_context['email']}!" |
| 217 | + )] |
| 218 | +``` |
| 219 | + |
| 220 | +## Benefits of Proposed Solution |
| 221 | + |
| 222 | +1. **Clean Separation**: Authentication logic separated from business logic |
| 223 | +2. **Type Safety**: Can use TypedDict or dataclasses for context types |
| 224 | +3. **Reusability**: Context extraction logic in one place |
| 225 | +4. **Transport Agnostic**: Works with any transport that supports context |
| 226 | +5. **Backward Compatible**: Existing code continues to work |
| 227 | +6. **Minimal Changes**: Small, focused changes to core SDK |
| 228 | + |
| 229 | +## Migration Path |
| 230 | + |
| 231 | +1. Changes are backward compatible - existing handlers continue working |
| 232 | +2. New `custom_context` field is optional |
| 233 | +3. Gradual adoption - handlers can migrate to use custom context as needed |
| 234 | +4. Documentation and examples to guide migration |
| 235 | + |
| 236 | +## Testing Strategy |
| 237 | + |
| 238 | +1. Unit tests for context injection and retrieval |
| 239 | +2. Integration tests with authentication middleware |
| 240 | +3. Example server demonstrating custom context usage |
| 241 | +4. Performance tests to ensure no regression |
| 242 | + |
| 243 | +## Conclusion |
| 244 | + |
| 245 | +The Python MCP SDK currently lacks the custom context injection capability that was recently added to the TypeScript SDK. The proposed fix (Option 1) provides a minimal, backward-compatible solution that brings feature parity between the two SDKs while maintaining the Python SDK's design principles. |
| 246 | + |
| 247 | +The implementation is straightforward and can be completed in a few hours, with most of the work involving: |
| 248 | +1. Adding fields to existing dataclasses |
| 249 | +2. Passing context through the call chain |
| 250 | +3. Adding a context middleware hook |
| 251 | +4. Creating examples and documentation |
| 252 | + |
| 253 | +This would enable Python MCP servers to properly handle authentication, multi-tenancy, and permission-based access control in a clean, maintainable way. |
0 commit comments