Local MCP server for German public procurement search (Vergabe Dashboard). Semantic search, tender matching, and company profiles — queries and profiles are embedded locally; only vectors leave the machine.
Vergabe MCP demonstrates solid tool design with clear naming, comprehensive descriptions, and well-structured schemas. All 11 tools follow verb_noun conventions (search_text, list_releases, get_release, etc.). Descriptions are detailed (100-400 chars) and explain WHAT, WHEN, and WHY. Input schemas are present with type definitions and parameter descriptions. However, output schemas are not explicitly documented in the source, only described in prose. Error handling guidance is minimal. No tool annotations (readOnlyHint/destructiveHint) despite clear risk classifications. The server demonstrates production-grade intent but lacks formal output schema documentation and error recovery patterns.
Create a company profile for tender matching. Stores name, description, CPV codes, categories, and location in a local SQLite database. The description is embedded locally (passage prefix); neither the text nor the embedding is sent until a match_tenders call sends the vector. Returns the profile UUID and embedding status. German description recommended.
Delete a company profile and its embedding by UUID.
Get a company profile by its UUID. Returns name, description, CPV codes, categories, location, and embedding status.
Health + version check of the API plus local profile counts and the embedding-contract comparison. Reports the API status/version, the client's embedding model/contract/dimension, the server's advertised contract (when present), and whether they match. Call this first to check connectivity.
Fetch the raw eForms XML envelope for a single OCID (5 fields: ocid, notice_id, data_source, country, raw_xml). Optional `notice_id` selects a specific notice of the OCID (EU siblings share one OCID); absent fetches the latest notice. Pair with linked_notices to walk a procurement's lifecycle (PIN->CN->CAN). Parse the XML yourself for detailed eForms fields — the envelope is intentionally thin.
Output schemas not formally documented. Tool descriptions mention return fields in prose (e.g., 'Returns SearchResult rows with ocid, score, title...') but no JSON Schema definitions are visible. LLMs cannot reliably parse unstructured prose output schemas.
No tool annotations (readOnlyHint, destructiveHint, idempotentHint) despite clear risk classifications in metadata. delete_company_profile is marked DESTRUCTIVE but lacks destructiveHint annotation. create_company_profile and update_company_profile lack idempotentHint guidance.
Error handling lacks recovery guidance. No tool descriptions include 'what to do if this fails' or error classification (retryable vs user-fixable vs fatal). E.g., search_text may fail if the API is unreachable, but no guidance on retry strategy or fallback.
Inferred effective spec: <=2025-11-25.
| Scored | Grade | Overall | Spec posture | Rubric |
|---|---|---|---|---|
| 2026-09-23 | C | 64 | <=2025-11-25 | v2 |
List the notices that make up one procurement: given an OCID, return the linked set of {ocid, notice_id} refs (the procurement's notice lineage — PIN/CN/CAN siblings sharing the procurement key). Ids only; fetch each notice's XML via get_release with the returned notice_id. A length-1 set means a single isolated notice with no lifecycle to walk (the sub-threshold case).
List all company profiles (summary without description), ordered by creation date.
Filter-only browse over the German procurement corpus (no semantic query — only filter values cross the wire). Same filter taxonomy as search_text. Paginate with `limit` (max 200, default 20) and `offset`. Returns SearchResult rows ordered by deadline DESC.
Semantic match: find tenders similar to a profile's stored description vector. The vector is embedded locally and sent to the API (the profile text never leaves the machine). Optional `filters` (same taxonomy as search_text) apply server-side. Returns SearchResult rows. `k` defaults to 10, max 100.
Semantic full-text search over the German procurement corpus. The query is embedded locally with multilingual-e5-small and only the 384-float vector is sent to the API (the query text never leaves the machine). Returns the top `k` SearchResult rows grouped by OCID, each carrying ocid, score, title, buyer_name, procurement_method, main_procurement_category, value_amount/currency/currency_eur_amount, deadline (RFC3339), award_date, cpv_codes, phase, documents_url, data_source, country. Optional `filters`: phase (planning/open/closed/awarded/unsuccessful), cpv_starts_with (2..8 digit CPV prefix), country (ISO-2), value_min/value_max (EUR), deadline_after/deadline_before (RFC3339), main_procurement_category (goods/works/services), buyer_name, procurement_method (eForms codes, match-any), data_source.
Update a company profile. Only provided fields change. If the description changes, the profile is re-embedded locally.
Destructive operations (delete_company_profile) lack confirmation/dry-run pattern. No mention of a confirm_before_execute step or preview capability. Agents could accidentally delete profiles without safeguards.
Parameter constraints not fully formalized. E.g., 'k' (number of results) is described as 'default 10, max 100' in prose but no JSON Schema minItems/maxItems. 'filters' is an object with undocumented nested keys (phase, cpv_starts_with, etc.), no schema for the filter object structure.