Cartrack Fleet MCP Server
by Procon-Group
README.md
# Cartrack Fleet MCP Server — Procon Fleet
Wraps Cartrack's Fleet API (`developer.cartrack.com`) as an MCP server for interactive use in
Claude Code, plus two standalone CLI scripts that a cloud Routine runs on a schedule:
- `sync:daily` — pulls the previous 24h of vehicle status, trips, and fuel data for the whole
fleet and appends it to the **Procon Fleet — Live Dashboard** Google Sheet.
- `sync:monthly` — compares that month's Cartrack-metered fuel per vehicle against the fleet
card's litres/Rand already merged into the master workbook, and flags mismatches.
Endpoints, parameters, pagination, and rate limits below were confirmed against the live
OpenAPI spec (`https://developer.cartrack.com/openapi/openapi.yaml`, v1.26.0824.1) — not
guessed. See "Things to double-check with real data" at the bottom before trusting output.
## 1. Setup
```bash
npm install
cp .env.example .env # then fill in the values below
npm run build
```
### Cartrack credentials
`CARTRACK_USERNAME`/`CARTRACK_PASSWORD` are **deliberately optional** (see `CartrackConfig` in
`src/cartrackClient.ts`) and should stay blank in `.env` and `.env.example` — real credentials
never belong in this repo, tracked or not. `CARTRACK_BASE_URL` is not secret and stays filled
in (`https://fleetapi-na.cartrack.com/rest` — confirmed from Cartrack's docs: Namibia has its
own country code `na`, not South Africa's; Cartrack has ~26 per-country hosts total, all
shaped `https://fleetapi-<cc>.cartrack.com/rest`).
This machine keeps real credentials in a separate vault, not in this repo:
`C:\Users\Quinton\Desktop\Claude Second Brain\API's\Enviroment secrets` (read that folder's
own `CLAUDE.md`/`README.md` first). For a **local** run:
```powershell
& "...\Enviroment secrets\Load-Secrets.ps1" cartrack -Quiet # loads into this shell session
node --env-file=.env dist/scripts/syncDaily.js
```
For the **cloud Routine**, Cartrack access is not an environment variable at all — see
"API credentials for cloud Routines" below.
### Google Sheets/Drive (service account — no OAuth, works unattended)
1. In Google Cloud Console: create a project (or reuse one), enable the **Google Sheets API**
and **Google Drive API**.
2. Create a service account, generate a JSON key, save it as
`google-service-account.json` in this folder (already gitignored).
**Important**: service accounts have **zero personal Drive storage quota** under Google's
current policy — they cannot create new files (a new Sheet, a new dated monthly `.xlsx`
export) at all unless the file lives inside a Google Workspace **Shared Drive**, where
storage is billed to the Drive rather than the account. This shows up as a
`403 "The caller does not have permission"` error that has nothing to do with API enablement
or auth being wrong — confirmed the hard way against a real project. Two paths from here,
depending on whether you have a Shared Drive available:
**Path A — you have a Shared Drive** (or can create one):
3. Google Drive -> New -> Shared Drive, e.g. "Procon Fleet Dashboard", then add the service
account's `client_email` as a member with **Content Manager** access.
4. Grab the Shared Drive's ID from its URL when you open it
(`drive.google.com/drive/folders/<id>`).
5. Run the setup script — creates the dashboard Sheet inside that Shared Drive, shares it
with your own account, and pre-creates every tab:
```bash
npm run setup:sheet -- <sharedDriveId> you@procongroup.co
```
It prints the new Sheet's ID — set that as `GOOGLE_SHEET_ID` in `.env`.
6. For the monthly `.xlsx` export as a new dated file each month, create a folder inside the
same Shared Drive and set `GOOGLE_EXPORT_FOLDER_ID` to its ID.
**Path B — no Shared Drive available** (e.g. Workspace access issues, or plain personal
Gmail): a human-owned file has normal storage quota, so writing rows into a Sheet *you*
already created and shared isn't "creating a file" in the quota sense — only the service
account creating a brand-new file hits the wall.
3. Create the "Procon Fleet — Live Dashboard" Sheet yourself (sheets.new), share it with the
service account's `client_email` as **Editor**.
4. Set `GOOGLE_SHEET_ID` to that Sheet's ID (the long string in its URL).
5. Run `npm run setup:tabs` to pre-create every tab with headers.
6. For the monthly `.xlsx` export, create one placeholder `.xlsx` file yourself (any content),
share it with the service account as Editor, and set `GOOGLE_EXPORT_FILE_ID` to its ID.
Its content gets overwritten with the latest month's numbers each run — one recurring file
with the latest snapshot, not a dated file per month (that trade-off is the price of not
having a Shared Drive; switch to Path A later if that changes).
Tabs (`Vehicle Status`, `Trips`, `Fuel`, `Flags`, `Monthly Reconciliation`) are created by
either setup script; `ensureTabs()` also creates any that are still missing on every sync
run, so it's safe if you add a tab by hand later.
### Registering the MCP server with Claude Code (local/interactive use)
```bash
claude mcp add cartrack-fleet -- node "<absolute-path>/dist/server.js"
```
Claude Code will pick up `.env` if you run it from this directory, or set the four
`CARTRACK_*`/`GOOGLE_*` variables in your shell profile / MCP server config instead.
## 2. The four tools
| Tool | Cartrack endpoint(s) | Notes |
|---|---|---|
| `list_vehicles` | `GET /vehicles` | Auto-paginated. No filters by default — whole fleet. |
| `get_vehicle_status` | `GET /vehicles/status` | Live snapshot only, no date range. Rate-limited by Cartrack to 60 req/min. |
| `get_trips` | `GET /trips` (fleet) or `GET /trips/{registration}` (one vehicle) | Defaults to previous 24h. Cartrack caps each request at 31 days — longer ranges are chunked automatically. The fleet-wide endpoint has no vehicle filter at the API level; pass `registration` to query one vehicle. |
| `get_fuel_data` | `POST /fuel/consumed` + `POST /fuel/level` (bulk, ≤24h, ≤100 vehicles) or the per-vehicle `GET` equivalents (≤31 days) | Defaults to previous 24h fleet-wide, which fits the bulk endpoints in one call each. Wider ranges/single vehicle fall back to per-vehicle calls, paced to Cartrack's 10 req/min cap on the bulk endpoints. |
## 3. Test before scheduling anything
Run each tool manually first, against the real account:
```bash
npm run dev # starts the MCP server over stdio — drive it from Claude Code, or:
```
Or call the underlying script paths directly for a quick sanity check without an MCP client —
e.g. add a throwaway `node -e` snippet importing `CartrackClient`.
Specifically confirm, before scheduling the daily Routine:
1. **Vehicle count matches Procon's actual fleet** (~41 vehicles, Electrical + Steel). If
`list_vehicles` returns a different count, check for vehicles marked
`is_under_maintenance` or decommissioned units still in Cartrack's system.
2. **Timestamps line up with Namibia local time (UTC+2)**. The OpenAPI spec's `date` schema
(`"2023-01-01 12:00:00"`) carries no timezone marker — it's genuinely ambiguous from the
spec alone whether Cartrack expects/returns UTC or each terminal's local time. Call
`get_vehicle_status` for a vehicle you can see in person right now and compare
`location.updated` / `event_ts` against the actual wall-clock time. If it's off by 2
hours (or by the vehicle's DST-naive local offset), adjust `dateWindow.ts` and the
request-building code in `cartrackClient.ts` accordingly — right now both assume the API
wants/returns values already in local time in that plain format.
3. **A vehicle's registration format matches the Fleet Register** in the fuel tracking
workbook exactly (Cartrack vs. the workbook sometimes differ in spacing/hyphenation) —
this matters for `syncMonthly.ts`'s vehicle matching.
## 4. Daily Routine (cloud, ~6am Namibia time)
Namibia is UTC+2 year-round (no DST), so 6am local = **04:00 UTC**. Cron for the Routine:
```
0 4 * * *
```
The Routine's prompt should be effectively: *"Run `npm run sync:daily` in this repo and
report the console output."* Since the whole sync — Cartrack calls, flag computation, and
Sheet writes — happens inside the script (not via separate MCP tool calls from the cloud
agent), the Routine just needs `Bash` access and the environment variables below.
### Dashboard regeneration + publish (both Routines, since 2026-09-01)
Both the daily and monthly Routines also run `npm run dashboard:generate && npm run
dashboard:build`, then call the **Artifact** tool to republish `dashboard/preview.html` in
place over the live "Procon Fleet Fuel Dashboard" artifact
(`https://claude.ai/code/artifact/3b674af3-5d68-4117-9c66-0209d9b8bbd0`) — same `title` and
`favicon` every time (`"Procon Fleet Fuel Dashboard"` / `🚚`), so it updates rather than
forking into a new artifact. This is a second, independent step in each Routine's prompt —
it still runs even if the sync/reconciliation half fails.
**Confirmed live (not assumed) that a Routine session can call the Artifact tool** by firing
a disposable diagnostic prompt through the daily trigger with `session_context.allowed_tools`
temporarily set to `["Bash", "Artifact", "Skill"]` — it successfully listed the existing
artifact and loaded the `artifact-design` skill. Routines default to `["Bash"]` only, so
`allowed_tools` must include `Artifact` and `Skill` for this to work — check
`job_config.ccr.session_context.allowed_tools` if the publish step starts failing with a
"tool not available" style error.
**No mode-splitting**: `generateDashboard.ts` doesn't distinguish a cheap daily run from an
expensive monthly one — every run refetches the full 3-month per-vehicle window that backs
Cost/KM, Fuel Efficiency, and the Vehicle Report's Cartrack columns (~26 vehicles × 3 months
of trips), even though that window's fleet-card side (`existing-fleet-data.json`) only
changes when someone manually re-extracts it from the master workbook — so most days it
refetches data whose comparison values won't have moved. Deliberately kept simple rather than
adding a `DASHBOARD_MODE=daily|full` flag with carry-forward-from-previous-run logic; revisit
if the daily API call volume becomes a real cost/rate-limit concern.
If `dashboard:generate` fails, the Routine prompt tells it to retry once, then skip the
publish for that run rather than fail the whole Routine — a transient
`ConnectTimeoutError` to `fleetapi-na.cartrack.com` has already happened once in testing and
is not a code bug.
### API credentials for cloud Routines — current status: not available yet
A Routine's *environment variables* are readable by anyone who uses that Environment on
claude.ai — fine for `GOOGLE_SHEET_ID`, not fine for a password. Anthropic's agent proxy
documents a mechanism for exactly this case: an **API credential**, attached to outbound
requests for hosts you list *after the request leaves the session*, so the key never reaches
the agent or the run log — see [Configure cloud
environments](https://code.claude.com/docs/en/cloud-environments#add-api-credentials).
**Confirmed against this account on 2026-09-01: the feature doesn't actually appear in the
UI.** Neither the "New cloud environment" dialog nor the "Edit cloud environment" dialog for
an existing environment (hover the environment in the selector → gear icon on hover) shows an
API credentials section, even though the docs say Pro/Max users hold the required role
automatically and should see one. Checked both the create and edit flows directly — this is
very likely a documented feature still in staged/research-preview rollout, not a
misconfiguration on this account. **Check for it again before assuming it's still
unavailable** — hover the environment, click the gear, look below "Environment variables."
**Until it appears, Cartrack auth uses plain Environment variables instead** — the pragmatic
fallback, accepted deliberately (see the "Use plain Environment variables now" decision) given
this is a personal (not team/org) account, so "anyone using this environment" is just the
account owner. Set these on the Environment (via the gear-icon edit dialog, `.env` format):
```
CARTRACK_USERNAME
CARTRACK_PASSWORD
CARTRACK_BASE_URL
GOOGLE_SHEET_ID
GOOGLE_APPLICATION_CREDENTIALS_CONTENTS (raw JSON contents — the Environment can't see a
local file path, so this has to be the file's actual content, not a path. The Routine's
prompt tells the agent to write it to ./google-service-account.json, export
GOOGLE_APPLICATION_CREDENTIALS to that path, and delete the file again after the sync)
FLEET_TIMEZONE_OFFSET_MINUTES=120
ENABLE_GEOFENCE_FLAG=false
```
`CartrackConfig` in `src/cartrackClient.ts` treats `username`/`password` as optional — if API
credentials becomes available later, unset these three from the Environment, add the API
credential instead, and the client automatically stops building its own `Authorization` header
and relies on the proxy-injected one. No code change needed either way.
Also set **Network access** to **Custom** and add to **Allowed domains** (needed regardless of
which auth path Cartrack ends up using): `fleetapi-na.cartrack.com` (Cartrack) and
`sheets.googleapis.com`, `www.googleapis.com`, `oauth2.googleapis.com` (Google Sheets/Drive —
the `googleapis` package hits all three). The default allowlist doesn't include any of these;
requests to a domain not listed fail with `403`.
**Getting the actual credential values into the browser safely**: don't type or paste secret
values through an AI-driven browser session — its clipboard is sandboxed and can't see the
real OS clipboard anyway, so this doesn't actually work, and typing them directly would put
the raw value in that session's own transcript. Build the `.env`-format block locally instead
(reads from the vault, never printed):
```powershell
Set-Location "C:\Users\Quinton\Desktop\Claude Second Brain\API's\Enviroment secrets"
.\Load-Secrets.ps1 cartrack -Quiet
Set-Clipboard -Value "CARTRACK_USERNAME=$env:CARTRACK_USERNAME`nCARTRACK_PASSWORD=$env:CARTRACK_PASSWORD`nCARTRACK_BASE_URL=$env:CARTRACK_BASE_URL"
```
then paste into the Environment variables box in your own real browser.
### What it writes
- `Vehicle Status`, `Trips`, `Fuel` — **appended**, one row per vehicle per sync, so history
accumulates day over day.
- `Flags` — **replaced** each run (it's a snapshot of today's issues, not a log):
- *Idle all day*: no trips in the last 24h and ignition currently off, checked only during
work hours (07:00–17:00 local — adjust `WORK_HOURS_START`/`END` in `syncDaily.ts` if
Procon's hours differ).
- *No job-site match*: a trip where Cartrack matched no geofence at either end
(`start_geofence_name`/`end_geofence_name` both empty). This only works if geofences are
set up in Fleetweb for Procon's job sites — if none exist yet, every trip will flag, which
isn't useful. Set up geofences first, or treat this flag as informational until then.
- *Fuel anomaly*: today's fuel consumed deviates >50% from that vehicle's trailing 30-day
average (needs ≥5 days of history before it starts flagging). This 50% threshold is a
day-to-day noise filter, unrelated to the monthly 5% fleet-card tolerance below — tune
`FUEL_ANOMALY_DEVIATION` in `syncDaily.ts` if it's too noisy or too quiet.
## 5. Monthly reconciliation
**Design choice, read before changing anything**: `syncMonthly.ts` only *reads* the master
"Procon Electrical & Steel Fleet Fuel Tracking System" workbook — it never writes to it. That
workbook's formulas, charts, and recalculation order are fragile (see the
`fleet-cost-workbook` skill's warnings about `openpyxl` and `recalc.py`), and this runs
unattended in the cloud with nobody watching a broken chart happen. So instead:
- The comparison (Cartrack litres vs. fleet-card litres/Rand per vehicle, this month) is
written to a new **`Monthly Reconciliation`** tab in the live Google Sheet — additive,
reversible, safe to overwrite. Re-running for the same month replaces just that month's
rows (idempotent).
- Vehicles beyond `CARTRACK_VARIANCE_TOLERANCE` (default 5%, per your instruction) get
`Flagged = YES`.
- If you want the numbers folded into the master workbook's actual Dashboard, that's still a
manual step — same fleet-cost-workbook merge process as today, just with the Cartrack side
of the comparison already computed and sitting in the Sheet ready to copy across.
**Requires the master workbook to be in Google Drive** (not just local disk) — set
`MASTER_WORKBOOK_DRIVE_FILE_ID` to its Drive file ID. A cloud Routine can't reach a file
that only exists on your machine.
**Column matching is a best-effort guess** (`readWorkbookFuelTotals` in `syncMonthly.ts`
looks for columns containing "date", "registration", "litre", and "amount"/"rand" in the
`Fuel Log` tab's header row). Check `references/workbook-structure.md` (in the fleet-cost-
workbook skill) against the actual header row before trusting this, and adjust the column-
matching if it's wrong — a silent mismatch here produces confidently wrong numbers.
Suggested schedule: monthly, a few days after the statement typically lands (adjust to
Procon's actual billing cycle) — e.g. the 5th at 6am Namibia time:
```
0 4 5 * *
```
### Monthly .xlsx export
If `GOOGLE_EXPORT_FOLDER_ID` is set, `syncMonthly.ts` exports the whole live Sheet to `.xlsx`
and uploads it to that Drive folder as `Procon Fleet — Live Dashboard - YYYY-MM.xlsx` — the
literal Excel snapshot you asked for, alongside the Sheet itself. Leave it unset to skip.
### Refreshing the dashboard's own fleet-card data (separate from the above)
The above is the Google *Sheet's* Monthly Reconciliation tab. The **dashboard artifact** gets
its fleet-card side (fuel litres/Rand, tyres/service expenses, the Fleet Register) from
`dashboard/existing-fleet-data.json` — a static extraction baked into every published dashboard
build, not queried live. It only updates when someone re-runs the extraction against a newer
copy of the master workbook (after that month's statement has been merged in via the
`fleet-cost-workbook` skill's normal process — this step is downstream of that, not a
replacement for it):
```bash
python3 dashboard/extract-workbook.py "<path to the updated .xlsx>" dashboard/existing-fleet-data.json
npm run dashboard:generate && npm run dashboard:build
```
Then publish `dashboard/preview.html` over the live artifact as usual. `extract-workbook.py`
reads the workbook's Fuel Log, Vehicle Expenses and Fleet Register tabs directly with
`openpyxl` (`data_only=True` — never writes to the workbook) and derives everything else
(monthly per-vehicle totals, all-time totals, `km` from each vehicle's own odometer-reading
spread, `costPerKm`) the same way the original hand-built JSON did — verified once by diffing
its output against the previous `existing-fleet-data.json` before trusting it. This matters
because Cost/KM, Fuel Efficiency and Cost to Company's target month is *whichever month has
real fuel cost in this file* — until this file is refreshed, those tabs keep reconciling
against a stale month even while the Routines pull fresh Cartrack data every day.
## 6. Things to double-check with real data
- **`max_speed` units** — the `/trips` schema example suggests meters/second, while the
vehicle-level `max_speed` in `/vehicles` looks like km/h. Not used in any flag logic yet;
verify before displaying it anywhere user-facing.
- **Trip-window overlap** — Cartrack's docs note a trip active at any point in a requested
window is returned in full even if its own timestamps fall outside it. Don't sum
`trip_distance` across days expecting an exact daily total; if that's ever needed, use the
dedicated odometer endpoint instead.
- **`get_vehicle_status` has no pagination** — for a ~41-vehicle fleet this is fine, but if
the fleet grows a lot, revisit.
TDQS
A4.4/5.0
Scored across 4 tools
Disambiguation5/5
Each tool targets a distinct resource and data type: live vehicle status, the full vehicle list, trip history, and fuel data. No overlap or ambiguity between them.
Naming Consistency4/5
Names mostly follow a get_/list_ + resource pattern, with list_vehicles being the only deviation from the get_ prefix. This is predictable and easy to navigate.
Tool Count5/5
Four tools is well-scoped for a fleet data retrieval server. Each tool covers a meaningful slice of the domain without unnecessary bloat or overlap.
Completeness4/5
The core fleet data surface is covered: vehicle inventory, live status, trip history, and fuel consumption. Minor gaps exist such as driver details or location history, but nothing that blocks primary workflows.
Maintenance
ActivityMaintained
ResponsivenessNo issues