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
- Distinctive: line 26
Test All MCP Handlers: Every tool, resource, and prompt must have tests
- Distinctive: line 27
Mock External APIs: Never call real European Parliament API in tests
- Distinctive: line 28
Test Input Validation: Verify Zod schema validation with valid/invalid inputs
- Distinctive: line 29
Test Error Paths: Test all error scenarios and exception handling
- Distinctive: line 31
Use Descriptive Names: Test names should describe what is being tested
- Distinctive: line 33
Test Response Structure: Verify MCP-compliant response formats
- Distinctive: line 35
Integration Tests: Test full MCP request/response cycle
- Distinctive: line 420
SC-002: Test security validations (negative / injection / boundary)
- Distinctive: line 421
AU-002: Test audit-logging paths
≈ “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.
Files
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
- Test All MCP Handlers: Every tool, resource, and prompt must have tests
- Mock External APIs: Never call real European Parliament API in tests
- Test Input Validation: Verify Zod schema validation with valid/invalid inputs
- Test Error Paths: Test all error scenarios and exception handling
- Achieve Coverage Target: 80% line coverage, 70% branch coverage minimum
- Use Descriptive Names: Test names should describe what is being tested
- Arrange-Act-Assert: Structure tests with clear AAA pattern
- Test Response Structure: Verify MCP-compliant response formats
- Test Edge Cases: Empty inputs, max lengths, boundary conditions
- 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:
- Secure Development Policy — Mandatory unit / integration / security / performance testing
- Vulnerability Management — Regression tests block re-introduction
Related:
- Information Security Policy
- Privacy Policy — No real PII in fixtures
- Open Source Policy — Licence-compliance tests (
npm run test:licenses) - OWASP LLM Security Policy — Prompt-injection / output-leak test cases for MCP tools
- Change Management — Tests are merge gates
In this repository41 other files it gives its agents.
- .github/copilot-instructions.mdCopilot instructions
- ai-development-governanceSkill
- ai-governanceSkill
- architecture-documentationSkill
- aws-security-architectureSkill
- behavioral-analysisSkill
- business-model-canvasSkill
- c4-architecture-documentationSkill
- code-quality-excellenceSkill
- compliance-frameworksSkill
- contribution-guidelinesSkill
- data-science-for-intelligenceSkill
- documentation-standardsSkill
- electoral-analysisSkill
- european-parliament-apiSkill
- european-political-systemSkill
- gdpr-complianceSkill
- github-actions-workflowsSkill
- github-agentic-workflowsSkill
- incident-responseSkill
- intelligence-analysis-techniquesSkill
- isms-complianceSkill
- legislative-monitoringSkill
- mcp-gateway-configurationSkill
- mcp-gateway-securitySkill
- mcp-server-developmentSkill
- open-source-governanceSkill
- osint-methodologiesSkill
- performance-optimizationSkill
- political-science-analysisSkill
- risk-assessment-frameworksSkill
- secrets-managementSkill
- secure-code-reviewSkill
- secure-development-lifecycleSkill
- security-by-designSkill
- seo-best-practicesSkill
- strategic-communication-analysisSkill
- testing-strategySkill
- threat-modeling-frameworkSkill
- typescript-strict-patternsSkill
- vulnerability-managementSkill
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
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.

