@cyanheads/eu-parliament-mcp-server
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 |
| Discover filter codes, identifier formats, coverage, committees, and political groups; resolve organization IDs |
| List procedure references by type |
| Read procedure identity, stage, events, participants, and document references |
| Search adopted texts, list committee/plenary documents, or retrieve any canonical document |
| Search ninth- or tenth-term speech activities and retrieve metadata, recording-document links, and on request the published text |
| Find MEPs across a complete current, dated, or selected-term roster, by name or person ID |
| Read member identity and published membership history |
| Discover sittings, list a sitting's vote items, inspect decisions, and page recorded or intended voters |
| 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, orcoveragecommitteesandpolitical_groupspage Parliament's corporate-body records, historical bodies included; their organization IDs are not filter codes. Published full names and dates are included when availableorganizationsrequiresorganization_ids: decimal IDs or the case-sensitive codes other tools return (IMCO,S-D,ep-9,EU_PARLIAMENT), bare, with anorg/prefix, or as exact public organization URIs. A code in the wrong case is afound: falsemiss; labels such asS&Dand EU authority URIs are rejected, and a question'saddressee_idsdo not resolve here (COMis the Communist and Allies Group;EU_COMMISSION,EU_COUNCILandHRVPreturn no name). Results retain input order and duplicates with per-itemfound; repeat the same IDs withpage.next_offsetfor continuation. At most 20 IDs are looked up per call; reference pages need no snapshot
euparl_list_procedures tool
Optional
process_typefromeuparl_list_reference; no keyword, year, committee, or stage filterWith
process_type, every procedure of that type is read in one request and paged inprocedure_idorder; followpage.next_offset, optionally passingpage.snapshotback to detect a changed setWithout
process_type, Parliament serves pages from two orderings, so pages can repeat or omit procedures; each page says soSparse rows carry
procedure_id, source URL, and available reference/type; retrieve titles, stages, and participants witheuparl_get_procedure
euparl_get_procedure tool
Required
procedure_idacceptsYYYY-NNNN,YYYY/NNNN(TYPE), an optional letter suffix, the known path prefix, or the exact public procedure URIsection: "overview"returns scalar metadata and collection counts;events,participants, anddocumentspage one collection using snapshots. Events and participants come in date order (undated last), documents in ID orderHistorical participant groups stay historical. Follow document IDs with
euparl_search_documentsin get mode
euparl_search_documents tool
mode: "search"defaults tosource: "adopted_texts", the only family withtext,title, sitting-date, andrelated_process_idfilters; adopted texts takeyearandterm,committeetakesterm,plenarytakesyear.work_typecodes are family-specificAdopted-text rows carry
title(in the requested language only),date, andprocedure_ids; a page holds at most 100 rows. Committee and plenary rows carry identity onlymode: "get"takes adocument_idfrom any tool (procedures, speeches, votes, questions, answers) in any letter case, or a citation such asT9-0138/2024,P9_TA(2024)0138,A9-0188/2023,RC-B9-0163/2024,B9-0163/2024, orP9_OJQ(2024)03-13, and rejects search filters and pagingDetail returns metadata, procedure and
parent_document_idsreferences, publishedadopted_document_idsandbased_on_document_ids, optionalpublic_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 upstreamAdopted 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 thatprocedure_idsmay be incomplete
euparl_search_speeches tool
Search by
text,title,person_id,activity_type, or sitting-date range; optionaltermselects 9 or 10, defaulting to 10. Term-9 records start in July 2021mode: "get"requiresspeech_idand rejects search filters and nondefault pagingReturns activity metadata, participants, and
original_languagesfor every row. Followdocument_idsthrough document get and any parent references to source filesinclude_text: true(search and get) adds each speech's publishedtext,speaker, andlanguage_tagsin 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
termandmandate_dateselects members in office today;termselects a published roster from terms 0–10;mandate_date(YYYY-MM-DD, not after today, exclusive withterm) selects members seated on that date. Optional country, political-group, and gender codes come frommep_filtersOn a
mandate_dateroster, country and group are matched and returned as published on that date, sopolitical_groupmatches only the code in force then, as it does on the current roster; for earlier groups passmandate_date, or omitpolitical_groupand read each member's published group. A term's first sitting day can return an incomplete roster or an upstream error; use the following daypolitical_grouptakes the 46 published group codes, current and historical (PPE-DE,ALDE,GUE-NGL, listed with labels and periods byeuparl_list_referencetopicmep_filters), their published labels in any case, and the aliasesEPP,Greens/EFA, andPatriots.GUE/NGLnames two codes: it selectsGUE-NGLon terms 0–8 and dates through 2020-12-18,THE-LEFTotherwise, 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 termCountry
UK→GB, the code Parliament publishes for United Kingdom members on historical rosters; published historical group identities remain intactname_containsmatches every normalized token across the complete roster before paging, ignoring case, diacritics, and punctuation; a value with no letter or digit is rejectedmep_idskeeps the listed person IDs (decimal,person/…, or the public URI) from the selected roster, narrowing with every other filter;unmatched_mep_idslists the requested IDs the roster does not hold. Pick the roster the IDs belong to:mandate_dateset to the vote or sitting date for country and group as published that day,termfor names onlyResults use snapshot continuation. Country/group describe the selected roster context, never inferred for another date
euparl_get_mep tool
mep_idaccepts a decimal person ID,person/prefix, or exact public person URIDefault
section: "overview"returns identity,membership_count, andmembership_classifications(a count per classification code);section: "memberships"pages published history with snapshots, lateststart_datefirstclassification(a code such asCOMMITTEE_PARLIAMENTARY_STANDING, bare or as published, case-insensitive) andactive_on(YYYY-MM-DD, not after today) select memberships before paging and apply tosection: "memberships"only. Memberships without a classification never matchclassificationMissing membership end dates do not establish current membership
Resolve membership organization IDs, decimal or term codes such as
org/ep-9, througheuparl_list_referencewithtopic: "organizations"
euparl_get_plenary_votes tool
mode: "sittings"takesyear(sittings data starts in 2014; earlier years return no sittings);mode: "vote_items"takessitting_id(aYYYY-MM-DDdate orMTG-PL-ID) and lists what the sitting voted on in published order, each item with its title, based-on documents, procedures, recording documents, anddecision_count; defaultdecisionstakessitting_idand 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 sittingsdecision_countis absent andvote_item_idselects none of their decisions. Optionaldocument_id(a canonical ID or citation, in any letter case) filters any document relation by exact ID or hyphen-boundary descendant before paging; optionalvote_item_id(a procedure'sPLENARY_VOTE_RESULTSevent or avote_itemsrow) 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, withlanguageset to the title's language (French through 2023), are the alternativeOptional
label_contains, indecisionsorvote_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: adocument_idsentry is cited with its prefix and term joined and the year after the number (A-9-2022-0116isA9-0116/2022, the joint motionRC-9-2022-0257isRC-B9-0257/2022); matching text does not establish a final vote. Indecisionsit combines with document and vote-item filters before pagingdecisionaddsdecision_idand returns one rich record with commentary (an ID absent from the sitting failsdecision_not_found). Optionalmep_idindecisionsordecisionreturns separate recorded/intended positions and publication availability without a roster lookup; an empty match does not prove absence from the votemode: "voters"addsposition:favor,against,abstention, or theintended_*corrections, which never replace recorded votes.available: falsemeans the array was not published; a published empty array hasavailable: 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 ofvoters,voter_countsandmep_position, the tallies stay as published, and a notice says so. Voters still without a name afterresolve_namesare named by oneeuparl_search_mepscall withmep_ids,mandate_dateset to the sitting date andlimitat least their countresolve_names: truerequiresroster_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"acceptsyearandwork_type; no keyword, author, or addressee searchmode: "get"requiresquestion_id: a canonical work ID, known document prefix/public URI, or human citation such asE-000001/2025(also P, O, and the G/K interpellation prefixes). Optionaltermresolves a citation directly; omission checks the corresponding mandate, including both candidates in election years, and requires one exact matchGet rejects list filters and nondefault paging. Detail returns metadata,
alternative_title,addressee_ids(the organizations published with theADDRESSEErole, verbatim; they do not resolve througheuparl_list_reference),original_languages, source files, and published answer references; emptyanswersdoes not prove the question is unansweredQuestion prefixes E/P/O/G/K are case-insensitive and sequences of one to six digits are padded to six:
e-3/2025andE-10-2025-3resolve asE-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), andlanguage(defaulten, any of the 24 EU languages). Rosters, procedure sections, memberships, vote items, decisions, and voters are acquired whole, then paged locally with apage.snapshotthat continuation must pass back;snapshot_changedmeans restart at offset 0. Procedure listings withprocess_typeare acquired whole too, and passing their snapshot back is optionalWhole-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_text100 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=falserejects 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, soeuparl_search_documentsrejects 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, andhas_morecan remain true without a reachablenext_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 conflatedTyped error reasons with recovery hints, and the same data on
content[]andstructuredContent. 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'sretryAfterin 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/mcpOr run it locally:
claude mcp add eu-parliament-mcp-server -- npx -y @cyanheads/eu-parliament-mcp-serverFor 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 |
| Transport: |
|
| HTTP listen host. |
|
| HTTP server port. |
|
| HTTP session mode: |
|
| Authentication: |
|
| Log level ( |
|
| Directory for log files (Node.js only). |
|
| Storage backend: |
|
| Enable OpenTelemetry. |
|
| Normalize bounded-mode limits with a notice; |
|
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 .envLocal 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:stdioRun 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:latestThe 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 |
| Nine read-only tool definitions ( |
| Shared schemas, lossless rendering, and enrichment. |
| Server environment: the |
| JSON-LD client, edge schemas, reference codes, normalization, and domain workflows. |
| Unit and integration tests, mirroring the |
| 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/catchin tool logicUse
ctx.logfor logging,ctx.statefor storageRegister new tools and resources in the
createApp()arrays insrc/index.tsWrap 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 testLicense
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…
This server cannot be deployed
Maintenance
Related MCP Connectors
Query FDA data on drugs, food, devices, and recalls via openFDA. STDIO or Streamable HTTP.
Query SEC EDGAR filings, XBRL financials, and company data via MCP. STDIO or Streamable HTTP.
Search MPs and Lords, fetch profiles, synopses, and Westminster constituencies
Official EUR-Lex, UK, US, and gazette search, metadata fetch, and change-only watches.
Related MCP Servers
- AlicenseNot gradedqualityBmaintenanceProvides access to European Parliament open data, enabling queries about MEPs, plenary documents, and session calendar.151 npmMIT
- AlicenseNot gradedqualityBmaintenanceEnables 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 npm2Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables 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 npm1Apache 2.0
- AlicenseNot gradedqualityBmaintenanceEnables 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 npm1Apache 2.0