Developer
MCP Server
For the ChatGPT plugin with optional free account linking, see TrackOut in ChatGPT. The API-key setup below is for other agent clients.
The TrackOut MCP server lets AI agents access your garage data through the Model Context Protocol. No local install required -- just configure your agent with a URL and API key.
Setup
Prerequisites
- An API key from your TrackOut Settings page
Configure your agent
Add TrackOut to your agent's MCP configuration. For Claude Code, add to .claude/mcp.json:
{
"mcpServers": {
"trackout": {
"type": "url",
"url": "https://trackout.app/api/mcp",
"headers": {
"Authorization": "Bearer tok_..."
}
}
}
}
Replace tok_... with your API key from Settings.
Available tools
Once connected, the following tools are available to your agent:
Cars
| Tool | Description |
|---|---|
list_cars | List all cars in your garage with nickname, make, model, trim, year, mileage, and VIN or chassis identity |
get_car | Get full details for a specific car including trim, color, VIN or chassis number, and price |
create_car | Create a new car with nickname, make, model, trim, year, color, VIN or race chassis number, and mileage |
update_car | Update any field on an existing car by car ID |
delete_car | Move a car to the trash. Its records are detached rather than destroyed, and restoring it is a web-app action |
Parts
| Tool | Description |
|---|---|
list_parts | List parts, optionally filtered by car |
create_part | Create a new part with name, manufacturer, part number, purchase details, and status. A new part can only start as IN_STOCK or WANT_TO_BUY |
update_part | Update an existing part -- correct its details or life limits, assign it to a car (durable parts only; a consumable is shared across the garage and stays unassigned), or retire it as SOLD or DISCARDED. Fitting a part is not a status write: installing opens an installation record and logs a service record, so INSTALLED is not offered here and a part that is currently fitted must be uninstalled in the app before it can be retired |
delete_part | Delete a part along with its wear readings, usage history, and uploaded attachments |
Service records
| Tool | Description |
|---|---|
list_service_records | List service records, optionally filtered by car |
create_service_record | Create a new service record with name, type, mileage, cost, date, notes, an optional linked receipt ID, and optional maintenanceRuleIds — the maintenance items this service satisfied, each of which restarts its interval from this record |
list_service_types | List valid service type values (use before creating records) |
update_service_record | Update any field on an existing service record by ID, including replacing its maintenanceRuleIds |
delete_service_record | Delete a service record. The car's counters are decremented and any reminder it satisfied is recomputed |
Installers
| Tool | Description |
|---|---|
list_installers | List your saved service shops and installers |
create_installer | Add a new installer by name |
Track sessions
| Tool | Description |
|---|---|
list_track_days | List track days, optionally filtered by car |
create_track_day | Create a new track day event with car, date, and track name |
update_track_day | Correct a track day's date, track, layout, notes, or event link. The sessions inside it are untouched |
delete_track_day | Delete a track day and every session inside it |
list_track_sessions | List track sessions, optionally filtered by car |
create_track_session | Create a track session within a track day. bestLapTime is in seconds -- 92.4 for a 1:32.400 lap. Every track type writes this field, from a 4-second drag pass to a rally stage of many minutes. When creating it, you can also record fuel added and fuel used (US gallons, 0-100), track temperature (°F, -20 to 200), and peak front and rear brake temperatures (°F, 0-2000) |
update_track_session | Correct a session's date, lap figures, goal, conditions, or captured readings. Send null for a reading to erase it. A session that is already complete refuses edits to the fields its tire and part counters consumed |
delete_track_session | Delete one session. Its track day survives and the day's totals are recomputed |
Maintenance
| Tool | Description |
|---|---|
list_maintenance_item_kinds | List maintenance item kinds -- what a rule tracks, such as ENGINE_OIL, BRAKE_FLUID, GEARBOX_OIL, or HARNESS -- with each kind's reset service type, extra matching types, and suggested track-car intervals |
list_maintenance_rules | List recurring maintenance rules, optionally filtered by car. Each rule includes its itemKind and matchServiceTypes |
get_maintenance_status | Where every tracked item on a car stands: used vs. limit on each interval, status (ok, due_soon, due, overdue, never_logged, unknown), next due, last service, and product |
create_maintenance_rule | Track a maintenance item with an interval by miles, months, heat cycles (one per track session), or track days. Pass an itemKind (it picks the service type) or a serviceType; optionally matchServiceTypes for other services that also reset it, and trackFrom: "never_logged" to wait for the first logged service instead of counting from today. At least one interval must be positive -- a rule with none set could never come due, and is rejected |
update_maintenance_rule | Change a rule's item kind, intervals, anchors, or matching service types, or set isActive: false to pause it without deleting it |
delete_maintenance_rule | Delete a rule. Reminders it already generated are kept but detached, so they stop regenerating |
list_maintenance_reminders | List service reminders with optional car filter and completion status. Each includes atShopSince when the car is at the shop for it, and trackerUrgency (overdue, due-soon or not-due) when its tracker decides whether it is due |
complete_maintenance_reminder | Mark a service reminder as complete, optionally linking a service record |
set_maintenance_reminder_at_shop | Mark that the car is at the shop for a pending reminder (or back from it). While set, the reminder reads "at the shop since" its drop-off date instead of overdue; completing the reminder clears it |
Tires
| Tool | Description |
|---|---|
list_tires | List tires with brand, size, heat cycles, and mileage, optionally filtered by car |
update_tire | Correct a tire's spec, wear baseline, or storage location, or archive it. Mounting and unmounting stay web-app actions |
delete_tire | Delete a tire and its readings. A tire mounted on a live car is refused -- unmount it first |
Checklists
| Tool | Description |
|---|---|
list_checklists | List your checklists, optionally filtered by car |
get_checklist | Get a single checklist with all of its items |
create_checklist | Create a checklist, either empty or from one of your templates |
list_checklist_templates | List the checklist templates available to start from |
Documents
| Tool | Description |
|---|---|
upload_document | Upload a file (base64-encoded) and receive a fileId, storageId, and file metadata |
scan_receipt | Start or poll AI receipt extraction for an uploaded receipt file or existing receipt ID. Pass wait: true to have the server watch the extraction for you |
create_records_from_receipt | Create multiple service records from reviewed receipt line items in one call |
Reading the tool schemas
Everything your agent needs to build a valid call is in the tool list itself, so it should not need this page open to get a first attempt right.
- Dates take
YYYY-MM-DD--2024-01-15. A bare date is stored as a calendar day and reads the same in every timezone. An ISO 8601 timestamp is accepted too and keeps its time of day, but it must carry an explicit offset --2024-01-15T10:00:00Zor2024-01-15T10:00:00-08:00.2024-01-15T10:00is rejected: without an offset it means local time to whichever machine parses it, so the same call would store two different instants. Nothing else parses.
Date parsing got stricter
Date arguments used to fall back to the JavaScript Date parser, which quietly accepted 01/15/2024, Jan 15, 2024 and RFC-2822 strings -- and resolved some of them differently depending on the runtime, so the same call could store two different days. Those forms are now rejected with an error naming the accepted one. If you have a script pinned to a non-ISO format, switch it to YYYY-MM-DD; agents that read the tool schema at connect time need no change.
- Units travel with the number. Lap times are seconds, fuel is US gallons, temperatures are °F, tread depth is millimetres (a US gauge reading of 8/32" is 6.3 mm), pressures are PSI, money is US dollars. Each numeric field publishes its minimum and maximum, so a value sent in the wrong unit usually fails immediately instead of being stored.
- Enumerated values are listed in the schema.
serviceTypecarries the full set, solist_service_typesis available but no longer a required first step. Maintenance rules offer only the types that can recur, andcreate_partoffers only the statuses a part can be created in. nullclears, omitting leaves alone. On an update tool, a field you leave out keeps its stored value. Fields whose description says so also acceptnullto erase the stored value -- that is how you remove a captured reading, an interval, or a linked shop.- The high-value create tools carry a worked example in their description, with a realistic set of optional fields.
Review and approve assistant deletions
delete_car moves a car to the trash and is recoverable in the web app. Every other delete_* tool is permanent, and each one's description says what else goes with the row -- deleting a track day also deletes every session inside it. Where you only want to retire something, prefer update_tire with isArchived, update_part with a SOLD or DISCARDED status, or update_maintenance_rule with isActive: false.
A remote delete_* call first returns a review link and leaves the record intact. Open the link, sign in to the account that owns the API key, review the named record and its related deletions, then choose Approve deletion or Decline. After you approve, your assistant retries the same tool with the returned confirmation ID. Approval expires ten minutes after the request and works once, only for that key, tool, and record. An expired or declined request needs a fresh review.
A key with write scope can request a deletion but cannot approve its own request. Keep one key per agent so you can revoke access narrowly. The local stdio MCP server asks for confirmation through the connected client's user confirmation prompt; a client without that support cannot execute a deletion.
Protocol compatibility
TrackOut negotiates whichever of these Model Context Protocol versions your client asks for:
| Protocol version | Status |
|---|---|
2025-11-25 | Supported (latest) |
2025-06-18 | Supported |
2025-03-26 | Supported |
2024-11-05 | Supported |
2024-10-07 | Supported |
A client that sends no version is answered on 2025-03-26. Every version in this table is covered by a regression test that runs initialize, tools/list, and a tool call, so support here means exercised rather than merely accepted.
The server identifies itself as trackout and answers on a per-request stateless transport: it issues no mcp-session-id and keeps no session store.
The tool list does not push updates
TrackOut reports tools.listChanged: false, because a stateless endpoint has no long-lived connection on which a notifications/tools/list_changed could arrive. The tool list is fixed for a given release, so caching it for the life of a connection is safe -- just re-list when you reconnect rather than waiting for a notification that will never come.
Receipt workflow
The MCP receipt flow mirrors the TrackOut UI:
- Call
upload_documentwith a PDF or receipt image. - Call
scan_receiptwith the returnedfileIdto start extraction. - Wait for the extraction to finish, either way round:
- Let the server wait. Pass
wait: trueand the call holds open for up to 25 seconds, returning the finished extraction as soon as it is ready. If it is still running when the wait ends, you get the same in-progress response as below -- call again with itsreceiptId. - Poll yourself. Call
scan_receiptagain with either the samefileIdor the returnedreceiptIduntil the receipt is ready for review.fileIdstays valid after the file is stored encrypted.
- Let the server wait. Pass
- Create linked records with either:
create_service_recordfor a single record and passreceiptIdcreate_records_from_receiptfor a multi-line-item receipt import
Give each record the cost the app would file it at: its labor line, plus the parts and consumables used in that job, plus its share of the receipt's tax and fees, split in proportion to each record's amount over all the lines you are importing. That is what Reports and the logbook add up, so a record costed at its labor line alone leaves the rest of the receipt out of your spend.
create_service_record accepts a receiptId. (The HTTP API and the local stdio server also accept receiptDocumentId as a compatibility alias.)
update_service_record deliberately does not. A record's provenance -- whether it came from a scan, an email, or the API -- is decided when the record is created and never revisited, so linking a receipt to a record that already exists is a different operation, not a field you can patch. That operation is available in the web app and is not on the API or MCP surface; attaching a receipt to a record that already has one has to decide what becomes of the old link, and that choice belongs in a screen where you can see both.
Streaming and progress
Tools that can take a while -- today that is scan_receipt with wait: true -- report progress while they run, so your agent can show "still scanning" instead of sitting silent.
This happens automatically: if your MCP client asks for progress on a call, TrackOut answers that call as a text/event-stream and sends progress updates on it until the result arrives. Clients that do not ask for progress get the same single JSON response they always have. Nothing to configure either way.
Two things worth knowing:
- Progress is scoped to the call that asked for it. TrackOut never opens a long-lived stream of its own, so there is no background connection to keep alive or reconnect.
- A
wait: truecall is bounded at 25 seconds. That is a ceiling, not a delay -- the call returns the moment extraction finishes.
Example usage
Once configured, you can ask your agent things like:
- "List my cars"
- "Show service records for my M3"
- "Log an oil change for my 911 at 42,000 miles, cost $85"
- "What tires are installed on my track car?"
- "Add a new installer called Track Performance Shop"
- "Create a new car named 'Track Beast' -- 2024 Porsche 911 GT3"
- "Add an oil filter part for my M3"
- "Set up a recurring oil change every 5,000 miles"
- "What's due on my M3?"
- "Upload this receipt, scan it, and show me the extracted line items"
- "Create linked service records from this scanned receipt for my M3"
The agent will use the appropriate MCP tools to read or write your TrackOut data.
HTTP API
The MCP server calls the TrackOut HTTP API under the hood. If you want to build your own integration, you can call these endpoints directly:
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/v1/cars | List cars |
| GET | /api/v1/cars/get?id=... | Get car details |
| POST | /api/v1/cars | Create car, including a vin or race chassisNumber with its identifierKind |
| PATCH | /api/v1/cars | Update car identity or details (requires carId in body) |
| DELETE | /api/v1/cars | Soft-delete car (requires carId in body) |
| GET | /api/v1/service-records?carId=... | List service records |
| POST | /api/v1/service-records | Create service record, optionally linked to a receipt |
| PATCH | /api/v1/service-records | Update service record (requires id in body) |
| DELETE | /api/v1/service-records | Delete service record (requires id in body) |
| GET | /api/v1/service-types | List service type enum values |
| GET | /api/v1/installers | List installers |
| POST | /api/v1/installers | Create installer |
| GET | /api/v1/track-sessions?carId=... | List track sessions |
| POST | /api/v1/track-sessions | Create track session. bestLapTime is in seconds -- send 92.4 for a 1:32.400 lap, not 92400. A whole number from 1000 up is rejected: it is ambiguous with a legacy encoding and would be read back as a different time, so send a fractional value for a stage that long. Optional readings: fuelAddedGal and fuelUsedGal (US gallons, 0-100), trackTempF (°F, -20 to 200), brakeTempFrontPeakF and brakeTempRearPeakF (°F, 0-2000) |
| PATCH | /api/v1/track-sessions | Update track session (requires id in body). bestLapTime is in seconds, as above. Rejects date, startTime, endTime and sessionType changes on a session that is already complete -- those feed the tire and part counters completion applied. Send null for fuelAddedGal, fuelUsedGal, trackTempF, brakeTempFrontPeakF, or brakeTempRearPeakF to clear that reading. |
| DELETE | /api/v1/track-sessions | Delete track session (requires id in body) |
| GET | /api/v1/track-days?carId=... | List track days |
| POST | /api/v1/track-days | Create track day |
| PATCH | /api/v1/track-days | Update track day (requires id in body) |
| DELETE | /api/v1/track-days | Delete track day and its sessions (requires id in body) |
| GET | /api/v1/tires?carId=... | List tires |
| PATCH | /api/v1/tires | Update tire (requires id in body). storageLocation is free text for an off-car tire; null or an empty string clears it. |
| DELETE | /api/v1/tires | Delete tire (requires id in body; mounted tires are rejected) |
| GET | /api/v1/parts?carId=... | List parts |
| POST | /api/v1/parts | Create part |
| PATCH | /api/v1/parts | Update part (requires id in body) |
| DELETE | /api/v1/parts | Delete part (requires id in body) |
| GET | /api/v1/maintenance-item-kinds | List maintenance item kinds |
| GET | /api/v1/maintenance-rules?carId=... | List maintenance rules |
| GET | /api/v1/maintenance-status?carId=... | Where every tracked item on a car stands |
| POST | /api/v1/maintenance-rules | Create maintenance rule |
| PATCH | /api/v1/maintenance-rules | Update maintenance rule (requires id in body) |
| DELETE | /api/v1/maintenance-rules | Delete maintenance rule (requires id in body) |
| GET | /api/v1/maintenance-reminders?carId=...&includeComplete=... | List reminders |
| POST | /api/v1/maintenance-reminders/complete | Complete a reminder |
| POST | /api/v1/maintenance-reminders/at-shop | Mark a reminder at the shop ({ reminderId, atShop }) |
| GET | /api/v1/checklists?carId=... | List checklists |
| GET | /api/v1/checklists/get?id=... | Get a checklist with its items |
| POST | /api/v1/checklists | Create checklist, optionally from a template |
| GET | /api/v1/checklist-templates | List checklist templates |
| POST | /api/v1/documents/upload | Upload document (base64 content) and receive fileId |
| POST | /api/v1/receipts/scan | Start or poll receipt extraction with a fileId or receiptId |
| POST | /api/v1/receipts/create-records | Create multiple linked service records from a scanned receipt |
All endpoints require an Authorization: Bearer tok_... header. The base URL for direct API calls is https://site.trackout.app.
curl -H "Authorization: Bearer tok_..." \
https://site.trackout.app/api/v1/cars
Error responses
All error responses are JSON shaped { "error": "<message>" } with an appropriate HTTP status code. The MCP route passes user-facing 4xx messages through unchanged; 5xx responses are mapped to a generic Upstream service temporarily unavailable. message on the MCP client so internal errors never leak through a tool call.
| Status | When you'll see it |
|---|---|
| 400 | Missing or invalid body/query fields |
| 401 | Missing, invalid, or revoked API token |
| 403 | Token is valid but lacks the required scope or access to the resource |
| 404 | Resource does not exist or is not owned by your account |
| 413 | Request body (or decoded upload) exceeds the limit |
| 429 | Rate limit exceeded — back off and retry |
| 5xx | Upstream error — logged server-side, generic message on the client |
Upload limits
The upload_document tool (and POST /api/v1/documents/upload) enforces:
- Encoded request body: up to 14 MB
- Decoded file: up to 10 MB
- Allowed content types:
application/pdf,image/jpeg,image/png,image/webp,image/heic,image/heif,image/gif,text/plain - Uploads per minute: up to 60 per account, the same cap the web app uses. Past that, the tool returns Upload limit reached. Please try again later. until the minute rolls over.
Requests that exceed these limits fail fast with a 4xx before bytes are persisted.
Security notes
- Authorization is driven exclusively by your API token — the MCP session ID plays no role in authorization. Every request is independently authenticated.
- The MCP endpoint is intentionally per-request stateless: it issues no
mcp-session-idat all, and keeps no server-side session store -- not even for streamed calls. There is no session key two users could collide on. - API tokens are SHA-256 hashed at rest. Revoke a compromised token from the API keys page; revocation takes effect on the next request.
- Rotate tokens periodically and prefer creating one token per agent/tool so you can revoke narrowly.
- Uploaded documents are not served at a public URL.
upload_documentreturnsurl: null; receipt images and part attachments are encrypted at rest and shown only through authenticated decrypt actions. - Requesting Delete account in Settings immediately invalidates the API token. HTTP API calls then receive 401. MCP tools return an authentication error -- even while garage data is still being removed.