MCP Diagnostic Server
A read-only MCP server (services/mcp_diagnostics) that lets an AI agent
triage the two most common tracker support complaints directly, instead of an
engineer running ad hoc queries:
- A tracker hasn't reported for a few days.
- A tracker hasn't shown as arrived in a delivery/storage zone, even though the customer knows it has physically arrived.
Diagnosing either usually comes down to two questions: has Apple actually seen the device recently, and is the zone/geofence itself configured where the customer thinks it is. The server has read-only access to the database and, for the "has Apple seen it" question, can make a live call to Apple.
Access
Local dev: an MCP container with direct read credentials in the local docker-compose setup. No access-control problem worth solving there.
Staging/Production: rather than an always-on service (a new public/ALB-fronted endpoint or a persistent bastion — more attack surface than a rarely-used diagnostic tool justifies, for something used on-demand rather than continuously), it runs as a one-off ECS task, started only when needed, reached over an SSM Session Manager port-forward. No open ports, no bastion host, no VPN — access is gated entirely by IAM identity plus the database role's own grants.
This reused infrastructure that already existed in this repo rather than adding new plumbing:
infra/modules/ecs/variables.tf'sjobsmap for one-off task definitions (already used formigrationsandseed-users), launched withaws ecs run-taskrather than as a long-running service.infra/modules/ecs/main.tf(aws_iam_role_policy.task_exec) already grants the shared ECS task rolessmmessages:CreateControlChannel/OpenDataChanneletc. to every task — exactly what ECS Exec / SSM port forwarding needs, so no IAM change was needed for the transport itself.scripts/open_tracker_ecs_shell.sh's lifecycle (run-task --enable-execute-command, poll until the exec agent isRUNNING, open a session,stop-taskin anEXITtrap) was forked intoscripts/open_tracker_mcp_tunnel.sh, swapping the finalaws ecs execute-command --command /bin/shstep foraws ssm start-session --document-name AWS-StartPortForwardingSession --target ecs:<cluster>_<task-id>_<runtime-id> --parameters portNumber=<mcp-port>,localPortNumber=<local-port>. The agent then points its MCP client atlocalhost:<local-port>.- The existing NAT gateway (
infra/modules/network/main.tf) already gives private-subnet tasks the egress needed to reach the SSM endpoints for the port-forwarding data channel.
What was built on top of that:
jobs["mcp-diagnostics"]in bothinfra/envs/staging/main.tfandinfra/envs/prod/main.tf, reusing the existingtracker-servicesimage (just a differentcommand) and the existing ECS security group.- A dedicated read-only
tracker_readonlyPostgres role, created byalembic/versions/29938722c984_create_mcp_readonly_role.py(SELECT-only, including future tables viaALTER DEFAULT PRIVILEGES), backed by a manually-managedmcp_readonly_passwordsecret (see Manual Bootstrap below — the role can't be created by the app's own migrations and the password is deliberately not Terraform-managed). scripts/open_tracker_mcp_tunnel.sh— forksopen_tracker_ecs_shell.sh, swapping the interactive shell step foraws ssm start-session --document-name AWS-StartPortForwardingSession.aws_iam_policy.mcp_diagnostics_accessin both envs, scopingecs:RunTaskto themcp-diagnosticstask-definition family andssm:StartSession/ecs:ExecuteCommandto that cluster's tasks only. Not attached to anything by Terraform — attach it to the relevant engineer/CI IAM role/user out of band.
Not done: a self-idle-timeout inside the MCP container as a backstop if a
session is killed uncleanly and the script's EXIT trap doesn't fire (Fargate
bills while the task is running). Left for later — a cost/hygiene concern,
not a security one.
Tools
Grounded in the schema and existing diagnostic code/endpoints:
Lookup
find_tracker(query)— resolve a MAC address (with or without separators) or name substring to atracker_id. Added after actually needing raw DB access mid-investigation to look up a tracker from a MAC address alone - every other tool takestracker_id, but that's rarely what support starts with.
"Hasn't reported for days"
get_tracker_reporting_status(tracker_id)—last_report_received,last_processed_at,last_status_check,current_status, and the sameCOALESCE(last_report_received, max(location_history.timestamp))checkapp/api/routes/trackers/dark.py(GET /trackers/gone-dark) already uses.get_daily_report_counts(tracker_id, days)— wraps the existingGET /trackers/{id}/daily-reportslogic; shows whether reports trailed off gradually or stopped dead.check_apple_live(tracker_id)— a live call to Apple via thefindmylibrary, asscripts/check_tracker_activity.pyalready does, bypassing our DB. This is what separates "Apple genuinely hasn't seen this device" (customer-side) from "Apple has seen it but our pipeline hasn't ingested it yet" (our bug).check_fetcher_health(tracker_id)— is the tracker actually being polled (last_processed_atrecency / round-robin position), or stuck behind a distributed lock (scripts/inspect_tracker_locks.py).check_duplicate_advertisement_key(tracker_id)— wrapsscripts/diagnose_duplicate_trackers.sql; two trackers sharing ahashed_advertisement_keyis a real recurring failure mode.
"Not in the zone we expected"
get_current_location(tracker_id)— latestlocation_historyrow (lat/lng, confidence, accuracy, timestamp).get_status_history(tracker_id)—status_historytransitions.get_geofence_events(tracker_id)—geofence_eventsentry/exit/dwell records against delivery/storage locations, with confidence.get_zone_config(zone_id)— the delivery/storage location's registered lat/lng andgeofence_size_meters, for the case where the zone itself is mis-registered rather than the tracker being lost.list_nearby_zones(tracker_id, radius)— given the tracker's actual location, which zones exist nearby that didn't match. Surfaces "radius too small" or "coordinates off by 200m" directly.find_status_zone_mismatches(limit)— fleet-wide scan for trackers physically inside a delivery/storage geofence (scoped to their own production run, or a global zone) whosecurrent_statushasn't caught up — e.g. sitting inside a storage zone while stillIN_TRANSIT. See Known Limitations below for what this turned up.
Bundle
get_tracker_overview(tracker_id)— combines reporting status, current location, and status history into the single call that should cover most support triage.
All 12 tools are live in local dev, staging, and (once applied) prod.
Status
- Local dev: fully working, verified live against real dev data through
the actual MCP protocol (all tools except
check_apple_live, which needs a real Apple session this sandbox doesn't have). - Staging: fully verified end-to-end (2026-07-23) — secret created,
tracker_readonlyrole created on the real DB host,scripts/open_tracker_mcp_tunnel.shstarted a real one-off ECS task, opened a real SSM port-forward, and a real MCP client connected through it - all 12 tools present,find_status_zone_mismatchesreturned real staging data (6 trackers stuckIN_TRANSIT~50-72m inside a zone named "Test Storage Location #1" - worth checking whether that zone is misregistered as adelivery_locationsrow when the name suggests storage). Tunnel torn down cleanly, task wentRUNNING→DEPROVISIONING→STOPPEDvia the script'sEXITtrap - no orphaned task left billing. - Prod: infra defined but not yet applied/bootstrapped. See Next Steps.
Two real infra bugs turned up applying this to staging (not caught by local dev, since its Postgres container runs everything as superuser):
scripts/run_staging_migrations.shused aterraform output -raw >fileread -r VAR <filepattern forCLUSTER_ARN/TASK_DEFINITION_ARN/LOG_GROUP_NAME.terraform output -rawdoesn't emit a trailing newline, soreadreturned non-zero on EOF andset -ekilled the script right after the first read - before ever callingaws ecs run-task. Fixed by switching those three to direct command substitution (VAR="$(terraform ...)"), matching the patternscripts/open_tracker_ecs_shell.shalready used safely.- The migration itself then failed:
psycopg.errors.InsufficientPrivilege: permission denied to create role. Staging'strackerapp role deliberately doesn't haveCREATEROLE. Fixed by making role creation a one-time manual step, not part of the migration chain — see Manual Bootstrap below.
check_apple_live mounts the same EFS-backed account.json the fetcher
services use (read-only, so it can never race the fetcher's periodic
token-refresh writes), reusing the existing Apple session instead of needing
its own login. This does mean it shares Apple's rate limits and session state
with the production fetcher - "use sparingly, not in a loop", same as local
dev.
Manual Bootstrap (staging/prod)
The tracker_readonly role can't be created by the app's own migrations (see
above), and its password is deliberately not managed by Terraform -
mcp_readonly_password_secret_arn in each env's terraform.tfvars is a
plain out-of-band ARN, same pattern as google_client_id_secret_arn. Do this
once per environment:
- Create the secret with a password of your choosing:
If it already exists (e.g. left over from before this section existed, when it was briefly Terraform-managed), update its value instead:
aws secretsmanager create-secret \ --name tracker/staging/mcp-readonly-password \ --secret-string '<password>' \ --profile glimpse-staging --region eu-west-2aws secretsmanager put-secret-value --secret-id tracker/staging/mcp-readonly-password --secret-string '<password>'. If Terraform still has it in state from that earlier attempt, remove it first so a futureapplydoesn't destroy it:terraform state rm 'module.secrets.aws_secretsmanager_secret.this["mcp_readonly_password"]'(and the matching..._versionandrandom_passwordentries). - Put its ARN into
infra/envs/staging/terraform.tfvarsasmcp_readonly_password_secret_arn, thenterraform apply. - SSM onto the database host (
terraform output -raw database_instance_id) and create the role as the Postgres superuser, using the same password:aws ssm start-session --target <instance-id> --profile glimpse-staging --region eu-west-2 sudo -u postgres psql -d tracker(Same SQLCREATE ROLE tracker_readonly WITH LOGIN PASSWORD '<same password>'; GRANT CONNECT ON DATABASE tracker TO tracker_readonly; GRANT USAGE ON SCHEMA public TO tracker_readonly; GRANT SELECT ON ALL TABLES IN SCHEMA public TO tracker_readonly; ALTER DEFAULT PRIVILEGES FOR ROLE tracker IN SCHEMA public GRANT SELECT ON TABLES TO tracker_readonly;build/db/create-readonly-role.shruns automatically for local dev - just run manually here since the app role can't run it itself.)
Known Limitations
- No "expected zone" is stored anywhere — not on
Tracker, not onProductionRun. So "didn't arrive where expected" can only be answered as "here's where it actually is and what zones are nearby," not "here's why it missed its target," unless that expectation lives in a client's own system outside this DB. - Status/zone containment bug: a tracker can sit inside the correct
zone's geofence with
current_statusstillIN_TRANSITinstead ofDELIVERED/IN_STORAGE- a status-update pipeline bug, not a missing-expectation problem.find_status_zone_mismatchesdetects this directly and found 4 real cases in dev data and 6 in staging (see Status above). Tracked as a follow-up in docs/development/TODO.md.
Next Steps
- Attach
aws_iam_policy.mcp_diagnostics_accessto whichever engineer/CI role should be allowed to use it - it isn't attached to anything yet. - Apply to prod once its secret/role are bootstrapped the same way as staging.