Skip to content

Commit 2a4d6ce

Browse files
committed
1 parent 59b923c commit 2a4d6ce

File tree

1 file changed

+99
-5
lines changed

1 file changed

+99
-5
lines changed

src/index.ts

Lines changed: 99 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -242,10 +242,37 @@ if (useHttp) {
242242
logger.info(`Starting HTTP server on port ${port}`);
243243

244244
const httpServer = createServer(async (req, res) => {
245-
// Enable CORS
246-
res.setHeader('Access-Control-Allow-Origin', '*');
245+
// Validate Origin header as required by MCP spec
246+
const origin = req.headers.origin;
247+
const allowedOrigins = [
248+
'http://localhost:3000',
249+
'http://127.0.0.1:3000',
250+
'https://mcp.socket.dev',
251+
'https://mcp.socket-staging.dev'
252+
];
253+
254+
const isValidOrigin = !origin || allowedOrigins.includes(origin);
255+
256+
if (origin && !isValidOrigin) {
257+
logger.warn(`Rejected request from invalid origin: ${origin}`);
258+
res.writeHead(403, { 'Content-Type': 'application/json' });
259+
res.end(JSON.stringify({
260+
jsonrpc: '2.0',
261+
error: { code: -32000, message: 'Forbidden: Invalid origin' },
262+
id: null
263+
}));
264+
return;
265+
}
266+
267+
// Set CORS headers for valid origins
268+
if (origin && isValidOrigin) {
269+
res.setHeader('Access-Control-Allow-Origin', origin);
270+
} else {
271+
res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000');
272+
}
247273
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
248-
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id');
274+
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, mcp-session-id, Accept, Last-Event-ID');
275+
res.setHeader('Access-Control-Expose-Headers', 'mcp-session-id');
249276

250277
if (req.method === 'OPTIONS') {
251278
res.writeHead(200);
@@ -269,6 +296,19 @@ if (useHttp) {
269296

270297
if (url.pathname === '/') {
271298
if (req.method === 'POST') {
299+
// Validate Accept header as required by MCP spec
300+
const acceptHeader = req.headers.accept;
301+
if (!acceptHeader || (!acceptHeader.includes('application/json') && !acceptHeader.includes('text/event-stream'))) {
302+
logger.warn(`Invalid Accept header: ${acceptHeader}`);
303+
res.writeHead(400, { 'Content-Type': 'application/json' });
304+
res.end(JSON.stringify({
305+
jsonrpc: '2.0',
306+
error: { code: -32000, message: 'Bad Request: Accept header must include application/json or text/event-stream' },
307+
id: null
308+
}));
309+
return;
310+
}
311+
272312
// Handle JSON-RPC messages
273313
let body = '';
274314
req.on('data', chunk => body += chunk);
@@ -277,6 +317,18 @@ if (useHttp) {
277317
const jsonData = JSON.parse(body);
278318
const sessionId = req.headers['mcp-session-id'] as string;
279319

320+
// Validate session ID format if provided (must contain only visible ASCII characters)
321+
if (sessionId && !/^[\x21-\x7E]+$/.test(sessionId)) {
322+
logger.warn(`Invalid session ID format: ${sessionId}`);
323+
res.writeHead(400, { 'Content-Type': 'application/json' });
324+
res.end(JSON.stringify({
325+
jsonrpc: '2.0',
326+
error: { code: -32000, message: 'Bad Request: Session ID must contain only visible ASCII characters' },
327+
id: jsonData.id || null
328+
}));
329+
return;
330+
}
331+
280332
let transport: StreamableHTTPServerTransport;
281333

282334
if (sessionId && transports[sessionId]) {
@@ -298,6 +350,8 @@ if (useHttp) {
298350
onsessioninitialized: (id) => {
299351
transports[id] = transport;
300352
logger.info(`Session initialized: ${id}`);
353+
// Set session ID in response headers as required by MCP spec
354+
res.setHeader('mcp-session-id', id);
301355
}
302356
});
303357

@@ -343,14 +397,54 @@ if (useHttp) {
343397
});
344398

345399
} else if (req.method === 'GET') {
400+
// Validate Accept header for SSE as required by MCP spec
401+
const acceptHeader = req.headers.accept;
402+
if (!acceptHeader || !acceptHeader.includes('text/event-stream')) {
403+
logger.warn(`GET request without text/event-stream Accept header: ${acceptHeader}`);
404+
res.writeHead(405, { 'Content-Type': 'application/json' });
405+
res.end(JSON.stringify({
406+
jsonrpc: '2.0',
407+
error: { code: -32000, message: 'Method Not Allowed: GET requires Accept: text/event-stream' },
408+
id: null
409+
}));
410+
return;
411+
}
412+
346413
// Handle SSE streams
347414
const sessionId = req.headers['mcp-session-id'] as string;
415+
416+
// Validate session ID format
417+
if (sessionId && !/^[\x21-\x7E]+$/.test(sessionId)) {
418+
logger.warn(`Invalid session ID format in GET request: ${sessionId}`);
419+
res.writeHead(400, { 'Content-Type': 'application/json' });
420+
res.end(JSON.stringify({
421+
jsonrpc: '2.0',
422+
error: { code: -32000, message: 'Bad Request: Session ID must contain only visible ASCII characters' },
423+
id: null
424+
}));
425+
return;
426+
}
427+
348428
if (!sessionId || !transports[sessionId]) {
349-
res.writeHead(400);
350-
res.end('Invalid or missing session ID');
429+
logger.warn(`SSE request with invalid session ID: ${sessionId}`);
430+
res.writeHead(400, { 'Content-Type': 'application/json' });
431+
res.end(JSON.stringify({
432+
jsonrpc: '2.0',
433+
error: { code: -32000, message: 'Bad Request: Invalid or missing session ID for SSE stream' },
434+
id: null
435+
}));
351436
return;
352437
}
353438

439+
// Check for Last-Event-ID header for resumability (optional MCP feature)
440+
const lastEventId = req.headers['last-event-id'] as string;
441+
if (lastEventId) {
442+
logger.info(`SSE resumability requested with Last-Event-ID: ${lastEventId}`);
443+
// Note: Actual resumability implementation would require message storage
444+
// For now, we log the request but don't implement full resumability
445+
}
446+
447+
// Let the transport handle SSE headers and response
354448
const transport = transports[sessionId];
355449
await transport.handleRequest(req, res);
356450

0 commit comments

Comments
 (0)