diff --git a/docs/ANALYTICS_MCP.html b/docs/ANALYTICS_MCP.html
index 939be71..34f5f1c 100644
--- a/docs/ANALYTICS_MCP.html
+++ b/docs/ANALYTICS_MCP.html
@@ -32,7 +32,7 @@ ul.chk li:before{content:"☐";position:absolute;left:0;color:var(--mut)}
Read-only Analytics MCP Server
-
Implementation guide · standalone repo fleetanalytics_mcp, hosted on the tracksolid_db Coolify host · 2026-06-16 · built — pending deploy
+
Implementation guide · standalone repo fleetanalytics_mcp, hosted on the tracksolid_db Coolify host · updated 2026-06-17 · deployed & live
1. Purpose & context
The decision & analytics team needs to pull fleet reporting data (fuel, utilisation,
@@ -62,7 +62,7 @@ SQL guard in the query tool.
| Tool | Purpose |
query(sql, max_rows=1000) | guarded read-only SELECT/WITH; single statement, keyword-blocked, auto-LIMIT; returns rows + truncated flag |
-list_schemas() | readable schemas (reporting, tracksolid) + object counts |
+list_schemas() | readable schemas (reporting, tracksolid, tickets, fuel) + object counts |
list_tables(schema) | tables + views in a schema |
describe_table(schema, table) | columns, types, nullability, defaults |
list_functions(schema='reporting') | reporting.fn_* signatures |
@@ -156,6 +164,30 @@ Flask-style @app.route decorator — verified against the installed
Full, current source is the repo's analytics_mcp.py; the excerpt
above is abridged.
+4.1 Readable data surface
+analytics_ro (and the helper tools, via READABLE_SCHEMAS, default
+reporting,tracksolid,tickets,fuel, override with env MCP_READABLE_SCHEMAS)
+can read:
+
+| Schema | Owner | What's there |
+reporting | tracksolid_owner | curated views/matviews (v_daily_summary, v_trips, v_monthly_cost_centre, …) + fn_* functions |
+tracksolid | tracksolid_owner | raw ingestion tables (devices, positions, events, …) |
+tickets | postgres | INC/CRQ tickets: inc, crq, closure_events, inc_daily_snapshot, geo_clusters, geo_locations, inc_open_sla (view) + 7 functions |
+fuel | postgres | records, ingest_state + 7 functions |
+
+Adding a schema later is config-only: GRANT USAGE/SELECT/EXECUTE … to
+analytics_ro (persist in analytics_ro_role.sql), then add it to
+MCP_READABLE_SCHEMAS and redeploy — no code change.
+Owner gotcha. ALTER DEFAULT PRIVILEGES FOR ROLE … must name
+the role that owns the schema's objects — postgres for
+tickets/fuel, tracksolid_owner for
+reporting/tracksolid — or future objects won't auto-grant.
+tickets.inc shape. Each row has both typed columns
+(bucket, raw_status, normalized_status, sla_status,
+region, cluster, owner, mttr, closed_at,
+latitude/longitude, geog/geom, …) and a
+raw text blob with the original source fields. Query the typed columns, not raw.
+
5. Packaging — Dockerfile + pyproject.toml
Self-contained: pyproject.toml declares the deps (mcp[cli],
psycopg2-binary, uvicorn[standard]) and the Dockerfile
@@ -177,8 +209,16 @@ TLS from the domain; auto-deploys on push via the Forgejo webhook.
Fallback — deploy.sh. Check the repo out on the host and run it: it builds
the image, resolves the DB network + DSN from the running stack, swaps in the
analytics_ro credentials, and runs a standalone Traefik bridge.
-cd ~/fleetanalytics_mcp && git pull
-MCP_AUTH_TOKENS="alice:$(openssl rand -hex 16)" bash deploy.sh
+# add/rotate a token (sets the full token list):
+cd ~/fleetanalytics_mcp && git pull
+MCP_AUTH_TOKENS="alice:$(openssl rand -hex 16)" bash deploy.sh
+
+# code-only redeploy (e.g. a schema change): omit MCP_AUTH_TOKENS —
+# deploy.sh reuses the running container's tokens, no secret to re-type:
+cd ~/fleetanalytics_mcp && git pull && bash deploy.sh
+Optional env: MCP_READABLE_SCHEMAS (default
+reporting,tracksolid,tickets,fuel) controls which schemas the introspection helpers
+expose; MCP_MAX_ROWS (default 10000) the row ceiling.
7. Deploy runbook (ordered)
@@ -196,7 +236,8 @@ claude mcp list # → "fireside-analytics: connected"Claude Desktop / claude.ai: add a custom connector with the same URL and an
Authorization: Bearer <your-token> header. Example prompts: "list the
schemas", "describe reporting.v_daily_summary", "top 10 cost centres by distance
-in the last 30 days".
+in the last 30 days", "open INC tickets by region and SLA status from tickets.inc",
+"MTTR by cluster this month".
9. Verification checklist
@@ -213,7 +254,7 @@ in the last 30 days".
10. Security notes
- Four read-only layers: role GRANTs ·
default_transaction_read_only=on (role + connection) · rolled-back txn · SQL keyword guard.
-- Least privilege:
analytics_ro only has USAGE+SELECT on reporting/tracksolid and EXECUTE on reporting functions.
+- Least privilege:
analytics_ro only has USAGE+SELECT on reporting/tracksolid/tickets/fuel and EXECUTE on those schemas' functions — no other schema (e.g. infrastructure stays unreadable), no write of any kind.
- Per-analyst tokens make access revocable and queries attributable; rotate via
MCP_AUTH_TOKENS + redeploy (recreate).
- Resource guards:
statement_timeout=30s, idle-txn timeout, row cap (1000 default / 10000 ceiling).
- Future: swap static Bearer for OAuth if the team scales; add a column deny-list if PII lives in
tracksolid.*.
diff --git a/docs/ANALYTICS_MCP.md b/docs/ANALYTICS_MCP.md
index 4ac713f..d0eb1c5 100644
--- a/docs/ANALYTICS_MCP.md
+++ b/docs/ANALYTICS_MCP.md
@@ -1,13 +1,15 @@
# Read-only Analytics MCP Server — Implementation Guide
-> **Audience:** engineer deploying/maintaining the server. **Status:** built — pending deploy.
+> **Audience:** engineer deploying/maintaining the server. **Status:** deployed & live at
+> `https://fleetmcp.fivetitude.com/mcp`.
> **Repo:** `fleetanalytics_mcp` (standalone; `repo.rahamafresh.com/kianiadee/fleetanalytics_mcp.git`).
-> Hosted on the same Coolify host as `tracksolid_db`. **Date:** 2026-06-16.
+> Hosted on the same Coolify host as `tracksolid_db`. **Last updated:** 2026-06-17
+> (added `tickets` + `fuel` schemas).
## 1. Purpose & context
The decision & analytics team needs to pull fleet reporting data (fuel, utilisation, driver
-behaviour, INC tickets, raw telemetry) from `tracksolid_db` to make decisions — **read-only,
+behaviour, INC/CRQ tickets, fuel, raw telemetry) from `tracksolid_db` to make decisions — **read-only,
never edit/delete**. The only programmatic surface today is the `dashboard_api` FastAPI
bridge with a fixed set of `/analytics/*` / `/webhook/*` endpoints — too rigid for ad-hoc
analysis.
@@ -37,7 +39,7 @@ analyst's Claude ──HTTPS (Bearer)──► fleetmcp.fivetitude.com (Traefi
│ psycopg2, role = analytics_ro, READ ONLY
▼
timescale_db:5432 (tracksolid_db)
- reporting.* · tracksolid.*
+ reporting.* · tracksolid.* · tickets.* · fuel.*
```
Ports in use: `8890` prod dashboard_api · `8891` staging dashboard_api · **`8892` analytics_mcp**.
@@ -71,9 +73,10 @@ Modelled on `scripts/dashboard_ro_role.sql`. Run as the **postgres superuser** (
```sql
-- analytics_ro_role.sql — dedicated read-only LOGIN role for the analytics MCP server.
-- Run as postgres SUPERUSER via scripts/bootstrap_analytics_ro.sh (NOT run_migrations.py).
--- Grants exactly the read surface: SELECT on reporting.* + tracksolid.*, the v_trips
--- matview, and EXECUTE on reporting.fn_*. No INSERT/UPDATE/DELETE, not the matview owner,
--- so analytics_ro can never write or REFRESH. Idempotent — safe to re-apply (rotates pw).
+-- Grants exactly the read surface: SELECT on reporting.* + tracksolid.* + tickets.* +
+-- fuel.*, the v_trips matview, and EXECUTE on reporting/tickets/fuel functions. No
+-- INSERT/UPDATE/DELETE, not the matview owner, so analytics_ro can never write or
+-- REFRESH. Idempotent — safe to re-apply (rotates pw).
\set ON_ERROR_STOP on
DO $role$
@@ -86,17 +89,28 @@ END $role$;
ALTER ROLE analytics_ro WITH LOGIN PASSWORD :'ro_pw';
GRANT CONNECT ON DATABASE tracksolid_db TO analytics_ro;
-GRANT USAGE ON SCHEMA reporting, tracksolid TO analytics_ro;
+GRANT USAGE ON SCHEMA reporting, tracksolid, tickets, fuel TO analytics_ro;
GRANT SELECT ON ALL TABLES IN SCHEMA reporting TO analytics_ro; -- tables + views
GRANT SELECT ON ALL TABLES IN SCHEMA tracksolid TO analytics_ro; -- tables + views
+GRANT SELECT ON ALL TABLES IN SCHEMA tickets TO analytics_ro; -- INC/CRQ tickets
+GRANT SELECT ON ALL TABLES IN SCHEMA fuel TO analytics_ro; -- fuel
GRANT SELECT ON reporting.v_trips TO analytics_ro; -- MATERIALIZED VIEW
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA reporting TO analytics_ro;
+GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA tickets TO analytics_ro;
+GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA fuel TO analytics_ro;
--- Future objects created by the migration role auto-grant (matviews still need explicit GRANT).
+-- Future objects auto-grant. reporting/tracksolid are owned by the migration role
+-- (tracksolid_owner); tickets/fuel are owned by postgres, so their default-privilege
+-- grants MUST be keyed to postgres or new objects won't be readable. Matviews are
+-- never covered by ALL TABLES — a new matview still needs its own explicit GRANT.
ALTER DEFAULT PRIVILEGES FOR ROLE tracksolid_owner IN SCHEMA reporting GRANT SELECT ON TABLES TO analytics_ro;
ALTER DEFAULT PRIVILEGES FOR ROLE tracksolid_owner IN SCHEMA tracksolid GRANT SELECT ON TABLES TO analytics_ro;
ALTER DEFAULT PRIVILEGES FOR ROLE tracksolid_owner IN SCHEMA reporting GRANT EXECUTE ON FUNCTIONS TO analytics_ro;
+ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA tickets GRANT SELECT ON TABLES TO analytics_ro;
+ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA fuel GRANT SELECT ON TABLES TO analytics_ro;
+ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA tickets GRANT EXECUTE ON FUNCTIONS TO analytics_ro;
+ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA fuel GRANT EXECUTE ON FUNCTIONS TO analytics_ro;
-- Extra hardening over dashboard_ro: this role serves ad-hoc HUMAN queries.
ALTER ROLE analytics_ro SET default_transaction_read_only = on;
@@ -155,7 +169,7 @@ tools, and the auth:
analytics_mcp.py — Fireside Communications · Read-only Analytics MCP Server
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Hosted MCP server for the decision & analytics team. Exposes the fleet reporting
-data (reporting.* + tracksolid.*) to Claude as READ-ONLY query + introspection
+data (reporting.* + tracksolid.* + tickets.* + fuel.*) to Claude as READ-ONLY query + introspection
tools. Connects as the analytics_ro role; every query runs in a read-only txn
that is rolled back. Served over streamable HTTP behind Traefik with Bearer auth.
"""
@@ -179,7 +193,14 @@ log = _get_logger("server")
DATABASE_URL = os.environ["DATABASE_URL"] # set to the analytics_ro DSN by deploy
MAX_ROWS_CEIL = int(os.getenv("MCP_MAX_ROWS", "10000"))
-READABLE_SCHEMAS = ("reporting", "tracksolid")
+# Schemas the introspection helpers (list_tables/describe_table/sample_table) expose.
+# Override with MCP_READABLE_SCHEMAS — must stay in sync with the GRANTs in
+# analytics_ro_role.sql. The raw query() tool is bounded by the role's GRANTs, not this list.
+READABLE_SCHEMAS = tuple(
+ s.strip() for s in os.getenv(
+ "MCP_READABLE_SCHEMAS", "reporting,tracksolid,tickets,fuel"
+ ).split(",") if s.strip()
+)
# ── read-only connection pool ────────────────────────────────────────────────
# Force read-only + a statement timeout at the connection level (belt + braces;
@@ -234,7 +255,7 @@ mcp = FastMCP("fireside-analytics", stateless_http=True)
@mcp.tool()
def query(sql: str, max_rows: int = 1000) -> dict:
"""Run a read-only SELECT/WITH query against the fleet database.
- Only the reporting.* and tracksolid.* schemas are readable. Returns up to
+ Readable schemas: reporting.*, tracksolid.*, tickets.*, fuel.*. Returns up to
`max_rows` rows (default 1000, hard cap 10000). Auto-applies LIMIT if absent."""
stmt = _guard(sql)
cap = max(1, min(int(max_rows), MAX_ROWS_CEIL))
@@ -262,7 +283,7 @@ def list_schemas() -> list[dict]:
@mcp.tool()
def list_tables(schema: str) -> list[dict]:
- """List tables + views in a schema (reporting or tracksolid)."""
+ """List tables + views in a schema (one of READABLE_SCHEMAS)."""
if schema not in READABLE_SCHEMAS:
raise ValueError(f"schema must be one of {READABLE_SCHEMAS}")
with _ro_conn() as conn, conn.cursor() as cur:
@@ -338,6 +359,28 @@ app.add_route("/healthz", healthz, methods=["GET"]) # Starlette: add_route, no
> exact FastMCP app-factory method name against the installed `mcp` version
> (`streamable_http_app()` vs `http_app()`); the deploy command's `app` target must match.
+### 4.1 Readable data surface
+
+`analytics_ro` (and therefore the helper tools, via `READABLE_SCHEMAS`) can read:
+
+| Schema | Owner | What's there |
+|---|---|---|
+| `reporting` | `tracksolid_owner` | curated views/matviews (`v_daily_summary`, `v_trips`, `v_monthly_cost_centre`, …) + `fn_*` functions |
+| `tracksolid` | `tracksolid_owner` | raw ingestion tables (devices, positions, events, …) |
+| `tickets` | `postgres` | INC/CRQ tickets: `inc`, `crq`, `closure_events`, `inc_daily_snapshot`, `geo_clusters`, `geo_locations`, `inc_open_sla` (view) + 7 functions |
+| `fuel` | `postgres` | `records`, `ingest_state` + 7 functions |
+
+**Adding a schema later** is config-only: `GRANT USAGE/SELECT/EXECUTE …` to `analytics_ro`
+(persist it in `analytics_ro_role.sql`), then set `MCP_READABLE_SCHEMAS` to include it and
+redeploy. No code change. **Watch the owner** — `ALTER DEFAULT PRIVILEGES FOR ROLE …` must
+name the role that *owns* the schema's objects (e.g. `postgres` for `tickets`/`fuel`,
+`tracksolid_owner` for `reporting`/`tracksolid`) or future objects won't auto-grant.
+
+> **`tickets.inc` shape.** Each row carries both typed columns (`bucket`, `raw_status`,
+> `normalized_status`, `sla_status`, `region`, `cluster`, `owner`, `mttr`, `closed_at`,
+> `latitude`/`longitude`, `geog`/`geom`, …) **and** a `raw` text blob with the original
+> source fields. Query the **typed columns**, not `raw`.
+
---
## 5. Packaging — `Dockerfile` + `pyproject.toml`
@@ -369,10 +412,18 @@ the running stack, swaps in the `analytics_ro` credentials, and runs a standalon
Traefik-labelled bridge (the proven `dashboard_api` pattern). See the script header.
```bash
+# add/rotate a token (sets the full token list):
cd ~/fleetanalytics_mcp && git pull
MCP_AUTH_TOKENS="alice:$(openssl rand -hex 16)" bash deploy.sh
+
+# code-only redeploy (e.g. a schema/allowlist change): omit MCP_AUTH_TOKENS and the
+# script reuses the running container's existing tokens — no secret to re-type:
+cd ~/fleetanalytics_mcp && git pull && bash deploy.sh
```
+> Optional env: `MCP_READABLE_SCHEMAS` (default `reporting,tracksolid,tickets,fuel`) controls
+> which schemas the introspection helpers expose; `MCP_MAX_ROWS` (default 10000) the row ceiling.
+
---
## 7. Deploy runbook (ordered)
@@ -397,7 +448,8 @@ claude mcp list # should show "fireside-analytics: connected"
`Authorization: Bearer ` header.
Example session prompts: *"list the schemas"*, *"describe reporting.v_daily_summary"*,
-*"top 10 cost centres by distance in the last 30 days"* (the model writes the SELECT and
+*"top 10 cost centres by distance in the last 30 days"*, *"open INC tickets by region and SLA
+status from tickets.inc"*, *"MTTR by cluster this month"* (the model writes the SELECT and
calls `query`).
---
@@ -419,7 +471,7 @@ calls `query`).
## 10. Security notes
- **Four read-only layers:** role GRANTs · `default_transaction_read_only=on` (role + connection) · rolled-back txn · SQL keyword guard.
-- **Least privilege:** `analytics_ro` only has `USAGE`+`SELECT` on `reporting`/`tracksolid` and `EXECUTE` on `reporting` functions — no other schema, no write of any kind.
+- **Least privilege:** `analytics_ro` only has `USAGE`+`SELECT` on `reporting`/`tracksolid`/`tickets`/`fuel` and `EXECUTE` on those schemas' functions — no other schema, no write of any kind. (`infrastructure` and other schemas remain unreadable by design.)
- **Per-analyst tokens** make access revocable and queries attributable; rotate by editing `MCP_AUTH_TOKENS` and re-running the deploy (a recreate).
- **Resource guards:** `statement_timeout=30s`, idle-txn timeout, row cap (default 1000 / ceiling 10000) protect the DB from runaway analyst queries.
- **Future:** swap static Bearer tokens for OAuth (MCP supports it) if/when the team scales; consider a column-level deny-list if any PII lives in `tracksolid.*`.