Skip to content

Commit fa6265e

Browse files
committed
docs: clarify custom context supports MCP access tokens alongside API keys
- Updated documentation to explicitly mention MCP access tokens (OAuth flow) - Simplified examples to focus on API keys and MCP tokens only - Removed mentions of other auth methods to avoid confusion - Emphasized that the pattern works for both API keys and MCP OAuth tokens
1 parent 40739fd commit fa6265e

File tree

4 files changed

+305
-21
lines changed

4 files changed

+305
-21
lines changed

python-sdk-custom-context-analysis.md

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
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.

src/examples/client/customContextClient.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@ import {
1212
/**
1313
* Interactive client demonstrating custom context feature.
1414
*
15-
* This client shows how API keys are used to authenticate and
15+
* This example uses API keys for authentication, but the same pattern works
16+
* with MCP access tokens from the OAuth flow.
17+
*
18+
* The client shows how authentication credentials are sent with requests and
1619
* how the server uses the context to provide user-specific responses.
1720
*/
1821

@@ -46,8 +49,8 @@ async function main(): Promise<void> {
4649
console.log('MCP Custom Context Demo Client');
4750
console.log('==============================================');
4851
console.log('\nThis client demonstrates how custom context works:');
49-
console.log('1. Authenticate with an API key');
50-
console.log('2. The server fetches user context from the API key');
52+
console.log('1. Authenticate with credentials (API key or MCP access token)');
53+
console.log('2. The server validates credentials and fetches user context');
5154
console.log('3. Tools receive the context and respond based on user permissions\n');
5255

5356
printHelp();
@@ -160,12 +163,14 @@ async function authenticateWithKey(apiKey: string): Promise<void> {
160163
// Store the API key for this session (used in fetch)
161164
console.log(`\n🔐 Authenticating with API key: ${apiKey.substring(0, 15)}...`);
162165

163-
// Create transport with API key in headers
166+
// Create transport with authentication credentials in headers
167+
// This example uses API key, but you could also use MCP access tokens
164168
transport = new StreamableHTTPClientTransport(
165169
new URL(serverUrl),
166170
{
167171
fetch: async (url: string | URL, options?: RequestInit) => {
168-
// Add API key to all requests
172+
// Add authentication credentials to all requests
173+
// For MCP access token: use Authorization header instead of X-API-Key
169174
// Handle Headers object or plain object
170175
let headers: HeadersInit;
171176
if (options?.headers instanceof Headers) {

src/examples/custom-context-example.md

Lines changed: 27 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ This example demonstrates the **Custom Context** feature that allows MCP servers
66

77
Custom Context allows transport implementations to attach arbitrary data that will be available to all request handlers (tools, prompts, resources). This is essential for:
88

9-
- **Authentication**: Pass user identity from API keys or tokens
9+
- **Authentication**: Pass user identity from API keys or MCP access tokens
1010
- **Multi-tenancy**: Isolate data between different organizations
1111
- **Permissions**: Enforce access control based on user roles
1212
- **Request tracking**: Add request IDs for debugging and auditing
@@ -142,17 +142,23 @@ Content:
142142

143143
### Server Side
144144

145-
1. **API Key Extraction**: The server extracts the API key from request headers
146-
2. **Context Fetching**: Uses the API key to fetch user context from a "database"
145+
1. **Authentication Extraction**: The server extracts authentication credentials from request headers
146+
2. **Context Fetching**: Uses the credentials to fetch/validate user context
147147
3. **Context Injection**: Calls `transport.setCustomContext(userContext)`
148148
4. **Tool Access**: Tools receive context via `extra.customContext`
149149

150150
```typescript
151-
// In the server
152-
const context = fetchUserContext(apiKey);
151+
// Example with API key (shown in demo)
152+
const apiKey = request.headers.get('x-api-key');
153+
const context = await fetchUserContextByApiKey(apiKey);
153154
transport.setCustomContext(context);
154155

155-
// In tool handlers
156+
// Example with MCP access token (OAuth flow)
157+
const accessToken = request.headers.get('authorization')?.replace('Bearer ', '');
158+
const context = await validateMcpAccessToken(accessToken);
159+
transport.setCustomContext(context);
160+
161+
// In tool handlers (same regardless of auth method)
156162
async (params, extra) => {
157163
const context = extra.customContext as UserContext;
158164
if (!context.permissions.includes('required:permission')) {
@@ -164,9 +170,10 @@ async (params, extra) => {
164170

165171
### Client Side
166172

167-
The client adds the API key to all requests:
173+
The client adds authentication credentials to all requests:
168174

169175
```typescript
176+
// Example with API key (shown in demo)
170177
const transport = new StreamableHTTPClientTransport({
171178
url: serverUrl,
172179
fetch: async (url, options) => {
@@ -177,6 +184,18 @@ const transport = new StreamableHTTPClientTransport({
177184
return fetch(url, { ...options, headers });
178185
}
179186
});
187+
188+
// Example with MCP access token (OAuth flow)
189+
const transport = new StreamableHTTPClientTransport({
190+
url: serverUrl,
191+
fetch: async (url, options) => {
192+
const headers = {
193+
...options?.headers,
194+
'Authorization': `Bearer ${mcpAccessToken}`,
195+
};
196+
return fetch(url, { ...options, headers });
197+
}
198+
});
180199
```
181200

182201
## Available Test Users
@@ -190,7 +209,7 @@ const transport = new StreamableHTTPClientTransport({
190209

191210
## Key Features Demonstrated
192211

193-
1. **Authentication**: API key-based user authentication
212+
1. **Authentication**: Supports both API keys and MCP access tokens
194213
2. **Tool Context**: `get_user` tool accesses user context
195214
3. **Prompt Context**: `user-dashboard` prompt personalizes content based on context
196215
4. **Resource Context**: `user-profile` resource returns context-aware data

0 commit comments

Comments
 (0)