Developer Guide

June 30, 2026 ยท View on GitHub

Hack23 Logo

European Parliament MCP Server - Developer Guide

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


๐Ÿ“‹ Table of Contents


๐Ÿš€ Getting Started

Prerequisites

Required:

  • Node.js 26.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)

Initial Setup

# 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

Development Environment

# 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

๐Ÿ“ Project Structure

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

Key Directories

DirectoryPurposeFile 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

๐Ÿ”„ Development Workflow

Daily Development

# 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

Development Scripts

# 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

# Mermaid diagrams should remain readable and consistent
# (Mermaid validation is not currently enforced in CI)

๐Ÿ› ๏ธ Adding New Tools

Step 1: Define Tool Schema

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>;

Step 2: Define TypeScript Types

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;
  };
}

Step 3: Implement Tool Handler

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']
  }
};

Step 4: Register Tool

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
    ]
  };
});

Step 5: Add EP API Client Method

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'
    }
  };
}

Step 6: Write Tests

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();
    });
  });
});

Step 7: Update Documentation

  1. Add tool to docs/API_USAGE_GUIDE.md
  2. Update tool count in README.md
  3. Add to Architecture Diagrams if significant

๐Ÿงช Testing Strategy

Test Pyramid

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

Unit Tests

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);
  });
});

Integration Tests

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);
  });
});

E2E Tests

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);
  });
});

Performance Tests

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);
  });
});

๐ŸŽจ Code Style & Conventions

TypeScript Guidelines

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

Naming Conventions

TypeConventionExample
FilescamelCasegetMEPs.ts
ClassesPascalCaseRateLimiter
InterfacesPascalCaseMEPDetails
FunctionscamelCasehandleGetMEPs
ConstantsUPPER_SNAKE_CASEDEFAULT_LIMIT
Private fields_camelCase_cache
TypesPascalCaseToolMetadata

Code Organization

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 = {
  // ...
};

Error Handling

// 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}`);
}

Documentation

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
}

๐Ÿ”’ Security Considerations

Input Validation

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
}

Output Validation

// 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;

Error Sanitization

// โŒ Bad - Exposes internal details
catch (error) {
  throw error;
}

// โœ… Good - Sanitized error
catch (error) {
  logger.error('Internal error', { error });
  throw new Error('Operation failed');
}

Rate Limiting

// Always check rate limits
if (!await rateLimiter.tryRemoveTokens(1)) {
  throw new Error('Rate limit exceeded');
}

// Make request
const response = await fetch(...);

Audit Logging

// Log all data access
logger.info('Accessed MEP data', {
  user: 'client-id',
  mepId: params.id,
  timestamp: new Date().toISOString()
});

Security Checklist

  • 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)

โšก Performance Guidelines

Caching Strategy

// 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;

Async Best Practices

// โŒ 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()
]);

Memory Management

// 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;
  }
}

๐Ÿค Contributing Workflow

1. Fork and Clone

# 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

2. Create Feature Branch

git checkout -b feature/my-feature

Branch naming:

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

3. Make Changes

Follow code style, add tests, update documentation.

4. Commit Changes

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

5. Push and Create PR

git push origin feature/my-feature

Then create Pull Request on GitHub.

6. PR Review Process

  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

๐Ÿ“ฆ Release Process

Versioning

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)

Automated Release

  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

Manual Release

# 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

๐Ÿ“š Additional Resources

Documentation

External Resources

ISMS Compliance


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