@cyanheads/eur-lex-mcp-server
Provides access to European Union legal information, including searching and retrieving EU legislation, CJEU case law, and treaties, traversing amendment and citation relationships, and browsing EuroVoc subjects via EUR-Lex and CELLAR.
Click on "Deploy Server".
Wait a few minutes for the server to deploy. Once ready, it will show a "Started" state.
In the chat, type
@followed by the MCP server name and your instructions, e.g., "@@cyanheads/eur-lex-mcp-serversearch for recent regulations on artificial intelligence"
That's it! The server will respond to your query, and you can continue using it as needed.
Here is a step-by-step guide with screenshots.
Public Hosted Server: https://eur-lex.caseyjhand.com/mcp
Overview
EU legislation, CJEU case law, and treaties over the EU Publications Office's CELLAR semantic repository and the EUR-Lex content API. Search documents and case law, fetch full text, resolve citations, traverse the amendment and citation graph, and browse the EuroVoc thesaurus from any MCP client. Runs as a stdio process, a local Streamable HTTP server, or the public hosted endpoint above.
Tools
Tool | Description |
| Search EU legislation, treaties, and preparatory acts by type, date, EuroVoc subject, author institution, and in-force status |
| Fetch metadata and full text (HTML, Markdown, or Formex4 XML) for an act or case by CELEX, ELI, or work URI, with a section outline of either |
| Resolve a CELEX number, ELI URI, ECLI, or OJ citation ( |
| Search CJEU and General Court case law by case number, court, case type, and date range |
| Traverse the CELLAR relationship graph — amendments, repeals, consolidations, legal basis, citations, transpositions |
| Search the EuroVoc thesaurus to resolve terms to concept URIs |
| Run a raw, read-only SPARQL SELECT against the CELLAR endpoint |
Resources
Resource | Description |
| Metadata snapshot for a CELLAR work |
| One-hop relationship summary for a CELLAR work |
All resource data is also reachable via tools.
Prompts
Prompt | Description |
| Frame a comparative EU/US legal analysis for a policy domain |
Related MCP server: eurlex-mcp-server
Capability reference
eurlex_search_documents tool
At least one filter:
keyword(English titles, plus CELEX numbers when it holds a digit: a whole CELEX with its(01)–(20)siblings andR(01)–R(20)corrigenda by exact lookup, a partial one such as2016R0679,R0679,J0131, or the OJ C2024/01469through the CELEX full-text index; one opening with letters no digit follows, such asR(01), or with a year, a slash, and a number under four characters, such as2017/111, by a scan of every CELEX that can take tens of seconds; a bare year or a fragment opening mid-year or mid-number, such as0679R, matches titles only; no body search),document_type(REG,DIR,DEC,TREATY,JUDG,OPIN_AG,PROP,REC, each its full CELLAR authority family),date_from/date_to,eurovoc_concept(fromeurlex_browse_subjects),author_institution, orin_force(true/false;falsecovers repealed, expired, and not-yet-in-force acts)Pages of up to 100 via
offset/limit, newest first with the CELEX breaking date ties, so a page is the same on every call; each row flagsis_consolidatedandis_corrigendum, and corrigenda join only underinclude_corrigenda, consolidated texts of adocument_typeonly underinclude_consolidatedNo match returns an empty page with a
noticenaming the filters and how to broaden them; typed errors:no_filters,invalid_date_range,invalid_author_institutionandinvalid_keyword(no letters or digits)
eurlex_get_document tool
Exactly one of
celex_number,eli_uri, orwork_uri, served as the work the CELEX resolves to (seeeurlex_lookup_celex); awork_uricarrying several CELEX numbers (a national implementing measure) serves its lowest, with anoticegiving the count; body ashtml(default),markdown, orxml(Formex4) in any of the 24 EUR-Lex languages, falling back to English;titleis in the language served, or English when CELLAR has none in that languageauthor_institution(s)name each author by the English label of its CELLAR authority code — an EU institution or body (European Union,Council of the European Union), a member state (Netherlands), an MEP (VAN MIERT), or a national court — the same labelseurlex_search_documentsaccepts asauthor_institution; case law lists itsadvocates_generalapartCase law also carries its
ecli(the oneeurlex_lookup_celexreports) and its English CELLAR title, in every language, parsed aseurlex_get_casesparses it:titleis the parties (the court/AG descriptor when there are none), withformation,referring_court,subject_matter, andcase_referencealongside, or the raw#-joined title when the parse misses part of itcontent_mode"paged"(default),"full", or"metadata_only", every body capped at 100,000 characters per call withcontent_chars_total/has_moreto page on;outline: truelists chapter/section/article/annex headings with offsets, the preamble's recitals as oneRecitals 1–173entry (include_recitals: truelists each), andselect(e.g.{ articles: "1,5,17" }) returns just those sections, each source character once, under the same cap with nooffset/limit, withselected_sectionsgiving each one's ownoffset/charsfor apagedread. Headings are read in the language served and labelled in English in every format (Article 4,CHAPTER IV); a selector number may carry an English kind word or the served language's (Artikel 4,4. cikk). Headings of text an amending act inserts into another act are not the act's own and are skippedFor a judgment, order, or AG opinion,
outlinelists each top-level section heading as served (Legal context,Sur les dépens), numbered by position, and a judgment's or order's ruling as oneoperative_partentry;select: { headings: "2" }returns a headed section up to the next heading, andselect: { operative_part: true }the ruling alone to the end: from "On those grounds, …" in a modern body, from the "Operative part" section in a legacy one. Headings come from the body's heading markup in each CELLAR html generation and in Formex, so html, Markdown, and xml get the same outline; a summary-only body and some AG opinions carry no heading markup and outline emptyis_supersededsays whether a newer consolidated version than the text served is in effect (falseon the newest one, and on a consolidated version dated after it that does not apply yet), withcurrent_consolidated_celex/consolidated_as_ofnaming that version;resolve: "current_consolidated"serves it, for a base act or any of its consolidated texts; when no consolidated version is in effect yet, anoticesays a future-dated consolidated text does not apply yet, or thatresolveserved the base actin_force: falsecomes with its reason where CELLAR records one:repealed_by(CELEX of the explicitly repealing acts),end_of_validity(omitted when open-ended; a future date on an act not yet in force), orentry_into_force(the earliest date, when still ahead)A consolidated text keeps its own title, date, and type and reports its base act as
base_act_celex, with that act's authors,in_forceand its reason, legal basis, and EuroVoc subjects; typed errors:invalid_identifier_args,not_found,content_challenge(a WAF bot-challenge in place of text)
eurlex_lookup_celex tool
A CELEX number, ELI URI, ECLI, or OJ citation naming its act type and year (
Regulation (EU) 2016/679,Regulation (EC) No 1049/2001,Directive 95/46/EC,Council Framework Decision 2002/584/JHA), parsed to its CELEX underidentifier_type: "auto":Nobefore the numbers means number/year, a two-digit year is 19YY, and the act type sets the CELEX letter (an ECSC Decision isS, a Framework DecisionF, a Joint Action or Common PositionE); a citation without its act type (95/46/EC) or year (Regulation No 17) is not parsedidentifier_typeauto-detects the format or sets it, andambiguous_identifierfires when auto-detection can't classify the input, its recovery listing the accepted formsReturns work URI, confirmed CELEX number, resource type, date, and the case's ECLI (recorded on any work holding the CELEX);
found: falsefor a well-formed identifier that matches no work, with anoticenaming the CELEX, ELI, or ECLI tried and pointing toeurlex_search_documentsA CELEX held by several works resolves to the one
owl:sameAsitshttp://publications.europa.eu/resource/celex/{CELEX}IRI, which EUR-Lex serves the text from, else the lowest work URI, and every CELEX-taking tool and resource resolves the same way; an ECLI shared by several records resolves to the primary record with the lowest CELEX
eurlex_get_cases tool
Filters:
case_number(one case per value —C-131/12,T-22/20,F-12/05, or a pre-198926/62— reaching every judgment, order, and AG opinion filed under it),court(CJEUorGC, by CELEX court letter),case_type(judgment,order,ag_opinion),keyword(English titles, plus CELEX numbers: a whole CELEX with its(01)–(20)siblings and_INF/_RES/_SUM/_EXTrecords, a partial one such as2013CJ0131orJ0131through the CELEX full-text index, one opening with letters no digit follows by a scan of every CELEX), anddate_from/date_to; primary records only unlessinclude_derivativeadds notices, abstracts, summaries, and corrigendaPages of up to 100 via
offset/limit, newest first with the CELEX breaking date ties, so a page is the same on every call; each case carries its ECLI where CELLAR records one, plusformation,advocate_general,display_title,parties,referring_court,subject_matter, andcase_referenceparsed from the CELLAR title; the rawtitlecomes back only when those fields miss part of it (an unrecognized segment, an "(Extracts)" marker, or a title date that differs from the case date)No match returns an empty page with a
noticenaming the filters and how to broaden them; typed errors:invalid_case_number,invalid_date_range,invalid_keyword(no letters or digits)
eurlex_get_relations tool
Exactly one of
celex_numberorwork_uri;relation_typesnarrows to any ofcites,amends,amended_by,repeals,repealed_by,implicitly_repeals,implicitly_repealed_by,legal_basis,consolidated_version,national_transposition(omit for all)One hop, paged per relation type and direction via
offset/limit(max 100, default 100), newest first with the work URI breaking ties, so a page is the same on every call; undated works come lastEach relation carries
relation_type,direction(outgoing/incoming),related_work_uri,related_celex_numberwhen known,related_date(the date the page is ordered by) andrelated_title(the English title, whole) when the work has them, and onnational_transpositionrowsrelated_member_state(ISO 3166-1 alpha-3,GBRfor the United Kingdom) — national measures rarely carry an English title, and no other language stands in;empty_relation_typesseparates "no edges of this type" from "paged out", a work with no edges of the requested types returns an empty page with anotice, and typed errors areinvalid_identifier_argsandnot_found
eurlex_browse_subjects tool
Matches preferred and alternative EuroVoc labels, so a common synonym resolves to its concept, in any EU official
language(default English);offset/limitpagination (max 50)Returns concept URI, preferred label, code, broader (parent) label, and the alternative label that matched when one did; an empty page with a
noticewhen nothing matchesExact label matches rank first, then label or word-start matches, then other substring matches, so
"AI"leads with artificial intelligence
eurlex_query_sparql tool
Read-only SELECT only — update forms and ASK/CONSTRUCT/DESCRIBE are rejected before execution;
cdm:,skos:, andxsd:prefixes are auto-injectedResults capped at 100 rows; optional
timeout_hint(1000–55000 ms) under the endpoint's 60-second hard limitA zero-row result whose query has an untyped string literal as a triple object carries a
noticeto type it (^^xsd:string,^^xsd:anyURIfor an ELI) or language-tag it; the query itself is sent unchangedTyped errors:
not_read_only(a SPARQL Update),unsupported_query_form(ASK, CONSTRUCT, DESCRIBE, or no SELECT),sparql_error,sparql_timeout
eurlex://document/{celexNumber} resource
Metadata snapshot as
application/json— resource type, author institution(s) labelled aseurlex_get_documentlabels them, Advocates General, date, English title, in-force flag, legal basis, EuroVoc subjects; a consolidated text addsbase_act_celexand reads authors, in-force flag, legal basis, and subjects from that actcelexNumbercomes fromeurlex_search_documents,eurlex_get_cases, oreurlex_lookup_celex
eurlex://document/{celexNumber}/relations resource
One-hop relationship summary — amendment chain, consolidations, national transposition (each measure's
related_member_stateincluded), legal basis, citations — each related work with itsrelated_dateand Englishrelated_titlewhere it has them, capped at 25 per relation type and direction, keeping the newesttruncatedplus acontinuationpointer toeurlex_get_relationswhen more relations exist
eurlex_comparative_analysis prompt
Arguments:
domainrequired;focusoptional, folded into its matching analysis axis or added as its own sectionReturns a research plan chaining
eurlex_browse_subjects→eurlex_search_documents→eurlex_get_document→eurlex_get_relationsfor the EU side andcourtlistener_search_opinionsfor the US side, plus a six-axis analysis framework
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.
EUR-Lex-specific:
No API key required — CELLAR SPARQL and the EUR-Lex REST content endpoints are both publicly accessible
SPARQL is POSTed with CDM prefix declarations built in; server-side LIMIT enforcement (max 100) guards against Virtuoso timeouts
Act text is fetched via CELLAR content negotiation (
/resource/celex/{CELEX}); HTML passes through as served; Formex4 XML passes through for a single-part act and is assembled into one document from the parts of a multi-part act (HTTP 300 streams) or a zipped Formex package; Markdown is converted server-sideVirtuoso errors (HTTP 200 with a
Virtuoso 37000 Errorbody) are classified and re-raised asServiceUnavailableorValidationErrorAutomatic English fallback when a requested translation is unavailable, with requested/effective language reported
Agent-friendly output:
EuroVoc prerequisite guidance in server-level instructions — agents are directed to
eurlex_browse_subjectsbefore concept-filtered searcheseurlex_lookup_celexconfirms CELEX/ELI/ECLI existence upfront, preventing downstream errors in document or relation fetchescontent_status,content_unavailability_reason, and requested/effective language fields distinguish skipped, available, absent, upstream-failed, and incomplete content without string parsingTyped
reasoncodes on every tool's error contract let agents branch on outcomes programmatically
Getting started
Public Hosted Instance
A public instance is available at https://eur-lex.caseyjhand.com/mcp — no installation required. Point any MCP client at it via Streamable HTTP:
{
"mcpServers": {
"eur-lex-mcp-server": {
"type": "streamable-http",
"url": "https://eur-lex.caseyjhand.com/mcp"
}
}
}Self-Hosted / Local
Add the following to your MCP client configuration file. No API key is required.
{
"mcpServers": {
"eur-lex-mcp-server": {
"type": "stdio",
"command": "bunx",
"args": ["@cyanheads/eur-lex-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}Or with npx (no Bun required):
{
"mcpServers": {
"eur-lex-mcp-server": {
"type": "stdio",
"command": "npx",
"args": ["-y", "@cyanheads/eur-lex-mcp-server@latest"],
"env": {
"MCP_TRANSPORT_TYPE": "stdio",
"MCP_LOG_LEVEL": "info"
}
}
}
}Or with Docker:
{
"mcpServers": {
"eur-lex-mcp-server": {
"type": "stdio",
"command": "docker",
"args": [
"run", "-i", "--rm",
"-e", "MCP_TRANSPORT_TYPE=stdio",
"ghcr.io/cyanheads/eur-lex-mcp-server:latest"
]
}
}
}For Streamable HTTP, set the transport and start the server:
MCP_TRANSPORT_TYPE=http MCP_HTTP_PORT=3010 bun run start:http
# Server listens at http://localhost:3010/mcpPrerequisites
Bun v1.4.0 or higher (or Node.js v24+).
No API key needed — EUR-Lex and CELLAR are publicly accessible.
Installation
Clone the repository:
git clone https://github.com/cyanheads/eur-lex-mcp-server.gitNavigate into the directory:
cd eur-lex-mcp-serverInstall dependencies:
bun installConfigure environment (optional):
cp .env.example .env
# All server-specific vars have sensible defaults — no required varsConfiguration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts.
Variable | Description | Default |
| CELLAR SPARQL endpoint URL override (e.g., for a local Virtuoso mirror). |
|
| EU Publications Office CELLAR content resolver base URL override. |
|
| Client-side timeout for SPARQL requests in milliseconds. |
|
| Enforced ceiling on LIMIT in all generated SPARQL queries. |
|
| Transport: |
|
| Port for HTTP server. |
|
| Auth mode: |
|
| Session handling: |
|
| Log level (RFC 5424). |
|
| Directory for log files (Node.js only). |
|
| Enable OpenTelemetry instrumentation. |
|
See .env.example for the full list of optional overrides.
Running the server
Local development
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:httpRun checks and tests:
bun run devcheck # Lint, format, typecheck, security bun run test # Vitest test suite bun run lint:mcp # Validate MCP definitions against spec
Docker
docker build -t eur-lex-mcp-server .
docker run --rm -p 3010:3010 eur-lex-mcp-serverThe Dockerfile defaults to HTTP transport, stateless session mode, and logs to /var/log/eur-lex-mcp-server. OpenTelemetry peer dependencies are installed by default — build with --build-arg OTEL_ENABLED=false to omit them.
Project structure
Directory | Purpose |
|
|
| Server-specific environment variable parsing and validation with Zod. |
| CELLAR SPARQL service — POST client, binding mapper, LIMIT enforcement, CDM PREFIX declarations. |
| CELLAR content service — content-negotiation GET client for |
| Tool definitions ( |
| Resource definitions ( |
| Prompt definitions ( |
| Unit and integration tests mirroring |
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 request-scoped logging,ctx.statefor tenant-scoped storageRegister new tools and resources via the barrels in
src/mcp-server/*/definitions/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
Apache-2.0 — see LICENSE for details.
This server cannot be deployed
Maintenance
Related MCP Connectors
Search Swiss federal legislation: laws, articles, amendments via the Fedlex SPARQL endpoint.
Temporal search and comparison for official Luxembourg and reviewed EU law, with provenance.
Resolve, search and verify legal citations against the official sources, with provenance.
Search and fetch Wikidata entities, execute SPARQL queries, and resolve external identifiers.
Related MCP Servers
- AlicenseNot gradedqualityDmaintenanceA community-maintained MCP server that simplifies access to EU legal and legislative data from the CELLAR service, supporting lookups, metadata retrieval, relation checks, and monitoring.2MIT
- AlicenseAqualityAmaintenanceEnables searching and retrieving EU legal documents (regulations, directives, court decisions) via the EUR-Lex Cellar API, supporting full-text search, metadata, citations, and consolidated versions without requiring an API key.1130 npm6MIT
- AlicenseAqualityBmaintenanceMCP server for EU law via the EUR-Lex / Cellar SPARQL endpoint — legislation (ELI/CELEX) and CJEU case-law (ECLI) with verifiable citations.3232 npm1MIT

LexAPI MCPofficial
AlicenseAqualityCmaintenanceEnables querying EU legal documents, case law, and citation graphs through natural language using the LexAPI.1044 npm3MIT