|
1 | 1 | #!/usr/bin/env node
|
2 | 2 | import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
3 | 3 | import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
| 4 | +import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; |
4 | 5 | import { z } from "zod";
|
5 | 6 | import { pino } from 'pino';
|
6 | 7 | import readline from 'readline';
|
7 | 8 | import { fileURLToPath } from 'url';
|
8 | 9 | import { dirname, join } from 'path';
|
9 | 10 | import { readFileSync } from 'fs';
|
| 11 | +import { createServer } from 'http'; |
| 12 | +import { randomUUID } from 'crypto'; |
| 13 | +import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js"; |
10 | 14 | const __filename = fileURLToPath(import.meta.url);
|
11 | 15 | const __dirname = dirname(__filename);
|
12 | 16 | // Extract version from package.json
|
@@ -56,14 +60,17 @@ const SOCKET_HEADERS = {
|
56 | 60 | "content-type": "application/json",
|
57 | 61 | "authorization": `Bearer ${SOCKET_API_KEY}`
|
58 | 62 | };
|
| 63 | +// Transport management |
| 64 | +const transports = {}; |
59 | 65 | // Create server instance
|
60 | 66 | const server = new McpServer({
|
61 | 67 | name: "socket",
|
62 | 68 | version: VERSION,
|
63 |
| - description: "Socket MCP server", |
| 69 | + description: "Socket MCP server with streamable HTTP support", |
64 | 70 | capabilities: {
|
65 | 71 | resources: {},
|
66 | 72 | tools: {},
|
| 73 | + streaming: {} |
67 | 74 | },
|
68 | 75 | });
|
69 | 76 | 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
|
185 | 192 | };
|
186 | 193 | }
|
187 | 194 | });
|
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 | +} |
0 commit comments