osc-mcp-server
Version:
Model Context Protocol server for OSC (Open Sound Control) endpoint management
633 lines • 30.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const dgram_1 = require("dgram");
const server_1 = require("../server");
const manager_1 = require("./manager");
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;
details;
constructor(code, message, details) {
super(message);
this.code = code;
this.details = details;
}
},
}));
class OSCTestSender {
socket;
constructor() {
this.socket = (0, dgram_1.createSocket)('udp4');
}
createOSCMessage(address, typeTags, ...args) {
const addressBuffer = Buffer.from(address + '\0');
const addressPadded = this.padToMultipleOf4(addressBuffer);
const typeTagsBuffer = Buffer.from(',' + typeTags + '\0');
const typeTagsPadded = this.padToMultipleOf4(typeTagsBuffer);
const argBuffers = [];
for (let i = 0; i < args.length; i++) {
const arg = args[i];
const typeTag = typeTags[i];
switch (typeTag) {
case 'i': {
const intBuffer = Buffer.allocUnsafe(4);
intBuffer.writeInt32BE(arg, 0);
argBuffers.push(intBuffer);
break;
}
case 'f': {
const floatBuffer = Buffer.allocUnsafe(4);
floatBuffer.writeFloatBE(arg, 0);
argBuffers.push(floatBuffer);
break;
}
case 's': {
const stringBuffer = Buffer.from(arg + '\0');
argBuffers.push(this.padToMultipleOf4(stringBuffer));
break;
}
case 'b': {
const blobSize = Buffer.allocUnsafe(4);
blobSize.writeInt32BE(arg.length, 0);
const blobData = this.padToMultipleOf4(arg);
argBuffers.push(Buffer.concat([blobSize, blobData]));
break;
}
}
}
return Buffer.concat([addressPadded, typeTagsPadded, ...argBuffers]);
}
padToMultipleOf4(buffer) {
const remainder = buffer.length % 4;
if (remainder === 0)
return buffer;
const padding = Buffer.alloc(4 - remainder);
return Buffer.concat([buffer, padding]);
}
async sendMessage(port, address, typeTags, ...args) {
return new Promise((resolve, reject) => {
const message = this.createOSCMessage(address, typeTags, ...args);
this.socket.send(message, port, 'localhost', error => {
if (error) {
reject(error);
}
else {
resolve();
}
});
});
}
async sendMessages(port, messages) {
for (const msg of messages) {
await this.sendMessage(port, msg.address, msg.typeTags, ...msg.args);
await new Promise(resolve => setTimeout(resolve, 10));
}
}
async sendRapidMessages(port, count, baseAddress = '/test') {
const promises = [];
for (let i = 0; i < count; i++) {
const address = `${baseAddress}/${i}`;
const promise = this.sendMessage(port, address, 'if', i, i * 0.1);
promises.push(promise);
}
await Promise.all(promises);
}
close() {
this.socket.close();
}
}
describe('OSC Integration Tests with Real Communication', () => {
let server;
let oscManager;
let mockMCPServer;
let mockTransport;
let callToolHandler;
let testSender;
beforeEach(() => {
jest.clearAllMocks();
mockMCPServer = {
connect: jest.fn().mockResolvedValue(undefined),
close: jest.fn().mockResolvedValue(undefined),
setRequestHandler: jest.fn(),
};
mockTransport = {};
const { Server } = require('@modelcontextprotocol/sdk/server/index.js');
const { StdioServerTransport } = require('@modelcontextprotocol/sdk/server/stdio.js');
Server.mockImplementation(() => mockMCPServer);
StdioServerTransport.mockImplementation(() => mockTransport);
server = new server_1.OSCMCPServer();
oscManager = server.getOSCManager();
const calls = mockMCPServer.setRequestHandler.mock.calls;
const callToolCall = calls.find((call) => call[0].method === 'tools/call');
callToolHandler = callToolCall[1];
testSender = new OSCTestSender();
});
afterEach(async () => {
testSender.close();
try {
await oscManager.shutdown();
}
catch (_error) {
}
});
describe('Real OSC Message Communication', () => {
it('should receive and parse actual OSC messages', async () => {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port: 9001, bufferSize: 100 },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
expect(createResponse.status).toBe('active');
const endpointId = createResponse.endpointId;
await new Promise(resolve => setTimeout(resolve, 100));
await testSender.sendMessage(9001, '/synth/freq', 'f', 440.0);
await testSender.sendMessage(9001, '/synth/amp', 'f', 0.8);
await testSender.sendMessage(9001, '/drum/kick', 'i', 1);
await new Promise(resolve => setTimeout(resolve, 200));
const queryRequest = {
params: {
name: 'get_osc_messages',
arguments: { endpointId },
},
};
const queryResult = await callToolHandler(queryRequest);
const queryResponse = JSON.parse(queryResult.content[0].text);
expect(queryResponse.messages).toHaveLength(3);
expect(queryResponse.totalCount).toBe(3);
expect(queryResponse.filteredCount).toBe(3);
const messages = queryResponse.messages;
expect(messages.some((m) => m.address === '/synth/freq' && Math.abs(m.arguments[0] - 440) < 0.001)).toBe(true);
expect(messages.some((m) => m.address === '/synth/amp' && Math.abs(m.arguments[0] - 0.8) < 0.001)).toBe(true);
expect(messages.some((m) => m.address === '/drum/kick' && m.arguments[0] === 1)).toBe(true);
});
it('should handle different OSC data types correctly', async () => {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port: 9002 },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
const endpointId = createResponse.endpointId;
await new Promise(resolve => setTimeout(resolve, 100));
await testSender.sendMessage(9002, '/test/int', 'i', 42);
await testSender.sendMessage(9002, '/test/float', 'f', 3.14159);
await testSender.sendMessage(9002, '/test/string', 's', 'hello world');
await testSender.sendMessage(9002, '/test/mixed', 'ifs', 123, 4.56, 'test');
await new Promise(resolve => setTimeout(resolve, 200));
const queryRequest = {
params: {
name: 'get_osc_messages',
arguments: { endpointId },
},
};
const queryResult = await callToolHandler(queryRequest);
const queryResponse = JSON.parse(queryResult.content[0].text);
expect(queryResponse.messages).toHaveLength(4);
const messages = queryResponse.messages;
const intMsg = messages.find((m) => m.address === '/test/int');
expect(intMsg).toBeDefined();
expect(intMsg.typeTags).toBe('i');
expect(intMsg.arguments[0]).toBe(42);
const floatMsg = messages.find((m) => m.address === '/test/float');
expect(floatMsg).toBeDefined();
expect(floatMsg.typeTags).toBe('f');
expect(floatMsg.arguments[0]).toBeCloseTo(3.14159, 5);
const stringMsg = messages.find((m) => m.address === '/test/string');
expect(stringMsg).toBeDefined();
expect(stringMsg.typeTags).toBe('s');
expect(stringMsg.arguments[0]).toBe('hello world');
const mixedMsg = messages.find((m) => m.address === '/test/mixed');
expect(mixedMsg).toBeDefined();
expect(mixedMsg.typeTags).toBe('ifs');
expect(mixedMsg.arguments).toEqual([123, expect.closeTo(4.56, 2), 'test']);
});
it('should filter messages by address pattern', async () => {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: {
port: 9003,
addressFilters: ['/synth/*', '/drum/kick'],
},
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
const endpointId = createResponse.endpointId;
await new Promise(resolve => setTimeout(resolve, 100));
await testSender.sendMessage(9003, '/synth/freq', 'f', 440.0);
await testSender.sendMessage(9003, '/synth/amp', 'f', 0.8);
await testSender.sendMessage(9003, '/drum/kick', 'i', 1);
await testSender.sendMessage(9003, '/drum/snare', 'i', 1);
await testSender.sendMessage(9003, '/fx/reverb', 'f', 0.3);
await new Promise(resolve => setTimeout(resolve, 200));
const queryRequest = {
params: {
name: 'get_osc_messages',
arguments: { endpointId },
},
};
const queryResult = await callToolHandler(queryRequest);
const queryResponse = JSON.parse(queryResult.content[0].text);
expect(queryResponse.messages).toHaveLength(3);
const addresses = queryResponse.messages.map((m) => m.address);
expect(addresses).toContain('/synth/freq');
expect(addresses).toContain('/synth/amp');
expect(addresses).toContain('/drum/kick');
expect(addresses).not.toContain('/drum/snare');
expect(addresses).not.toContain('/fx/reverb');
});
});
describe('Multiple Concurrent Endpoints', () => {
it('should handle multiple endpoints receiving messages simultaneously', async () => {
const endpoints = [];
const ports = [9010, 9011, 9012];
for (const port of ports) {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port, bufferSize: 50 },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
expect(createResponse.status).toBe('active');
endpoints.push(createResponse.endpointId);
}
await new Promise(resolve => setTimeout(resolve, 100));
await testSender.sendMessage(9010, '/endpoint1/test', 'i', 1);
await testSender.sendMessage(9011, '/endpoint2/test', 'i', 2);
await testSender.sendMessage(9012, '/endpoint3/test', 'i', 3);
await testSender.sendMessage(9010, '/shared/msg', 's', 'port1');
await testSender.sendMessage(9011, '/shared/msg', 's', 'port2');
await new Promise(resolve => setTimeout(resolve, 200));
for (let i = 0; i < endpoints.length; i++) {
const queryRequest = {
params: {
name: 'get_osc_messages',
arguments: { endpointId: endpoints[i] },
},
};
const queryResult = await callToolHandler(queryRequest);
const queryResponse = JSON.parse(queryResult.content[0].text);
expect(queryResponse.messages.length).toBeGreaterThan(0);
const hasEndpointSpecificMessage = queryResponse.messages.some((m) => m.address === `/endpoint${i + 1}/test`);
expect(hasEndpointSpecificMessage).toBe(true);
}
const allQueryRequest = {
params: {
name: 'get_osc_messages',
arguments: {},
},
};
const allQueryResult = await callToolHandler(allQueryRequest);
const allQueryResponse = JSON.parse(allQueryResult.content[0].text);
expect(allQueryResponse.messages.length).toBe(5);
expect(allQueryResponse.totalCount).toBe(5);
});
it('should handle concurrent message sending to same endpoint', async () => {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port: 9020, bufferSize: 200 },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
const endpointId = createResponse.endpointId;
await new Promise(resolve => setTimeout(resolve, 100));
const messageCount = 50;
await testSender.sendRapidMessages(9020, messageCount, '/rapid');
await new Promise(resolve => setTimeout(resolve, 500));
const queryRequest = {
params: {
name: 'get_osc_messages',
arguments: { endpointId },
},
};
const queryResult = await callToolHandler(queryRequest);
const queryResponse = JSON.parse(queryResult.content[0].text);
expect(queryResponse.messages.length).toBe(messageCount);
expect(queryResponse.totalCount).toBe(messageCount);
const addresses = queryResponse.messages.map((m) => m.address);
for (let i = 0; i < messageCount; i++) {
expect(addresses).toContain(`/rapid/${i}`);
}
});
});
describe('MCP Tool Execution with Real OSC Data Flow', () => {
it('should demonstrate complete MCP workflow with real OSC messages', async () => {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port: 9030, bufferSize: 100 },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
expect(createResponse.status).toBe('active');
const endpointId = createResponse.endpointId;
const statusRequest = {
params: {
name: 'get_endpoint_status',
arguments: { endpointId },
},
};
const statusResult = await callToolHandler(statusRequest);
const statusResponse = JSON.parse(statusResult.content[0].text);
expect(statusResponse.endpoints).toHaveLength(1);
expect(statusResponse.endpoints[0].status).toBe('active');
expect(statusResponse.endpoints[0].messageCount).toBe(0);
await new Promise(resolve => setTimeout(resolve, 100));
await testSender.sendMessage(9030, '/workflow/step1', 'i', 1);
await testSender.sendMessage(9030, '/workflow/step2', 'f', 2.5);
await testSender.sendMessage(9030, '/workflow/step3', 's', 'complete');
await new Promise(resolve => setTimeout(resolve, 200));
const queryRequest = {
params: {
name: 'get_osc_messages',
arguments: { endpointId, limit: 10 },
},
};
const queryResult = await callToolHandler(queryRequest);
const queryResponse = JSON.parse(queryResult.content[0].text);
expect(queryResponse.messages).toHaveLength(3);
expect(queryResponse.totalCount).toBe(3);
const filteredQueryRequest = {
params: {
name: 'get_osc_messages',
arguments: {
endpointId,
addressPattern: '/workflow/step*',
limit: 5,
},
},
};
const filteredQueryResult = await callToolHandler(filteredQueryRequest);
const filteredQueryResponse = JSON.parse(filteredQueryResult.content[0].text);
expect(filteredQueryResponse.messages).toHaveLength(3);
const finalStatusResult = await callToolHandler(statusRequest);
const finalStatusResponse = JSON.parse(finalStatusResult.content[0].text);
expect(finalStatusResponse.endpoints[0].messageCount).toBe(3);
const stopRequest = {
params: {
name: 'stop_osc_endpoint',
arguments: { endpointId },
},
};
const stopResult = await callToolHandler(stopRequest);
const stopResponse = JSON.parse(stopResult.content[0].text);
expect(stopResponse.message).toContain('stopped successfully');
const finalStatusCheck = await callToolHandler(statusRequest);
const finalStatusCheckResponse = JSON.parse(finalStatusCheck.content[0].text);
expect(finalStatusCheckResponse.endpoints).toHaveLength(0);
});
it('should handle time-based message queries with real data', async () => {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port: 9031 },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
const endpointId = createResponse.endpointId;
await new Promise(resolve => setTimeout(resolve, 100));
await testSender.sendMessage(9031, '/time/batch1', 'i', 1);
await testSender.sendMessage(9031, '/time/batch1', 'i', 2);
await new Promise(resolve => setTimeout(resolve, 1000));
await testSender.sendMessage(9031, '/time/batch2', 'i', 3);
await testSender.sendMessage(9031, '/time/batch2', 'i', 4);
await new Promise(resolve => setTimeout(resolve, 200));
const recentQueryRequest = {
params: {
name: 'get_osc_messages',
arguments: {
endpointId,
timeWindowSeconds: 2,
},
},
};
const recentQueryResult = await callToolHandler(recentQueryRequest);
const recentQueryResponse = JSON.parse(recentQueryResult.content[0].text);
expect(recentQueryResponse.messages).toHaveLength(4);
const veryRecentQueryRequest = {
params: {
name: 'get_osc_messages',
arguments: {
endpointId,
timeWindowSeconds: 1,
},
},
};
const veryRecentQueryResult = await callToolHandler(veryRecentQueryRequest);
const veryRecentQueryResponse = JSON.parse(veryRecentQueryResult.content[0].text);
expect(veryRecentQueryResponse.messages.length).toBeLessThanOrEqual(4);
const recentAddresses = veryRecentQueryResponse.messages.map((m) => m.address);
recentAddresses.forEach((addr) => {
expect(addr).toBe('/time/batch2');
});
});
});
describe('Resource Cleanup and Memory Management', () => {
it('should properly clean up resources when endpoints are stopped', async () => {
const endpointIds = [];
const ports = [9040, 9041, 9042];
for (const port of ports) {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port, bufferSize: 50 },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
endpointIds.push(createResponse.endpointId);
}
await new Promise(resolve => setTimeout(resolve, 100));
for (let i = 0; i < ports.length; i++) {
await testSender.sendMessage(ports[i], `/cleanup/test${i}`, 'i', i);
}
await new Promise(resolve => setTimeout(resolve, 200));
const allStatusRequest = {
params: {
name: 'get_endpoint_status',
arguments: {},
},
};
const allStatusResult = await callToolHandler(allStatusRequest);
const allStatusResponse = JSON.parse(allStatusResult.content[0].text);
expect(allStatusResponse.endpoints).toHaveLength(3);
for (let i = 0; i < endpointIds.length; i++) {
const stopRequest = {
params: {
name: 'stop_osc_endpoint',
arguments: { endpointId: endpointIds[i] },
},
};
const stopResult = await callToolHandler(stopRequest);
const stopResponse = JSON.parse(stopResult.content[0].text);
expect(stopResponse.message).toContain('stopped successfully');
const statusResult = await callToolHandler(allStatusRequest);
const statusResponse = JSON.parse(statusResult.content[0].text);
expect(statusResponse.endpoints).toHaveLength(2 - i);
}
const finalStatusResult = await callToolHandler(allStatusRequest);
const finalStatusResponse = JSON.parse(finalStatusResult.content[0].text);
expect(finalStatusResponse.endpoints).toHaveLength(0);
const reuseRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port: 9040 },
},
};
const reuseResult = await callToolHandler(reuseRequest);
const reuseResponse = JSON.parse(reuseResult.content[0].text);
expect(reuseResponse.status).toBe('active');
});
it('should handle message buffer overflow correctly', async () => {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port: 9050, bufferSize: 10 },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
const endpointId = createResponse.endpointId;
await new Promise(resolve => setTimeout(resolve, 100));
const messageCount = 20;
for (let i = 0; i < messageCount; i++) {
await testSender.sendMessage(9050, `/overflow/${i}`, 'i', i);
await new Promise(resolve => setTimeout(resolve, 10));
}
await new Promise(resolve => setTimeout(resolve, 200));
const queryRequest = {
params: {
name: 'get_osc_messages',
arguments: { endpointId },
},
};
const queryResult = await callToolHandler(queryRequest);
const queryResponse = JSON.parse(queryResult.content[0].text);
expect(queryResponse.messages.length).toBeLessThanOrEqual(10);
expect(queryResponse.totalCount).toBe(10);
const addresses = queryResponse.messages.map((m) => m.address);
const hasRecentMessages = addresses.some((addr) => {
const parts = addr.split('/');
const indexStr = parts[2];
return indexStr && parseInt(indexStr) >= messageCount - 10;
});
expect(hasRecentMessages).toBe(true);
});
it('should handle graceful shutdown of all resources', async () => {
const endpointIds = [];
const ports = [9060, 9061, 9062];
for (const port of ports) {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
endpointIds.push(createResponse.endpointId);
}
await new Promise(resolve => setTimeout(resolve, 100));
for (let i = 0; i < ports.length; i++) {
await testSender.sendMessage(ports[i], `/shutdown/test${i}`, 'i', i);
}
await new Promise(resolve => setTimeout(resolve, 200));
const statusRequest = {
params: {
name: 'get_endpoint_status',
arguments: {},
},
};
const statusResult = await callToolHandler(statusRequest);
const statusResponse = JSON.parse(statusResult.content[0].text);
expect(statusResponse.endpoints).toHaveLength(3);
await oscManager.shutdown();
const finalStatusResult = await callToolHandler(statusRequest);
const finalStatusResponse = JSON.parse(finalStatusResult.content[0].text);
expect(finalStatusResponse.endpoints).toHaveLength(0);
const newManager = (0, manager_1.createOSCManager)();
const config = { port: 9060 };
const createResponse = await newManager.createEndpoint(config);
expect(createResponse.status).toBe('active');
await newManager.shutdown();
});
it('should handle memory usage efficiently with large message volumes', async () => {
const createRequest = {
params: {
name: 'create_osc_endpoint',
arguments: { port: 9070, bufferSize: 1000 },
},
};
const createResult = await callToolHandler(createRequest);
const createResponse = JSON.parse(createResult.content[0].text);
const endpointId = createResponse.endpointId;
await new Promise(resolve => setTimeout(resolve, 100));
const batchSize = 100;
const batches = 5;
for (let batch = 0; batch < batches; batch++) {
for (let i = 0; i < batchSize; i++) {
const messageIndex = batch * batchSize + i;
await testSender.sendMessage(9070, `/memory/test${messageIndex}`, 'ifs', messageIndex, messageIndex * 0.1, `message${messageIndex}`);
}
await new Promise(resolve => setTimeout(resolve, 50));
if (batch % 2 === 0) {
const queryRequest = {
params: {
name: 'get_osc_messages',
arguments: { endpointId, limit: 10 },
},
};
const queryResult = await callToolHandler(queryRequest);
const queryResponse = JSON.parse(queryResult.content[0].text);
expect(queryResponse.messages.length).toBeGreaterThan(0);
}
}
await new Promise(resolve => setTimeout(resolve, 300));
const finalQueryRequest = {
params: {
name: 'get_osc_messages',
arguments: { endpointId },
},
};
const finalQueryResult = await callToolHandler(finalQueryRequest);
const finalQueryResponse = JSON.parse(finalQueryResult.content[0].text);
expect(finalQueryResponse.messages.length).toBeLessThanOrEqual(1000);
expect(finalQueryResponse.totalCount).toBeLessThanOrEqual(1000);
const messages = finalQueryResponse.messages;
messages.forEach((msg) => {
expect(msg.address).toMatch(/^\/memory\/test\d+$/);
expect(msg.typeTags).toBe('ifs');
expect(msg.arguments).toHaveLength(3);
expect(typeof msg.arguments[0]).toBe('number');
expect(typeof msg.arguments[1]).toBe('number');
expect(typeof msg.arguments[2]).toBe('string');
});
});
});
});
//# sourceMappingURL=integration.test.js.map