Skip to main content
Glama
internal-tools-mcp-protocol.test.ts16.8 kB
/** * E2E tests for Internal MCP Tools Protocol * * This test file validates that the internal MCP discovery tools work correctly * with the MCP protocol and return properly structured data that matches their schemas. */ import { TestFixtures } from '@test/e2e/fixtures/TestFixtures.js'; import { CommandTestEnvironment } from '@test/e2e/utils/index.js'; import { ChildProcess, spawn } from 'child_process'; import { resolve } from 'path'; import { afterEach, beforeEach, describe, expect, it } from 'vitest'; // Simple MCP client implementation for testing class SimpleMcpClient { private process: ChildProcess; private id = 1; private responses: Map<number, any> = new Map(); private buffer = ''; constructor(config: { command: string; args: string[]; env?: Record<string, string> }) { this.process = spawn(config.command, config.args, { env: { ...process.env, ...config.env }, }); this.process.stdout?.on('data', (data) => { this.buffer += data.toString(); this.processBuffer(); }); this.process.on('error', (error) => { throw new Error(`MCP process error: ${error.message}`); }); } private processBuffer() { const lines = this.buffer.split('\n'); let completeLines = 0; for (let i = 0; i < lines.length - 1; i++) { if (lines[i].trim()) { try { const response = JSON.parse(lines[i]); if (response.id) { this.responses.set(response.id, response); } completeLines = i + 1; } catch (_error) { // Skip invalid JSON lines } } } if (completeLines > 0) { this.buffer = lines.slice(completeLines).join('\n'); } } async initialize(): Promise<void> { const response = await this.request({ jsonrpc: '2.0', method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: { tools: {} }, clientInfo: { name: 'test-client', version: '1.0.0' }, }, }); if (!response.result) { throw new Error('Failed to initialize MCP connection'); } } async listTools(): Promise<any> { const response = await this.request({ jsonrpc: '2.0', method: 'tools/list', params: {}, }); return response.result; } async callTool(name: string, arguments_?: Record<string, unknown>): Promise<any> { const response = await this.request({ jsonrpc: '2.0', method: 'tools/call', params: { name, arguments: arguments_ || {}, }, }); return response.result; } private async request(request: any): Promise<any> { const id = this.id++; request.id = id; this.process.stdin?.write(JSON.stringify(request) + '\n'); // Wait for response let attempts = 0; const maxAttempts = 50; while (attempts < maxAttempts) { if (this.responses.has(id)) { const response = this.responses.get(id); this.responses.delete(id); if (response.error) { throw new Error(`MCP error: ${response.error.message}`); } return response; } await new Promise((resolve) => setTimeout(resolve, 100)); attempts++; } throw new Error('Request timeout'); } async disconnect(): Promise<void> { this.process.kill(); } } describe('Internal MCP Tools Protocol E2E Tests', () => { let environment: CommandTestEnvironment; let mcpClient: SimpleMcpClient; beforeEach(async () => { environment = new CommandTestEnvironment(TestFixtures.createTestScenario('internal-mcp-tools-test', 'empty')); await environment.setup(); // Initialize simple MCP client with stdio transport mcpClient = new SimpleMcpClient({ command: 'node', args: [resolve(__dirname, '../../build/index.js'), '--transport', 'stdio', '--enable-internal-tools'], env: { ONE_MCP_CONFIG_DIR: (environment as any).getConfigDir(), NODE_ENV: 'test', }, }); await mcpClient.initialize(); }); afterEach(async () => { try { await mcpClient.disconnect(); } catch (_error) { // Ignore disconnect errors } await environment.cleanup(); }); describe('MCP Tool Discovery', () => { it('should list all available tools including internal discovery tools', async () => { const toolsResponse = await mcpClient.listTools(); expect(toolsResponse).toBeDefined(); expect(Array.isArray(toolsResponse.tools)).toBe(true); // Find internal discovery tools const discoveryTools = toolsResponse.tools.filter( (tool: any) => tool.name.startsWith('1mcp_1mcp_') && ['mcp_search', 'mcp_registry_status', 'mcp_registry_info', 'mcp_registry_list', 'mcp_info'].some((name) => tool.name.includes(name), ), ); expect(discoveryTools.length).toBeGreaterThan(0); // Verify each discovery tool has proper schema for (const tool of discoveryTools) { expect(tool.name).toMatch(/^1mcp_1mcp_/); expect(tool.description).toBeDefined(); expect(typeof tool.description).toBe('string'); expect(tool.inputSchema).toBeDefined(); expect(tool.outputSchema).toBeDefined(); } }); it('should validate mcp_search tool schema', async () => { const toolsResponse = await mcpClient.listTools(); const searchTool = toolsResponse.tools.find((tool: any) => tool.name === '1mcp_1mcp_mcp_search'); expect(searchTool).toBeDefined(); expect(searchTool?.inputSchema).toBeDefined(); expect(searchTool?.outputSchema).toBeDefined(); // Verify input schema has expected properties expect(searchTool?.inputSchema.properties).toHaveProperty('query'); expect(searchTool?.inputSchema.properties).toHaveProperty('limit'); expect(searchTool?.inputSchema.properties).toHaveProperty('status'); // Verify output schema has expected structure expect(searchTool?.outputSchema.properties).toHaveProperty('results'); expect(searchTool?.outputSchema.properties).toHaveProperty('total'); expect(searchTool?.outputSchema.properties).toHaveProperty('query'); expect(searchTool?.outputSchema.properties).toHaveProperty('registry'); }); }); describe('MCP Tool Execution - Structured Output Validation', () => { it('should execute mcp_search and return structured data', async () => { const result = await mcpClient.callTool('1mcp_1mcp_mcp_search', { query: 'filesystem', limit: 5, }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(Array.isArray(result.content)).toBe(true); // Should have content since we fixed the handlers to return structured data expect(result.content.length).toBeGreaterThan(0); // The content should be structured data (not text) const content = result.content[0]; expect(content).toBeDefined(); // The content should be text containing JSON matching the schema expect(content.type).toBe('text'); expect(content.text).toBeDefined(); // Parse and validate the structured response const parsed = JSON.parse(content.text); expect(parsed).toHaveProperty('results'); expect(parsed).toHaveProperty('total'); expect(parsed).toHaveProperty('query'); expect(parsed).toHaveProperty('registry'); expect(Array.isArray(parsed.results)).toBe(true); expect(typeof parsed.total).toBe('number'); expect(typeof parsed.query).toBe('string'); expect(typeof parsed.registry).toBe('string'); }); it('should execute mcp_registry_status and return structured data', async () => { const result = await mcpClient.callTool('1mcp_1mcp_mcp_registry_status', { registry: 'official', includeStats: false, // Set to false to avoid timeout }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(Array.isArray(result.content)).toBe(true); expect(result.content.length).toBeGreaterThan(0); const content = result.content[0]; expect(content).toBeDefined(); // The content should be text containing JSON expect(content.type).toBe('text'); expect(content.text).toBeDefined(); const parsed = JSON.parse(content.text); expect(parsed).toHaveProperty('registry'); expect(parsed).toHaveProperty('status'); expect(parsed).toHaveProperty('lastCheck'); expect(['online', 'offline', 'error']).toContain(parsed.status); expect(typeof parsed.registry).toBe('string'); }); it('should execute mcp_registry_info and return structured data', async () => { const result = await mcpClient.callTool('1mcp_1mcp_mcp_registry_info', { registry: 'official', }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(Array.isArray(result.content)).toBe(true); expect(result.content.length).toBeGreaterThan(0); const content = result.content[0]; expect(content).toBeDefined(); // The content should be text containing JSON expect(content.type).toBe('text'); expect(content.text).toBeDefined(); const parsed = JSON.parse(content.text); expect(parsed).toHaveProperty('name'); expect(parsed).toHaveProperty('url'); expect(parsed).toHaveProperty('description'); expect(parsed).toHaveProperty('supportedFormats'); expect(parsed).toHaveProperty('features'); expect(Array.isArray(parsed.supportedFormats)).toBe(true); expect(Array.isArray(parsed.features)).toBe(true); }); it('should execute mcp_registry_list and return structured data', async () => { const result = await mcpClient.callTool('1mcp_1mcp_mcp_registry_list', { includeStats: false, }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(Array.isArray(result.content)).toBe(true); expect(result.content.length).toBeGreaterThan(0); const content = result.content[0]; expect(content).toBeDefined(); // The content should be text containing JSON expect(content.type).toBe('text'); expect(content.text).toBeDefined(); const parsed = JSON.parse(content.text); expect(parsed).toHaveProperty('registries'); expect(parsed).toHaveProperty('total'); expect(Array.isArray(parsed.registries)).toBe(true); expect(typeof parsed.total).toBe('number'); // Validate registry structure if (parsed.registries.length > 0) { const registry = parsed.registries[0]; expect(registry).toHaveProperty('name'); expect(registry).toHaveProperty('url'); expect(registry).toHaveProperty('status'); expect(registry).toHaveProperty('description'); expect(['online', 'offline', 'unknown']).toContain(registry.status); } }); it('should handle mcp_info for non-existent server', async () => { // This test may fail if the server info tool has issues, so let's handle it gracefully try { const result = await mcpClient.callTool('1mcp_1mcp_mcp_info', { name: 'non-existent-server-12345', }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(Array.isArray(result.content)).toBe(true); expect(result.content.length).toBeGreaterThan(0); const content = result.content[0]; expect(content).toBeDefined(); // The content should be text containing JSON expect(content.type).toBe('text'); expect(content.text).toBeDefined(); const parsed = JSON.parse(content.text); expect(parsed).toHaveProperty('server'); expect(parsed.server).toHaveProperty('name'); expect(parsed.server).toHaveProperty('status'); expect(parsed.server).toHaveProperty('transport'); expect(parsed.server.name).toBe('non-existent-server-12345'); } catch (error) { // If the tool fails, that's also acceptable behavior for this test expect(error).toBeDefined(); expect((error as Error).message).toContain('MCP error'); } }); }); describe('Error Handling with Structured Output', () => { it('should handle invalid arguments gracefully', async () => { // Test with invalid arguments that should trigger validation errors try { const result = await mcpClient.callTool('1mcp_1mcp_mcp_search', { query: 'test', limit: -1, // Invalid negative limit }); // Should either succeed with validation errors or fail gracefully expect(result).toBeDefined(); } catch (error) { // Should be a proper MCP error, not a crash expect(error).toBeDefined(); expect(typeof error).toBe('object'); } }); it('should handle network errors in registry operations gracefully', async () => { // Mock a network failure by using an invalid registry try { const result = await mcpClient.callTool('1mcp_1mcp_mcp_registry_status', { registry: 'http://invalid-registry-url-that-does-not-exist.com', }); // Should either return error information or fail gracefully expect(result).toBeDefined(); } catch (error) { // Should be a proper MCP error expect(error).toBeDefined(); } }); }); describe('Protocol Compliance', () => { it('should support ping operation', async () => { // Since our SimpleMcpClient doesn't have ping, we'll test that the connection is still alive const result = await mcpClient.listTools(); expect(result).toBeDefined(); expect(result.tools).toBeDefined(); }); it('should maintain connection health across multiple calls', async () => { // Make multiple calls await mcpClient.callTool('1mcp_1mcp_mcp_registry_status', {}); await mcpClient.callTool('1mcp_1mcp_mcp_registry_list', {}); await mcpClient.callTool('1mcp_1mcp_mcp_registry_info', {}); // Connection should still be alive by listing tools const result = await mcpClient.listTools(); expect(result).toBeDefined(); expect(result.tools).toBeDefined(); }); it('should handle concurrent requests', async () => { // Make multiple concurrent calls const promises = [ mcpClient.callTool('1mcp_1mcp_mcp_search', { query: 'test1' }), mcpClient.callTool('1mcp_1mcp_mcp_search', { query: 'test2' }), mcpClient.callTool('1mcp_1mcp_mcp_registry_status', {}), ]; const results = await Promise.allSettled(promises); // All requests should complete (either successfully or with proper errors) expect(results.length).toBe(3); results.forEach((result) => { expect(result.status).toBe('fulfilled'); }); }); }); describe('Schema Validation Edge Cases', () => { it('should validate empty search results structure', async () => { const result = await mcpClient.callTool('1mcp_1mcp_mcp_search', { query: 'non-existent-server-name-xyz-123', limit: 1, }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); const content = result.content[0]; // Even empty results should have proper structure expect(content.type).toBe('text'); expect(content.text).toBeDefined(); const parsed = JSON.parse(content.text); expect(parsed).toHaveProperty('results'); expect(parsed).toHaveProperty('total'); expect(parsed).toHaveProperty('query'); expect(parsed).toHaveProperty('registry'); expect(Array.isArray(parsed.results)).toBe(true); expect(parsed.total).toBe(0); expect(parsed.results.length).toBe(0); }); it('should validate maximum limits are enforced', async () => { // Test with reasonable high limit (avoiding the error that occurs with very high limits) try { const result = await mcpClient.callTool('1mcp_1mcp_mcp_search', { query: 'test', limit: 100, // High but reasonable limit }); expect(result).toBeDefined(); expect(result.content).toBeDefined(); expect(result.content.length).toBeGreaterThan(0); // Should not crash and should return reasonable results const content = result.content[0]; expect(content).toBeDefined(); } catch (error) { // High limits may cause errors, which is also acceptable behavior expect(error).toBeDefined(); expect((error as Error).message).toContain('MCP error'); } }); }); });

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/1mcp-app/agent'

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