MCP Catalog Now Available: Simplified Discovery, Configuration, and AI Observability in Tetrate Agent Router Service

Learn more

How to Test MCP Implementations: Validation & Debugging Guide

Testing Model Context Protocol (MCP) implementations is essential for ensuring reliability, security, and performance in production environments. Comprehensive testing strategies validate that MCP servers and hosts behave correctly, handle errors gracefully, and maintain security across the MCP architecture.

Why Test MCP Implementations?

MCP testing provides critical benefits:

  • Reliability: Ensure servers and hosts handle requests correctly across all scenarios
  • Security: Validate authentication, authorization, and input sanitization
  • Performance: Verify response times and resource utilization meet requirements
  • Interoperability: Confirm protocol compliance with MCP specification
  • Regression prevention: Catch breaking changes before deployment

MCP Testing Checklist

Before diving into specific testing approaches, use this checklist to ensure comprehensive coverage:

Protocol Compliance

  •  Server implements required protocol methods (initialize, tools/list, resources/list)
  •  Request/response messages follow JSON-RPC 2.0 format
  •  Error codes match MCP specification
  •  Capabilities are correctly advertised

Security Testing

  •  Authentication mechanisms work correctly
  •  Authorization prevents unauthorized access
  •  Input validation prevents injection attacks
  •  Sensitive data is properly sanitized

Functional Testing

  •  All tools execute correctly with valid inputs
  •  Resources return expected content
  •  Prompts generate appropriate message structures
  •  Error handling works for invalid inputs

Performance Testing

  •  Response times meet latency requirements
  •  Server handles concurrent connections
  •  Resource usage stays within acceptable limits
  •  No memory leaks under sustained load

Integration Testing

  •  Server works with multiple MCP hosts
  •  Transport mechanisms function correctly
  •  External dependencies are properly mocked

Testing MCP Servers

Unit Testing Server Components

Test individual server components in isolation to ensure correct behavior.

import { describe, it, expect, beforeEach } from 'vitest';
import { MCPServer } from './mcp-server';

describe('MCP Server', () => {
  let server: MCPServer;

  beforeEach(() => {
    server = new MCPServer({
      name: 'test-server',
      version: '1.0.0'
    });
  });

  describe('initialize', () => {
    it('should return server capabilities', async () => {
      const result = await server.initialize({
        protocolVersion: '2024-11-05',
        capabilities: {},
        clientInfo: {
          name: 'test-client',
          version: '1.0.0'
        }
      });

      expect(result).toHaveProperty('capabilities');
      expect(result).toHaveProperty('serverInfo');
      expect(result.serverInfo.name).toBe('test-server');
    });

    it('should reject incompatible protocol versions', async () => {
      await expect(
        server.initialize({
          protocolVersion: '1999-01-01',
          capabilities: {},
          clientInfo: { name: 'test', version: '1.0.0' }
        })
      ).rejects.toThrow('Unsupported protocol version');
    });
  });

  describe('tools/list', () => {
    it('should return available tools', async () => {
      const result = await server.listTools();

      expect(Array.isArray(result.tools)).toBe(true);
      expect(result.tools.length).toBeGreaterThan(0);

      const tool = result.tools[0];
      expect(tool).toHaveProperty('name');
      expect(tool).toHaveProperty('description');
      expect(tool).toHaveProperty('inputSchema');
    });
  });

  describe('tools/call', () => {
    it('should execute tool with valid arguments', async () => {
      const result = await server.callTool({
        name: 'search',
        arguments: {
          query: 'test query',
          limit: 10
        }
      });

      expect(result).toHaveProperty('content');
      expect(Array.isArray(result.content)).toBe(true);
      expect(result.isError).toBeFalsy();
    });

    it('should validate required arguments', async () => {
      await expect(
        server.callTool({
          name: 'search',
          arguments: {
            // Missing required 'query' argument
            limit: 10
          }
        })
      ).rejects.toThrow('Missing required argument: query');
    });

    it('should handle unknown tools gracefully', async () => {
      await expect(
        server.callTool({
          name: 'nonexistent_tool',
          arguments: {}
        })
      ).rejects.toThrow('Unknown tool: nonexistent_tool');
    });
  });
});

Deploy this MCP implementation on Tetrate Agent Router Service for production-ready infrastructure with built-in observability.

Try TARS Free

Testing Resource Providers

Validate that resource providers return correct content and handle errors.

describe('Resource Provider', () => {
  let server: MCPServer;

  beforeEach(() => {
    server = new MCPServer({
      name: 'file-server',
      version: '1.0.0'
    });
  });

  describe('resources/list', () => {
    it('should return available resources', async () => {
      const result = await server.listResources();

      expect(Array.isArray(result.resources)).toBe(true);

      const resource = result.resources[0];
      expect(resource).toHaveProperty('uri');
      expect(resource).toHaveProperty('name');
      expect(resource.uri).toMatch(/^file:\/\//);
    });
  });

  describe('resources/read', () => {
    it('should return resource contents', async () => {
      const result = await server.readResource({
        uri: 'file:///test/data.txt'
      });

      expect(result).toHaveProperty('contents');
      expect(Array.isArray(result.contents)).toBe(true);
      expect(result.contents[0]).toHaveProperty('text');
    });

    it('should handle non-existent resources', async () => {
      await expect(
        server.readResource({
          uri: 'file:///nonexistent/file.txt'
        })
      ).rejects.toThrow('Resource not found');
    });

    it('should prevent path traversal attacks', async () => {
      await expect(
        server.readResource({
          uri: 'file:///../../../etc/passwd'
        })
      ).rejects.toThrow('Invalid resource URI');
    });
  });
});

Testing Protocol Compliance

Ensure server responses conform to JSON-RPC 2.0 and MCP specifications.

describe('Protocol Compliance', () => {
  let server: MCPServer;

  beforeEach(() => {
    server = new MCPServer({ name: 'test', version: '1.0.0' });
  });

  it('should format responses as JSON-RPC 2.0', async () => {
    const request = {
      jsonrpc: '2.0',
      id: 1,
      method: 'tools/list',
      params: {}
    };

    const response = await server.handleRequest(request);

    expect(response).toHaveProperty('jsonrpc', '2.0');
    expect(response).toHaveProperty('id', 1);
    expect(response).toHaveProperty('result');
    expect(response).not.toHaveProperty('error');
  });

  it('should return error in JSON-RPC format', async () => {
    const request = {
      jsonrpc: '2.0',
      id: 2,
      method: 'invalid/method',
      params: {}
    };

    const response = await server.handleRequest(request);

    expect(response).toHaveProperty('jsonrpc', '2.0');
    expect(response).toHaveProperty('id', 2);
    expect(response).toHaveProperty('error');
    expect(response.error).toHaveProperty('code');
    expect(response.error).toHaveProperty('message');
  });

  it('should use standard MCP error codes', async () => {
    const request = {
      jsonrpc: '2.0',
      id: 3,
      method: 'nonexistent_method',
      params: {}
    };

    const response = await server.handleRequest(request);

    expect(response.error.code).toBe(-32601); // Method not found
  });
});

Testing MCP Clients (Hosts)

Testing Connection Establishment

describe('MCP Client', () => {
  let client: MCPClient;

  beforeEach(() => {
    client = new MCPClient({
      serverConfig: {
        command: 'test-mcp-server',
        args: []
      }
    });
  });

  afterEach(async () => {
    await client.disconnect();
  });

  it('should connect to server', async () => {
    await client.connect();
    expect(client.isConnected()).toBe(true);
  });

  it('should handle connection failures gracefully', async () => {
    const badClient = new MCPClient({
      serverConfig: {
        command: 'nonexistent-server',
        args: []
      }
    });

    await expect(badClient.connect()).rejects.toThrow('Failed to start server');
  });

  it('should initialize with server capabilities', async () => {
    await client.connect();

    const capabilities = client.getServerCapabilities();
    expect(capabilities).toHaveProperty('tools');
    expect(capabilities).toHaveProperty('resources');
  });
});

Testing Request Handling

describe('Client Requests', () => {
  let client: MCPClient;

  beforeEach(async () => {
    client = new MCPClient({
      serverConfig: {
        command: 'test-mcp-server',
        args: []
      }
    });
    await client.connect();
  });

  afterEach(async () => {
    await client.disconnect();
  });

  it('should call tools successfully', async () => {
    const result = await client.callTool('echo', {
      message: 'Hello, MCP!'
    });

    expect(result).toHaveProperty('content');
    expect(result.content[0].text).toContain('Hello, MCP!');
  });

  it('should handle request timeouts', async () => {
    await expect(
      client.callTool('slow_operation', {}, { timeout: 100 })
    ).rejects.toThrow('Request timeout');
  });

  it('should handle concurrent requests', async () => {
    const requests = Array.from({ length: 10 }, (_, i) =>
      client.callTool('echo', { message: `Request ${i}` })
    );

    const results = await Promise.all(requests);

    expect(results).toHaveLength(10);
    results.forEach((result, i) => {
      expect(result.content[0].text).toContain(`Request ${i}`);
    });
  });
});

Integration Testing

Test complete interactions between hosts and servers across transport mechanisms.

describe('MCP Integration', () => {
  describe('stdio transport', () => {
    it('should communicate via stdio', async () => {
      const client = new MCPClient({
        serverConfig: {
          command: 'python',
          args: ['-m', 'mcp_server_example']
        },
        transport: {
          type: 'stdio'
        }
      });

      await client.connect();
      const tools = await client.listTools();
      expect(tools.tools.length).toBeGreaterThan(0);
      await client.disconnect();
    });
  });

  describe('HTTP transport', () => {
    it('should communicate via HTTP/SSE', async () => {
      const client = new MCPClient({
        transport: {
          type: 'http',
          url: 'http://localhost:3000/mcp'
        }
      });

      await client.connect();
      const resources = await client.listResources();
      expect(Array.isArray(resources.resources)).toBe(true);
      await client.disconnect();
    });
  });
});

Security Testing

Validate security controls to ensure MCP security best practices are properly implemented.

describe('MCP Security', () => {
  describe('Authentication', () => {
    it('should reject unauthenticated requests', async () => {
      const client = new MCPClient({
        transport: {
          type: 'http',
          url: 'https://secure-server.example.com/mcp'
          // No auth credentials
        }
      });

      await expect(client.connect()).rejects.toThrow('Authentication required');
    });

    it('should accept valid API keys', async () => {
      const client = new MCPClient({
        transport: {
          type: 'http',
          url: 'https://secure-server.example.com/mcp',
          headers: {
            'Authorization': 'Bearer valid-api-key'
          }
        }
      });

      await client.connect();
      expect(client.isConnected()).toBe(true);
      await client.disconnect();
    });
  });

  describe('Input Validation', () => {
    let client: MCPClient;

    beforeEach(async () => {
      client = await createAuthenticatedClient();
    });

    afterEach(async () => {
      await client.disconnect();
    });

    it('should sanitize tool arguments', async () => {
      await expect(
        client.callTool('execute_command', {
          command: 'rm -rf /'  // Dangerous command
        })
      ).rejects.toThrow('Invalid input');
    });

    it('should prevent path traversal in resources', async () => {
      await expect(
        client.readResource({
          uri: 'file://../../sensitive/data.txt'
        })
      ).rejects.toThrow('Invalid resource URI');
    });

    it('should reject oversized inputs', async () => {
      const largeInput = 'A'.repeat(1000000); // 1MB string

      await expect(
        client.callTool('process_text', {
          text: largeInput
        })
      ).rejects.toThrow('Input exceeds maximum size');
    });
  });

  describe('Rate Limiting', () => {
    it('should enforce rate limits', async () => {
      const client = await createAuthenticatedClient();

      // Make requests in rapid succession
      const requests = Array.from({ length: 100 }, () =>
        client.callTool('echo', { message: 'test' })
      );

      await expect(Promise.all(requests)).rejects.toThrow('Rate limit exceeded');

      await client.disconnect();
    });
  });
});

Tetrate Agent Router Service provides enterprise-grade MCP routing with $5 free credit.

Get Started

Performance Testing

Measure response times, throughput, and resource utilization.

describe('MCP Performance', () => {
  let client: MCPClient;

  beforeEach(async () => {
    client = new MCPClient({
      serverConfig: {
        command: 'test-mcp-server',
        args: []
      }
    });
    await client.connect();
  });

  afterEach(async () => {
    await client.disconnect();
  });

  it('should respond within latency targets', async () => {
    const start = Date.now();
    await client.callTool('fast_operation', {});
    const duration = Date.now() - start;

    expect(duration).toBeLessThan(100); // 100ms latency target
  });

  it('should handle sustained load', async () => {
    const requestsPerSecond = 50;
    const duration = 10000; // 10 seconds
    const totalRequests = (requestsPerSecond * duration) / 1000;

    const startTime = Date.now();
    let completed = 0;
    const errors: Error[] = [];

    const makeRequest = async () => {
      try {
        await client.callTool('echo', { message: 'load test' });
        completed++;
      } catch (error) {
        errors.push(error as Error);
      }
    };

    // Generate requests at target rate
    const interval = 1000 / requestsPerSecond;
    const intervalId = setInterval(() => {
      makeRequest();
    }, interval);

    // Wait for test duration
    await new Promise(resolve => setTimeout(resolve, duration));
    clearInterval(intervalId);

    // Verify results
    const actualDuration = Date.now() - startTime;
    const actualRPS = (completed / actualDuration) * 1000;

    expect(actualRPS).toBeGreaterThan(requestsPerSecond * 0.9); // 90% target
    expect(errors.length / totalRequests).toBeLessThan(0.01); // <1% error rate
  });

  it('should not leak memory', async () => {
    const initialMemory = process.memoryUsage().heapUsed;

    // Generate significant load
    for (let i = 0; i < 1000; i++) {
      await client.callTool('echo', { message: `Request ${i}` });
    }

    // Force garbage collection if available
    if (global.gc) {
      global.gc();
    }

    const finalMemory = process.memoryUsage().heapUsed;
    const memoryGrowth = finalMemory - initialMemory;

    // Memory growth should be minimal after GC
    expect(memoryGrowth).toBeLessThan(10 * 1024 * 1024); // 10MB threshold
  });
});

Debugging MCP Implementations

Enabling Debug Logging

// Client-side debugging
const client = new MCPClient({
  serverConfig: {
    command: 'test-mcp-server',
    args: ['--debug']
  },
  logging: {
    level: 'debug',
    handler: (level, message, meta) => {
      console.log(`[${level}] ${message}`, meta);
    }
  }
});

// Server-side debugging
const server = new MCPServer({
  name: 'debug-server',
  version: '1.0.0',
  logging: {
    level: 'trace',
    includeTimestamps: true,
    includeStackTraces: true
  }
});

Inspecting Protocol Messages

// Log all JSON-RPC messages
client.on('message:sent', (message) => {
  console.log('� Sent:', JSON.stringify(message, null, 2));
});

client.on('message:received', (message) => {
  console.log('� Received:', JSON.stringify(message, null, 2));
});

// Example output:
// � Sent: {
//   "jsonrpc": "2.0",
//   "id": 1,
//   "method": "tools/call",
//   "params": {
//     "name": "search",
//     "arguments": { "query": "test" }
//   }
// }
//
// � Received: {
//   "jsonrpc": "2.0",
//   "id": 1,
//   "result": {
//     "content": [{ "type": "text", "text": "Results..." }]
//   }
// }

Common Debugging Scenarios

// Debugging connection issues
try {
  await client.connect();
} catch (error) {
  if (error.code === 'ENOENT') {
    console.error('Server executable not found. Check serverConfig.command');
  } else if (error.code === 'ETIMEDOUT') {
    console.error('Connection timeout. Server may not be responding');
  } else {
    console.error('Connection failed:', error.message);
  }
}

// Debugging protocol errors
try {
  await client.callTool('my_tool', { arg: 'value' });
} catch (error) {
  if (error.code === -32601) {
    console.error('Method not found. Tool may not be registered');
  } else if (error.code === -32602) {
    console.error('Invalid params. Check tool input schema');
  } else if (error.code === -32603) {
    console.error('Internal error:', error.message);
  }
}

// Debugging performance issues
const performanceObserver = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    console.log(`${entry.name}: ${entry.duration}ms`);
  }
});

performanceObserver.observe({ entryTypes: ['measure'] });

performance.mark('call-start');
await client.callTool('slow_operation', {});
performance.mark('call-end');
performance.measure('tool-call', 'call-start', 'call-end');

Continuous Integration Testing

Integrate MCP tests into CI/CD pipelines.

# .github/workflows/mcp-tests.yml
name: MCP Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest

    steps:
      - uses: actions/checkout@v3

      - name: Set up Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'

      - name: Install dependencies
        run: npm ci

      - name: Run unit tests
        run: npm run test:unit

      - name: Run integration tests
        run: npm run test:integration

      - name: Run security tests
        run: npm run test:security

      - name: Run performance tests
        run: npm run test:performance

      - name: Upload coverage
        uses: codecov/codecov-action@v3
        with:
          files: ./coverage/lcov.info

Test Coverage Goals

Aim for comprehensive test coverage across all MCP components:

  • Unit tests: >90% code coverage
  • Integration tests: All transport mechanisms and server types
  • Security tests: All authentication, authorization, and input validation paths
  • Performance tests: All latency-critical operations
  • Protocol compliance: 100% of required MCP methods

Best Practices

1. Test Early and Often

Implement tests before writing MCP server code (TDD approach) and run tests on every commit.

2. Use Test Fixtures

Create reusable test fixtures for common scenarios:

export function createTestServer(): MCPServer {
  return new MCPServer({
    name: 'test-server',
    version: '1.0.0',
    tools: [testTool1, testTool2],
    resources: [testResource1, testResource2]
  });
}

export function createTestClient(): MCPClient {
  return new MCPClient({
    serverConfig: { command: 'test-mcp-server', args: [] }
  });
}

3. Mock External Dependencies

Isolate tests by mocking external systems:

vi.mock('./database', () => ({
  query: vi.fn().mockResolvedValue([{ id: 1, name: 'Test' }])
}));

vi.mock('./api-client', () => ({
  fetch: vi.fn().mockResolvedValue({ status: 'success' })
}));

4. Test Error Paths

Don’t just test the happy pathensure error handling works correctly:

it('should handle database connection failures', async () => {
  vi.mocked(database.connect).mockRejectedValue(new Error('Connection failed'));

  await expect(
    server.callTool('query_database', { query: 'SELECT * FROM users' })
  ).rejects.toThrow('Database unavailable');
});

5. Monitor Test Performance

Track test execution times and optimize slow tests:

// Test should complete quickly
it('should list tools efficiently', async () => {
  const start = Date.now();
  await server.listTools();
  const duration = Date.now() - start;

  expect(duration).toBeLessThan(10); // Test should take <10ms
});

Conclusion

Comprehensive testing is essential for reliable MCP implementations. By following these testing strategies and using the provided examples, you can ensure your MCP servers and clients function correctly, perform well, and maintain security in production environments.

Effective testing combined with performance monitoring, implementation best practices, and understanding of MCP architecture creates robust, production-ready MCP systems.

Deploy MCP in Production with TARS

Enterprise-grade MCP infrastructure in minutes

  • Native MCP Integration - Seamless protocol support out of the box
  • Advanced Observability - Monitor and optimize your MCP implementations
  • Optimized Routing - Intelligent request routing for maximum performance
  • $5 Free Credit - Start with production features at no cost
Deploy TARS Now →

Production-tested by leading AI development teams

Strengthen your MCP implementation with these essential resources:

Decorative CTA background pattern background background
Tetrate logo in the CTA section Tetrate logo in the CTA section for mobile

Ready to enhance your
network

with more
intelligence?