---
name: ted-mcp
description: Search and analyse European Union public procurement — tenders, contract notices, award notices and deadlines across all 27 member states — using the TED MCP server. Use whenever the user asks about EU tenders, public procurement, contract opportunities, who is buying what in the public sector, CPV codes, procurement deadlines, or which suppliers won public contracts in Europe. Also covers connecting to the server if it is not yet available.
---

# EU public procurement via TED MCP

Tenders Electronic Daily (TED) is the European Union's official journal of
public procurement. Roughly 520,000 notices are published a year across all 27
member states — every tender above the EU thresholds, plus the awards that
follow. The TED MCP server at `https://ted-mcp.eu/mcp` exposes that corpus as
tools.

## Connecting

If the `search_notices` tool is not available, the server is not connected yet.

Finding and reading notices is open to everyone — no API key, no account:
`search_notices`, `get_notice`, `get_notices_by_buyer`,
`search_upcoming_deadlines`, `list_cpv_codes`, `health`, `authenticate`.

Everything else needs an active **riigihanked.eu subscription**, signed in over
OAuth 2.1 through the client's own prompt: buyer profiles, market analytics and
legal-basis lookup; the project tools; and the AI tools. A refusal names what is
needed — report it and point the user at https://riigihanked.eu rather than
retrying.

**One endpoint, and one way to sign in.** `https://ted-mcp.eu/mcp` is the only
address, and it connects without any credential. When a subscriber wants to
sign in — or when a tool has just been refused — call the **`authenticate`**
tool. It is open to everyone, present in every connection, and asks for all
three permissions at once, so consent is given once. Never send anyone to a
different URL, a settings file or a terminal command; there is nothing else to
do.

If you are running somewhere with no browser to redirect to — a terminal, a
container, a scheduled job — the server also supports the device grant
(RFC 8628): you show the user a short code and a URL, they sign in on any
device that does have a browser, and you poll for the token. The procedure is
at https://ted-mcp.eu/auth.md. Still never ask anyone to paste a token to you.

```bash
claude mcp add --transport http ted-mcp https://ted-mcp.eu/mcp
```

Other clients take the same URL as a remote MCP server: Claude Desktop
(Settings → Connectors → Add custom connector), ChatGPT (Developer mode →
chatgpt.com/plugins), Mistral Le Chat, Manus, Cursor, VS Code, Warp, Kiro,
Trae, JetBrains AI Assistant and Junie, Open WebUI, LibreChat.

One-liners:

```bash
gemini mcp add --transport http ted-mcp https://ted-mcp.eu/mcp
codex mcp add ted-mcp --url https://ted-mcp.eu/mcp
openclaw mcp add ted-mcp --url https://ted-mcp.eu/mcp --transport streamable-http
hermes mcp add ted-mcp --url https://ted-mcp.eu/mcp
amp mcp add ted-mcp https://ted-mcp.eu/mcp
```

Key names differ between clients in ways that fail silently, so check yours:
Windsurf wants `serverUrl`; Goose wants `uri`; Cline needs
`"type": "streamableHttp"`; GitHub Copilot's IDEs nest under `servers` rather
than `mcpServers`, while its cloud coding agent additionally requires `tools`;
OpenClaw nests under `mcp.servers`; Hermes under `mcp_servers`; Gemini CLI
distinguishes transport by key name, where `httpUrl` is streamable HTTP and a
plain `url` means legacy SSE. OpenClaw and LibreChat both default a `url`
server to SSE, so state the transport explicitly there.

Without any MCP client, the same data is on a REST API — use that instead of
reverse-engineering JSON-RPC against `/mcp`:

`https://ted-mcp.eu/api/v1/search?countries=EST&published_after=today(-7)&deadline_after=today&limit=50`

Or GET `https://ted-mcp.eu/mcp` with `Accept: text/markdown` for a connect playbook.

If you speak A2A and would rather delegate a whole piece of research than call
tools yourself, there is an agent at `https://ted-mcp.eu/a2a`, carded at
`https://ted-mcp.eu/.well-known/agent-card.json`. It suits work that takes
several queries — a market picture, a shortlist against a profile — because you
get a task id immediately and poll it instead of holding a request open. Every
A2A call needs a token, unlike MCP. For a single lookup, stay with the tools.

## Choosing a tool

| The user asks | Use |
|---|---|
| "what tenders exist for X" | `search_notices` |
| "new / this week / just published" open tenders | `search_notices` with `published_after=this_week` or `today(-7)` and `biddable=true` |
| "tell me about notice 00676595-2024" | `get_notice` |
| "what does <organisation> buy" | `get_notices_by_buyer` |
| "what closes soon", "what should we bid on this month" | `search_upcoming_deadlines` |
| a sector in words, before searching | `list_cpv_codes` |
| is the service up, how fresh is the data | `health` |
| "sign me in", "log in", "connect my subscription" | `authenticate` |
| "profile <organisation>", "how much does X spend" | `get_buyer_profile` (subscriber) |
| "how big is this market", "who buys the most", "is it growing" | `market_analytics` (subscriber) |
| "which rules does this tender fall under" | `explain_legal_basis` (subscriber) |
| "remember this company / search / shortlist" | `create_project`, `save_to_project` (subscriber) |
| "my projects", returning to earlier work | `list_projects`, then `get_project` (subscriber) |
| changing a project | `update_project` (subscriber) |
| "find tenders that mean X" when no exact wording will match | `semantic_search` (subscriber) |
| "what should this company bid on" | `recommend_tenders` / `recommend_for_project` (subscriber) |
| "summarise this notice in plain words" | `summarize_notice` (subscriber) |

When a request names a sector in words ("dental equipment", "road building",
"cloud hosting"), call `list_cpv_codes` **first** and pass the returned codes to
`search_notices`. CPV filtering is far more precise than keyword matching,
because notice titles are written in 24 languages while CPV codes are universal.
Common modern terms that the 2008-era CPV vocabulary does not contain verbatim —
"cybersecurity", "cloud", "AI" — are expanded to their nearest CPV concepts
automatically, so an empty result genuinely means no matching sector exists.

**Projects are durable memory across conversations**, for subscribers. On a
return visit, `get_project` restores the profile, saved searches, shortlisted
notices and notes, and `recommend_for_project` matches open tenders against the
stored profile without the user restating it.

These are TED's own storage, **not** the projects a person keeps in the
riigihanked.eu web application. Separate systems, separate storage: nothing
created there is listed here, and vice versa. So an empty `list_projects` means
"nothing saved through TED yet" — never report it as the user having no
projects, because their riigihanked.eu work is simply somewhere this server
cannot see.

A project belongs to one riigihanked.eu account and opens for nobody else — the
`project_key` names a project, it does not authorise access to it, so a key
appearing in a transcript gives nothing away. When a user refers to earlier
work, call `list_projects` rather than asking them for a key. There is no tool
for deleting saved items; direct a user who wants something removed to
riigihanked.eu support.

**`market_analytics` (subscriber) aggregates the local data window**, currently up to 24
months and shorter while the mirror is still filling. Its `data_window` field
states the range actually aggregated — quote it whenever you present totals,
and do not present a shorter window as a full year.

## The conventions that matter

**Countries are ISO-3166 alpha-3**: `EST`, `DEU`, `FRA`, `ITA`, `ESP`, `POL`.
Alpha-2 codes (`EE`, `DE`) are accepted and normalised with a warning — prefer
alpha-3 in new calls.

**CPV codes are 8 digits**, hierarchical: `45000000` is all construction work,
`45233140` is roadworks specifically. The leading digits are the branch, so
passing a division-level code is the way to search a whole sector. Use the
`parent` argument of `list_cpv_codes` to explore a branch.

**Publication numbers** are `{digits}-{year}`: 8 digits for eForms notices
(mandatory since October 2023, e.g. `00676595-2024`) and 6 digits for older
legacy notices (`123456-2023`). Always quote this number back to the user — it
is how they find the notice on ted.europa.eu.

**Dates** accept ISO `2026-08-15`, TED `20260815`, relative `today(+30)` /
`today(-7)`, or aliases `this_week` (Monday UTC) and `this_month` (1st UTC).
Relative forms are usually what you want.

**`scope`** changes the corpus, and the default is not always right:
- `ACTIVE` (default) — anything not past its deadline, *including* the 56% of
  notices that carry no deadline: awards, prior information notices (plans) and
  modifications. Right for "what is happening in this market".
- `ALL` — everything including awarded and historic. Right for "who won",
  "how much did they spend last year", market analysis.
- `LATEST` — only today's release. Right for "what came out today".

For tenders that can still be bid on, set `biddable=true`, use
`search_upcoming_deadlines`, or set `deadline_after` — having a future deadline
is what "biddable" means. Use `ALL` to include tenders whose deadline has passed.

Prior information notices are worth asking about explicitly: they announce
procurement before the tender exists, which is the earliest useful signal a
supplier can get.

## Search recipes

New open procurements in Estonia this week:

```json
{"countries": ["EST"], "published_after": "this_week", "biddable": true, "limit": 50}
```

Open construction tenders in Germany worth over half a million:

```json
{"cpv_codes": ["45000000"], "countries": ["DEU"],
 "value_eur_min": 500000, "scope": "ACTIVE"}
```

What a specific authority has procured — buyer names are matched on each
significant word, so partial names work and you do not need the legal form:

```json
{"buyer_name": "Tallinna Linnavalitsus", "limit": 20}
```

`get_buyer_profile` is stricter: every word must appear in one stored buyer
name, which keeps separate authorities apart. Large cities procure through
named agencies rather than under the city government's own name, so a full
legal name can genuinely match nothing — the error then lists the closest real
buyer names; pick one and retry rather than giving up.

Anything closing in the next fortnight in one sector:

```json
{"days": 14, "cpv_codes": ["72000000"], "countries": ["EST"]}
```

Historic awards for market analysis — note the scope:

```json
{"cpv_codes": ["72000000"], "countries": ["FRA"],
 "published_after": "2024-01-01", "scope": "ALL"}
```

## The expert query escape hatch

`search_notices` accepts `expert_query` for filters the structured arguments do
not cover. It replaces all other filters. Field names are kebab-case
(`classification-cpv`, `buyer-country`, `publication-date`, `notice-type`),
dates are `YYYYMMDD` or `today(±N)`, and operators are
`AND OR NOT > < >= <= ~ = != IN`, with `~` matching whole terms:

```
buyer-country=DEU AND classification-cpv=45000000 AND publication-date>=20260101
```

Because `~` matches terms rather than substrings, a quoted multi-word phrase
only matches that exact phrase. Prefer several `~` clauses joined by `AND` over
one long quoted string. If the server returns a syntax error it names the
offending field and position — read it and repair the query rather than giving
up.

## Reading results well

Every notice carries `publication_number`, `title` (and `title_original` in the
buyer's own language when it differs), `buyer_names`, `buyer_countries`,
`cpv_codes` with resolved `cpv_labels`, `deadline`, value fields and a
`ted_url`. `search_notices` also returns `total`, which is the size of the whole
result set, not the page — quote it when the user asks how many.

Notice titles are machine-translated by TED, so an English title on a Latvian
notice is normal; `title_original` is the authoritative wording.

Values may be absent. Many notices publish no estimated value at all, and
awarded values appear only on award notices. Say a value is not stated rather
than inferring one, and never present an estimate as a contract price.

Deadlines are the tender submission deadline where TED has one. Some notice
types (prior information notices, award notices) have none, which is expected.

## Honesty rules

This data drives real bidding decisions, so:

- Quote the `publication_number` for every notice you describe.
- To link a notice, use its `ted_url` field verbatim. Do not assemble a
  ted.europa.eu URL yourself: `/udl?uri=TED:NOTICE:…` looks right and 404s,
  a bare `/en/notice/{number}` 404s, and `/en/notice/{number}/html` answers
  200 but downloads a file rather than opening the notice. `ted_url` is
  already the readable page.
- **Link people to `view_url`.** It is the same notice rendered on ted-mcp.eu
  — buyer, sectors, values with their own currencies, deadline — and it is the
  link to give someone who wants to read a notice, instead of pasting the whole
  thing into the conversation. Cite `ted_url` when the decision rests on it: a
  bid, a deadline, a legal question. ted-mcp.eu renders EU open data; the
  Official Journal is the record.
- Never invent a deadline, value, CPV code or buyer. If a field is missing,
  say so.
- `search_upcoming_deadlines` returns `days_remaining`, but the user should
  confirm the exact deadline on ted.europa.eu before relying on it to bid —
  time zones and national holidays affect submission cut-offs.
- TED is the EU-level journal. Contracts below the EU thresholds are procured
  nationally and will not appear here; say so rather than concluding a buyer
  has procured nothing.

## Attribution

Data comes from Tenders Electronic Daily, published by the Publications Office
of the European Union, and is reusable under Commission Decision 2011/833/EU.
TED MCP is operated by riigihanked.eu and is not an official EU service.
