agentleFS
Hack23Hack23/European-Parliament-MCP-Server/.github/skills/testing-mcp-tools/SKILL.md

testing-mcp-tools

Skill30 starsChanged 9 months agoApache-2.0View on GitHub

Testing patterns for MCP tools, integration tests, mocking European Parliament API, and achieving 80%+ coverage

We split this skill into the rules it gives an agent

9 rules. About 0 are common, 9 are distinctive, and 0 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈9

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 0

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Test All MCP Handlers: Every tool, resource, and prompt must have tests

    line 26
  2. Distinctive:

    Mock External APIs: Never call real European Parliament API in tests

    line 27
  3. Distinctive:

    Test Input Validation: Verify Zod schema validation with valid/invalid inputs

    line 28
  4. Distinctive:

    Test Error Paths: Test all error scenarios and exception handling

    line 29
  5. Distinctive:

    Use Descriptive Names: Test names should describe what is being tested

    line 31
  6. Distinctive:

    Test Response Structure: Verify MCP-compliant response formats

    line 33
  7. Distinctive:

    Integration Tests: Test full MCP request/response cycle

    line 35
  8. Distinctive:

    SC-002: Test security validations (negative / injection / boundary)

    line 420
  9. Distinctive:

    AU-002: Test audit-logging paths

    line 421

≈ “Common” is an estimate: at least 1 in 5 of the 308,232 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Testing MCP Tools Skill

Context

This skill applies when:

  • Writing unit tests for MCP tools, resources, and prompts
  • Testing European Parliament API integrations
  • Mocking external API calls
  • Writing integration tests for MCP server
  • Achieving 80%+ code coverage target
  • Testing error handling and edge cases
  • Validating Zod schema validation
  • Performance testing for response times
  • Testing GDPR compliance features

This project uses Vitest as the test framework with a coverage target of 80% line coverage and 70% branch coverage.

Rules

  1. Test All MCP Handlers: Every tool, resource, and prompt must have tests
  2. Mock External APIs: Never call real European Parliament API in tests
  3. Test Input Validation: Verify Zod schema validation with valid/invalid inputs
  4. Test Error Paths: Test all error scenarios and exception handling
  5. Achieve Coverage Target: 80% line coverage, 70% branch coverage minimum
  6. Use Descriptive Names: Test names should describe what is being tested
  7. Arrange-Act-Assert: Structure tests with clear AAA pattern
  8. Test Response Structure: Verify MCP-compliant response formats
  9. Test Edge Cases: Empty inputs, max lengths, boundary conditions
  10. Integration Tests: Test full MCP request/response cycle

Examples

✅ Good Pattern: Testing MCP Tool

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { handleSearchMEPs } from './search-meps';

describe('MCP Tool: search_meps', () => {
  beforeEach(() => {
    // Reset mocks before each test
    vi.clearAllMocks();
  });
  
  it('should validate input schema', async () => {
    const invalidRequest = {
      params: {
        name: 'search_meps',
        arguments: {
          country: 'USA',  // Invalid: not EU country
          limit: 200,      // Invalid: exceeds max (100)
        },
      },
    };
    
    await expect(handleSearchMEPs(invalidRequest)).rejects.toThrow();
  });
  
  it('should return MCP-compliant response structure', async () => {
    // Mock API
    vi.mock('./api', () => ({
      searchMEPs: vi.fn().mockResolvedValue([
        { id: 1, fullName: 'Test MEP', country: 'DE' },
      ]),
    }));
    
    const validRequest = {
      params: {
        name: 'search_meps',
        arguments: {
          country: 'DE',
          limit: 10,
        },
      },
    };
    
    const response = await handleSearchMEPs(validRequest);
    
    // Verify MCP response structure
    expect(response).toHaveProperty('content');
    expect(Array.isArray(response.content)).toBe(true);
    expect(response.content[0]).toHaveProperty('type', 'text');
    expect(response.content[0]).toHaveProperty('text');
    
    // Verify response content
    const data = JSON.parse(response.content[0].text);
    expect(data).toHaveProperty('count');
    expect(data).toHaveProperty('meps');
    expect(Array.isArray(data.meps)).toBe(true);
  });
  
  it('should handle API errors gracefully', async () => {
    // Mock API failure
    vi.mock('./api', () => ({
      searchMEPs: vi.fn().mockRejectedValue(new Error('API Error')),
    }));
    
    const request = {
      params: {
        name: 'search_meps',
        arguments: { country: 'DE' },
      },
    };
    
    await expect(handleSearchMEPs(request)).rejects.toThrow('Failed to search MEPs');
    // Verify internal error is NOT exposed
  });
  
  it('should reject invalid characters in country code', async () => {
    const request = {
      params: {
        name: 'search_meps',
        arguments: {
          country: '<script>alert("xss")</script>',
        },
      },
    };
    
    await expect(handleSearchMEPs(request)).rejects.toThrow();
  });
  
  it('should apply default values for optional parameters', async () => {
    vi.mock('./api', () => ({
      searchMEPs: vi.fn().mockResolvedValue([]),
    }));
    
    const request = {
      params: {
        name: 'search_meps',
        arguments: {
          country: 'DE',
          // limit not provided - should default to 20
        },
      },
    };
    
    await handleSearchMEPs(request);
    
    const apiMock = await import('./api');
    expect(apiMock.searchMEPs).toHaveBeenCalledWith(
      expect.objectContaining({ limit: 20 })
    );
  });
});

✅ Good Pattern: Mocking European Parliament API

import { vi } from 'vitest';

/**
 * Mock European Parliament API responses
 */
export function mockEPAPI() {
  // Mock fetch globally
  global.fetch = vi.fn();
  
  return {
    mockMEP(id: number, data: Partial<MEP> = {}) {
      (global.fetch as any).mockResolvedValueOnce(
        new Response(JSON.stringify({
          id,
          fullName: 'Test MEP',
          country: 'DE',
          partyGroup: 'PPE',
          active: true,
          ...data,
        }), {
          status: 200,
          headers: { 'Content-Type': 'application/json' },
        })
      );
    },
    
    mockAPIError(status: number = 500) {
      (global.fetch as any).mockResolvedValueOnce(
        new Response(null, { status })
      );
    },
    
    mockRateLimit(retryAfter: number = 60) {
      (global.fetch as any).mockResolvedValueOnce(
        new Response(null, {
          status: 429,
          headers: { 'Retry-After': retryAfter.toString() },
        })
      );
    },
  };
}

// Usage in tests
describe('EP API Integration', () => {
  it('should handle rate limiting', async () => {
    const api = mockEPAPI();
    
    // First call returns rate limit
    api.mockRateLimit(1);
    
    // Second call succeeds
    api.mockMEP(12345);
    
    const mep = await fetchMEPWithRetry(12345);
    
    expect(mep.id).toBe(12345);
    expect(fetch).toHaveBeenCalledTimes(2);
  });
});

✅ Good Pattern: Testing Zod Schemas

import { describe, it, expect } from 'vitest';
import { MEPSchema } from './schemas';

describe('MEPSchema', () => {
  it('should validate valid MEP data', () => {
    const validMEP = {
      id: 12345,
      fullName: 'Jane Doe',
      country: 'DE',
      partyGroup: 'PPE',
      active: true,
      termStart: '2019-07-02',
      committees: [],
    };
    
    const result = MEPSchema.parse(validMEP);
    expect(result.id).toBe(12345);
  });
  
  it('should reject invalid country code', () => {
    const invalidMEP = {
      id: 12345,
      fullName: 'Jane Doe',
      country: 'USA', // Not EU country
      partyGroup: 'PPE',
      active: true,
      termStart: '2019-07-02',
    };
    
    expect(() => MEPSchema.parse(invalidMEP)).toThrow();
  });
  
  it('should apply default values', () => {
    const mepWithoutDefaults = {
      id: 12345,
      fullName: 'Jane Doe',
      country: 'DE',
      partyGroup: 'PPE',
      active: true,
      termStart: '2019-07-02',
      // committees not provided
    };
    
    const result = MEPSchema.parse(mepWithoutDefaults);
    expect(result.committees).toEqual([]);
  });
  
  it('should transform and normalize data', () => {
    const mepWithWhitespace = {
      id: 12345,
      fullName: '  Jane Doe  ',
      country: 'DE',
      partyGroup: 'PPE',
      active: true,
      termStart: '2019-07-02',
    };
    
    const result = MEPSchema.parse(mepWithWhitespace);
    expect(result.fullName).toBe('Jane Doe'); // Trimmed
  });
});

✅ Good Pattern: Integration Tests

import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { EuropeanParliamentMCPServer } from './server';

describe('MCP Server Integration', () => {
  let server: EuropeanParliamentMCPServer;
  
  beforeAll(async () => {
    server = new EuropeanParliamentMCPServer();
    await server.start();
  });
  
  afterAll(async () => {
    await server.shutdown();
  });
  
  it('should list available tools', async () => {
    const request = {
      jsonrpc: '2.0',
      id: 1,
      method: 'tools/list',
    };
    
    const response = await server.handleRequest(request);
    
    expect(response.result).toHaveProperty('tools');
    expect(Array.isArray(response.result.tools)).toBe(true);
    expect(response.result.tools.length).toBeGreaterThan(0);
  });
  
  it('should execute tool and return result', async () => {
    const request = {
      jsonrpc: '2.0',
      id: 1,
      method: 'tools/call',
      params: {
        name: 'search_meps',
        arguments: {
          country: 'DE',
          limit: 5,
        },
      },
    };
    
    const response = await server.handleRequest(request);
    
    expect(response.result).toHaveProperty('content');
    expect(Array.isArray(response.result.content)).toBe(true);
  });
});

✅ Good Pattern: Performance Testing

import { describe, it, expect } from 'vitest';
import { performance } from 'perf_hooks';

describe('Performance Tests', () => {
  it('should respond to MEP requests in <200ms', async () => {
    const startTime = performance.now();
    
    await getMEP(12345);
    
    const duration = performance.now() - startTime;
    expect(duration).toBeLessThan(200);
  });
  
  it('should achieve >80% cache hit rate', async () => {
    // Warm cache
    await getCachedMEP(12345);
    
    // Make 100 requests
    for (let i = 0; i < 100; i++) {
      await getCachedMEP(12345);
    }
    
    const stats = getCacheStats('mep');
    const hitRate = stats.hits / (stats.hits + stats.misses);
    
    expect(hitRate).toBeGreaterThan(0.8);
  });
});

Anti-Patterns

❌ Bad: Calling Real API

// NEVER - slow and unreliable!
it('should fetch MEP', async () => {
  const mep = await fetch('https://data.europarl.europa.eu/api/v2/meps/12345');
  expect(mep).toBeDefined();
});

❌ Bad: No Assertions

// NEVER - test doesn't verify anything!
it('should process MEP', async () => {
  await processMEP(12345);
  // No assertions!
});

❌ Bad: Testing Implementation

// NEVER - test behavior, not implementation!
it('should call internal function', async () => {
  const spy = vi.spyOn(internal, 'helperFunction');
  await publicAPI();
  expect(spy).toHaveBeenCalled(); // Testing internals!
});

Coverage Requirements

Target: 80% line coverage, 70% branch coverage

{
  "vitest": {
    "coverage": {
      "statements": 80,
      "branches": 70,
      "functions": 80,
      "lines": 80
    }
  }
}

ISMS Compliance

  • SC-002: Test security validations (negative / injection / boundary)
  • AU-002: Test audit-logging paths
  • PE-001: Performance-regression testing
  • VM-001: Regression test for every closed CVE / bug

Policy References

Primary:

Related:

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.