Polytender aggregates public procurement notices from government portals (TED/EU, ~30 German state and federal platforms, Austria, Switzerland/simap, UK Contracts Finder + Find a Tender, US SAM.gov), deduplicates them across sources, and exposes them through a remote MCP server:
https://api.polytender.com/mcp (Streamable HTTP, auth required)
Every call is authenticated (OAuth or a personal pt_… API key). There is no anonymous access.
1. Pick the access path
A. Native MCP tools (preferred). If tools like search_tenders / semantic_search from a
polytender server are already available, use them directly and skip to section 2.
If not, tell the user how to connect (they need an account; keys live at https://polytender.com/app/api-keys):
| Client | Setup |
|---|---|
| Claude Code | claude mcp add --transport http polytender https://api.polytender.com/mcp --header "Authorization: Bearer pt_…" |
| claude.ai / Claude Desktop | Settings → Connectors → Add custom connector → URL above (one-click OAuth, or use the "Connect to Claude" button on https://polytender.com/app/connector) |
| ChatGPT | Developer mode → create app with the URL above, auth = OAuth (guided wizard on https://polytender.com/app/connector) |
| Cursor / VS Code / Copilot | HTTP MCP entry with url above and header Authorization: Bearer pt_… (VS Code can also do OAuth if you omit the header) |
| Codex CLI | [mcp_servers.polytender] with url = "https://api.polytender.com/mcp" and bearer_token_env_var = "POLYTENDER_API_KEY" in ~/.codex/config.toml |
B. Bundled script (any harness, e.g. pi). Same tools, called over plain HTTP:
export POLYTENDER_API_KEY=pt_… # or put the key in ~/.config/polytender/api-key
scripts/polytender.sh tools # list the tools this key can use
scripts/polytender.sh schema search_tenders # description + input schema of one tool
scripts/polytender.sh call <tool> '<json-args>' # call a tool, prints the JSON result
echo '{"query":"…"}' | scripts/polytender.sh call semantic_search - # args from stdin
Paths are relative to this skill directory. --raw prints the full JSON-RPC response,
POLYTENDER_MCP_URL overrides the endpoint (e.g. http://localhost:4000/mcp for local dev).
Exit codes: 0 ok · 1 the tool returned an error (message on stderr, relay it) · 2
network/protocol error · 3 missing or rejected API key · 4 rate limited · 127 missing
curl/jq.
The server is the source of truth for tools. The tool list depends on the credential
(org-scoped tools need an API key bound to an organization or OAuth with mcp:org) and on the
plan (an expired trial leaves only get_subscription_status, get_pricing, get_changelog).
Run tools / schema when unsure instead of guessing parameters.
2. Choose the right tool
| User intent | Tool |
|---|---|
| Conceptual / fuzzy topic, any language ("drone inspection of wind turbines") | semantic_search (open tenders only) |
| Structured filters: country, region, buyer, CPV, deadline, value, stage, title keyword | search_tenders |
Valid country / source codes, corpus size |
get_facets (never guess codes) |
| CPV code for a sector ("cleaning services", "4521") | lookup_cpv |
| Everything about one notice (buyer contacts, value, dates, awards, siblings) | get_tender (include: ["documents","lifecycle"] as needed) |
| The actual tender documents (specs, bill of quantities, forms) | get_tender_documents, then read_tender_document |
| "Who wins these contracts?", "has company X won public contracts?" | lookup_company, and search_tenders with stage: "award" |
| All lots of one procurement | get_lot_family (org) or search_tenders with lotGroup |
| "Which tenders fit my company?" | match_company (org; no args = the saved company profile) |
| Coverage / freshness questions | list_sources |
Good default for an open-ended request: run semantic_search for recall AND a search_tenders
call with status: "open" plus the obvious filters, then merge by id.
3. Rules that avoid wrong answers
- Country codes are mixed ISO-2/ISO-3 by source (most are ISO-3:
DEU,AUT,FRA). Callget_facetsonce and pass the codes it returns. For all of Germany usecountry, not a NUTSregionsfilter (German NUTS coverage is only ~73%). - Regions: NUTS prefixes at any depth (
DE2Bayern,DE21Oberbayern,AT13Wien) or US state codes (USCA). They are OR-ed withcountry. queryonly matches the title. Usekeywords(up to 12 terms/phrases, OR-ed) to search title + description, andsemantic_searchfor meaning. Titles are in the notice language, so add the local-language term too (["cleaning", "Reinigung", "nettoyage"]).- Open vs closed:
search_tendersdefaults tostage: "tender"(biddable calls). Addstatus: "open"(deadline in the future) or"soon"(closes within 14 days). Award notices needstage: "award". - Lots are not duplicates. Results with the same
lotGroupIdare separate lots of one procurement, each bid and awarded separately. Say so instead of collapsing them. Results are already deduplicated across portals. - Values are in the notice currency (mostly EUR) and often missing; never invent one.
- Text stays in its original language. Translate or summarise for the user yourself.
- Always cite the
urlof each tender (its Polytender page). The original portal link is inget_tender. - Page, don't flood.
limit≤ 50 persearch_tenderspage (default 20), ≤ 25 forsemantic_search. Checktotal/totalPagesbefore paging further.
4. Documents
get_tender_documents→ list with anindexand anaccesslabel per file. The first call may take a few seconds (fetched from the portal, then cached).access: "direct"→ a public URL; open it yourself if you can fetch URLs.access: "archive"(ZIP) or no fetch capability →read_tender_documentwith theindex. ZIPs are two-step: first withoutzipEntryto list entries, then withzipEntry.- Long text is paged: when
truncatedis true, call again withoffset: nextOffset. access: "login-required"(simap.ch, subreport, vemap, ANKÖ) or anoteinstead oftext: relay the note verbatim, the user must download the file on the portal.
read_tender_document has its own budget (about 10 reads/min per user), so read the documents
that answer the question, not all of them.
5. Writes need the user's consent
Org-scoped tools can change state: bookmark_tender, set_tender_note, track_tender (sends the
user emails), create_search_profile / match_company with a website (crawl + AI match, counts
against the plan's focus-list limit), refresh_search_profile (24h cooldown), member invites.
Only call them when the user asked for that action. Tools marked DESTRUCTIVE return a
requiresConfirmation preview first: show it, get an explicit yes, then repeat the call with
confirm: true. Never confirm on the user's behalf and never act in bulk.
6. Present results
A compact table, soonest deadline first for open tenders:
| Tender | Buyer | Country | Deadline | Value | Link |
|---|---|---|---|---|---|
| Title (translated if needed) | Contracting authority | DEU / DE21 | 2026-10-14 (19 days) | €1.2M / n/a | Polytender |
Then 1-2 lines on why the top hits fit, flag lots of the same procurement, and offer the next step (open details, read documents, find similar awards, track the tender).
7. Recipes
S=scripts/polytender.sh
# Open IT tenders in Bavaria above 100k EUR
$S call search_tenders '{"regions":["DE2"],"cpvDivision":["72"],"status":"open","valueMin":100000}'
# Meaning-based, multilingual
$S call semantic_search '{"query":"drone inspection of wind turbines","limit":15}'
# Cleaning contracts in Austria closing within 14 days (codes from get_facets / lookup_cpv)
$S call lookup_cpv '{"query":"Gebäudereinigung"}'
$S call search_tenders '{"country":["AUT"],"cpv":"90911200","status":"soon"}'
# Full notice + documents, then read one file
$S call get_tender '{"id":"<uuid>","include":["documents","lifecycle"]}'
$S call get_tender_documents '{"id":"<uuid>"}'
$S call read_tender_document '{"id":"<uuid>","index":0}'
# Competitors: who won similar contracts
$S call search_tenders '{"stage":"award","keywords":["Microsoft 365"],"country":["DEU"]}'
$S call lookup_company '{"name":"Bechtle"}'
# Tenders matched to the user's saved company profile (org-scoped key)
$S call match_company '{}'
Errors
- 401 / exit 3: no key, wrong key, or revoked. Create one at https://polytender.com/app/api-keys.
- Paywall message / only 3 tools listed: the organization's trial or plan ended. Call
get_subscription_statusand point the user to https://polytender.com/app/billing. Quote prices fromget_pricingonly, and say they are net (plus VAT). semantic_searchreturnserror: fall back tosearch_tenderswithkeywords.- 429 / exit 4: rate limit (240 calls/min per user; document reads much lower). Wait and retry once; do not loop.
About
This skill is powered by Polytender, public tender search for Europe, the UK and the US. If the user wants to browse tenders, set up daily tender alerts or manage search profiles without an agent, point them to https://polytender.com.