No description
Find a file
kiania e1472adc3a infra(db-roles): dedicated non-superuser roles for the six apps on postgres
Six service connections run as the postgres SUPERUSER across two databases on the
shared 100-connection server — the root of the "too many connections" peaks and a
standing least-privilege risk. Superuser sessions ignore per-role CONNECTION LIMIT
and can consume the superuser-reserved slots.

Drafts (apply as postgres; nothing applied here):
- scripts/app_roles_tracksolid_db.sql — webhook_app, ingest_app, worker_app,
  dashboard_app. Capability groups (ts_app_read / ts_app_write), per-app NOSUPERUSER
  login roles with hard CONNECTION LIMIT + bounded GUCs (statement_timeout,
  idle_session_timeout, idle_in_transaction, lock_timeout).
- scripts/app_roles_fleet_platform.sql — gateway_app, cron_app (the apps on the
  separate fleet_platform DB), fp_app_rw group over its schemas.
- scripts/MIGRATE_APPS_OFF_SUPERUSER.md — runbook: discovery (what each app actually
  writes / whether it runs DDL), connection-budget table (sum ≈ 81 < 100), the
  object-ownership step for migration-running apps (reassign app schemas to the
  existing tracksolid_owner — scoped, never REASSIGN OWNED globally), one-at-a-time
  cutover, and instant rollback (DATABASE_URL only).

Grants are best-effort by app function and explicitly call out where to verify before
cutover; all objects are postgres-owned, so row DML works but DDL needs the ownership
step. See the runbook.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-19 23:51:52 +03:00
docs docs(analytics-mcp): document tickets + fuel schemas and MCP_READABLE_SCHEMAS 2026-06-17 11:37:25 +03:00
scripts infra(db-roles): dedicated non-superuser roles for the six apps on postgres 2026-06-19 23:51:52 +03:00
.dockerignore feat: read-only Fleet Analytics MCP server 2026-06-16 23:43:24 +03:00
.gitignore feat: read-only Fleet Analytics MCP server 2026-06-16 23:43:24 +03:00
analytics_mcp.py feat(access): expose tickets + fuel schemas to analytics_ro (read-only) 2026-06-17 11:37:25 +03:00
deploy.sh feat(access): expose tickets + fuel schemas to analytics_ro (read-only) 2026-06-17 11:37:25 +03:00
Dockerfile feat: read-only Fleet Analytics MCP server 2026-06-16 23:43:24 +03:00
pyproject.toml feat: read-only Fleet Analytics MCP server 2026-06-16 23:43:24 +03:00
README.md fix: disable MCP DNS-rebinding host guard behind reverse proxy 2026-06-17 00:00:40 +03:00

Fleet Analytics MCP (read-only)

A read-only MCP server that lets the decision & analytics team query the Fireside fleet database (tracksolid_db — PostgreSQL 16 + TimescaleDB + PostGIS) directly from Claude — for reporting and decisions, never edit/delete.

It exposes a guarded general SELECT tool plus schema-introspection tools over the reporting.* (curated analytics layer) and tracksolid.* (raw telemetry) schemas, connecting as a dedicated least-privilege analytics_ro role. It is hosted on the same Coolify host as the database (the DB is internal-only and not reachable from a laptop), and authed with per-analyst Bearer tokens.

Sibling of the tracksolid_timescale_grafana_prod backend (the DB/ingestion stack) and the dashboard_api read bridge. This repo owns only the analytics MCP server and its analytics_ro role.

Read-only is enforced at four layers

  1. Role GRANTsanalytics_ro has only USAGE+SELECT on reporting/tracksolid and EXECUTE on reporting functions; no INSERT/UPDATE/DELETE, not the matview owner.
  2. default_transaction_read_only = on — set on the role and on every connection.
  3. Rolled-back transactions — every query runs in a txn that is rolled back, never committed.
  4. SQL guard — the query tool accepts a single SELECT/WITH statement only and rejects write/DDL keywords (clean errors instead of DB faults).

It deliberately does not import the backend's ts_shared_rev, so it carries none of the Tracksolid ingestion secrets — it needs only DATABASE_URL + MCP_AUTH_TOKENS.

Tools

Tool Purpose
query(sql, max_rows=1000) guarded read-only SELECT/WITH; auto-LIMIT; returns {row_count, truncated, rows}
list_schemas() readable schemas + 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
sample_table(schema, table, n=20) first n rows (wrapper over query)

Layout

analytics_mcp.py                 # the MCP server (FastMCP streamable-HTTP; uvicorn target analytics_mcp:app)
Dockerfile                       # Coolify-buildable image (port 8892)
deploy.sh                        # manual host deploy (standalone Traefik bridge) — fallback to Coolify
scripts/analytics_ro_role.sql    # the read-only role DDL
scripts/bootstrap_analytics_ro.sh# host bootstrap: generate pw → apply role SQL
docs/ANALYTICS_MCP.md / .html    # full implementation guide + runbook

Deploy

The DB is internal-only, so the server runs on the same Coolify host as timescale_db.

0. Create the read-only role (once, on the host):

scp scripts/analytics_ro_role.sql scripts/bootstrap_analytics_ro.sh kianiadee@twala.rahamafresh.com:~/
ssh kianiadee@twala.rahamafresh.com 'bash ~/bootstrap_analytics_ro.sh'   # writes ~/.analytics_ro.pw (0600)

1a. Coolify-managed app (recommended): create a Coolify application from this repo (Forgejo repo.rahamafresh.com/kianiadee/fleetanalytics_mcp.git), Dockerfile build, app port 8892, attach the domain fleetmcp.rahamafresh.com (prod) / fleetmcp.fivetitude.com (staging). Set as secrets:

  • DATABASE_URL=postgresql://analytics_ro:<pw>@timescale_db:5432/tracksolid_db
  • MCP_AUTH_TOKENS=alice:<tok>,bob:<tok> (per-analyst)

Then connect the app to the network that can reach timescale_db (the tracksolid stack's network) so the timescale_db hostname resolves. Coolify manages the Traefik labels + TLS from the domain you set.

Optional env (sensible defaults):

  • MCP_MAX_ROWS (default 10000) — hard ceiling on rows a query may return.
  • MCP_DNS_REBINDING_PROTECTION (default 0/off) — the MCP SDK's localhost Host-header guard. It returns 421 behind a reverse proxy, so it's off by default (the service is TLS-terminated + Bearer-authed). Set 1 to enforce, with MCP_ALLOWED_HOSTS.
  • MCP_ALLOWED_HOSTS — comma-separated allowlist used only when the guard is on (default: the two fleetmcp.* domains + localhost).

1b. Manual host deploy (fallback): check this repo out on the host and run deploy.sh — it builds the image, derives the read-only DATABASE_URL from the running stack, and runs a standalone Traefik bridge. See the script header.

Add to Claude (for analysts)

claude mcp add --transport http fireside-analytics https://fleetmcp.rahamafresh.com \
  --header "Authorization: Bearer <your-token>"
claude mcp list      # → "fireside-analytics: connected"

Claude Desktop / claude.ai: add a custom connector with the same URL and Authorization: Bearer <your-token> header.

Local dev

uv sync
DATABASE_URL=postgresql://... MCP_AUTH_TOKENS=me:dev uv run uvicorn analytics_mcp:app --port 8892

Full reference, security notes, and verification checklist: docs/ANALYTICS_MCP.md.