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.
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.
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.
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.
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.
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 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.
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 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.
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.
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.
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.
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.
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.