mcp-server.test.ts•6.07 kB
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
import { JSONValidator } from '../../src/utils/json-validator.js';
import { JsonRpcValidator } from '../../src/utils/json-rpc-validator.js';
import { ConfigurationManager } from '../../src/config/manager.js';
describe('MCP Server JSON-RPC Response Validation', () => {
  beforeEach(() => {
    // Mock environment variables for testing
    vi.stubEnv(
      'OPENROUTER_API_KEY',
      'sk-or-v1-test-key-that-is-long-enough-for-validation-12345'
    );
    vi.stubEnv('NODE_ENV', 'test');
    // Reset singleton instance
    ConfigurationManager.reset();
  });
  afterEach(() => {
    vi.restoreAllMocks();
  });
  describe('list_tools response format', () => {
    it('should produce valid JSON-RPC 2.0 response for tools list', () => {
      // Arrange: Create the same response structure that the server creates
      const toolsResponse = {
        tools: [
          {
            name: 'search',
            description:
              'Nexus AI-powered search using Perplexity models via OpenRouter. Searches the web for current information and provides comprehensive answers with sources.',
            inputSchema: {
              type: 'object',
              properties: {
                query: {
                  type: 'string',
                  description:
                    'The search query to process (required, 1-2000 characters)',
                  minLength: 1,
                  maxLength: 2000,
                },
                model: {
                  type: 'string',
                  description: 'Perplexity model to use for search',
                  enum: ['perplexity/sonar'],
                  default: 'perplexity/sonar',
                },
                maxTokens: {
                  type: 'number',
                  description:
                    'Maximum number of tokens in the response (1-4000)',
                  minimum: 1,
                  maximum: 4000,
                  default: 1000,
                },
                temperature: {
                  type: 'number',
                  description: 'Controls randomness in the response (0-2)',
                  minimum: 0,
                  maximum: 2,
                  default: 0.3,
                },
              },
              required: ['query'],
            },
          },
        ],
      };
      // Act: Process the response through the same validation that the server uses
      const wrappedResponse = JSONValidator.wrapMCPResponse(toolsResponse);
      // Assert: The response should be valid JSON-RPC 2.0
      expect(wrappedResponse).toBeDefined();
      expect(wrappedResponse.jsonrpc).toBe('2.0');
      expect('id' in wrappedResponse).toBe(true);
      // The response should either have 'result' or 'error', not both
      expect('result' in wrappedResponse !== 'error' in wrappedResponse).toBe(
        true
      );
      // CRITICAL: The response should contain the tools, not an error
      // This is the key assertion that will fail and demonstrate the bug
      expect('result' in wrappedResponse).toBe(true);
      expect('error' in wrappedResponse).toBe(false);
      // Validate using our JSON-RPC validator
      const validationResult =
        JsonRpcValidator.validateMessage(wrappedResponse);
      if (!validationResult.valid) {
        console.error('Validation errors:', validationResult.errors);
        console.error(
          'Actual response:',
          JSON.stringify(wrappedResponse, null, 2)
        );
      }
      expect(validationResult.valid).toBe(true);
      expect(validationResult.errors).toHaveLength(0);
      // Should contain tools array (this will fail because we get an error response)
      if (
        'result' in wrappedResponse &&
        wrappedResponse.result &&
        typeof wrappedResponse.result === 'object'
      ) {
        const result = wrappedResponse.result as any;
        expect(result.tools).toBeDefined();
        expect(Array.isArray(result.tools)).toBe(true);
        // Each tool should have required properties
        result.tools.forEach((tool: any) => {
          expect(tool.name).toBeDefined();
          expect(tool.description).toBeDefined();
          expect(tool.inputSchema).toBeDefined();
        });
      }
    });
    it('should handle empty tools list with valid JSON-RPC response', () => {
      // Arrange: Empty tools response
      const emptyToolsResponse = { tools: [] };
      // Act
      const wrappedResponse = JSONValidator.wrapMCPResponse(emptyToolsResponse);
      // Assert
      expect(wrappedResponse.jsonrpc).toBe('2.0');
      const validationResult =
        JsonRpcValidator.validateMessage(wrappedResponse);
      expect(validationResult.valid).toBe(true);
      if (
        'result' in wrappedResponse &&
        wrappedResponse.result &&
        typeof wrappedResponse.result === 'object'
      ) {
        const result = wrappedResponse.result as any;
        expect(result.tools).toEqual([]);
      }
    });
    it('should be serializable and parseable without corruption', () => {
      // Arrange
      const toolsResponse = {
        tools: [
          {
            name: 'search',
            description: 'Test tool',
            inputSchema: {
              type: 'object',
              properties: { query: { type: 'string' } },
              required: ['query'],
            },
          },
        ],
      };
      // Act
      const wrappedResponse = JSONValidator.wrapMCPResponse(toolsResponse);
      // Assert: Should be serializable without errors
      expect(() => JSON.stringify(wrappedResponse)).not.toThrow();
      // Should be parseable back to same structure
      const serialized = JSON.stringify(wrappedResponse);
      const parsed = JSON.parse(serialized);
      expect(parsed).toEqual(wrappedResponse);
      // Parsed version should still be valid JSON-RPC
      const reparsedValidation = JsonRpcValidator.validateMessage(parsed);
      expect(reparsedValidation.valid).toBe(true);
    });
  });
});