|
| 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