vergabe-mcp
v1.0.1
Published
MCP server for oeffentlichevergabe.de — search and fetch German public-tender notices
Maintainers
Readme
vergabe-mcp
An MCP server for oeffentlichevergabe.de, the German federal public-tender portal. Germany publishes every public tender there and offers a bulk open-data export, but that export lags a day, ships as daily ZIPs, and cannot filter — you download everything and sort it out yourself. The portal's own search API can filter (CPV code, region, deadline window, the eForms "suitable for freelancers" flag) and is current the moment a notice goes live, but it is undocumented. This server wraps it, so any MCP client can search the live feed with real filters and pull a notice in the portal's domain model, in eForms XML, or in OCDS. Anonymous, no API key, no account.
The reverse-engineered API reference is in docs/API.md — it stands on its own if you want to call the endpoints directly instead of through MCP.
Install
Nothing to install; run it with npx.
Claude Code
claude mcp add vergabe -- npx vergabe-mcpClaude Desktop — in claude_desktop_config.json:
{
"mcpServers": {
"vergabe": {
"command": "npx",
"args": ["vergabe-mcp"]
}
}
}Any other stdio client
npx vergabe-mcpStreamable HTTP, if your client wants a URL rather than a subprocess:
npx vergabe-mcp --http --port 3000Tools
| Tool | What it does |
|---|---|
| search_tenders | Search lots with flat parameters — free text, CPV prefixes, suitableFor, contract nature, NUTS region, platform, deadline and publication windows, paging, sort. Returns normalized results. |
| get_tender | One notice by id, as a curated summary (default) or verbatim domain JSON, eforms XML, or ocds JSON. |
| search_tenders_raw | The native SELECT/FROM/WHERE/PAGE/ORDER DSL, passed through and returned raw — for anything the curated tool does not express. |
| lookup_code | Resolve a CPV or NUTS code to its German or English description. |
| get_export_url | Build the bulk open-data export URL for a day or month, with the caveats attached. |
All five are read-only; the server writes nothing, stores nothing on disk, and holds no credentials.
Resources. vergabe://api-reference serves the API document, so a client can read what the
backend actually supports before composing a query. Templates cover single notices —
vergabe://notice/{noticeId}, .../eforms, .../ocds — and vergabe://codelist/{list}/{code}.
Prompt. find-solo-tenders (args: keywords, cpv, maxDays) searches with the
freelancer/self-employed flags, then walks the model through triaging candidates: person-days
buried in the description, whether the near deadline is a request to participate or the offer
itself, and the fact that an empty value field means nothing. That triage knowledge ships as a
prompt rather than as scoring code, so you can argue with it.
Example
You: Find open IT tenders that are flagged as suitable for freelancers.
The model calls search_tenders with cpv: ["72"], suitableFor: ["freelance"],
onlyActive: true. On 2026-08-17 that is 30 open lots, among them:
{
"noticeId": "7c70692d-dfb9-4b96-b777-4a5539eaee50",
"title": "Beschaffung einer Beratungsdienstleistung zur Fachberatung im Rahmen eines Identity and Access Management (IAM) Projektes für die Stadtwerke Bielefeld (SWB)",
"lotTitle": "Beratungsdienstleistung zur Fachberatung im Rahmen eines Identity and Access Management (IAM) Projektes",
"buyer": "Stadtwerke Bielefeld GmbH",
"cpv": "72260000",
"noticeType": "cn-standard",
"nature": "services",
"deadline": "2026-09-14T10:00:00+02:00",
"published": "2026-08-12T00:00:00+02:00"
}It then calls get_tender on the interesting ones to read the description, the estimated value,
and the exclusion grounds before telling you which are worth a bid — that judgment is the model's
job, and the server deliberately does not score anything.
Note the shape of the data in that result: the deadline on this one is
deadlineReceiptRequests, a request to participate, not an offer deadline. The
find-solo-tenders prompt exists because distinctions like that decide whether a tender is
reachable.
Development
Node 20 or newer.
npm install
npm test # vitest, no network
npm run typecheck
npm run lint # biomeIntegration tests hit the live portal and are skipped unless you ask for them:
VERGABE_LIVE=1 npm testCommits follow Conventional Commits;
release-please turns them into a release PR on
main — merging that PR tags the release, writes the changelog, publishes to npm, and attaches
the tarball to the GitHub release. The commit type decides the version bump.
License
MIT.
