Skip to main content
Glama
swiss-procurement-mcp

swiss-procurement-mcp

by malkreide

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

CI PyPI Python License: MIT MCP Portfolio Deutsch

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

Demo: Claude using search_procurements_detailed and search_construction_codes


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

/publications/v2/project/project-search

none

20 hits, canton filter, current-day

/publications/v1/.../publication-details/...

none

full record: criteria, deadlines, codes

/publications/v1/publication/{id}/past-publications

none

project lifecycle

/codes/v1/cpv/search

none

CPV full-text search

/codes/v1/{bkp,npk,ebkp-h,ebkp-t,oag,cpc}/search

none

Swiss construction codes

/procoffices/v1/po/public

none

~1 MB office list (client-side filter)

/cantons/v1, /countries/v1

none

reference data

Known findings

  1. Wrong host, wrong conclusion. The read API lives under www.simap.ch/api. The simap.ch/de web UI is a separate SSR app that exposes none of it — probing the UI produced an earlier, mistaken "no API" verdict.

  2. lang is mandatory on project-search. Omitting it is HTTP 400 (errorCode E0025), not an empty result. The client injects a default.

  3. Award is not "award". newestPubTypes=award returns HTTP 400. Awards are split by procedure: award_tender, award_study_contract, award_competition, direct_award. The search_awards tool queries all four.

  4. Canton ids are bare. ZH, not CH-ZH. Passing an ISO subdivision code silently matches nothing; this server rejects it with a clear error.

  5. A session cookie is required. The first request sets it; a persistent HTTP client handles this transparently.


Tools

Tool

Purpose

search_procurements

Search projects by canton, CPV, process type, date, text

search_procurements_detailed

Search + full detail for the top n hits in one call (aggregated)

search_awards

Awarded contracts only (all four award types at once)

get_procurement_details

Full record for one publication

get_publication_history

Earlier publications of the same project (tender → award)

search_cpv_codes

Resolve keywords to CPV classification codes

search_construction_codes

Swiss construction codes (BKP, NPK, eBKP, OAG, CPC)

find_procurement_office

Public procurement offices by partial name

source_status

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

procuring_body (default)

procured by that canton's public bodies, incl. communal and subordinate offices (issuedByOrganizations)

410 projects

place_of_delivery

the work is delivered there (orderAddressCantons)

263 projects

both

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-mcp with national coverage instead of three cantons.


Installation

uvx swiss-procurement-mcp

Claude 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_mcp

Container

docker compose up --build        # SSE on :8000

The 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

MCP_TRANSPORT

stdio

stdio | sse | streamable-http

MCP_HOST / HOST

127.0.0.1

HTTP binding (cloud transports only). Defaults to loopback; set 0.0.0.0 explicitly to expose all interfaces in a cloud deployment.

MCP_CORS_ORIGINS

(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. Mcp-Session-Id is exposed and accepted for the listed origins, so a browser client can hold a session. * is honoured but logs a warning and disables credentials, because browsers reject a wildcard origin together with credentials.

MCP_STATELESS

(off)

Set to 1 to run the streamable-http transport with no session tracking. Removes session affinity as a concern for multi-instance deployments; gives up SSE stream resumption and server-initiated notifications. No effect on the legacy SSE transport, which logs a warning if asked. See docs/load-balancing.md.

PORT / MCP_PORT

8000

HTTP port (cloud transports only)

LOG_LEVEL

INFO

DEBUG | INFO | WARNING | ERROR. Structured JSON, one object per line, always on stderr — stdout carries the MCP protocol on a stdio transport.

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

DEBUG

a tool call was entered (tool_call_started) — tells you whether a hung call ever started

INFO

a tool call finished cleanly, with latency

WARNING

simap.ch was unreachable or errored (upstream_degraded)

ERROR

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 initialize handshake

2024-11-05 … 2025-11-25 — the handshake ceiling

Served via the per-request envelope

2026-07-28

Who picks

The client's first request, once per connection. A request carrying the 2026-07-28 _meta envelope opens a modern connection; anything else opens a handshake connection.

Pinned in

MCP_PROTOCOL_VERSION in server.py

SDK

mcp>=2.0.0,<3

Cache hints

tools/list, server/discover, prompts/list, resources/list, resources/templates/list: ttlMs 300000, cacheScope public

Server identity

server/discover answers name, title, version (the installed distribution's) and websiteUrl, plus server-level instructions

Verified by

tests/test_modern_era.py — real 2026-07-28 envelope requests over HTTP and stdio

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_drift event at WARNING. The server keeps working.

  • In CI, tests/test_protocol_version.py fails.

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/discover replaces it, and it is the only place a native client can ask who this server is. Every request carries its own _meta envelope (protocol version, client info, client capabilities); without it the server answers -32602.

  • Requests route by header. Mcp-Method and Mcp-Name must agree with the body, or the request is refused with -32020 before 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.)

  • initialize gets 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

    -32601 METHOD_NOT_FOUND, HTTP 404

    initialize after a modern request, over stdio

    -32022 UNSUPPORTED_PROTOCOL_VERSION

    enveloped initialize first, over stdio

    accepted — a 2025-11-25 handshake

    The fourth cell does not exist: over stdio an initialize without history opens a legacy connection instead of being refused, so the HTTP situation cannot be reproduced there. Note the third row — stamping the envelope on initialize silently 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-28 the SDK derives tools.listChanged, prompts.listChanged, resources.listChanged and resources.subscribe from one fact — whether subscriptions/listen is served — and MCPServer registers that unconditionally. So server/discover reports all four as true where initialize reports all four as false, 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.md in 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

source_status

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.

search_cpv_codes

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 API

See EXAMPLES.md for use cases grouped by audience (schools, public, administration, developers) and a tool-selection reference table.


Known limitations

  • Projects, not publications. project-search indexes projects and represents each by its newest publication. A project tendered in March and awarded in July appears once, as the July award; search_awards likewise only finds projects whose newest publication is an award, so a later correction hides it. get_publication_history reaches the earlier publications.

  • Lot-based procurements are traced per lot. Upstream keeps the publication history per lot, so get_publication_history needs a lot_id whenever the search result shows lots_type: "with" — take one from that result's lots list. 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 publish

Why 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-probe methodology.

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 tools
find_procurement_officeA
Read-onlyIdempotent

<use_case>Resolve a partial organisation name to the procuring offices simap knows, when the user names an authority rather than a project.</use_case>

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.

Inputs: args (required). Returns: note, count, source, offices, +3 more

get_procurement_detailsA
Read-onlyIdempotent

<use_case>Retrieve the full record for one publication once you have its ids: criteria, deadlines, classification codes and the procuring body.</use_case>

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.

Inputs: args (required). Returns: note, title, source, cpv_code, +15 more

get_publication_historyA
Read-onlyIdempotent

<use_case>Trace one project through time — tender to award to correction. Use when the question is "what happened to this procurement?".</use_case>

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.

Inputs: args (required). Returns: note, count, source, project_id, +3 more

search_awardsA
Read-onlyIdempotent

<use_case>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.</use_case>

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.

Inputs: args (required). Returns: note, count, source, results, +5 more

search_construction_codesA
Read-onlyIdempotent

<use_case>Translate a keyword into Swiss construction cost codes (BKP, NPK, eBKP, OAG, CPC) — the bridge between a building topic and procurement filters.</use_case>

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.

Inputs: args (required). Returns: note, codes, count, source, +4 more

search_cpv_codesA
Read-onlyIdempotent

<use_case>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.</use_case>

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=[...]).

Inputs: args (required). Returns: note, codes, count, source, +4 more

search_procurementsA
Read-onlyIdempotent

<use_case>Find open tenders matching a topic, canton, CPV code or date window — the default entry point when the question is "what is being tendered?".</use_case>

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.

Inputs: args (required). Returns: note, count, source, results, +5 more

search_procurements_detailedA
Read-onlyIdempotent

<use_case>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.</use_case>

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.

Inputs: args (required). Returns: note, count, source, results, +4 more

source_statusA
Read-onlyIdempotent

<use_case>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.</use_case>

Report reachability and latency of the simap.ch read API.

Inputs: args. Returns: note, source, sources, provenance, +2 more

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections.

Loading tool schema changelog…

    TDQS

    A4.2/5.0

    Scored across 9 tools

    Disambiguation4/5

    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.

    Naming Consistency4/5

    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.

    Tool Count5/5

    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.

    Completeness4/5

    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

    ActivityMaintained
    ResponsivenessLimited data

    Related MCP Connectors

    Related MCP Servers

    • A
      license
      A
      quality
      B
      maintenance
      Enables AI assistants to search and view public tenders from Switzerland's simap.ch platform.
      14
      309 npm
      9
      MIT
    • A
      license
      Not graded
      quality
      C
      maintenance
      Provides access to Swiss public procurement tenders and awards data, queryable via natural language through the Pipeworx gateway.
      304 npm
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      MCP server for amtsblattportal.ch — the Swiss official gazette portal (SHAB + 27 cantonal gazettes). Public procurement and official notices, person-data rubrics excluded by design.
      6
      54 PyPI
      MIT
    • A
      license
      A
      quality
      B
      maintenance
      MCP 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.
      13
      30 PyPI
      MIT