Home · Guides

Per diem and travel allowances from chat, on the rate tables the tax authorities publish

A per diem is a number somebody else decided. A tax authority publishes a daily amount, a ladder of partial days and a rule about the meals your host paid for, and your job is to apply their arithmetic to your trip and put the answer on a claim or a return. That is a calculation, not a judgement, which is exactly the kind of thing worth handing to a tool. The MCP Per Diem server holds three of those schemes as files inside the package, prices a trip against one of them, and shows the ladder and every deduction next to the number that produced it. There is no network call anywhere in it.

Install it

claude mcp add per-diem -- npx -y @theluckystrike/mcp-per-diem

Cursor, in .cursor/mcp.json, and Claude Desktop with the same block under claude_desktop_config.json:

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

It reads the same business profile as MCP Invoice and MCP Expense Tracker, so the traveller's name and your default currency are set once, and it borrows its datetime and zone handling from MCP Timezone rather than carrying a second copy of it.

The three schemes, and what each one counts

SchemeWhat a day isWhat it pays
Poland, domestic and per country24-hour periods from departure (the doba)PLN 45.00 a day at home plus a 150 percent lodging lump sum; abroad the annex diet in the country's own currency
UK, inside the UKHours away, in bandsGBP 5.00 from 5 hours, GBP 10.00 from 10, GBP 25.00 from 15 when the journey is still going at 8pm, plus the late-evening supplement
US, GSA CONUS standardCalendar days in the destination zoneThe standard M&IE at 75 percent on the first and last day, with lodging as a receipted cap rather than an allowance paid out

The same trip gives different answers under different schemes, and both are right. Noon to noon across Poland's spring clock change is 23 elapsed hours: one Polish doba, PLN 45.00, and two US calendar days, USD 102.00. The server counts elapsed hours as a difference between two instants, never as text, which is why it gets that trip right in March and October rather than only in the other ten months. Pass ISO 8601 with an offset, or a local datetime plus an IANA zone.

What you say, and which tool runs

What you sayTool
What is the per diem for 58 hours in Krakow, with breakfast provided on the first day and two hotel nights?perdiem_calc
Show me the Polish foreign rates for Germany, and where they come from.perdiem_rates
Save that as the Krakow audit trip.trip_record
What have I claimed this quarter?trip_list
Give me the expense lines for that trip.trip_export
Total my per diems per scheme and per month.perdiem_report

A worked Polish domestic answer, 58 hours to Krakow with breakfast provided on the first day and two nights in a hotel:

day 1  45.00 less 25 percent breakfast   PLN 33.75
day 2  full doba                         PLN 45.00
day 3  10 h remainder, over 8 h          PLN 45.00
diets                                    PLN 123.75
lodging lump sum  2 x 67.50              PLN 135.00
total                                    PLN 258.75

The measured thing: a substring match priced a trip to Oman at Romania's rate

The first build resolved a destination by exact country name, then by ISO code, then by country.includes(destination) as a last resort. "romania".includes("oman") is true. A trip to Oman came back priced at Romania's EUR 42.00 diet, in the wrong currency, from a country three thousand kilometres away, with no note and no warning. It was caught by a test that expected a refusal and got a successful calculation instead.

The fix is one line: the fallback is now a prefix match of four characters or more, so a destination that is genuinely absent is refused by name. The lesson is worth more than the fix. A fuzzy match is safe when a miss is cheap and dangerous when a miss is silent, and here a miss produces a tax figure. It is the table being deliberately partial that makes the substring fallback unsafe: with a complete table a wrong row is a near miss on a real country, and with a partial one it is a row that should never have matched at all, handed to the caller as a confident number in place of the refusal that would have sent them to the regulation.

The gap this server will not paper over: HMRC overseas

HMRC publishes worldwide subsistence scale rates per city, roughly 250 of them with eight figures each in the destination's own currency. None of them is bundled here. They could not be stated with confidence from the public text at build time, so the file ships with an empty rate list and a header that says why, perdiem_rates reports the gap in its notes, and perdiem_calc {scheme: "uk", destination: "Paris"} refuses by name and gives you the HMRC page. The UK domestic benchmark rates, all four of them, are bundled and complete.

The same rule shapes the other tables. The Polish annex ships 34 countries rather than all of them, and a country outside those 34 is refused with the words "not verified here", never "no rate exists", because those are different statements and only one of them is true. The US table ships the CONUS standard rate rather than the roughly 300 non-standard localities, which is the rate a destination outside those localities takes anyway.

A per diem ends up on a tax return. A wrong figure that looks authoritative is worse than an absent one, because an absent one is refused by name and you go and look it up. Every rate that is bundled carries its authority, its instrument, the source URL and the date it took effect, and perdiem_rates hands that header back with the numbers so the provenance travels with them.

Handing a trip to the expense tracker

trip_export returns the exact expense_add arguments for the expense tracker, one payload per currency, and writes nothing itself. That is deliberate: expense-tracker publishes no library entry point, and its id counter, category rules, VAT split and currency defaults all live inside its own handler under its own lock. Appending a row to its store directly would produce an expense with none of those applied, one that looks native and is not. No vat_rate is set either, because a statutory per diem is an allowance rather than a purchase and there is no input VAT to reclaim on it.

Currencies are never added together anywhere in this server. A PLN diet and a EUR one stay two figures, in trip_list and in perdiem_report alike, because there is no exchange rate here and inventing one would be inventing the total.

Free and Pro

Rate lookups and calculations are free and unlimited on every tier, on all three schemes. That is deliberate: the tables are public regulation, and metering the reading of a regulation would be charging for the tax authority's work rather than for this server's. What the free tier limits is saving trips, at 5 a calendar month counted by start date, with trip lists unlimited. Pro ($19 one-time, or $39 for the bundle) removes that cap and adds the expense-tracker export payloads and the report of totals per scheme and per calendar month.

Questions

Where do the rates come from, and how do I know they are current?

Each table is a file inside the package carrying the authority, the instrument, the source URL, the date the rates took effect and the date they were read, and perdiem_rates returns that header with the rates. There is no live feed, deliberately: a figure that changed under you between two runs of the same trip is worse than one that is visibly stale, because the stale one is checkable. Read the effective_date in any answer before you rely on it.

Why are the HMRC overseas rates missing?

Because they could not be stated with confidence from the public text at build time, and a wrong per diem that looks authoritative is worse than an absent one. The file ships with an empty rate list and a header saying so, and perdiem_calc with a foreign destination under the uk scheme refuses by name and points at the HMRC page. The four UK domestic benchmark scale rates are bundled and complete.

What happens if my destination is not in the table?

It is refused by name, with the words not verified here rather than no rate exists, and you are pointed at the regulation. It is never priced at a neighbouring country's rate. That is the fix for the Oman defect above: the name fallback is a prefix match of four characters or more, so an absent country fails closed.

Does it handle a trip across a daylight saving change?

Yes, and that is why start and end are instants rather than wall clocks. Noon to noon across Poland's spring change is 23 elapsed hours, one Polish 24-hour period at PLN 45.00, and the same trip under the US scheme is two calendar days at USD 102.00. A local time that falls inside the spring-forward gap resolves forward rather than being kept silently.

How are provided meals deducted?

By each scheme's own rule, and a day never goes below zero. Poland takes 25/50/25 percent of a domestic day and 15/30/30 of a foreign one; the US deducts the published breakfast, lunch and dinner amounts of the M&IE tier and never the incidentals; the UK removes a pro rata share of the band. The UK pro rata is this server's reading of HMRC's principle rather than a published figure, and the answer says so in its rule field.

What does the free tier actually limit?

Only saving a trip, at 5 a calendar month by start date. perdiem_rates, perdiem_calc and trip_list are unlimited on every tier. trip_export and perdiem_report are Pro. A trip starting in a different month is not blocked by this month's five.

Where is the data kept?

Plain JSON under ~/.local/share/mcp-servers/per-diem/, or $XDG_DATA_HOME if you set it. The rate tables are files inside the package. There is no network call anywhere in this server, no account and no API key, and license keys are verified offline.

Related

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