# 📊 European Parliament MCP Server — Data Model

Entity Relationships, Branded Types, and EP API Data Structures
Comprehensive data model documentation for parliamentary intelligence data

📋 Document Owner: Hack23 | 📄 Version: 1.2 | 📅 Last Updated: 2026-04-21 (UTC) 🔄 Review Cycle: Quarterly | ⏰ Next Review: 2026-07-21 🏷️ Classification: Public (Open Source MCP Server) ✅ ISMS Compliance: ISO 27001 (A.5.1, A.8.1, A.14.2), NIST CSF 2.0 (ID.AM, PR.DS), CIS Controls v8.1 (2.1, 16.1)

## 📑 Table of Contents

1. Security Documentation Map
2. Overview
3. Core Entity Relationships
4. MEP Entity Model
5. Procedure Entity Model
6. Meeting and Session Model
7. Document Entity Model
8. Vote Entity Model
9. OSINT Output and Data Quality Model
10. Branded Type Documentation
11. Zod Schema Overview
12. EP API JSON-LD Structure
13. Cache Key Patterns

## 🗺️ Security Documentation Map

Document | Current | Future | Description
--- | --- | --- | ---
Architecture | ARCHITECTURE.md | FUTURE_ARCHITECTURE.md | C4 model, containers, components, ADRs
Security Architecture | SECURITY_ARCHITECTURE.md | FUTURE_SECURITY_ARCHITECTURE.md | Security controls, threat model
Data Model | DATA_MODEL.md | FUTURE_DATA_MODEL.md | Entity relationships, branded types
Flowchart | FLOWCHART.md | FUTURE_FLOWCHART.md | Business process flows
State Diagram | STATEDIAGRAM.md | FUTURE_STATEDIAGRAM.md | System state transitions
Mind Map | MINDMAP.md | FUTURE_MINDMAP.md | System concepts and relationships
SWOT Analysis | SWOT.md | FUTURE_SWOT.md | Strategic positioning
Threat Model | THREAT_MODEL.md | FUTURE_THREAT_MODEL.md | STRIDE, MITRE ATT&CK, attack trees
CRA Assessment | CRA-ASSESSMENT.md | — | EU Cyber Resilience Act conformity

## 🎯 Overview

The EP MCP Server's data model reflects the structure of the European Parliament Open Data Portal API v2 (JSON-LD format). All entities are defined as TypeScript interfaces with corresponding Zod schemas for runtime validation. Branded types enforce semantic correctness for EP-specific identifiers.

### Data Source

- Base URL: `https://data.europarl.europa.eu/api/v2/`
- Format: JSON-LD (also supports RDF/XML)
- Authentication: None required (public open data)
- Rate Limits: 100 requests/minute (enforced server-side)

### Secondary authoritative source — DOCEO RCV XML

For per-MEP roll-call vote (RCV) statistics, the EP Open Data API exposes only the placeholder `MEPDetails.votingStatistics` shape and frequently returns no real per-MEP vote counts. The MCP server therefore enriches voting-related metrics from the European Parliament DOCEO XML source (`https://www.europarl.europa.eu/doceo/document/PV-{term}-{date}-RCV_EN.xml`).

DOCEO XML is the authoritative near-real-time source for the following per-MEP fields consumed by `assess_mep_influence`, `detect_voting_anomalies`, and `comparative_intelligence`:

- `totalVotes`, `votesFor`, `votesAgainst`, `abstentions`
- `attendanceRate` (fraction of inspected RCVs the MEP appeared on)
- `loyaltyScore` (% of decisive votes where the MEP voted with their political-group majority)

The shared aggregator lives in `src/utils/doceoMepAggregator.ts` and is bounded (≤ 2 s) and cached (5-minute TTL keyed by `${mepId}|${dateFrom}|${dateTo}`). OSINT tool responses surface a `dataSource: 'EP_API' | 'DOCEO' | 'EP_API+DOCEO'` field so consumers can distinguish placeholder data from real RCV-backed metrics.

### VotingAnomaly (detect_voting_anomalies)

`detect_voting_anomalies` builds its per-MEP defection / abstention / alignment baselines from the same DOCEO RCV records via `src/utils/votingBaseline.ts`. Each detected anomaly carries the following shape:

    interface VotingAnomaly {
      type: 'PARTY_DEFECTION'
          | 'ABSTENTION_SPIKE'
          | 'ALIGNMENT_SHIFT'
          | 'CROSS_PARTY_ALIGNMENT_SHIFT';
      severity: 'HIGH' | 'MEDIUM' | 'LOW';
      mepId: string;
      mepName: string;
      description: string;
      metrics: { expectedValue: number; actualValue: number; deviation: number };
      detectedDate: string;          // YYYY-MM-DD
      evidenceVoteIds: string[];     // DOCEO LatestVoteRecord.id values
    }

`evidenceVoteIds` are the unique vote identifiers (`LatestVoteRecord.id`, e.g. `RCV-10-2026-01-15-001`) returned by `get_latest_votes` — use them to drill down to the contributing roll-call records. Thresholds: defection-rate z ≥ 1.5, abstention-rate z ≥ 1.5, week-over-week defection delta ≥ 20pp, non-home-group alignment share ≥ 60% in a weekly sub-window (min 3 decisive RCVs). Confidence reflects RCV coverage: HIGH ≥50, MEDIUM 10–49, LOW <10 RCVs inspected.

## 🔗 Core Entity Relationships

    erDiagram
        MEP {
            MEP_ID id PK
            string identifier
            string label
            CountryCode country
            string politicalGroup
            DateString mandateStart
            DateString mandateEnd
            string email
            string status
        }

        POLITICAL_GROUP {
            string id PK
            string label
            string abbreviation
            int memberCount
        }

        COMMITTEE {
            string id PK
            string label
            string abbreviation
            string type
        }

        PROCEDURE {
            ProcedureID id PK
            string title
            string type
            string stage
            DateString startDate
            string leadCommittee
        }

        PLENARY_SESSION {
            string id PK
            DateString date
            string location
            string status
            int documentCount
        }

        VOTE {
            string id PK
            string procedureRef
            string result
            int votesFor
            int votesAgainst
            int abstentions
            DateString date
        }

        DOCUMENT {
            string id PK
            string title
            string type
            string procedureRef
            DateString adoptedDate
            string url
        }

        SPEECH {
            string id PK
            MEP_ID mepRef
            string sessionRef
            DateString date
            string language
            int durationSeconds
        }

        QUESTION {
            string id PK
            MEP_ID authorRef
            string type
            string subject
            DateString submittedDate
            string status
        }

        MEP }|--|| POLITICAL_GROUP : "belongs to"
        MEP }|--|{ COMMITTEE : "member of"
        MEP ||--|{ SPEECH : "delivered"
        MEP ||--|{ QUESTION : "submitted"
        PROCEDURE ||--|{ DOCUMENT : "produces"
        PROCEDURE ||--|{ VOTE : "subject of"
        PLENARY_SESSION ||--|{ VOTE : "held in"
        PLENARY_SESSION ||--|{ SPEECH : "delivered in"
        COMMITTEE ||--|{ PROCEDURE : "responsible for"

## 👤 MEP Entity Model

    erDiagram
        MEP {
            MEP_ID id PK
            string identifier "EP identifier e.g. ID12345"
            string label "Full name"
            CountryCode country "ISO 3166-1 alpha-2"
            string nationalParty "Home party name"
            string politicalGroup "EPP, S-D, Renew, etc."
            string politicalGroupAbbr "Short code"
            DateString mandateStart "Current mandate start"
            DateString mandateEnd "Mandate end (null if current)"
            string officialEmail "EP institutional email"
            string status "current, incoming, outgoing, homonym"
            string[] committeeRoles "Committee memberships"
            string photoUrl "Official EP photo URL"
        }

        MEP_DECLARATION {
            string id PK
            MEP_ID mepRef FK
            string type "financial, interest, activity"
            DateString submittedDate
            string documentUrl
        }

        MEP_ATTENDANCE {
            string id PK
            MEP_ID mepRef FK
            string sessionRef FK
            string status "present, absent, excused"
            DateString date
        }

        MEP ||--|{ MEP_DECLARATION : "has"
        MEP ||--|{ MEP_ATTENDANCE : "tracked in"

## 📜 Procedure Entity Model

    erDiagram
        PROCEDURE {
            ProcedureID id PK "Format: YYYY-NNNN-TYPE"
            string title
            string type "COD, CNS, INI, etc."
            string stage "committee, plenary, trilogue, etc."
            string status "ongoing, completed, withdrawn"
            DateString startDate
            DateString adoptedDate
            string leadCommittee
            string rapporteur
            string[] coRapporteurs
            string legalBasis
            string[] relatedDocuments
        }

        PROCEDURE_EVENT {
            string id PK
            ProcedureID procedureRef FK
            string eventType "committee_vote, plenary_vote, etc."
            DateString date
            string description
            string documentRef
        }

        ADOPTED_TEXT {
            string id PK
            ProcedureID procedureRef FK
            string title
            DateString adoptedDate
            string url
            string documentType
        }

        PROCEDURE ||--|{ PROCEDURE_EVENT : "has events"
        PROCEDURE ||--|{ ADOPTED_TEXT : "produces"

## 🏛️ Meeting and Session Model

    erDiagram
        PLENARY_SESSION {
            string id PK
            DateString date
            string location "Strasbourg or Brussels"
            string status "scheduled, completed, cancelled"
            string[] agendaItems
            int attendanceCount
        }

        COMMITTEE_MEETING {
            string id PK
            string committeeId FK
            DateString date
            string location
            string status
            string[] agendaItems
        }

        COMMITTEE_ACTIVITY {
            string committeeId PK
            DateString periodFrom
            DateString periodTo
            int activeLegislativeFiles "Filtered: /procedures by responsibleCommittee + status=ongoing + date window"
            int documentsProduced "Filtered: /committee-documents by committee + date window"
            int meetingsHeld "Filtered: /meetings by date window"
            int decisionsAdopted "Fan-out: /meetings/{id}/decisions across up to 8 sittings"
            int opinionsIssued "Not exposed by EP API — always 0"
            float decisionsPerMeeting
            float documentsPerMonth
            float activeFilesPerMember
            string documentsSourceStatus "OK | EMPTY | TIMEOUT | UNAVAILABLE"
            string proceduresSourceStatus "OK | EMPTY | TIMEOUT | UNAVAILABLE"
            string meetingsSourceStatus "OK | EMPTY | TIMEOUT | UNAVAILABLE"
            string decisionsSourceStatus "OK | EMPTY | TIMEOUT | UNAVAILABLE"
            string confidenceLevel "HIGH | MEDIUM | LOW"
        }

        MEETING_ACTIVITY {
            string id PK
            string meetingRef FK
            string activityType
            string description
            string documentRef
            string outcome
        }

        MEETING_DECISION {
            string id PK
            string meetingRef FK
            string decisionType
            string text
            DateString date
            string[] votes
        }

        PLENARY_SESSION ||--|{ MEETING_ACTIVITY : "contains"
        COMMITTEE_MEETING ||--|{ MEETING_ACTIVITY : "contains"
        PLENARY_SESSION ||--|{ MEETING_DECISION : "produces"
        COMMITTEE_MEETING ||--|{ MEETING_DECISION : "produces"

## 📄 Document Entity Model

    erDiagram
        DOCUMENT {
            string id PK
            string title
            string type "A-item, B-item, report, amendment"
            string procedureRef
            DateString createdDate
            DateString adoptedDate
            string[] languages
            string url
            string status "draft, adopted, rejected"
        }

        EXTERNAL_DOCUMENT {
            string id PK
            string title
            string source "Commission, Council, etc."
            string documentNumber
            DateString date
            string url
            string[] relatedProcedures
        }

        PLENARY_DOCUMENT {
            string id PK
            string sessionRef FK
            string documentRef FK
            string agendaPosition
            string discussionType
        }

        COMMITTEE_DOCUMENT {
            string id PK
            string committeeRef FK
            string documentRef FK
            string role "report, opinion, position"
        }

        DOCUMENT ||--|{ PLENARY_DOCUMENT : "appears in"
        DOCUMENT ||--|{ COMMITTEE_DOCUMENT : "assigned to"

## 🗳️ Vote Entity Model

    erDiagram
        VOTE_RECORD {
            string id PK
            string procedureRef
            string sessionRef FK
            DateString date
            string voteType "rollcall, show of hands, electronic"
            string subject
            int votesFor
            int votesAgainst
            int abstentions
            string result "adopted, rejected"
        }

        MEP_VOTE {
            string id PK
            string voteRecordRef FK
            MEP_ID mepRef FK
            string position "for, against, abstention, absent"
            string politicalGroup
        }

        VOTE_RECORD ||--|{ MEP_VOTE : "composed of"

## 📈 OSINT Output and Data Quality Model

All OSINT intelligence tools produce outputs conforming to the `OsintStandardOutput` interface, which includes explicit data quality metadata:

    erDiagram
        OSINT_OUTPUT {
            string confidenceLevel "HIGH, MEDIUM, LOW"
            string methodology "Analytical approach description"
            string dataFreshness "Data recency description"
            string sourceAttribution "EP Open Data Portal API v2"
            string_array dataQualityWarnings "Plain string warnings about data limitations"
        }

        METRIC_RESULT {
            number value "Computed value or null"
            string availability "AVAILABLE, PARTIAL, ESTIMATED, UNAVAILABLE"
            string confidence "HIGH, MEDIUM, LOW, NONE"
            string source "Data source description"
            string reason "Why unavailable or estimated"
        }

        OSINT_OUTPUT ||--o{ METRIC_RESULT : "wraps metrics with"

> Note: `dataQualityWarnings` is currently implemented as `string[]` (see `src/tools/shared/types.ts`). A structured warning type with `message`, `affectedMetric`, and `severity` fields is a future enhancement not yet implemented.

### Lifecycle Pipeline Output Model

`monitor_legislative_pipeline` extends the standard OSINT envelope with lifecycle-driven fields derived from the authoritative `/procedures/{id}/events` timeline:

    erDiagram
        PIPELINE_ITEM {
            string procedureId
            string currentStage "Normalized stage from latest lifecycle event when available; falls back to procedure.stage / procedure.status / 'Unknown' when no usable events exist"
            number daysInCurrentStage "Delta between latest event and now"
            boolean isStalled
        }

        COMPUTED_ATTRIBUTES {
            string bottleneckRisk "HIGH if dwell >= p95, MEDIUM if >= median, LOW otherwise"
            number estimatedCompletionDays "Historical median remaining-time, or heuristic fallback"
        }

        LIFECYCLE_EVENT {
            string date "ISO date of the event"
            string stage "Normalized stage key"
            string rawType "Original EP API event type (e.g. def/ep-activities/REFERRAL)"
            string title
        }

        LIFECYCLE_CORPUS {
            number corpusSize "Procedures contributing to the distribution"
            number totalObservations "Dwell samples across all (type, stage) cells"
            number computationTimeMs
        }

        PIPELINE_ENVELOPE {
            string forecastBasis "HISTORICAL_MEDIAN | INSUFFICIENT_DATA | NOT_APPLICABLE"
        }

        PIPELINE_ENVELOPE ||--o{ PIPELINE_ITEM : "pipeline[]"
        PIPELINE_ITEM ||--|| COMPUTED_ATTRIBUTES : "computedAttributes"
        PIPELINE_ITEM ||--o{ LIFECYCLE_EVENT : "lifecycleEvents[]"
        PIPELINE_ENVELOPE ||--|| LIFECYCLE_CORPUS : "lifecycleCorpus"

- `lifecycleEvents` — per-procedure chronological echo of the underlying event sequence for traceability. Includes normalised `stage` and original `rawType` to support both display and downstream analytics.
- `forecastBasis` — discriminated union at the envelope level. Set to `HISTORICAL_MEDIAN` when at least one procedure in scope used the historical median forecast; `INSUFFICIENT_DATA` when every forecast fell back to the heuristic; `NOT_APPLICABLE` when all procedures are already completed.
- `lifecycleCorpus` — metadata about the corpus used to build the dwell distributions (latest 500 procedures, cached 30 min). No PII is retained — only event types, dates, and procedure types.

### DataAvailability Enum

Value | Meaning | Example
--- | --- | ---
`AVAILABLE` | All required data retrieved from EP API | MEP name, country, political group
`PARTIAL` | Some data retrieved; metric may be incomplete | Committee membership without role details
`ESTIMATED` | Metric derived from proxy/indirect data sources | Influence score based on committee count (not votes)
`UNAVAILABLE` | Required data not provided by EP API endpoint | Per-MEP voting statistics (EP API returns 0)

## 🏷️ Branded Type Documentation

Branded types enforce semantic correctness for EP domain identifiers at both compile-time and runtime.

### Implemented Branded Types

    import { z } from 'zod';

    // Procedure ID: YYYY/NNNN(TYPE) format
    // Example: 2024/0001(COD), 2023/0089(INI)
    const ProcedureIDSchema = z
      .string()
      .regex(/^\d{4}\/\d{4}\([A-Z]{2,4}\)$/)
      .brand<'ProcedureID'>();
    type ProcedureID = z.infer<typeof ProcedureIDSchema>;

    // ISO 3166-1 alpha-2 country code
    // Example: DE, FR, SE, PL
    const CountryCodeSchema = z
      .string()
      .length(2)
      .regex(/^[A-Z]{2}$/)
      .brand<'CountryCode'>();
    type CountryCode = z.infer<typeof CountryCodeSchema>;

    // ISO 8601 date string
    // Example: 2024-03-15
    const DateStringSchema = z
      .string()
      .regex(/^\d{4}-\d{2}-\d{2}$/)
      .brand<'DateString'>();
    type DateString = z.infer<typeof DateStringSchema>;

    // MEP identifier (positive integer)
    const MEP_IDSchema = z
      .number()
      .int()
      .positive()
      .brand<'MEP_ID'>();
    type MEP_ID = z.infer<typeof MEP_IDSchema>;

    // Political group abbreviation
    // Example: EPP, SD, Renew, Greens, ECR, ID, GUE
    const PoliticalGroupSchema = z
      .string()
      .min(1)
      .max(20)
      .brand<'PoliticalGroup'>();
    type PoliticalGroup = z.infer<typeof PoliticalGroupSchema>;

### Branded Type Validation Matrix

Type | Format | Example | Validation Rule
--- | --- | --- | ---
`ProcedureID` | `YYYY/NNNN(TYPE)` | `2024/0001(COD)` | Regex: `^\d{4}\/\d{4}\([A-Z]{2,4}\)$`
`CountryCode` | `AA` | `DE`, `FR` | 2 uppercase letters
`DateString` | `YYYY-MM-DD` | `2024-03-15` | ISO 8601 format
`MEP_ID` | Integer | `12345` | Positive integer
`PoliticalGroup` | String | `EPP`, `SD` | 1-20 chars

## 🔍 Zod Schema Overview

Each of the 62 MCP tools has a corresponding Zod input schema. Representative examples:

    // get_meps tool schema
    const GetMEPsInputSchema = z.object({
      country: CountryCodeSchema.optional(),
      politicalGroup: PoliticalGroupSchema.optional(),
      term: z.number().int().positive().optional(),
      limit: z.number().int().min(1).max(100).default(50),
      offset: z.number().int().min(0).default(0),
    });

    // get_mep_details tool schema
    const GetMEPDetailsInputSchema = z.object({
      mepId: MEP_IDSchema,
      includeDeclarations: z.boolean().default(false),
      includeAttendance: z.boolean().default(false),
    });

    // get_procedures tool schema
    const GetProceduresInputSchema = z.object({
      procedureId: ProcedureIDSchema.optional(),
      type: z.enum(['COD', 'CNS', 'INI', 'RSP', 'BUD']).optional(),
      status: z.enum(['ongoing', 'completed', 'withdrawn']).optional(),
      fromDate: DateStringSchema.optional(),
      toDate: DateStringSchema.optional(),
      limit: z.number().int().min(1).max(100).default(20),
    });

## 🌐 EP API JSON-LD Structure

The EP API returns JSON-LD format. The server normalizes this to plain TypeScript objects:

    {
      "@context": "https://data.europarl.europa.eu/api/v2/",
      "@graph": [
        {
          "@type": "ep:Member",
          "@id": "https://data.europarl.europa.eu/api/v2/meps/12345",
          "identifier": "12345",
          "label": "Maria Example",
          "country": "DE",
          "politicalGroup": "EPP",
          "mandateStart": "2024-07-16",
          "mandateEnd": null
        }
      ],
      "meta": {
        "total": 720,
        "offset": 0,
        "limit": 50
      }
    }

### Normalization Pipeline

    EP API JSON-LD → JSON.parse() → Extract @graph array → Map to typed objects → Zod validation → Branded types applied

## 🗝️ Cache Key Patterns

The LRU cache uses deterministic key generation for all EP API calls. Parameter keys are sorted alphabetically before serialization to ensure identical queries always produce the same cache key regardless of property insertion order (ISMS A.8.11 — Data integrity).

Pattern | Example Key | TTL
--- | --- | ---
`mep:{id}` | `mep:12345` | 15 min
`meps:list:{country}:{group}:{offset}:{limit}` | `meps:list:DE:EPP:0:50` | 15 min
`procedure:{id}` | `procedure:2024/0001(COD)` | 15 min
`procedures:list:{type}:{status}:{from}:{to}:{limit}` | `procedures:list:COD:ongoing:::20` | 15 min
`plenary:{id}` | `plenary:20240315` | 15 min
`plenary:list:{from}:{to}:{limit}` | `plenary:list:2024-01-01:2024-03-31:20` | 15 min
`votes:{id}` | `votes:V-2024-001` | 15 min
`committee:{id}` | `committee:ENVI` | 15 min
`vocab:{type}` | `vocab:countries` | 60 min

Deterministic Cache Key Construction:

    // Cache keys are deterministic regardless of property insertion order.
    // Object.entries(params).sort() ensures { a: 1, b: 2 } and { b: 2, a: 1 }
    // produce identical keys, preventing cache misses and duplicate entries.
    private getCacheKey(endpoint: string, params?: Record<string, unknown>): string {
      const sortedParams = params !== undefined
        ? Object.fromEntries(Object.entries(params).sort(([a], [b]) => a.localeCompare(b)))
        : undefined;
      return JSON.stringify({ endpoint, params: sortedParams });
    }

## Derived Models — analyze_legislative_effectiveness

The `analyze_legislative_effectiveness` MCP tool emits a derived envelope assembled from four EP Open Data Portal sources. Schema additions:

Field | Type | Description
--- | --- | ---
`metrics.reportsAuthored` | `number` | Procedures rapporteured by the subject (deduped by procedureId).
`metrics.opinionsDelivered` | `number` | Procedures where the subject is a `shadow` / `opinion` rapporteur.
`metrics.amendmentsTabled` | `number` | Plenary-session document items of type `AMENDMENT` authored by the subject.
`metrics.amendmentsAdopted` | `number` | Subset of `amendmentsTabled` whose `status` includes `ADOPTED`.
`metrics.questionsAsked` | `number` | Parliamentary questions filed by the subject within the window.
`metrics.legislativeSuccessRate` | `number` | `100 × procedures-with-adopted-text / attributed-procedures` (0–100, 2 decimals).
`attributions.reportProcedureIds` | `string[]` | Procedure IDs counted as reports authored, sorted ascending.
`attributions.opinionProcedureIds` | `string[]` | Procedure IDs counted as opinions delivered, sorted ascending.
`attributions.amendmentDocumentIds` | `string[]` | Document IDs counted as amendments tabled, sorted ascending.
`attributions.amendmentAdoptedDocumentIds` | `string[]` | Document IDs counted as adopted amendments, sorted ascending.
`attributions.questionIds` | `string[]` | Parliamentary question IDs, sorted ascending.
`dataSources.procedures` | `'OK' | 'EMPTY' | 'TIMEOUT' | 'UNAVAILABLE'` | Status of the `/procedures` fetch.
`dataSources.adoptedTexts` | `'OK' | 'EMPTY' | 'TIMEOUT' | 'UNAVAILABLE'` | Status of the `/adopted-texts` fetch.
`dataSources.plenaryDocumentItems` | `'OK' | 'EMPTY' | 'TIMEOUT' | 'UNAVAILABLE'` | Status of the `/plenary-session-documents-items` fetch.
`dataSources.questions` | `'OK' | 'EMPTY' | 'TIMEOUT' | 'UNAVAILABLE'` | Status of the `/parliamentary-questions` fetch.

Each source runs under an independent 6 s `AbortController` budget; failures are tagged in `dataSources` rather than bubbled. Lists in `attributions` are guaranteed sorted ascending so repeated runs are byte-identical.

See FUTURE_DATA_MODEL.md for planned enhancements including graph database support and temporal data models.