Skip to main content
Glama
elouansalaun

SEAO procurement-intelligence MCP server

by elouansalaun

SEAO Procurement Intelligence (MCP server/tool for Claude/GPT)

In one sentence: you ask a question about Quebec's public security contracts in plain language ("Who held security at the Sûreté du Québec?", "What is Garda's win rate?"), an LLM like Claude or ChatGPT calls a set of analysis tools over a cleaned database of official procurement data, and you get a sourced answer with tables.

A full example of chat (in french) with Claude is available at the end of this file


1. The big picture

flowchart LR
    Q["🗣️ Question<br/><i>'Who usually wins security<br/>contracts in Montreal?'</i>"] --> L
    L["🧠 LLM<br/>Claude / ChatGPT<br/>(plans, explains)"] -->|MCP tool calls| S["🛠️ MCP server<br/>9 read-only tools"]
    S -->|SQL| D[("🗄️ seao.db<br/>SQLite")]
    D --> S
    S -->|markdown tables| L
    L --> R["📋 Answer<br/>tables, rankings, insights"]

MCP (Model Context Protocol) is the open standard that lets an AI assistant call external tools. This project is the server side: it turns a public dataset into tools an assistant can use, and the tools do the counting so the LLM does not have to guess.

Who it is for: a company (or an analyst) that wants to answer business questions about public tenders without writing SQL, e.g. pricing a bid, sizing up a competitor, or understanding a buyer.


Related MCP server: Bay Street MCP

2. The data

Source

SEAO, Quebec's official public-tendering system, published as open data on open.canada.ca

Format

OCDS (Open Contracting Data Standard), nested JSON, one file per week

Raw volume

267 weekly files, ≈3.2 GB

Scope kept

Security services: guarding, security agencies, patrols (fire-safety tenders excluded)

After filtering

1,785 procurement processes, 336 public buyers, 480 distinct suppliers, 3,674 bids, 1,950 awards, 1,299 contracts

Period

2008–2026, but 75% of processes fall in 2021–2025

Currency

CAD


Of course adapting filters is necessary if you want to study another different domain instead of security services but the tool should work same

3. The pipeline, step by step

flowchart LR
    A["🌐 Open-data feed<br/>(Atom + CKAN API)"] --> B["⬇️ Fetch<br/>weekly OCDS JSON<br/>≈3.2 GB"]
    B --> C["🔍 Filter<br/>title keywords +<br/>UNSPSC codes"]
    C --> D["🧱 Normalize<br/>nested JSON →<br/>18 relational tables"]
    D --> E[("🗄️ seao.db<br/>≈7 MB")]
    E --> F["🛠️ MCP server"]

Step

Script

What it does

Fetch

fetch_seao_opendata.py

Reads the government Atom feed and downloads only new or updated weekly files

Filter

build_raw_json_db.py

Keeps a tender if its title matches security keywords (gardiennage, agents de sécurité, patrouille…) or its product code (UNSPSC) is a security code; removes duplicates

Normalize

build_work_sqlite_db.py

Flattens the nested JSON into tables (process, tender, lot, bid, award, supplier, contract, amendment…), adds a French full-text index, 5 summary views and a built-in data dictionary

The build uses only Python's standard library. Changing the keyword list at the bottom of build_raw_json_db.py points the whole project at another sector.

Cleaning the raw data

Government open data is messy. The build step fixes this before anyone queries the data:

Problem in the raw data

Fix

Same field spelled differently across years (NEQ/neq, totalAmount/totalamount, durationInDay/durationInDays)

Read every known spelling into one column

Party IDs only unique inside one tender

Key every party on (tender, party)

French accents ("Sûreté" vs "Surete")

Accent-insensitive search

Codes stored as bare numbers (region 6, unit 7)

Lookup tables served to the LLM ("6 = Montréal", "7 = hourly rate")


4. Why not just give the LLM raw SQL access?

This is the main design decision. A plain SQL connection lets the LLM write queries that run without errors and still give wrong answers. Three traps in this dataset cause that:

Trap

What a naive query does

What the tools do

One company, many names

Groups by name. Garda appears under 9 business numbers and 6 spellings, so its results are split into small pieces

Resolve any name to the official Quebec business number (NEQ) and add up all of them

Wins are not stored

Finds no "won" column, or guesses

Derives it: a bidder won if it appears among that tender's awarded suppliers, counted once per tender even when the tender has several lots

Sole-source contracts

Counts direct awards (gré à gré) in win rates, though nobody else could bid

Computes win rates and price margins on competitive tenders only (open and limited)

These rules are also written into the server's instructions, so the LLM reads them before it writes any query of its own.


5. The tools

A hybrid design: 3 flexible tools for any question, plus 6 ready-made analyses that apply the rules above.

flowchart TB
    subgraph Core["Flexible core"]
        direction LR
        A1["describe_schema<br/>data dictionary + code lookups"]
        A2["search_processes<br/>full-text + filters"]
        A3["run_sql<br/>read-only SELECT"]
    end
    subgraph Curated["Curated analyses"]
        direction LR
        B1["🎯 incumbent"] --- B2["💰 price_benchmark"]
        B3["🏢 supplier_profile"] --- B4["⚔️ head_to_head"]
        B5["🏛️ buyer_profile"] --- B6["📈 market_overview"]
    end

Business question

Tool

Returns

Who holds this buyer's contract today?

incumbent

Current and past winners, contract values, most recent first

What price should we bid?

price_benchmark

Distribution of winning bids and the gap between the winner and the next-lowest bidder

How strong is this competitor?

supplier_profile

Win rate, total won, top clients, regions, most frequent rivals

Us vs them?

head_to_head

How often two firms bid on the same tender and who won

How does this buyer behave?

buyer_profile

Open vs sole-source mix, spending, whether it keeps the same supplier or rotates

How big is the market and who leads it?

market_overview

Value per year, top-N market share, regional spread

Anything else

run_sql

Any read-only query, guided by describe_schema

Example output, supplier_profile("Garda"):

# Supplier profile: GROUPE DE SÉCURITÉ GARDA SENC
matched 9 NEQs (aggregated together)
- Awards won (all methods): 341 across 320 processes, total 623,281,730 $
- Competitive win rate (open+limited): 21.9% (51 won / 233 bid on)
Most frequent rival bidders:
- NEPTUNE SECURITY SERVICES INC.: co-bid 108x, rival won 55
- TRIMAX SÉCURITÉ INC.: co-bid 54x, rival won 15

6. Safety

The server is read-only:

flowchart LR
    Q["SQL from the LLM"] --> C1{"single SELECT / WITH?"}
    C1 -->|no| X["❌ rejected"]
    C1 -->|yes| C2{"write / DDL / PRAGMA<br/>keyword?"}
    C2 -->|yes| X
    C2 -->|no| R["DB opened mode=ro<br/>auto LIMIT · 5 s timeout"] --> OK["✅ rows"]
  • The database file is opened in read-only mode, so even a query that got past the checks could not modify it.

  • Results are capped (automatic LIMIT), and any query running longer than 5 seconds is stopped.


7. Works with

Client

How

Claude Code

.mcp.json is in the repo, so the server is offered as soon as the project is opened

Claude Desktop

One entry in claude_desktop_config.json

ChatGPT

Through an HTTP bridge (mcp-proxy) and a tunnel, added as a custom connector


8. Stack and numbers at a glance

Layer

Tech

Data acquisition

requests, Atom/XML parsing, CKAN API

Storage

SQLite: 18 tables, FTS5 full-text index, 5 views, data dictionary

Server

Python, MCP SDK (FastMCP), stdio transport

Testing

Smoke test that runs every tool on real analyst questions (test_mcp_tools.py)

Raw data → database

3.2 GB of JSON → 7 MB SQLite

Coverage

1,785 processes · 336 buyers · 480 suppliers

Full smoke test (all 9 tools)

≈0.3 s

Runtime dependencies

2 (mcp, requests)

Known limits

  • Coverage is partial: SEAO's open data only reaches back reliably to about 2021. Older contracts are sparse, so figures are indicative, not exhaustive.

  • The filter decides the scope. A security tender with an unusual title and no security product code will be missed.

Example

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    A Model Context Protocol server that lets Claude (and any MCP client) cite actual Canadian financial-services regulations: OSFI, PIPEDA, FINTRAC, Quebec Law 25.
    1
    MIT
  • F
    license
    Not graded
    quality
    D
    maintenance
    A Model Context Protocol server that exposes SQL Server metadata and read-only query execution as a structured HTTP API, with safety validation and allowlist policy enforcement.
    1
    -
  • F
    license
    Not graded
    quality
    D
    maintenance
    Model Context Protocol (MCP) server that gives AI assistants a safe, correct data-analyst capability over business metrics - without raw SQL improvisation.
    -