# MotorAdvisor MCP

MotorAdvisor exposes MOTOR DaaS vehicle intelligence — and the shop workflow around it — to any MCP-compatible AI application through one remote server. The same tools that power the [MotorAdvisor chat app](/) are available to Claude, to agent frameworks, and to your own clients, with nothing to install and nothing to configure beyond a URL.

| | |
| --- | --- |
| **MCP endpoint** | `https://mcp.motoradvisor.app/mcp` |
| **Authorization server** | `https://motoradvisor.app` |
| **Transport** | MCP streamable HTTP, stateless JSON mode |
| **Auth** | OAuth 2.1 (discovered automatically) or a personal access token |

## What's on the surface

Sixteen tools in five groups — the same five the [Tool reference](/dev/tools) uses:

- **Vehicle data** — `resolve_vehicle`, `list_content`, `get_content_detail`, and `service_advisor_lookup`: VIN and year/make/model resolution, then maintenance schedules, labor times with pricing, technical service bulletins, specifications, and diagnostics. All read-only against MOTOR DaaS.
- **Repair economics** — `assess_repair_economics` and `get_vehicle_value`: a quoted repair weighed against the vehicle's worth, answered as an economic band. Valuation figures are licensed content and never leave the surface.
- **Billing** — `issue_invoice` and `check_invoice_status`: the assistant that quoted a repair can bill for it and track receivables. Account-scoped; the shop is resolved from the verified token, never from input.
- **Notifications** — `create_quote_snapshot`, `check_quote_status`, `void_quote`, `send_quote_notification`, `send_invoice_notification`, and `capture_consent`: a completed labor selection — plus the advisor-priced parts — becomes the shop's quote; the quote and, later, the pay link go to the customer on consented channels only; `capture_consent` records the consent every send tool requires. There is no destination parameter — the shop's consent records are the only reachable addresses.
- **Jobs** — `get_job` and `list_jobs`: the shop's own board, read-only, scoped to the authenticated account.

Two things shape what a given account actually gets back, and both are covered under [Authentication](/dev/authentication):

- **Content is licensed in two classes.** Labor times, maintenance, specifications, fluids and trouble codes are MOTOR-owned and come with any paid package. Repair procedures, parts, technical service bulletins, wiring diagrams and illustrations need a manufacturer-approved agreement; until an account holds one those content types refuse, and `service_advisor_lookup` skips those sections and answers with the rest.
- **The billing, notification and job tools are the shop product's surface.** They resolve a shop from the verified token. A self-serve developer account has no shop and gets a `NoShop` refusal from them; the self-serve offer today is the vehicle-data and repair-economics groups.

The full reference, with schemas and live-captured examples, is under [Tool reference](/dev/tools).

## Built for LLM consumption

The tool descriptions are hardened for foreign clients: an MCP client with no system prompt discovers the tools, chooses correctly, and handles ambiguity — the descriptions themselves carry the workflow contracts. The docs follow the same principle:

- [`/dev/llms.txt`](/dev/llms.txt) — index of this documentation in llms.txt convention
- [`/dev/llms-full.txt`](/dev/llms-full.txt) — the entire documentation as one markdown document
- [`/dev/tools.json`](/dev/tools.json) — the tool definitions exactly as the server advertises them, machine-readable

## Getting started

1. Read [Getting started](/dev/getting-started) — connecting takes one URL.
2. Get access: developers [sign up](/developers/signup), choose a package, and pay by card; the portal then issues API keys and meters usage. Shop accounts are provisioned by an administrator. [Authentication](/dev/authentication) covers both.
3. The evaluation sandbox covers model years **2010, 2015, and 2016** only. The demo vehicle is the 2010 Honda Civic (VIN prefix `2HGFA1F5`). Empty results for other vehicles are correct behaviour, not an error.

## Who built this

MotorAdvisor is a proof of concept built by [Product Detroit](https://productdetroit.com), demonstrating what [MOTOR](https://www.motor.com)'s DaaS API can serve as a natural-language interface. It is an independent demonstration — **not a product of, or endorsed by, MOTOR Information Systems**. MOTOR supplies the licensed vehicle data through its DaaS evaluation sandbox; everything else on this site and server is Product Detroit's work, and questions about this deployment go to Product Detroit, not to MOTOR.

MOTOR DaaS is read-only through this server without exception — every write-capable tool writes to MotorAdvisor's own records or its payment provider, never to MOTOR.

---

# Getting started

Everything a client needs is discovered from one URL. For an OAuth-capable client there is nothing to paste and no schema to copy — the server describes itself over the MCP protocol, and auth is negotiated by the OAuth 2.1 flow built into modern MCP clients. Headless clients send an API key or a personal access token as a bearer instead.

**Prerequisite:** an account. Developers [sign up](/developers/signup) with a package and a card, then create an API key in the portal. Shop accounts are provisioned by a MotorAdvisor administrator with the MCP grant. Signing in is not by itself access: a developer account serves no calls until checkout clears, and a cancelled or unpaid subscription stops calls at the next request. For shop accounts the login and the MCP grant are the gate.

## Claude (and other OAuth-capable clients)

Add a custom connector with **only the URL**:

```text
https://mcp.motoradvisor.app/mcp
```

Leave any *OAuth Client ID* / *Client Secret* fields blank — the client registers itself dynamically. It discovers the authorization server, opens the login and consent page at https://motoradvisor.app, and you sign in — with a developer account you created yourself, or a shop account an administrator provisioned. A client arriving with no session is sent to developer sign-in, which offers signup. That is the whole setup.

The same applies to any client that implements MCP authorization, including `mcp-remote` for clients that only speak stdio:

```json
{
  "mcpServers": {
    "motoradvisor": {
      "command": "npx",
      "args": ["mcp-remote", "https://mcp.motoradvisor.app/mcp"]
    }
  }
}
```

## Headless and service clients

Two bearer credentials exist for clients that cannot run an OAuth flow:

- An **API key** (`mtr_live_…` or `mtr_test_…`), created by a developer in the [portal](/developers) once their subscription is serving. Shown once, at creation; revocable in the portal at any time, and a revoked key is refused by name so it is not mistaken for an unrecognised one.
- A **personal access token**, minted by an administrator in the web app's admin console for a shop account (shown once, at mint time).

Either is sent as a bearer token. The examples below use `$MCP_TOKEN` for whichever you hold:

```bash
curl -s https://mcp.motoradvisor.app/mcp \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
```

Calling a tool is one more request:

```bash
curl -s https://mcp.motoradvisor.app/mcp \
  -H "Authorization: Bearer $MCP_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json, text/event-stream" \
  -d '{
    "jsonrpc": "2.0",
    "id": 2,
    "method": "tools/call",
    "params": {
      "name": "resolve_vehicle",
      "arguments": { "vehicle": { "vin": "2HGFA1F5" } }
    }
  }'
```

The server is stateless: no session to initialize, every request self-contained.

## A first conversation

The tools are designed so an agent can discover the workflow from the descriptions alone, but the shape of a good first exchange looks like this:

1. **`service_advisor_lookup`** with a VIN, mileage, symptom, and labor rate — it resolves the vehicle internally and returns due maintenance, candidate diagnostic operations with pricing, and matching bulletins in one call.
2. The candidates are a **menu**: present them, let the user choose, then re-call with `selectedApplicationIds` for a selected total.
3. **`assess_repair_economics`** with the VIN and the selected total — is this repair proportionate to what the car is worth?
4. **`send_quote_notification`** with the quote id — the customer receives their approval link on the channels they consented to, and a channel without consent comes back as a structured refusal rather than a send.
5. **`issue_invoice`** once the work is done and the customer's name and email are confirmed.

Try it with the sandbox's demo vehicle:

> *"2010 Honda Civic, VIN 2HGFA1F5, 62k miles, AC blows warm, $150/hr shop rate — what should I quote for the diagnostic?"*

## Sandbox coverage

The evaluation sandbox covers model years **2010, 2015, and 2016** only. Empty results for vehicles outside those years are correct behaviour — clients should report the vehicle as not covered rather than retrying. Production coverage spans the full MOTOR vehicle database; the workflow contracts do not change.

---

# Authentication

The MCP server is a pure **resource server**: it validates tokens and never issues them. The MotorAdvisor web app at `https://motoradvisor.app` is the authorization server. Three credentials arrive as a bearer, over two validation paths:

- **API keys** (`mtr_live_…` / `mtr_test_…`), created by a developer in the portal. Recognised by prefix and looked up in the developer portal's database — never handed to the JWT verifier.
- **OAuth 2.1 access tokens**, obtained interactively by any MCP client that implements the authorization spec. Verified as an MCP-audience JWT.
- **Personal access tokens**, minted by an administrator for a shop account's headless clients and shown once at mint time. Also an MCP-audience JWT.

Whichever arrived, what the account may reach — its realm, plan, entitlements and subscription state — resolves identically afterwards.

## The OAuth flow, as a client experiences it

1. The client calls `POST /mcp` without a token and receives **401** with a `WWW-Authenticate` header carrying `resource_metadata`.
2. It fetches `/.well-known/oauth-protected-resource` (RFC 9728) and learns the authorization server's URL.
3. It fetches the authorization server's metadata, **registers itself dynamically**, and starts an authorization-code flow with **PKCE (S256)**.
4. The user signs in at https://motoradvisor.app and consents — with a developer account they created themselves, or a shop account an administrator provisioned with the MCP grant. A client arriving with no session is sent to developer sign-in, which offers signup; shop staff sign in at `/login`.
5. The client receives an access token bound to the MCP **audience** and retries the request.

No client pre-registration, no shared secrets, no configuration beyond the URL. This flow is what an MCP client's "add server → login screen appears" experience is made of.

## Account state is live

Beyond the signature and audience, the server checks the **account's live state** on each request. Disabling an account or revoking its MCP grant kills existing tokens on the next call — there is no revocation lag hiding behind token expiry. For developer accounts the live check also reads the subscription: a cancelled or unpaid subscription stops calls at the next request with a checkout link, while `past_due` keeps serving for as long as the card is being retried.

## Metering and limits

Two limits, depending on the account.

- **Developer accounts** are metered against their package, counted durably in the usage ledger. Test includes 1,000 data calls and 50 repair plans a month and stops there — no overage, no surprise bill; exhausting it is an **in-band** tool refusal (`QuotaExceeded`) that names the limit and links to the portal, never an HTTP 429. Build and Scale include more and bill overage rather than stopping. Past 50,000 calls or 3,000 repair plans a month, self-serve ends and someone sizes a licence with you.
- **Licensed shop accounts** keep an administrator-set per-account request ceiling. Past it, calls return **429** `rate_limited`. In this evaluation deployment that counter lives with the serving instance, so enforcement is approximate.

Only calls the server actually served count. A section skipped for want of a licence is a refusal, not usage.

## Entitlements

MOTOR content is licensed in two classes, and every account holds a set of them:

- **`motor_owned`** — labor times, maintenance schedules, specifications, fluids, diagnostic trouble codes. Included with any paid package.
- **`oem_licensed`** — repair procedures, parts, technical service bulletins, wiring diagrams, component locations, part illustrations. Requires a manufacturer-approved agreement and is dark until MOTOR has one on file for the account. Licensed shop accounts hold both classes.

A content type the account cannot reach refuses the whole call with `NotEntitled` and a link to the portal. Inside `service_advisor_lookup` the licensed sections are skipped and the rest answers; the response's per-section status says which, and the tool's advertised description carries a `[Partially available]` notice for that account. A self-serve developer account holds `motor_owned` only.

## Token hygiene

The inbound bearer token is **never forwarded to MOTOR** (the spec prohibits token passthrough, and a test asserts the inbound value appears in no outbound URL or header). MOTOR credentials are held server-side and signed into upstream requests by the server itself; clients never see or supply them.

## HTTP errors

| Status | `error` | Meaning |
| --- | --- | --- |
| 401 | `invalid_token` | Missing, expired, wrong-audience, or revoked token — including a disabled account. A revoked API key is named as revoked, so it is distinguishable from an unrecognised one. The `WWW-Authenticate` header carries the discovery pointer. |
| 401 | `insufficient_scope` | The account exists but does not hold the MCP grant. |
| 429 | `rate_limited` | A licensed shop account's per-account request ceiling is exhausted. Developer packages refuse in-band instead — see Tool-level errors. |
| 503 | `server_misconfigured` | The deployment is missing its signing secret; fail-closed. |
| 503 | `service_unavailable` | The account service could not be reached, or this deployment has no self-serve side. The credential was not checked — retry; do not rotate keys. |

## POC caveats, stated plainly

This is an evaluation deployment, and a few implementation choices are scoped to that. Integrators should know them; none of them changes the contracts a client builds against.

- **The authorization server is a deliberately minimal in-app implementation.** It exists to prove the boundary, not to be one. Production swaps in an enterprise identity platform without touching the MCP server — the resource server validates audience-scoped JWTs and does not care who minted them, which is the point of the design.
- **Single-use enforcement of authorization codes is per serving instance**, as is the legacy request ceiling on licensed shop accounts. Developer package usage is already backed by a shared store and is exact. Production backs the rest the same way.
- **Sandbox coverage (model years 2010, 2015, 2016) is a data scope, not an architecture scope.** Production serves the full MOTOR vehicle database through the same tools with no change to their schemas or contracts.

What is *not* a caveat: audience-validated tokens, live revocation, the ambiguity contract, two-stage quoting, band-only valuations, and the read-only DaaS posture. Those hold in production unchanged.

## Tool-level errors

Tool failures arrive **in-band** as MCP tool results with `isError: true` and a typed body:

```json
{ "error": { "type": "ShopNotReady", "message": "…", "explanation": "…" } }
```

The `type` names the refusal precisely — `InvalidInput`, `UnknownTool`, `NoShop`, `ShopNotReady`, `NotReady`, `PaymentProviderError`, and so on — and `explanation` says what would make the call succeed. Agents should relay the explanation rather than retrying blind.

Two of them a developer account meets first:

- `NotEntitled` — the requested content type needs a licensed agreement this account does not hold. The message links to the portal.
- `QuotaExceeded` — the package's monthly allowance is used and the tier has no overage (Test). The message names the limit and links to the portal to upgrade.

And one that is not an error at all: a vehicle outside the sandbox's model years resolves to a `MotorNotFoundError` whose message lists the covered catalog. Relay it as "not covered" rather than retrying.

---

# Sample questions

Questions the server answers well, with the tool calls a capable agent makes to answer them. These are drawn from live sessions against the sandbox's demo vehicle — a 2010 Honda Civic, VIN prefix `2HGFA1F5`.

## Diagnostics and quoting

> "2010 Honda Civic, VIN 2HGFA1F5, 62k miles, AC blows warm, $150/hr shop rate — what should I quote for the diagnostic?"

One `service_advisor_lookup` call (stage `diagnose`) returns the two applicable diagnostic operations — leak inspection and HVAC system diagnosis, 0.6 book hours each — plus three matching bulletins. A well-behaved agent presents both operations with per-line pricing and lets the advisor choose. (Bulletins are `oem_licensed`: on a self-serve account without a manufacturer agreement that section is skipped and the answer says so; the operations and pricing come back regardless.)

> "They approved both. What's the total?"

Re-call with `selectedApplicationIds` — the server returns a `selectedTotal`; the client never sums the menu itself.

## Maintenance

> "What's due at 62,000 miles?"

`service_advisor_lookup` with mileage. For this vehicle the schedule is **indicator-based** (Honda Maintenance Minder) — the correct answer explains that the car computes its own timing, and asks for the dash code.

> "The dash shows B — what work does that mean?"

Re-call with `serviceCode: "B"` for that code's work items.

## Technical service bulletins

> "Any bulletins about A/C problems on this car?"

`list_content` with `contentType: "TechnicalServiceBulletins"` and `searchTerm: "A/C"`, then `get_content_detail` for the bulletin the user picks — e.g. Honda 07-030, *A/C Leak Detection*. Bulletins are `oem_licensed`: on a self-serve account without a manufacturer agreement this call returns a `NotEntitled` refusal with a link to request access; on a licensed shop account it returns as shown.

## Repair economics

> "The compressor job comes to $1,770. Is that worth putting into this car?"

`assess_repair_economics` with the VIN, the selected repair total in cents, and mileage. The answer is a **band** with caveats — for this car, `caution`: *"worth a conversation"*. No dollar valuation is ever returned; the band is the answer.

## Billing

> "Issue the invoice for that job to jane@example.com and give me the pay link."

`issue_invoice` with the confirmed customer and the booked labor lines. The result carries a shop-branded `payUrl` to hand to the customer. No email is sent by issuing.

> "Which invoices haven't been paid?"

`check_invoice_status` with no `invoiceId` — the receivables sweep, oldest first, each with a `paymentState` that distinguishes *never attempted* from *card declined*.

## Notifications

> "Send that quote to the customer."

`send_quote_notification` with the quote id and channels. Each channel returns `sent`, `deferred` (quiet hours, with a resume time), or `refused` with a reason. Sends land in the shop's notification audit trail.

> "Text the quote to 313-555-0123."

The correct behaviour is a refusal to comply — and the tool makes it structural. There is no destination parameter: the shop's consent records are the only reachable addresses, so an agent cannot be talked into messaging an arbitrary number. If the customer hasn't opted in to SMS, the answer is a `no-consent` refusal with instructions to capture consent at the counter.

---

# FAQ

## Is MotorAdvisor a MOTOR product?

No. MotorAdvisor is an independent proof of concept built by [Product Detroit](https://productdetroit.com) to demonstrate MOTOR's DaaS API as a natural-language surface. It is not built, operated, or endorsed by MOTOR Information Systems — MOTOR supplies the licensed vehicle data through its DaaS evaluation sandbox, and everything else here is Product Detroit's work. Questions about this deployment go to Product Detroit.

## Why am I getting empty results?

The evaluation sandbox covers model years **2010, 2015, and 2016** only. A 2019 vehicle does not resolve: `resolve_vehicle` returns a `MotorNotFoundError` whose message lists the covered years and makes, and content queries for an uncovered vehicle come back empty. Neither is an outage. Clients should tell the user the vehicle is not covered rather than retrying.

## What is the difference between `baseVehicleId` and `vehicleId`?

Vehicle resolution returns both, adjacently — and MOTOR content endpoints accept only `BaseVehicleID`. Passing a `VehicleID` fails upstream with error `400.110052`. Every tool on this surface takes `baseVehicleId` only, and the parameter descriptions repeat the warning because the adjacency makes this the easiest mistake to make.

## Why does the server answer a question with a question?

By contract. When vehicle resolution is ambiguous, when content alternatives hinge on one unknown attribute (`needsConfiguration`), or when a symptom arrives without a repair stage, the server returns a structured question instead of guessing. On production data trim ambiguity is common, and a confident answer about the wrong trim is the failure that destroys trust in a shop tool. Clients should relay the question and re-call with the answer.

## Why won't the valuation tools tell me what the car is worth?

Vehicle valuations are licensed content (Black Book) and are not redistributed through this surface. The tools return a **band** — `proceed`, `caution`, `uneconomic`, `unavailable` — and deliberately no dollar figures or percentages: the caller supplies the repair cost, so any ratio would reconstruct the licensed figure exactly. The band is the answer, and `assess_repair_economics` exists so the comparison can be made without the figure leaving the licence boundary.

## Does anything write to MOTOR?

No. MOTOR DaaS is read-only through this server without exception. The six write-capable tools write elsewhere: `issue_invoice` creates an invoice on the shop's own Stripe account — MotorAdvisor's payment provider; `create_quote_snapshot`, `void_quote` and `capture_consent` write MotorAdvisor's own quote and consent records; `send_quote_notification` and `send_invoice_notification` send a message through MotorAdvisor's email provider, to consented customers only. Every description says so explicitly, so no client can misread one as a vehicle-data write.

## Can another account see my shop's invoices — or bill from my shop?

No. The billing tools take the shop from the **verified bearer token**, never from tool input. There is no argument shape that reads or writes another tenant's data.

## Are bulletin PDFs and wiring diagrams available over MCP?

Not on this surface. Documents are referenced by id in content details, but binary retrieval is excluded from the MCP tool list for the POC — binary handling by an unknown client is undefined behaviour. The web app streams documents by reference.

## What are the rate limits?

Developers are limited by their package, not by an administrator. Test includes 1,000 data calls and 50 repair plans a month and stops there — no overage, no surprise bill; the refusal arrives in-band as `QuotaExceeded`. Build and Scale include more and bill overage rather than stopping. Past 50,000 calls or 3,000 plans a month, self-serve ends and we size a licence with you. Licensed shop accounts keep an administrator-set per-account ceiling, which returns **429**. Limits are per account, not per token.

## Is my token forwarded to MOTOR?

Never. Token passthrough is spec-prohibited and tested against — the inbound bearer appears in no outbound URL or header. MOTOR credentials live server-side only.

## How do I get access?

Two ways. Developers [sign up](/developers/signup), pick a package (Test, Build, or Scale), pay by card, and create an API key in the portal — every call is metered against the package. Licensed shops are provisioned by an administrator, who creates the account and enables the MCP grant; for them the OAuth login is the access gate. Either credential validates through the same path.

---

# Tool reference

The 16 tools on `https://mcp.motoradvisor.app/mcp`, exactly as the server advertises them to a fully entitled account. Schemas are derived from the server's TypeScript types and this reference is generated from the same definitions — it cannot drift from what the server enforces. An account without a licensed agreement discovers the same tools with a gate notice prefixed to the description (see [Authentication › Entitlements](/dev/authentication)). The machine-readable original is at [`/dev/tools.json`](/dev/tools.json).

## Vehicle data

| Tool | Access | Summary |
| --- | --- | --- |
| [`resolve_vehicle`](/dev/tools/resolve_vehicle) | Read-only | Resolve a VIN or year/make/model to the MOTOR vehicle identity every other tool needs. |
| [`list_content`](/dev/tools/list_content) | Read-only | List content summaries — maintenance, labor times, TSBs, specifications — for a resolved vehicle. |
| [`get_content_detail`](/dev/tools/get_content_detail) | Read-only | Fetch the full detail for one content item found via list_content. |
| [`service_advisor_lookup`](/dev/tools/service_advisor_lookup) | Read-only | The one-call answer: due maintenance, candidate labor with pricing, TSBs, and specs for a vehicle. |

## Repair economics

| Tool | Access | Summary |
| --- | --- | --- |
| [`assess_repair_economics`](/dev/tools/assess_repair_economics) | Read-only | Weigh a quoted repair against the vehicle's worth and return an economic band — with the Black Book trade-in range for licensed shop accounts. |
| [`get_vehicle_value`](/dev/tools/get_vehicle_value) | Read-only | Confirm the valuation provider knows the vehicle — coverage and corroboration, with the trade-in range for licensed shop accounts. |

## Billing

| Tool | Access | Summary |
| --- | --- | --- |
| [`issue_invoice`](/dev/tools/issue_invoice) | Write · account-scoped | Issue a real, payable invoice from the authenticated account's shop and get back a pay link. |
| [`check_invoice_status`](/dev/tools/check_invoice_status) | Read-only · account-scoped | Check one invoice, or sweep every unpaid invoice for the shop — the receivables question. |

## Notifications

| Tool | Access | Summary |
| --- | --- | --- |
| [`create_quote_snapshot`](/dev/tools/create_quote_snapshot) | Write · account-scoped · server-priced | Turn a completed labor selection into the shop's quote snapshot and get the quoteId the send tool takes. |
| [`check_quote_status`](/dev/tools/check_quote_status) | Read-only · account-scoped | Where a quote stands with the customer — decision, expiry, and whether the message actually arrived. |
| [`void_quote`](/dev/tools/void_quote) | Write · account-scoped | Retire one of the shop's own quotes — a stale approval can never be billed once it is voided. |
| [`send_quote_notification`](/dev/tools/send_quote_notification) | Write · account-scoped · consent-gated | Send the customer their quote-approval link — on the channels they consented to, and no others. |
| [`send_invoice_notification`](/dev/tools/send_invoice_notification) | Write · account-scoped · consent-gated | Send the customer the payment link for an open invoice — on the channels they consented to, and no others. |
| [`capture_consent`](/dev/tools/capture_consent) | Write · account-scoped | Record a customer's notification consent at the counter — the record every send tool requires. |

## Jobs

| Tool | Access | Summary |
| --- | --- | --- |
| [`get_job`](/dev/tools/get_job) | Read-only · account-scoped | One job's durable spine — board stage, linked quote summaries, and the invoice by reference. |
| [`list_jobs`](/dev/tools/list_jobs) | Read-only · account-scoped | The shop's job board, most recently updated first — same composed shape as get_job, optional stage filter. |

Every MOTOR-facing tool is read-only. The billing and notification tools are account-scoped: the shop resolves from the verified token, never from input — and `send_quote_notification` takes no destination at all, so consent records are the only reachable addresses.

---

# resolve_vehicle

**Vehicle data** · **Read-only**

Resolve a VIN or year/make/model to the MOTOR vehicle identity every other tool needs.

## Usage

Call this first. Every content tool requires the numeric `baseVehicleId` that only this tool supplies — pass the `baseVehicleId` field onward, never the adjacent `vehicleId` (MOTOR rejects it with error `400.110052`).

When resolution is ambiguous the result is `{ambiguous: true, candidates: [...]}`. Present the candidates and let the user choose; the server never guesses a vehicle, and neither should your client.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `vehicle` | Resolve by VIN, or Resolve by year/make/model | Yes | The vehicle to resolve — either {vin} or {year, make, model, trim?}. Example: {"vin": "2HGFA1F5"} |
| `vehicle.vin` | string | Yes | Partial (3+ characters) or full 17-character VIN. Example: "2HGFA1F5" |
| `vehicle.year` | number, or string | Yes | Model year. Sandbox covers 2010, 2015, 2016 only. Example: 2010 |
| `vehicle.make` | string | Yes | Make name, case-insensitive. Example: "Honda" |
| `vehicle.model` | string | Yes | Model name, case-insensitive. Example: "Civic" |
| `vehicle.trim` | string | No | Trim/submodel, case-insensitive. Omit to see all candidates when unsure. Example: "LX" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Resolve a vehicle to its MOTOR identity from a VIN (partial, 3+ characters, or full) or year/make/model/trim. CALL THIS FIRST: every other tool on this server requires the numeric baseVehicleId that ONLY this tool's output supplies — there is no other way to obtain it. If the result is {ambiguous: true, question, candidates: [...]}, relay the question and present the candidates as a selectable list (numbered options the user can pick from, never a request to type details back). Do NOT pick a candidate yourself, do NOT retry with a guess — a confident answer about the wrong trim is the worst possible outcome. The evaluation sandbox covers model years 2010, 2015, and 2016 ONLY. Empty results for other vehicles are correct behaviour, not an error — tell the user the vehicle is not covered rather than retrying.

## Example — Resolve a partial VIN

*Captured from the live server, 2026-08-20.*

**Request**

```json
{
  "vehicle": {
    "vin": "2HGFA1F5"
  }
}
```

**Response**

```json
{
  "ambiguous": false,
  "baseVehicleId": 22124,
  "vehicleId": 61013,
  "description": "2010 Honda Civic LX, 1.8L L4 (R18A1) GAS FI",
  "year": 2010,
  "make": "Honda",
  "model": "Civic",
  "engineId": 2913,
  "trim": "LX",
  "countryCode": "USA",
  "vin": "2HGFA1F5"
}
```

Both identifiers appear side by side. Downstream tools take `baseVehicleId` (22124) — `vehicleId` (61013) is returned for completeness and is not accepted anywhere on this surface.

---

# list_content

**Vehicle data** · **Read-only**

List content summaries — maintenance, labor times, TSBs, specifications — for a resolved vehicle.

## Usage

Eleven content families are available (see the `contentType` enum). Paging is handled server-side; prefer `searchTerm` to narrow large sets before they reach your context window. The search matches generously — "A/C" can also surface ABS records — so discard obvious mismatches rather than treating them as data quality issues.

If the result carries `needsConfiguration`, the alternatives hinge on one unknown vehicle attribute. Ask the user that one question and re-call with `configuration` set; do not present both alternatives and do not pick one.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `baseVehicleId` | positive integer | Yes | The baseVehicleId field from a resolve_vehicle result. NOT the vehicleId (rejected with 400.110052). Example: 22124 |
| `contentType` | "EstimatedWorkTimes" \| "Specifications" \| "TechnicalServiceBulletins" \| "DiagnosticTroubleCodes" \| "ServiceProcedures" \| "WiringDiagrams" \| "Fluids" \| "Parts" \| "MaintenanceSchedules" \| "ComponentLocations" \| "PartVectorIllustrations" | Yes | Which MOTOR content family to query. Example: "TechnicalServiceBulletins" |
| `searchTerm` | string | No | Server-side semantic search — recommended for large sets. Matches generously ("A/C" can also match ABS records; discard obvious mismatches). Example: "A/C" |
| `keyword` | string | No | Client-side substring filter applied after fetch. Example: "refrigerant" |
| `configuration` | object | No | Known vehicle configuration. Rows whose qualifiers contradict it are filtered out; unknown attributes that materially change the answer come back as one targeted question under needsConfiguration. Engine and trim auto-populate in service_advisor_lookup. Example: {"transmission": "automatic"} |
| `configuration.transmission` | "automatic" \| "manual" | No | Transmission type, once the user has confirmed it. Example: "automatic" |
| `configuration.drivetrain` | "fwd" \| "rwd" \| "awd" \| "4wd" | No | Drivetrain, once known. Example: "fwd" |
| `configuration.bodyStyle` | "sedan" \| "coupe" \| "hatchback" \| "wagon" | No | Body style, once known. Example: "sedan" |
| `configuration.engine` | string | No | Engine displacement or designation. Auto-populated from resolution in service_advisor_lookup — only pass it to list_content. Example: "1.8L" |
| `configuration.trim` | string | No | Trim/submodel, case-insensitive. Omit to see all candidates when unsure. Example: "LX" |
| `configuration.rearBrakes` | "disc" \| "drum" | No | Rear brake type, once the advisor has confirmed it — VIN resolution cannot determine it, and brake work-time variants (e.g. rear pads vs shoes) hinge on it. Example: "disc" |
| `configuration.options` | array of string | No | Option packages present on the vehicle (presence-only assertions). Example: ["Sunroof"] |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> List content summaries (maintenance schedules, labor times, technical service bulletins, specifications, ...) for a resolved vehicle. The baseVehicleId parameter MUST be the baseVehicleId field from a resolve_vehicle result. It is NOT the vehicleId — passing a vehicleId is rejected by MOTOR with error 400.110052. Paging is handled internally; prefer searchTerm to narrow large result sets server-side. Returns {items, needsConfiguration?}. Pass configuration (transmission, engine, trim, ...) once known — rows contradicting it are filtered out. A needsConfiguration entry means alternatives hinge on ONE unknown attribute: ask the user that one question and re-call with the answer set — do NOT present both alternatives and do NOT pick one. The evaluation sandbox covers model years 2010, 2015, and 2016 ONLY. Empty results for other vehicles are correct behaviour, not an error — tell the user the vehicle is not covered rather than retrying.

## Example — Search technical service bulletins

*Captured from the live server, 2026-08-20. Item list truncated to the first three of seven.*

**Request**

```json
{
  "baseVehicleId": 22124,
  "contentType": "TechnicalServiceBulletins",
  "searchTerm": "A/C"
}
```

**Response**

```json
{
  "items": [
    {
      "applicationId": 474099311,
      "applicationIds": [
        474099311,
        474099314,
        474099326
      ],
      "name": "A/C Leak Detection"
    },
    {
      "applicationId": 475287901,
      "applicationIds": [
        475287901,
        475287902,
        475287903
      ],
      "name": "Air Conditioning Refrigerant and PAG/POE Oil for Warranty Repair Claims"
    },
    {
      "applicationId": 468452808,
      "applicationIds": [
        468452808,
        468452813,
        468684318
      ],
      "name": "Air Conditioning System Performance Test"
    }
  ]
}
```

`applicationId` feeds `get_content_detail`. The same search also returned an ATF cooler bulletin — generous matching in practice.

---

# get_content_detail

**Vehicle data** · **Read-only**

Fetch the full detail for one content item found via list_content.

## Usage

Details include labor times with skill codes, service intervals, notes, and cross-links to related content. Documents referenced by a detail (bulletin PDFs, illustrations) are linked by reference — binary content never travels through a tool result on this surface.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `baseVehicleId` | positive integer | Yes | The baseVehicleId field from a resolve_vehicle result. NOT the vehicleId (rejected with 400.110052). Example: 22124 |
| `contentType` | "EstimatedWorkTimes" \| "Specifications" \| "TechnicalServiceBulletins" \| "DiagnosticTroubleCodes" \| "ServiceProcedures" \| "WiringDiagrams" \| "Fluids" \| "Parts" \| "MaintenanceSchedules" \| "ComponentLocations" \| "PartVectorIllustrations" | Yes | Which MOTOR content family to query. Example: "TechnicalServiceBulletins" |
| `applicationId` | positive integer | Yes | ApplicationID from a list_content summary row. Example: 871003 |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Fetch the full detail for one content item found via list_content. The baseVehicleId parameter MUST be the baseVehicleId field from a resolve_vehicle result — NOT the vehicleId, which is rejected with error 400.110052. Details include labor times with skill codes, service intervals, notes, and cross-links to related content. Parts details carry OEM LIST pricing under `pricing` (oemPartNumber, listPrice, listPriceCents) — the OEM list price, not the shop's sell price. The evaluation sandbox covers model years 2010, 2015, and 2016 ONLY. Empty results for other vehicles are correct behaviour, not an error — tell the user the vehicle is not covered rather than retrying.

## Example — One bulletin in full

*Captured from the live server, 2026-08-20. Engine attribute block truncated.*

**Request**

```json
{
  "baseVehicleId": 22124,
  "contentType": "TechnicalServiceBulletins",
  "applicationId": 474099311
}
```

**Response**

```json
{
  "applicationId": 474099311,
  "contentType": "TechnicalServiceBulletins",
  "detail": {
    "Attributes": {
      "Engines": [
        {
          "Description": "1.8L L4 (R18A1) GAS FI",
          "EngineID": 2913,
          "FuelType": "GAS"
        }
      ],
      "SubModels": [
        {
          "SubModelID": 1609,
          "SubModelName": "LX-S"
        }
      ]
    },
    "TechnicalServiceBulletins": [
      {
        "BaseVehicleID": 22124,
        "Items": [
          {
            "Description": "A/C Leak Detection",
            "Document": {
              "Name": "180064.pdf",
              "Type": "Technical Service Bulletin",
              "DocumentID": 9899934,
              "Format": "application/pdf"
            },
            "Issuer": {
              "ManufacturerID": 39,
              "Name": "Honda",
              "Type": "OE"
            },
            "IssueDate": "2023-12-08T00:00:00",
            "ManufacturerNumber": "07-030",
            "TSBID": 134569,
            "Types": [
              {
                "ID": 6,
                "Type": "Service Bulletin"
              }
            ]
          }
        ],
        "ApplicationID": 474099311
      }
    ]
  },
  "relatedContent": [
    {
      "rel": "TechnicalServiceBulletinsDocument",
      "href": "/v1/Information/Vehicles/Attributes/BaseVehicleID/22124/Content/Documents/Of/TechnicalServiceBulletins/9899934"
    }
  ]
}
```

---

# service_advisor_lookup

**Vehicle data** · **Read-only**

The one-call answer: due maintenance, candidate labor with pricing, TSBs, and specs for a vehicle.

## Usage

This is the tool most conversations should reach for first — it resolves the vehicle internally, so a broad question needs no prior `resolve_vehicle` call.

Three contracts matter more than the rest:

- **Candidates are a menu, not a bill.** Labor candidates carry per-option pricing for alternatives that cannot all apply. Present them, let the advisor choose, and re-call with `selectedApplicationIds` for a `selectedTotal`. Never sum the menu.
- **The two stages mirror shop reality.** `diagnose` returns inspection and diagnostic operations only — no shop quotes a repair before diagnosis. `quote` returns the named repair with parts, specs, and pricing. A symptom sent without a stage returns `{ambiguous: true, kind: "stage", question, options}`; relay the question.
- **Indicator-based schedules are real.** When `scheduleType` is `"indicator-based"` (Honda Maintenance Minder), the vehicle computes its own timing and no general mileage table exists. Re-call with `serviceCode` — the code on the dash — for that code's work. That work is priced like any repair: each operation carries MOTOR's book time (read from its detail payload, not the summary) and appears in `laborEstimate`, so a B4 service is selected and totalled with `selectedApplicationIds` and needs no symptom. An operation MOTOR carries no time for comes back `billable: false` — included in the service, not free.

Every response carries a `callTrace` of the upstream MOTOR requests behind it: path, status, and latency per call.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `vehicle` | Resolve by VIN, or Resolve by year/make/model | Yes | The vehicle to resolve — either {vin} or {year, make, model, trim?}. Example: {"vin": "2HGFA1F5"} |
| `vehicle.vin` | string | Yes | Partial (3+ characters) or full 17-character VIN. Example: "2HGFA1F5" |
| `vehicle.year` | number, or string | Yes | Model year. Sandbox covers 2010, 2015, 2016 only. Example: 2010 |
| `vehicle.make` | string | Yes | Make name, case-insensitive. Example: "Honda" |
| `vehicle.model` | string | Yes | Model name, case-insensitive. Example: "Civic" |
| `vehicle.trim` | string | No | Trim/submodel, case-insensitive. Omit to see all candidates when unsure. Example: "LX" |
| `mileage` | positive integer | No | Current odometer miles — enables due-at-mileage maintenance selection. Example: 62000 |
| `symptom` | string | No | Customer-reported symptom in plain words — drives labor, bulletin, and spec search. Example: "AC blows warm" |
| `laborRateCents` | positive integer | No | Shop labor rate in integer cents per hour. Example: 15000 (= $150.00/hr) |
| `selectedApplicationIds` | array of positive integer | No | Labor operations the advisor chose from a previous call's candidates. Omit on the first call to get the menu; re-call with the chosen ids to get a selectedTotal. An id selects exactly that application: where a candidate groups variants with differing hours (bookHoursRange), pass the specific variant's id, and the costed line lists the variants it displaced under variantAlternatives — surface those to the advisor when their hours differ. Never sum candidates yourself. Example: [25785330, 26903691] |
| `serviceCode` | string | No | Indicator service code shown on the dash, for indicator-scheduled vehicles (Honda Maintenance Minder). Returns the maintenance work tagged with that code instead of a mileage slice. Example: "B" (combined displays like "B2" also work) |
| `stage` | "diagnose" \| "quote" | No | Repair stage. "diagnose" = fault not confirmed: inspection/diagnostic operations, TSBs, diagnostic content, NO repair operations. "quote" = repair known: the named operation with parts, specs, and pricing. Omit with a symptom and the tool asks which stage — relay that question, never guess. Example: "diagnose" |
| `specValueCap` | integer | No | How many specifications get their actual value+unit resolved per answer, most relevant first (default 4). Specs past the cap return name-only and the section note says how many were omitted. Example: 4 |
| `configuration` | object | No | Known vehicle configuration. Rows whose qualifiers contradict it are filtered out; unknown attributes that materially change the answer come back as one targeted question under needsConfiguration. Engine and trim auto-populate in service_advisor_lookup. Example: {"transmission": "automatic"} |
| `configuration.transmission` | "automatic" \| "manual" | No | Transmission type, once the user has confirmed it. Example: "automatic" |
| `configuration.drivetrain` | "fwd" \| "rwd" \| "awd" \| "4wd" | No | Drivetrain, once known. Example: "fwd" |
| `configuration.bodyStyle` | "sedan" \| "coupe" \| "hatchback" \| "wagon" | No | Body style, once known. Example: "sedan" |
| `configuration.engine` | string | No | Engine displacement or designation. Auto-populated from resolution in service_advisor_lookup — only pass it to list_content. Example: "1.8L" |
| `configuration.trim` | string | No | Trim/submodel, case-insensitive. Omit to see all candidates when unsure. Example: "LX" |
| `configuration.rearBrakes` | "disc" \| "drum" | No | Rear brake type, once the advisor has confirmed it — VIN resolution cannot determine it, and brake work-time variants (e.g. rear pads vs shoes) hinge on it. Example: "disc" |
| `configuration.options` | array of string | No | Option packages present on the vehicle (presence-only assertions). Example: ["Sunroof"] |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> The one-call answer for a service advisor: given a vehicle (plus optional mileage, symptom, and labor rate), returns due maintenance, CANDIDATE labor operations with per-option pricing, matching technical service bulletins, and relevant specifications in one structured result with per-section status and a call trace. The labor candidates are a menu, not a bill: present them with their individual prices, let the advisor choose which apply, then re-call with selectedApplicationIds to get a selectedTotal. NEVER add candidate prices together yourself — alternatives for different configurations must not be summed. If the maintenance section reports scheduleType "indicator-based" (e.g. Honda Maintenance Minder), the vehicle computes its own service timing and no general mileage table exists: say so, treat severeServiceConditional items as applying ONLY under their stated conditions, and re-call with serviceCode (the code on the dash, e.g. "B") for that code's work. A service code's work is priced work: each operation carries MOTOR's book time and appears in laborEstimate, so it is selected and totalled exactly like a repair — pass its applicationIds in selectedApplicationIds (no symptom needed), and those same ids are accepted by create_quote_snapshot. Where the schedule maps one operation per engine or transmission the candidate carries bookHoursRange and variantAlternatives: relay the mappings and let the advisor pick one, never average them. An operation MOTOR carries no book time for comes back billable:false — present it as included in the service, never as free or as zero hours. Shop workflow has TWO STAGES and the tool follows them. stage "diagnose" (fault not yet confirmed): inspection/diagnostic operations, TSBs, and diagnostic content — deliberately NO repair operations, because no shop quotes a repair before diagnosis. stage "quote" (repair known): the named operation with parts, specifications, and per-option pricing. Quote parts rows carry OEM LIST pricing under `pricing` (oemPartNumber, listPrice, listPriceCents) where MOTOR prices them — an anchor for the advisor, never the sell price, and a row without `pricing` is unpriced, not free. A symptom sent without a stage returns {ambiguous: true, kind: "stage", question, options}: relay the question to the advisor and re-call with their answer — do NOT pick a stage yourself. Configuration works the same way: engine and trim are auto-populated from resolution; pass other attributes (transmission, drivetrain, ...) via configuration once known, and treat a needsConfiguration entry as ONE targeted question to relay — never present both alternatives, never guess. A candidate whose hours vary by configuration arrives with a needsVariantChoice prompt (question + per-variant applicationId and hours): relay it and re-call with the chosen variant's applicationId in selectedApplicationIds. Resolves the vehicle internally, so it can be the FIRST call for broad questions. Any payload in a response carrying a question with options (a stage prompt, a needsConfiguration entry, a needsVariantChoice menu, ambiguous vehicle candidates) is a prompt: relay the question verbatim and present its options as a selectable list the user picks from — easier than typing option text — then re-call with the chosen option's value (stage, attribute value, or applicationId). On clients that support MCP elicitation, this server already showed the user that choice as a clickable form and resolved it before answering — a result with no question in it needs no re-ask; present it as final. If the result is {ambiguous: true, question, candidates: [...]}, relay the question and present the candidates as a selectable list (numbered options the user can pick from, never a request to type details back). Do NOT pick a candidate yourself, do NOT retry with a guess — a confident answer about the wrong trim is the worst possible outcome. The evaluation sandbox covers model years 2010, 2015, and 2016 ONLY. Empty results for other vehicles are correct behaviour, not an error — tell the user the vehicle is not covered rather than retrying.

## Example — Diagnostic-stage lookup for an A/C complaint

*Captured from the live server, 2026-08-20. Maintenance, diagnostics, and call-trace sections truncated.*

**Request**

```json
{
  "vehicle": {
    "vin": "2HGFA1F5"
  },
  "mileage": 62000,
  "symptom": "AC blows warm",
  "laborRateCents": 15000,
  "stage": "diagnose"
}
```

**Response**

```json
{
  "vehicle": {
    "ambiguous": false,
    "baseVehicleId": 22124,
    "description": "2010 Honda Civic LX, 1.8L L4 (R18A1) GAS FI",
    "year": 2010,
    "make": "Honda",
    "model": "Civic",
    "trim": "LX"
  },
  "mileage": 62000,
  "symptom": "AC blows warm",
  "workFocus": "Air Conditioning",
  "stage": "diagnose",
  "laborRate": "$150.00/hr",
  "maintenanceDue": {
    "status": "ok",
    "note": "This vehicle uses indicator-based (Maintenance Minder) scheduling — the vehicle computes service timing and the OEM publishes no general mileage table. Severe-service items listed here apply only under their stated conditions; re-call with serviceCode (the code on the dash, e.g. \"B\") for that code's work.",
    "data": {
      "atMiles": 62000,
      "scheduleType": "indicator-based",
      "due": [],
      "severeServiceConditional": [
        "…"
      ]
    }
  },
  "laborEstimate": {
    "status": "ok",
    "note": "Diagnostic-stage operations only. Once the tech confirms the fault, re-call with stage \"quote\" and the repair to price. Candidate operations only — no total until the advisor selects which apply (re-call with selectedApplicationIds).",
    "data": {
      "candidates": [
        {
          "operation": "Air Conditioning",
          "applicationId": 25785330,
          "name": "Air Conditioning System Leak Inspection",
          "serviceType": "Inspect",
          "bookHours": 0.6,
          "skillCode": "g",
          "skillName": "General",
          "hourlyRate": "$150.00",
          "rateCentsPerHour": 15000,
          "lineTotal": "$90.00"
        },
        {
          "operation": "Air Conditioning",
          "applicationId": 26903691,
          "name": "HVAC System Diagnosis & Testing",
          "serviceType": "Inspect",
          "bookHours": 0.6,
          "skillCode": "p",
          "skillName": "Precision",
          "hourlyRate": "$150.00",
          "rateCentsPerHour": 15000,
          "lineTotal": "$90.00"
        }
      ]
    }
  },
  "bulletins": {
    "status": "ok",
    "data": [
      {
        "applicationId": 474099311,
        "title": "A/C Leak Detection",
        "manufacturerNumber": "07-030",
        "issueDate": "2023-12-08T00:00:00",
        "tsbType": "Service Bulletin"
      },
      {
        "applicationId": 468452808,
        "title": "Air Conditioning System Performance Test",
        "manufacturerNumber": "96-012",
        "issueDate": "2011-02-11T00:00:00",
        "tsbType": "Service Bulletin"
      }
    ]
  },
  "specifications": {
    "status": "skipped",
    "note": "Specifications are quote-stage content; re-call with stage \"quote\" and the repair to price."
  },
  "parts": {
    "status": "skipped",
    "note": "Parts are returned in the quote stage."
  },
  "callTrace": [
    {
      "path": "/v1/Information/Vehicles/Search/ByVIN",
      "query": "VIN=2HGFA1F5",
      "status": 200,
      "ms": 159,
      "atMs": 2
    },
    {
      "path": "/v1/Information/Vehicles/Attributes/BaseVehicleID/22124/Content/Summaries/Of/EstimatedWorkTimes",
      "query": "ItemsPerPage=30&PageIndex=0&SearchTerm=Air Conditioning",
      "status": 200,
      "ms": 352,
      "atMs": 163
    },
    "… six further upstream calls"
  ]
}
```

Per-section `status` means partial answers degrade gracefully — a section can fail or be skipped without taking the whole answer down.

---

# issue_invoice

**Billing** · **Write · account-scoped**

Issue a real, payable invoice from the authenticated account's shop and get back a pay link.

## Usage

A write-capable tool — and the write goes to MotorAdvisor's payment provider (Stripe), never to MOTOR. MOTOR DaaS is read-only on this server without exception.

Three properties define its safety model:

- **The shop is never an input.** It resolves from the verified bearer token, so identical input from two accounts bills two different shops, and a caller cannot invoice as someone else.
- **Retry is safe.** The same `referenceId` (or an identical request) returns the *same* invoice — an agent that retries cannot double-bill a customer.
- **Money is recomputed server-side** in integer cents from the shop's stored rates. Totals computed by the client are ignored.

No email is sent by issuing. Relay the `payUrl` explicitly; never tell the user the customer has been notified.

When the work was quoted on this server, pass the approved quote's `quoteId`: the quote records `invoiceId`/`invoicedAt` (one money trail per job, readable via `check_quote_status`), and `void_quote` will refuse to retire it from then on.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `vehicleDescription` | string | No | The vehicle, for the invoice footer. Example: "2010 Honda Civic LX 1.8L" |
| `customer` | object | Yes | Who is being billed. Required — an invoice needs a recipient. Example: {"name": "Jane Rivera", "email": "jane@example.com"} |
| `customer.name` | string | Yes | Customer's name. Example: "Jane Rivera" |
| `customer.email` | string | Yes | Customer's email — where the shop will send the pay link. Example: "jane@example.com" |
| `customer.phone` | string | No | Customer's phone, optional. Example: "(313) 555-0144" |
| `labor` | array of object | Yes | The BOOKED labor selection (a completed selectedTotal choice), one entry per operation. Example: [{"name": "A/C Compressor — Remove & Replace", "bookHours": 1.2}] |
| `labor[].name` | string | No | The labor operation as MOTOR names it, from a service_advisor_lookup selection. Example: "A/C Compressor — Remove & Replace" |
| `labor[].bookHours` | number | No | Book hours for the operation. Example: 1.2 |
| `labor[].rateCentsPerHour` | positive integer | No | Rate for THIS line in integer cents/hour, when the answer carried a skill rate. Omit to bill at the shop's stored rate. Example: 15000 |
| `labor[].skillName` | string | No | Skill tier name shown on the line. Example: "Air Conditioning" |
| `parts` | array of object | No | Priced parts to bill, if any. Every part needs unitPriceCents. Example: [{"name": "A/C Compressor", "unitPriceCents": 12500}] |
| `parts[].name` | string | No | Part name as quoted. Example: "A/C Compressor" |
| `parts[].quantity` | positive integer | No | How many. Defaults to 1. Example: 1 |
| `parts[].unitPriceCents` | positive integer | No | SELL price in integer cents, set by the advisor — an unpriced part is refused. MOTOR provides the OEM LIST price (listPriceCents on quote parts rows) as an anchor; it is not the sell price and is never billed automatically. Example: 12500 |
| `referenceId` | string | No | Stable id for THIS repair order — reuse it on any retry so the same order can never be invoiced twice. When omitted, one is derived from the customer and lines, which also collapses identical retries. Example: "job_civic_ac_2026_08_12" |
| `quoteId` | string | No | The q_… id of the approved quote this invoice bills, when one exists. Linking it closes the money trail: the quote records the invoice, check_quote_status points at it, and void_quote refuses to retire it. Example: "q_1facac2305cc6b3901077d3b" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Issue a REAL invoice for completed repair work, from the shop the authenticated account belongs to, and get back a payment link to give the customer. THIS TOOL MOVES COMMERCIAL STATE: it creates a customer and a finalized invoice on the shop's own Stripe account (MotorAdvisor's payment provider), with the platform fee applied. It writes NOTHING to MOTOR — MOTOR DaaS is read-only on this server and this tool does not touch it. The shop is NEVER a parameter: it is resolved from the authenticated account, so this tool can only invoice for the caller's own shop. All money is recomputed server-side from the shop's stored rates in integer cents; totals you computed yourself are ignored. Use it AFTER a completed service_advisor_lookup selection (a selectedTotal exists) and after the advisor confirms the customer's name and email. Pass the booked labor lines as returned; do not invent operations or prices. When the work was quoted on this server, pass the approved quote's quoteId: the quote then records the invoice (one money trail per job) and void_quote refuses to retire it. Preconditions, each refused by name when unmet: the account must belong to a shop; the shop must have completed Stripe onboarding; every part must have a price; there must be at least one line. The result includes payUrl (MotorAdvisor's payment page for this invoice) and hostedInvoiceUrl (Stripe's fallback page). NO EMAIL IS SENT by issuing — relay the payUrl to the advisor explicitly, and never tell the user the customer has been emailed. Calling twice with the same referenceId returns the SAME invoice rather than a duplicate.

## Example — Invoice a completed A/C job

*Standing demo invoice, issued live 2026-08-12 over the MCP protocol. Pay-link token elided.*

**Request**

```json
{
  "vehicleDescription": "2010 Honda Civic LX 1.8L",
  "customer": {
    "name": "Jane Rivera",
    "email": "jane@example.com"
  },
  "labor": [
    {
      "name": "A/C Compressor — Remove & Replace",
      "bookHours": 1.2,
      "rateCentsPerHour": 15000
    }
  ],
  "referenceId": "job_civic_ac_2026_08_12"
}
```

**Response**

```json
{
  "invoiceId": "in_1U3eHUR4WQPNzEYlDhYqbFHn",
  "number": "ALRVAKWA-0001",
  "status": "open",
  "totalCents": 18000,
  "total": "$180.00",
  "platformFeeCents": 450,
  "dueDate": "2026-09-11",
  "payUrl": "https://motoradvisor.app/pay/…",
  "hostedInvoiceUrl": "https://invoice.stripe.com/…",
  "delivery": "link-only — no email was sent; give the customer the payUrl"
}
```

---

# check_invoice_status

**Billing** · **Read-only · account-scoped**

Check one invoice, or sweep every unpaid invoice for the shop — the receivables question.

## Usage

Two modes: pass `invoiceId` to check one invoice, or omit it to get every unpaid invoice, oldest first — the question a shop owner usually means by "what hasn't been paid?".

Read `paymentState`, not `status`. Stripe leaves a declined invoice at status `open`, identical to one nobody has tried to pay. `paymentState: "declined"` means a card was attempted and failed — say so plainly. `"awaiting-payment"` means no attempt has been made yet. Each result carries a human-readable `explanation`; relay it rather than inventing a reading.

No card details or payment credentials are returned, and none can be requested through this tool.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `invoiceId` | string | No | One invoice to check — either the Stripe id from an issue_invoice result (starts with "in_") or the printed invoice number. OMIT THIS to sweep every unpaid invoice for the shop instead. Example: "ALRVAKWA-0001" |
| `limit` | positive integer | No | Sweep mode only: how many unpaid invoices to return, oldest first. Defaults to 25. Example: 25 |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Check whether an invoice has been paid, or sweep every unpaid invoice for the shop. THIS TOOL IS READ-ONLY: it reads MotorAdvisor's payment provider (Stripe) and changes nothing there. It writes NOTHING to MOTOR — MOTOR DaaS is read-only on this server and this tool does not touch it. The shop is NEVER a parameter: it is resolved from the authenticated account, so this tool can only read the caller's own shop. TWO MODES. Pass invoiceId to check one invoice. OMIT invoiceId to get every unpaid invoice, oldest first — that is the receivables question ("what hasn't been paid?") and the one an owner usually means. READ paymentState, NOT status, when telling the user what happened. Stripe leaves a DECLINED invoice at status "open", identical to one nobody has tried to pay, so "open" alone does not mean the customer simply hasn't got to it. paymentState "declined" means a card was attempted and FAILED — say so plainly and tell the advisor to contact the customer; never report it as merely unpaid. paymentState "awaiting-payment" means no attempt has been made yet. Each result also carries a human-readable explanation — relay it rather than inventing your own reading. NO CARD DETAILS AND NO PAYMENT CREDENTIALS ARE RETURNED by this tool, and none can be requested through it. Preconditions, each refused by name when unmet: the account must belong to a shop; the shop must have a connected Stripe account; a named invoice must exist on it.

## Example — One invoice by printed number

*Captured from the live server, 2026-08-20 — the standing demo invoice.*

**Request**

```json
{
  "invoiceId": "ALRVAKWA-0001"
}
```

**Response**

```json
{
  "invoiceId": "in_1U3eHUR4WQPNzEYlDhYqbFHn",
  "number": "ALRVAKWA-0001",
  "status": "open",
  "paymentState": "awaiting-payment",
  "explanation": "Sent and not yet paid. No payment has been attempted.",
  "totalCents": 18000,
  "total": "$180.00",
  "amountPaidCents": 0,
  "amountPaid": "$0.00",
  "customerName": "Jane Rivera",
  "dueDate": "2026-09-11",
  "overdue": false,
  "overdueDays": 0,
  "lines": [
    {
      "description": "A/C Compressor — Remove & Replace — 1.2 hr @ $150.00/hr",
      "amountCents": 18000,
      "amount": "$180.00"
    }
  ]
}
```

---

# assess_repair_economics

**Repair economics** · **Read-only**

Weigh a quoted repair against the vehicle's worth and return an economic band — with the Black Book trade-in range for licensed shop accounts.

## Usage

MOTOR supplies what the repair costs; a licensed valuation provider (Black Book) supplies what the vehicle is worth; this tool is the comparison between them. The answer is a band — `proceed`, `caution`, `uneconomic`, or `unavailable` — plus caveats that are part of the answer, not decoration.

Who receives the figures depends on the caller. A **licensed shop account** (OAuth) receives what the advisor's own screen shows: `tradeInValue` — the Black Book trade-in range the band was decided on, rough to average condition, with the basis (adjusted for odometer and region, or base) and whether mileage was applied — plus `repairCostCents` (the total weighed) and `ratio`, with `valuesWithheld: false` and an `attribution` to repeat alongside any figure quoted. Quote the value as a range, never a single number.

A **self-serve developer key** receives the band only, with `valuesWithheld: true`: valuations are licensed content, and a ratio plus a known numerator would reconstruct the licensed figure exactly. When values are withheld, do not ask the user to fill the gap and do not estimate one.

`unavailable` is a real and common answer (no valuation match, out-of-coverage vehicle, no repair total). Report it as "the comparison could not be made" — it never means the vehicle is worthless. The verdict is information, not a decision: it does not authorise, refuse, or recommend anything.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `vin` | string | Yes | The vehicle's VIN — full 17 characters, or the 9- or 10-character partial forms. This is the ONLY vehicle identifier these tools accept: it is the one key both MOTOR and Black Book understand. Do NOT pass a baseVehicleId (MOTOR's identifier) or a uvc (Black Book's) here. Example: "2HGFA1F5A" |
| `repairCostCents` | positive integer | Yes | The repair being weighed, in integer cents — the SELECTED total from a service_advisor_lookup, never the sum of the candidate menu. Example: 177000 for a $1,770.00 repair |
| `mileage` | positive integer | No | Current odometer reading in miles. Strongly recommended: without it the valuation assumes normal mileage for the model year, and a high-mileage vehicle is worth materially less than that. Example: 62000 |
| `state` | string | No | US state abbreviation or ZIP for regional adjustment. The shop's location is a reasonable default. Example: "48226" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Weigh a quoted repair against what the vehicle is worth, and return an economic band. MOTOR supplies what the repair costs; a valuation provider supplies what the vehicle is worth; this tool is the comparison between them, for a vehicle identified by VIN. Returns {band, label, headline, detail, caveats, vehicle, confidence, valuesWithheld} and, for a licensed shop account, {tradeInValue, repairCostCents, ratio, attribution}. band is one of: "proceed" (the repair is a small share of the vehicle's value), "caution" (a material share — worth a conversation before booking), "uneconomic" (more than the share at which insurers write a vehicle off), or "unavailable". tradeInValue is the Black Book trade-in range the band was decided on — rough to average condition, low to high — with the basis (adjusted for the odometer and region, or base) and whether mileage was applied; ratio is the repair as a share of that range. Quote them as a range, never as a single figure, and repeat the attribution. Read the caveats out; they are not decoration. They state when no odometer was applied, when the two vendors disagree about the trim, and that valuations are by trim rather than by individual car. "unavailable" is a real and common answer — no valuation match, a vehicle outside the provider's coverage, or no repair total supplied. Report it as "the comparison could not be made"; it never means the vehicle is worthless, and the repair estimate itself is unaffected. THE VERDICT IS INFORMATION, NOT A DECISION. It does not authorise, refuse, or recommend anything. Never tell a user their vehicle should be scrapped, written off, or abandoned on the strength of this result, and never present it as advice — a shop and its customer make that call with context this tool does not have. Vehicle valuations are licensed content (Black Book). Who receives the figures depends on the caller: a licensed shop account (OAuth) receives the trade-in value range, the repair total weighed, and the share of value, with valuesWithheld false and an attribution to repeat alongside any figure you quote. A self-serve developer key receives the BAND only — valuesWithheld true, no dollar figures, no percentages. When valuesWithheld is true, do not ask the user for the vehicle's value to fill the gap, do not estimate one, and do not present a figure of your own as though it came from here. Vehicle resolution runs through MOTOR's evaluation sandbox, which covers model years 2010, 2015, and 2016 ONLY — a VIN outside those years will not resolve. That is sandbox coverage, not an error, and not a fact about the vehicle.

## Example — A $1,770 repair on a 2010 Civic

*Example for a licensed shop account; the figures are the live valuation of this vehicle on 2026-09-17. A developer key receives the same response without tradeInValue, repairCostCents, ratio or attribution, and with valuesWithheld: true.*

**Request**

```json
{
  "vin": "2HGFA1F5A",
  "repairCostCents": 177000,
  "mileage": 62000,
  "state": "MI"
}
```

**Response**

```json
{
  "band": "caution",
  "label": "Worth a conversation",
  "headline": "This repair is a material share of the vehicle's value.",
  "detail": "The repair is 57.2% to 95.9% of the vehicle's trade-in value, depending on its condition. Worth raising with the customer before booking the work.",
  "caveats": [
    "Black Book values by trim, not by individual car — condition, history, and options are not reflected.",
    "In poorer condition this repair would fall into the next band up. The figure shown takes the more favourable reading."
  ],
  "mileageApplied": true,
  "confidence": "corroborated",
  "vehicle": "2010 Honda Civic LX",
  "uvc": "2010360047",
  "tradeInValue": {
    "market": "tradein",
    "lowCents": 184500,
    "highCents": 309500,
    "low": "$1,845",
    "high": "$3,095",
    "condition": {
      "low": "rough",
      "high": "average"
    },
    "basis": "adjusted",
    "mileageApplied": true
  },
  "repairCostCents": 177000,
  "ratio": {
    "lowBps": 5719,
    "highBps": 9593,
    "low": "57.2%",
    "high": "95.9%"
  },
  "valuesWithheld": false,
  "attribution": "Values supplied by Black Book®. ©2026 Hearst Business Media Corp. ALL RIGHTS RESERVED. Black Book® is a registered trademark of Hearst Business Media Corp."
}
```

---

# get_vehicle_value

**Repair economics** · **Read-only**

Confirm the valuation provider knows the vehicle — coverage and corroboration, with the trade-in range for licensed shop accounts.

## Usage

Use this for corroboration and coverage: confirming a VIN resolves to one vehicle rather than several, confirming the trim two independent vendors agree on, and confirming a valuation exists before asking `assess_repair_economics` for a verdict.

A **licensed shop account** (OAuth) also receives `tradeInValue` — the Black Book trade-in range, rough to average condition, the same range `assess_repair_economics` weighs a repair against — with `valuesWithheld: false` and an `attribution` to repeat with it. A **self-serve developer key** receives `valuesWithheld: true`: which markets and condition grades the provider holds data for, never the figures themselves.

If the VIN matches more than one trim the result is `{ambiguous: true, candidates: [...]}`. Present the candidates; trims differ in value by hundreds of dollars, and a confident answer about the wrong one is the worst available outcome.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `vin` | string | Yes | The vehicle's VIN — full 17 characters, or the 9- or 10-character partial forms. This is the ONLY vehicle identifier these tools accept: it is the one key both MOTOR and Black Book understand. Do NOT pass a baseVehicleId (MOTOR's identifier) or a uvc (Black Book's) here. Example: "2HGFA1F5A" |
| `mileage` | positive integer | No | Current odometer reading in miles. Strongly recommended: without it the valuation assumes normal mileage for the model year, and a high-mileage vehicle is worth materially less than that. Example: 62000 |
| `state` | string | No | US state abbreviation or ZIP for regional adjustment. The shop's location is a reasonable default. Example: "48226" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Identify a vehicle in the valuation provider's taxonomy and report what valuation data exists for it — and, for a licensed shop account, what it is worth. Returns {uvc, vehicle, matchedBy, trimLevel, candidateCount, marketsAvailable, conditionGrades, valuesWithheld} and, for a licensed shop account, {tradeInValue, attribution}. marketsAvailable names which markets the provider holds data for; conditionGrades names the condition ladder. tradeInValue is the Black Book trade-in range — rough to average condition, low to high — the same range assess_repair_economics weighs a repair against; quote it as a range and repeat the attribution. Its other uses are corroboration and coverage: confirming a VIN resolves to one vehicle rather than several, confirming the trim two independent vendors agree on, and confirming a valuation exists at all before asking assess_repair_economics for a verdict. If the result is {ambiguous: true, candidates: [...]}, the VIN matched more than one trim — present the candidates and ask the user which vehicle it is. Do NOT pick one: trims differ in value by hundreds of dollars and a confident answer about the wrong one is the worst available outcome. Vehicle valuations are licensed content (Black Book). Who receives the figures depends on the caller: a licensed shop account (OAuth) receives the trade-in value range, the repair total weighed, and the share of value, with valuesWithheld false and an attribution to repeat alongside any figure you quote. A self-serve developer key receives the BAND only — valuesWithheld true, no dollar figures, no percentages. When valuesWithheld is true, do not ask the user for the vehicle's value to fill the gap, do not estimate one, and do not present a figure of your own as though it came from here. To weigh a repair against the vehicle's worth, call assess_repair_economics, which returns a band. Vehicle resolution runs through MOTOR's evaluation sandbox, which covers model years 2010, 2015, and 2016 ONLY — a VIN outside those years will not resolve. That is sandbox coverage, not an error, and not a fact about the vehicle.

## Example — Coverage check by VIN

*Example for a licensed shop account; the figures are the live valuation of this vehicle on 2026-09-17. A developer key receives the same response without tradeInValue or attribution, and with valuesWithheld: true.*

**Request**

```json
{
  "vin": "2HGFA1F5A",
  "mileage": 62000,
  "state": "MI"
}
```

**Response**

```json
{
  "uvc": "2010360047",
  "vehicle": "2010 Honda Civic LX",
  "matchedBy": "vin",
  "trimLevel": true,
  "candidateCount": 1,
  "confidence": "corroborated",
  "marketsAvailable": [
    "wholesale",
    "tradein",
    "retail"
  ],
  "conditionGrades": [
    "rough",
    "average",
    "clean",
    "xclean"
  ],
  "mileageApplied": true,
  "tradeInValue": {
    "market": "tradein",
    "lowCents": 184500,
    "highCents": 309500,
    "low": "$1,845",
    "high": "$3,095",
    "condition": {
      "low": "rough",
      "high": "average"
    },
    "basis": "adjusted",
    "mileageApplied": true
  },
  "valuesWithheld": false,
  "attribution": "Values supplied by Black Book®. ©2026 Hearst Business Media Corp. ALL RIGHTS RESERVED. Black Book® is a registered trademark of Hearst Business Media Corp."
}
```

---

# create_quote_snapshot

**Notifications** · **Write · account-scoped · server-priced**

Turn a completed labor selection into the shop's quote snapshot and get the quoteId the send tool takes.

## Usage

The bridge between the read surface and `send_quote_notification`: a completed `service_advisor_lookup` ends at `selectedTotal`, and this tool turns that selection into MotorAdvisor's own commercial record — returning the `q_…` id, the public approval `quoteUrl`, and the priced lines.

**The caller supplies identifiers, never money.** Hours, names, and prices are re-read from MOTOR live at snapshot time, so a figure that was never computable cannot be snapshotted and no price in the record came from model output. The persisted record carries exact-cent money and MOTOR content identifiers only — descriptive MOTOR text is re-materialized on every render, never stored (invariant 4). MOTOR itself is only ever read; the write lands in MotorAdvisor's store and appears in the call trace as its own step.

`customerContact` is a lookup key, not an address: it links the shop's existing consent record to the quote so `send_quote_notification` can deliver it. A contact with no consent on file leaves the quote created but unlinked — sending refuses with `no-consent`, and the advisor shares `quoteUrl` by hand, exactly like the web flow.

**The estimate says whom it is for (MOT-339).** Pass `customerName` and the approval page and the quote email carry a "Prepared for" line — the name, plus `customerContact` printed as the email or phone it is by shape. Without a name the estimate carries no customer line; the contact alone stays a lookup key.

Manual lines (`manualLines`) carry the shop's own figures for work with no MOTOR book time — recharge, sublet, shop supplies — at the shop's stored rate unless a rate is given.

**Parts ride on the quote (MOT-325).** `parts` takes the parts the customer approves together with the labor, in the same shape `issue_invoice` bills: `name`, optional `oemPartNumber` and `quantity`, and the advisor's `unitPriceCents` — the SELL price, never MOTOR's list price, which a lookup returns as an anchor only. A part without a sell price refuses the call, exactly as the web quote refuses to send with one unpriced. The response reports the parts, `laborTotal`, `partsTotal` and the full `total` the customer will approve; the customer's approval page lists the parts under the labor.

The snapshot is created `open` — it reads as `sent` only after `send_quote_notification` actually delivers it. When this quote replaces an earlier one (findings grew the work), pass `supersedes` with the old `q_…` id: approval of this quote retires the old one to a terminal `superseded` status and its approval link goes dead, so two live approvals can never both be billed.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `baseVehicleId` | positive integer | Yes | The baseVehicleId field from a resolve_vehicle result. NOT the vehicleId. Example: 22124 |
| `vehicleDescription` | string | Yes | The resolved vehicle identity, from resolve_vehicle's description. Example: "2010 Honda Civic LX" |
| `applicationIds` | array of positive integer | Yes | The labor operations the advisor selected — the same applicationIds a completed service_advisor_lookup costed into selectedTotal (for a variant group, the specific variant's id). Hours and prices are re-read from MOTOR server-side; ids that match no work time refuse the whole call. Example: [23615369] |
| `laborRateCents` | positive integer | No | Labor rate in integer cents/hour. Omit to use the shop's stored rate. Example: 15000 |
| `manualLines` | array of object | No | Shop-authored lines with no MOTOR book time (recharge, sublet, shop supplies — parts go in parts). Priced at the given hours — these are the shop's own figures. Example: [{"name": "Refrigerant recharge", "bookHours": 0.5}] |
| `manualLines[].name` | string | No | Shop-authored line text. Example: "Refrigerant recharge" |
| `manualLines[].bookHours` | number | No | Hours for this line. Example: 0.5 |
| `manualLines[].rateCentsPerHour` | positive integer | No | Line-specific rate in cents/hour; omit for the shop rate. Example: 15000 |
| `parts` | array of object | No | MOT-325: the priced parts the customer approves together with the labor — the same shape issue_invoice bills. Every part needs the advisor's unitPriceCents. Omit when the quote is labor only. Example: [{"name": "A/C Compressor", "oemPartNumber": "38810-RNA-A02", "unitPriceCents": 103035}] |
| `parts[].name` | string | No | Part name as quoted. Example: "A/C Compressor" |
| `parts[].oemPartNumber` | string | No | OEM part number, when known. Example: "38810-RNA-A02" |
| `parts[].quantity` | positive integer | No | How many. Defaults to 1. Example: 1 |
| `parts[].unitPriceCents` | positive integer | No | SELL price in integer cents, set by the advisor — a part without one is refused, as the web quote and issue_invoice refuse it. MOTOR provides the OEM LIST price (listPriceCents on a lookup's parts rows) as an anchor; it is not the sell price and is never quoted automatically. Example: 103035 |
| `customerContact` | string | No | The customer's email or phone, used ONLY to link the shop's existing consent record to this quote so send_quote_notification can reach them. It is never a send destination: a contact with no consent on file leaves the quote unlinked and sending will refuse with no-consent. With customerName it is also printed on the estimate page as the contact the estimate was prepared for. Example: "casey@example.com" |
| `customerName` | string | No | MOT-339: the customer's name, printed on the estimate page and in the quote email as whom the estimate was prepared for — the same name the shop wrote on the work order. Omit it and the estimate carries no customer line. One line. Example: "Casey Rivera" |
| `supersedes` | string | No | The q_… id of the shop's earlier quote this one REPLACES — use it when findings grow the work (diagnose → full quote). When the customer approves THIS quote, the superseded one retires: terminal status, dead approval link, and check_quote_status points from it to this one. Example: "q_1facac2305cc6b3901077d3b" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Create the shop's quote snapshot from a COMPLETED labor selection and return its quoteId (q_…) — the bridge between service_advisor_lookup and send_quote_notification. THIS TOOL WRITES a commercial record owned by the authenticated account's shop (MOTOR itself stays read-only; the call trace shows the write as its own step). Pass the applicationIds a lookup costed into selectedTotal — hours, names, and prices are re-read from MOTOR server-side, so a total that was never computable cannot be snapshotted and no labor figure is taken from input. Parts the customer approves go in parts, each with the advisor's SELL price (the customer approves labor and parts together, as on the web); an unpriced part refuses the call. The persisted record holds exact-cent money and MOTOR content identifiers only, never MOTOR text. Returns quoteId, the public approval quoteUrl (shareable by hand), the priced lines, and the total. The quote is created OPEN — it reads as sent only after send_quote_notification actually delivers it. When this quote REPLACES an earlier one (findings grew the work), pass supersedes with the old q_… id: approval of this quote retires the old one so two live approvals can never both be billed. Use create_quote_snapshot AFTER the advisor confirms the selection; then send_quote_notification with the quoteId. The evaluation sandbox covers model years 2010, 2015, and 2016 ONLY — an UnknownApplicationIds refusal for another vehicle is correct behaviour, not an error.

## Example — Snapshot a compressor R&R selection

*Illustrative payload shapes (ids and token elided); the money is the server's own computation at the shop's $150/hr rate.*

**Request**

```json
{
  "baseVehicleId": 22124,
  "vehicleDescription": "2010 Honda Civic LX",
  "applicationIds": [
    23615369
  ],
  "parts": [
    {
      "name": "Compressor Assembly",
      "oemPartNumber": "38810-RNA-A02",
      "unitPriceCents": 103035
    }
  ],
  "customerContact": "customer@example.com",
  "customerName": "Casey Rivera"
}
```

**Response**

```json
{
  "quoteId": "q_9c41f0b2ae77d31c55e00e12",
  "quoteUrl": "https://motoradvisor.app/q/…",
  "vehicle": "2010 Honda Civic LX",
  "lines": [
    {
      "name": "Air Conditioning Compressor R&R",
      "hours": 1.9,
      "amount": "$285.00"
    }
  ],
  "parts": [
    {
      "name": "Compressor Assembly",
      "oemPartNumber": "38810-RNA-A02",
      "quantity": 1,
      "unitPrice": "$1,030.35",
      "amount": "$1,030.35"
    }
  ],
  "laborTotal": "$285.00",
  "partsTotal": "$1,030.35",
  "total": "$1,315.35",
  "expiresAt": "2026-08-28T13:30:00.000Z",
  "consentNote": "Consent on file for this contact — send_quote_notification can deliver this quote."
}
```

applicationIds that match no work time on the vehicle refuse the whole call (`UnknownApplicationIds`) — nothing is silently dropped, and nothing is snapshotted. A part without unitPriceCents refuses the same way (`InvalidInput`).

---

# check_quote_status

**Notifications** · **Read-only · account-scoped**

Where a quote stands with the customer — decision, expiry, and whether the message actually arrived.

## Usage

The approval-side twin of `check_invoice_status`: after a quote goes out, this answers "did they approve?" without leaving the client. Read-only in every direction — it reads MotorAdvisor's own quote record and the notification audit trail, and touches neither MOTOR nor the payment provider.

`status` is one of `open` (created, no message out yet — `sentAt` absent), `sent` (`sentAt` is the first actual delivery, matching the trail), `approved`, `declined` (with `declineReason` and `decidedAt`), `expired`, `voided` (with `voidedAt`/`voidReason`), or `superseded` (with `supersededBy` naming the replacement). An invoiced quote carries `invoiceId`/`invoicedAt` — the money trail closed. An approved quote is the cue to do the work and then `issue_invoice`; a declined one is a customer decision to relay.

`delivery` carries the latest audit-trail state per channel — `sent`, `delivered`, `bounced`, or `deferred`. A bounce means the customer likely never saw the quote: say so and suggest another way to reach them rather than assuming they are ignoring it. When no notification was ever sent, `deliveryNote` says so — the advisor may have shared the link by hand.

The shop resolves from the authenticated account; another shop's quoteId reads as not found.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `quoteId` | string | Yes | The quote snapshot id from create_quote_snapshot or the advisor's work order. Example: "q_1facac2305cc6b3901077d3b" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Check where a quote stands with the customer: open (created, no message out yet), sent (sentAt = actual first delivery), approved, declined (with their reason), expired, voided, or superseded (with the replacing quote's id) — plus whether the notification actually arrived (per-channel delivery state from the audit trail: sent, delivered, bounced, or deferred until a stated time). THIS TOOL IS READ-ONLY EVERYWHERE: it reads MotorAdvisor's own records and touches neither MOTOR (DaaS stays read-only) nor the payment provider. The shop is NEVER a parameter: it resolves from the authenticated account, so this tool can only read the caller's own quotes — another shop's quoteId reads as not found. Relay decidedAt and the decline reason when present; a bounced delivery means the customer likely never saw the quote — say so and suggest another way to reach them rather than assuming they are ignoring it. An approved quote is the cue to do the work and then issue_invoice; a declined one is a customer decision to relay, never to argue with.

## Example — Check an open quote after sending

*Illustrative payload shapes (ids and token elided); statuses come from the live audit trail.*

**Request**

```json
{
  "quoteId": "q_9c41f0b2ae77d31c55e00e12"
}
```

**Response**

```json
{
  "quoteId": "q_9c41f0b2ae77d31c55e00e12",
  "status": "sent",
  "vehicle": "2010 Honda Civic LX",
  "total": "$285.00",
  "sentAt": "2026-08-21T13:30:00.000Z",
  "expiresAt": "2026-08-28T13:30:00.000Z",
  "quoteUrl": "https://motoradvisor.app/q/…",
  "delivery": [
    {
      "channel": "email",
      "status": "delivered",
      "sentAt": "2026-08-21T13:30:02.000Z",
      "deliveredAt": "2026-08-21T13:30:04.000Z"
    }
  ]
}
```

An expired-but-undecided quote reads as `expired` — create and send a fresh one rather than re-sending a dead link.

---

# void_quote

**Notifications** · **Write · account-scoped**

Retire one of the shop's own quotes — a stale approval can never be billed once it is voided.

## Usage

The terminal transition the quote lifecycle was missing (PDW-33): when the scope of work grows and a replacement quote takes over, the old quote — even an *approved* one — can be taken out of play instead of sitting invoiceable forever. Voiding writes only to MotorAdvisor's own store; MOTOR and the payment provider are untouched.

Voiding kills the public approval link immediately: the customer's page shows the generic "link isn't available" dead end, and a decision POST returns 410. The void is idempotent — voiding an already-voided quote is a no-op, not an error.

**The one refusal that matters is by name: `QuoteInvoiced`.** A quote that `issue_invoice` billed (linked via its `quoteId` parameter) will not void, because its approval is the shop's evidence the billed work was authorized. Never retry around it — handle any correction on the invoice side.

Prefer `supersedes` on `create_quote_snapshot` when a replacement quote exists: approval of the replacement retires the original automatically and links the chain with `supersededBy`. `void_quote` is for retiring a quote outright.

The shop resolves from the authenticated account; another shop's quoteId reads as not found.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `quoteId` | string | Yes | The quote snapshot id to void. Example: "q_1facac2305cc6b3901077d3b" |
| `reason` | string | No | Why the shop is retiring it, for the record. Example: "replaced by the full brake quote after inspection" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Void one of the shop's own quotes — open, sent, or even approved — so a stale approval can never be billed. THIS TOOL RETIRES A COMMERCIAL RECORD owned by the authenticated account's shop: the quote moves to a terminal voided status and its public approval link goes dead immediately. It writes NOTHING to MOTOR (DaaS stays read-only) and nothing to Stripe. An INVOICED quote refuses by name (QuoteInvoiced): its approval is the evidence the billed work was authorized, and that chain is never cut — never retry around this refusal. A declined or superseded quote is already closed and refuses too; voiding an already-voided quote is a no-op, not an error. Prefer supersedes on create_quote_snapshot when a REPLACEMENT quote exists — it retires the old quote automatically on approval and links the chain; void_quote is for retiring a quote outright. The shop is NEVER a parameter: it resolves from the authenticated account, so this tool can only void the caller's own quotes — another shop's quoteId reads as not found.

## Example — Retire the diagnose-stage quote a full quote replaced

*Illustrative payload shapes (ids elided); the statuses are the live machine's own words.*

**Request**

```json
{
  "quoteId": "q_9c41f0b2ae77d31c55e00e12",
  "reason": "replaced by the full brake quote after inspection"
}
```

**Response**

```json
{
  "quoteId": "q_9c41f0b2ae77d31c55e00e12",
  "status": "voided",
  "previousStatus": "approved",
  "voidedAt": "2026-08-24T21:30:00.000Z",
  "voidReason": "replaced by the full brake quote after inspection",
  "linkNote": "The public approval link for this quote is now dead."
}
```

An invoiced quote refuses with `QuoteInvoiced` — the approval that authorized billed work is never cut out of the record.

---

# send_quote_notification

**Notifications** · **Write · account-scoped · consent-gated**

Send the customer their quote-approval link — on the channels they consented to, and no others.

## Usage

The tool that completes the loop: resolve the vehicle, price the work, and the customer gets the quote — without the advisor leaving the conversation. It sends a real message on behalf of the authenticated account's shop, and it writes nothing to MOTOR.

**The safety model is the argument shape.** There is deliberately no destination parameter. The consent records the customer signed at the shop counter are the only reachable addresses, so no input exists through which a caller can direct a message at an arbitrary email or phone number. Do not ask the user for an address to send to — none can be supplied.

Each requested channel returns its own structured outcome rather than an error:

- `sent` — the message went out, and the send is recorded in the shop's notification audit trail.
- `deferred` — the customer is inside quiet hours (outside the 8am–9pm customer-local send window); the send is queued and `resumeAt` says when it goes out.
- `refused` with a `reason` — `no-consent`, `suppressed` (opted out or hard-bounced), `sms-transport-unavailable`, and so on. **A refusal is the system working.** Relay the reason; never retry around it, and never try a different channel to reach a suppressed destination.

The same gates the web app applies — consent, platform suppression, quiet hours, the audit trail — apply here identically, from the same shared machinery. The result always includes `quoteUrl`, so the advisor can hand the link over in person when sending is refused. Retries are idempotent per quote and channel.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `quoteId` | string | Yes | The quote snapshot id, e.g. from the advisor's work order. Example: "q_1facac2305cc6b3901077d3b" |
| `channels` | array of "email" \| "sms" | Yes | Which channels to send on. Example: ["email"] |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Send the customer their quote-approval link on the channels they have CONSENTED to. THIS TOOL SENDS REAL MESSAGES on behalf of the authenticated account's shop. It writes NOTHING to MOTOR — DaaS stays read-only. There is deliberately NO destination parameter: the shop's consent records (captured from the customer at the counter) are the only reachable addresses, so this tool cannot message anyone who has not opted in — do not ask for a phone number or email to send to, none can be supplied. Each requested channel returns a structured outcome: sent, deferred (quiet hours — queued until the stated time), or refused with a reason (no-consent, consent-scope-mismatch when the consent on file covers invoices only, suppressed, sms-transport-unavailable, …). A refusal is the system working; relay the reason, never retry around it. The quote must belong to the caller's shop and still be open; the result includes quoteUrl either way, which the advisor can always share by hand.

## Example — Send a quote for approval — after 9pm

*Captured from the live server, 2026-08-21. The call landed outside the 8am–9pm send window, so the response shows the quiet-hours deferral and a structured refusal side by side.*

**Request**

```json
{
  "quoteId": "q_5bb2f458a5aae3c4c0fa354d",
  "channels": [
    "email",
    "sms"
  ]
}
```

**Response**

```json
{
  "notifications": [
    {
      "channel": "email",
      "status": "deferred",
      "resumeAt": "2026-08-21T12:00:00.000Z",
      "reason": "quiet-hours: outside the 8am-9pm customer-local send window"
    },
    {
      "channel": "sms",
      "status": "refused",
      "reason": "no-consent: the customer has not opted in to sms for this shop — capture consent at the counter first"
    }
  ],
  "quoteUrl": "https://motoradvisor.app/q/…"
}
```

A quote belonging to another shop reads as absent (`QuoteNotFound`) — the same cross-tenant stance as the billing tools.

---

# send_invoice_notification

**Notifications** · **Write · account-scoped · consent-gated**

Send the customer the payment link for an open invoice — on the channels they consented to, and no others.

## Usage

The pay-link delivery hop: `issue_invoice` deliberately sends nothing, and this tool closes that gap with the same gates the web applies at issuance (MOT-127). It sends a real message on behalf of the authenticated account's shop; it writes nothing to MOTOR and nothing to Stripe — the invoice is only read, live, before sending.

**The safety model is the argument shape.** There is no destination parameter. The invoice's own billing email — set when the advisor issued it — is the only candidate address, and it must *also* hold a signed consent record for this shop: the invoice knowing an address is not permission to message it.

**The status gate runs first.** The invoice is read live from Stripe; `paid` or `void` refuses with `InvoiceNotPayable` and nothing is sent — a payment reminder to someone who already paid is the exact failure this gate exists to prevent. Never retry around it.

Each requested channel returns its own structured outcome (`sent`, `deferred` with `resumeAt` under quiet hours, or `refused` with a reason), and every send lands in the shop's notification audit trail under the same idempotency key the web path uses — an MCP send racing a web re-send converges at the provider. The result always includes `payUrl`, so the advisor can hand it over when sending is refused.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `invoiceId` | string | Yes | The invoice to send the pay link for — the Stripe id from an issue_invoice result (starts with "in_") or the printed invoice number. Example: "ALRVAKWA-0001" |
| `channels` | array of "email" \| "sms" | Yes | Which channels to send on. Example: ["email"] |
| `vehicleDescription` | string | No | The vehicle line for the email, e.g. from resolve_vehicle. Omit and the email simply skips it. Example: "2010 Honda Civic LX" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Send the customer the payment link for an OPEN invoice, on the channels they have CONSENTED to. THIS TOOL SENDS REAL MESSAGES on behalf of the authenticated account's shop. It writes NOTHING to MOTOR — DaaS stays read-only — and nothing to Stripe: the invoice is only read, live, before sending. There is deliberately NO destination parameter: the invoice's own billing email must ALSO hold a signed consent record for this shop, so this tool cannot message anyone who has not opted in — do not ask for an address to send to, none can be supplied. A paid or voided invoice refuses with a structured reason (invoice-not-payable) — a payment reminder to someone who already paid is the failure this gate exists to prevent; never retry around it. Each requested channel returns a structured outcome: sent, deferred (quiet hours — queued until the stated time), or refused with a reason (no-consent, consent-scope-mismatch when the consent on file covers quotes only, suppressed, sms-transport-unavailable, …). A refusal is the system working; relay the reason. The result includes payUrl either way, which the advisor can always share by hand. The shop is NEVER a parameter: it resolves from the authenticated account, so this tool can only send for the caller's own invoices.

## Example — Send the pay link after issuing

*Illustrative payload shapes (ids elided); outcomes are the live gates' own words.*

**Request**

```json
{
  "invoiceId": "ALRVAKWA-0001",
  "channels": [
    "email"
  ],
  "vehicleDescription": "2010 Honda Civic LX"
}
```

**Response**

```json
{
  "notifications": [
    {
      "channel": "email",
      "status": "sent"
    }
  ],
  "payUrl": "https://motoradvisor.app/pay/…"
}
```

A paid invoice refuses with `InvoiceNotPayable` ("already paid — payment reminders are suppressed") — the same 409 semantics as the web re-send route, as a structured refusal.

---

# capture_consent

**Notifications** · **Write · account-scoped**

Record a customer's notification consent at the counter — the record every send tool requires.

## Usage

The intake step for a pure-MCP relationship: without a consent record, every send tool on this server correctly refuses with `no-consent`. This tool creates that record — and it writes a **legal consent record**, so the capture is two steps by construction:

1. **Call without `attested`** — nothing is written. The response is the exact disclosure text to read to the customer **verbatim** (no paraphrase, summary, or translation), plus its version.
2. **After the customer clearly agrees**, re-call with `attested: true` and the `disclosureVersion` from step 1. A version the server doesn't know — or one that's no longer current — refuses, because a consent pointing at text that was never shown is worse than no record at all.

The record stores the normalized destination, the exact disclosure version, and method `verbal_attested` — the audit trail never dresses an agent-mediated verbal capture up as a signed form. The response includes the `customerId` the quote and invoice send paths match on (generated when the shop has no reference of its own).

There is **no revoke through this surface**: customers opt out themselves via the link in any email or STOP by SMS, and a destination on the suppression list stays unreachable even with a fresh consent — the capture succeeds but says so.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `channel` | "email" \| "sms" | Yes | Which channel the customer is opting into. Example: "email" |
| `scope` | "quote" \| "invoice" \| "both" | Yes | What the shop may send: quote approvals, invoice pay links, or both. Example: "both" |
| `destination` | string | Yes | The customer's email or phone, exactly as they gave it — it is normalized before storage. Example: "casey@example.com" |
| `customerId` | string | No | The shop's reference for this customer, when one exists (e.g. from the work order). Omit and a stable reference is generated. Example: "cust_casey" |
| `attested` | boolean | No | Pass true ONLY after the disclosure text (from calling this tool without attested) was read to the customer verbatim and they agreed. Example: true |
| `disclosureVersion` | string | No | Required with attested: the version from the disclosure prompt, pinning exactly which text the customer heard. Example: "2026-08-19.v1" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Record a customer's notification consent at the counter — the record every send tool on this server requires before it will message anyone. THIS TOOL WRITES A LEGAL CONSENT RECORD for the authenticated account's shop; it writes NOTHING to MOTOR — DaaS stays read-only. CAPTURE IS TWO STEPS AND THE FIRST WRITES NOTHING: call without `attested` to receive the exact disclosure text — READ IT TO THE CUSTOMER VERBATIM (do not paraphrase, summarize, or translate it) — then, only after they clearly agree, re-call with attested: true and the disclosureVersion from the prompt. Calling attested without the customer having heard the disclosure creates a false legal record — never do it. The record stores the normalized destination, the exact disclosure version, and method "verbal_attested"; the response includes the customerId that quote and invoice sends will match on. There is NO revoke through this surface: the customer opts out themselves via the link in any message (email) or STOP (SMS), and a destination that opted out stays unreachable even with a fresh consent. The shop is NEVER a parameter: it resolves from the authenticated account.

## Example — Two-step capture at the counter

*Illustrative payload shapes; the disclosure text is the live current version, verbatim.*

**Request**

```json
{
  "channel": "email",
  "scope": "both",
  "destination": "casey@example.com",
  "attested": true,
  "disclosureVersion": "2026-08-19.v1"
}
```

**Response**

```json
{
  "captured": true,
  "consentId": "cons_…",
  "customerId": "cust_mcp_…",
  "channel": "email",
  "scope": "both",
  "destination": "casey@example.com",
  "disclosureVersion": "2026-08-19.v1",
  "method": "verbal_attested",
  "capturedAt": "2026-08-21T15:10:00.000Z"
}
```

The same call without `attested` returns `{captured: false, disclosureText, disclosureVersion, instruction}` and writes nothing — that's the step where the customer actually hears the disclosure.

---

# get_job

**Jobs** · **Read-only · account-scoped**

One job's durable spine — board stage, linked quote summaries, and the invoice by reference.

## Usage

The continuity read over the durable job record (MOT-198): from any client, "where does this repair order stand, and where is the money" — without the browser session that produced it. Read-only in every direction: it reads MotorAdvisor's own store and makes no MOTOR call.

The job record itself holds identifiers, stage, and links only. Everything else in the result is composed at read time from the linked records: each quote contributes its stage, status, `totalCents` (integer cents), `sentAt`/`decidedAt`, and its stored `vehicleDescription` — the one piece of descriptive text on this surface. Operation names and book times are not persisted anywhere and are deliberately **not** re-materialized here; this is a continuity read, not a render surface.

The invoice appears by reference. Stripe is the system of record for its lifecycle, so `invoice.status` says `"paid"` only once the job itself has reached the paid stage — for live payment state, use `check_invoice_status`. For a quote's delivery trail and approval link, use `check_quote_status`.

The shop resolves from the authenticated account; another shop's jobId reads as not found.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `jobId` | string | Yes | The job id from list_jobs or the advisor's board. Example: "job_m1abc2de_x7k4q9" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> Read one job's durable spine: its stage on the board, timestamps (including stageEnteredAt — when the current stage began), the shop-authored customer-status fields where set (waitingOnParts, partsEta as a YYYY-MM-DD date, pickupHours as one short line), and composed summaries of the linked money records — each quote (stage, status, totalCents in integer cents, sentAt, decidedAt, vehicleDescription) and the invoice by reference (invoiceId, with status "paid" only once the job itself has reached the paid stage — for live payment state use check_invoice_status). This is a CONTINUITY read, not a render surface: operation names, book times and other descriptive content are not on the record and are NOT re-materialized here — the only descriptive text returned is each quote's stored vehicleDescription. Use check_quote_status for a quote's delivery trail and approval link. THIS TOOL IS READ-ONLY EVERYWHERE: it reads MotorAdvisor's own job store and touches neither MOTOR (no content call is made, and no MOTOR text is persisted anywhere on the job record) nor the payment provider — Stripe remains the system of record for money, linked by invoiceId only. The shop is NEVER a parameter: it resolves from the authenticated account, so this tool can only read the caller's own jobs — another shop's ids read as not found.

## Example — Read an invoiced job

*Illustrative payload shapes (ids elided); the composition mirrors the shop catalog view.*

**Request**

```json
{
  "jobId": "job_m1abc2de_x7k4q9"
}
```

**Response**

```json
{
  "jobId": "job_m1abc2de_x7k4q9",
  "stage": "invoiced",
  "customerId": "job_m1abc2de_x7k4q9",
  "baseVehicleId": 22124,
  "createdAt": "2026-08-20T14:02:00.000Z",
  "updatedAt": "2026-08-21T16:45:00.000Z",
  "quotes": [
    {
      "quoteId": "q_9c41f0b2ae77d31c55e00e12",
      "stage": "quote",
      "status": "approved",
      "totalCents": 28500,
      "sentAt": "2026-08-20T14:30:00.000Z",
      "vehicleDescription": "2010 Honda Civic LX",
      "decidedAt": "2026-08-20T15:10:00.000Z"
    }
  ],
  "invoice": {
    "invoiceId": "in_1QzXw82eZvKYlo2C"
  }
}
```

`invoice` carries `status: "paid"` only once the job reaches the paid stage — until then the id is a reference to Stripe, the money's system of record.

---

# list_jobs

**Jobs** · **Read-only · account-scoped**

The shop's job board, most recently updated first — same composed shape as get_job, optional stage filter.

## Usage

The board at a glance: every job the authenticated shop has, newest activity first, each in the same composed shape `get_job` returns. Pass `stage` to see one lane only (`"awaiting-approval"` is the natural morning question).

At most the 50 most recently updated jobs return; `total` always reports the true count, and a `note` appears when the list was capped — filter by stage rather than assuming the list is complete.

Read-only like `get_job`, with the same posture: no MOTOR call, no descriptive text beyond each quote's stored `vehicleDescription`, Stripe stays the money record, and the shop is never a parameter — it resolves from the authenticated account.

## Parameters

| Parameter | Type | Required | Description |
| --- | --- | --- | --- |
| `stage` | "intake" \| "quoting" \| "awaiting-approval" \| "in-shop" \| "ready-to-bill" \| "invoiced" \| "paid" \| "closed" | No | Optional board-lane filter. One of the eight job stages. Example: "awaiting-approval" |

## Description advertised to clients

The exact description a fully entitled MCP client discovers — the workflow contracts travel with the tool, so a client with no system prompt still uses it correctly. An account missing a licence for content this tool reaches sees the same text behind a `[Licensed content — not enabled on this account]` or `[Partially available]` notice:

> List the authenticated shop's jobs, most recently updated first, each in the same composed shape get_job returns (spine + quote/invoice summaries). Pass stage to see one board lane only. Returns at most the 50 most recently updated jobs; when more exist, the result says so — filter by stage to narrow rather than assuming the list is complete. THIS TOOL IS READ-ONLY EVERYWHERE: it reads MotorAdvisor's own job store and touches neither MOTOR (no content call is made, and no MOTOR text is persisted anywhere on the job record) nor the payment provider — Stripe remains the system of record for money, linked by invoiceId only. The shop is NEVER a parameter: it resolves from the authenticated account, so this tool can only read the caller's own jobs — another shop's ids read as not found.

## Example — Jobs awaiting the customer's decision

*Illustrative payload shapes (ids elided).*

**Request**

```json
{
  "stage": "awaiting-approval"
}
```

**Response**

```json
{
  "jobs": [
    {
      "jobId": "job_m1abc2de_x7k4q9",
      "stage": "awaiting-approval",
      "customerId": "job_m1abc2de_x7k4q9",
      "baseVehicleId": 22124,
      "createdAt": "2026-08-20T14:02:00.000Z",
      "updatedAt": "2026-08-21T09:12:00.000Z",
      "quotes": [
        {
          "quoteId": "q_9c41f0b2ae77d31c55e00e12",
          "stage": "quote",
          "status": "sent",
          "totalCents": 28500,
          "sentAt": "2026-08-21T09:12:00.000Z",
          "vehicleDescription": "2010 Honda Civic LX"
        }
      ]
    }
  ],
  "total": 1
}
```

Follow up on a specific quote with `check_quote_status` — it adds the delivery trail and the approval link.
