import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"
import * as http from "http"
import { StreamableHttpServerTransport } from "../src/transport/StreamableHttpServerTransport"
import type { JSONRPCMessage } from "@modelcontextprotocol/sdk/types.js"
// Mock http server
vi.mock("http", () => {
const mockServer = {
listen: vi.fn((port: number, host: string, cb: Function) => {
setTimeout(() => cb(), 0)
}),
close: vi.fn((cb: Function) => {
setTimeout(() => cb(), 0)
}),
on: vi.fn(),
}
return {
createServer: vi.fn(() => mockServer),
Server: vi.fn(() => mockServer),
}
})
// Need to mock console.warn for one of our tests
const originalConsoleWarn = console.warn
describe("StreamableHttpServerTransport", () => {
let transport: StreamableHttpServerTransport
beforeEach(() => {
// Reset mocks
vi.clearAllMocks()
console.warn = vi.fn()
// Create transport
transport = new StreamableHttpServerTransport(3000, "127.0.0.1", "/mcp")
})
afterEach(async () => {
// Clean up
await transport.close()
console.warn = originalConsoleWarn
})
it("should start the HTTP server on the specified port and host", async () => {
await transport.start()
expect(http.createServer).toHaveBeenCalled()
expect((http.createServer() as any).listen).toHaveBeenCalledWith(
3000,
"127.0.0.1",
expect.any(Function),
)
})
it("should handle initialization request and set session ID", () => {
transport.onmessage = vi.fn()
// Access the private sessions Map directly
const transportAny = transport as any
// Create a test session ID
const sessionId = "test-session-id"
// Mock initialization data
const initData = {
jsonrpc: "2.0" as const,
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "test", version: "1.0" },
},
}
// Create mock response
const initRes = createMockResponse()
// Call the handleInitializeRequest method directly
transportAny.handleInitializeRequest(initData, {}, initRes)
// Verify headers were set
expect(initRes.setHeader).toHaveBeenCalledWith("Content-Type", "application/json")
expect(initRes.setHeader).toHaveBeenCalledWith("Mcp-Session-Id", expect.any(String))
// Extract the generated session ID
const generatedSessionId = initRes.setHeader.mock.calls.find(
(call) => call[0] === "Mcp-Session-Id",
)?.[1]
// Make sure a session was created
expect(transportAny.sessions.has(generatedSessionId)).toBeTruthy()
// Verify the message handler was set correctly
const sessionData = transportAny.sessions.get(generatedSessionId)
expect(sessionData.messageHandler).toBe(transport.onmessage)
// Verify onmessage was called with the init data
expect(transport.onmessage).toHaveBeenCalledWith(initData)
// Verify that the init request ID is mapped to the session
expect(transportAny.requestSessionMap.get(1)).toBe(generatedSessionId)
})
it("should send messages to active sessions", async () => {
// Setup spy for message handler
transport.onmessage = vi.fn()
// Create a session manually
const transportAny = transport as any
const sessionId = "test-session-id"
transportAny.sessions.set(sessionId, {
messageHandler: transport.onmessage,
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set(),
})
// Create mock streaming response
const mockRes = createMockResponse()
// Add the response to active responses for the session
const session = transportAny.sessions.get(sessionId)
session.activeResponses.add(mockRes)
// Create a test message
const message: JSONRPCMessage = {
jsonrpc: "2.0" as const,
id: 123,
result: { success: true },
}
// Map this message ID to the session
transportAny.requestSessionMap.set(123, sessionId)
// Send the message
await transport.send(message)
// Verify the message was written to the response
expect(mockRes.write).toHaveBeenCalledWith(`data: ${JSON.stringify(message)}\n\n`)
// Verify the message mapping was removed after sending
expect(transportAny.requestSessionMap.has(123)).toBe(false)
})
it("should handle session termination with DELETE", () => {
// Setup the transport with a session
const transportAny = transport as any
const sessionId = "test-session-id"
// Create a session and add it to the transport
transportAny.sessions.set(sessionId, {
messageHandler: vi.fn(),
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set([111, 222, 333]),
})
// Add some request mappings
transportAny.requestSessionMap.set(111, sessionId)
transportAny.requestSessionMap.set(222, sessionId)
transportAny.requestSessionMap.set(333, sessionId)
// Create a DELETE request with the session ID
const deleteReq = {
url: "/mcp",
method: "DELETE",
headers: {
"mcp-session-id": sessionId,
},
on: vi.fn(),
}
const deleteRes = createMockResponse()
// Handle the DELETE request
transportAny.handleDeleteRequest(deleteReq, deleteRes)
// Should respond with 204 No Content
expect(deleteRes.writeHead).toHaveBeenCalledWith(204)
expect(deleteRes.end).toHaveBeenCalled()
// Session should be removed
expect(transportAny.sessions.has(sessionId)).toBe(false)
// All request mappings should be cleaned up
expect(transportAny.requestSessionMap.has(111)).toBe(false)
expect(transportAny.requestSessionMap.has(222)).toBe(false)
expect(transportAny.requestSessionMap.has(333)).toBe(false)
})
it("should properly route responses to the correct session", async () => {
const transportAny = transport as any
transport.onmessage = vi.fn()
// Create two sessions
const sessionId1 = "session-1"
const sessionId2 = "session-2"
// Set up both sessions with active responses
transportAny.sessions.set(sessionId1, {
messageHandler: transport.onmessage,
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set([101, 102]),
})
transportAny.sessions.set(sessionId2, {
messageHandler: transport.onmessage,
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set([201, 202]),
})
// Map request IDs to sessions
transportAny.requestSessionMap.set(101, sessionId1)
transportAny.requestSessionMap.set(102, sessionId1)
transportAny.requestSessionMap.set(201, sessionId2)
transportAny.requestSessionMap.set(202, sessionId2)
// Create mock responses for both sessions
const mockRes1 = createMockResponse()
const mockRes2 = createMockResponse()
// Add responses to sessions
transportAny.sessions.get(sessionId1).activeResponses.add(mockRes1)
transportAny.sessions.get(sessionId2).activeResponses.add(mockRes2)
// Send response to session 1
const message1: JSONRPCMessage = {
jsonrpc: "2.0" as const,
id: 101,
result: { session: 1 },
}
await transport.send(message1)
// Send response to session 2
const message2: JSONRPCMessage = {
jsonrpc: "2.0" as const,
id: 201,
result: { session: 2 },
}
await transport.send(message2)
// Verify each message was sent to the correct session only
expect(mockRes1.write).toHaveBeenCalledWith(`data: ${JSON.stringify(message1)}\n\n`)
expect(mockRes1.write).not.toHaveBeenCalledWith(`data: ${JSON.stringify(message2)}\n\n`)
expect(mockRes2.write).toHaveBeenCalledWith(`data: ${JSON.stringify(message2)}\n\n`)
expect(mockRes2.write).not.toHaveBeenCalledWith(`data: ${JSON.stringify(message1)}\n\n`)
// Verify request mappings were cleaned up
expect(transportAny.requestSessionMap.has(101)).toBe(false)
expect(transportAny.requestSessionMap.has(201)).toBe(false)
expect(transportAny.requestSessionMap.has(102)).toBe(true) // Still pending
expect(transportAny.requestSessionMap.has(202)).toBe(true) // Still pending
})
it("should handle requests with invalid session IDs", () => {
const transportAny = transport as any
// Create a POST request with an invalid session ID
const req = {
url: "/mcp",
method: "POST",
headers: {
"content-type": "application/json",
"mcp-session-id": "non-existent-session",
},
on: vi.fn(),
destroy: vi.fn(),
}
// Mock data for the request body
const requestBody = JSON.stringify({
jsonrpc: "2.0",
method: "test",
id: 999,
})
// Setup on('data') and on('end') event handlers
req.on.mockImplementation((event: string, handler: Function) => {
if (event === "data") {
// Simulate receiving data
setTimeout(() => handler(requestBody), 0)
}
if (event === "end") {
// Simulate end event after data is received
setTimeout(() => handler(), 10)
}
return req
})
const res = createMockResponse()
// Call handleRequest method directly
transportAny.handleRequest(req, res)
// Return a promise to allow the async events to complete
return new Promise<void>((resolve) => {
setTimeout(() => {
// Now check the assertions after allowing time for the events to process
expect(res.writeHead).toHaveBeenCalledWith(400)
expect(res.end).toHaveBeenCalledWith(expect.stringContaining("Invalid session"))
resolve()
}, 50)
})
})
it("should handle multiple GET requests from the same session", () => {
const transportAny = transport as any
const sessionId = "test-session-id"
// Create a session
transportAny.sessions.set(sessionId, {
messageHandler: vi.fn(),
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set(),
})
// Create multiple GET requests with the same session ID
const req1 = {
url: "/mcp",
method: "GET",
headers: {
"mcp-session-id": sessionId,
},
on: vi.fn(),
}
const req2 = {
url: "/mcp",
method: "GET",
headers: {
"mcp-session-id": sessionId,
},
on: vi.fn(),
}
// Setup event handlers
req1.on.mockImplementation(() => req1)
req2.on.mockImplementation(() => req2)
const res1 = createMockResponse()
const res2 = createMockResponse()
// Handle both GET requests
transportAny.handleGetRequest(req1, res1)
transportAny.handleGetRequest(req2, res2)
// Verify both responses were added to the session's activeResponses
const session = transportAny.sessions.get(sessionId)
expect(session.activeResponses.size).toBe(2)
expect(session.activeResponses.has(res1)).toBe(true)
expect(session.activeResponses.has(res2)).toBe(true)
// Verify headers were set properly on both responses
expect(res1.setHeader).toHaveBeenCalledWith("Content-Type", "text/event-stream")
expect(res1.setHeader).toHaveBeenCalledWith("Connection", "keep-alive")
expect(res1.setHeader).toHaveBeenCalledWith("Cache-Control", "no-cache, no-transform")
expect(res1.setHeader).toHaveBeenCalledWith("Transfer-Encoding", "chunked")
expect(res1.setHeader).toHaveBeenCalledWith("Mcp-Session-Id", sessionId)
expect(res2.setHeader).toHaveBeenCalledWith("Content-Type", "text/event-stream")
expect(res2.setHeader).toHaveBeenCalledWith("Mcp-Session-Id", sessionId)
})
it("should handle client disconnects and clean up resources", () => {
const transportAny = transport as any
const sessionId = "test-session-id"
// Create a session
transportAny.sessions.set(sessionId, {
messageHandler: vi.fn(),
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set(),
})
// Create a GET request
const req = {
url: "/mcp",
method: "GET",
headers: {
"mcp-session-id": sessionId,
},
on: vi.fn(),
}
let closeHandler: Function | undefined
// Setup event handlers
req.on.mockImplementation((event: string, handler: Function) => {
if (event === "close") closeHandler = handler
return req
})
const res = createMockResponse()
// Handle the GET request
transportAny.handleGetRequest(req, res)
// Verify the response was added to activeResponses
const session = transportAny.sessions.get(sessionId)
expect(session.activeResponses.has(res)).toBe(true)
// Simulate client disconnect by triggering the close event
if (closeHandler) {
closeHandler()
// Verify the response was removed from activeResponses
expect(session.activeResponses.has(res)).toBe(false)
} else {
// This should not happen, but added as a safeguard
expect.fail("closeHandler was not set")
}
})
it("should fallback to broadcasting when target session cannot be determined", async () => {
const transportAny = transport as any
transport.onmessage = vi.fn()
// Create two sessions
const sessionId1 = "session-1"
const sessionId2 = "session-2"
transportAny.sessions.set(sessionId1, {
messageHandler: transport.onmessage,
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set(),
})
transportAny.sessions.set(sessionId2, {
messageHandler: transport.onmessage,
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set(),
})
// Create mock responses for both sessions
const mockRes1 = createMockResponse()
const mockRes2 = createMockResponse()
// Add responses to sessions
transportAny.sessions.get(sessionId1).activeResponses.add(mockRes1)
transportAny.sessions.get(sessionId2).activeResponses.add(mockRes2)
// Create a notification message (no ID)
const notification: JSONRPCMessage = {
jsonrpc: "2.0" as const,
method: "notify",
params: { type: "broadcast" },
}
// Send the notification (no target session)
await transport.send(notification)
// Verify warning was logged using the mocked console.warn
expect(console.warn).toHaveBeenCalled()
// Verify message was broadcast to all sessions
expect(mockRes1.write).toHaveBeenCalledWith(`data: ${JSON.stringify(notification)}\n\n`)
expect(mockRes2.write).toHaveBeenCalledWith(`data: ${JSON.stringify(notification)}\n\n`)
})
it("should handle POST requests and track request session mappings", () => {
const transportAny = transport as any
transport.onmessage = vi.fn()
// Create a session
const sessionId = "test-session-id"
transportAny.sessions.set(sessionId, {
messageHandler: transport.onmessage,
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set(),
})
// Create the request message
const requestMessage = {
jsonrpc: "2.0",
method: "test",
id: 555,
}
// Create a mock POST request function that directly calls our handler
// This simulates what would happen in handleRequest -> handlePostRequest
const simulatePostRequest = () => {
// Call the messageHandler directly like the real implementation would
const session = transportAny.sessions.get(sessionId)
session.messageHandler(requestMessage)
// Manually set up the mapping as handlePostRequest would
transportAny.requestSessionMap.set(555, sessionId)
if (!session.pendingRequests) {
session.pendingRequests = new Set()
}
session.pendingRequests.add(555)
}
// Simulate the POST request
simulatePostRequest()
// Verify the message handler was called
expect(transport.onmessage).toHaveBeenCalledWith(requestMessage)
// Verify request ID was mapped to the session
expect(transportAny.requestSessionMap.get(555)).toBe(sessionId)
// Verify the request ID was added to the session's pendingRequests
expect(transportAny.sessions.get(sessionId).pendingRequests.has(555)).toBe(true)
})
it("should properly handle initialize request with response body", async () => {
// Setup mock handler to simulate the protocol layer's response
transport.onmessage = vi.fn((message) => {
// Simulate the protocol layer's response to the initialize request
if ("method" in message && message.method === "initialize" && "id" in message) {
// Create the mock response that would come from the Protocol layer
const response: JSONRPCMessage = {
jsonrpc: "2.0" as const,
id: message.id,
result: {
protocolVersion: "2025-03-26",
serverInfo: {
name: "test-server",
version: "1.0.0",
},
capabilities: {
// Basic capabilities
tools: {},
},
},
}
// Send the response via the transport's send method
// This is how the Protocol layer would respond
setTimeout(() => {
transport.send(response)
}, 0)
}
})
// Create init request data
const initData = {
jsonrpc: "2.0" as const,
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "test", version: "1.0" },
},
}
// Create mock request and response
const req = createInitRequest()
const res = createMockResponse()
// Access the private method to directly test initialize request handling
const transportAny = transport as any
// Call initialize handler directly
transportAny.handleInitializeRequest(initData, req, res)
// Wait for the async handler to process the response
await new Promise((resolve) => setTimeout(resolve, 10))
// Verify response contains session ID header
expect(res.setHeader).toHaveBeenCalledWith("Mcp-Session-Id", expect.any(String))
// Verify onmessage was called with the init request
expect(transport.onmessage).toHaveBeenCalledWith(initData)
// Verify response was sent with the response from the protocol layer
expect(res.end).toHaveBeenCalled()
expect(res.writeHead).toHaveBeenCalledWith(200)
const responseArg = res.end.mock.calls[0]?.[0]
if (responseArg) {
const responseObj = JSON.parse(responseArg)
// Verify it's a proper JSON-RPC response
expect(responseObj.jsonrpc).toBe("2.0")
expect(responseObj.id).toBe(1)
expect(responseObj.result).toBeDefined()
expect(responseObj.result.protocolVersion).toBe("2025-03-26")
expect(responseObj.result.serverInfo).toBeDefined()
expect(responseObj.result.capabilities).toBeDefined()
}
// Verify session was created
const sessionId = res.setHeader.mock.calls.find((call) => call[0] === "Mcp-Session-Id")?.[1]
expect(transportAny.sessions.has(sessionId)).toBe(true)
})
it("should correctly include server capabilities in initialize response", async () => {
// Set up a custom message handler that simulates the protocol layer's response
transport.onmessage = vi.fn((message) => {
// Simulate the protocol layer's response to the initialize request
if ("method" in message && message.method === "initialize" && "id" in message) {
// Create the mock response that would come from the Protocol layer
// with actual capabilities
const response: JSONRPCMessage = {
jsonrpc: "2.0" as const,
id: message.id,
result: {
protocolVersion: "2025-03-26",
serverInfo: {
name: "test-server",
version: "1.0.0",
},
capabilities: {
tools: {
execute: true,
},
resources: {
read: true,
list: true,
},
},
},
}
// Send the response via the transport's send method
// This is how the Protocol layer would respond
setTimeout(() => {
transport.send(response)
}, 0)
}
})
// Create init request
const initData = {
jsonrpc: "2.0" as const,
id: 1,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "test", version: "1.0" },
},
}
// Create mock request and response objects
const req = createInitRequest()
const res = createMockResponse()
// Access the private method to directly test initialize request handling
const transportAny = transport as any
// Call initialize handler directly
transportAny.handleInitializeRequest(initData, req, res)
// Wait for the async handler to process the response
await new Promise((resolve) => setTimeout(resolve, 10))
// Verify headers are set
expect(res.setHeader).toHaveBeenCalledWith("Mcp-Session-Id", expect.any(String))
// Verify onmessage was called with the init request
expect(transport.onmessage).toHaveBeenCalledWith(initData)
// Verify response was sent with capabilities
expect(res.end).toHaveBeenCalled()
const responseArg = res.end.mock.calls[0]?.[0]
if (responseArg) {
const responseObj = JSON.parse(responseArg)
// Verify it's a proper JSON-RPC response
expect(responseObj.jsonrpc).toBe("2.0")
expect(responseObj.id).toBe(1)
expect(responseObj.result).toBeDefined()
// Verify capabilities are included from the protocol layer
expect(responseObj.result.capabilities).toEqual({
tools: {
execute: true,
},
resources: {
read: true,
list: true,
},
})
}
})
it("should handle tools/list requests synchronously", async () => {
// Set up a custom message handler that simulates the protocol layer's response
transport.onmessage = vi.fn((message) => {
// Simulate the protocol layer's response to the tools/list request
if ("method" in message && message.method === "tools/list" && "id" in message) {
console.error(`Test: received tools/list message in onmessage handler`)
// Create the mock response that would come from the Protocol layer
// with actual tools data
const response: JSONRPCMessage = {
jsonrpc: "2.0" as const,
id: message.id,
result: {
tools: [
{
id: "test-tool-1",
name: "Test Tool 1",
description: "A test tool for testing",
parameters: {
type: "object",
properties: {
param1: {
type: "string",
description: "A test parameter",
},
},
},
},
],
},
}
// Call the send method to simulate the protocol's response
console.error(`Test: sending response for tools/list request`)
transport.send(response)
}
})
// Create a mock session ID
const sessionId = "test-session-123"
const transportAny = transport as any
// Create session in the transport
transportAny.sessions.set(sessionId, {
messageHandler: transport.onmessage || (() => {}),
activeResponses: new Set(),
initialized: true,
pendingRequests: new Set(),
})
// Create tools/list request data
const toolsListRequest = {
jsonrpc: "2.0",
id: 123,
method: "tools/list",
}
// More realistic mock request with proper handlers
const reqHandlers: Record<string, Array<(...args: any[]) => void>> = {
data: [],
end: [],
error: [],
}
const req = {
url: "/mcp",
method: "POST",
headers: {
"mcp-session-id": sessionId,
"content-type": "application/json",
},
on: vi.fn((event, handler) => {
console.error(`Test: adding handler for ${event} event`)
if (reqHandlers[event]) {
reqHandlers[event].push(handler)
}
return req
}),
emit: vi.fn((event, ...args) => {
console.error(`Test: emitting ${event} event`)
if (reqHandlers[event]) {
reqHandlers[event].forEach((handler) => handler(...args))
}
return true
}),
}
const res = {
writeHead: vi.fn(),
end: vi.fn(),
write: vi.fn(),
setHeader: vi.fn(),
}
// Simulate the POST request with the tools/list request
console.error(`Test: calling handlePostRequest`)
transportAny.handlePostRequest(req, res)
// Simulate the request body
console.error(`Test: emitting data event`)
req.emit("data", Buffer.from(JSON.stringify(toolsListRequest)))
console.error(`Test: emitting end event`)
req.emit("end")
// Wait for async operations
await new Promise((resolve) => setTimeout(resolve, 100))
console.error(`Test: checking expectations`)
// Verify the tools/list response was sent synchronously
expect(res.end).toHaveBeenCalled()
expect(res.writeHead).toHaveBeenCalledWith(200)
try {
// Get the JSON response if it exists
const responseJson = JSON.parse(res.end.mock.calls[0][0])
// Verify the response structure
expect(responseJson.jsonrpc).toBe("2.0")
expect(responseJson.id).toBe(123)
expect(responseJson.result).toBeDefined()
expect(responseJson.result.tools).toBeInstanceOf(Array)
expect(responseJson.result.tools.length).toBe(1)
expect(responseJson.result.tools[0].id).toBe("test-tool-1")
} catch (e) {
console.error(`Test: Failed to parse response: ${e}`)
console.error(`Response end calls: ${res.end.mock.calls.length}`)
if (res.end.mock.calls.length > 0) {
console.error(`Response body: ${res.end.mock.calls[0][0]}`)
}
throw e
}
})
it("should handle full HTTP initialize request flow and reject invalid format", async () => {
// This test simulates the exact scenario from the GitHub issue
// It tests the full HTTP request flow, not just the handleInitializeRequest method
transport.onmessage = vi.fn()
const transportAny = transport as any
// Test 1: Invalid format (the old format that was in the README)
const invalidInitRequest = {
jsonrpc: "2.0",
id: 0,
method: "initialize",
params: {
client: { name: "curl-client", version: "1.0.0" },
protocol: { name: "mcp", version: "2025-03-26" },
},
}
const reqHandlers1: Record<string, Array<(...args: any[]) => void>> = {
data: [],
end: [],
error: [],
}
const req1 = {
url: "/mcp",
method: "POST",
headers: {
"content-type": "application/json",
// No session ID - this is an initialize request
},
on: vi.fn((event, handler) => {
if (reqHandlers1[event]) {
reqHandlers1[event].push(handler)
}
return req1
}),
emit: vi.fn((event, ...args) => {
if (reqHandlers1[event]) {
reqHandlers1[event].forEach((handler) => handler(...args))
}
return true
}),
destroy: vi.fn(),
}
const res1 = {
writeHead: vi.fn(),
end: vi.fn(),
write: vi.fn(),
setHeader: vi.fn(),
}
// Simulate the POST request with invalid format
transportAny.handlePostRequest(req1, res1)
req1.emit("data", Buffer.from(JSON.stringify(invalidInitRequest)))
req1.emit("end")
// Wait for processing
await new Promise((resolve) => setTimeout(resolve, 10))
// Should return 400 Bad Request for invalid format
expect(res1.writeHead).toHaveBeenCalledWith(400)
expect(res1.end).toHaveBeenCalledWith(
expect.stringContaining("Invalid session. A valid Mcp-Session-Id header is required."),
)
// Test 2: Valid format (the correct MCP format)
const validInitRequest = {
jsonrpc: "2.0",
id: 0,
method: "initialize",
params: {
protocolVersion: "2025-03-26",
capabilities: {},
clientInfo: { name: "curl-client", version: "1.0.0" },
},
}
// Set up a mock response handler for the valid request
transport.onmessage = vi.fn((message) => {
if ("method" in message && message.method === "initialize" && "id" in message) {
const response: JSONRPCMessage = {
jsonrpc: "2.0" as const,
id: message.id,
result: {
protocolVersion: "2025-03-26",
serverInfo: { name: "test-server", version: "1.0.0" },
capabilities: { tools: {} },
},
}
setTimeout(() => transport.send(response), 0)
}
})
const reqHandlers2: Record<string, Array<(...args: any[]) => void>> = {
data: [],
end: [],
error: [],
}
const req2 = {
url: "/mcp",
method: "POST",
headers: {
"content-type": "application/json",
// No session ID - this is an initialize request
},
on: vi.fn((event, handler) => {
if (reqHandlers2[event]) {
reqHandlers2[event].push(handler)
}
return req2
}),
emit: vi.fn((event, ...args) => {
if (reqHandlers2[event]) {
reqHandlers2[event].forEach((handler) => handler(...args))
}
return true
}),
destroy: vi.fn(),
}
const res2 = {
writeHead: vi.fn(),
end: vi.fn(),
write: vi.fn(),
setHeader: vi.fn(),
}
// Simulate the POST request with valid format
transportAny.handlePostRequest(req2, res2)
req2.emit("data", Buffer.from(JSON.stringify(validInitRequest)))
req2.emit("end")
// Wait for processing
await new Promise((resolve) => setTimeout(resolve, 50))
// Should return 200 OK with session ID for valid format
expect(res2.writeHead).toHaveBeenCalledWith(200)
expect(res2.setHeader).toHaveBeenCalledWith("Mcp-Session-Id", expect.any(String))
expect(res2.end).toHaveBeenCalled()
// Verify the response contains proper initialize result
const responseBody = res2.end.mock.calls[0]?.[0]
if (responseBody) {
const responseObj = JSON.parse(responseBody)
expect(responseObj.jsonrpc).toBe("2.0")
expect(responseObj.id).toBe(0)
expect(responseObj.result).toBeDefined()
expect(responseObj.result.protocolVersion).toBe("2025-03-26")
}
// Verify a session was created
expect(transportAny.sessions.size).toBeGreaterThan(0)
})
it("should handle non-initialize requests without session ID correctly", async () => {
// This test ensures that non-initialize requests properly require session IDs
transport.onmessage = vi.fn()
const transportAny = transport as any
const regularRequest = {
jsonrpc: "2.0",
id: 1,
method: "tools/list",
}
const reqHandlers: Record<string, Array<(...args: any[]) => void>> = {
data: [],
end: [],
error: [],
}
const req = {
url: "/mcp",
method: "POST",
headers: {
"content-type": "application/json",
// No session ID - this should fail for non-initialize requests
},
on: vi.fn((event, handler) => {
if (reqHandlers[event]) {
reqHandlers[event].push(handler)
}
return req
}),
emit: vi.fn((event, ...args) => {
if (reqHandlers[event]) {
reqHandlers[event].forEach((handler) => handler(...args))
}
return true
}),
destroy: vi.fn(),
}
const res = {
writeHead: vi.fn(),
end: vi.fn(),
write: vi.fn(),
setHeader: vi.fn(),
}
// Simulate the POST request
transportAny.handlePostRequest(req, res)
req.emit("data", Buffer.from(JSON.stringify(regularRequest)))
req.emit("end")
// Wait for processing
await new Promise((resolve) => setTimeout(resolve, 10))
// Should return 400 Bad Request for missing session ID
expect(res.writeHead).toHaveBeenCalledWith(400)
expect(res.end).toHaveBeenCalledWith(
expect.stringContaining("Invalid session. A valid Mcp-Session-Id header is required."),
)
})
})
// Helper functions to create mock objects
function createInitRequest() {
const req = {
url: "/mcp",
method: "POST",
headers: {
"content-type": "application/json",
},
on: vi.fn(),
destroy: vi.fn(),
}
// Set up the mock implementation to return req
req.on.mockImplementation(() => req)
return req
}
function createMockResponse() {
return {
writeHead: vi.fn(),
end: vi.fn(),
write: vi.fn(),
setHeader: vi.fn(),
on: vi.fn(),
}
}
function createMockRequest(method: string, headers: Record<string, string>) {
const req = {
url: "/mcp",
method: method,
headers: headers,
on: vi.fn(),
emit: vi.fn(),
}
// Set up the mock implementation to return req
req.on.mockImplementation(() => req)
return req
}