Skip to content

Commit 37cf628

Browse files
committed
Adding support for streamable http transport
1 parent 9d421d4 commit 37cf628

File tree

3 files changed

+303
-21
lines changed

3 files changed

+303
-21
lines changed

Dockerfile

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
FROM node:lts-alpine
2+
3+
WORKDIR /usr/src/app
4+
5+
# Copy package.json and package-lock.json
6+
COPY *.json ./
7+
8+
# Install dependencies without triggering any unwanted scripts
9+
RUN npm install --ignore-scripts
10+
11+
# Copy all source code
12+
COPY src ./src
13+
14+
# Build the application
15+
RUN npm run build
16+
17+
# Environment variables for configuration
18+
ENV MCP_PORT="3000"
19+
20+
# Expose the default port
21+
EXPOSE ${MCP_PORT}
22+
23+
# Command to run the server
24+
CMD [ "node", "build/index.js" , "--http"]

build/index.js

Lines changed: 135 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
#!/usr/bin/env node
22
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
45
import { z } from "zod";
56
import { pino } from 'pino';
67
import readline from 'readline';
78
import { fileURLToPath } from 'url';
89
import { dirname, join } from 'path';
910
import { readFileSync } from 'fs';
11+
import { createServer } from 'http';
12+
import { randomUUID } from 'crypto';
13+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
1014
const __filename = fileURLToPath(import.meta.url);
1115
const __dirname = dirname(__filename);
1216
// Extract version from package.json
@@ -56,14 +60,17 @@ const SOCKET_HEADERS = {
5660
"content-type": "application/json",
5761
"authorization": `Bearer ${SOCKET_API_KEY}`
5862
};
63+
// Transport management
64+
const transports = {};
5965
// Create server instance
6066
const server = new McpServer({
6167
name: "socket",
6268
version: VERSION,
63-
description: "Socket MCP server",
69+
description: "Socket MCP server with streamable HTTP support",
6470
capabilities: {
6571
resources: {},
6672
tools: {},
73+
streaming: {}
6774
},
6875
});
6976
server.tool("depscore", "Get the dependency score of packages with the `depscore` tool from Socket. Use 'unknown' for version if not known. Use this tool to scan dependencies for their quality and security on existing code or when code is generated. Stop generating code and ask the user how to proceed when any of the scores are low. When checking dependencies, make sure to also check the imports in the code, not just the manifest files (pyproject.toml, package.json, etc).", {
@@ -185,13 +192,130 @@ server.tool("depscore", "Get the dependency score of packages with the `depscore
185192
};
186193
}
187194
});
188-
// Create a stdio transport and start the server
189-
const transport = new StdioServerTransport();
190-
server.connect(transport)
191-
.then(() => {
192-
logger.info(`Socket MCP server version ${VERSION} started successfully`);
193-
})
194-
.catch((error) => {
195-
logger.error(`Failed to start Socket MCP server: ${error.message}`);
196-
process.exit(1);
197-
});
195+
// Determine transport mode from environment or arguments
196+
const useHttp = process.env.MCP_HTTP_MODE === 'true' || process.argv.includes('--http');
197+
const port = parseInt(process.env.MCP_PORT || '3000', 10);
198+
if (useHttp) {
199+
// HTTP mode with Server-Sent Events
200+
logger.info(`Starting HTTP server on port ${port}`);
201+
const httpServer = createServer(async (req, res) => {
202+
// Enable CORS
203+
res.setHeader('Access-Control-Allow-Origin', '*');
204+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
205+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
206+
if (req.method === 'OPTIONS') {
207+
res.writeHead(200);
208+
res.end();
209+
return;
210+
}
211+
const url = new URL(req.url, `http://localhost:${port}`);
212+
if (url.pathname === '/mcp') {
213+
if (req.method === 'POST') {
214+
// Handle JSON-RPC messages
215+
let body = '';
216+
req.on('data', chunk => body += chunk);
217+
req.on('end', async () => {
218+
try {
219+
const jsonData = JSON.parse(body);
220+
const sessionId = req.headers['mcp-session-id'];
221+
let transport;
222+
if (sessionId && transports[sessionId]) {
223+
// Reuse existing transport
224+
transport = transports[sessionId];
225+
}
226+
else if (!sessionId && isInitializeRequest(jsonData)) {
227+
// New initialization request
228+
transport = new StreamableHTTPServerTransport({
229+
sessionIdGenerator: () => randomUUID(),
230+
onsessioninitialized: (id) => {
231+
transports[id] = transport;
232+
logger.info(`Session initialized: ${id}`);
233+
}
234+
});
235+
transport.onclose = () => {
236+
const sid = transport.sessionId;
237+
if (sid && transports[sid]) {
238+
delete transports[sid];
239+
logger.info(`Session closed: ${sid}`);
240+
}
241+
};
242+
await server.connect(transport);
243+
await transport.handleRequest(req, res, jsonData);
244+
return;
245+
}
246+
else {
247+
// Invalid request
248+
res.writeHead(400);
249+
res.end(JSON.stringify({
250+
jsonrpc: '2.0',
251+
error: { code: -32000, message: 'Bad Request: No valid session ID' },
252+
id: null
253+
}));
254+
return;
255+
}
256+
// Handle request with existing transport
257+
await transport.handleRequest(req, res, jsonData);
258+
}
259+
catch (error) {
260+
logger.error(`Error processing POST request: ${error}`);
261+
if (!res.headersSent) {
262+
res.writeHead(500);
263+
res.end(JSON.stringify({
264+
jsonrpc: '2.0',
265+
error: { code: -32603, message: 'Internal server error' },
266+
id: null
267+
}));
268+
}
269+
}
270+
});
271+
}
272+
else if (req.method === 'GET') {
273+
// Handle SSE streams
274+
const sessionId = req.headers['mcp-session-id'];
275+
if (!sessionId || !transports[sessionId]) {
276+
res.writeHead(400);
277+
res.end('Invalid or missing session ID');
278+
return;
279+
}
280+
const transport = transports[sessionId];
281+
await transport.handleRequest(req, res);
282+
}
283+
else if (req.method === 'DELETE') {
284+
// Handle session termination
285+
const sessionId = req.headers['mcp-session-id'];
286+
if (!sessionId || !transports[sessionId]) {
287+
res.writeHead(400);
288+
res.end('Invalid or missing session ID');
289+
return;
290+
}
291+
const transport = transports[sessionId];
292+
await transport.handleRequest(req, res);
293+
}
294+
else {
295+
res.writeHead(405);
296+
res.end('Method not allowed');
297+
}
298+
}
299+
else {
300+
res.writeHead(404);
301+
res.end('Not found');
302+
}
303+
});
304+
httpServer.listen(port, () => {
305+
logger.info(`Socket MCP HTTP server started successfully on port ${port}`);
306+
logger.info(`Connect to: http://localhost:${port}/mcp`);
307+
});
308+
}
309+
else {
310+
// Stdio mode (default)
311+
logger.info("Starting in stdio mode");
312+
const transport = new StdioServerTransport();
313+
server.connect(transport)
314+
.then(() => {
315+
logger.info(`Socket MCP server version ${VERSION} started successfully`);
316+
})
317+
.catch((error) => {
318+
logger.error(`Failed to start Socket MCP server: ${error.message}`);
319+
process.exit(1);
320+
});
321+
}

src/index.ts

Lines changed: 144 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
#!/usr/bin/env node
22
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
33
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
4+
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
45
import { z } from "zod";
56
import { pino } from 'pino';
67
import readline from 'readline';
78
import { fileURLToPath } from 'url';
89
import { dirname, join } from 'path';
910
import { readFileSync } from 'fs';
11+
import { createServer } from 'http';
12+
import { randomUUID } from 'crypto';
13+
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
1014

1115
const __filename = fileURLToPath(import.meta.url);
1216
const __dirname = dirname(__filename);
@@ -66,6 +70,9 @@ const SOCKET_HEADERS = {
6670
"authorization": `Bearer ${SOCKET_API_KEY}`
6771
};
6872

73+
// Transport management
74+
const transports: Record<string, StreamableHTTPServerTransport> = {};
75+
6976
// Create server instance
7077
const server = new McpServer({
7178
name: "socket",
@@ -74,6 +81,7 @@ const server = new McpServer({
7481
capabilities: {
7582
resources: {},
7683
tools: {},
84+
streaming: {}
7785
},
7886
});
7987

@@ -209,13 +217,139 @@ server.tool(
209217
);
210218

211219

212-
// Create a stdio transport and start the server
213-
const transport = new StdioServerTransport();
214-
server.connect(transport)
215-
.then(() => {
216-
logger.info(`Socket MCP server version ${VERSION} started successfully`);
217-
})
218-
.catch((error: Error) => {
219-
logger.error(`Failed to start Socket MCP server: ${error.message}`);
220-
process.exit(1);
221-
});
220+
// Determine transport mode from environment or arguments
221+
const useHttp = process.env.MCP_HTTP_MODE === 'true' || process.argv.includes('--http');
222+
const port = parseInt(process.env.MCP_PORT || '3000', 10);
223+
224+
if (useHttp) {
225+
// HTTP mode with Server-Sent Events
226+
logger.info(`Starting HTTP server on port ${port}`);
227+
228+
const httpServer = createServer(async (req, res) => {
229+
// Enable CORS
230+
res.setHeader('Access-Control-Allow-Origin', '*');
231+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
232+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
233+
234+
if (req.method === 'OPTIONS') {
235+
res.writeHead(200);
236+
res.end();
237+
return;
238+
}
239+
240+
const url = new URL(req.url!, `http://localhost:${port}`);
241+
242+
if (url.pathname === '/mcp') {
243+
if (req.method === 'POST') {
244+
// Handle JSON-RPC messages
245+
let body = '';
246+
req.on('data', chunk => body += chunk);
247+
req.on('end', async () => {
248+
try {
249+
const jsonData = JSON.parse(body);
250+
const sessionId = req.headers['mcp-session-id'] as string;
251+
252+
let transport: StreamableHTTPServerTransport;
253+
254+
if (sessionId && transports[sessionId]) {
255+
// Reuse existing transport
256+
transport = transports[sessionId];
257+
} else if (!sessionId && isInitializeRequest(jsonData)) {
258+
// New initialization request
259+
transport = new StreamableHTTPServerTransport({
260+
sessionIdGenerator: () => randomUUID(),
261+
onsessioninitialized: (id) => {
262+
transports[id] = transport;
263+
logger.info(`Session initialized: ${id}`);
264+
}
265+
});
266+
267+
transport.onclose = () => {
268+
const sid = transport.sessionId;
269+
if (sid && transports[sid]) {
270+
delete transports[sid];
271+
logger.info(`Session closed: ${sid}`);
272+
}
273+
};
274+
275+
await server.connect(transport);
276+
await transport.handleRequest(req, res, jsonData);
277+
return;
278+
} else {
279+
// Invalid request
280+
res.writeHead(400);
281+
res.end(JSON.stringify({
282+
jsonrpc: '2.0',
283+
error: { code: -32000, message: 'Bad Request: No valid session ID' },
284+
id: null
285+
}));
286+
return;
287+
}
288+
289+
// Handle request with existing transport
290+
await transport.handleRequest(req, res, jsonData);
291+
} catch (error) {
292+
logger.error(`Error processing POST request: ${error}`);
293+
if (!res.headersSent) {
294+
res.writeHead(500);
295+
res.end(JSON.stringify({
296+
jsonrpc: '2.0',
297+
error: { code: -32603, message: 'Internal server error' },
298+
id: null
299+
}));
300+
}
301+
}
302+
});
303+
304+
} else if (req.method === 'GET') {
305+
// Handle SSE streams
306+
const sessionId = req.headers['mcp-session-id'] as string;
307+
if (!sessionId || !transports[sessionId]) {
308+
res.writeHead(400);
309+
res.end('Invalid or missing session ID');
310+
return;
311+
}
312+
313+
const transport = transports[sessionId];
314+
await transport.handleRequest(req, res);
315+
316+
} else if (req.method === 'DELETE') {
317+
// Handle session termination
318+
const sessionId = req.headers['mcp-session-id'] as string;
319+
if (!sessionId || !transports[sessionId]) {
320+
res.writeHead(400);
321+
res.end('Invalid or missing session ID');
322+
return;
323+
}
324+
325+
const transport = transports[sessionId];
326+
await transport.handleRequest(req, res);
327+
328+
} else {
329+
res.writeHead(405);
330+
res.end('Method not allowed');
331+
}
332+
} else {
333+
res.writeHead(404);
334+
res.end('Not found');
335+
}
336+
});
337+
338+
httpServer.listen(port, () => {
339+
logger.info(`Socket MCP HTTP server started successfully on port ${port}`);
340+
logger.info(`Connect to: http://localhost:${port}/mcp`);
341+
});
342+
343+
} else {
344+
// Stdio mode (default)
345+
logger.info("Starting in stdio mode");
346+
const transport = new StdioServerTransport();
347+
server.connect(transport)
348+
.then(() => {
349+
logger.info(`Socket MCP server version ${VERSION} started successfully`);
350+
})
351+
.catch((error: Error) => {
352+
logger.error(`Failed to start Socket MCP server: ${error.message}`);
353+
process.exit(1);
354+
});
355+
}

0 commit comments

Comments
 (0)