Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions src/__tests__/__snapshots__/options.test.ts.snap
Original file line number Diff line number Diff line change
Expand Up @@ -137,24 +137,40 @@ exports[`options should return specific properties 1`] = `
},
"URL_REGEX": /\\^\\(https\\?:\\)\\\\/\\\\//i,
"freezeOptions": [Function],
"getArgValue": [Function],
"parseCliOptions": [Function],
}
`;

exports[`parseCliOptions should attempt to parse args with --docs-host flag 1`] = `
{
"allowedHosts": undefined,
"allowedOrigins": undefined,
"docsHost": true,
"host": "localhost",
"http": false,
"port": 3000,
}
`;

exports[`parseCliOptions should attempt to parse args with other arguments 1`] = `
{
"allowedHosts": undefined,
"allowedOrigins": undefined,
"docsHost": false,
"host": "localhost",
"http": false,
"port": 3000,
}
`;

exports[`parseCliOptions should attempt to parse args without --docs-host flag 1`] = `
{
"allowedHosts": undefined,
"allowedOrigins": undefined,
"docsHost": false,
"host": "localhost",
"http": false,
"port": 3000,
}
`;
58 changes: 58 additions & 0 deletions src/__tests__/options.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,64 @@ describe('parseCliOptions', () => {

expect(result).toMatchSnapshot();
});

describe('HTTP transport options', () => {
it.each([
{
description: 'with --http flag',
args: ['node', 'script.js', '--http'],
expected: { http: true, port: 3000, host: 'localhost' }
},
{
description: 'with --http and --port',
args: ['node', 'script.js', '--http', '--port', '8080'],
expected: { http: true, port: 8080, host: 'localhost' }
},
{
description: 'with --http and --host',
args: ['node', 'script.js', '--http', '--host', '0.0.0.0'],
expected: { http: true, port: 3000, host: '0.0.0.0' }
},
{
description: 'with --allowed-origins',
args: ['node', 'script.js', '--http', '--allowed-origins', 'https://app.com,https://admin.app.com'],
expected: {
http: true,
port: 3000,
host: 'localhost',
allowedOrigins: ['https://app.com', 'https://admin.app.com']
}
},
{
description: 'with --allowed-hosts',
args: ['node', 'script.js', '--http', '--allowed-hosts', 'localhost,127.0.0.1'],
expected: {
http: true,
port: 3000,
host: 'localhost',
allowedHosts: ['localhost', '127.0.0.1']
}
}
])('should parse HTTP options $description', ({ args, expected }) => {
process.argv = args;

const result = parseCliOptions();

expect(result).toMatchObject(expected);
});

it('should throw error for invalid port', () => {
process.argv = ['node', 'script.js', '--http', '--port', '99999'];

expect(() => parseCliOptions()).toThrow('Invalid port: 99999. Must be between 1 and 65535.');
});

it('should throw error for invalid port (negative)', () => {
process.argv = ['node', 'script.js', '--http', '--port', '-1'];

expect(() => parseCliOptions()).toThrow('Invalid port: -1. Must be between 1 and 65535.');
});
});
});

describe('freezeOptions', () => {
Expand Down
211 changes: 211 additions & 0 deletions src/__tests__/server.http.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,211 @@
import { createServer } from 'node:http';
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
import { startHttpTransport } from '../server.http';

// Mock dependencies
jest.mock('@modelcontextprotocol/sdk/server/mcp.js');
jest.mock('@modelcontextprotocol/sdk/server/streamableHttp.js');
jest.mock('node:http');

const MockMcpServer = McpServer as jest.MockedClass<typeof McpServer>;
const MockStreamableHTTPServerTransport = StreamableHTTPServerTransport as jest.MockedClass<typeof StreamableHTTPServerTransport>;
const MockCreateServer = createServer as jest.MockedFunction<typeof createServer>;

describe('HTTP Transport', () => {
let mockServer: any;

Check warning on line 16 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Unexpected any. Specify a different type

Check warning on line 16 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Unexpected any. Specify a different type

Check warning on line 16 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Unexpected any. Specify a different type
let mockTransport: any;

Check warning on line 17 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Unexpected any. Specify a different type

Check warning on line 17 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Unexpected any. Specify a different type

Check warning on line 17 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Unexpected any. Specify a different type
let mockHttpServer: any;

Check warning on line 18 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Unexpected any. Specify a different type

Check warning on line 18 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Unexpected any. Specify a different type

Check warning on line 18 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Unexpected any. Specify a different type

beforeEach(() => {
mockServer = {
connect: jest.fn(),
registerTool: jest.fn()
};
mockTransport = {
handleRequest: jest.fn(),
sessionId: 'test-session-123'
};
mockHttpServer = {
on: jest.fn(),
listen: jest.fn().mockImplementation((_port: any, _host: any, callback: any) => {

Check warning on line 31 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Unexpected any. Specify a different type

Check warning on line 31 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Unexpected any. Specify a different type

Check warning on line 31 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Unexpected any. Specify a different type

Check warning on line 31 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Unexpected any. Specify a different type

Check warning on line 31 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Unexpected any. Specify a different type

Check warning on line 31 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Unexpected any. Specify a different type

Check warning on line 31 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Unexpected any. Specify a different type

Check warning on line 31 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Unexpected any. Specify a different type

Check warning on line 31 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Unexpected any. Specify a different type
// Immediately call the callback to simulate successful server start
if (callback) callback();
}),
close: jest.fn()
};

MockMcpServer.mockImplementation(() => mockServer);
MockStreamableHTTPServerTransport.mockImplementation(() => mockTransport);
MockCreateServer.mockReturnValue(mockHttpServer);
});

afterEach(() => {
jest.clearAllMocks();
});

describe('startHttpTransport', () => {
it('should start HTTP server on specified port and host', async () => {
// Uses default parameter pattern - no need to pass options explicitly
await startHttpTransport(mockServer);

expect(MockCreateServer).toHaveBeenCalled();
expect(mockHttpServer.listen).toHaveBeenCalledWith(3000, 'localhost', expect.any(Function));
});

it('should create StreamableHTTPServerTransport with correct options', async () => {
await startHttpTransport(mockServer);

expect(MockStreamableHTTPServerTransport).toHaveBeenCalledWith({
sessionIdGenerator: expect.any(Function),
enableJsonResponse: false,
allowedOrigins: undefined,
allowedHosts: undefined,
enableDnsRebindingProtection: true,
onsessioninitialized: expect.any(Function),
onsessionclosed: expect.any(Function)
});
});

it('should connect MCP server to transport', async () => {
await startHttpTransport(mockServer);

expect(mockServer.connect).toHaveBeenCalledWith(mockTransport);
});

it('should handle server errors', async () => {
const error = new Error('Server error');

mockHttpServer.listen.mockImplementation((_port: any, _host: any, _callback: any) => {

Check warning on line 79 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (20.x)

Unexpected any. Specify a different type

Check warning on line 79 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (22.x)

Unexpected any. Specify a different type

Check warning on line 79 in src/__tests__/server.http.test.ts

View workflow job for this annotation

GitHub Actions / Integration-checks (24.x)

Unexpected any. Specify a different type
mockHttpServer.on.mockImplementation((event: any, handler: any) => {
if (event === 'error') {
handler(error);
}
});
throw error;
});

await expect(startHttpTransport(mockServer)).rejects.toThrow('Server error');
});

it('should set up request handler', async () => {
await startHttpTransport(mockServer);

// StreamableHTTPServerTransport handles requests directly
expect(MockStreamableHTTPServerTransport).toHaveBeenCalled();
});
});

describe('HTTP request handling', () => {
it('should delegate requests to StreamableHTTPServerTransport', async () => {
await startHttpTransport(mockServer);

// Mock request and response
const mockReq = {
method: 'GET',
url: '/mcp',
headers: { host: 'localhost:3000' }
};
const mockRes = {
setHeader: jest.fn(),
writeHead: jest.fn(),
end: jest.fn()
};

// Call the transport's handleRequest method directly
await mockTransport.handleRequest(mockReq, mockRes);

// Verify transport handles the request
expect(mockTransport.handleRequest).toHaveBeenCalledWith(mockReq, mockRes);
});

it('should handle all HTTP methods through transport', async () => {
await startHttpTransport(mockServer);

// Test different HTTP methods
const methods = ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'];

for (const method of methods) {
const mockReq = {
method,
url: '/mcp',
headers: { host: 'localhost:3000' }
};
const mockRes = {
setHeader: jest.fn(),
writeHead: jest.fn(),
end: jest.fn()
};

await mockTransport.handleRequest(mockReq, mockRes);
expect(mockTransport.handleRequest).toHaveBeenCalledWith(mockReq, mockRes);
}
});

it('should handle transport errors gracefully', async () => {
await startHttpTransport(mockServer);

// Mock transport error
const transportError = new Error('Transport error');

mockTransport.handleRequest.mockRejectedValue(transportError);

const mockReq = {
method: 'GET',
url: '/mcp',
headers: { host: 'localhost:3000' }
};
const mockRes = {
setHeader: jest.fn(),
writeHead: jest.fn(),
end: jest.fn()
};

// Should throw - transport errors are propagated
await expect(mockTransport.handleRequest(mockReq, mockRes)).rejects.toThrow('Transport error');
});
});

describe('StreamableHTTPServerTransport configuration', () => {
it('should use crypto.randomUUID for session ID generation', async () => {
await startHttpTransport(mockServer);

const transportOptions = MockStreamableHTTPServerTransport.mock.calls[0]?.[0];

expect(transportOptions?.sessionIdGenerator).toBeDefined();
expect(typeof transportOptions?.sessionIdGenerator).toBe('function');

// Test that it generates UUIDs
const sessionId = transportOptions?.sessionIdGenerator?.();

expect(sessionId).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
});

it('should configure session callbacks', async () => {
await startHttpTransport(mockServer);

const transportOptions = MockStreamableHTTPServerTransport.mock.calls[0]?.[0];

expect(transportOptions?.onsessioninitialized).toBeDefined();
expect(transportOptions?.onsessionclosed).toBeDefined();
expect(typeof transportOptions?.onsessioninitialized).toBe('function');
expect(typeof transportOptions?.onsessionclosed).toBe('function');
});

it('should enable SSE streaming', async () => {
await startHttpTransport(mockServer);

const transportOptions = MockStreamableHTTPServerTransport.mock.calls[0]?.[0];

expect(transportOptions?.enableJsonResponse).toBe(false);
});

it('should enable DNS rebinding protection', async () => {
await startHttpTransport(mockServer);

const transportOptions = MockStreamableHTTPServerTransport.mock.calls[0]?.[0];

expect(transportOptions?.enableDnsRebindingProtection).toBe(true);
});
});
});
Loading
Loading