urbitools-mcp 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,3 +1,35 @@
1
- # Temporary Holding Version
1
+ # urbitools-mcp
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Free MCP server for UK employment calculators from [urbitools.com](https://urbitools.com), using 2026/27 statutory rates. It lets any MCP-compatible assistant run the calculations directly.
4
+
5
+ ## Tools
6
+
7
+ | Tool | What it does |
8
+ |---|---|
9
+ | `calculate_redundancy_pay` | Statutory redundancy pay: age bands, 20-year cap, 2-year eligibility, £751 (GB) / £783 (NI) weekly cap, optional enhanced multiplier |
10
+ | `calculate_paternity_pay` | Paternity leave and pay eligibility, weekly rate (£194.32 or 90% of earnings), total and claim deadline |
11
+ | `calculate_notice_pay` | PILON: higher of statutory and contractual notice, uncapped weekly pay, net estimate |
12
+
13
+ Results are estimates, not legal, tax or financial advice. Calculations run locally and nothing you enter is sent anywhere.
14
+
15
+ ## Use with Claude Desktop / Claude Code
16
+
17
+ ```json
18
+ {
19
+ "mcpServers": {
20
+ "urbitools": { "command": "npx", "args": ["-y", "urbitools-mcp"] }
21
+ }
22
+ }
23
+ ```
24
+
25
+ Claude Code: `claude mcp add urbitools -- npx -y urbitools-mcp`
26
+
27
+ ## Develop
28
+
29
+ ```bash
30
+ npm install
31
+ npm test # builds and runs the smoke test over stdio
32
+ node dist/http.js # optional: stateless Streamable HTTP on :3000/mcp (PORT to change)
33
+ ```
34
+
35
+ The calculation code is shared with the website (`../lib/calc-*.ts`) and bundled at build time.
package/dist/http.js ADDED
@@ -0,0 +1,315 @@
1
+ // src/http.ts
2
+ import { createServer as createHttp } from "node:http";
3
+ import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
+
5
+ // src/server.ts
6
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
7
+ import { z } from "zod";
8
+
9
+ // ../lib/calc-redundancy.ts
10
+ var REDUNDANCY_CAPS = { gb: 751, ni: 783 };
11
+ function parseLocalDate(s) {
12
+ const [y, m, d] = s.split("-").map(Number);
13
+ return new Date(y, m - 1, d);
14
+ }
15
+ function fullYears(from, to) {
16
+ let y = to.getFullYear() - from.getFullYear();
17
+ const m = to.getMonth() - from.getMonth();
18
+ if (m < 0 || m === 0 && to.getDate() < from.getDate()) y--;
19
+ return y;
20
+ }
21
+ function addYears(d, n) {
22
+ const x = new Date(d);
23
+ x.setFullYear(x.getFullYear() + n);
24
+ return x;
25
+ }
26
+ function formatGBDate(d) {
27
+ return d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
28
+ }
29
+ function calculateRedundancy(input) {
30
+ const { dob, start, end, weeklyPay, jurisdiction, multiplier } = input;
31
+ if (!dob || !start) return { ok: false, error: "Please enter your date of birth and employment start date." };
32
+ if (!isFinite(weeklyPay) || weeklyPay <= 0) return { ok: false, error: "Please enter your gross weekly pay." };
33
+ const dobD = parseLocalDate(dob);
34
+ const startD = parseLocalDate(start);
35
+ const endD = end ? parseLocalDate(end) : /* @__PURE__ */ new Date();
36
+ endD.setHours(0, 0, 0, 0);
37
+ if (endD < startD) return { ok: false, error: "Employment end date can't be before the start date." };
38
+ if (dobD >= endD) return { ok: false, error: "Date of birth must be before the employment end date." };
39
+ const cap = REDUNDANCY_CAPS[jurisdiction];
40
+ const cappedPay = Math.min(weeklyPay, cap);
41
+ const serviceYears = Math.min(fullYears(startD, endD), 20);
42
+ if (serviceYears < 2) {
43
+ return {
44
+ ok: true,
45
+ eligible: false,
46
+ serviceYears: fullYears(startD, endD),
47
+ countedYears: 0,
48
+ cap,
49
+ cappedPay,
50
+ rows: [],
51
+ total: 0,
52
+ enhanced: 0,
53
+ multiplier,
54
+ capped: weeklyPay > cap
55
+ };
56
+ }
57
+ const rows = [];
58
+ let total = 0;
59
+ let counted = 0;
60
+ for (let i = 0; i < serviceYears; i++) {
61
+ const yearEnd = addYears(endD, -i);
62
+ const age = fullYears(dobD, yearEnd);
63
+ if (age < 18) continue;
64
+ counted++;
65
+ let weeks;
66
+ let band;
67
+ if (age >= 41) {
68
+ weeks = 1.5;
69
+ band = "41+ \xB7 1.5 weeks";
70
+ } else if (age >= 22) {
71
+ weeks = 1;
72
+ band = "22\u201340 \xB7 1 week";
73
+ } else {
74
+ weeks = 0.5;
75
+ band = "18\u201321 \xB7 0.5 weeks";
76
+ }
77
+ const amount = weeks * cappedPay;
78
+ total += amount;
79
+ rows.push({
80
+ year: i + 1,
81
+ fromLabel: formatGBDate(addYears(yearEnd, -1)),
82
+ toLabel: formatGBDate(yearEnd),
83
+ age,
84
+ band,
85
+ weeks,
86
+ amount
87
+ });
88
+ }
89
+ return {
90
+ ok: true,
91
+ eligible: true,
92
+ serviceYears,
93
+ countedYears: counted,
94
+ cap,
95
+ cappedPay,
96
+ rows,
97
+ total,
98
+ enhanced: total * Math.max(1, multiplier || 1),
99
+ multiplier: Math.max(1, multiplier || 1),
100
+ capped: weeklyPay > cap
101
+ };
102
+ }
103
+
104
+ // ../lib/calc-paternity.ts
105
+ var SPP_RATE = 194.32;
106
+ var SPP_LEL = 129;
107
+ var DAY_ONE_LEAVE = "2026-04-06";
108
+ function parseLocalDate2(s) {
109
+ const [y, m, d] = s.split("-").map(Number);
110
+ return new Date(y, m - 1, d);
111
+ }
112
+ function addDays(d, n) {
113
+ const x = new Date(d);
114
+ x.setDate(x.getDate() + n);
115
+ return x;
116
+ }
117
+ function weekStart(x) {
118
+ const y = new Date(x);
119
+ y.setDate(y.getDate() - y.getDay());
120
+ y.setHours(0, 0, 0, 0);
121
+ return y;
122
+ }
123
+ function formatGBDate2(d) {
124
+ return d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
125
+ }
126
+ function calculatePaternity(input) {
127
+ const { due, start, averageWeeklyEarnings, leaveWeeks } = input;
128
+ if (!due || !start) return { ok: false, error: "Please enter the due date and your employment start date." };
129
+ if (!isFinite(averageWeeklyEarnings) || averageWeeklyEarnings < 0)
130
+ return { ok: false, error: "Please enter your average weekly earnings." };
131
+ const dueD = parseLocalDate2(due);
132
+ const startD = parseLocalDate2(start);
133
+ if (startD > dueD) return { ok: false, error: "Employment start date can't be after the due date." };
134
+ const ewc = weekStart(dueD);
135
+ const qualWeekStart = addDays(ewc, -15 * 7);
136
+ const qualWeekEnd = addDays(qualWeekStart, 6);
137
+ const serviceCutoff = addDays(qualWeekEnd, -26 * 7 + 1);
138
+ const serviceOK = startD <= serviceCutoff;
139
+ const earnOK = averageWeeklyEarnings >= SPP_LEL;
140
+ const payOK = serviceOK && earnOK;
141
+ const dayOneApplies = dueD >= parseLocalDate2(DAY_ONE_LEAVE);
142
+ const leaveOK = dayOneApplies ? true : serviceOK;
143
+ const weeklyRate = Math.min(SPP_RATE, averageWeeklyEarnings * 0.9);
144
+ const total = payOK ? weeklyRate * leaveWeeks : 0;
145
+ return {
146
+ ok: true,
147
+ leaveEligible: leaveOK,
148
+ payEligible: payOK,
149
+ weeklyRate,
150
+ total,
151
+ qualifyingWeekStart: formatGBDate2(qualWeekStart),
152
+ qualifyingWeekEnd: formatGBDate2(qualWeekEnd),
153
+ claimDeadline: formatGBDate2(addDays(dueD, -15 * 7)),
154
+ belowCap: averageWeeklyEarnings * 0.9 < SPP_RATE,
155
+ dayOneApplies
156
+ };
157
+ }
158
+
159
+ // ../lib/calc-notice.ts
160
+ function calculateNotice(input) {
161
+ const { annualSalary, yearsService, contractualWeeks, workedWeeks, taxRate } = input;
162
+ if (!isFinite(annualSalary) || annualSalary <= 0)
163
+ return { ok: false, error: "Please enter your gross annual salary." };
164
+ if (!isFinite(yearsService) || yearsService < 0)
165
+ return { ok: false, error: "Please enter your complete years of service." };
166
+ if (!isFinite(workedWeeks) || workedWeeks < 0)
167
+ return { ok: false, error: "Weeks already worked can't be negative." };
168
+ if (contractualWeeks != null && (!isFinite(contractualWeeks) || contractualWeeks < 0))
169
+ return { ok: false, error: "Contractual notice weeks can't be negative." };
170
+ const noStatutoryMinimum = yearsService < 1;
171
+ const statutoryWeeks = noStatutoryMinimum ? 0 : Math.min(12, Math.max(1, Math.floor(yearsService)));
172
+ const applicableWeeks = Math.max(statutoryWeeks, contractualWeeks ?? 0);
173
+ const unworkedWeeks = Math.max(0, applicableWeeks - workedWeeks);
174
+ const weeklyPay = annualSalary / 52;
175
+ const gross = weeklyPay * unworkedWeeks;
176
+ const tax = gross * taxRate;
177
+ const net = gross - tax;
178
+ return {
179
+ ok: true,
180
+ statutoryWeeks,
181
+ contractualWeeks,
182
+ applicableWeeks,
183
+ workedWeeks,
184
+ unworkedWeeks,
185
+ weeklyPay,
186
+ gross,
187
+ tax,
188
+ net,
189
+ taxRate,
190
+ noStatutoryMinimum
191
+ };
192
+ }
193
+
194
+ // src/server.ts
195
+ var SITE = "https://urbitools.com";
196
+ var DISCLAIMER = "Estimate only, not legal, tax or financial advice. Rates are for the 2026/27 tax year.";
197
+ var date = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use YYYY-MM-DD");
198
+ function reply(result, page, summary) {
199
+ if (!result.ok) {
200
+ return { isError: true, content: [{ type: "text", text: result.error ?? "Calculation failed." }] };
201
+ }
202
+ const text = `${summary(result)}
203
+
204
+ Full working and an interactive version: ${SITE}${page}
205
+ ${DISCLAIMER}`;
206
+ return {
207
+ content: [{ type: "text", text }],
208
+ structuredContent: { ...result, source: `${SITE}${page}`, disclaimer: DISCLAIMER }
209
+ };
210
+ }
211
+ var gbp = (n) => "\xA3" + n.toLocaleString("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
212
+ function createServer() {
213
+ const server = new McpServer({ name: "urbitools", version: "0.1.0" });
214
+ server.registerTool(
215
+ "calculate_redundancy_pay",
216
+ {
217
+ title: "UK statutory redundancy pay",
218
+ description: "Calculate UK statutory redundancy pay (2026/27): age-band formula (0.5/1/1.5 weeks' pay per year), 20-year cap, 2-year eligibility, weekly pay cap of \xA3751 (GB) or \xA3783 (NI), with a year-by-year breakdown and optional enhanced-scheme multiplier. Source: urbitools.com.",
219
+ inputSchema: {
220
+ date_of_birth: date.describe("Employee date of birth, YYYY-MM-DD"),
221
+ employment_start: date.describe("Employment start date, YYYY-MM-DD"),
222
+ employment_end: date.optional().describe("Last day of employment, YYYY-MM-DD. Defaults to today."),
223
+ weekly_pay: z.number().positive().describe("Gross weekly pay in GBP (before tax)"),
224
+ jurisdiction: z.enum(["gb", "ni"]).default("gb").describe("gb = Great Britain, ni = Northern Ireland"),
225
+ enhanced_multiplier: z.number().min(1).default(1).describe("1 = statutory minimum; e.g. 2 for a scheme paying double")
226
+ },
227
+ annotations: { readOnlyHint: true, openWorldHint: false }
228
+ },
229
+ async (a) => reply(
230
+ calculateRedundancy({
231
+ dob: a.date_of_birth,
232
+ start: a.employment_start,
233
+ end: a.employment_end ?? null,
234
+ weeklyPay: a.weekly_pay,
235
+ jurisdiction: a.jurisdiction,
236
+ multiplier: a.enhanced_multiplier
237
+ }),
238
+ "/redundancy-pay-calculator",
239
+ (r) => r.eligible ? `Statutory redundancy pay: ${gbp(r.total)} (${r.countedYears} counted years, weekly pay used ${gbp(r.cappedPay)}${r.capped ? `, capped at ${gbp(r.cap)}` : ""}).` + (r.multiplier !== 1 ? ` With a ${r.multiplier}x enhanced scheme: ${gbp(r.enhanced)}.` : "") + ` Statutory redundancy pay is normally tax-free within the \xA330,000 termination-payments allowance.` : "Not eligible for statutory redundancy pay: it needs at least 2 years' continuous service."
240
+ )
241
+ );
242
+ server.registerTool(
243
+ "calculate_paternity_pay",
244
+ {
245
+ title: "UK statutory paternity pay",
246
+ description: "Check UK paternity leave and pay eligibility and calculate statutory paternity pay (2026/27): \xA3194.32/week or 90% of average weekly earnings if lower, \xA3129/week lower earnings limit, 26 weeks' service by the 15th week before the due date. Source: urbitools.com.",
247
+ inputSchema: {
248
+ due_date: date.describe("Baby's due date, YYYY-MM-DD"),
249
+ employment_start: date.describe("Employment start date, YYYY-MM-DD"),
250
+ average_weekly_earnings: z.number().min(0).describe("Average gross weekly earnings in GBP"),
251
+ leave_weeks: z.union([z.literal(1), z.literal(2)]).default(2).describe("Weeks of paternity leave: 1 or 2")
252
+ },
253
+ annotations: { readOnlyHint: true, openWorldHint: false }
254
+ },
255
+ async (a) => reply(
256
+ calculatePaternity({
257
+ due: a.due_date,
258
+ start: a.employment_start,
259
+ averageWeeklyEarnings: a.average_weekly_earnings,
260
+ leaveWeeks: a.leave_weeks
261
+ }),
262
+ "/paternity-pay-calculator",
263
+ (r) => `Paternity leave eligible: ${r.leaveEligible ? "yes" : "no"}. Paternity pay eligible: ${r.payEligible ? "yes" : "no"}.` + (r.payEligible ? ` Weekly rate ${gbp(r.weeklyRate)}${r.belowCap ? " (90% of earnings, below the flat rate)" : ""}, total ${gbp(r.total)}. Claim deadline ${r.claimDeadline}.` : "")
264
+ )
265
+ );
266
+ server.registerTool(
267
+ "calculate_notice_pay",
268
+ {
269
+ title: "UK notice pay / PILON",
270
+ description: "Estimate UK payment in lieu of notice (PILON): uses the higher of statutory notice (1 week per complete year, max 12) and contractual notice, uncapped weekly pay (salary / 52), with a net estimate. PILON is fully taxable as earnings; the \xA330,000 allowance does not apply. Source: urbitools.com.",
271
+ inputSchema: {
272
+ annual_salary: z.number().positive().describe("Gross annual salary in GBP"),
273
+ years_service: z.number().min(0).describe("Complete years of service"),
274
+ contractual_notice_weeks: z.number().min(0).optional().describe("Contractual notice in weeks, if any"),
275
+ weeks_already_worked: z.number().min(0).default(0).describe("Notice weeks already worked"),
276
+ marginal_tax_rate: z.union([z.literal(0.2), z.literal(0.4), z.literal(0.45)]).default(0.2).describe("Marginal income tax rate for the net estimate")
277
+ },
278
+ annotations: { readOnlyHint: true, openWorldHint: false }
279
+ },
280
+ async (a) => reply(
281
+ calculateNotice({
282
+ annualSalary: a.annual_salary,
283
+ yearsService: a.years_service,
284
+ contractualWeeks: a.contractual_notice_weeks ?? null,
285
+ workedWeeks: a.weeks_already_worked,
286
+ taxRate: a.marginal_tax_rate
287
+ }),
288
+ "/notice-pay-calculator",
289
+ (r) => `Notice period applying: ${r.applicableWeeks} weeks (statutory ${r.statutoryWeeks}${r.contractualWeeks != null ? `, contractual ${r.contractualWeeks}` : ""}). Unworked: ${r.unworkedWeeks} weeks at ${gbp(r.weeklyPay)}/week. PILON gross ${gbp(r.gross)}, estimated tax ${gbp(r.tax)}, net ${gbp(r.net)}.` + (r.noStatutoryMinimum ? " Note: under 1 year of service may mean no statutory minimum notice." : "")
290
+ )
291
+ );
292
+ return server;
293
+ }
294
+
295
+ // src/http.ts
296
+ createHttp(async (req, res) => {
297
+ if (req.url?.split("?")[0] !== "/mcp") {
298
+ res.writeHead(404).end("Not found");
299
+ return;
300
+ }
301
+ if (req.method !== "POST") {
302
+ res.writeHead(405, { Allow: "POST" }).end("Method not allowed");
303
+ return;
304
+ }
305
+ const chunks = [];
306
+ for await (const c of req) chunks.push(c);
307
+ const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
308
+ const server = createServer();
309
+ res.on("close", () => {
310
+ transport.close();
311
+ server.close();
312
+ });
313
+ await server.connect(transport);
314
+ await transport.handleRequest(req, res, JSON.parse(Buffer.concat(chunks).toString() || "{}"));
315
+ }).listen(Number(process.env.PORT ?? 3e3));
package/dist/stdio.js ADDED
@@ -0,0 +1,297 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/stdio.ts
4
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
+
6
+ // src/server.ts
7
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8
+ import { z } from "zod";
9
+
10
+ // ../lib/calc-redundancy.ts
11
+ var REDUNDANCY_CAPS = { gb: 751, ni: 783 };
12
+ function parseLocalDate(s) {
13
+ const [y, m, d] = s.split("-").map(Number);
14
+ return new Date(y, m - 1, d);
15
+ }
16
+ function fullYears(from, to) {
17
+ let y = to.getFullYear() - from.getFullYear();
18
+ const m = to.getMonth() - from.getMonth();
19
+ if (m < 0 || m === 0 && to.getDate() < from.getDate()) y--;
20
+ return y;
21
+ }
22
+ function addYears(d, n) {
23
+ const x = new Date(d);
24
+ x.setFullYear(x.getFullYear() + n);
25
+ return x;
26
+ }
27
+ function formatGBDate(d) {
28
+ return d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
29
+ }
30
+ function calculateRedundancy(input) {
31
+ const { dob, start, end, weeklyPay, jurisdiction, multiplier } = input;
32
+ if (!dob || !start) return { ok: false, error: "Please enter your date of birth and employment start date." };
33
+ if (!isFinite(weeklyPay) || weeklyPay <= 0) return { ok: false, error: "Please enter your gross weekly pay." };
34
+ const dobD = parseLocalDate(dob);
35
+ const startD = parseLocalDate(start);
36
+ const endD = end ? parseLocalDate(end) : /* @__PURE__ */ new Date();
37
+ endD.setHours(0, 0, 0, 0);
38
+ if (endD < startD) return { ok: false, error: "Employment end date can't be before the start date." };
39
+ if (dobD >= endD) return { ok: false, error: "Date of birth must be before the employment end date." };
40
+ const cap = REDUNDANCY_CAPS[jurisdiction];
41
+ const cappedPay = Math.min(weeklyPay, cap);
42
+ const serviceYears = Math.min(fullYears(startD, endD), 20);
43
+ if (serviceYears < 2) {
44
+ return {
45
+ ok: true,
46
+ eligible: false,
47
+ serviceYears: fullYears(startD, endD),
48
+ countedYears: 0,
49
+ cap,
50
+ cappedPay,
51
+ rows: [],
52
+ total: 0,
53
+ enhanced: 0,
54
+ multiplier,
55
+ capped: weeklyPay > cap
56
+ };
57
+ }
58
+ const rows = [];
59
+ let total = 0;
60
+ let counted = 0;
61
+ for (let i = 0; i < serviceYears; i++) {
62
+ const yearEnd = addYears(endD, -i);
63
+ const age = fullYears(dobD, yearEnd);
64
+ if (age < 18) continue;
65
+ counted++;
66
+ let weeks;
67
+ let band;
68
+ if (age >= 41) {
69
+ weeks = 1.5;
70
+ band = "41+ \xB7 1.5 weeks";
71
+ } else if (age >= 22) {
72
+ weeks = 1;
73
+ band = "22\u201340 \xB7 1 week";
74
+ } else {
75
+ weeks = 0.5;
76
+ band = "18\u201321 \xB7 0.5 weeks";
77
+ }
78
+ const amount = weeks * cappedPay;
79
+ total += amount;
80
+ rows.push({
81
+ year: i + 1,
82
+ fromLabel: formatGBDate(addYears(yearEnd, -1)),
83
+ toLabel: formatGBDate(yearEnd),
84
+ age,
85
+ band,
86
+ weeks,
87
+ amount
88
+ });
89
+ }
90
+ return {
91
+ ok: true,
92
+ eligible: true,
93
+ serviceYears,
94
+ countedYears: counted,
95
+ cap,
96
+ cappedPay,
97
+ rows,
98
+ total,
99
+ enhanced: total * Math.max(1, multiplier || 1),
100
+ multiplier: Math.max(1, multiplier || 1),
101
+ capped: weeklyPay > cap
102
+ };
103
+ }
104
+
105
+ // ../lib/calc-paternity.ts
106
+ var SPP_RATE = 194.32;
107
+ var SPP_LEL = 129;
108
+ var DAY_ONE_LEAVE = "2026-04-06";
109
+ function parseLocalDate2(s) {
110
+ const [y, m, d] = s.split("-").map(Number);
111
+ return new Date(y, m - 1, d);
112
+ }
113
+ function addDays(d, n) {
114
+ const x = new Date(d);
115
+ x.setDate(x.getDate() + n);
116
+ return x;
117
+ }
118
+ function weekStart(x) {
119
+ const y = new Date(x);
120
+ y.setDate(y.getDate() - y.getDay());
121
+ y.setHours(0, 0, 0, 0);
122
+ return y;
123
+ }
124
+ function formatGBDate2(d) {
125
+ return d.toLocaleDateString("en-GB", { day: "numeric", month: "short", year: "numeric" });
126
+ }
127
+ function calculatePaternity(input) {
128
+ const { due, start, averageWeeklyEarnings, leaveWeeks } = input;
129
+ if (!due || !start) return { ok: false, error: "Please enter the due date and your employment start date." };
130
+ if (!isFinite(averageWeeklyEarnings) || averageWeeklyEarnings < 0)
131
+ return { ok: false, error: "Please enter your average weekly earnings." };
132
+ const dueD = parseLocalDate2(due);
133
+ const startD = parseLocalDate2(start);
134
+ if (startD > dueD) return { ok: false, error: "Employment start date can't be after the due date." };
135
+ const ewc = weekStart(dueD);
136
+ const qualWeekStart = addDays(ewc, -15 * 7);
137
+ const qualWeekEnd = addDays(qualWeekStart, 6);
138
+ const serviceCutoff = addDays(qualWeekEnd, -26 * 7 + 1);
139
+ const serviceOK = startD <= serviceCutoff;
140
+ const earnOK = averageWeeklyEarnings >= SPP_LEL;
141
+ const payOK = serviceOK && earnOK;
142
+ const dayOneApplies = dueD >= parseLocalDate2(DAY_ONE_LEAVE);
143
+ const leaveOK = dayOneApplies ? true : serviceOK;
144
+ const weeklyRate = Math.min(SPP_RATE, averageWeeklyEarnings * 0.9);
145
+ const total = payOK ? weeklyRate * leaveWeeks : 0;
146
+ return {
147
+ ok: true,
148
+ leaveEligible: leaveOK,
149
+ payEligible: payOK,
150
+ weeklyRate,
151
+ total,
152
+ qualifyingWeekStart: formatGBDate2(qualWeekStart),
153
+ qualifyingWeekEnd: formatGBDate2(qualWeekEnd),
154
+ claimDeadline: formatGBDate2(addDays(dueD, -15 * 7)),
155
+ belowCap: averageWeeklyEarnings * 0.9 < SPP_RATE,
156
+ dayOneApplies
157
+ };
158
+ }
159
+
160
+ // ../lib/calc-notice.ts
161
+ function calculateNotice(input) {
162
+ const { annualSalary, yearsService, contractualWeeks, workedWeeks, taxRate } = input;
163
+ if (!isFinite(annualSalary) || annualSalary <= 0)
164
+ return { ok: false, error: "Please enter your gross annual salary." };
165
+ if (!isFinite(yearsService) || yearsService < 0)
166
+ return { ok: false, error: "Please enter your complete years of service." };
167
+ if (!isFinite(workedWeeks) || workedWeeks < 0)
168
+ return { ok: false, error: "Weeks already worked can't be negative." };
169
+ if (contractualWeeks != null && (!isFinite(contractualWeeks) || contractualWeeks < 0))
170
+ return { ok: false, error: "Contractual notice weeks can't be negative." };
171
+ const noStatutoryMinimum = yearsService < 1;
172
+ const statutoryWeeks = noStatutoryMinimum ? 0 : Math.min(12, Math.max(1, Math.floor(yearsService)));
173
+ const applicableWeeks = Math.max(statutoryWeeks, contractualWeeks ?? 0);
174
+ const unworkedWeeks = Math.max(0, applicableWeeks - workedWeeks);
175
+ const weeklyPay = annualSalary / 52;
176
+ const gross = weeklyPay * unworkedWeeks;
177
+ const tax = gross * taxRate;
178
+ const net = gross - tax;
179
+ return {
180
+ ok: true,
181
+ statutoryWeeks,
182
+ contractualWeeks,
183
+ applicableWeeks,
184
+ workedWeeks,
185
+ unworkedWeeks,
186
+ weeklyPay,
187
+ gross,
188
+ tax,
189
+ net,
190
+ taxRate,
191
+ noStatutoryMinimum
192
+ };
193
+ }
194
+
195
+ // src/server.ts
196
+ var SITE = "https://urbitools.com";
197
+ var DISCLAIMER = "Estimate only, not legal, tax or financial advice. Rates are for the 2026/27 tax year.";
198
+ var date = z.string().regex(/^\d{4}-\d{2}-\d{2}$/, "Use YYYY-MM-DD");
199
+ function reply(result, page, summary) {
200
+ if (!result.ok) {
201
+ return { isError: true, content: [{ type: "text", text: result.error ?? "Calculation failed." }] };
202
+ }
203
+ const text = `${summary(result)}
204
+
205
+ Full working and an interactive version: ${SITE}${page}
206
+ ${DISCLAIMER}`;
207
+ return {
208
+ content: [{ type: "text", text }],
209
+ structuredContent: { ...result, source: `${SITE}${page}`, disclaimer: DISCLAIMER }
210
+ };
211
+ }
212
+ var gbp = (n) => "\xA3" + n.toLocaleString("en-GB", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
213
+ function createServer() {
214
+ const server = new McpServer({ name: "urbitools", version: "0.1.0" });
215
+ server.registerTool(
216
+ "calculate_redundancy_pay",
217
+ {
218
+ title: "UK statutory redundancy pay",
219
+ description: "Calculate UK statutory redundancy pay (2026/27): age-band formula (0.5/1/1.5 weeks' pay per year), 20-year cap, 2-year eligibility, weekly pay cap of \xA3751 (GB) or \xA3783 (NI), with a year-by-year breakdown and optional enhanced-scheme multiplier. Source: urbitools.com.",
220
+ inputSchema: {
221
+ date_of_birth: date.describe("Employee date of birth, YYYY-MM-DD"),
222
+ employment_start: date.describe("Employment start date, YYYY-MM-DD"),
223
+ employment_end: date.optional().describe("Last day of employment, YYYY-MM-DD. Defaults to today."),
224
+ weekly_pay: z.number().positive().describe("Gross weekly pay in GBP (before tax)"),
225
+ jurisdiction: z.enum(["gb", "ni"]).default("gb").describe("gb = Great Britain, ni = Northern Ireland"),
226
+ enhanced_multiplier: z.number().min(1).default(1).describe("1 = statutory minimum; e.g. 2 for a scheme paying double")
227
+ },
228
+ annotations: { readOnlyHint: true, openWorldHint: false }
229
+ },
230
+ async (a) => reply(
231
+ calculateRedundancy({
232
+ dob: a.date_of_birth,
233
+ start: a.employment_start,
234
+ end: a.employment_end ?? null,
235
+ weeklyPay: a.weekly_pay,
236
+ jurisdiction: a.jurisdiction,
237
+ multiplier: a.enhanced_multiplier
238
+ }),
239
+ "/redundancy-pay-calculator",
240
+ (r) => r.eligible ? `Statutory redundancy pay: ${gbp(r.total)} (${r.countedYears} counted years, weekly pay used ${gbp(r.cappedPay)}${r.capped ? `, capped at ${gbp(r.cap)}` : ""}).` + (r.multiplier !== 1 ? ` With a ${r.multiplier}x enhanced scheme: ${gbp(r.enhanced)}.` : "") + ` Statutory redundancy pay is normally tax-free within the \xA330,000 termination-payments allowance.` : "Not eligible for statutory redundancy pay: it needs at least 2 years' continuous service."
241
+ )
242
+ );
243
+ server.registerTool(
244
+ "calculate_paternity_pay",
245
+ {
246
+ title: "UK statutory paternity pay",
247
+ description: "Check UK paternity leave and pay eligibility and calculate statutory paternity pay (2026/27): \xA3194.32/week or 90% of average weekly earnings if lower, \xA3129/week lower earnings limit, 26 weeks' service by the 15th week before the due date. Source: urbitools.com.",
248
+ inputSchema: {
249
+ due_date: date.describe("Baby's due date, YYYY-MM-DD"),
250
+ employment_start: date.describe("Employment start date, YYYY-MM-DD"),
251
+ average_weekly_earnings: z.number().min(0).describe("Average gross weekly earnings in GBP"),
252
+ leave_weeks: z.union([z.literal(1), z.literal(2)]).default(2).describe("Weeks of paternity leave: 1 or 2")
253
+ },
254
+ annotations: { readOnlyHint: true, openWorldHint: false }
255
+ },
256
+ async (a) => reply(
257
+ calculatePaternity({
258
+ due: a.due_date,
259
+ start: a.employment_start,
260
+ averageWeeklyEarnings: a.average_weekly_earnings,
261
+ leaveWeeks: a.leave_weeks
262
+ }),
263
+ "/paternity-pay-calculator",
264
+ (r) => `Paternity leave eligible: ${r.leaveEligible ? "yes" : "no"}. Paternity pay eligible: ${r.payEligible ? "yes" : "no"}.` + (r.payEligible ? ` Weekly rate ${gbp(r.weeklyRate)}${r.belowCap ? " (90% of earnings, below the flat rate)" : ""}, total ${gbp(r.total)}. Claim deadline ${r.claimDeadline}.` : "")
265
+ )
266
+ );
267
+ server.registerTool(
268
+ "calculate_notice_pay",
269
+ {
270
+ title: "UK notice pay / PILON",
271
+ description: "Estimate UK payment in lieu of notice (PILON): uses the higher of statutory notice (1 week per complete year, max 12) and contractual notice, uncapped weekly pay (salary / 52), with a net estimate. PILON is fully taxable as earnings; the \xA330,000 allowance does not apply. Source: urbitools.com.",
272
+ inputSchema: {
273
+ annual_salary: z.number().positive().describe("Gross annual salary in GBP"),
274
+ years_service: z.number().min(0).describe("Complete years of service"),
275
+ contractual_notice_weeks: z.number().min(0).optional().describe("Contractual notice in weeks, if any"),
276
+ weeks_already_worked: z.number().min(0).default(0).describe("Notice weeks already worked"),
277
+ marginal_tax_rate: z.union([z.literal(0.2), z.literal(0.4), z.literal(0.45)]).default(0.2).describe("Marginal income tax rate for the net estimate")
278
+ },
279
+ annotations: { readOnlyHint: true, openWorldHint: false }
280
+ },
281
+ async (a) => reply(
282
+ calculateNotice({
283
+ annualSalary: a.annual_salary,
284
+ yearsService: a.years_service,
285
+ contractualWeeks: a.contractual_notice_weeks ?? null,
286
+ workedWeeks: a.weeks_already_worked,
287
+ taxRate: a.marginal_tax_rate
288
+ }),
289
+ "/notice-pay-calculator",
290
+ (r) => `Notice period applying: ${r.applicableWeeks} weeks (statutory ${r.statutoryWeeks}${r.contractualWeeks != null ? `, contractual ${r.contractualWeeks}` : ""}). Unworked: ${r.unworkedWeeks} weeks at ${gbp(r.weeklyPay)}/week. PILON gross ${gbp(r.gross)}, estimated tax ${gbp(r.tax)}, net ${gbp(r.net)}.` + (r.noStatutoryMinimum ? " Note: under 1 year of service may mean no statutory minimum notice." : "")
291
+ )
292
+ );
293
+ return server;
294
+ }
295
+
296
+ // src/stdio.ts
297
+ await createServer().connect(new StdioServerTransport());
package/package.json CHANGED
@@ -1,6 +1,18 @@
1
1
  {
2
2
  "name": "urbitools-mcp",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Free MCP server for UK employment calculators (statutory redundancy pay, paternity pay, notice pay/PILON) with 2026/27 rates, from urbitools.com",
5
+ "type": "module",
6
+ "bin": { "urbitools-mcp": "dist/stdio.js" },
7
+ "files": ["dist", "README.md"],
8
+ "scripts": {
9
+ "build": "esbuild src/stdio.ts src/http.ts --bundle --platform=node --format=esm --target=node18 --outdir=dist --packages=external && chmod +x dist/stdio.js",
10
+ "test": "npm run build && node test/smoke.mjs"
11
+ },
12
+ "keywords": ["mcp", "uk", "redundancy", "paternity", "notice", "pilon", "calculator"],
13
+ "license": "MIT",
14
+ "homepage": "https://urbitools.com",
15
+ "engines": { "node": ">=18" },
16
+ "dependencies": { "@modelcontextprotocol/sdk": "^1.0.0", "zod": "^3.23.0" },
17
+ "devDependencies": { "esbuild": "^0.25.0", "@types/node": "latest", "typescript": "latest" }
18
+ }