Skip to content

Commit 18caec1

Browse files
committed
add streamableHttp server support for everything server
1 parent 7fd336c commit 18caec1

File tree

2 files changed

+154
-2
lines changed

2 files changed

+154
-2
lines changed

src/everything/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,11 @@
1818
"prepare": "npm run build",
1919
"watch": "tsc --watch",
2020
"start": "node dist/index.js",
21-
"start:sse": "node dist/sse.js"
21+
"start:sse": "node dist/sse.js",
22+
"start:streamableHttp": "node dist/streamableHttp.js"
2223
},
2324
"dependencies": {
24-
"@modelcontextprotocol/sdk": "^1.9.0",
25+
"@modelcontextprotocol/sdk": "^1.10.1",
2526
"express": "^4.21.1",
2627
"zod": "^3.23.8",
2728
"zod-to-json-schema": "^3.23.5"

src/everything/streamableHttp.ts

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
2+
import { isInitializeRequest } from '@modelcontextprotocol/sdk/types.js';
3+
import { InMemoryEventStore } from '@modelcontextprotocol/sdk/examples/shared/inMemoryEventStore.js';
4+
import express, { Request, Response } from "express";
5+
import { createServer } from "./everything.js";
6+
import { randomUUID } from 'node:crypto';
7+
8+
const app = express();
9+
10+
app.use(express.json());
11+
12+
const { server, cleanup } = createServer();
13+
14+
const transports: { [sessionId: string]: StreamableHTTPServerTransport } = {};
15+
16+
app.post('/mcp', async (req: Request, res: Response) => {
17+
console.log('Received MCP request:', req.body);
18+
try {
19+
// Check for existing session ID
20+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
21+
let transport: StreamableHTTPServerTransport;
22+
23+
if (sessionId && transports[sessionId]) {
24+
// Reuse existing transport
25+
transport = transports[sessionId];
26+
} else if (!sessionId && isInitializeRequest(req.body)) {
27+
// New initialization request
28+
const eventStore = new InMemoryEventStore();
29+
transport = new StreamableHTTPServerTransport({
30+
sessionIdGenerator: () => randomUUID(),
31+
eventStore, // Enable resumability
32+
onsessioninitialized: (sessionId) => {
33+
// Store the transport by session ID when session is initialized
34+
// This avoids race conditions where requests might come in before the session is stored
35+
console.log(`Session initialized with ID: ${sessionId}`);
36+
transports[sessionId] = transport;
37+
}
38+
});
39+
40+
// Set up onclose handler to clean up transport when closed
41+
transport.onclose = () => {
42+
const sid = transport.sessionId;
43+
if (sid && transports[sid]) {
44+
console.log(`Transport closed for session ${sid}, removing from transports map`);
45+
delete transports[sid];
46+
}
47+
};
48+
49+
// Connect the transport to the MCP server BEFORE handling the request
50+
// so responses can flow back through the same transport
51+
await server.connect(transport);
52+
53+
await transport.handleRequest(req, res, req.body);
54+
return; // Already handled
55+
} else {
56+
// Invalid request - no session ID or not initialization request
57+
res.status(400).json({
58+
jsonrpc: '2.0',
59+
error: {
60+
code: -32000,
61+
message: 'Bad Request: No valid session ID provided',
62+
},
63+
id: null,
64+
});
65+
return;
66+
}
67+
68+
// Handle the request with existing transport - no need to reconnect
69+
// The existing transport is already connected to the server
70+
await transport.handleRequest(req, res, req.body);
71+
} catch (error) {
72+
console.error('Error handling MCP request:', error);
73+
if (!res.headersSent) {
74+
res.status(500).json({
75+
jsonrpc: '2.0',
76+
error: {
77+
code: -32603,
78+
message: 'Internal server error',
79+
},
80+
id: null,
81+
});
82+
}
83+
}
84+
});
85+
86+
// Handle GET requests for SSE streams (using built-in support from StreamableHTTP)
87+
app.get('/mcp', async (req: Request, res: Response) => {
88+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
89+
if (!sessionId || !transports[sessionId]) {
90+
res.status(400).send('Invalid or missing session ID');
91+
return;
92+
}
93+
94+
// Check for Last-Event-ID header for resumability
95+
const lastEventId = req.headers['last-event-id'] as string | undefined;
96+
if (lastEventId) {
97+
console.log(`Client reconnecting with Last-Event-ID: ${lastEventId}`);
98+
} else {
99+
console.log(`Establishing new SSE stream for session ${sessionId}`);
100+
}
101+
102+
const transport = transports[sessionId];
103+
await transport.handleRequest(req, res);
104+
});
105+
106+
// Handle DELETE requests for session termination (according to MCP spec)
107+
app.delete('/mcp', async (req: Request, res: Response) => {
108+
const sessionId = req.headers['mcp-session-id'] as string | undefined;
109+
if (!sessionId || !transports[sessionId]) {
110+
res.status(400).send('Invalid or missing session ID');
111+
return;
112+
}
113+
114+
console.log(`Received session termination request for session ${sessionId}`);
115+
116+
try {
117+
const transport = transports[sessionId];
118+
await transport.handleRequest(req, res);
119+
} catch (error) {
120+
console.error('Error handling session termination:', error);
121+
if (!res.headersSent) {
122+
res.status(500).send('Error processing session termination');
123+
}
124+
}
125+
});
126+
127+
// Start the server
128+
const PORT = process.env.PORT || 3001;
129+
app.listen(PORT, () => {
130+
console.log(`MCP Streamable HTTP Server listening on port ${PORT}`);
131+
});
132+
133+
// Handle server shutdown
134+
process.on('SIGINT', async () => {
135+
console.log('Shutting down server...');
136+
137+
// Close all active transports to properly clean up resources
138+
for (const sessionId in transports) {
139+
try {
140+
console.log(`Closing transport for session ${sessionId}`);
141+
await transports[sessionId].close();
142+
delete transports[sessionId];
143+
} catch (error) {
144+
console.error(`Error closing transport for session ${sessionId}:`, error);
145+
}
146+
}
147+
await cleanup();
148+
await server.close();
149+
console.log('Server shutdown complete');
150+
process.exit(0);
151+
});

0 commit comments

Comments
 (0)