swiss-procurement-mcp
Search and analyze Swiss public procurement data from simap.ch (all cantons + Confederation) via MCP tools.
Search public tenders by free text, canton, CPV codes, process type, publication type, and date range
Run detailed searches that return full records for top matches in one call
Find awarded contracts across all four award types
Fetch full procurement details: BKP/NPK/CPV codes, deadlines, procurement office, order description
Trace a project's publication history (tender → award → corrections)
Resolve keywords to CPV codes or Swiss construction codes (BKP, NPK, eBKP, OAG, CPC)
Find public procurement offices by partial name
Check simap.ch API reachability and latency
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., "@swiss-procurement-mcpShow me recent building construction tenders in Zurich"
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.
Part of the Swiss Public Data MCP Portfolio — open-source MCP servers connecting AI agents to Swiss public and open data.
This is a private project. It is independent of any employer or institutional affiliation and represents no official position of any authority.
swiss-procurement-mcp
MCP server for Swiss public procurement — read access to the official simap.ch API, covering all cantons and the Confederation, updated intraday.
🎯 Anchor demo query
«Which school-building tenders did the City of Zurich publish in 2026, which BKP construction categories do they concern, and who are the procuring offices?»
A single search_procurements_detailed(query="Schulhaus", canton="ZH", published_from="2026-01-01")
returns the leading tenders already expanded with their BKP construction codes and
procuring offices — connecting procurement to school-building planning in one call
(optionally paired with search_construction_codes to resolve a category).
Demo
Related MCP server: simap
Why this server exists
Swiss public procurement is published on simap.ch. The platform's web UI is
searchable by hand, but the amtsblatt-mcp
server only reaches the three cantons (AR, BS, TI) that still mirror tenders to
the Amtsblattportal — Zurich among the missing.
simap closes that gap: it operates a documented OpenAPI 3 read API (v1.5.1)
whose search and detail endpoints are marked security: None and are callable
without authentication. This server wraps exactly those read endpoints.
Mnemonic: The web UI is the front door; the API is the loading dock. Probe the dock.
Architecture decision
Architecture A (live API only, short-lived cache).
The public search, detail and reference endpoints are unauthenticated and were confirmed working live (2026-07-26).
Publications change intraday, so the cache TTL is deliberately short (30 min).
The ~200 write /
my// OIDC-protected endpoints (publishing tenders, submitting offers) are out of scope — this server never writes.
Every response carries source and provenance (live_api / cached /
degraded). Upstream failure yields a degraded envelope, never a silent empty
list.
Live-probe findings (2026-07-26)
Endpoint | Auth | Result |
| none | 20 hits, canton filter, current-day |
| none | full record: criteria, deadlines, codes |
| none | project lifecycle |
| none | CPV full-text search |
| none | Swiss construction codes |
| none | ~1 MB office list (client-side filter) |
| none | reference data |
Known findings
Wrong host, wrong conclusion. The read API lives under
www.simap.ch/api. Thesimap.ch/deweb UI is a separate SSR app that exposes none of it — probing the UI produced an earlier, mistaken "no API" verdict.langis mandatory on project-search. Omitting it is HTTP 400 (errorCodeE0025), not an empty result. The client injects a default.Award is not "award".
newestPubTypes=awardreturns HTTP 400. Awards are split by procedure:award_tender,award_study_contract,award_competition,direct_award. Thesearch_awardstool queries all four.Canton ids are bare.
ZH, notCH-ZH. Passing an ISO subdivision code silently matches nothing; this server rejects it with a clear error.A session cookie is required. The first request sets it; a persistent HTTP client handles this transparently.
Tools
Tool | Purpose |
| Search projects by canton, CPV, process type, date, text |
| Search + full detail for the top n hits in one call (aggregated) |
| Awarded contracts only (all four award types at once) |
| Full record for one publication |
| Earlier publications of the same project (tender → award) |
| Resolve keywords to CPV classification codes |
| Swiss construction codes (BKP, NPK, eBKP, OAG, CPC) |
| Public procurement offices by partial name |
| Reachability and latency of the simap.ch API |
All tools carry readOnlyHint, idempotentHint and openWorldHint (they query
the live simap.ch API).
Every tool takes a single validated argument object. Bounds, allow-lists and
patterns are declared on the input models in
inputs.py — so an out-of-range limit or
an unknown canton is rejected before any upstream request, and the constraints
are visible to the model in the tool schema rather than buried in the tool body:
search_procurements({"canton": "ZH", "query": "Schulhaus", "limit": 20})The models set strict=True (no silent "10" → 10 coercion) and
extra="forbid" (unknown fields are rejected, not ignored). The canton, process
type, publication type, code system and language allow-lists are derived from
constants.py, so they cannot drift from the probe-verified tables.
What canton= means
simap offers exactly one geographic filter, orderAddressCantons, and it selects
by where the work is delivered — not by who is procuring. When a procuring
office files a free-text address, the structured canton is null and the
publication is invisible to that filter. Measured CH-wide over 500 projects
published since 2026-07-01: 303 (60.6%) carry no canton, among them the Amt
für Hochbauten Zürich, Grün Stadt Zürich, USZ, BBL and SBB.
canton_match therefore makes the question explicit:
Value | Matches | Zurich, 2026-07-01…27 |
| procured by that canton's public bodies, incl. communal and subordinate offices ( | 410 projects |
| the work is delivered there ( | 263 projects |
| union of the two; two upstream calls, no pagination | 441 projects |
The 31 projects only place_of_delivery finds are federal bodies procuring in
Zurich (ETH, Empa, Flughafen Zürich AG) — a different question, not a gap, which
is why this is three explicit semantics rather than a silent union.
Every response states in note which semantics were applied.
Portfolio connections
A vendor's UID links to
register-mcp.BKP / eBKP construction codes on a tender connect procurement to school-building planning and to
zh-education-mcp.Complements
amtsblatt-mcpwith national coverage instead of three cantons.
Installation
uvx swiss-procurement-mcpClaude Desktop
{
"mcpServers": {
"swiss-procurement": {
"command": "uvx",
"args": ["swiss-procurement-mcp"]
}
}
}Cloud (Render / Railway)
MCP_TRANSPORT=sse HOST=0.0.0.0 PORT=8000 python -m swiss_procurement_mcpContainer
docker compose up --build # SSE on :8000The image is multi-stage and runs as a non-root system user. compose.yaml
adds a read-only root filesystem, drops all capabilities, sets
no-new-privileges, and caps memory, CPU and PIDs. No secret is needed at
runtime — the wrapped simap.ch endpoints are public.
CI builds the image on every push and asserts both properties that matter:
that the container does not run as uid 0, and that the server still imports
under --read-only --cap-drop ALL.
Configuration
Variable | Default | Purpose |
|
|
|
|
| HTTP binding (cloud transports only). Defaults to loopback; set |
| (unset) | Comma-separated origins allowed to call the HTTP transports from a browser. Unset means no cross-origin browser access at all — stdio and non-browser clients are unaffected. |
| (off) | Set to |
|
| HTTP port (cloud transports only) |
|
|
|
No API keys — the wrapped simap.ch read endpoints are fully public.
Built on structlog. Every event emitted during a
tool call carries that call's correlation_id, bound via contextvars — so a
failure logged deep inside the HTTP client can be joined to the request that
caused it without threading context through every function.
Level | Emitted when |
| a tool call was entered ( |
| a tool call finished cleanly, with latency |
| simap.ch was unreachable or errored ( |
| a tool call raised |
Records carry the exception type only — never its message and never an upstream response body (OBS-002).
{"event":"tool_call_started","tool":"search_procurements","correlation_id":"23221af26ae640c7","level":"debug","timestamp":"2026-07-27T22:20:07.494276Z"}
{"status":"ok","latency_ms":312,"event":"tool_call","tool":"search_procurements","correlation_id":"23221af26ae640c7","level":"info","timestamp":"2026-07-27T22:20:07.806Z"}MCP Protocol Version
Served via the |
|
Served via the per-request envelope |
|
Who picks | The client's first request, once per connection. A request carrying the |
Pinned in |
|
SDK |
|
Cache hints |
|
Server identity |
|
Verified by |
|
The MCP Python SDK negotiates the protocol version in the session layer and offers no constructor parameter for it, so the version cannot be pinned by configuration. It is pinned as a declared constant and enforced by detection:
At runtime, a mismatch between the constant and the SDK logs a
protocol_version_driftevent atWARNING. The server keeps working.In CI,
tests/test_protocol_version.pyfails.
That split is deliberate. An SDK bump should break our build, not the runtime
of someone who upgraded mcp in their own environment.
What the two eras actually do differently
The pin used to be checked against SDK constants and one initialize round
trip. That round trip can only ever reach the handshake era, which caps at
2025-11-25 — so nothing in this repository had ever sent a 2026-07-28
request. A server can pin a revision it answers with 400 and stay green.
Measured on 2026-09-18 against mcp 2.2.0, through both transports:
There is no handshake.
server/discoverreplaces it, and it is the only place a native client can ask who this server is. Every request carries its own_metaenvelope (protocol version, client info, client capabilities); without it the server answers-32602.Requests route by header.
Mcp-MethodandMcp-Namemust agree with the body, or the request is refused with-32020before the tool runs — so a proxy that reads only headers cannot be pointed at a different tool than the body names. (Mcp-Param-*is unused here; see_cors.py.)initializegets three different answers, and the transport is not what separates them. The first write-up here called the HTTP and stdio cases "two sides" of one situation; they differ in two variables at once, transport and history, so neither explains the other. The three measured cells:Situation
Answer
enveloped
initialize, alone, over HTTP-32601METHOD_NOT_FOUND, HTTP 404initializeafter a modern request, over stdio-32022UNSUPPORTED_PROTOCOL_VERSIONenveloped
initializefirst, over stdioaccepted — a
2025-11-25handshakeThe fourth cell does not exist: over stdio an
initializewithout history opens a legacy connection instead of being refused, so the HTTP situation cannot be reproduced there. Note the third row — stamping the envelope oninitializesilently gets you a connection of an older era, with nothing saying the envelope was ignored.Capabilities read differently per era, and this is the SDK's doing, not a setting here. At
2026-07-28the SDK derivestools.listChanged,prompts.listChanged,resources.listChangedandresources.subscribefrom one fact — whethersubscriptions/listenis served — andMCPServerregisters that unconditionally. Soserver/discoverreports all four astruewhereinitializereports all four asfalse, for the same server.
Update policy
Dependabot opens SDK update PRs monthly (
.github/dependabot.yml).When an SDK update moves the protocol version, the CI test fails. The fix is not to edit the constant blindly: read the spec changelog for what changed between the two versions, verify the server still behaves, then bump the constant, this section and
CHANGELOG.mdin one commit.Protocol-version bumps are called out explicitly in
CHANGELOG.md, not folded into a dependency-bump line.
Primitives: tools only
This server exposes tools and neither resources nor prompts. That is a decision, not an omission, so here is the reasoning (ARCH-008).
Why not resources. Resources address identifiable, listable content —
GET-like reads the client can enumerate and cache. simap's endpoints are the
opposite: every useful call is a query with filters over a corpus of ~200k
publications that changes intraday. A resource URI would either enumerate
something unbounded or encode a full query in the URI, which is a tool with
extra steps.
Two tools were checked concretely for migration potential and rejected for specific reasons, not by blanket policy:
Candidate | Why it stays a tool |
| Genuinely resource-shaped — one fixed, cacheable document. But it exists to be called when a result looks wrong, and a resource the model has to remember to re-read is worse at that job than a tool it can invoke on suspicion. |
| The CPV catalogue is finite and stable enough to enumerate. But it is ~10k entries; exposing it as a resource would push the whole classification into the context window, when the point of the tool is that the server does the lookup. |
Why not prompts. A curated prompt list would encode question templates ("which tenders in canton X…"). The tool docstrings already carry that guidance where the model actually reads it, and prompts would duplicate it in a second place that can drift — this repo has already been bitten twice by exactly that class of duplication.
This will be revisited if the server ever gains a genuinely enumerable, slow-changing dataset.
Testing
PYTHONPATH=src pytest tests/ -m "not live" # offline, respx-mocked
PYTHONPATH=src pytest tests/ -m live # hits the real APISee EXAMPLES.md for use cases grouped by audience (schools, public, administration, developers) and a tool-selection reference table.
Known limitations
Projects, not publications.
project-searchindexes projects and represents each by its newest publication. A project tendered in March and awarded in July appears once, as the July award;search_awardslikewise only finds projects whose newest publication is an award, so a later correction hides it.get_publication_historyreaches the earlier publications.Lot-based procurements are traced per lot. Upstream keeps the publication history per lot, so
get_publication_historyneeds alot_idwhenever the search result showslots_type: "with"— take one from that result'slotslist. Without it the source answers HTTP 400 and the tool reports a degraded response naming the missing parameter. Measured 2026-08-29 over 80 publications: all 4 with lots behaved this way, all 76 without lots answered directly.At least one filter is required. simap answers a filterless query with nothing rather than everything, so the tools refuse it with that reason instead of reporting an empty result.
Read-only by design. Publishing and submission endpoints exist in the simap API but are deliberately not wrapped.
Award coverage is uneven across cantons; some publish awards diligently, others rarely. Absence of an award is not proof none happened.
No contract values in search results. Amounts, where published, live in the detail record's statistics section, which varies by procedure.
Unofficial client. Publications remain authoritative on simap.ch itself.
Project structure
swiss-procurement-mcp/
├── src/swiss_procurement_mcp/
│ ├── server.py # MCPServer tools (9, read-only)
│ ├── client.py # simap.ch HTTP client + retry + normalisation
│ ├── constants.py # probe-derived lookup tables (cantons, pub types, codes)
│ ├── models.py # Pydantic v2 envelopes (source + provenance)
│ ├── inputs.py # strict Pydantic tool-input models (bounds, allow-lists)
│ ├── _fuzzy.py # term widening for the taxonomy lookups (ARCH-003)
│ ├── _log.py # structured JSON logging to stderr + @logged_tool
│ ├── _net.py # DNS-pinned transport (egress allow-list)
│ ├── _cors.py # CORS layer for the HTTP transports
│ └── __main__.py # Dual-transport entry point (stdio / SSE / streamable-http)
├── tests/ # respx-mocked + @pytest.mark.live
└── .github/workflows/ # CI + OIDC PyPI/MCP-registry publishWhy there is no tools/ package
The portfolio structure standard asks for a tools/ package once a server
exposes more than five tools. This one exposes nine and keeps them in
server.py, which is a deliberate deviation rather than an oversight — recorded
here because this is where the standard, and anyone comparing against it, looks.
server.py is ~900 lines and the surrounding modules above are already split out
by concern, so the intent of the standard — a codebase navigable without
scrolling one omnibus file — is met. What the split would add is the literal file
layout.
The companion server amtsblatt-mcp is the case where it was worth doing: its
server.py had grown to 2477 lines holding HTTP plumbing, XML parsing, a
taxonomy cache, the input models and every handler, and it was split in that
project's 0.21.0. That refactor is also the reason for caution here — it
introduced a defect (an extracted module captured a cache global by value, so a
tool silently reported stale state) that the entire test suite passed
through, because no test covered the affected path. It was caught by reading
the diff.
Moving nine handlers for the literal form of a standard whose intent is already
satisfied would take that risk for no navigational gain. This should be revisited
if server.py passes roughly 1500 lines.
Maturity & updates
Phase 1 — read-only (see ROADMAP.md for the phase-specific backlog and what a phase transition would require). This server wraps only the public read endpoints; the write / OIDC-protected simap endpoints are deliberately out of scope. See the SECURITY.md re-evaluation triggers for the conditions that would move it to a write phase.
The server targets the MCP spec version pinned as MCP_PROTOCOL_VERSION — see
MCP Protocol Version above for the current value and
how the pin is enforced. SDK and dependency updates arrive as
Dependabot PRs, so a breaking protocol or SDK change
is reviewed deliberately rather than drifting in silently.
Contributing
Contributions are welcome — see CONTRIBUTING.md for how to report bugs, suggest a new endpoint, or submit code.
Security
This is a read-only, no-PII, public-open-data server. Audited against the
portfolio MCP best-practice catalogue (15 pass / 16 partial / 1 fail across
32 applicable checks, production-ready). See SECURITY.md for the
posture and how to report a vulnerability, and audits/ for the full
report.
License
MIT License — see LICENSE. The tenders are official public-procurement announcements; simap.ch publishes no explicit open-data licence, so reuse follows the simap.ch terms (see Credits).
Author
Hayal Oezkan · github.com/malkreide
Changelog
See CHANGELOG.md.
Credits
Data: simap.ch read API v1.5.1, operated by the simap.ch association. API docs: simap.ch/api-doc — machine-readable OpenAPI spec at
/api/specifications/simap.yaml, which a live test checks the enum constants against. Guides: kissimap.ch.The underlying tenders are official public-procurement announcements by Swiss public bodies. simap.ch publishes no explicit open-data licence; reuse is subject to the simap.ch terms. Attribute the source as simap.ch (Verein simap.ch).
Built following the
mcp-data-source-probemethodology.
The code in this repository is MIT licensed; the data is simap.ch's, under its terms (see above). Public money, public code.
Available Tools
9 toolsfind_procurement_officeARead-onlyIdempotent
Resolve a partial organisation name to the procuring offices simap knows, when the user names an authority rather than a project.
Find public procurement offices by (partial) name.
The public office list is large (~1 MB), so this fetches it once and filters client-side. Returns the office id, type (cantonal / federal / communal) and the linked institution id.
| Name | Required | Description | Default |
|---|---|---|---|
| args | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| count | Yes | |
| source | Yes | |
| offices | Yes | |
| match_type | No | exact, fuzzy (broader term, see note), or none. |
| provenance | Yes | |
| retrieved_at | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and destructiveHint=false, so the safety profile is known. The description adds useful behavioral context: the office list is large (~1 MB), fetch-once and client-side filtering, and the returned fields (id, type, linked institution id). This goes beyond annotations without contradicting them.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise and front-loaded with a use_case tag, then a clear function statement and two additional useful details (performance, return fields). Every sentence earns its place. Minor redundancy between the use_case and first sentence, but it is efficient overall.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a simple filtered-list tool with strong annotations and a detailed input schema, the description is complete. It covers the use case, behavior, performance note, and key return fields. The output schema exists, so return-value detail is optional; the description provides enough context for correct selection and invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema already provides descriptions for all parameters (name_contains, limit, language), so the baseline is 3. The description's mention of '(partial) name' aligns with name_contains but does not add semantics beyond the schema. No ambiguity or gap that requires compensation.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a clear, specific purpose: find public procurement offices by partial name. It explicitly distinguishes this tool from sibling tools that search procurements/awards by noting it resolves an authority name rather than a project. The use_case tag adds valuable context.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description includes a clear when-to-use clause ('when the user names an authority rather than a project'), which guides selection. It does not explicitly name alternative tools, but the context strongly implies the distinction. This is clear context without formal exclusion.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_procurement_detailsARead-onlyIdempotent
Retrieve the full record for one publication once you have its ids: criteria, deadlines, classification codes and the procuring body.
Return the full record for one procurement publication.
Both ids come from a search_procurements result. The record includes the
order description, CPV and Swiss construction codes (BKP, NPK), deadlines and
the procurement office — the BKP codes make this joinable with construction
cost data and school-building planning.
| Name | Required | Description | Default |
|---|---|---|---|
| args | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| title | Yes | |
| source | Yes | |
| cpv_code | No | Main CPV classification code. |
| bkp_codes | No | Swiss BKP construction codes. |
| npk_codes | No | |
| order_type | No | |
| project_id | Yes | |
| provenance | Yes | |
| process_type | No | |
| retrieved_at | Yes | |
| has_documents | No | |
| offer_deadline | No | |
| publication_id | Yes | |
| publication_date | No | |
| order_description | No | |
| procurement_office | No | |
| additional_cpv_codes | No | |
| procurement_office_address | No |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare the tool as read-only, idempotent, and non-destructive. The description adds valuable context about what the record contains (order description, CPV, BKP/NPK codes, deadlines, procurement office) and highlights the joinability of BKP codes with construction cost data. This goes beyond the annotations without contradicting them.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is front-loaded with a use case but contains redundancy: 'Retrieve the full record' and 'Return the full record' say the same thing. The second paragraph adds useful detail, but the opening could be tightened without losing meaning.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The description explains the source of the IDs, the specific fields included in the record, and even a downstream use (joining with construction cost data). With an output schema present, return-value documentation isn't needed. This is comprehensive for a single-record retrieval tool, though it could mention error behavior.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema provides comprehensive descriptions for both project_id and publication_id (e.g., 'Project id from a search_procurements result'), giving near-total schema coverage. The description's mention that both IDs come from a search result largely repeats the schema, adding no new parameter-level syntax or constraints.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's function: 'Return the full record for one procurement publication' and specifies the required inputs ('once you have its ids'). It distinguishes itself from sibling search tools by focusing on a single record's full details, including specific content like CPV codes and the procuring body.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description explicitly notes that 'Both ids come from a search_procurements result', establishing a clear prerequisite and usage context. It doesn't enumerate alternatives or exclusions, but given the sibling list, the intended workflow (search first, then retrieve full record) is clear enough.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
get_publication_historyARead-onlyIdempotent
Trace one project through time — tender to award to correction. Use when the question is "what happened to this procurement?".
Return earlier publications of the same procurement project.
Traces a project's lifecycle: tender → correction → award. An empty list is normal for a first publication.
When the publication has lots (lots_type is "with" in the search result),
pass lot_id from its lots list: upstream keeps the history per lot and
refuses the request without one.
| Name | Required | Description | Default |
|---|---|---|---|
| args | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| count | Yes | |
| source | Yes | |
| project_id | No | |
| provenance | Yes | |
| publications | Yes | |
| retrieved_at | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnly, idempotent, openWorld and non-destructive, so safety is covered. The description adds genuinely useful behavior beyond that: an empty list is a normal result for a first publication, and the upstream service rejects requests for lot-based publications without lot_id.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with the use case, then the core function, then the edge cases. Every sentence carries information; the XML-style <use_case> wrapper is slightly ornamental but costs almost nothing.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
An output schema exists, so return structure needn't be explained, and the description still pre-empts the two things an agent could get wrong: interpreting an empty list and omitting lot_id on lot publications. Nothing further is needed to call this correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The included-schema descriptions already document publication_id and language; the prose adds real semantics for lot_id by explaining the failure mode (upstream keeps history per lot and refuses without one) and where to source the value from the search result. Minor gap: language selection is not addressed in the description.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource — 'Return earlier publications of the same procurement project' — and frames it as a lifecycle trace (tender → correction → award). This clearly distinguishes it from the sibling search_* tools, which find procurements rather than trace one project's history.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It gives an explicit trigger ("Use when the question is 'what happened to this procurement?'") and a conditional rule for when lot_id is required. It does not name an alternative sibling tool or state when not to use it, which keeps it short of a 5.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_awardsARead-onlyIdempotent
Find who won, not what is open — all four award types at once. Use when the question is about completed procurement rather than current opportunities.
Search only awarded contracts (who won).
Convenience wrapper over search_procurements that queries all four award
publication types at once.
Two coverage caveats. First, award coverage is uneven across cantons — some
publish awards diligently, others rarely, so absence is not proof that no
award happened. Second, the filter matches a project's NEWEST publication:
a project awarded in May and corrected in June is no longer an "award" to
this filter and drops out. Use get_publication_history on a project to see
whether an award exists further back.
canton_match works exactly as in search_procurements and defaults to
matching the procuring body.
| Name | Required | Description | Default |
|---|---|---|---|
| args | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| count | Yes | |
| source | Yes | |
| results | Yes | |
| has_more | Yes | True if the pagination cursor can be advanced. |
| match_type | No | exact when results were returned, none when empty. |
| provenance | Yes | |
| next_cursor | No | Pass as `cursor` to search_procurements for the next page. |
| retrieved_at | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already cover read-only, idempotent, open-world, and non-destructive behavior, but the description adds important caveats: uneven canton award coverage, absence not proof, and the newest-publication filter that can drop corrected awards. This goes well beyond what annotations provide.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Front-loaded with a clear use-case summary, then caveats. There is some redundancy between the `<use_case>` tag and the following sentence, and the wrapper sentence repeats 'all four award types', but overall the structure is efficient.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a filtered search wrapper with coverage caveats and an existing output schema, the description supplies purpose, routing, and limitations. Nothing essential is missing for correct invocation.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The nested schema already documents canton, cursor, language, canton_match, and date fields thoroughly. The description only cross-references `canton_match` semantics and its default, adding little beyond what the schema provides, so baseline 3 is appropriate.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
States a specific verb ('Search') and resource ('awarded contracts'), plus scope ('all four award types at once'). Distinguishes from siblings by contrasting completed procurement with current opportunities and naming `search_procurements` as the wrapper base.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Explicitly says to use it when the question is about completed procurement rather than current opportunities, effectively stating when-not. It also names `get_publication_history` as an alternative for checking older awards, and the caveats guide appropriate use.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_construction_codesARead-onlyIdempotent
Translate a keyword into Swiss construction cost codes (BKP, NPK, eBKP, OAG, CPC) — the bridge between a building topic and procurement filters.
Search Swiss construction classification codes by keyword.
Args: system: One of bkp, npk, ebkp-h, ebkp-t, oag, cpc. query: Keyword.
These are the Swiss construction cost standards (Baukostenplan, Normpositionen- katalog) used in building tenders — relevant for school-building procurement.
| Name | Required | Description | Default |
|---|---|---|---|
| args | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| codes | Yes | |
| count | Yes | |
| source | Yes | |
| system | Yes | cpv, bkp, npk, cpc, ebkp-h, ebkp-t or oag. |
| match_type | No | exact, fuzzy (broader term, see note), or none. |
| provenance | Yes | |
| retrieved_at | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, idempotentHint=true, and destructiveHint=false, so the safety profile is clear. The description adds the use-case context but does not elaborate on behavioral details like return format, pagination, or error handling. No contradiction with annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is structured and mostly concise, with a use_case tag, a one-sentence summary, an Args list, and a brief context paragraph. Each section adds relevant context, though the final paragraph could be tightened without losing value.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool has an output schema and annotations that cover safety, so the description adequately explains the purpose and domain. It does not discuss optional parameters like limit and language, but the schema fills that gap, making it complete enough for an agent to select and invoke the tool correctly.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The input schema provides descriptions for all four parameters (system, query, limit, language), including enum values. The tool description redundantly lists system enum values and describes query as 'Keyword,' but omits limit and language, adding minimal semantic value beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool translates a keyword into Swiss construction cost codes (BKP, NPK, eBKP, OAG, CPC). It uses specific verbs like 'search' and 'translate' with a well-defined resource, distinguishing it from sibling tools like search_cpv_codes by the Swiss classification context.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It provides strong context that this is for Swiss construction cost standards used in building tenders, relevant for school-building procurement. However, it does not explicitly mention alternatives or when not to use it, leaving some ambiguity compared to similar sibling tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_cpv_codesARead-onlyIdempotent
Translate a keyword into the CPV classification codes needed to filter a search. Call this first when the user names a subject rather than a code.
Search CPV classification codes by keyword.
CPV (Common Procurement Vocabulary) is the international code system used to
filter search_procurements by category. Resolve a keyword like "Metall" to
its code here, then pass the code to search_procurements(cpv_codes=[...]).
| Name | Required | Description | Default |
|---|---|---|---|
| args | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| codes | Yes | |
| count | Yes | |
| source | Yes | |
| system | Yes | cpv, bkp, npk, cpc, ebkp-h, ebkp-t or oag. |
| match_type | No | exact, fuzzy (broader term, see note), or none. |
| provenance | Yes | |
| retrieved_at | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true, openWorldHint=true, idempotentHint=true, and destructiveHint=false. The description adds context about the tool's role in the overall search pipeline but does not disclose additional behavioral traits like matched/unmatched behavior, result ordering, or rate limits. It is adequate, but not rich beyond annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise and well-structured, starting with a clear use_case tag, followed by a one-sentence summary, and then context about CPV and integration. Every sentence contributes to understanding the tool's purpose and usage. No redundant or fluff content.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a read-only search tool, the description covers purpose, usage timing, and integration with `search_procurements`. It does not explain the return format, but an output schema is present. It also doesn't address edge cases like no matches, but given the tool's simplicity and the presence of annotations/schema, this is sufficiently complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0% – the tool description does not explain any parameters. Although the schema itself provides detailed descriptions for `query`, `limit`, and `language`, the instruction states that with low coverage the description must compensate. It only gives a single example keyword ('Metall') but does not explain the other parameters or their constraints, so it fails to add meaningful value beyond the schema.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description clearly states the tool's function: 'Search CPV classification codes by keyword.' It also specifies that it translates a keyword into CPV codes for filtering `search_procurements`, which differentiates it from sibling tools like `search_construction_codes`. The verb 'search' + resource 'CPV classification codes' is specific and unambiguous.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The use_case explicitly states when to call this tool: 'Call this first when the user names a subject rather than a code.' It also explains the workflow: resolve keyword to code, then pass to `search_procurements(cpv_codes=[...])`. This provides clear when-to-use guidance and distinguishes it from the procurement search tools.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_procurementsARead-onlyIdempotent
Find open tenders matching a topic, canton, CPV code or date window — the default entry point when the question is "what is being tendered?".
Search Swiss public procurement projects on simap.ch.
Covers all cantons and the Confederation, updated intraday. This is the
entry point; use get_procurement_details with the returned ids for the
full record.
Note that simap indexes PROJECTS, not publications: one hit is one project,
represented by its NEWEST publication. A project tendered in March and
awarded in July appears once, as the July award. Use
get_publication_history to see the earlier publications of a project.
At least one filter is required — simap answers a filterless query with nothing rather than everything.
Args:
query: Free-text search over titles and descriptions.
canton: Bare canton id, e.g. ZH (NOT CH-ZH). See CANTON_IDS.
canton_match: How canton is interpreted.
procuring_body (default) — procured by that canton's public
bodies, including communal and subordinate offices.
place_of_delivery — the work is delivered there. Beware: ~60% of
publications carry no structured order address and are invisible to
this filter.
both — the union of the two. Costs two upstream calls and does
not support cursor.
cpv_codes: One or more CPV classification codes. Resolve names to codes
with search_cpv_codes first.
process_type: One of open, selective, invitation, direct, no_process.
pub_type: Publication type. For awarded contracts use one of
award_tender, award_study_contract, award_competition, direct_award —
a plain "award" is rejected by the API.
published_from / published_until: ISO dates YYYY-MM-DD. These filter on
the NEWEST publication date of a project.
cursor: Pagination cursor from a previous response's next_cursor.
language: de, fr, it or en.
| Name | Required | Description | Default |
|---|---|---|---|
| args | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| count | Yes | |
| source | Yes | |
| results | Yes | |
| has_more | Yes | True if the pagination cursor can be advanced. |
| match_type | No | exact when results were returned, none when empty. |
| provenance | Yes | |
| next_cursor | No | Pass as `cursor` to search_procurements for the next page. |
| retrieved_at | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
The annotations already mark this as read-only, open-world, idempotent, and non-destructive. Beyond that, the description discloses important behavior: simap indexes projects rather than publications, only the newest publication represents a project, filterless queries return nothing, and canton_match='both' costs two upstream calls and disables pagination.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is long but front-loaded and well organized: use case, core behavior, sibling routing, indexing caveat, filter requirement, then Args. Every section adds practical guidance for invoking the tool correctly.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a complex procurement search tool with an output schema available, the description covers purpose, alternatives, filtering rules, pagination limitations, and regional/classification semantics. Nothing critical for correct invocation appears to be missing.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Although the input schema contains parameter descriptions, the tool description adds substantial semantic guidance: canton must be a bare id like ZH rather than CH-ZH, canton_match meanings and caveats, cpv_codes should be resolved via search_cpv_codes, pub_type 'award' is rejected, dates filter on the newest publication date, and cursor comes from next_cursor.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description states a specific verb and resource: searching Swiss public procurement projects on simap.ch. It clearly identifies itself as the default entry point for 'what is being tendered?' and distinguishes itself from get_procurement_details and get_publication_history by explaining the project/publication indexing difference.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
It explicitly says when to use this tool ('default entry point when the question is what is being tendered?'), how to follow up with get_procurement_details and get_publication_history, and how to resolve CPV codes with search_cpv_codes. It also states the non-obvious rule that at least one filter is required.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
search_procurements_detailedARead-onlyIdempotent
Answer a question needing both the hit list and each hit's detail in one step, e.g. "which school-building tenders ran in ZH and what BKP codes do they carry?". Prefer this over search_procurements followed by N detail calls.
Search publications and return the FULL record for the top matches at once.
Aggregated entry point for the common "find tenders and show me their details"
question: it runs the search and then fetches get_procurement_details for the
first top_n hits in parallel, so a typical query is answered in a single tool
call instead of a search-then-N-details chain. Each result carries the CPV and
Swiss construction codes (BKP, NPK), deadlines and procurement office.
Prefer search_procurements when you only need the summaries or want to
paginate; use this when you want the leading hits fully expanded immediately.
Args:
top_n: How many of the top hits to expand to full detail (1-5).
query, canton, canton_match, cpv_codes, process_type, pub_type,
published_from, published_until, language: identical to
search_procurements — including the canton_match semantics, which
default to matching the procuring body.
| Name | Required | Description | Default |
|---|---|---|---|
| args | Yes |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| count | Yes | Number of full detail records returned (<= top_n). |
| source | Yes | |
| results | Yes | |
| match_type | No | exact when results were returned, none when empty. |
| provenance | Yes | |
| retrieved_at | Yes | |
| total_matched | No | Total search hits before the top_n detail cutoff. |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already declare readOnlyHint=true and destructiveHint=false. The description adds meaningful context: it runs a search then fetches get_procurement_details in parallel for top_n hits, and each result carries CPV/BKP codes, deadlines, and office. This goes beyond basic safety disclosure to explain the composite behavior.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
Longer than typical, but well-structured with a <use_case> tag, a one-sentence summary, explanation of aggregation, and an Args block. Every sentence contributes value; no redundancy from the schema.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
For a composite tool with one parameter and a sibling reference, the description covers use case, internal behavior, result contents, alternatives, and parameter semantics. The presence of an output schema lowers the burden for return-value details, making this complete.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
Schema description coverage is 0% for the single 'args' parameter, but the description explicitly defines top_n as 'How many of the top hits to expand to full detail (1-5)' and states all other params are 'identical to search_procurements', linking to a sibling for full semantics. This compensates for the schema gap well.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
Description opens with 'Search publications and return the FULL record for the top matches at once' – a specific verb+resource+scope statement. It also explicitly contrasts with sibling search_procurements, clarifying its unique aggregation behavior.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
Provides explicit when/when-not guidance: 'Prefer this over search_procurements followed by N detail calls' and 'Prefer search_procurements when you only need the summaries or want to paginate'. This disambiguates tool selection clearly.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
source_statusARead-onlyIdempotent
Check whether simap.ch is reachable and how fast it is responding. Call this when a search returns nothing and you need to distinguish "no matching tenders" from "the source could not be asked" — the two are not the same answer.
Report reachability and latency of the simap.ch read API.
| Name | Required | Description | Default |
|---|---|---|---|
| args | No |
Output Schema
| Name | Required | Description |
|---|---|---|
| note | No | |
| source | Yes | |
| sources | Yes | |
| provenance | Yes | |
| all_healthy | Yes | |
| retrieved_at | Yes |
TDQS
Does the description disclose side effects, auth requirements, rate limits, or destructive behavior?
Annotations already indicate readOnlyHint, openWorldHint, idempotentHint, and non-destructive behavior. The description adds context beyond annotations by clarifying that the tool reports reachability and latency, and by explaining the semantic importance of distinguishing source failures from empty results. This is useful behavioral context without contradicting any annotations.
Agents need to know what a tool does to the world before calling it. Descriptions should go beyond structured annotations to explain consequences.
Is the description appropriately sized, front-loaded, and free of redundancy?
The description is concise and well-structured. It begins with an XML-like use_case tag conveying the scenario, then a direct statement of what the tool reports. Every sentence is meaningful, with no fluff or repetition.
Shorter descriptions cost fewer tokens and are easier for agents to parse. Every sentence should earn its place.
Given the tool's complexity, does the description cover enough for an agent to succeed on first attempt?
The tool is simple: it checks reachability and latency, with no parameters. The description explains both purpose and usage context, while annotations cover safety and idempotency. An output schema exists (not shown), so return-value details are not required in the description. The description is fully complete for this tool.
Complex tools with many parameters or behaviors need more documentation. Simple tools need less. This dimension scales expectations accordingly.
Does the description clarify parameter syntax, constraints, interactions, or defaults beyond what the schema provides?
The tool takes no arguments (StatusInput has no properties, and the optional 'args' defaults to null). With 0 parameters, the baseline score is 4. The description does not repeat schema details, and no parameter explanations are needed since none exist.
Input schemas describe structure but not intent. Descriptions should explain non-obvious parameter relationships and valid value ranges.
Does the description clearly state what the tool does and how it differs from similar tools?
The description explicitly states the tool's function: 'Check whether simap.ch is reachable and how fast it is responding.' It specifies the resource (simap.ch read API) and the action (checking reachability and latency), clearly distinguishing it from sibling tools that search or retrieve procurement data.
Agents choose between tools based on descriptions. A clear purpose with a specific verb and resource helps agents select the right tool.
Does the description explain when to use this tool, when not to, or what alternatives exist?
The description provides a clear scenario for when to use the tool: 'Call this when a search returns nothing and you need to distinguish "no matching tenders" from "the source could not be asked".' This explains the context and decision point, though it does not explicitly mention alternative tools or when not to use it.
Agents often have multiple tools that could apply. Explicit usage guidance like "use X instead of Y when Z" prevents misuse.
Tool Schema Changelog
Recent tool additions, removals, and schema changes observed during successful MCP inspections.
3 tool updates
v0.19.0- Changed
get_publication_history1 field changed- added
Input schema / $defs / HistoryInput / properties / lot_idAdded value: +{ + "anyOf": [ + { + "pattern": "^[A-Za-z0-9._-]{1,64}$", + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "Required when the publication has lots — take `lot_id` from the matching entry in the search result's `lots` list. Upstream history is kept per lot, so a lot publication cannot be traced without it. Leave unset for a publication without lots.", + "title": "Lot Id" +}
- Changed
search_awards4 fields changed- added
Output schema / $defs / LotRefAdded value: +{ + "description": "One lot of a publication, carrying the id `get_publication_history` needs.\n\nUpstream keeps the publication history per lot: a lot publication answers\n`past-publications` only when a `lotId` comes with it. Dropping this list\nfrom the summary left that id unobtainable through this server, so the\nhistory tool could not be called correctly for any lot-based procurement.", + "properties": { + "lot_id": { + "title": "Lot Id", + "type": "string" + }, + "lot_number": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lot Number" + }, + "lot_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lot Title" + }, + "pub_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pub Type" + }, + "publication_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The lot's own publication id, which may differ from the project's.", + "title": "Publication Id" + } + }, + "required": [ + "lot_id" + ], + "title": "LotRef", + "type": "object" +} - added
Output schema / $defs / ProcurementSummary / properties / lotsAdded value: +{ + "description": "Empty unless lots_type is 'with'. Pass a lot_id from here to get_publication_history, which cannot trace a lot publication without one.", + "items": { + "$ref": "#/$defs/LotRef" + }, + "title": "Lots", + "type": "array" +} - added
Output schema / $defs / ProcurementSummary / properties / lots_droppedAdded value: +{ + "default": 0, + "description": "How many lots upstream listed without a lotId, and this server could therefore not carry through. Above 0 the `lots` list is incomplete: those lots exist upstream, but their history is unreachable from here. Treat a non-zero value as missing data, not as fewer lots.", + "title": "Lots Dropped", + "type": "integer" +} - added
Output schema / $defs / ProcurementSummary / properties / lots_typeAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "with or without — whether the project is split into lots.", + "title": "Lots Type" +}
- Changed
search_procurements4 fields changed- added
Output schema / $defs / LotRefAdded value: +{ + "description": "One lot of a publication, carrying the id `get_publication_history` needs.\n\nUpstream keeps the publication history per lot: a lot publication answers\n`past-publications` only when a `lotId` comes with it. Dropping this list\nfrom the summary left that id unobtainable through this server, so the\nhistory tool could not be called correctly for any lot-based procurement.", + "properties": { + "lot_id": { + "title": "Lot Id", + "type": "string" + }, + "lot_number": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lot Number" + }, + "lot_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Lot Title" + }, + "pub_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "title": "Pub Type" + }, + "publication_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "The lot's own publication id, which may differ from the project's.", + "title": "Publication Id" + } + }, + "required": [ + "lot_id" + ], + "title": "LotRef", + "type": "object" +} - added
Output schema / $defs / ProcurementSummary / properties / lotsAdded value: +{ + "description": "Empty unless lots_type is 'with'. Pass a lot_id from here to get_publication_history, which cannot trace a lot publication without one.", + "items": { + "$ref": "#/$defs/LotRef" + }, + "title": "Lots", + "type": "array" +} - added
Output schema / $defs / ProcurementSummary / properties / lots_droppedAdded value: +{ + "default": 0, + "description": "How many lots upstream listed without a lotId, and this server could therefore not carry through. Above 0 the `lots` list is incomplete: those lots exist upstream, but their history is unreachable from here. Treat a non-zero value as missing data, not as fewer lots.", + "title": "Lots Dropped", + "type": "integer" +} - added
Output schema / $defs / ProcurementSummary / properties / lots_typeAdded value: +{ + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": null, + "description": "with or without — whether the project is split into lots.", + "title": "Lots Type" +}
9 tool updates
v0.18.3- First observed
find_procurement_office - First observed
get_procurement_details - First observed
get_publication_history - First observed
search_awards - First observed
search_construction_codes - First observed
search_cpv_codes - First observed
search_procurements - First observed
search_procurements_detailed - First observed
source_status
TDQS
Scored across 9 tools
The three search-family tools (search_procurements, search_procurements_detailed, search_awards) target genuinely different questions (open tenders vs. expanded hits vs. awarded contracts), and descriptions explicitly state when to pick each. However, search_procurements_detailed is a wrapper over search_procurements + get_procurement_details, so an agent could plausibly reach for the wrong one without reading carefully.
Nearly all tools follow a consistent snake_case verb_noun pattern (search_awards, get_procurement_details, find_procurement_office, search_cpv_codes). The lone exception is source_status, a noun phrase that breaks the verb-first convention but remains clear.
Nine tools is well-scoped for a procurement-search server, covering search, detail retrieval, history, code/canton resolution and health checks. Each tool earns its place with no obvious redundancy beyond the deliberate convenience wrappers.
Covers the full read lifecycle: keyword search, award filtering, full-record fetch, project history, plus the resolver tools (CPV, construction codes, offices) needed to build queries. As a read-only simap front-end it is thorough, though finer-grained capabilities like lot-level detail retrieval or bulk export are absent.
Maintenance
Related MCP Connectors
simap MCP — Swiss public procurement tenders and awards (keyless).
MCP server for French (BOAMP) + EU (TED) public procurement data via TenderAPI.
TED MCP Server: Real-time EU public tenders access. https://www.lexsocket.ai/
Read-only MCP server for searching Japan government procurement bid information from the KKJ portal.
Related MCP Servers
- AlicenseAqualityAmaintenanceEnables AI assistants to search and view public tenders from Switzerland's simap.ch platform.14123 npm8MIT
- AlicenseNot gradedqualityBmaintenanceProvides access to Swiss public procurement tenders and awards data, queryable via natural language through the Pipeworx gateway.3 npmMIT
- AlicenseAqualityAmaintenanceMCP server for amtsblattportal.ch — the Swiss official gazette portal (SHAB + 27 cantonal gazettes). Public procurement and official notices, person-data rubrics excluded by design.6154 PyPIMIT
- AlicenseAqualityAmaintenanceMCP server for Switzerland's national metadata catalogue, enabling AI agents to discover datasets, APIs, public services, and publishers through free-text search and structured queries.1355 PyPIMIT