Skip to main content
Glama
liratanak

Tonle OpenProject MCP Server

by liratanak
transport-comparison.test.ts9.58 kB
/** * Transport Comparison Tests * Verifies that STDIO and HTTP transports produce identical results */ import { describe, test, expect, beforeAll, afterAll } from 'bun:test'; import { Client } from '@modelcontextprotocol/sdk/client/index.js'; import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js'; import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js'; import { spawn, type Subprocess } from 'bun'; const HTTP_PORT = 3198; const HTTP_URL = `http://localhost:${HTTP_PORT}/mcp`; let httpServer: Subprocess | null = null; // Create clients async function createHttpClient(): Promise<Client> { const client = new Client({ name: 'test-http-client', version: '1.0.0', }); const transport = new StreamableHTTPClientTransport(new URL(HTTP_URL)); await client.connect(transport); return client; } async function createStdioClient(): Promise<{ client: Client; process: Subprocess }> { const client = new Client({ name: 'test-stdio-client', version: '1.0.0', }); const transport = new StdioClientTransport({ command: 'bun', args: ['run', 'index.ts'], env: process.env as Record<string, string>, }); await client.connect(transport); // @ts-ignore - Access the underlying process const proc = transport._process; return { client, process: proc }; } // Helper to call a tool and extract result async function callTool(client: Client, name: string, args: Record<string, unknown> = {}): Promise<string> { const result = await client.callTool({ name, arguments: args }); const content = result.content as Array<{ type: string; text: string }>; return content[0]?.text || ''; } // Helper to normalize response for comparison (removes timestamps and varying fields) function normalizeResponse(response: string): object { try { const data = JSON.parse(response); // Remove fields that may vary between calls const removeVaryingFields = (obj: any): any => { if (Array.isArray(obj)) { return obj.map(removeVaryingFields); } if (obj && typeof obj === 'object') { const result: any = {}; for (const [key, value] of Object.entries(obj)) { // Skip timestamp and volatile fields if (['createdAt', 'updatedAt', 'lockVersion', '_links'].includes(key)) { continue; } result[key] = removeVaryingFields(value); } return result; } return obj; }; return removeVaryingFields(data); } catch { return { raw: response }; } } // Global setup and teardown beforeAll(async () => { // Start HTTP server console.log('Starting HTTP server for comparison tests...'); httpServer = spawn({ cmd: ['bun', 'run', 'http-server.ts'], cwd: process.cwd(), env: { ...process.env, MCP_HTTP_PORT: String(HTTP_PORT), }, stdout: 'pipe', stderr: 'pipe', }); // Wait for server to start await new Promise((resolve) => setTimeout(resolve, 2000)); // Verify server is running const response = await fetch(`http://localhost:${HTTP_PORT}/health`); if (!response.ok) { throw new Error('HTTP server failed to start'); } console.log('HTTP server ready for comparison tests'); }); afterAll(async () => { if (httpServer) { httpServer.kill(); await httpServer.exited; } }); describe('Transport Comparison', () => { test('both transports list same tools', async () => { const httpClient = await createHttpClient(); const { client: stdioClient } = await createStdioClient(); try { const httpTools = await httpClient.listTools(); const stdioTools = await stdioClient.listTools(); const httpToolNames = httpTools.tools.map(t => t.name).sort(); const stdioToolNames = stdioTools.tools.map(t => t.name).sort(); expect(httpToolNames).toEqual(stdioToolNames); expect(httpToolNames.length).toBeGreaterThan(30); // We have many tools } finally { await httpClient.close(); await stdioClient.close(); } }); test('get_current_user returns same user on both transports', async () => { const httpClient = await createHttpClient(); const { client: stdioClient } = await createStdioClient(); try { const httpResult = await callTool(httpClient, 'get_current_user'); const stdioResult = await callTool(stdioClient, 'get_current_user'); const httpData = normalizeResponse(httpResult); const stdioData = normalizeResponse(stdioResult); expect(httpData).toEqual(stdioData); } finally { await httpClient.close(); await stdioClient.close(); } }); test('list_projects returns same structure on both transports', async () => { const httpClient = await createHttpClient(); const { client: stdioClient } = await createStdioClient(); try { const httpResult = await callTool(httpClient, 'list_projects', { pageSize: 3 }); const stdioResult = await callTool(stdioClient, 'list_projects', { pageSize: 3 }); // Both should be valid JSON const httpData = JSON.parse(httpResult); const stdioData = JSON.parse(stdioResult); // Both should have the same structure expect(httpData._type).toBe(stdioData._type); expect(typeof httpData.total).toBe(typeof stdioData.total); expect(typeof httpData.count).toBe(typeof stdioData.count); } finally { await httpClient.close(); await stdioClient.close(); } }); test('list_types returns identical results on both transports', async () => { const httpClient = await createHttpClient(); const { client: stdioClient } = await createStdioClient(); try { const httpResult = await callTool(httpClient, 'list_types'); const stdioResult = await callTool(stdioClient, 'list_types'); const httpData = normalizeResponse(httpResult); const stdioData = normalizeResponse(stdioResult); expect(httpData).toEqual(stdioData); } finally { await httpClient.close(); await stdioClient.close(); } }); test('list_statuses returns identical results on both transports', async () => { const httpClient = await createHttpClient(); const { client: stdioClient } = await createStdioClient(); try { const httpResult = await callTool(httpClient, 'list_statuses'); const stdioResult = await callTool(stdioClient, 'list_statuses'); const httpData = normalizeResponse(httpResult); const stdioData = normalizeResponse(stdioResult); expect(httpData).toEqual(stdioData); } finally { await httpClient.close(); await stdioClient.close(); } }); test('list_priorities returns identical results on both transports', async () => { const httpClient = await createHttpClient(); const { client: stdioClient } = await createStdioClient(); try { const httpResult = await callTool(httpClient, 'list_priorities'); const stdioResult = await callTool(stdioClient, 'list_priorities'); const httpData = normalizeResponse(httpResult); const stdioData = normalizeResponse(stdioResult); expect(httpData).toEqual(stdioData); } finally { await httpClient.close(); await stdioClient.close(); } }); test('get_api_root returns same structure on both transports', async () => { const httpClient = await createHttpClient(); const { client: stdioClient } = await createStdioClient(); try { const httpResult = await callTool(httpClient, 'get_api_root'); const stdioResult = await callTool(stdioClient, 'get_api_root'); // Both should be valid JSON const httpData = JSON.parse(httpResult); const stdioData = JSON.parse(stdioResult); // Both should have _links expect(httpData._links).toBeDefined(); expect(stdioData._links).toBeDefined(); } finally { await httpClient.close(); await stdioClient.close(); } }); test('error responses are consistent across transports', async () => { const httpClient = await createHttpClient(); const { client: stdioClient } = await createStdioClient(); try { const httpResult = await callTool(httpClient, 'get_project', { id: 999999999 }); const stdioResult = await callTool(stdioClient, 'get_project', { id: 999999999 }); // Both should return errors expect(httpResult).toContain('Error'); expect(stdioResult).toContain('Error'); } finally { await httpClient.close(); await stdioClient.close(); } }); test('all tools have matching input schemas on both transports', async () => { const httpClient = await createHttpClient(); const { client: stdioClient } = await createStdioClient(); try { const httpTools = await httpClient.listTools(); const stdioTools = await stdioClient.listTools(); // Create maps for easy comparison const httpToolMap = new Map(httpTools.tools.map(t => [t.name, t])); const stdioToolMap = new Map(stdioTools.tools.map(t => [t.name, t])); // Check each tool for (const [name, httpTool] of httpToolMap) { const stdioTool = stdioToolMap.get(name); expect(stdioTool).toBeDefined(); if (stdioTool) { expect(httpTool.description).toBe(stdioTool.description); expect(JSON.stringify(httpTool.inputSchema)).toBe(JSON.stringify(stdioTool.inputSchema)); } } } finally { await httpClient.close(); await stdioClient.close(); } }); });

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/liratanak/openproject-mcp'

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