{
  "server": {
    "name": "motor-daas-mcp",
    "version": "0.1.0"
  },
  "endpoint": "https://mcp.motoradvisor.app/mcp",
  "tools": [
    {
      "name": "resolve_vehicle",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "vehicle": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "vin": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 17,
                    "description": "Partial (3+ characters) or full 17-character VIN. Example: \"2HGFA1F5\""
                  }
                },
                "required": [
                  "vin"
                ],
                "additionalProperties": false,
                "description": "Resolve by VIN"
              },
              {
                "type": "object",
                "properties": {
                  "year": {
                    "anyOf": [
                      {
                        "type": "number"
                      },
                      {
                        "type": "string"
                      }
                    ],
                    "description": "Model year. Sandbox covers 2010, 2015, 2016 only. Example: 2010"
                  },
                  "make": {
                    "type": "string",
                    "description": "Make name, case-insensitive. Example: \"Honda\""
                  },
                  "model": {
                    "type": "string",
                    "description": "Model name, case-insensitive. Example: \"Civic\""
                  },
                  "trim": {
                    "description": "Trim/submodel, case-insensitive. Omit to see all candidates when unsure. Example: \"LX\"",
                    "type": "string"
                  }
                },
                "required": [
                  "year",
                  "make",
                  "model"
                ],
                "additionalProperties": false,
                "description": "Resolve by year/make/model"
              }
            ],
            "description": "The vehicle to resolve — either {vin} or {year, make, model, trim?}. Example: {\"vin\": \"2HGFA1F5\"}"
          }
        },
        "required": [
          "vehicle"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "list_content",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "baseVehicleId": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "The baseVehicleId field from a resolve_vehicle result. NOT the vehicleId (rejected with 400.110052). Example: 22124"
          },
          "contentType": {
            "type": "string",
            "enum": [
              "EstimatedWorkTimes",
              "Specifications",
              "TechnicalServiceBulletins",
              "DiagnosticTroubleCodes",
              "ServiceProcedures",
              "WiringDiagrams",
              "Fluids",
              "Parts",
              "MaintenanceSchedules",
              "ComponentLocations",
              "PartVectorIllustrations"
            ],
            "description": "Which MOTOR content family to query. Example: \"TechnicalServiceBulletins\""
          },
          "searchTerm": {
            "description": "Server-side semantic search — recommended for large sets. Matches generously (\"A/C\" can also match ABS records; discard obvious mismatches). Example: \"A/C\"",
            "type": "string"
          },
          "keyword": {
            "description": "Client-side substring filter applied after fetch. Example: \"refrigerant\"",
            "type": "string"
          },
          "configuration": {
            "description": "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\"}",
            "type": "object",
            "properties": {
              "transmission": {
                "description": "Transmission type, once the user has confirmed it. Example: \"automatic\"",
                "type": "string",
                "enum": [
                  "automatic",
                  "manual"
                ]
              },
              "drivetrain": {
                "description": "Drivetrain, once known. Example: \"fwd\"",
                "type": "string",
                "enum": [
                  "fwd",
                  "rwd",
                  "awd",
                  "4wd"
                ]
              },
              "bodyStyle": {
                "description": "Body style, once known. Example: \"sedan\"",
                "type": "string",
                "enum": [
                  "sedan",
                  "coupe",
                  "hatchback",
                  "wagon"
                ]
              },
              "engine": {
                "description": "Engine displacement or designation. Auto-populated from resolution in service_advisor_lookup — only pass it to list_content. Example: \"1.8L\"",
                "type": "string"
              },
              "trim": {
                "description": "Trim/submodel, case-insensitive. Omit to see all candidates when unsure. Example: \"LX\"",
                "type": "string"
              },
              "rearBrakes": {
                "description": "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\"",
                "type": "string",
                "enum": [
                  "disc",
                  "drum"
                ]
              },
              "options": {
                "description": "Option packages present on the vehicle (presence-only assertions). Example: [\"Sunroof\"]",
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "additionalProperties": false
          }
        },
        "required": [
          "baseVehicleId",
          "contentType"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "get_content_detail",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "baseVehicleId": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "The baseVehicleId field from a resolve_vehicle result. NOT the vehicleId (rejected with 400.110052). Example: 22124"
          },
          "contentType": {
            "type": "string",
            "enum": [
              "EstimatedWorkTimes",
              "Specifications",
              "TechnicalServiceBulletins",
              "DiagnosticTroubleCodes",
              "ServiceProcedures",
              "WiringDiagrams",
              "Fluids",
              "Parts",
              "MaintenanceSchedules",
              "ComponentLocations",
              "PartVectorIllustrations"
            ],
            "description": "Which MOTOR content family to query. Example: \"TechnicalServiceBulletins\""
          },
          "applicationId": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "ApplicationID from a list_content summary row. Example: 871003"
          }
        },
        "required": [
          "baseVehicleId",
          "contentType",
          "applicationId"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "service_advisor_lookup",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "vehicle": {
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "vin": {
                    "type": "string",
                    "minLength": 3,
                    "maxLength": 17,
                    "description": "Partial (3+ characters) or full 17-character VIN. Example: \"2HGFA1F5\""
                  }
                },
                "required": [
                  "vin"
                ],
                "additionalProperties": false,
                "description": "Resolve by VIN"
              },
              {
                "type": "object",
                "properties": {
                  "year": {
                    "anyOf": [
                      {
                        "type": "number"
                      },
                      {
                        "type": "string"
                      }
                    ],
                    "description": "Model year. Sandbox covers 2010, 2015, 2016 only. Example: 2010"
                  },
                  "make": {
                    "type": "string",
                    "description": "Make name, case-insensitive. Example: \"Honda\""
                  },
                  "model": {
                    "type": "string",
                    "description": "Model name, case-insensitive. Example: \"Civic\""
                  },
                  "trim": {
                    "description": "Trim/submodel, case-insensitive. Omit to see all candidates when unsure. Example: \"LX\"",
                    "type": "string"
                  }
                },
                "required": [
                  "year",
                  "make",
                  "model"
                ],
                "additionalProperties": false,
                "description": "Resolve by year/make/model"
              }
            ],
            "description": "The vehicle to resolve — either {vin} or {year, make, model, trim?}. Example: {\"vin\": \"2HGFA1F5\"}"
          },
          "mileage": {
            "description": "Current odometer miles — enables due-at-mileage maintenance selection. Example: 62000",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          },
          "symptom": {
            "description": "Customer-reported symptom in plain words — drives labor, bulletin, and spec search. Example: \"AC blows warm\"",
            "type": "string"
          },
          "laborRateCents": {
            "description": "Shop labor rate in integer cents per hour. Example: 15000 (= $150.00/hr)",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          },
          "selectedApplicationIds": {
            "description": "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]",
            "type": "array",
            "items": {
              "type": "integer",
              "exclusiveMinimum": 0,
              "maximum": 9007199254740991
            }
          },
          "serviceCode": {
            "description": "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)",
            "type": "string"
          },
          "stage": {
            "description": "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\"",
            "type": "string",
            "enum": [
              "diagnose",
              "quote"
            ]
          },
          "specValueCap": {
            "description": "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",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "configuration": {
            "description": "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\"}",
            "type": "object",
            "properties": {
              "transmission": {
                "description": "Transmission type, once the user has confirmed it. Example: \"automatic\"",
                "type": "string",
                "enum": [
                  "automatic",
                  "manual"
                ]
              },
              "drivetrain": {
                "description": "Drivetrain, once known. Example: \"fwd\"",
                "type": "string",
                "enum": [
                  "fwd",
                  "rwd",
                  "awd",
                  "4wd"
                ]
              },
              "bodyStyle": {
                "description": "Body style, once known. Example: \"sedan\"",
                "type": "string",
                "enum": [
                  "sedan",
                  "coupe",
                  "hatchback",
                  "wagon"
                ]
              },
              "engine": {
                "description": "Engine displacement or designation. Auto-populated from resolution in service_advisor_lookup — only pass it to list_content. Example: \"1.8L\"",
                "type": "string"
              },
              "trim": {
                "description": "Trim/submodel, case-insensitive. Omit to see all candidates when unsure. Example: \"LX\"",
                "type": "string"
              },
              "rearBrakes": {
                "description": "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\"",
                "type": "string",
                "enum": [
                  "disc",
                  "drum"
                ]
              },
              "options": {
                "description": "Option packages present on the vehicle (presence-only assertions). Example: [\"Sunroof\"]",
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            },
            "additionalProperties": false
          }
        },
        "required": [
          "vehicle"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "issue_invoice",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "vehicleDescription": {
            "description": "The vehicle, for the invoice footer. Example: \"2010 Honda Civic LX 1.8L\"",
            "type": "string"
          },
          "customer": {
            "type": "object",
            "properties": {
              "name": {
                "type": "string",
                "minLength": 1,
                "description": "Customer's name. Example: \"Jane Rivera\""
              },
              "email": {
                "type": "string",
                "minLength": 3,
                "description": "Customer's email — where the shop will send the pay link. Example: \"jane@example.com\""
              },
              "phone": {
                "description": "Customer's phone, optional. Example: \"(313) 555-0144\"",
                "type": "string"
              }
            },
            "required": [
              "name",
              "email"
            ],
            "additionalProperties": false,
            "description": "Who is being billed. Required — an invoice needs a recipient. Example: {\"name\": \"Jane Rivera\", \"email\": \"jane@example.com\"}"
          },
          "labor": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "description": "The labor operation as MOTOR names it, from a service_advisor_lookup selection. Example: \"A/C Compressor — Remove & Replace\""
                },
                "bookHours": {
                  "type": "number",
                  "exclusiveMinimum": 0,
                  "description": "Book hours for the operation. Example: 1.2"
                },
                "rateCentsPerHour": {
                  "description": "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",
                  "type": "integer",
                  "exclusiveMinimum": 0,
                  "maximum": 9007199254740991
                },
                "skillName": {
                  "description": "Skill tier name shown on the line. Example: \"Air Conditioning\"",
                  "type": "string"
                }
              },
              "required": [
                "name",
                "bookHours"
              ],
              "additionalProperties": false
            },
            "description": "The BOOKED labor selection (a completed selectedTotal choice), one entry per operation. Example: [{\"name\": \"A/C Compressor — Remove & Replace\", \"bookHours\": 1.2}]"
          },
          "parts": {
            "description": "Priced parts to bill, if any. Every part needs unitPriceCents. Example: [{\"name\": \"A/C Compressor\", \"unitPriceCents\": 12500}]",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "description": "Part name as quoted. Example: \"A/C Compressor\""
                },
                "quantity": {
                  "description": "How many. Defaults to 1. Example: 1",
                  "type": "integer",
                  "exclusiveMinimum": 0,
                  "maximum": 9007199254740991
                },
                "unitPriceCents": {
                  "type": "integer",
                  "exclusiveMinimum": 0,
                  "maximum": 9007199254740991,
                  "description": "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"
                }
              },
              "required": [
                "name",
                "unitPriceCents"
              ],
              "additionalProperties": false
            }
          },
          "referenceId": {
            "description": "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\"",
            "type": "string",
            "minLength": 4
          },
          "quoteId": {
            "description": "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\"",
            "type": "string",
            "minLength": 6
          }
        },
        "required": [
          "customer",
          "labor"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "check_invoice_status",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "invoiceId": {
            "description": "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\"",
            "type": "string",
            "minLength": 3
          },
          "limit": {
            "description": "Sweep mode only: how many unpaid invoices to return, oldest first. Defaults to 25. Example: 25",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 100
          }
        },
        "additionalProperties": false
      }
    },
    {
      "name": "assess_repair_economics",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "vin": {
            "type": "string",
            "minLength": 9,
            "maxLength": 17,
            "description": "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": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "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": {
            "description": "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",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          },
          "state": {
            "description": "US state abbreviation or ZIP for regional adjustment. The shop's location is a reasonable default. Example: \"48226\"",
            "type": "string"
          }
        },
        "required": [
          "vin",
          "repairCostCents"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "get_vehicle_value",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "vin": {
            "type": "string",
            "minLength": 9,
            "maxLength": 17,
            "description": "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": {
            "description": "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",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          },
          "state": {
            "description": "US state abbreviation or ZIP for regional adjustment. The shop's location is a reasonable default. Example: \"48226\"",
            "type": "string"
          }
        },
        "required": [
          "vin"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "create_quote_snapshot",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "baseVehicleId": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "The baseVehicleId field from a resolve_vehicle result. NOT the vehicleId. Example: 22124"
          },
          "vehicleDescription": {
            "type": "string",
            "minLength": 4,
            "maxLength": 120,
            "description": "The resolved vehicle identity, from resolve_vehicle's description. Example: \"2010 Honda Civic LX\""
          },
          "applicationIds": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "integer",
              "exclusiveMinimum": 0,
              "maximum": 9007199254740991
            },
            "description": "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": {
            "description": "Labor rate in integer cents/hour. Omit to use the shop's stored rate. Example: 15000",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          },
          "manualLines": {
            "description": "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}]",
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 2,
                  "maxLength": 200,
                  "description": "Shop-authored line text. Example: \"Refrigerant recharge\""
                },
                "bookHours": {
                  "type": "number",
                  "exclusiveMinimum": 0,
                  "maximum": 100,
                  "description": "Hours for this line. Example: 0.5"
                },
                "rateCentsPerHour": {
                  "description": "Line-specific rate in cents/hour; omit for the shop rate. Example: 15000",
                  "type": "integer",
                  "exclusiveMinimum": 0,
                  "maximum": 9007199254740991
                }
              },
              "required": [
                "name",
                "bookHours"
              ],
              "additionalProperties": false
            }
          },
          "parts": {
            "description": "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}]",
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 200,
                  "description": "Part name as quoted. Example: \"A/C Compressor\""
                },
                "oemPartNumber": {
                  "description": "OEM part number, when known. Example: \"38810-RNA-A02\"",
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 64
                },
                "quantity": {
                  "description": "How many. Defaults to 1. Example: 1",
                  "type": "integer",
                  "exclusiveMinimum": 0,
                  "maximum": 999
                },
                "unitPriceCents": {
                  "type": "integer",
                  "exclusiveMinimum": 0,
                  "maximum": 9007199254740991,
                  "description": "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"
                }
              },
              "required": [
                "name",
                "unitPriceCents"
              ],
              "additionalProperties": false
            }
          },
          "customerContact": {
            "description": "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\"",
            "type": "string",
            "minLength": 3,
            "maxLength": 254
          },
          "customerName": {
            "description": "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\"",
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "supersedes": {
            "description": "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\"",
            "type": "string",
            "minLength": 6
          }
        },
        "required": [
          "baseVehicleId",
          "vehicleDescription",
          "applicationIds"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "check_quote_status",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "quoteId": {
            "type": "string",
            "minLength": 6,
            "description": "The quote snapshot id from create_quote_snapshot or the advisor's work order. Example: \"q_1facac2305cc6b3901077d3b\""
          }
        },
        "required": [
          "quoteId"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "void_quote",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "quoteId": {
            "type": "string",
            "minLength": 6,
            "description": "The quote snapshot id to void. Example: \"q_1facac2305cc6b3901077d3b\""
          },
          "reason": {
            "description": "Why the shop is retiring it, for the record. Example: \"replaced by the full brake quote after inspection\"",
            "type": "string",
            "minLength": 3,
            "maxLength": 500
          }
        },
        "required": [
          "quoteId"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "send_quote_notification",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "quoteId": {
            "type": "string",
            "minLength": 6,
            "description": "The quote snapshot id, e.g. from the advisor's work order. Example: \"q_1facac2305cc6b3901077d3b\""
          },
          "channels": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "email",
                "sms"
              ]
            },
            "description": "Which channels to send on. Example: [\"email\"]"
          }
        },
        "required": [
          "quoteId",
          "channels"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "send_invoice_notification",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "invoiceId": {
            "type": "string",
            "minLength": 3,
            "description": "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": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "email",
                "sms"
              ]
            },
            "description": "Which channels to send on. Example: [\"email\"]"
          },
          "vehicleDescription": {
            "description": "The vehicle line for the email, e.g. from resolve_vehicle. Omit and the email simply skips it. Example: \"2010 Honda Civic LX\"",
            "type": "string",
            "minLength": 4,
            "maxLength": 120
          }
        },
        "required": [
          "invoiceId",
          "channels"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "capture_consent",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "channel": {
            "type": "string",
            "enum": [
              "email",
              "sms"
            ],
            "description": "Which channel the customer is opting into. Example: \"email\""
          },
          "scope": {
            "type": "string",
            "enum": [
              "quote",
              "invoice",
              "both"
            ],
            "description": "What the shop may send: quote approvals, invoice pay links, or both. Example: \"both\""
          },
          "destination": {
            "type": "string",
            "minLength": 3,
            "maxLength": 254,
            "description": "The customer's email or phone, exactly as they gave it — it is normalized before storage. Example: \"casey@example.com\""
          },
          "customerId": {
            "description": "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\"",
            "type": "string",
            "minLength": 3,
            "maxLength": 64
          },
          "attested": {
            "description": "Pass true ONLY after the disclosure text (from calling this tool without attested) was read to the customer verbatim and they agreed. Example: true",
            "type": "boolean",
            "const": true
          },
          "disclosureVersion": {
            "description": "Required with attested: the version from the disclosure prompt, pinning exactly which text the customer heard. Example: \"2026-08-19.v1\"",
            "type": "string",
            "minLength": 4
          }
        },
        "required": [
          "channel",
          "scope",
          "destination"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "get_job",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "jobId": {
            "type": "string",
            "minLength": 4,
            "description": "The job id from list_jobs or the advisor's board. Example: \"job_m1abc2de_x7k4q9\""
          }
        },
        "required": [
          "jobId"
        ],
        "additionalProperties": false
      }
    },
    {
      "name": "list_jobs",
      "description": "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.",
      "input_schema": {
        "$schema": "http://json-schema.org/draft-07/schema#",
        "type": "object",
        "properties": {
          "stage": {
            "description": "Optional board-lane filter. One of the eight job stages. Example: \"awaiting-approval\"",
            "type": "string",
            "enum": [
              "intake",
              "quoting",
              "awaiting-approval",
              "in-shop",
              "ready-to-bill",
              "invoiced",
              "paid",
              "closed"
            ]
          }
        },
        "additionalProperties": false
      }
    }
  ]
}
