Skip to main content
Glama
ZohaibAamer

tender-radar-mcp

by ZohaibAamer

Tender Radar

CI

A Public Tender Intelligence Agent: unified UK + EU procurement data, served to AI agents through a governed MCP server, proven by an eval suite.

Ask it "Which open tenders fit a 50-person UK data consultancy?" and it answers with ranked matches and cited source links — through Claude Desktop, MCP Inspector, or the bundled CLI agent.


The problem

Public-sector contracts are a huge market that small and mid-sized companies systematically miss, for a mundane reason: the notices are scattered.

  • The UK publishes above-threshold tenders on Find a Tender and lower-value ones on Contracts Finder — in OCDS format, and often the same tender appears on both portals with different IDs.

  • The EU publishes on TED — in a completely different standard (eForms), across 24 languages.

An SME bid team can't watch three portals in two data standards. And if you put an LLM on top of raw data, you trade one problem for another: agents that write their own SQL produce confident, wrong numbers — silently adding euros to pounds, averaging framework ceilings into contract values, citing tenders that don't exist.

Tender Radar solves both halves: one unified dataset and an agent that can only query it through governed, tested paths.

Related MCP server: oro-intel-mcp

What it does

Capability

How

Unifies 3 portals, 2 standards

One OCDS-shaped schema; one mapper per standard

Deduplicates

Lifecycle grouping by OCDS ocid + cross-portal matching with measured precision 0.93 / recall 0.89

Serves agents

MCP server (stdio) with 5 tools + a discoverable semantic-layer resource

Governs queries

Agents aggregate only via query_metric, compiled from one YAML file — no free-SQL tool exists

Matches companies

YAML company profiles (Pydantic-validated) ranked against open tenders with reasons

Proves it works

52 agent evals with SQL-derived gold answers, incl. governance negatives and prompt-injection tests

Architecture overview

  UK Find a Tender      UK Contracts Finder        EU TED
  (OCDS releases)        (OCDS releases)         (eForms API)
        │                      │                      │
        └───────── one OCDS mapper ──────────── eForms mapper
                       │                              │
                       ▼                              ▼
              Python ingest  →  data/raw/{source}.parquet
                  (idempotent merge, keyed per notice)
                                 │
                                 ▼
                       dbt on DuckDB
         staging (views) → core (notices, tenders, CPV) → marts
                                 │
                fct_tenders ← enforced model contract
                                 │
           ┌─────────────────────┼─────────────────────┐
           ▼                     ▼                     ▼
   semantic layer         MCP server (stdio)     dedupe matcher
   (YAML → SQL           5 tools, 1 resource,    links + confidence,
    compiler)             NO free-SQL tool       measured P/R
                                 │
                                 ▼
               any MCP client: Claude Desktop,
               MCP Inspector, or the CLI agent
                   (Gemini Flash, swappable)
                                 │
                                 ▼
                  evals: 52 questions, SQL gold
                  answers, frozen 511-notice fixture

The flow in words: Python clients fetch each portal and map every notice into one UnifiedNotice schema, merging idempotently into per-source Parquet. dbt builds the warehouse in textbook layers — staging views per source, a core layer that groups a tender's lifecycle notices (planning → tender → award) by their shared ocid, and a single mart (fct_tenders) protected by an enforced dbt contract. On top of that mart sit three consumers: a hand-rolled semantic layer that compiles declared metrics to SQL, the MCP server that exposes everything agents may do, and the cross-portal matcher that links the same tender across UK portals. Deep dive: ARCHITECTURE.md, decisions: docs/adr/.

Design invariants:

  1. Governance lives in the server, not the agent. There is no tool that accepts SQL, so no client — however prompted or jailbroken — can bypass the semantic layer (ADR-0001).

  2. The clock is injectable (TENDER_RADAR_TODAY), so "open tender" and "deadline within 7 days" mean the same thing on live data and the frozen test fixture.

  3. Money is guarded. Value metrics refuse to mix currencies and exclude framework ceilings by default — the YAML documents both rules.

  4. Tender text is data, not instructions. Descriptions are third-party input; prompt-injection evals against a planted hostile tender verify the agent treats them that way.

Tech stack

Layer

Technology

Why

Language

Python 3.12, uv

Fast, reproducible env management

Ingestion

httpx, Pydantic

Typed unified schema at the edge

Storage

Parquet + DuckDB

Zero-infra analytical warehouse

Transform

dbt (dbt-duckdb)

Layered models, tests, contracts, source freshness

Semantic layer

Hand-rolled YAML + compiler

Governance legible in one file (ADR-0003)

Agent interface

MCP Python SDK (stdio)

Works with Claude Desktop, Inspector, any MCP client

LLM

Gemini Flash via google-genai

Free tier; swappable behind a 3-method protocol

CI/CD

GitHub Actions

Tier-1 checks per push; on-demand data refresh + LLM evals

Data persistence

GitHub Release assets

No credentials, one-command download (ADR-0004)

Lint/test

ruff, pytest

42 deterministic tests incl. the eval scorer itself

Project structure

tender-radar/
├── src/tender_radar/
│   ├── config.py            # paths + the injectable clock (TENDER_RADAR_TODAY)
│   ├── profiles.py          # Pydantic company profiles (YAML + generated JSON Schema)
│   ├── ingest/              # source clients, mappers, idempotent parquet store
│   │   ├── models.py        #   UnifiedNotice — the one schema everything maps into
│   │   ├── ocds.py          #   shared mapper for both UK portals
│   │   ├── ted.py           #   eForms → unified mapper (the cross-standard one)
│   │   └── store.py         #   keyed merge: reruns never duplicate
│   ├── dedupe/              # cross-portal matcher (blocking + scoring + links)
│   ├── semantic/            # layer.yaml (the governed surface) + SQL compiler
│   ├── mcp_server/          # stdio MCP server: 5 tools, semantic://layer resource
│   └── agent/               # ~100-line loop + swappable LLM provider
├── dbt/                     # staging → core → marts; CPV seed; contracts
├── profiles/                # company profiles as data (+ JSON Schema)
├── evals/                   # 52 questions, SQL golds, harness, dedupe labels
├── fixtures/                # frozen 511-notice eval fixture (script-built)
├── scripts/                 # fixture builder, matcher evaluation, schema gen
├── tests/                   # 42 deterministic tests (tier 1)
├── .github/workflows/       # ci.yml (every push) + data-refresh.yml (on demand)
├── ARCHITECTURE.md          # diagrams and component walkthrough
├── docs/adr/                # six architecture decision records
└── DATA_LICENSE.md          # OGL v3 / EU reuse attribution

How to run it

Prerequisites: uv (manages Python itself). Everything below is idempotent — rerunning is always safe. Every step also has a make shortcut — run make help for the list.

1. Install

git clone https://github.com/ZohaibAamer/tender-radar && cd tender-radar
uv sync

2. Get data and build the warehouse

# ingest live notices from all three portals (public APIs, no keys needed)
uv run python -m tender_radar.ingest --source all --since 2026-10-01

# build the DuckDB warehouse with dbt
DBT_PROFILES_DIR=dbt uv run dbt deps --project-dir dbt
DBT_PROFILES_DIR=dbt uv run dbt build --project-dir dbt

# compute cross-portal duplicate links
uv run python -m tender_radar.dedupe

(If a data-latest release has been published, you can skip ingestion: gh release download data-latest --dir data/raw.)

3. Use it from an MCP client

Fastest look — the official Inspector UI:

npx @modelcontextprotocol/inspector uv run tender-radar-mcp

Claude Desktop / Claude Code (mcpServers config):

{
  "tender-radar": {
    "command": "uv",
    "args": ["run", "--directory", "/path/to/tender-radar", "tender-radar-mcp"]
  }
}

Tools: search_tenders · get_tender · match_profile · upcoming_deadlines · query_metric · list_company_profiles. The semantic layer is readable as the semantic://layer resource, so agents discover metrics instead of guessing.

4. Ask the CLI agent

export GEMINI_API_KEY=...   # free tier: https://aistudio.google.com
uv run python -m tender_radar.agent \
  "Which open tenders fit a 50-person UK data consultancy?"

Company profiles are plain YAML — copy profiles/example-uk-data-consultancy.yaml and describe your own company.

5. Run the tests and evals

# tier 1 — deterministic, no LLM, what CI runs on every push
uv run ruff check . && uv run pytest

# the matcher's measured scorecard (precision/recall sweep)
uv run python scripts/evaluate_matcher.py

# tier 2 — the 52-question agent eval suite (needs GEMINI_API_KEY)
TENDER_RADAR_DATA_DIR=$PWD/fixtures DBT_PROFILES_DIR=dbt \
  uv run dbt build --project-dir dbt          # fixture warehouse, once
uv run python evals/harness.py                # responses cached per question
uv run python evals/harness.py --category injection

Data refresh and storage

The Data refresh & evals workflow (Actions tab, run on demand) ingests the last 7 days from all three sources, rebuilds the warehouse, runs dbt source freshness as the pipeline health check, recomputes dedup links, and uploads Parquet to the rolling data-latest release, optionally cutting a dated snapshot. Nothing runs unattended by design — this is a reference implementation, not an operated service. To run it nightly on your own fork, add the cron trigger shown at the top of data-refresh.yml.

Licensing

Code is MIT. Tender data belongs to its sources and is reused under their terms — Open Government Licence v3.0 (both UK portals declare it in their API responses) and the EU's reuse policy for TED. Full attribution and the synthetic-test-row disclosure: DATA_LICENSE.md.

Honest limitations

  • The TED mapper carries a pragmatic field subset (title, buyer, CPV, value, deadline, links); eForms is far richer than any flat schema.

  • Cross-portal matching is title-driven: a renamed re-publication scoring below the measured 0.75 threshold is missed — a trade-off that is measured (P 0.93 / R 0.89), not hidden.

  • Disclosed values are floors, not market sizes: many notices omit value, and the semantic layer's metric descriptions say so.

Related MCP Connectors

Related MCP Servers

  • A
    license
    Not graded
    quality
    D
    maintenance
    UK public procurement data for AI agents. Tenders, contracts, buyer and supplier profiles over MCP and REST. 250 free credits.
    MIT
  • A
    license
    A
    quality
    B
    maintenance
    Enables AI agents to find, score, and monitor government contract opportunities across UK, EU, and US with AI-powered relevance scoring.
    2
    103 npm
    1
    MIT
  • A
    license
    Not graded
    quality
    B
    maintenance
    Enables searching and analyzing government tenders, contract awards, and pre-tender pipelines from 21 official sources, with tools for tender search, award intelligence, and detailed notice retrieval.
    MIT