Home · Guides

Send a quote from chat, then turn the yes into an invoice

A quote and an invoice are the same document at two different moments: one is a price you are proposing, the other is a price you are owed. Most tooling treats them as unrelated, so the numbers get retyped between a quoting tool and a billing tool, and the two can drift apart. The MCP Quotes server keeps them as one lifecycle: "quote Acme for 12 hours at 90 EUR plus a 300 EUR setup, 23% VAT, good for 14 days" gets you a numbered quote with a line table, VAT and a validity date, and "Acme said yes" turns that same quote into a real invoice in the invoice server's own store, same client list, same number series. Nothing is uploaded; everything lives in plain JSON on your machine.

Install

claude mcp add quotes -- npx -y @theluckystrike/mcp-quotes

Cursor and Claude Desktop take the same block, under their own config file:

{
  "mcpServers": {
    "quotes": {
      "command": "npx",
      "args": ["-y", "@theluckystrike/mcp-quotes"]
    }
  }
}

Build servers/invoice first if you are running from source: the money, VAT, client and numbering engine is imported from it rather than duplicated, which is also why a quote and the invoice it becomes round to the same numbers. See the setup pages for the exact file and key per client.

The lifecycle: open, sent, accepted or declined, expired

A quote is created with quote_create: line items in minor units, a VAT rate per line or the business default, an optional discount, and a validity window in days. It is allocated a Q-YYYY-NNNN id, the same shape the invoice server uses for INV-YYYY-NNNN, for the same reason: a bare Q-0001 that resets every January collides with last January's quote the moment history spans a year boundary. From there a quote moves through exactly one of three closing states. quote_accept marks it accepted and invoices it. quote_decline marks it lost with a reason, so it stops counting against the open-quote limit and starts counting toward the win rate. Left untouched past its validity date, it becomes expired on its own, with nothing to call: state is computed from today's date against valid_until, not stored and left to go stale.

quote_send_text renders the aligned line table, the VAT lines, the total and the validity date as plain text ready to paste into an email, and it is free on every tier. The quote_followup prompt reviews what is open, what lapses soon and what already lapsed, and drafts the chase, which is the part of quoting that a stored JSON file does not do for you on its own: nobody enjoys the second email that just asks whether the first one was seen. An expired quote is refused by quote_accept until you either extend it with quote_update {valid_until} or pass allow_expired: true, which is deliberate friction: a lapsed price should be re-confirmed before it becomes an invoice, not honoured automatically as if nothing changed.

Validity in the profile timezone, not the laptop's

Whether a quote has lapsed is a question about "today," and "today" is ambiguous the moment the person asking and the machine running the server are in different places. The server answers it by computing the current date in the timezone field of the shared business profile, when one is set, rather than in the host machine's own zone. A quote issued at 00:30 in Warsaw from a laptop still set to US Eastern time would otherwise be stamped the previous calendar day and lapse a day earlier than the client was actually told, which is the kind of one-day-early expiry that looks like a bug in front of a client and is really a bug in the machine's clock settings.

The worked example

quote_create {
  client: "Acme", currency: "EUR", validity_days: 14,
  items: [
    { description: "API work",  quantity: 12, unit_price_minor: 9000,  tax_rate: 23 },
    { description: "Setup fee", quantity: 1,  unit_price_minor: 30000, tax_rate: 23 }
  ]
}
-> Q-2026-0001, valid until 2026-09-18

API work    12  x  EUR 90.00   =  EUR 1080.00
Setup fee    1  x  EUR 300.00  =  EUR 300.00
Subtotal                          EUR 1380.00
VAT 23% on EUR 1380.00             EUR 317.40
TOTAL                              EUR 1697.40
Valid until 2026-09-18.

quote_accept { id: "Q-2026-0001" }
-> accepted, invoice INV-2026-0004, due 2026-09-18, total EUR 1697.40

Prices go in as minor units: 9000 is EUR 90.00, and there is no decimal anywhere on the input side, so a price can never land in the system ten times too small because a "90" was read as 90 minor units instead of 90 major units.

Accept writes the invoice through the shared engine

quote_accept creates the invoice directly in the invoice server's own store when that store is present, defined as invoices.json or clients.json already existing, or a shared business profile with a name. When none of those exist yet, the response instead hands back invoice_create-ready arguments, with unit prices converted back to major units, so the same sentence still gets you to an invoice, just through one more explicit call. Either way, the invoice is written by the exact computeTotals, currencyDecimals and formatMoney code the invoice server itself uses, imported rather than reimplemented, which is what keeps a quote and the invoice it becomes agreeing line for line and then in the total. One side effect worth knowing: accepting bypasses the invoice server's own free cap of 3 invoices a month, because it writes through the shared engine rather than through that server's gated tool handler. The quotes free cap, 5 open quotes at a time, is the one that applies here, and the accept response says so.

The measured reason acceptance copies the quote instead of recomputing it

The obvious way to build quote_accept is to take the client, the line items and today's tax settings, and run them back through the same pricing logic that made the quote in the first place. That was tried, measured, and rejected, because a business profile changes over the life of a quote in ways that have nothing to do with the quote itself: a VAT rate correction, a new client class, a bookkeeping fix someone made in the meantime.

The measured case (test/adversarial.test.mjs, "a VAT rate change between quote and acceptance never moves the agreed total"): a quote of EUR 1,000.00 net is issued while the profile's default_tax_rate is 23%, so the client is given EUR 1,230.00. Before the client answers, the profile's default rate is changed to 8%. Recomputing at acceptance time invoices EUR 1,080.00 -- EUR 150.00 below the number the client actually agreed to, on one document, with nothing on either record explaining why they differ. Copying the quote's stored lines instead invoices EUR 1,230.00, and the assertion holds tax_lines[0].rate === 23. A later tax-rate change in the shared profile cannot move the agreed total on a quote that already went out the door. The same logic is why prices are taken as minor units with no decimal on the input side: the integer on the quote and the integer on the invoice are the same integer, all the way through, with no rounding step in between that a rate change could quietly ride along with.

The win rate report

quote_report, a Pro feature, totals open, accepted, declined and expired quotes per currency, the value still sitting open, and the win rate: accepted divided by accepted plus declined, expired quotes counted as neither a win nor a loss because nobody actually said no. That last choice matters for what the number means. Counting an expired quote as a loss would punish a slow client the same as a client who chose someone else, and the two are different problems: one wants a better follow-up cadence, and the other wants a better price or a better pitch. Keeping expired quotes out of the ratio keeps the win rate answering only the question it can actually answer.

Free tier and Pro

Free holds 5 open quotes at a time; accepting, declining or letting one lapse frees the slot, so a freelancer who actually closes their quotes never hits the cap. quote_send_text, creating, revising, accepting, declining, VAT, discounts and multi-currency are all unrestricted on free. quote_pdf and quote_report are Pro. Pro is a one-time $19, or $39 for every server in the collection, lifetime, no subscription. Full detail on the MCP Quotes page and the general free versus Pro comparison. Once a quote is accepted, the invoice it produced is the same one the invoice PDF guide covers.

Questions

What happens to an unpaid quote once its validity date passes?

It becomes expired on its own; nothing needs to be called. State is computed from today's date against valid_until, so a quote that nobody accepted or declined simply stops being acceptable, and quote_accept refuses it until you extend valid_until or pass allow_expired: true.

If the business's VAT rate changes after a quote is sent, which rate does the invoice use?

The rate the client was quoted. quote_accept copies the quote's own stored lines rather than recomputing them against the shared profile's current default_tax_rate, so a rate change made after the quote went out, for any reason, cannot move the total on a quote the client already agreed to.

Does accepting a quote count against the invoice server's free cap of 3 invoices a month?

No. Accepting writes the invoice through the same engine the recurring-invoice server uses, bypassing the invoice server's own tool-level cap. The quotes server's own cap, 5 open quotes at a time, is the one that applies, and the accept response states this.

What is a quote's win rate and why don't expired quotes count against it?

quote_report divides accepted quotes by accepted plus declined. Expired quotes are excluded because nobody actually said no; folding a slow client in with a lost one would answer a different question than the one a win rate is meant to answer.

Why does validity use the business profile's timezone instead of the computer's own clock?

Because whether a quote has lapsed is a question about today's date, and a quote issued at 00:30 local time from a machine still set to a different zone would otherwise be dated the wrong calendar day and expire early. Setting timezone on the shared business profile fixes the date to where the business actually is.

Related

All MCP servers and prices · All guides