Seao Mcp — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Seao Mcp (MCP Server) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
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
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.
| 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
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.
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") |
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.
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 15The 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"]LIMIT), and any query running longer than 5 seconds is stopped.| 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 |
| 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) |
<img src="figures/Exemple_Prompt.png" width="99%" alt="Claude answering questions about the Sûreté du Québec's guarding contracts and Charco Industries' bidding history, using the seao tools">
~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.