European Parliament MCP Server API - v1.1.26
    Preparing search index...

    Developer Guide

    Hack23 Logo

    European Parliament MCP Server - Developer Guide

    Complete guide for contributing and extending the MCP server
    Project structure, development workflow, and best practices



    Required:

    • Node.js 25.x or higher
    • npm 10.x or higher
    • Git
    • TypeScript knowledge
    • MCP protocol familiarity

    Recommended:

    • VS Code with extensions:
      • ESLint
      • Prettier
      • TypeScript and JavaScript Language Features
    • Docker (for containerized development)
    # Clone repository
    git clone https://github.com/Hack23/European-Parliament-MCP-Server.git
    cd European-Parliament-MCP-Server

    # Install dependencies
    npm install

    # Build project
    npm run build

    # Run tests
    npm test

    # Start development server
    npm run dev
    # Install recommended tools
    npm install -g tsx vitest

    # Setup git hooks (optional)
    npm run prepare

    # Verify setup
    npm run type-check
    npm run lint
    npm test

    European-Parliament-MCP-Server/
    โ”œโ”€โ”€ .github/ # GitHub configuration
    โ”‚ โ”œโ”€โ”€ workflows/ # CI/CD workflows
    โ”‚ โ”‚ โ”œโ”€โ”€ test-and-report.yml # Unit tests + coverage
    โ”‚ โ”‚ โ”œโ”€โ”€ integration-tests.yml # E2E + performance tests
    โ”‚ โ”‚ โ””โ”€โ”€ release.yml # Automated releases
    โ”‚ โ”œโ”€โ”€ agents/ # Custom Copilot agents
    โ”‚ โ””โ”€โ”€ skills/ # Reusable skill patterns
    โ”‚
    โ”œโ”€โ”€ docs/ # Documentation
    โ”‚ โ”œโ”€โ”€ API_USAGE_GUIDE.md # Tool usage documentation
    โ”‚ โ”œโ”€โ”€ ARCHITECTURE_DIAGRAMS.md # Visual architecture
    โ”‚ โ”œโ”€โ”€ TROUBLESHOOTING.md # Common issues
    โ”‚ โ”œโ”€โ”€ DEVELOPER_GUIDE.md # This file
    โ”‚ โ”œโ”€โ”€ DEPLOYMENT_GUIDE.md # Deployment instructions
    โ”‚ โ””โ”€โ”€ PERFORMANCE_GUIDE.md # Optimization strategies
    โ”‚
    โ”œโ”€โ”€ src/ # Source code
    โ”‚ โ”œโ”€โ”€ index.ts # Server entry point
    โ”‚ โ”‚
    โ”‚ โ”œโ”€โ”€ tools/ # MCP tool implementations
    โ”‚ โ”‚ โ”œโ”€โ”€ getMEPs.ts
    โ”‚ โ”‚ โ”œโ”€โ”€ getMEPDetails.ts
    โ”‚ โ”‚ โ”œโ”€โ”€ getPlenarySessions.ts
    โ”‚ โ”‚ โ”œโ”€โ”€ getVotingRecords.ts
    โ”‚ โ”‚ โ”œโ”€โ”€ searchDocuments.ts
    โ”‚ โ”‚ โ”œโ”€โ”€ getCommitteeInfo.ts
    โ”‚ โ”‚ โ”œโ”€โ”€ getParliamentaryQuestions.ts
    โ”‚ โ”‚ โ”œโ”€โ”€ analyzeVotingPatterns.ts
    โ”‚ โ”‚ โ”œโ”€โ”€ trackLegislation.ts
    โ”‚ โ”‚ โ””โ”€โ”€ generateReport.ts
    โ”‚ โ”‚
    โ”‚ โ”œโ”€โ”€ clients/ # External API clients
    โ”‚ โ”‚ โ””โ”€โ”€ europeanParliamentClient.ts
    โ”‚ โ”‚
    โ”‚ โ”œโ”€โ”€ schemas/ # Zod validation schemas
    โ”‚ โ”‚ โ””โ”€โ”€ europeanParliament.ts
    โ”‚ โ”‚
    โ”‚ โ”œโ”€โ”€ types/ # TypeScript type definitions
    โ”‚ โ”‚ โ””โ”€โ”€ europeanParliament.ts
    โ”‚ โ”‚
    โ”‚ โ”œโ”€โ”€ utils/ # Utility functions
    โ”‚ โ”‚ โ”œโ”€โ”€ cache.ts
    โ”‚ โ”‚ โ”œโ”€โ”€ rateLimiter.ts
    โ”‚ โ”‚ โ””โ”€โ”€ logger.ts
    โ”‚ โ”‚
    โ”‚ โ””โ”€โ”€ services/ # Business services
    โ”‚ โ””โ”€โ”€ MetricsService.ts
    โ”‚
    โ”œโ”€โ”€ tests/ # Test files
    โ”‚ โ”œโ”€โ”€ integration/ # Integration tests
    โ”‚ โ”œโ”€โ”€ e2e/ # End-to-end tests
    โ”‚ โ”œโ”€โ”€ performance/ # Performance benchmarks
    โ”‚ โ””โ”€โ”€ helpers/ # Test utilities
    โ”‚
    โ”œโ”€โ”€ dist/ # Compiled JavaScript (gitignored)
    โ”œโ”€โ”€ node_modules/ # Dependencies (gitignored)
    โ”‚
    โ”œโ”€โ”€ package.json # Project metadata
    โ”œโ”€โ”€ tsconfig.json # TypeScript configuration
    โ”œโ”€โ”€ vitest.config.ts # Test configuration
    โ”œโ”€โ”€ eslint.config.js # ESLint configuration
    โ””โ”€โ”€ README.md # Project overview
    Directory Purpose File Types
    src/tools/ MCP tool implementations .ts, .test.ts
    src/clients/ External API clients .ts, .test.ts
    src/schemas/ Zod validation schemas .ts
    src/types/ TypeScript type definitions .ts
    src/utils/ Utility functions .ts, .test.ts
    tests/integration/ Integration tests .test.ts
    tests/e2e/ End-to-end MCP tests .e2e.test.ts
    docs/ Documentation .md

    # 1. Pull latest changes
    git checkout main
    git pull origin main

    # 2. Create feature branch
    git checkout -b feature/my-feature

    # 3. Make changes
    # Edit files...

    # 4. Run tests continuously
    npm run test:watch

    # 5. Lint and format
    npm run lint:fix
    npm run format

    # 6. Build
    npm run build

    # 7. Run full test suite
    npm test

    # 8. Commit changes
    git add .
    git commit -m "feat: add new feature"

    # 9. Push to GitHub
    git push origin feature/my-feature

    # 10. Create Pull Request
    # Build
    npm run build # Production build
    npm run build:watch # Watch mode

    # Testing
    npm test # All tests
    npm run test:unit # Unit tests only
    npm run test:integration # Integration tests
    npm run test:e2e # End-to-end tests
    npm run test:performance # Performance benchmarks
    npm run test:coverage # Coverage report
    npm run test:watch # Watch mode

    # Code Quality
    npm run lint # Check linting
    npm run lint:fix # Fix linting issues
    npm run format # Format with Prettier
    npm run type-check # TypeScript type checking

    # Other
    npm run dev # Development server
    npm run knip # Find unused exports
    npm run test:licenses # Check license compliance

    Create Zod schema in src/schemas/europeanParliament.ts:

    /**
    * Schema for new_tool input validation
    */
    export const NewToolSchema = z.object({
    param1: z.string().min(1).max(100),
    param2: z.string().regex(/^\d{4}-\d{2}-\d{2}$/).optional(),
    param3: z.number().int().min(1).max(100).default(50)
    });

    export type NewToolParams = z.infer<typeof NewToolSchema>;

    Add types in src/types/europeanParliament.ts:

    /**
    * Result type for new_tool
    */
    export interface NewToolResult {
    id: string;
    name: string;
    data: unknown[];
    metadata: {
    timestamp: string;
    source: string;
    };
    }

    Create src/tools/newTool.ts:

    /**
    * MCP Tool: new_tool
    *
    * Brief description of what this tool does
    *
    * ISMS Policy: SC-002 (Input Validation), AC-003 (Least Privilege)
    */

    import { NewToolSchema } from '../schemas/europeanParliament.js';
    import { epClient } from '../clients/europeanParliamentClient.js';
    import type { NewToolResult } from '../types/europeanParliament.js';

    /**
    * New tool handler
    *
    * @param args - Tool arguments
    * @returns MCP tool result
    *
    * @example
    * ```json
    * {
    * "param1": "value1",
    * "param2": "2024-01-01"
    * }
    * ```
    */
    export async function handleNewTool(
    args: unknown
    ): Promise<{ content: { type: string; text: string }[] }> {
    // Step 1: Validate input
    const params = NewToolSchema.parse(args);

    try {
    // Step 2: Fetch data from EP API client
    const result = await epClient.getNewData(params);

    // Step 3: Transform to expected format
    const transformed: NewToolResult = {
    id: result.id,
    name: result.name,
    data: result.items,
    metadata: {
    timestamp: new Date().toISOString(),
    source: 'European Parliament'
    }
    };

    // Step 4: Return MCP-compliant response
    return {
    content: [{
    type: 'text',
    text: JSON.stringify(transformed, null, 2)
    }]
    };
    } catch (error) {
    // Step 5: Handle errors without exposing internal details
    const errorMessage = error instanceof Error ? error.message : 'Unknown error';
    throw new Error(`Failed to execute new_tool: ${errorMessage}`);
    }
    }

    /**
    * Tool metadata for MCP registration
    */
    export const newToolMetadata = {
    name: 'new_tool',
    description: 'Brief description of what this tool does and what data it returns. Include key use cases.',
    inputSchema: {
    type: 'object' as const,
    properties: {
    param1: {
    type: 'string',
    description: 'Description of param1',
    minLength: 1,
    maxLength: 100
    },
    param2: {
    type: 'string',
    description: 'Optional date parameter (YYYY-MM-DD)',
    pattern: '^\\d{4}-\\d{2}-\\d{2}$'
    },
    param3: {
    type: 'number',
    description: 'Page size (1-100)',
    minimum: 1,
    maximum: 100,
    default: 50
    }
    },
    required: ['param1']
    }
    };

    Add to src/index.ts:

    // Import tool
    import { handleNewTool, newToolMetadata } from './tools/newTool.js';

    // Register in tool handlers map
    const toolHandlers: Record<string, (args: unknown) => Promise<any>> = {
    get_meps: handleGetMEPs,
    // ... other tools ...
    new_tool: handleNewTool // Add new tool
    };

    // Register metadata in list_tools handler
    server.setRequestHandler(ListToolsRequestSchema, async () => {
    return {
    tools: [
    getMEPsToolMetadata,
    // ... other tools ...
    newToolMetadata // Add new tool metadata
    ]
    };
    });

    If needed, add method to src/clients/europeanParliamentClient.ts:

    /**
    * Get new data from European Parliament API
    */
    async getNewData(params: {
    param1: string;
    param2?: string;
    }): Promise<NewToolResult> {
    const cacheKey = `newdata:${params.param1}:${params.param2 ?? 'all'}`;

    // Check cache
    const cached = this.cache.get(cacheKey);
    if (cached) {
    return cached;
    }

    // Rate limiting
    await this.rateLimiter.waitForToken();

    // Build URL
    const url = new URL('/api/v2/newdata', this.baseUrl);
    url.searchParams.append('param1', params.param1);
    if (params.param2) {
    url.searchParams.append('param2', params.param2);
    }

    // Fetch data
    const response = await fetch(url.toString(), {
    headers: {
    'Accept': 'application/ld+json',
    'User-Agent': 'European-Parliament-MCP-Server/1.0'
    }
    });

    if (!response.ok) {
    throw new Error(`EP API error: ${response.status}`);
    }

    const data = await response.json();

    // Transform from JSON-LD to internal format
    const result = this.transformNewData(data);

    // Cache result
    this.cache.set(cacheKey, result);

    // Log access for audit
    this.logger.info('Fetched new data', { param1: params.param1 });

    return result;
    }

    /**
    * Transform EP API JSON-LD to internal format
    */
    private transformNewData(apiData: any): NewToolResult {
    return {
    id: this.toSafeString(apiData['id']),
    name: this.toSafeString(apiData['label']),
    data: Array.isArray(apiData['items']) ? apiData['items'] : [],
    metadata: {
    timestamp: new Date().toISOString(),
    source: 'European Parliament'
    }
    };
    }

    Create src/tools/newTool.test.ts:

    import { describe, it, expect, vi, beforeEach } from 'vitest';
    import { handleNewTool, newToolMetadata } from './newTool.js';
    import { epClient } from '../clients/europeanParliamentClient.js';

    // Mock EP client
    vi.mock('../clients/europeanParliamentClient.js', () => ({
    epClient: {
    getNewData: vi.fn()
    }
    }));

    describe('newTool', () => {
    beforeEach(() => {
    vi.clearAllMocks();
    });

    describe('Input Validation', () => {
    it('should accept valid parameters', async () => {
    vi.mocked(epClient.getNewData).mockResolvedValue({
    id: 'test-id',
    name: 'Test Name',
    data: [],
    metadata: {
    timestamp: '2024-01-01T00:00:00Z',
    source: 'European Parliament'
    }
    });

    await expect(
    handleNewTool({
    param1: 'value1',
    param2: '2024-01-01'
    })
    ).resolves.toBeDefined();
    });

    it('should reject invalid param1', async () => {
    await expect(
    handleNewTool({
    param1: '', // Empty not allowed
    param2: '2024-01-01'
    })
    ).rejects.toThrow();
    });

    it('should reject invalid date format', async () => {
    await expect(
    handleNewTool({
    param1: 'value1',
    param2: '01-01-2024' // Wrong format
    })
    ).rejects.toThrow();
    });

    it('should use default values', async () => {
    vi.mocked(epClient.getNewData).mockResolvedValue({
    id: 'test-id',
    name: 'Test Name',
    data: [],
    metadata: {
    timestamp: '2024-01-01T00:00:00Z',
    source: 'European Parliament'
    }
    });

    await handleNewTool({ param1: 'value1' });

    expect(epClient.getNewData).toHaveBeenCalledWith(
    expect.objectContaining({
    param1: 'value1'
    })
    );
    });
    });

    describe('Response Format', () => {
    it('should return MCP-compliant response', async () => {
    const mockData = {
    id: 'test-id',
    name: 'Test Name',
    data: [{ item: 1 }],
    metadata: {
    timestamp: '2024-01-01T00:00:00Z',
    source: 'European Parliament'
    }
    };

    vi.mocked(epClient.getNewData).mockResolvedValue(mockData);

    const result = await handleNewTool({ param1: 'value1' });

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

    it('should return valid JSON', async () => {
    const mockData = {
    id: 'test-id',
    name: 'Test Name',
    data: [],
    metadata: {
    timestamp: '2024-01-01T00:00:00Z',
    source: 'European Parliament'
    }
    };

    vi.mocked(epClient.getNewData).mockResolvedValue(mockData);

    const result = await handleNewTool({ param1: 'value1' });
    const text = result.content[0].text;

    expect(() => JSON.parse(text)).not.toThrow();
    });
    });

    describe('Error Handling', () => {
    it('should handle EP API errors gracefully', async () => {
    vi.mocked(epClient.getNewData).mockRejectedValue(
    new Error('EP API unavailable')
    );

    await expect(
    handleNewTool({ param1: 'value1' })
    ).rejects.toThrow('Failed to execute new_tool');
    });
    });

    describe('Tool Metadata', () => {
    it('should have correct tool name', () => {
    expect(newToolMetadata.name).toBe('new_tool');
    });

    it('should have description', () => {
    expect(newToolMetadata.description).toBeDefined();
    expect(newToolMetadata.description.length).toBeGreaterThan(10);
    });

    it('should define input schema', () => {
    expect(newToolMetadata.inputSchema).toBeDefined();
    expect(newToolMetadata.inputSchema.properties).toBeDefined();
    });
    });
    });
    1. Add tool to docs/API_USAGE_GUIDE.md
    2. Update tool count in README.md
    3. Add to Architecture Diagrams if significant

            /\
    /E2E\ End-to-End (10%)
    /------\ - Full MCP client-server tests
    / Integr \ Integration (20%)
    /----------\ - EP API client tests
    / Unit \ Unit (70%)
    /--------------\ - Tool handlers, schemas, utils

    Location: Colocated with source files (*.test.ts)

    Coverage Target: 80% overall, 95% for security-critical code

    Example:

    // src/utils/cache.test.ts
    import { describe, it, expect } from 'vitest';
    import { LRUCache } from 'lru-cache';

    describe('Cache', () => {
    it('should store and retrieve values', () => {
    const cache = new LRUCache({ max: 10 });
    cache.set('key', 'value');
    expect(cache.get('key')).toBe('value');
    });

    it('should respect TTL', async () => {
    const cache = new LRUCache({ max: 10, ttl: 100 });
    cache.set('key', 'value');

    await new Promise(resolve => setTimeout(resolve, 150));

    expect(cache.get('key')).toBeUndefined();
    });

    it('should evict oldest when max reached', () => {
    const cache = new LRUCache({ max: 2 });
    cache.set('key1', 'value1');
    cache.set('key2', 'value2');
    cache.set('key3', 'value3');

    expect(cache.has('key1')).toBe(false);
    expect(cache.has('key2')).toBe(true);
    expect(cache.has('key3')).toBe(true);
    });
    });

    Location: tests/integration/

    Purpose: Test EP API client integration

    Example:

    // tests/integration/epClient.integration.test.ts
    import { describe, it, expect } from 'vitest';
    import { epClient } from '../../src/clients/europeanParliamentClient.js';

    describe('EP API Client Integration', () => {
    it('should fetch MEPs from real API', async () => {
    const result = await epClient.getMEPs({
    country: 'SE',
    limit: 5
    });

    expect(result.data).toBeDefined();
    expect(Array.isArray(result.data)).toBe(true);
    expect(result.data.length).toBeGreaterThan(0);
    expect(result.total).toBeGreaterThan(0);
    });

    it('should respect rate limiting', async () => {
    const requests = Array(5).fill(null).map(() =>
    epClient.getMEPs({ limit: 1 })
    );

    const start = Date.now();
    await Promise.all(requests);
    const duration = Date.now() - start;

    // Should complete reasonably fast with caching
    expect(duration).toBeLessThan(5000);
    });
    });

    Location: tests/e2e/

    Purpose: Test complete MCP client-server flow

    Example:

    // tests/e2e/mepQueries.e2e.test.ts
    import { describe, it, expect, beforeAll, afterAll } from 'vitest';
    import { Client } from '@modelcontextprotocol/sdk/client/index.js';
    import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';

    describe('MEP Queries E2E', () => {
    let client: Client;
    let transport: StdioClientTransport;

    beforeAll(async () => {
    transport = new StdioClientTransport({
    command: 'node',
    args: ['dist/index.js']
    });

    client = new Client({
    name: 'test-client',
    version: '1.0.0'
    }, {
    capabilities: {}
    });

    await client.connect(transport);
    });

    afterAll(async () => {
    await client.close();
    });

    it('should list MEPs', async () => {
    const result = await client.callTool('get_meps', {
    country: 'SE',
    limit: 10
    });

    expect(result.content).toBeDefined();
    expect(result.content[0].type).toBe('text');

    const data = JSON.parse(result.content[0].text);
    expect(data.data).toBeDefined();
    expect(Array.isArray(data.data)).toBe(true);
    });
    });

    Location: tests/performance/

    Purpose: Verify performance requirements (<200ms cached)

    Example:

    // tests/performance/benchmarks.test.ts
    import { describe, it, expect } from 'vitest';
    import { measureTime } from '../helpers/testUtils.js';

    describe('Performance Benchmarks', () => {
    it('should respond in <200ms for cached requests', async () => {
    // Warm cache
    await client.callTool('get_meps', { country: 'SE' });

    // Measure cached response
    const duration = await measureTime(() =>
    client.callTool('get_meps', { country: 'SE' })
    );

    expect(duration).toBeLessThan(200);
    });

    it('should handle 50 concurrent requests', async () => {
    const requests = Array(50).fill(null).map((_, i) =>
    client.callTool('get_meps', {
    country: 'SE',
    offset: i * 10,
    limit: 10
    })
    );

    const start = Date.now();
    await Promise.all(requests);
    const duration = Date.now() - start;

    // Should complete in reasonable time
    expect(duration).toBeLessThan(10000);
    });
    });

    1. Use Strict Mode

    // tsconfig.json already enables strict mode
    {
    "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true
    }
    }

    2. Explicit Return Types

    // โŒ Bad
    export async function getData(id: string) {
    return await fetch(`/api/${id}`);
    }

    // โœ… Good
    export async function getData(id: string): Promise<Response> {
    return await fetch(`/api/${id}`);
    }

    3. No any Type

    // โŒ Bad
    function process(data: any) {
    return data.field;
    }

    // โœ… Good
    function process(data: unknown): string {
    if (typeof data === 'object' && data !== null && 'field' in data) {
    return String(data.field);
    }
    throw new Error('Invalid data structure');
    }

    4. Use Branded Types

    // Good for IDs
    type MEPID = string & { readonly __brand: 'MEPID' };

    function getMEP(id: MEPID): Promise<MEP> {
    // ...
    }

    // Prevents accidental string mixing
    const id: MEPID = 'MEP-123' as MEPID;
    getMEP(id); // OK
    getMEP('random-string'); // โŒ Type error
    Type Convention Example
    Files camelCase getMEPs.ts
    Classes PascalCase RateLimiter
    Interfaces PascalCase MEPDetails
    Functions camelCase handleGetMEPs
    Constants UPPER_SNAKE_CASE DEFAULT_LIMIT
    Private fields _camelCase _cache
    Types PascalCase ToolMetadata

    1. Import Order

    // 1. External packages
    import { z } from 'zod';
    import { LRUCache } from 'lru-cache';

    // 2. Internal modules (absolute imports)
    import { epClient } from '../clients/europeanParliamentClient.js';
    import { MEPSchema } from '../schemas/europeanParliament.js';

    // 3. Types
    import type { MEP, Committee } from '../types/europeanParliament.js';

    2. File Structure

    /**
    * File header comment
    */

    // Imports
    import { ... } from '...';

    // Constants
    const DEFAULT_LIMIT = 50;

    // Types/Interfaces
    interface LocalType {
    // ...
    }

    // Main implementation
    export async function mainFunction() {
    // ...
    }

    // Helper functions (private)
    function helperFunction() {
    // ...
    }

    // Exports
    export const metadata = {
    // ...
    };
    // Always catch and sanitize errors
    try {
    const result = await externalAPI();
    return result;
    } catch (error) {
    // Log full error internally
    logger.error('External API failed', { error });

    // Throw sanitized error
    const message = error instanceof Error ? error.message : 'Unknown error';
    throw new Error(`Operation failed: ${message}`);
    }

    JSDoc for Public APIs:

    /**
    * Retrieve Members of European Parliament with filters
    *
    * @param args - Tool arguments including country, group, committee filters
    * @returns MCP-compliant response with paginated MEP data
    * @throws {ValidationError} If input parameters are invalid
    * @throws {APIError} If European Parliament API request fails
    *
    * @example
    * ```typescript
    * const result = await handleGetMEPs({
    * country: 'SE',
    * group: 'S&D',
    * limit: 20
    * });
    * ```
    *
    * @security
    * - Input validated with Zod schemas
    * - Rate limited to 100 requests per 15 minutes
    * - All requests logged for audit
    *
    * @performance
    * - Cached responses: <1ms
    * - API requests: ~150-200ms
    * - Cache TTL: 15 minutes
    */
    export async function handleGetMEPs(args: unknown): Promise<MCPResponse> {
    // Implementation
    }

    Always use Zod schemas:

    // Define schema
    const InputSchema = z.object({
    id: z.string().min(1).max(100),
    date: z.string().regex(/^\d{4}-\d{2}-\d{2}$/),
    keywords: z.string().regex(/^[a-zA-Z0-9\s\-_]+$/)
    });

    // Validate input
    export function handleTool(args: unknown) {
    const params = InputSchema.parse(args); // Throws on invalid
    // params is now type-safe
    }
    // Validate API responses
    const OutputSchema = z.object({
    id: z.string(),
    name: z.string(),
    data: z.array(z.unknown())
    });

    const apiResponse = await fetch(...);
    const validated = OutputSchema.parse(apiResponse); // Ensure structure
    return validated;
    // โŒ Bad - Exposes internal details
    catch (error) {
    throw error;
    }

    // โœ… Good - Sanitized error
    catch (error) {
    logger.error('Internal error', { error });
    throw new Error('Operation failed');
    }
    // Always check rate limits
    if (!await rateLimiter.tryRemoveTokens(1)) {
    throw new Error('Rate limit exceeded');
    }

    // Make request
    const response = await fetch(...);
    // Log all data access
    logger.info('Accessed MEP data', {
    user: 'client-id',
    mepId: params.id,
    timestamp: new Date().toISOString()
    });
    • [ ] Input validated with Zod schemas
    • [ ] Output validated before returning
    • [ ] Errors sanitized (no internal details exposed)
    • [ ] Rate limiting applied
    • [ ] Audit logging implemented
    • [ ] No secrets in code or logs
    • [ ] HTTPS only for external APIs
    • [ ] GDPR compliance (data minimization)

    // Use LRU cache with appropriate TTL
    const cache = new LRUCache<string, any>({
    max: 500, // Max entries
    ttl: 15 * 60 * 1000, // 15 minutes
    allowStale: false
    });

    // Cache key strategy
    function getCacheKey(params: any): string {
    return `${method}:${JSON.stringify(params)}`;
    }

    // Check cache before API call
    const cached = cache.get(cacheKey);
    if (cached) {
    return cached;
    }

    // Fetch and cache
    const fresh = await fetchFromAPI();
    cache.set(cacheKey, fresh);
    return fresh;
    // โŒ Bad - Sequential
    const meps = await getMEPs();
    const sessions = await getSessions();
    const votes = await getVotes();

    // โœ… Good - Parallel
    const [meps, sessions, votes] = await Promise.all([
    getMEPs(),
    getSessions(),
    getVotes()
    ]);
    // Limit array sizes
    const results = largeArray.slice(0, 100); // Take first 100

    // Stream large datasets
    async function* streamData() {
    let offset = 0;
    while (true) {
    const batch = await fetchBatch(offset, 100);
    if (batch.length === 0) break;

    for (const item of batch) {
    yield item;
    }

    offset += batch.length;
    }
    }

    # Fork on GitHub, then:
    git clone https://github.com/YOUR_USERNAME/European-Parliament-MCP-Server.git
    cd European-Parliament-MCP-Server
    git remote add upstream https://github.com/Hack23/European-Parliament-MCP-Server.git
    git checkout -b feature/my-feature
    

    Branch naming:

    • feature/ - New features
    • fix/ - Bug fixes
    • docs/ - Documentation
    • refactor/ - Code refactoring
    • test/ - Test improvements

    Follow code style, add tests, update documentation.

    git add .
    git commit -m "feat: add new MEP filtering option"

    Commit message format (Conventional Commits):

    type(scope): description

    [optional body]

    [optional footer]

    Types:

    • feat: New feature
    • fix: Bug fix
    • docs: Documentation
    • style: Formatting
    • refactor: Code restructuring
    • test: Testing
    • chore: Maintenance

    Examples:

    feat(tools): add committee member filtering
    fix(cache): resolve cache invalidation bug
    docs(api): update get_meps examples
    test(e2e): add voting records integration test
    git push origin feature/my-feature
    

    Then create Pull Request on GitHub.

    1. Automated Checks:

      • All tests pass
      • Linting passes
      • Coverage meets threshold (80%)
      • Security scan passes
    2. Code Review:

      • At least one approval required
      • Security team review for security-critical changes
      • Documentation updated
      • CHANGELOG.md updated for user-facing changes
    3. Merge:

      • Squash and merge to main
      • Delete branch after merge

    Follow Semantic Versioning:

    • Major (x.0.0): Breaking changes
    • Minor (0.x.0): New features (backward compatible)
    • Patch (0.0.x): Bug fixes (backward compatible)
    1. Create Release PR:

      • Update version in package.json
      • Update CHANGELOG.md
      • Commit: chore(release): v1.2.0
    2. Merge to Main:

      • All tests pass
      • Approved by maintainers
    3. GitHub Actions:

      • Automatically creates release
      • Generates release notes
      • Publishes to npm (if configured)
      • Creates git tag
    # Update version
    npm version minor # or major, patch

    # Push with tags
    git push origin main --follow-tags

    # Create GitHub release
    gh release create v1.2.0 \
    --title "v1.2.0" \
    --notes-file CHANGELOG.md


    Built with โค๏ธ by Hack23 AB
    ISMS-compliant development demonstrating security excellence