About eur-lex-mcp-server
eur-lex-mcp-server exposes EU legislation, CJEU case law, and treaties from EUR-Lex for legal and policy research. Its tools also resolve citations, traverse document relationships, and browse EuroVoc concepts.
Key features
- 01Search legislation, treaties, and preparatory acts
- 02Retrieve document metadata and full text
- 03Resolve CELEX, ELI, ECLI, and OJ citations
- 04Search CJEU and General Court cases
- 05Trace CELLAR document relationships
- 06Browse EuroVoc concepts
Use cases
- 01Find EU acts by date, type, or subject
- 02Retrieve articles or rulings from a cited document
- 03Trace amendments, repeals, and legal citations
- 04Research EU case law by court and case number
MCP support
Provides seven tools, two resources, and a comparative-analysis prompt to MCP clients. Supports stdio and Streamable HTTP, and serves its skill through the Skills extension.
Configuration
Remote servers need no local install. Pick your client, then copy the config or install in one click.
https://eur-lex.caseyjhand.com/mcpclaude mcp add --transport http cyanheads-eur-lex https://eur-lex.caseyjhand.com/mcp{"mcpServers":{"cyanheads-eur-lex":{"type":"http","url":"https://eur-lex.caseyjhand.com/mcp"}}}Open in Cursor{"servers":{"cyanheads-eur-lex":{"type":"http","url":"https://eur-lex.caseyjhand.com/mcp"}}}Open in VS Code{"mcpServers":{"cyanheads-eur-lex":{"type":"http","url":"https://eur-lex.caseyjhand.com/mcp"}}}{"mcpServers":{"cyanheads-eur-lex":{"serverUrl":"https://eur-lex.caseyjhand.com/mcp"}}}Run commands in a terminal; paste JSON into the client’s MCP config and restart. Servers that need sign-in will prompt on first connect.
Tools
7| Parameter | Type | Required | Description |
|---|---|---|---|
| identifier | string | Yes | The EU legal citation to resolve: a CELEX number (e.g. 32016R0679), a work-level ELI URI (e.g. http://data.europa.eu/eli/reg/2016/679, with or without the /oj suffix), an ECLI (e.g. ECLI:EU:C:2014:317), or an OJ citation that names its act type and year, resolved to the CELEX it names under identifier_type "auto": Regulation (EU) 2016/679, Regulation (EC) No 1049/2001, Directive 95/46/EC, Decision No 1313/2013/EU, Council Framework Decision 2002/584/JHA, Council Joint Action 2008/124/CFSP, Common Position 2003/444/CFSP. "No" before the numbers means number/year, otherwise year/number; a two-digit year is 19YY. A citation without its act type (95/46/EC) or year (Regulation No 17) is not parsed. A CELEX is also read in lowercase, after a CELEX: prefix, from a EUR-Lex page URL carrying uri=CELEX:… (e.g. https://eur-lex.europa.eu/legal-content/EN/TXT/?uri=CELEX:32016R0679), or from a publications.europa.eu/resource/celex/ URI; an ELI over https, on the eur-lex.europa.eu host, without a scheme, with a trailing slash, or with a language segment after /oj or a date (…/oj/eng). A case number (C-131/12) names several records rather than one work and returns found false with a pointer to eurlex_get_cases. |
| identifier_type | enum | No | Format of the identifier. "auto" detects a CELEX, an ELI URI, an ECLI (by its ECLI: prefix, in any letter case), or an OJ citation; "celex", "eli", or "ecli" forces that lookup. An OJ citation is recognized only under "auto".celex | eli | ecli | auto |
Details
From the project README@cyanheads/eur-lex-mcp-server
Search EU legislation, CJEU case law, and treaties; traverse the CELLAR relationship graph; resolve EuroVoc concepts via MCP. STDIO or Streamable HTTP.
Public Hosted Server: https://eur-lex.caseyjhand.com/mcp
Overview
EU legislation, CJEU case law, and treaties from EUR-Lex. Search documents and case law, fetch full text, resolve citations, trace how acts amend and cite each other, and browse the EuroVoc subject thesaurus.
Tools
| Tool | Description |
|---|---|
eurlex_search_documents | Search EU legislation, treaties, and preparatory acts by type, date, EuroVoc subject, author institution, and in-force status |
eurlex_get_document | Fetch metadata (including ELI, Official Journal reference, and dates of effect) 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 |
eurlex_lookup_celex | Resolve a CELEX number, ELI URI, ECLI, OJ citation (Regulation (EU) 2016/679), or Official Journal reference (OJ L 119, 4.5.2016, p. 1) to its canonical CELLAR work |
eurlex_get_cases | Search CJEU and General Court case law by case number, court, case type, and date range |
eurlex_get_relations | Traverse the CELLAR relationship graph — amendments, repeals, consolidations, legal basis and the works adopted under it, the proposal an act adopts, interpreting rulings (by article), citations, transpositions |
eurlex_browse_subjects | Search the EuroVoc thesaurus to resolve terms to concept URIs |
eurlex_query_sparql | Run a raw, read-only SPARQL SELECT against the CELLAR endpoint |
Resources
| Resource | Description |
|---|---|
eurlex://document/{celexNumber} | Metadata snapshot for a CELLAR work |
eurlex://document/{celexNumber}/relations | One-hop relationship summary for a CELLAR work |
All resource data is also reachable via tools.
Prompts
| Prompt | Description |
|---|---|
eurlex_comparative_analysis | Frame a comparative EU/US legal analysis for a policy domain |
Skills
| Skill | Description |
|---|---|
eur-lex-guide | For agents calling the tools: where each CELEX, ELI, ECLI, and case-number form comes from, the checks to run before concluding an act is in force, repealed, uninterpreted, or final, and a CELLAR SPARQL and CDM reference for eurlex_query_sparql |
The skill is in skills/. Plugin installs, the .mcpb bundle, the npm package, and the Docker image include it, and the server serves it to MCP clients through the Skills extension (skills/list, skill:// resources).
Capability reference
eurlex_search_documents tool
- At least one filter:
keyword(English titles, plus CELEX numbers or fragments such as2016R0679; a keyword with letters and no digit, such asR(01), scans every CELEX and can take tens of seconds; no body search),document_type(REG,DIR,DEC,TREATY,JUDG,OPIN_AG,PROP,REC),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(a largerlimitis served at 100 and reported inlimit_clamp), newest first; each row flagsis_consolidatedandis_corrigendum, and corrigenda and consolidated texts appear only underinclude_corrigendaandinclude_consolidated. Paging stops at row 10,000: the last page carries nonext_offsetand anoticegiving thedate_from/date_towindow that continues past it - No match returns an empty page with a
noticeon how to broaden the filters; typed errors:no_filters,invalid_date_range,invalid_author_institutionandinvalid_keyword(no letters or digits),offset_past_window(offset10,000 or more) author_institutionmatches English authority labels and national-court names returned byeurlex_get_document, such asEuropean ParliamentorSupremo Tribunal de Justiça, with or without date bounds
eurlex_get_document tool
- Exactly one of
celex_number,eli_uri, orwork_uri(pasted forms such as aCELEX:prefix or EUR-Lex URL are accepted); body ashtml(default),markdown, orxml(Formex4) in any of the 24 EUR-Lex languages, falling back to English. Case law also carriesecli,formation,referring_court,subject_matter, andcase_reference; a consolidated text reports its base act asbase_act_celex content_mode"paged"(default),"full", or"metadata_only", capped at 100,000 characters per call (a largerlimitis reported inlimit_clamp) withcontent_chars_total/has_moreto page on;outline: truelists headings with offsets, andselect(e.g.{ articles: "1,5,17" }) returns just those sections. For judgments and AG opinions the outline lists section headings, andselect: { operative_part: true }returns the ruling aloneis_supersededflags a newer consolidated version (current_consolidated_celex,consolidated_as_of), andresolve: "current_consolidated"serves it;in_force: falsecomes withrepealed_by,end_of_validity, orentry_into_forcewhere CELLAR records one. Typed errors:invalid_identifier_args,not_found,content_challenge(a WAF bot-challenge in place of text)- Citation facts, each absent when CELLAR records none:
eli(a consolidated text keeps its own point-in-time ELI);oj_reference, the Official Journal reference (OJ L 119, 4.5.2016, p. 1–88; no series letter before 1968, a class letter as inOJ L 42I,OJ L, 2024/1689, 12.7.2024for act-by-act publication), with pages from the English edition or, before 1973, the original-language editions, and the earliest issue for a reprinted act;dates_of_effect, every recorded date as{ date, type? }withtypeone ofentry_into_force,application,partial_application,provisional_application,takes_effect,takes_partial_effect; andtransposition_deadlinesfor a directive.entry_into_forcekeeps its meaning of an entry still ahead on a not-in-force act. A consolidated text reports its base act'soj_reference,dates_of_effect, andtransposition_deadlines - Available legacy HTML/Markdown bodies may carry
content_omissions: { tables }: missing-table markers counted across the full source, including outside the returned window. Paging cannot recover these tables.alternative_representationslists PDF streams advertised for the served work and language when discovery succeeds; XML availability remains independent. Metadata-only calls do not inspect source omissions - National implementing measures carry sparse
national_measurecitation fields:journal,number,pages,publication_date, andnotification_date, including metadata-only calls. Numbers and pages retain their source spelling; publication, notification, and document dates are distinct. When the body is unavailable, use the gazette citation to retrieve it; no national-portal URL is inferred, and notification does not prove complete transposition
eurlex_lookup_celex tool
- A CELEX number, ELI URI, ECLI, or OJ citation naming its act type and year (
Regulation (EU) 2016/679,Directive 95/46/EC, optionally followed by its parenthesized OJ reference,Directive 95/46/EC (OJ L 281, 23.11.1995, p. 31)); pasted forms (aCELEX:prefix, a EUR-Lex page URL, an ELI with or without scheme or language segment) are normalized and echoed incelex_number. A citation without its act type (95/46/EC) or year (Regulation No 17) is not parsed - An Official Journal reference resolves to the act starting on the cited page: issue, publication date, and first page (
OJ L 281, 23.11.1995, p. 31, alsoof 23.11.1995, a page range, a class letter as inOJ L 42I,OJ C, and before 1968 the series-lessOJ 13, 21.2.1962, p. 204/62); the year-firstOJ 1995 L 281, p. 31andOJ 1995 L 281/31; and act-by-actOJ L, 2024/1689, 12.7.2024andOJ C, C/2024/2664, 29.4.2024. A cited date must equal the issue's publication date. Pages are matched on the English edition, or on any language before 1973 when the issue has no English pages. A reference without its year or first page (OJ L 281,OJ L 119/1) is not parsed. Recognized only underauto identifier_typeauto-detects the format or sets it;ambiguous_identifierfires when auto-detection can't classify the input (including an OJ reference without its year or first page). A case number (C-131/12) returnsfound: falsewith anoticepointing toeurlex_get_cases- Returns work URI, confirmed CELEX number, resource type, date, and the case's ECLI;
found: falsewith anoticefor a well-formed identifier that matches no work, for an OJ reference whose date or page matches nothing, and for one whose first page starts several acts (thenoticenames each CELEX)
eurlex_get_cases tool
- Filters:
case_number(C-131/12,T-22/20,F-12/05, or a pre-198926/62; reaches every judgment, order, and AG opinion filed under it),court(CJEUorGC),case_type(judgment,order,ag_opinion),keyword(English titles, plus CELEX numbers or fragments such as2013CJ0131), anddate_from/date_to; primary records only unlessinclude_derivativeadds notices, abstracts, summaries, and corrigenda - Pages of up to 100 via
offset/limit(a largerlimitis served at 100 and reported inlimit_clamp), newest first; each case carries its ECLI where recorded, plusformation,advocate_general,display_title,parties,referring_court,subject_matter, andcase_reference, and the rawtitleonly when those fields miss part of it. Paging stops at row 10,000: the last page carries nonext_offsetand anoticegiving thedate_from/date_towindow that continues past it - No match returns an empty page with a
noticeon how to broaden the filters; typed errors:invalid_case_number,invalid_date_range,invalid_keyword(no letters or digits),offset_past_window(offset10,000 or more) case_type: "ag_opinion"includes Advocate General opinions (OPIN_AG) and views (VIEW_AG);resource_typedistinguishesAG OpinionfromAG View
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,basis_for,interprets,interpreted_by,consolidated_version,national_transposition,adopts,adopted_by(omit for all fifteen) basis_forlists the works naming the act among their legal bases;interpreted_bylists the CJEU and General Court rulings interpreting it, andinterpretswhat a ruling interprets;adoptslists the works an act adopts (usually its Commission proposal) andadopted_bythe act that adopted a work.member_statesnarrowsnational_transpositionto named member states (ISO 3166-1 alpha-3 or two-letter codes, echoed as alpha-3); using it withoutnational_transpositionfails asmember_states_not_applicablearticlenarrowsinterpreted_byto rulings with a recorded provision in one article of the act (17,6a,Article 17,Art. 17, any case; blank is omitted; a paragraph, range, or Roman numeral is rejected). It matches at any depth, ignores leading zeros, and a letter also matches its Latin ordinal (28creaches Article 28 quater); rulings CELLAR records no provision for are left out, as thenoticesays, and the echoedarticleis the normalized form. Using it withoutinterpreted_byfails asarticle_not_applicable- Amendment, repeal, interpretation, and adoption rows carry
provisions, the provisions CELLAR records the edge as targeting, in compact codes (A17P1LCis Article 17(1)(c)) or authority-token codes (AR 43 PA 4); a row without them records none, which does not mean the edge reaches the whole act. A page that cannot fit its provisions within the budget at depth 1 withholds them whole, largest first, and reports each row's count asprovisions_withheld; a provisions lookup CELLAR fails returns the rows without them, named in thenotice - One hop, paged per relation type and direction via
offset/limit(up to 100, default 100; a largerlimitis reported inlimit_clamp), newest first. Each type and direction pages to row 10,000, then anoticeroutes older works toeurlex_query_sparqlwith a ready-to-run query - Each complete response surface is budgeted independently at 64,000 UTF-8 bytes, including JSON and enrichment. Oversize pages retain the largest common per-type/direction depth that fits, with whole rows and titles; the notice gives requested/applied limits and rows withheld. Continue at
next_offset(the current offset plus the applied depth). A depth-1 page is returned whole even if it exceeds the budget, with disclosure. Fitting pages are unchanged; omittingrelation_typesequals listing all fifteen types - Each relation carries
relation_type,direction(outgoing/incoming),related_work_uri, and where knownrelated_celex_number,related_date,related_title(English only), andrelated_member_stateonnational_transpositionrows;empty_relation_typesseparates "no edges of this type" from "paged out". Typed errors:invalid_identifier_args,member_states_not_applicable,article_not_applicable,not_found,offset_past_window
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 (up to 50; a largerlimitis served at 50 and reported inlimit_clamp), to row 10,000 (offset_past_windowbeyond) - 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 matches - Exact 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);
cdm:,skos:, andxsd:prefixes are auto-injected; optionaltimeout_hint(1000–55000 ms) - Results capped at
MAX_SPARQL_RESULTS(default 100); a truncated result'snoticesays to addORDER BYand repeat with a higherOFFSET, up to row 10,000. A zero-row result from an untyped string literal gets anoticeto type it (^^xsd:string,^^xsd:anyURIfor an ELI) or language-tag it - Typed 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,eli,oj_reference,dates_of_effect,transposition_deadlines, legal basis, EuroVoc subjects (the four citation fields as ineurlex_get_document); a consolidated text keeps its owneli, addsbase_act_celex, and reads authors, in-force flag,oj_reference,dates_of_effect,transposition_deadlines, legal basis, and subjects from that act celexNumbercomes fromeurlex_search_documents,eurlex_get_cases, oreurlex_lookup_celex
eurlex://document/{celexNumber}/relations resource
- One-hop relationship summary over all fifteen
eurlex_get_relationstypes, with noprovisions, each related work with itsrelated_dateand Englishrelated_titlewhere it has them, capped at 25 per relation type and direction (newest kept) truncatedplus 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 section - Returns a research plan that restates the domain in EU legal vocabulary ("antitrust" → "competition law"), chains
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; HTML passes through as served, multi-part Formex4 XML is assembled into one document, and Markdown is converted server-side
- Syntax, compile, and sort-window errors fail at once as
sparql_error(a sort-window error,SR353, hints to lowerOFFSETor narrow the query) and timeouts assparql_timeout, neither retried; gateway pages and other transient failures are retried inside the call's shared query budget (SPARQL_QUERY_TIMEOUT_MS), and a retry ladder the budget cuts short reports the last attempt's own failure - Automatic 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 searches eurlex_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 parsing- Typed
reasoncodes on every tool's error contract let agents branch on outcomes programmatically
Getting started
Connect to the hosted instance, no install:
claude mcp add --transport http eur-lex-mcp-server https://eur-lex.caseyjhand.com/mcp
Or run it locally:
claude mcp add eur-lex-mcp-server -- npx -y @cyanheads/eur-lex-mcp-server
For other clients, add this to the MCP config file:
{
"mcpServers": {
"eur-lex-mcp-server": {
"command": "npx",
"args": ["-y", "@cyanheads/eur-lex-mcp-server"]
}
}
}
No API key is required. For Docker or a source build, see Running the server.
Configuration
All configuration is validated at startup via Zod schemas in src/config/server-config.ts.
| Variable | Description | Default |
|---|---|---|
CELLAR_SPARQL_ENDPOINT | CELLAR SPARQL endpoint URL override (e.g., for a local Virtuoso mirror). | http://publications.europa.eu/webapi/rdf/sparql |
EURLEX_CONTENT_BASE_URL | EU Publications Office CELLAR content resolver base URL override. | http://publications.europa.eu |
SPARQL_QUERY_TIMEOUT_MS | Client-side timeout in milliseconds, an integer from 1 to 2147483647: the budget a call's CELLAR queries share (a call that chains queries spends it once, not per query) and the ceiling on any single query; an EUR-Lex content fetch gets its own timeout of this length, outside the shared budget. | 55000 |
MAX_SPARQL_RESULTS | Enforced ceiling on LIMIT in all generated SPARQL queries. | 100 |
MCP_TRANSPORT_TYPE | Transport: stdio or http. | stdio |
MCP_HTTP_PORT | Port for HTTP server. | 3010 |
MCP_AUTH_MODE | Auth mode: none, jwt, or oauth. | none |
MCP_SESSION_MODE | Session handling: stateful, stateless, or auto (auto resolves to stateful). | stateless |
MCP_LOG_LEVEL | Log level (RFC 5424). | info |
LOGS_DIR | Directory for log files (Node.js only). | <project-root>/logs |
LOG_TOOL_FAILURE_PAYLOADS | Log failed tool calls' arguments and result, redacted by key name (free-form values are not), capped at LOG_TOOL_FAILURE_PAYLOAD_MAX_BYTES (default 16384). | false |
OTEL_ENABLED | Enable OpenTelemetry instrumentation. | false |
See .env.example for the full list of optional overrides.
Running the server
Local development
Requires Bun v1.4.0 or higher (or Node.js v24+).
-
Clone and install:
git clone https://github.com/cyanheads/eur-lex-mcp-server.git cd eur-lex-mcp-server && bun install cp .env.example .env # optional: every variable has a default -
Build and run:
# One-time build bun run rebuild # Run the built server bun run start:stdio # or bun run start:http # listens at http://localhost:3010/mcp -
Run 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 run --rm -p 3010:3010 ghcr.io/cyanheads/eur-lex-mcp-server:latest
The image defaults to HTTP transport, stateless session mode, and logs to /var/log/eur-lex-mcp-server. To run it over stdio from an MCP client, use "command": "docker" with "args": ["run", "-i", "--rm", "-e", "MCP_TRANSPORT_TYPE=stdio", "ghcr.io/cyanheads/eur-lex-mcp-server:latest"]. To build locally, docker build -t eur-lex-mcp-server . then docker run --rm -p 3010:3010 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 |
|---|---|
src/index.ts | createApp() entry point — registers tools, resources, prompts, and the published skills; initializes services. |
src/config | Server-specific environment variable parsing and validation with Zod. |
src/services/cellar-sparql | CELLAR SPARQL service — POST client, binding mapper, LIMIT enforcement, CDM PREFIX declarations. |
src/services/eurlex-content | CELLAR content service — content-negotiation GET client for /resource/celex/{CELEX} (Accept / Accept-Language) with English language fallback, and an in-process cache of served bodies so paging an act fetches it once. |
src/mcp-server/tools | Tool definitions (*.tool.ts). Seven tools across document search, retrieval, resolution, case law, relations, EuroVoc, and raw SPARQL. |
src/mcp-server/resources | Resource definitions (*.resource.ts). Metadata and relations resources. |
src/mcp-server/prompts | Prompt definitions (*.prompt.ts). Comparative analysis prompt. |
skills/ | Agent Skills published to the agents that call the tools (eur-lex-guide). |
tests/ | Unit and integration tests mirroring src/. |
Development guide
See CLAUDE.md for development guidelines and architectural rules. The short version:
- Handlers throw, framework catches — no
try/catchin tool logic - Use
ctx.logfor request-scoped logging,ctx.statefor tenant-scoped storage - Register new tools and resources via the barrels in
src/mcp-server/*/definitions/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
Apache-2.0 — see LICENSE for details.
Comments
New to MCP?
From what the protocol is to installing your first server, these answers cover most first questions.
An open protocol that defines how AI applications connect to external tools and data in a uniform way. Clients talk to the model; MCP servers provide tools, resources and prompt templates. They exchange standard messages, so one server can be reused by many clients.