Skip to main content
Glama
http-handler.test.ts14.7 kB
import { jest } from '@jest/globals'; import { Request, Response } from 'express'; import { HttpHandler } from '../../src/server/proxy/http-handler.js'; import { McpProxy } from '../../src/server/proxy/mcp-proxy.js'; import { ServerRegistry } from '../../src/server/registry/server-registry.js'; import { SSEServerTransport as SdkSSEServerTransport } from '@modelcontextprotocol/sdk/server/sse.js'; import { JSONRPCMessage } from '@modelcontextprotocol/sdk/types.js'; import { EventEmitter } from 'node:events'; // Mock uuid library jest.mock('uuid', () => ({ v4: jest.fn().mockReturnValue('test-client-id'), })); // Mock logger to test logging jest.mock('../../src/utils/logger.js', () => { const mockLogger = { debug: jest.fn(), info: jest.fn(), warn: jest.fn(), error: jest.fn(), }; return { createContextLogger: jest.fn().mockReturnValue(mockLogger), }; }); // Mock SSEServerTransport from SDK jest.mock('@modelcontextprotocol/sdk/server/sse.js', () => { return { SSEServerTransport: jest.fn().mockImplementation(() => { return { get sessionId() { return 'test-session-id'; }, start: jest.fn().mockResolvedValue(null), close: jest.fn().mockResolvedValue(null), send: jest.fn().mockResolvedValue(null), handlePostMessage: jest.fn().mockResolvedValue(null), onmessage: null, onerror: null, onclose: null, }; }), }; }); // Mock McpProxy jest.mock('../../src/server/proxy/mcp-proxy.js'); const mockProxy = { addClientTransport: jest.fn(), removeClientTransport: jest.fn(), handleDirectMessage: jest.fn(), getClientCount: jest.fn().mockReturnValue(1), }; // Mock ServerRegistry jest.mock('../../src/server/registry/server-registry.js'); const mockRegistry = { getServers: jest.fn().mockReturnValue(new Map()), getServer: jest.fn(), isServerRunning: jest.fn().mockReturnValue(true), startServer: jest.fn(), stopServer: jest.fn(), getServerCount: jest.fn().mockReturnValue(2), getRunningServerCount: jest.fn().mockReturnValue(1), }; // Mock express Request and Response const createMockRequest = (overrides = {}) => { return { query: {}, body: { jsonrpc: '2.0', id: 'test-id', method: 'test-method', params: {}, }, params: {}, on: jest.fn(), ...overrides, } as unknown as Request; }; const createMockResponse = () => { const res = { status: jest.fn().mockReturnThis(), json: jest.fn().mockReturnThis(), setHeader: jest.fn(), writeHead: jest.fn(), on: jest.fn(), once: jest.fn(), headersSent: false, write: jest.fn(), end: jest.fn(), flush: jest.fn(), } as unknown as Response; return res; }; describe('HttpHandler', () => { let handler: HttpHandler; // Reset mocks before each test beforeEach(() => { jest.clearAllMocks(); handler = new HttpHandler( mockProxy as unknown as McpProxy, mockRegistry as unknown as ServerRegistry ); }); describe('handleSSE', () => { it('should handle new SSE connection correctly', async () => { const req = createMockRequest(); const res = createMockResponse(); // Create a static session ID for consistent testing const mockedModule = jest.requireMock('@modelcontextprotocol/sdk/server/sse.js') as any; mockedModule.SSEServerTransport.mockImplementationOnce(() => ({ get sessionId() { return 'test-session-id'; }, start: jest.fn().mockResolvedValue(null), close: jest.fn().mockResolvedValue(null), send: jest.fn().mockResolvedValue(null), handlePostMessage: jest.fn().mockResolvedValue(null), onmessage: null, onerror: null, onclose: null, })); await handler.handleSSE(req, res); // Verify proper initialization expect(res.setHeader).toHaveBeenCalledWith('Access-Control-Allow-Origin', '*'); // Verify client is added to proxy expect(mockProxy.addClientTransport).toHaveBeenCalled(); // Verify close event listener is registered expect(req.on).toHaveBeenCalledWith('close', expect.any(Function)); // Trigger close event to test cleanup const mockEvent = req.on as jest.Mock; const closeHandler = mockEvent.mock.calls[0][1] as () => void; closeHandler(); // Verify cleanup actions expect(mockProxy.removeClientTransport).toHaveBeenCalled(); }); it('should close connections when errors occur', async () => { // This test just verifies basic functionality without trying to test detailed error handling const req = createMockRequest(); const res = createMockResponse(); // Create a mock transport const mockTransport = { get sessionId() { return 'test-session-id'; }, start: jest.fn().mockResolvedValue(null), close: jest.fn().mockResolvedValue(null), send: jest.fn().mockResolvedValue(null), handlePostMessage: jest.fn().mockResolvedValue(null), onmessage: null, onerror: null, onclose: null, }; // Setup the mock const mockedModule = jest.requireMock('@modelcontextprotocol/sdk/server/sse.js') as any; mockedModule.SSEServerTransport.mockImplementationOnce(() => mockTransport); // Call the handler await handler.handleSSE(req, res); // Trigger close event to test cleanup const mockEvent = req.on as jest.Mock; const closeHandler = mockEvent.mock.calls[0][1] as () => void; closeHandler(); // Verify client was removed during cleanup expect(mockProxy.removeClientTransport).toHaveBeenCalled(); }); it('should end response when headers already sent', async () => { const req = createMockRequest(); const res = createMockResponse(); res.headersSent = true; // Manually trigger error handler in the HttpHandler by creating a simulated error // in a pre-headersSent state const error = new Error('Test error'); handler['handleSseError'](req, res, error); // Verify end is called but not status or json when headers are already sent expect(res.status).not.toHaveBeenCalled(); expect(res.json).not.toHaveBeenCalled(); expect(res.end).toHaveBeenCalled(); }); }); describe('handleMessage', () => { it('should handle direct message without session', async () => { const req = createMockRequest(); const res = createMockResponse(); // Explicitly type the response const mockResponse = { jsonrpc: '2.0', id: 'test-id', result: { success: true }, } as JSONRPCMessage; mockProxy.handleDirectMessage.mockResolvedValueOnce(mockResponse); await handler.handleMessage(req, res); // Verify direct message handling expect(mockProxy.handleDirectMessage).toHaveBeenCalledWith(req.body); expect(res.json).toHaveBeenCalledWith({ jsonrpc: '2.0', id: 'test-id', result: { success: true }, }); }); it('should verify session IDs in message handling', async () => { // We'll consolidate by testing the session checking logic // This is simpler than trying to create and track sessions const req = createMockRequest({ query: { sessionId: 'non-existent-session' }, }); const res = createMockResponse(); await handler.handleMessage(req, res); // Verify error handling for non-existent session expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ jsonrpc: '2.0', error: expect.objectContaining({ code: -32000, message: 'Session not found', }), }) ); }); it('should return error for invalid request body', async () => { const req = createMockRequest({ body: null }); const res = createMockResponse(); await handler.handleMessage(req, res); // Verify error handling for invalid body expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ jsonrpc: '2.0', error: expect.objectContaining({ code: -32600, message: 'Invalid request body', }), }) ); }); it('should return error for invalid JSON-RPC version', async () => { const req = createMockRequest({ body: { jsonrpc: '1.0', // Invalid version id: 'test-id', method: 'test-method', params: {}, }, }); const res = createMockResponse(); await handler.handleMessage(req, res); // Verify error handling for invalid JSON-RPC version expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ jsonrpc: '2.0', error: expect.objectContaining({ code: -32600, message: 'Invalid JSON-RPC message', }), }) ); }); it('should return error for non-existent session', async () => { const req = createMockRequest({ query: { sessionId: 'non-existent-session' }, }); const res = createMockResponse(); await handler.handleMessage(req, res); // Verify error handling for non-existent session expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ jsonrpc: '2.0', error: expect.objectContaining({ code: -32000, message: 'Session not found', }), }) ); }); it('should return error for non-existent session', async () => { const req = createMockRequest({ query: { sessionId: 'non-existent-session' }, }); const res = createMockResponse(); await handler.handleMessage(req, res); // Verify error handling for non-existent session expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ jsonrpc: '2.0', error: expect.objectContaining({ code: -32000, message: 'Session not found', }), }) ); }); }); describe('API Routes', () => { it('should get servers list', async () => { const req = createMockRequest(); const res = createMockResponse(); // Setup mock registry data const mockServers = new Map(); mockServers.set('server1', { name: 'server1', transportType: 'stdio', description: 'Test server 1', tags: ['test'], }); mockServers.set('server2', { name: 'server2', transportType: 'http', description: 'Test server 2', tags: ['test', 'http'], }); mockRegistry.getServers.mockReturnValueOnce(mockServers); await handler.getServers(req, res); // Verify server list response expect(res.json).toHaveBeenCalledWith({ servers: expect.arrayContaining([ expect.objectContaining({ name: 'server1' }), expect.objectContaining({ name: 'server2' }), ]), }); }); it('should get server details', async () => { const req = createMockRequest({ params: { name: 'server1' } }); const res = createMockResponse(); // Setup mock server data mockRegistry.getServer.mockReturnValueOnce({ name: 'server1', transportType: 'stdio', description: 'Test server 1', tags: ['test'], }); await handler.getServerDetails(req, res); // Verify server details response expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ name: 'server1', transportType: 'stdio', description: 'Test server 1', tags: ['test'], running: true, }) ); }); it('should return error for non-existent server', async () => { const req = createMockRequest({ params: { name: 'non-existent' } }); const res = createMockResponse(); // Setup mock to return null for non-existent server mockRegistry.getServer.mockReturnValueOnce(null); await handler.getServerDetails(req, res); // Verify error response expect(res.status).toHaveBeenCalledWith(404); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: 'Server non-existent not found', }) ); }); it('should start server', async () => { const req = createMockRequest({ params: { name: 'server1' } }); const res = createMockResponse(); await handler.startServer(req, res); // Verify server start request expect(mockRegistry.startServer).toHaveBeenCalledWith('server1'); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ success: true, message: 'Server server1 started', }) ); }); it('should handle error when starting server', async () => { const req = createMockRequest({ params: { name: 'server1' } }); const res = createMockResponse(); // Setup mock to throw error const testError = new Error('Server start error'); mockRegistry.startServer.mockRejectedValueOnce(testError); await handler.startServer(req, res); // Verify error response expect(res.status).toHaveBeenCalledWith(400); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ error: 'Server start error', }) ); }); it('should stop server', async () => { const req = createMockRequest({ params: { name: 'server1' } }); const res = createMockResponse(); await handler.stopServer(req, res); // Verify server stop request expect(mockRegistry.stopServer).toHaveBeenCalledWith('server1'); expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ success: true, message: 'Server server1 stopped', }) ); }); it('should get proxy status', async () => { const req = createMockRequest(); const res = createMockResponse(); await handler.getStatus(req, res); // Verify status response expect(res.json).toHaveBeenCalledWith( expect.objectContaining({ servers: { total: 2, running: 1, }, clients: { connected: 1, }, }) ); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/metcalfc/atrax'

If you have feedback or need assistance with the MCP directory API, please join our Discord server