Skip to content

Commit 1dafa92

Browse files
stefanskiasanclaude
andcommitted
feat: Add SessionStore interface for distributed session management
This adds native support for external session storage in StreamableHTTPServerTransport, enabling multi-pod/multi-node deployments where session state must be shared. Changes: - Add SessionStore interface with store/get/update/delete/exists methods - Add SessionData interface for session metadata - Add SessionStorageMode type ('memory' | 'external') - Add sessionStorageMode option to transport options (default: 'memory') - Add sessionStore option for external session storage implementation - Add RedisSessionStore implementation example - Add InMemorySessionStore for development/testing - Add runtime getters: sessionStorageMode, isUsingExternalSessionStore - Validation: throws error if mode='external' without sessionStore Usage: ```typescript // Memory mode (default) new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), sessionStorageMode: 'memory' }); // External mode (Redis) new StreamableHTTPServerTransport({ sessionIdGenerator: () => randomUUID(), sessionStorageMode: 'external', sessionStore: new RedisSessionStore({ redis, ttlSeconds: 3600 }) }); ``` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 4fb4d4d commit 1dafa92

File tree

6 files changed

+819
-17
lines changed

6 files changed

+819
-17
lines changed

SESSIONSTORE.md

Lines changed: 244 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,244 @@
1+
# SessionStore Extension for Multi-Pod Deployments
2+
3+
This fork adds native support for distributed session storage in the MCP SDK, enabling multi-pod/multi-node deployments where session state must be shared across server instances.
4+
5+
## Quick Start
6+
7+
```typescript
8+
import { StreamableHTTPServerTransport, RedisSessionStore } from '@anthropic-advisori/mcp-sdk/server';
9+
import Redis from 'ioredis';
10+
11+
// Single-pod mode (default) - sessions in memory
12+
const singlePodTransport = new StreamableHTTPServerTransport({
13+
sessionIdGenerator: () => randomUUID(),
14+
sessionStorageMode: 'memory' // This is the default
15+
});
16+
17+
// Multi-pod mode - sessions in Redis
18+
const multiPodTransport = new StreamableHTTPServerTransport({
19+
sessionIdGenerator: () => randomUUID(),
20+
sessionStorageMode: 'external', // ← Explicit mode selection
21+
sessionStore: new RedisSessionStore({
22+
redis: new Redis(),
23+
ttlSeconds: 3600
24+
})
25+
});
26+
```
27+
28+
## The Problem
29+
30+
The official `@modelcontextprotocol/sdk` stores session state **in-memory**:
31+
32+
```typescript
33+
// Original SDK - sessions are local to each process
34+
private _initialized: boolean = false;
35+
sessionId?: string;
36+
```
37+
38+
This means:
39+
- Sessions cannot be shared across multiple pods/containers
40+
- Load balancers routing requests to different instances will fail
41+
- Sessions are lost on server restart
42+
43+
## The Solution: SessionStorageMode + SessionStore Interface
44+
45+
We've added two new options to `StreamableHTTPServerTransport`:
46+
47+
### SessionStorageMode
48+
49+
```typescript
50+
type SessionStorageMode = 'memory' | 'external';
51+
```
52+
53+
| Mode | Description | Use Case |
54+
|------|-------------|----------|
55+
| `memory` | Sessions in process memory (default) | Single-pod deployments, development |
56+
| `external` | Sessions in external store | Multi-pod deployments, production clusters |
57+
58+
### SessionStore Interface
59+
60+
```typescript
61+
export interface SessionStore {
62+
storeSession(sessionId: string, data: SessionData): Promise<void>;
63+
getSession(sessionId: string): Promise<SessionData | null>;
64+
updateSessionActivity(sessionId: string): Promise<void>;
65+
deleteSession(sessionId: string): Promise<void>;
66+
sessionExists(sessionId: string): Promise<boolean>;
67+
}
68+
69+
export interface SessionData {
70+
sessionId: string;
71+
initialized: boolean;
72+
createdAt: number;
73+
lastActivity: number;
74+
metadata?: Record<string, unknown>;
75+
}
76+
```
77+
78+
## Usage
79+
80+
### Memory Mode (Default - Single Pod)
81+
82+
```typescript
83+
import { StreamableHTTPServerTransport } from '@anthropic-advisori/mcp-sdk/server';
84+
import { randomUUID } from 'crypto';
85+
86+
// Memory mode is the default - no external dependencies needed
87+
const transport = new StreamableHTTPServerTransport({
88+
sessionIdGenerator: () => randomUUID(),
89+
// sessionStorageMode: 'memory' is implicit
90+
});
91+
```
92+
93+
### External Mode with Redis (Multi-Pod)
94+
95+
```typescript
96+
import Redis from 'ioredis';
97+
import { StreamableHTTPServerTransport, RedisSessionStore } from '@anthropic-advisori/mcp-sdk/server';
98+
import { randomUUID } from 'crypto';
99+
100+
// Create Redis client
101+
const redis = new Redis({
102+
host: 'redis.example.com',
103+
port: 6379,
104+
password: 'your-password'
105+
});
106+
107+
// Create session store with 1-hour TTL
108+
const sessionStore = new RedisSessionStore({
109+
redis,
110+
keyPrefix: 'mcp:session:',
111+
ttlSeconds: 3600
112+
});
113+
114+
// Create transport with external session storage
115+
const transport = new StreamableHTTPServerTransport({
116+
sessionIdGenerator: () => randomUUID(),
117+
sessionStorageMode: 'external', // ← Explicitly enable external storage
118+
sessionStore // ← Required when mode is 'external'
119+
});
120+
121+
// Check mode at runtime
122+
console.log(transport.sessionStorageMode); // 'external'
123+
console.log(transport.isUsingExternalSessionStore); // true
124+
```
125+
126+
**Important**: When `sessionStorageMode` is `'external'`, you MUST provide a `sessionStore`. Otherwise, an error will be thrown at construction time.
127+
128+
### Custom Session Store Implementation
129+
130+
Implement the `SessionStore` interface for any backend:
131+
132+
```typescript
133+
import { SessionStore, SessionData } from '@anthropic-advisori/mcp-sdk/server';
134+
135+
class PostgresSessionStore implements SessionStore {
136+
constructor(private pool: Pool) {}
137+
138+
async storeSession(sessionId: string, data: SessionData): Promise<void> {
139+
await this.pool.query(
140+
`INSERT INTO mcp_sessions (id, data, expires_at)
141+
VALUES ($1, $2, NOW() + INTERVAL '1 hour')
142+
ON CONFLICT (id) DO UPDATE SET data = $2, expires_at = NOW() + INTERVAL '1 hour'`,
143+
[sessionId, JSON.stringify(data)]
144+
);
145+
}
146+
147+
async getSession(sessionId: string): Promise<SessionData | null> {
148+
const result = await this.pool.query(
149+
`SELECT data FROM mcp_sessions WHERE id = $1 AND expires_at > NOW()`,
150+
[sessionId]
151+
);
152+
return result.rows[0]?.data ?? null;
153+
}
154+
155+
// ... implement other methods
156+
}
157+
```
158+
159+
## Backward Compatibility
160+
161+
This is a **non-breaking change**. When `sessionStore` is not provided, the transport behaves exactly as before with in-memory sessions.
162+
163+
| Scenario | Behavior |
164+
|----------|----------|
165+
| `sessionStore` not provided | In-memory sessions (original behavior) |
166+
| `sessionStore` provided | External session storage (Redis, DB, etc.) |
167+
168+
## Key Features
169+
170+
### Cross-Pod Session Recovery
171+
172+
When a request arrives at a different pod than where the session was created, the transport automatically recovers the session from the store:
173+
174+
```typescript
175+
// In validateSession():
176+
if (this._sessionStore) {
177+
const sessionData = await this._sessionStore.getSession(requestSessionId);
178+
if (sessionData) {
179+
// Recover session locally
180+
this.sessionId = requestSessionId;
181+
this._initialized = true;
182+
}
183+
}
184+
```
185+
186+
### Automatic TTL Refresh
187+
188+
Session activity updates refresh the TTL in the store:
189+
190+
```typescript
191+
// On every request:
192+
if (this._sessionStore && this.sessionId) {
193+
await this._sessionStore.updateSessionActivity(this.sessionId);
194+
}
195+
```
196+
197+
## Included Implementations
198+
199+
### RedisSessionStore
200+
201+
Production-ready Redis implementation with:
202+
- Configurable key prefix
203+
- Configurable TTL
204+
- Automatic activity-based TTL refresh
205+
- Logging callback support
206+
207+
### InMemorySessionStore
208+
209+
For development/testing only - NOT suitable for multi-pod production deployments.
210+
211+
## Migration from Custom Workarounds
212+
213+
If you've implemented custom session recovery (like we did in mcp_virtualserver), you can now remove that code and use native SessionStore:
214+
215+
**Before (with workarounds):**
216+
```typescript
217+
// Custom session recovery logic...
218+
const redisSession = await this.sessionStore.getSession(sessionId);
219+
if (redisSession) {
220+
// Manually reconstruct transport...
221+
(transport as any)._initialized = true;
222+
(transport as any).sessionId = sessionId;
223+
}
224+
```
225+
226+
**After (native support):**
227+
```typescript
228+
const transport = new StreamableHTTPServerTransport({
229+
sessionIdGenerator: () => randomUUID(),
230+
sessionStore: myRedisSessionStore // That's it!
231+
});
232+
```
233+
234+
## PR to Upstream
235+
236+
This feature has been submitted as a PR to the official SDK repository. Until it's merged, use this fork:
237+
238+
```bash
239+
npm install @anthropic-advisori/mcp-sdk
240+
```
241+
242+
## License
243+
244+
MIT (same as original SDK)

package.json

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,18 @@
11
{
2-
"name": "@modelcontextprotocol/sdk",
3-
"version": "1.23.0",
4-
"description": "Model Context Protocol implementation for TypeScript",
2+
"name": "@anthropic-advisori/mcp-sdk",
3+
"version": "1.23.0-sessionstore.1",
4+
"description": "Model Context Protocol implementation for TypeScript - Fork with SessionStore support for distributed deployments",
55
"license": "MIT",
66
"author": "Anthropic, PBC (https://anthropic.com)",
7+
"contributors": [
8+
"Advisori GmbH - SessionStore extension for multi-pod support"
9+
],
710
"homepage": "https://modelcontextprotocol.io",
811
"bugs": "https://github.com/modelcontextprotocol/typescript-sdk/issues",
912
"type": "module",
1013
"repository": {
1114
"type": "git",
12-
"url": "git+https://github.com/modelcontextprotocol/typescript-sdk.git"
15+
"url": "git+https://github.com/anthropic-advisori/mcp-typescript-sdk.git"
1316
},
1417
"engines": {
1518
"node": ">=18"
@@ -51,6 +54,14 @@
5154
"import": "./dist/esm/experimental/tasks/index.js",
5255
"require": "./dist/cjs/experimental/tasks/index.js"
5356
},
57+
"./server/session-stores": {
58+
"import": "./dist/esm/server/session-stores/index.js",
59+
"require": "./dist/cjs/server/session-stores/index.js"
60+
},
61+
"./server/session-stores/redis": {
62+
"import": "./dist/esm/server/session-stores/redis.js",
63+
"require": "./dist/cjs/server/session-stores/redis.js"
64+
},
5465
"./*": {
5566
"import": "./dist/esm/*",
5667
"require": "./dist/cjs/*"

src/server/index.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -633,3 +633,8 @@ export class Server<
633633
return this.notification({ method: 'notifications/prompts/list_changed' });
634634
}
635635
}
636+
637+
// Re-export session store types and implementations
638+
export type { SessionStore, SessionData, SessionStorageMode } from './streamableHttp.js';
639+
export { RedisSessionStore, InMemorySessionStore } from './session-stores/redis.js';
640+
export type { RedisClient, RedisSessionStoreOptions } from './session-stores/redis.js';

src/server/session-stores/index.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
/**
2+
* Session Store implementations for distributed MCP deployments
3+
*/
4+
5+
export { RedisSessionStore, InMemorySessionStore } from './redis.js';
6+
export type { RedisClient, RedisSessionStoreOptions } from './redis.js';

0 commit comments

Comments
 (0)