Skip to main content
Glama
@cyanheads/eu-parliament-mcp-server

@cyanheads/eu-parliament-mcp-server

by cyanheads

Version License Docker MCP SDK npm TypeScript Bun

Install in Claude Desktop Install in Cursor Install in VS Code

Framework

Public Hosted Server: https://eu-parliament.caseyjhand.com/mcp


Overview

European Parliament records from the Open Data API: discover legislative procedures, follow their evidence, find MEPs, inspect plenary decisions and voters, and retrieve document, speech, and question metadata. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above. No API key is required.

Tools

Tool

Description

euparl_list_reference

Discover filter codes, identifier formats, coverage, committees, and political groups; resolve organization IDs

euparl_list_procedures

List procedure references by type

euparl_get_procedure

Read procedure identity, stage, events, participants, and document references

euparl_search_documents

Search adopted texts, list committee/plenary documents, or retrieve any canonical document

euparl_search_speeches

Search ninth- or tenth-term speech activities and retrieve metadata, recording-document links, and on request the published text

euparl_search_meps

Find MEPs across a complete current, dated, or selected-term roster, by name or person ID

euparl_get_mep

Read member identity and published membership history

euparl_get_plenary_votes

Discover sittings, list a sitting's vote items, inspect decisions, and page recorded or intended voters

euparl_list_questions

List questions by year/type or retrieve a question and answer references

Agent skill

skills/eu-parliament-guide is served over MCP as skill://eu-parliament-guide/SKILL.md and ships in the npm package, Docker image, and .mcpb bundle. It covers identifier chains from procedure to vote and debate, the term calendar, choosing a file's final vote, a voter's country and group on the vote date, and conclusions missing data cannot support.

Related MCP server: congressgov-mcp-server

Capability reference

euparl_list_reference tool

  • Required topic: committees, political_groups, organizations, procedure_types, document_types, question_types, activity_types, mep_filters, identifiers, or coverage

  • committees and political_groups page Parliament's corporate-body records, historical bodies included; their organization IDs are not filter codes. Published full names and dates are included when available

  • organizations requires organization_ids: decimal IDs or the case-sensitive codes other tools return (IMCO, S-D, ep-9, EU_PARLIAMENT), bare, with an org/ prefix, or as exact public organization URIs. A code in the wrong case is a found: false miss; labels such as S&D and EU authority URIs are rejected, and a question's addressee_ids do not resolve here (COM is the Communist and Allies Group; EU_COMMISSION, EU_COUNCIL and HRVP return no name). Results retain input order and duplicates with per-item found; repeat the same IDs with page.next_offset for continuation. At most 20 IDs are looked up per call; reference pages need no snapshot


euparl_list_procedures tool

  • Optional process_type from euparl_list_reference; no keyword, year, committee, or stage filter

  • With process_type, every procedure of that type is read in one request and paged in procedure_id order; follow page.next_offset, optionally passing page.snapshot back to detect a changed set

  • Without process_type, Parliament serves pages from two orderings, so pages can repeat or omit procedures; each page says so

  • Sparse rows carry procedure_id, source URL, and available reference/type; retrieve titles, stages, and participants with euparl_get_procedure


euparl_get_procedure tool

  • Required procedure_id accepts YYYY-NNNN, YYYY/NNNN(TYPE), an optional letter suffix, the known path prefix, or the exact public procedure URI

  • section: "overview" returns scalar metadata and collection counts; events, participants, and documents page one collection using snapshots. Events and participants come in date order (undated last), documents in ID order

  • Historical participant groups stay historical. Follow document IDs with euparl_search_documents in get mode


euparl_search_documents tool

  • mode: "search" defaults to source: "adopted_texts", the only family with text, title, sitting-date, and related_process_id filters; adopted texts take year and term, committee takes term, plenary takes year. work_type codes are family-specific

  • Adopted-text rows carry title (in the requested language only), date, and procedure_ids; a page holds at most 100 rows. Committee and plenary rows carry identity only

  • mode: "get" takes a document_id from any tool (procedures, speeches, votes, questions, answers) in any letter case, or a citation such as T9-0138/2024, P9_TA(2024)0138, A9-0188/2023, RC-B9-0163/2024, B9-0163/2024, or P9_OJQ(2024)03-13, and rejects search filters and paging

  • Detail returns metadata, procedure and parent_document_ids references, published adopted_document_ids and based_on_document_ids, optional public_register_ref, and every source-file link in the selected language; bodies are linked, not extracted. A published relation does not guarantee its target is available upstream

  • Adopted texts, plenary documents (reports, motions) and committee documents publish their procedure only on their family record, so their get makes one extra request for procedure_ids. If that read fails, the rest of the record is returned with a notice that procedure_ids may be incomplete


euparl_search_speeches tool

  • Search by text, title, person_id, activity_type, or sitting-date range; optional term selects 9 or 10, defaulting to 10. Term-9 records start in July 2021

  • mode: "get" requires speech_id and rejects search filters and nondefault paging

  • Returns activity metadata, participants, and original_languages for every row. Follow document_ids through document get and any parent references to source files

  • include_text: true (search and get) adds each speech's published text, speaker, and language_tags in the selected language, with no fallback: a speech unpublished in that language omits its title or text, with a notice. Search returns at most 100 rows per call


euparl_search_meps tool

  • Omitting term and mandate_date selects members in office today; term selects a published roster from terms 0–10; mandate_date (YYYY-MM-DD, not after today, exclusive with term) selects members seated on that date. Optional country, political-group, and gender codes come from mep_filters

  • On a mandate_date roster, country and group are matched and returned as published on that date, so political_group matches only the code in force then, as it does on the current roster; for earlier groups pass mandate_date, or omit political_group and read each member's published group. A term's first sitting day can return an incomplete roster or an upstream error; use the following day

  • political_group takes the 46 published group codes, current and historical (PPE-DE, ALDE, GUE-NGL, listed with labels and periods by euparl_list_reference topic mep_filters), their published labels in any case, and the aliases EPP, Greens/EFA, and Patriots. GUE/NGL names two codes: it selects GUE-NGL on terms 0–8 and dates through 2020-12-18, THE-LEFT otherwise, and the notice names the other and how to reach it. On a term roster a code matches members holding a membership in it that had not ended when the term began, not who sat in the group during the term

  • Country UK → GB, the code Parliament publishes for United Kingdom members on historical rosters; published historical group identities remain intact

  • name_contains matches every normalized token across the complete roster before paging, ignoring case, diacritics, and punctuation; a value with no letter or digit is rejected

  • mep_ids keeps the listed person IDs (decimal, person/…, or the public URI) from the selected roster, narrowing with every other filter; unmatched_mep_ids lists the requested IDs the roster does not hold. Pick the roster the IDs belong to: mandate_date set to the vote or sitting date for country and group as published that day, term for names only

  • Results use snapshot continuation. Country/group describe the selected roster context, never inferred for another date


euparl_get_mep tool

  • mep_id accepts a decimal person ID, person/ prefix, or exact public person URI

  • Default section: "overview" returns identity, membership_count, and membership_classifications (a count per classification code); section: "memberships" pages published history with snapshots, latest start_date first

  • classification (a code such as COMMITTEE_PARLIAMENTARY_STANDING, bare or as published, case-insensitive) and active_on (YYYY-MM-DD, not after today) select memberships before paging and apply to section: "memberships" only. Memberships without a classification never match classification

  • Missing membership end dates do not establish current membership

  • Resolve membership organization IDs, decimal or term codes such as org/ep-9, through euparl_list_reference with topic: "organizations"


euparl_get_plenary_votes tool

  • mode: "sittings" takes year (sittings data starts in 2014; earlier years return no sittings); mode: "vote_items" takes sitting_id (a YYYY-MM-DD date or MTG-PL- ID) and lists what the sitting voted on in published order, each item with its title, based-on documents, procedures, recording documents, and decision_count; default decisions takes sitting_id and pages summaries with labels, tallies, distinct decided-on, recording and part-of document IDs, and vote item IDs. Decision records start with the 2019-01-14 sitting: earlier sittings list but return no decisions, and not every later sitting has them. Vote items publish decision links from 2024-01-15, the first sitting of 2024; on earlier sittings decision_count is absent and vote_item_id selects none of their decisions. Optional document_id (a canonical ID or citation, in any letter case) filters any document relation by exact ID or hyphen-boundary descendant before paging; optional vote_item_id (a procedure's PLENARY_VOTE_RESULTS event or a vote_items row) selects every decision of one vote item, the route to a report's final vote. A procedure event's vote date can select the sitting. Where decisions carry no report relations, the report citation appears only in decision labels, and some labels carry none: words of the vote item's title, with language set to the title's language (French through 2023), are the alternative

  • Optional label_contains, in decisions or vote_items, selects a literal substring of the selected-language decision label or vote item title, ignoring case, repeated whitespace and dash style. Use a report citation on sittings without document relations: a document_ids entry is cited with its prefix and term joined and the year after the number (A-9-2022-0116 is A9-0116/2022, the joint motion RC-9-2022-0257 is RC-B9-0257/2022); matching text does not establish a final vote. In decisions it combines with document and vote-item filters before paging

  • decision adds decision_id and returns one rich record with commentary (an ID absent from the sitting fails decision_not_found). Optional mep_id in decisions or decision returns separate recorded/intended positions and publication availability without a roster lookup; an empty match does not prove absence from the vote

  • mode: "voters" adds position: favor, against, abstention, or the intended_* corrections, which never replace recorded votes. available: false means the array was not published; a published empty array has available: true. Some decisions (for example on the 2019-01-14, 2019-03-27 and 2019-04-04 sittings) publish voter entries that name no person; those entries are left out of voters, voter_counts and mep_position, the tallies stay as published, and a notice says so. Voters still without a name after resolve_names are named by one euparl_search_meps call with mep_ids, mandate_date set to the sitting date and limit at least their count

  • resolve_names: true requires roster_term (0–10) and joins names from that term's roster only; unresolved IDs stay in the output


euparl_list_questions tool

  • Default mode: "list" accepts year and work_type; no keyword, author, or addressee search

  • mode: "get" requires question_id: a canonical work ID, known document prefix/public URI, or human citation such as E-000001/2025 (also P, O, and the G/K interpellation prefixes). Optional term resolves a citation directly; omission checks the corresponding mandate, including both candidates in election years, and requires one exact match

  • Get rejects list filters and nondefault paging. Detail returns metadata, alternative_title, addressee_ids (the organizations published with the ADDRESSEE role, verbatim; they do not resolve through euparl_list_reference), original_languages, source files, and published answer references; empty answers does not prove the question is unanswered

  • Question prefixes E/P/O/G/K are case-insensitive and sequences of one to six digits are padded to six: e-3/2025 and E-10-2025-3 resolve as E-10-2025-000003

Features

Built on @cyanheads/mcp-ts-core: stdio and Streamable HTTP transports, pluggable auth (none / jwt / oauth), swappable storage (in-memory, filesystem, Supabase, Cloudflare KV/R2/D1), structured logging with optional OpenTelemetry tracing.

European Parliament-specific:

  • Keyless access to Parliament's public JSON-LD records, with canonical identifiers linking discovery, detail, and source evidence

  • Shared request pacing, bounded retries, and one 40-second deadline per call covering every upstream request and queue wait

  • Shared paging on every list tool: offset, limit (default 20), and language (default en, any of the 24 EU languages). Rosters, procedure sections, memberships, vote items, decisions, and voters are acquired whole, then paged locally with a page.snapshot that continuation must pass back; snapshot_changed means restart at offset 0. Procedure listings with process_type are acquired whole too, and passing their snapshot back is optional

  • Whole-acquired local collections return the requested count or remaining rows without an output-size cap or character budget. Complete relation arrays are preserved. Upstream request bounds are separate from caller page sizes:

    Collection

    Acquisition / caller limit

    Process-type procedures, MEP rosters, memberships, procedure sections, vote items, decisions, voters, static references

    Requested count or remaining rows

    Non-keyword adopted texts and speeches

    Fixed aligned 100-row upstream pages; caller pages span requests within one deadline

    Adopted texts, keyword adopted texts, speeches with include_text

    100 per call

    Unfiltered procedures, questions, committee/plenary documents, keyword speeches, live reference bodies, sittings

    1,000 per request

    Organization lookups (one paced request each)

    20 per call

    Larger bounded-mode limits normalize with a notice. EUPARL_CLAMP_LIMITS=false rejects limits above the largest bound only for tools whose modes are all bounded; smaller mode bounds still normalize. Adopted texts count their 10000-result search window as that bound, so euparl_search_documents rejects only above 10000 and still returns 100-row adopted-text pages. Very large finite limits normalize to the safe-integer range. Non-keyword adopted texts sort by document ID; speeches by sitting date and numbering. Keyword searches retain relevance order and can repeat or omit records. Past-end pages of nonempty local collections disclose the offset, exact total and restart guidance after snapshot validation.

  • Speech and adopted-text searches, and procedure listings without process_type, expose a 10,000-result window. A page reaching past it narrows to the results left, with a notice; an offset of 10,000 or more is rejected. Narrow supported filters when the window ends; a capped upstream total is disclosed as a lower bound, and has_more can remain true without a reachable next_offset

Agent-friendly output:

  • Provenance on every response: source URLs, the actual language of each text (fallback disclosed, never translated), normalized filter echo, and continuation guidance

  • Missing data stays distinguishable: detail misses return found: false, empty lists succeed with guidance, and published zeros, unpublished arrays, and historical affiliations are never conflated

  • Typed error reasons with recovery hints, and the same data on content[] and structuredContent. A rejected filter names the parameter, quotes the value, lists the accepted codes or forms, and carries its own recovery hint; a rate-limit failure reports Parliament's retryAfter in whole seconds

Data source: European Parliament Open Data, licensed under CC BY 4.0. This server normalizes identifiers and record structure.

Getting started

Connect to the hosted instance, no install:

claude mcp add --transport http eu-parliament-mcp-server https://eu-parliament.caseyjhand.com/mcp

Or run it locally:

claude mcp add eu-parliament-mcp-server -- npx -y @cyanheads/eu-parliament-mcp-server

For other clients, add this to the MCP config file:

{
  "mcpServers": {
    "eu-parliament-mcp-server": {
      "command": "npx",
      "args": ["-y", "@cyanheads/eu-parliament-mcp-server"]
    }
  }
}

For Docker or a source build, see Running the server.

Configuration

Variable

Description

Default

MCP_TRANSPORT_TYPE

Transport: stdio or http.

stdio

MCP_HTTP_HOST

HTTP listen host.

127.0.0.1

MCP_HTTP_PORT

HTTP server port.

3010

MCP_SESSION_MODE

HTTP session mode: stateless, stateful, or auto.

stateless

MCP_AUTH_MODE

Authentication: none, jwt, or oauth.

none

MCP_LOG_LEVEL

Log level (debug, info, warning, error, etc.).

info

LOGS_DIR

Directory for log files (Node.js only).

<app-root>/logs

STORAGE_PROVIDER_TYPE

Storage backend: in-memory, filesystem, supabase, cloudflare-kv/r2/d1.

in-memory

OTEL_ENABLED

Enable OpenTelemetry.

false

EUPARL_CLAMP_LIMITS

Normalize bounded-mode limits with a notice; false rejects limits above a fully bounded tool's largest bound, adopted texts counting their 10000-result search window. Whole-acquired local modes have no output-size cap.

true

No variables are required; EUPARL_CLAMP_LIMITS is the one optional server-specific variable. See .env.example for common framework overrides.

Running the server

Requires Bun v1.4.4 or higher (or Node.js v24+). No API key or upstream account is required.

git clone https://github.com/cyanheads/eu-parliament-mcp-server.git
cd eu-parliament-mcp-server && bun install
cp .env.example .env

Local development

  • Build and run the production version:

    # One-time build
    bun run rebuild
    
    # Run the built server
    bun run start:http
    # or
    bun run start:stdio
  • Run checks and tests:

    bun run devcheck  # Lints, formats, type-checks, and more
    bun run test      # Runs the test suite

Docker

docker run --rm -p 3010:3010 ghcr.io/cyanheads/eu-parliament-mcp-server:latest

The image defaults to HTTP transport, stateless sessions, and logs to /var/log/eu-parliament-mcp-server. For stdio clients, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/eu-parliament-mcp-server:latest"]. Build locally with docker build -t eu-parliament-mcp-server .; OpenTelemetry peers install by default, or use --build-arg OTEL_ENABLED=false to omit them.

Project structure

Directory

Purpose

src/mcp-server/tools/definitions

Nine read-only tool definitions (*.tool.ts).

src/mcp-server/tools/utils

Shared schemas, lossless rendering, and enrichment.

src/config

Server environment: the limit clamp switch.

src/services/eu-parliament

JSON-LD client, edge schemas, reference codes, normalization, and domain workflows.

tests/

Unit and integration tests, mirroring the src/ structure.

docs/

Design contracts and generated project tree.

Development guide

See CLAUDE.md for development guidelines and architectural rules. The short version:

  • Handlers throw, framework catches — no try/catch in tool logic

  • Use ctx.log for logging, ctx.state for storage

  • Register new tools and resources in the createApp() arrays in src/index.ts

  • Wrap external API calls: validate raw → normalize to domain type → return output schema; never fabricate missing fields

Contributing

Issues are welcome. Run checks and tests before submitting:

bun run devcheck
bun run test

License

This project is licensed under the Apache 2.0 License. See the LICENSE file for details.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      Not graded
      quality
      B
      maintenance
      Provides access to European Parliament open data, enabling queries about MEPs, plenary documents, and session calendar.
      151 npm
      MIT
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables access to U.S. congressional data including bills, laws, members, committees, votes, nominations, CRS reports, and the Congressional Record through MCP, with stdio and Streamable HTTP support.
      585 npm
      2
      Apache 2.0
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables tracking of Canadian federal parliamentary records by searching bills, members, House divisions, and indexed Hansard speeches, then retrieving detailed dossiers, profiles, tallies, and full speech text from official public sources. Runs over STDIO or Streamable HTTP with no API key required.
      144 npm
      1
      Apache 2.0
    • A
      license
      Not graded
      quality
      B
      maintenance
      Enables searching and retrieving Swedish Riksdag open data — parliamentary documents, committee reports, consolidated SFS statutes, speeches, main votes, and member records — through seven tools, with paginated full text and continuation support. Runs keyless over STDIO or Streamable HTTP and includes dated reference tables for document types, committees, parties, constituencies, and sessions.
      88 npm
      1
      Apache 2.0