UNPKG

osc-mcp-server

Version:

Model Context Protocol server for OSC (Open Sound Control) endpoint management

567 lines 25.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const server_1 = require("./server"); jest.mock('@modelcontextprotocol/sdk/server/index.js', () => ({ Server: jest.fn(), })); jest.mock('@modelcontextprotocol/sdk/server/stdio.js', () => ({ StdioServerTransport: jest.fn(), })); jest.mock('@modelcontextprotocol/sdk/types.js', () => ({ CallToolRequestSchema: { method: 'tools/call' }, ListToolsRequestSchema: { method: 'tools/list' }, ErrorCode: { InvalidParams: 'InvalidParams', InternalError: 'InternalError', MethodNotFound: 'MethodNotFound', }, McpError: class McpError extends Error { code; constructor(code, message) { super(message); this.code = code; } }, })); jest.mock('./osc/manager'); describe('OSCMCPServer', () => { let server; let mockOSCManager; let mockMCPServer; let mockTransport; beforeEach(() => { jest.clearAllMocks(); mockMCPServer = { connect: jest.fn().mockResolvedValue(undefined), close: jest.fn().mockResolvedValue(undefined), setRequestHandler: jest.fn(), }; mockTransport = {}; mockOSCManager = { createEndpoint: jest.fn(), stopEndpoint: jest.fn(), getMessages: jest.fn(), getEndpointStatus: jest.fn(), shutdown: jest.fn().mockResolvedValue(undefined), on: jest.fn(), }; const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); const { createOSCManager } = require('./osc/manager'); Server.mockImplementation(() => mockMCPServer); StdioServerTransport.mockImplementation(() => mockTransport); createOSCManager.mockReturnValue(mockOSCManager); server = new server_1.OSCMCPServer(); }); describe('Constructor', () => { it('should initialize MCP server with correct configuration', () => { const { Server } = require('@modelcontextprotocol/sdk/server/index.js'); expect(Server).toHaveBeenCalledWith({ name: 'osc-mcp-server', version: '1.0.0', }); }); it('should create OSC manager instance', () => { const { createOSCManager } = require('./osc/manager'); expect(createOSCManager).toHaveBeenCalled(); }); it('should set up request handlers', () => { expect(mockMCPServer.setRequestHandler).toHaveBeenCalledTimes(2); }); it('should set up OSC manager event handlers', () => { expect(mockOSCManager.on).toHaveBeenCalledWith('endpointCreated', expect.any(Function)); expect(mockOSCManager.on).toHaveBeenCalledWith('endpointStopped', expect.any(Function)); expect(mockOSCManager.on).toHaveBeenCalledWith('endpointError', expect.any(Function)); expect(mockOSCManager.on).toHaveBeenCalledWith('messageReceived', expect.any(Function)); }); }); describe('Server Lifecycle', () => { describe('start()', () => { it('should start server successfully', async () => { await server.start(); expect(mockMCPServer.connect).toHaveBeenCalledWith(mockTransport); expect(server.isServerRunning()).toBe(true); }); it('should throw error if server is already running', async () => { await server.start(); await expect(server.start()).rejects.toThrow('Server is already running'); }); it('should handle connection errors', async () => { const error = new Error('Connection failed'); mockMCPServer.connect.mockRejectedValue(error); await expect(server.start()).rejects.toThrow('Failed to start MCP server: Connection failed'); }); }); describe('shutdown()', () => { it('should shutdown server gracefully', async () => { await server.start(); await server.shutdown(); expect(mockOSCManager.shutdown).toHaveBeenCalled(); expect(mockMCPServer.close).toHaveBeenCalled(); expect(server.isServerRunning()).toBe(false); }); it('should handle shutdown when server is not running', async () => { await server.shutdown(); expect(mockOSCManager.shutdown).not.toHaveBeenCalled(); expect(mockMCPServer.close).not.toHaveBeenCalled(); }); it('should handle shutdown errors gracefully', async () => { await server.start(); const error = new Error('Shutdown failed'); mockOSCManager.shutdown.mockRejectedValue(error); await server.shutdown(); expect(server.isServerRunning()).toBe(false); }); }); }); describe('Tool Registration', () => { let listToolsHandler; beforeEach(() => { const calls = mockMCPServer.setRequestHandler.mock.calls; const listToolsCall = calls.find((call) => call[0].method === 'tools/list'); listToolsHandler = listToolsCall[1]; }); it('should register all OSC tools', async () => { const result = await listToolsHandler(); expect(result.tools).toHaveLength(4); const toolNames = result.tools.map((tool) => tool.name); expect(toolNames).toContain('create_osc_endpoint'); expect(toolNames).toContain('stop_osc_endpoint'); expect(toolNames).toContain('get_osc_messages'); expect(toolNames).toContain('get_endpoint_status'); }); it('should provide proper tool schemas', async () => { const result = await listToolsHandler(); const createEndpointTool = result.tools.find((tool) => tool.name === 'create_osc_endpoint'); expect(createEndpointTool.inputSchema.properties.port).toBeDefined(); expect(createEndpointTool.inputSchema.properties.port.minimum).toBe(1024); expect(createEndpointTool.inputSchema.properties.port.maximum).toBe(65535); expect(createEndpointTool.inputSchema.required).toContain('port'); }); }); describe('Tool Execution', () => { let callToolHandler; beforeEach(() => { const calls = mockMCPServer.setRequestHandler.mock.calls; const callToolCall = calls.find((call) => call[0].method === 'tools/call'); callToolHandler = callToolCall[1]; }); describe('create_osc_endpoint', () => { it('should create endpoint successfully', async () => { const mockResponse = { endpointId: 'endpoint-1', port: 8000, status: 'active', message: 'OSC endpoint created successfully on port 8000', }; mockOSCManager.createEndpoint.mockResolvedValue(mockResponse); const request = { params: { name: 'create_osc_endpoint', arguments: { port: 8000, bufferSize: 1000, addressFilters: ['/synth/*'], }, }, }; const result = await callToolHandler(request); expect(mockOSCManager.createEndpoint).toHaveBeenCalledWith({ port: 8000, bufferSize: 1000, addressFilters: ['/synth/*'], }); expect(result.content[0].type).toBe('text'); expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); }); it('should validate port parameter', async () => { const request = { params: { name: 'create_osc_endpoint', arguments: { port: 500, }, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); it('should handle endpoint creation errors', async () => { const mockResponse = { endpointId: '', port: 8000, status: 'error', message: 'Port 8000 is already in use', }; mockOSCManager.createEndpoint.mockResolvedValue(mockResponse); const request = { params: { name: 'create_osc_endpoint', arguments: { port: 8000 }, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); }); describe('stop_osc_endpoint', () => { it('should stop endpoint successfully', async () => { const mockResponse = { endpointId: 'endpoint-1', message: 'Endpoint endpoint-1 stopped successfully', }; mockOSCManager.stopEndpoint.mockResolvedValue(mockResponse); const request = { params: { name: 'stop_osc_endpoint', arguments: { endpointId: 'endpoint-1' }, }, }; const result = await callToolHandler(request); expect(mockOSCManager.stopEndpoint).toHaveBeenCalledWith('endpoint-1'); expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); }); it('should validate endpointId parameter', async () => { const request = { params: { name: 'stop_osc_endpoint', arguments: {}, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); }); describe('get_osc_messages', () => { it('should query messages successfully', async () => { const mockMessages = [ { timestamp: new Date('2024-01-01T12:00:00Z'), address: '/synth/freq', typeTags: 'f', arguments: [440.0], sourceIp: '192.168.1.100', sourcePort: 57120, }, ]; const expectedResponse = { messages: [ { timestamp: '2024-01-01T12:00:00.000Z', address: '/synth/freq', typeTags: 'f', arguments: [440.0], sourceIp: '192.168.1.100', sourcePort: 57120, }, ], totalCount: 1, filteredCount: 1, }; const mockResponse = { messages: mockMessages, totalCount: 1, filteredCount: 1, }; mockOSCManager.getMessages.mockReturnValue(mockResponse); const request = { params: { name: 'get_osc_messages', arguments: { endpointId: 'endpoint-1', addressPattern: '/synth/*', timeWindowSeconds: 60, limit: 10, }, }, }; const result = await callToolHandler(request); expect(mockOSCManager.getMessages).toHaveBeenCalledWith('endpoint-1', { addressPattern: '/synth/*', since: expect.any(Date), limit: 10, }); expect(JSON.parse(result.content[0].text)).toEqual(expectedResponse); }); it('should handle query without parameters', async () => { const mockResponse = { messages: [], totalCount: 0, filteredCount: 0, }; mockOSCManager.getMessages.mockReturnValue(mockResponse); const request = { params: { name: 'get_osc_messages', arguments: {}, }, }; const result = await callToolHandler(request); expect(mockOSCManager.getMessages).toHaveBeenCalledWith(undefined, {}); expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); }); }); describe('get_endpoint_status', () => { it('should get endpoint status successfully', async () => { const mockEndpoints = [ { id: 'endpoint-1', port: 8000, status: 'active', bufferSize: 1000, addressFilters: [], createdAt: new Date('2024-01-01T12:00:00Z'), messageCount: 5, }, ]; const mockResponse = { endpoints: mockEndpoints, }; const expectedResponse = { endpoints: [ { id: 'endpoint-1', port: 8000, status: 'active', bufferSize: 1000, addressFilters: [], createdAt: '2024-01-01T12:00:00.000Z', messageCount: 5, }, ], }; mockOSCManager.getEndpointStatus.mockReturnValue(mockResponse); const request = { params: { name: 'get_endpoint_status', arguments: { endpointId: 'endpoint-1' }, }, }; const result = await callToolHandler(request); expect(mockOSCManager.getEndpointStatus).toHaveBeenCalledWith('endpoint-1'); expect(JSON.parse(result.content[0].text)).toEqual(expectedResponse); }); }); describe('Error Handling', () => { it('should handle unknown tool names', async () => { const request = { params: { name: 'unknown_tool', arguments: {}, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); it('should convert generic errors to MCP errors', async () => { mockOSCManager.createEndpoint.mockRejectedValue(new Error('Generic error')); const request = { params: { name: 'create_osc_endpoint', arguments: { port: 8000 }, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); describe('Parameter Validation Errors', () => { it('should validate create_osc_endpoint parameters', async () => { const invalidRequests = [ { arguments: {} }, { arguments: { port: 'invalid' } }, { arguments: { port: 100 } }, { arguments: { port: 8000, bufferSize: 'invalid' } }, { arguments: { port: 8000, addressFilters: 'invalid' } }, { arguments: { port: 8000, addressFilters: ['invalid'] } }, ]; for (const args of invalidRequests) { const request = { params: { name: 'create_osc_endpoint', arguments: args.arguments, }, }; await expect(callToolHandler(request)).rejects.toThrow(); } }); it('should validate stop_osc_endpoint parameters', async () => { const invalidRequests = [ { arguments: {} }, { arguments: { endpointId: 123 } }, { arguments: { endpointId: '' } }, ]; for (const args of invalidRequests) { const request = { params: { name: 'stop_osc_endpoint', arguments: args.arguments, }, }; await expect(callToolHandler(request)).rejects.toThrow(); } }); it('should validate get_osc_messages parameters', async () => { const invalidRequests = [ { arguments: { endpointId: 123 } }, { arguments: { addressPattern: 'invalid' } }, { arguments: { timeWindowSeconds: 'invalid' } }, { arguments: { timeWindowSeconds: 100000 } }, { arguments: { limit: 'invalid' } }, { arguments: { limit: 2000 } }, ]; for (const args of invalidRequests) { const request = { params: { name: 'get_osc_messages', arguments: args.arguments, }, }; await expect(callToolHandler(request)).rejects.toThrow(); } }); it('should validate get_endpoint_status parameters', async () => { const request = { params: { name: 'get_endpoint_status', arguments: { endpointId: 123 }, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); }); describe('Network Error Handling', () => { it('should handle port in use errors', async () => { const mockResponse = { endpointId: '', port: 8000, status: 'error', message: 'Port 8000 is already in use. Please try a different port.', }; mockOSCManager.createEndpoint.mockResolvedValue(mockResponse); const request = { params: { name: 'create_osc_endpoint', arguments: { port: 8000 }, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); it('should handle permission denied errors', async () => { const mockResponse = { endpointId: '', port: 80, status: 'error', message: 'Permission denied to bind to port 80. Try using a port number above 1024 or run with appropriate privileges.', }; mockOSCManager.createEndpoint.mockResolvedValue(mockResponse); const request = { params: { name: 'create_osc_endpoint', arguments: { port: 80 }, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); }); describe('Endpoint Error Handling', () => { it('should handle endpoint not found errors', async () => { const mockResponse = { endpointId: 'nonexistent', message: "Endpoint 'nonexistent' not found. Please check the endpoint ID and try again.", }; mockOSCManager.stopEndpoint.mockResolvedValue(mockResponse); const request = { params: { name: 'stop_osc_endpoint', arguments: { endpointId: 'nonexistent' }, }, }; const result = await callToolHandler(request); expect(JSON.parse(result.content[0].text)).toEqual(mockResponse); }); }); describe('Operation Error Handling', () => { it('should handle operation failures gracefully', async () => { mockOSCManager.createEndpoint.mockRejectedValue(new Error('Unexpected error')); const request = { params: { name: 'create_osc_endpoint', arguments: { port: 8000 }, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); it('should handle OSC manager errors in get_messages', async () => { mockOSCManager.getMessages.mockImplementation(() => { throw new Error('Buffer error'); }); const request = { params: { name: 'get_osc_messages', arguments: {}, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); it('should handle OSC manager errors in get_endpoint_status', async () => { mockOSCManager.getEndpointStatus.mockImplementation(() => { throw new Error('Status error'); }); const request = { params: { name: 'get_endpoint_status', arguments: {}, }, }; await expect(callToolHandler(request)).rejects.toThrow(); }); }); }); }); describe('VSCode Compatibility', () => { it('should use stdio transport for VSCode integration', async () => { const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js'); await server.start(); expect(StdioServerTransport).toHaveBeenCalled(); expect(mockMCPServer.connect).toHaveBeenCalledWith(mockTransport); }); it('should provide MCP-compliant tool schemas', async () => { const calls = mockMCPServer.setRequestHandler.mock.calls; const listToolsCall = calls.find((call) => call[0].method === 'tools/list'); const listToolsHandler = listToolsCall[1]; const result = await listToolsHandler(); result.tools.forEach((tool) => { expect(tool.name).toBeDefined(); expect(tool.description).toBeDefined(); expect(tool.inputSchema).toBeDefined(); expect(tool.inputSchema.type).toBe('object'); expect(tool.inputSchema.properties).toBeDefined(); }); }); it('should handle connection lifecycle gracefully', async () => { await server.start(); expect(server.isServerRunning()).toBe(true); await server.shutdown(); expect(server.isServerRunning()).toBe(false); await server.start(); expect(server.isServerRunning()).toBe(true); }); }); describe('Event Handling', () => { it('should set up OSC manager event listeners', () => { expect(mockOSCManager.on).toHaveBeenCalledWith('endpointCreated', expect.any(Function)); expect(mockOSCManager.on).toHaveBeenCalledWith('endpointStopped', expect.any(Function)); expect(mockOSCManager.on).toHaveBeenCalledWith('endpointError', expect.any(Function)); expect(mockOSCManager.on).toHaveBeenCalledWith('messageReceived', expect.any(Function)); }); }); describe('Utility Methods', () => { it('should provide access to OSC manager for testing', () => { const oscManager = server.getOSCManager(); expect(oscManager).toBe(mockOSCManager); }); it('should track running status correctly', async () => { expect(server.isServerRunning()).toBe(false); await server.start(); expect(server.isServerRunning()).toBe(true); await server.shutdown(); expect(server.isServerRunning()).toBe(false); }); }); }); //# sourceMappingURL=server.test.js.map