Skip to content

Commit 3de139c

Browse files
committed
DH-21947_mcp-sessions/task-09: Clean up project: move migration plan and architecture docs back to docs/ folder for long-term reference. These documents provide valuable context for understanding the session-based architecture and should be preserved in the main docs directory.
Moved mcp-session-migration-plan.md and mcp-session-architecture.md to docs/ directory. No broken references found in docs/mcp.md. (#DH-21947)
1 parent 6b59ec3 commit 3de139c

2 files changed

Lines changed: 845 additions & 0 deletions

File tree

docs/mcp-session-architecture.md

Lines changed: 337 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,337 @@
1+
# MCP Session Architecture
2+
3+
## Understanding the Layers
4+
5+
There are distinct layers in the MCP server architecture:
6+
7+
### Layer 1: HTTP Server (Network Layer)
8+
9+
- **ONE** `http.Server` instance per VS Code extension
10+
- Listens on a **single port** (e.g., `http://localhost:45678/mcp`)
11+
- Receives all incoming HTTP requests
12+
- Routes requests based on session ID
13+
- Lives for the entire lifetime of the extension
14+
15+
### Layer 2: MCP SDK Server Instances (Protocol Layer)
16+
17+
- Handles MCP protocol logic (tool registration, request/response)
18+
- **Current (Stateless)**: ONE shared instance
19+
- **New (Stateful)**: MULTIPLE instances (one per session)
20+
21+
### Layer 3: Transport Layer
22+
23+
- Manages request/response streaming
24+
- **Current (Stateless)**: New transport per HTTP request
25+
- **New (Stateful)**: One transport per session, reused across requests
26+
27+
## Current Architecture (Stateless)
28+
29+
```
30+
┌─────────────────────────────────────────────────────┐
31+
│ VS Code Extension Process │
32+
│ │
33+
│ ┌────────────────────────────────────────────┐ │
34+
│ │ McpServer Class Instance │ │
35+
│ │ │ │
36+
│ │ http.Server (Port 45678) ◄───────────────┼────┼─── Client Request 1
37+
│ │ │ │ │
38+
│ │ │ │ │
39+
│ │ ├─► Create Transport 1 │ │
40+
│ │ │ Connect SdkMcpServer ──────┐ │ │
41+
│ │ │ Handle Request │ │ │
42+
│ │ │ Close Transport │ │ │
43+
│ │ │ │ │ │
44+
│ │ ├─► Create Transport 2 ◄───────────┼────┼─── Client Request 2
45+
│ │ │ Connect SdkMcpServer ──┐ │ │ │ (parallel)
46+
│ │ │ Handle Request │ │ │ │
47+
│ │ │ Close Transport │ │ │ │
48+
│ │ │ │ │ │ │
49+
│ │ SHARED SdkMcpServer Instance ◄──┴───┴────┼─── ⚠️ RACE CONDITION!
50+
│ │ (ONE instance, multiple connections) │ │
51+
│ │ │ │
52+
│ └────────────────────────────────────────────┘ │
53+
│ │
54+
└─────────────────────────────────────────────────────┘
55+
```
56+
57+
**Problem**: Multiple transports trying to connect to the **same** SDK server instance simultaneously.
58+
59+
## New Architecture (Stateful with Sessions)
60+
61+
```
62+
┌─────────────────────────────────────────────────────────────────┐
63+
│ VS Code Extension Process │
64+
│ │
65+
│ ┌──────────────────────────────────────────────────────────┐ │
66+
│ │ McpServer Class Instance │ │
67+
│ │ │ │
68+
│ │ http.Server (Port 45678) ◄─────────────────────────────┼──┼─── Initialize Request
69+
│ │ │ │ │ (no session ID)
70+
│ │ │ │ │
71+
│ │ ├─► Detect Initialize Request │ │
72+
│ │ │ Create NEW SdkMcpServer for Session A │ │
73+
│ │ │ Create Transport A │ │
74+
│ │ │ Connect Server A to Transport A (ONCE) │ │
75+
│ │ │ Store in Maps │ │
76+
│ │ │ Return session ID: "session-abc" │ │
77+
│ │ │ │ │
78+
│ │ ├─► Request with session-abc ◄──────────────────┼──┼─── Follow-up Request 1
79+
│ │ │ Lookup Transport A │ │
80+
│ │ │ Reuse (no new connection) │ │
81+
│ │ │ │ │
82+
│ │ ├─► Request with session-abc ◄──────────────────┼──┼─── Follow-up Request 2
83+
│ │ │ Lookup Transport A │ │ (parallel)
84+
│ │ │ Reuse (no new connection) │ │
85+
│ │ │ │ │
86+
│ │ ├─► Initialize Request (different client) ◄──────┼──┼─── New Session
87+
│ │ │ Create NEW SdkMcpServer for Session B │ │
88+
│ │ │ Create Transport B │ │
89+
│ │ │ Connect Server B to Transport B (ONCE) │ │
90+
│ │ │ Store in Maps │ │
91+
│ │ │ Return session ID: "session-xyz" │ │
92+
│ │ │ │ │
93+
│ │ Session Storage: │ │
94+
│ │ ┌─────────────────────────────────────────────┐ │ │
95+
│ │ │ transports Map │ │ │
96+
│ │ │ "session-abc" → Transport A ──┐ │ │ │
97+
│ │ │ "session-xyz" → Transport B ──┼─┐ │ │ │
98+
│ │ └──────────────────────────────────┼─┼─────────┘ │ │
99+
│ │ │ │ │ │
100+
│ │ ┌─────────────────────────────────┼─┼─────────┐ │ │
101+
│ │ │ servers Map │ │ │ │ │
102+
│ │ │ "session-abc" → SdkMcpServer A│ │ │ │ │
103+
│ │ │ "session-xyz" → SdkMcpServer B │ │ │ │
104+
│ │ └────────────────────────────────────┼─────────┘ │ │
105+
│ │ │ │ │
106+
│ │ SdkMcpServer Instance A ◄───────────┘ │ │
107+
│ │ (tools registered, connected to Transport A) │ │
108+
│ │ │ │
109+
│ │ SdkMcpServer Instance B ◄──────────────────────────┐ │ │
110+
│ │ (tools registered, connected to Transport B) │ │ │
111+
│ │ │ │ │
112+
│ └───────────────────────────────────────────────────────┘ │
113+
│ │
114+
└──────────────────────────────────────────────────────────────┘
115+
```
116+
117+
**Solution**: Each session has its own isolated SDK server + transport pair.
118+
119+
## Key Points
120+
121+
### 1. HTTP Server (Always ONE)
122+
123+
```typescript
124+
class McpServer {
125+
private httpServer: http.Server | null = null; // ← ONE instance
126+
127+
async start(port: number) {
128+
// Create ONE HTTP server that listens on ONE port
129+
this.httpServer = http.createServer(async (req, res) => {
130+
// Route to appropriate session based on session ID
131+
});
132+
133+
this.httpServer.listen(port);
134+
}
135+
}
136+
```
137+
138+
- **Never changes**: Always one HTTP server per extension instance
139+
- **Port**: Single port shared by all sessions
140+
- **Routing**: Uses `mcp-session-id` header to route to correct session
141+
142+
### 2. SDK Server Instances (ONE → MANY)
143+
144+
**Current (Stateless)**:
145+
146+
```typescript
147+
class McpServer {
148+
private server: SdkMcpServer; // ← Shared by all requests
149+
150+
constructor() {
151+
this.server = new SdkMcpServer({...});
152+
// Register tools once
153+
}
154+
}
155+
```
156+
157+
**New (Stateful)**:
158+
159+
```typescript
160+
class McpServer {
161+
private servers: Map<string, SdkMcpServer>; // ← One per session
162+
163+
private createServer(): SdkMcpServer {
164+
const server = new SdkMcpServer({...});
165+
// Register tools on this instance
166+
return server;
167+
}
168+
169+
async handleInitialize() {
170+
const server = this.createServer(); // New instance!
171+
const sessionId = randomUUID();
172+
this.servers.set(sessionId, server);
173+
}
174+
}
175+
```
176+
177+
### 3. Request Flow Examples
178+
179+
#### Example 1: Single Client, Multiple Requests
180+
181+
```
182+
Client 1 (VS Code Copilot)
183+
184+
├─► POST /mcp (initialize) ──┐
185+
│ No session ID │
186+
│ ← Response: session-abc │
187+
│ │ Same Session
188+
├─► POST /mcp (list tools) ──┤ Same Server Instance
189+
│ Session: session-abc │ Same Transport
190+
│ │
191+
├─► POST /mcp (call tool) ──┤
192+
│ Session: session-abc │
193+
│ │
194+
└─► POST /mcp (call tool) ──┘
195+
Session: session-abc
196+
```
197+
198+
**Result**:
199+
200+
- 4 HTTP requests → ONE HTTP server
201+
- 1 session → ONE SDK server instance
202+
- 1 session → ONE transport (reused 4 times)
203+
204+
#### Example 2: Multiple Clients (Parallel Sessions)
205+
206+
```
207+
Client 1 (VS Code) Client 2 (Windsurf)
208+
│ │
209+
├─► POST /mcp (init) ├─► POST /mcp (init)
210+
│ ← session-abc │ ← session-xyz
211+
│ │
212+
│ Different Sessions │
213+
│ Different SDK Servers │
214+
│ Isolated from each other │
215+
│ │
216+
├─► POST /mcp (tool) ├─► POST /mcp (tool)
217+
│ session-abc │ session-xyz
218+
│ │
219+
└─► POST /mcp (tool) └─► POST /mcp (tool)
220+
session-abc session-xyz
221+
```
222+
223+
**Result**:
224+
225+
- 6 HTTP requests → ONE HTTP server (handles all)
226+
- 2 sessions → TWO SDK server instances
227+
- 2 transports (one per session)
228+
229+
#### Example 3: Parallel Requests in Same Session
230+
231+
```
232+
Client (parallel tool calls)
233+
234+
├──┬─► POST /mcp (tool A) ─┐
235+
│ │ session-abc │
236+
│ │ ├─► Same Transport
237+
│ └─► POST /mcp (tool B) ─┘ Queued internally
238+
│ session-abc
239+
│ (parallel)
240+
```
241+
242+
**Result**:
243+
244+
- Both requests arrive at HTTP server simultaneously
245+
- Both lookup same transport from `transports.get("session-abc")`
246+
- Transport handles queueing internally
247+
- No race condition because server is already connected
248+
249+
## Why This Architecture?
250+
251+
### Network Constraints
252+
253+
- **ONE port per service**: Can't have multiple HTTP servers on same port
254+
- **Solution**: One HTTP server routes to multiple sessions
255+
256+
### Protocol Isolation
257+
258+
- **Sessions must be isolated**: Different clients shouldn't interfere
259+
- **Solution**: Separate SDK server instance per session
260+
261+
### Connection Stability
262+
263+
- **Avoid reconnection overhead**: `server.connect()` should happen once
264+
- **Solution**: Create connection during initialization, reuse transport
265+
266+
## Memory Implications
267+
268+
### Current (Stateless)
269+
270+
```
271+
Memory per request:
272+
- Transport: ~1KB
273+
- Connection overhead: ~100ms
274+
- Total: Minimal but inefficient (created/destroyed constantly)
275+
```
276+
277+
### New (Stateful)
278+
279+
```
280+
Memory per session:
281+
- SDK Server instance: ~50KB
282+
- Transport: ~1KB
283+
- Event store (optional): ~10KB
284+
- Total: ~60KB per active session
285+
286+
Typical usage:
287+
- 1-2 active sessions (one per IDE)
288+
- ~120KB total
289+
- Sessions cleaned up when idle
290+
```
291+
292+
**Trade-off**: Slightly more memory for much better performance and new capabilities.
293+
294+
## Implementation Details
295+
296+
### HTTP Request Handler Pseudocode
297+
298+
```typescript
299+
this.httpServer = http.createServer(async (req, res) => {
300+
// Extract session ID from header
301+
const sessionId = req.headers['mcp-session-id'];
302+
303+
if (sessionId && this.transports.has(sessionId)) {
304+
// EXISTING SESSION: Reuse transport
305+
const transport = this.transports.get(sessionId);
306+
await transport.handleRequest(req, res, body);
307+
} else if (!sessionId && isInitializeRequest(body)) {
308+
// NEW SESSION: Create server + transport
309+
const server = this.createServer(); // New SdkMcpServer
310+
const transport = new StreamableHTTPServerTransport({
311+
sessionIdGenerator: () => randomUUID(),
312+
onsessioninitialized: sessionId => {
313+
this.servers.set(sessionId, server);
314+
this.transports.set(sessionId, transport);
315+
},
316+
});
317+
318+
await server.connect(transport); // Connect ONCE
319+
await transport.handleRequest(req, res, body);
320+
} else {
321+
// ERROR: Invalid request
322+
res.status(400).json({ error: 'Invalid session' });
323+
}
324+
});
325+
```
326+
327+
## Summary
328+
329+
| Layer | Current (Stateless) | New (Stateful) |
330+
| --------------- | ------------------- | --------------------------- |
331+
| **HTTP Server** | 1 instance | 1 instance (no change) |
332+
| **Port** | 1 port | 1 port (no change) |
333+
| **SDK Servers** | 1 shared instance | N instances (1 per session) |
334+
| **Transports** | 1 per request | 1 per session |
335+
| **Connections** | N per request | 1 per session |
336+
337+
The HTTP server is just a **router** - it receives requests and dispatches them to the appropriate session's SDK server instance based on the session ID.

0 commit comments

Comments
 (0)