Home · Guides

Price lists and rate cards from chat, and the 100x scale gap between the invoice and the quote

Most small businesses keep their prices in a spreadsheet and retype them into every quote and every invoice. That is not a filing problem, it is a correctness problem: the moment a price goes up, the spreadsheet is right and every document already sent is unexplainable, and the person retyping is the only thing standing between a customer and a figure nobody can account for. The MCP Catalogue server keeps that price list where the invoice and quote servers can both read it: a code, a name, a unit, an optional VAT rate, and prices with the day each one comes into force. It creates no invoice and no quote, stores no current price, and invents nothing.

Install it

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

Cursor, in .cursor/mcp.json, and Claude Desktop with the same block under mcpServers. Put it at the same scope as mcp-invoice and mcp-quotes: the whole point is that all three read one price list.

The measured thing: the same price is 100x apart in the two payloads

This is the decision the whole server rests on, and it is not a style preference. The invoice server's item carries unit_price in MAJOR units, documented in its own schema as "Price per unit in major units, e.g. 90 for 90 EUR". The quote server's item carries unit_price_minor in MINOR units, documented as "9000 = 90.00 EUR, 90 = JPY 90. Never a decimal". Both fields are plain numbers. Both are called the unit price in ordinary speech. And neither tool can tell that the number it was handed was scaled for the other one: 45000 passed as unit_price is a perfectly valid invoice line for EUR 45,000.00, and it reconciles against itself all the way down to the total.

Measured on the worked resolution: the correct payload nets 261,363 minor units. The quote payload's field fed into the invoice engine nets 26,136,300, which the unit suite asserts is exactly 100x. In a 3-decimal currency such as KWD the gap is 1000x. There is no arithmetic error anywhere in that: it is two correct engines handed one number that meant something different in each.

So the store holds MINOR units, which is the only lossless form, and lines_resolve builds BOTH payloads itself in one call, with the scale printed against each. A catalogue that returned "the price" and let the caller pick the field would be wrong half the time, and wrong by two orders of magnitude when it was. Take the payload named for the tool you are about to call, and change no number in it.

The second measured thing: a price is a history, not a number

A SKU holds its price ROWS, each one a currency, a tier, a valid-from date and an amount. The price on a date is the latest valid-from at or before that date, worked out on the call. Put WEB-AUDIT at 39,000 from 2025-01-01, 45,000 from 2026-01-01 and 49,500 from 2026-07-01, and ask what it was on 2026-03-15: the answer is 45,000, and sku_get names the row it picked, says how many rows it considered, and names the later row already booked. Raising a price in July does not rewrite what June was quoted at, because there is no current price stored anywhere to rewrite.

A date before every row has no price, and that is a refusal naming the earliest row and the day it starts. Filling it with the earliest row is the tempting default and it reprices history: a job done in December 2024 would be billed at the January 2025 price and would reconcile perfectly against a price list that did not exist yet.

A worked resolution

EUR, priced as of 2026-03-15, VAT 23% from the shared profile

WEB-AUDIT          3 x 45000   135,000   (the 2026-01-01 row, not 39000 and not 49500)
HOST-MO           12 x  3999    47,988
senior developer 7.5 h x 8500    63,750
junior developer 3.25 h x 4500   14,625
                                -------
net                             261,363
VAT 23% per line 31050 + 11037 + 14663 + 3364 = 60,114
gross                           321,477
rounding_drift_minor                  0

The test then hands lines_resolve's OWN invoice_create.arguments.items back to the invoice server's computeTotals in its own process and gets the same four line grosses and the same three totals. What that catches is not an arithmetic slip. It is a payload whose ITEMS do not carry what the catalogue thought they carried, which is the failure that survives every internal check.

Nothing is invented

No fallback, no profile default rate, no nearest match. An unknown code is refused by name. A code with no price in the currency and tier you asked for is refused by name. A line in a second currency is refused, because one resolution carries one currency and the alternative is a conversion nobody asked for. The invented number would be printed on a document a customer pays from, and it would look exactly like a real one.

One row per currency, tier and valid-from date. Setting that key again REPLACES the row, and the response says what it was and what it became, because two rows on one key make "the price that day" a coin toss decided by array order. A call that would change nothing at all is refused by name instead, so updated is not rewritten and no repricing is reported that did not happen.

Free and Pro

Free is 25 SKUs, the one standard price tier, unlimited rate cards, and every text answer including lines_resolve and price_list_text. A price list nobody can read is not a price list, and withholding lines_resolve would withhold the one thing the sibling servers came here for. sku_delete is free on every tier, because a way back that only a Pro key can reach is not a way back. A byte-identical duplicate is refused BEFORE the free cap is consulted, so a product filed twice names the code already stored rather than being met with an upgrade prompt.

Pro is $19 once, lifetime: an unlimited catalogue, price tiers beyond standard so trade and wholesale are a second column rather than a second catalogue, the A4 price list PDF, and the catalogue report. All servers together are $39.

Questions

Why does lines_resolve return two payloads instead of one price?

Because the two servers it feeds take the same price in different scales, and the gap is exactly 100x. invoice_create's item carries unit_price in MAJOR units, 90 for 90 EUR. quote_create's item carries unit_price_minor in MINOR units, 9000 for 90.00 EUR. Both are plain numbers and neither tool can tell it was handed the other one's scale: 45000 as unit_price is a valid invoice line for EUR 45,000.00 that reconciles against itself. Measured on the worked resolution, the correct payload nets 261,363 minor and the quote field fed to the invoice engine nets 26,136,300, asserted as exactly 100x, and 1000x in a 3-decimal currency such as KWD. Returning one price and letting the caller choose the field would be wrong half the time.

What price does it use for a date?

The latest valid_from at or before that date, for that currency and that tier, worked out on the call. sku_get names the row it picked, how many rows it considered and any later row already booked, so a question about a figure on an old invoice lands on one line of one file rather than on an argument. No current price is stored anywhere, because a stored current price is a second copy of what the rows already decide, and the copy is the one still being quoted a month after the rise.

What happens for a date before every price row?

It is refused, and the refusal names the earliest row and the day it starts. Falling back to the earliest row reprices history: a job done in December 2024 would be billed at the January 2025 price and would reconcile perfectly against a price list that did not exist yet.

Does this server create the invoice or the quote?

No. lines_resolve returns arguments and says posted false. You run invoice_create in the invoice server, or quote_create in the quotes server. A tool that did both would bill a customer as a side effect of asking what a job comes to.

What happens if I set the same price row twice?

The key is currency, tier and valid-from date, and setting that key again REPLACES the row. The response says what it was and what it became. Two rows on one key make the price that day a coin toss decided by array order. A call that would change nothing at all is refused by name instead, so nothing reports a repricing that did not happen.

What counts against the 25 free SKUs?

SKUs in the catalogue. Price rows are not metered, rate cards are not metered, and every text answer is free including lines_resolve and price_list_text. sku_delete is free on every tier, and the resolution register is what makes that safe: a code that has priced a line, or that a rate card points at, is refused by name with the times it was used and the last resolution id, because a code printed on a document somebody sent is a fact about that document.

Can it convert currencies?

No, and that is the same rule as the invented price. One resolution carries one currency, and a line in another is refused. catalogue_report counts the SKUs that carry no price in the profile's default currency, because those are the rows that stop a resolution dead, and naming them is more use than converting them at a rate nobody chose.

Why does the price list PDF show a total?

It shows the sum of one of each, and the footer says so in words. A price list has no total of its own. The A4 renderer takes a document with totals, so rather than printing a figure that looks like a document total and means nothing, the page states in words that this is a price list at a quantity of one per line and not a quotation.

Does it need the network or an account?

No. There is no network call anywhere in this server except the checkout host named in the licensing copy, and the contract suite asserts that. It reads exactly one file it does not own, read-only and best-effort: the shared business profile, for the default currency, the default VAT rate and the name at the top of the price list. There is no account and no API key, and license keys are verified offline.

Related

All MCP servers and prices · All guides · Buy the bundle $39