From aae33fb59017f5db1f85d86479a2e508270de106 Mon Sep 17 00:00:00 2001 From: david kiania Date: Wed, 17 Jun 2026 11:35:16 +0300 Subject: [PATCH] docs(analytics-mcp): document tickets + fuel schemas and MCP_READABLE_SCHEMAS Reflect the live state: readable data-surface table (reporting/tracksolid/ tickets/fuel + owners), the owner-keyed default-privilege gotcha, the tickets.inc typed-vs-raw column note, the env knob, code-only redeploy that reuses tokens, and tickets example prompts. Status flipped to deployed & live. Co-Authored-By: Claude Opus 4.8 --- docs/ANALYTICS_MCP.html | 59 ++++++++++++++++++++++++----- docs/ANALYTICS_MCP.md | 82 +++++++++++++++++++++++++++++++++-------- 2 files changed, 117 insertions(+), 24 deletions(-) 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.

analytics_mcpuvicorn :8892 · coolify net
role = analytics_ro · READ ONLY
-
timescale_db:5432tracksolid_db
reporting.* · tracksolid.*
+
timescale_db:5432tracksolid_db
reporting.* · tracksolid.* · tickets.* · fuel.*

Ports in use: 8890 prod dashboard_api · 8891 staging dashboard_api · 8892 analytics_mcp.

@@ -92,15 +92,23 @@ superuser (it does CREATE ROLE), with the password supplied as 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; +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; -- matview (not in ALL TABLES) GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA reporting TO analytics_ro; --- future objects auto-grant +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA tickets TO analytics_ro; +GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA fuel TO analytics_ro; +-- future objects auto-grant; key to the OWNER role (postgres for tickets/fuel) 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; ALTER ROLE analytics_ro SET statement_timeout = '30s'; @@ -118,7 +126,7 @@ Tracksolid ingestion secrets, which this read-only server has no business holdin - + @@ -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:

+
ToolPurpose
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
+ + + + + +
SchemaOwnerWhat's there
reportingtracksolid_ownercurated views/matviews (v_daily_summary, v_trips, v_monthly_cost_centre, …) + fn_* functions
tracksolidtracksolid_ownerraw ingestion tables (devices, positions, events, …)
ticketspostgresINC/CRQ tickets: inc, crq, closure_events, inc_daily_snapshot, geo_clusters, geo_locations, inc_open_sla (view) + 7 functions
fuelpostgresrecords, 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.*`.