@sodiumhq/mcp-pm 0.1.0-beta.5166 → 0.1.0-beta.5175

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/dist/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-Clf8G25g.mjs";
2
+ import { t as buildServer } from "./src-CxBEbLO4.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { SUPPORTED_PROTOCOL_VERSIONS, createMcpHandler } from "@modelcontextprotocol/server";
5
5
  import { createHash, randomUUID } from "node:crypto";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-Clf8G25g.mjs";
2
+ import { t as buildServer } from "./src-CxBEbLO4.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
  import { parseArgs } from "node:util";
@@ -1,6 +1,31 @@
1
1
  import { McpServer } from "@modelcontextprotocol/server";
2
2
  import { z } from "zod";
3
3
  import { randomUUID } from "node:crypto";
4
+ const DEFAULT_PRACTICE_LOCALE = {
5
+ currencyCode: "GBP",
6
+ locale: "en-GB"
7
+ };
8
+ /**
9
+ * Formats an amount in the practice's currency, e.g. "£1,234.50".
10
+ * Pass `currencyOverride` for records that carry their own currency (an invoice synced from an
11
+ * accounting platform) — it always wins over the practice default.
12
+ */
13
+ function money(amount, practice = DEFAULT_PRACTICE_LOCALE, currencyOverride) {
14
+ const value = amount ?? 0;
15
+ const currency = currencyOverride || practice.currencyCode || "GBP";
16
+ try {
17
+ return new Intl.NumberFormat(practice.locale || "en-GB", {
18
+ style: "currency",
19
+ currency,
20
+ currencyDisplay: "narrowSymbol",
21
+ minimumFractionDigits: 2,
22
+ maximumFractionDigits: 2
23
+ }).format(value);
24
+ } catch {
25
+ return `${currency} ${value.toFixed(2)}`;
26
+ }
27
+ }
28
+ //#endregion
4
29
  //#region ../mcp-core/src/generated/core/bodySerializer.gen.ts
5
30
  const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
6
31
  Object.entries({
@@ -2205,7 +2230,7 @@ const createTimeEntry = (options) => (options.client ?? client).post({
2205
2230
  /**
2206
2231
  * List Unbilled Time Clients
2207
2232
  *
2208
- * Lists the clients that have unbilled billable time, with the unbilled minutes and entry count for each. Paged, ordered by client name; search filters on the client name.
2233
+ * Lists the clients that have unbilled billable time, with the unbilled minutes, entry count, value, oldest entry date and unrated entry count for each. The date window and team member narrow which entries are counted, so the figures always match the entries behind them. Paged, ordered by client name unless sorted otherwise; search filters on the client name.
2209
2234
  */
2210
2235
  const getUnbilledTimeClients = (options) => (options.client ?? client).get({
2211
2236
  security: [{
@@ -2349,6 +2374,7 @@ var SodiumApiClient = class {
2349
2374
  return this.activeTenant;
2350
2375
  }
2351
2376
  toolCallListener = null;
2377
+ practiceLocale = null;
2352
2378
  setToolCallListener(listener) {
2353
2379
  this.toolCallListener = listener;
2354
2380
  }
@@ -2389,6 +2415,22 @@ var SodiumApiClient = class {
2389
2415
  if (!this.mcpClient) return base;
2390
2416
  return `${base} (client=${sanitizeToken(this.mcpClient.name)}/${sanitizeToken(this.mcpClient.version)})`;
2391
2417
  }
2418
+ /**
2419
+ * The practice's currency + locale, for formatting money and dates in tool output.
2420
+ * Memoised per client instance — one client per session, so this costs a single request and
2421
+ * can't leak one practice's currency into another's session. Falls back to GBP / en-GB if the
2422
+ * practice details can't be read (an unconfigured or unreachable tenant must not break output).
2423
+ */
2424
+ async getPracticeLocale() {
2425
+ this.practiceLocale ??= this.getPracticeDetails().then((details) => ({
2426
+ currencyCode: details.currencyCode || "GBP",
2427
+ locale: details.locale || "en-GB"
2428
+ })).catch(() => {
2429
+ this.practiceLocale = null;
2430
+ return DEFAULT_PRACTICE_LOCALE;
2431
+ });
2432
+ return this.practiceLocale;
2433
+ }
2392
2434
  async getPracticeDetails() {
2393
2435
  const correlationId = randomUUID();
2394
2436
  const { data, error, response } = await getPracticeDetails({
@@ -4019,7 +4061,7 @@ function describeFilters$5(args) {
4019
4061
  //#region ../mcp-core/src/tools/get-client-summary.ts
4020
4062
  const GetClientSummaryInputSchema = { code: z.string().min(1, "Client code is required").describe("The client code (identifier). Usually discovered via list_clients first.") };
4021
4063
  async function handleGetClientSummary(api, { code }) {
4022
- const [clientResult, contactsResult, servicesResult, businessResult, datesResult, overdueResult, upcomingResult, customFieldDefsResult, customFieldValsResult] = await Promise.allSettled([
4064
+ const [practice, settled] = await Promise.all([api.getPracticeLocale(), Promise.allSettled([
4023
4065
  api.getClient(code),
4024
4066
  api.listClientContacts(code),
4025
4067
  api.listClientServices(code),
@@ -4042,7 +4084,8 @@ async function handleGetClientSummary(api, { code }) {
4042
4084
  limit: 50
4043
4085
  }),
4044
4086
  api.getClientCustomFieldValues(code)
4045
- ]);
4087
+ ])]);
4088
+ const [clientResult, contactsResult, servicesResult, businessResult, datesResult, overdueResult, upcomingResult, customFieldDefsResult, customFieldValsResult] = settled;
4046
4089
  if (clientResult.status === "rejected") {
4047
4090
  const err = clientResult.reason;
4048
4091
  return {
@@ -4057,6 +4100,7 @@ async function handleGetClientSummary(api, { code }) {
4057
4100
  return { content: [{
4058
4101
  type: "text",
4059
4102
  text: format$3({
4103
+ practice,
4060
4104
  client: clientResult.value,
4061
4105
  contacts: extract(contactsResult),
4062
4106
  services: extract(servicesResult),
@@ -4083,7 +4127,7 @@ function extract(result) {
4083
4127
  return result.value.data ?? [];
4084
4128
  }
4085
4129
  function format$3(input) {
4086
- const { client, contacts, services, businessDetails, clientDates, overdueTasks, upcomingTasks, customFieldDefs, customFieldValues, gaps } = input;
4130
+ const { practice, client, contacts, services, businessDetails, clientDates, overdueTasks, upcomingTasks, customFieldDefs, customFieldValues, gaps } = input;
4087
4131
  const lines = [];
4088
4132
  const name = client.name ?? "(no name)";
4089
4133
  const code = client.code ?? "(no code)";
@@ -4134,7 +4178,7 @@ function format$3(input) {
4134
4178
  else for (const s of activeServices) {
4135
4179
  const svcName = s.billableService?.name ?? "(unnamed)";
4136
4180
  const freq = s.billingFrequency ? ` ${s.billingFrequency}` : "";
4137
- const price = s.effectivePrice !== void 0 ? ` — £${s.effectivePrice.toFixed(2)}${freq}` : freq ? ` —${freq}` : "";
4181
+ const price = s.effectivePrice !== void 0 ? ` — ${money(s.effectivePrice, practice)}${freq}` : freq ? ` —${freq}` : "";
4138
4182
  lines.push(`- ${svcName}${price}`);
4139
4183
  }
4140
4184
  lines.push("", "--- Tasks ---");
@@ -4206,7 +4250,7 @@ function humanizeDateType(type) {
4206
4250
  //#region ../mcp-core/src/tools/get-engagement-summary.ts
4207
4251
  const GetEngagementSummaryInputSchema = { code: z.string().min(1, "Engagement code is required").describe("REQUIRED: The engagement code (identifier, e.g. 'ENG-123'). This is the unique code of a specific engagement — NOT a description like 'the unsent proposal' or 'ACME's proposal'. If the user describes an engagement by attribute (status, client, date) rather than by code, you MUST call list_proposals or list_engagement_letters first (with the appropriate filters) to find the engagement's code, then pass that code here.") };
4208
4252
  async function handleGetEngagementSummary(api, { code }) {
4209
- const [engagementResult, emailsResult] = await Promise.allSettled([api.getEngagement(code), api.getEngagementEmails(code)]);
4253
+ const [practice, [engagementResult, emailsResult]] = await Promise.all([api.getPracticeLocale(), Promise.allSettled([api.getEngagement(code), api.getEngagementEmails(code)])]);
4210
4254
  if (engagementResult.status === "rejected") {
4211
4255
  const err = engagementResult.reason;
4212
4256
  return {
@@ -4223,12 +4267,13 @@ async function handleGetEngagementSummary(api, { code }) {
4223
4267
  text: format$2({
4224
4268
  engagement,
4225
4269
  emails: emailsResult.status === "fulfilled" ? emailsResult.value : [],
4226
- gaps: emailsResult.status === "rejected" ? ["email history"] : []
4270
+ gaps: emailsResult.status === "rejected" ? ["email history"] : [],
4271
+ practice
4227
4272
  })
4228
4273
  }] };
4229
4274
  }
4230
4275
  function format$2(input) {
4231
- const { engagement: e, emails, gaps } = input;
4276
+ const { engagement: e, emails, gaps, practice } = input;
4232
4277
  const lines = [];
4233
4278
  const code = e.code ?? "(no code)";
4234
4279
  const typeLabel = e.typeName ?? e.type ?? "";
@@ -4242,12 +4287,12 @@ function format$2(input) {
4242
4287
  lines.push(`Recipient: ${recipientName || "(no name)"}${email}`);
4243
4288
  }
4244
4289
  if (e.lastViewed) lines.push(`Last viewed by client: ${e.lastViewed}`);
4245
- const valueLines = formatValues(e);
4290
+ const valueLines = formatValues(e, practice);
4246
4291
  if (valueLines.length > 0) lines.push("", "--- Value ---", ...valueLines);
4247
4292
  const services = e.proposalServices ?? [];
4248
4293
  if (services.length > 0) {
4249
4294
  lines.push("", `--- Services (${services.length}) ---`);
4250
- for (const s of services) lines.push(`- ${formatService$1(s)}`);
4295
+ for (const s of services) lines.push(`- ${formatService$1(s, practice)}`);
4251
4296
  }
4252
4297
  if (e.acceptance && e.status === "Accepted") {
4253
4298
  lines.push("", "--- Acceptance ---");
@@ -4273,39 +4318,34 @@ function format$2(input) {
4273
4318
  if (gaps.length > 0) lines.push("", `Note: could not load ${gaps.join(", ")} (partial failure). Retry for a complete picture.`);
4274
4319
  return lines.join("\n");
4275
4320
  }
4276
- function formatValues(e) {
4321
+ function formatValues(e, practice) {
4277
4322
  const out = [];
4278
- if (e.annualTotal && e.annualTotal > 0) out.push(`Annual: £${formatMoney$1(e.annualTotal)}`);
4279
- if (e.quarterlyTotal && e.quarterlyTotal > 0) out.push(`Quarterly: £${formatMoney$1(e.quarterlyTotal)}`);
4280
- if (e.monthlyTotal && e.monthlyTotal > 0) out.push(`Monthly: £${formatMoney$1(e.monthlyTotal)}`);
4281
- if (e.weeklyTotal && e.weeklyTotal > 0) out.push(`Weekly: £${formatMoney$1(e.weeklyTotal)}`);
4282
- if (e.oneOffTotal && e.oneOffTotal > 0) out.push(`One-off: £${formatMoney$1(e.oneOffTotal)}`);
4283
- if (e.totalValue && e.totalValue > 0) out.push(`Total contracted value: £${formatMoney$1(e.totalValue)}`);
4323
+ if (e.annualTotal && e.annualTotal > 0) out.push(`Annual: ${money(e.annualTotal, practice)}`);
4324
+ if (e.quarterlyTotal && e.quarterlyTotal > 0) out.push(`Quarterly: ${money(e.quarterlyTotal, practice)}`);
4325
+ if (e.monthlyTotal && e.monthlyTotal > 0) out.push(`Monthly: ${money(e.monthlyTotal, practice)}`);
4326
+ if (e.weeklyTotal && e.weeklyTotal > 0) out.push(`Weekly: ${money(e.weeklyTotal, practice)}`);
4327
+ if (e.oneOffTotal && e.oneOffTotal > 0) out.push(`One-off: ${money(e.oneOffTotal, practice)}`);
4328
+ if (e.totalValue && e.totalValue > 0) out.push(`Total contracted value: ${money(e.totalValue, practice)}`);
4284
4329
  return out;
4285
4330
  }
4286
- function formatService$1(s) {
4331
+ function formatService$1(s, practice) {
4287
4332
  const name = s.billableService?.name ?? "(unnamed)";
4288
4333
  const freq = s.billingFrequency ? ` ${s.billingFrequency}` : "";
4289
- if (s.effectivePrice !== void 0 && s.effectivePrice !== null) return `${name} — £${formatMoney$1(s.effectivePrice)}${freq}`;
4334
+ if (s.effectivePrice !== void 0 && s.effectivePrice !== null) return `${name} — ${money(s.effectivePrice, practice)}${freq}`;
4290
4335
  return freq ? `${name} —${freq}` : name;
4291
4336
  }
4292
4337
  function formatEmail$1(em) {
4293
4338
  return `- ${em.sentDate ?? "(no date)"}: ${em.subject ?? "(no subject)"}${em.status ? ` · ${em.status}` : ""}${em.toRecipients?.length ? ` → ${em.toRecipients.join(", ")}` : ""}`;
4294
4339
  }
4295
- function formatMoney$1(n) {
4296
- return n.toLocaleString("en-GB", {
4297
- minimumFractionDigits: 2,
4298
- maximumFractionDigits: 2
4299
- });
4300
- }
4301
4340
  //#endregion
4302
4341
  //#region ../mcp-core/src/tools/get-service-details.ts
4303
4342
  const GetServiceDetailsInputSchema = { code: z.string().min(1, "Service code is required").describe("The service code (identifier). Usually discovered via list_services first.") };
4304
4343
  async function handleGetServiceDetails(api, { code }) {
4305
4344
  try {
4345
+ const [practice, service] = await Promise.all([api.getPracticeLocale(), api.getService(code)]);
4306
4346
  return { content: [{
4307
4347
  type: "text",
4308
- text: format$1(await api.getService(code))
4348
+ text: format$1(service, practice)
4309
4349
  }] };
4310
4350
  } catch (error) {
4311
4351
  return {
@@ -4317,7 +4357,7 @@ async function handleGetServiceDetails(api, { code }) {
4317
4357
  };
4318
4358
  }
4319
4359
  }
4320
- function format$1(s) {
4360
+ function format$1(s, practice) {
4321
4361
  const lines = [];
4322
4362
  const name = s.name ?? "(no name)";
4323
4363
  const code = s.code ?? "(no code)";
@@ -4339,7 +4379,7 @@ function format$1(s) {
4339
4379
  else for (const a of s.agentAuthorisations) lines.push(`- ${a}`);
4340
4380
  lines.push("", `--- Pricing Options (${s.pricing?.length ?? 0}) ---`);
4341
4381
  if (!s.pricing || s.pricing.length === 0) lines.push("No pricing options configured.");
4342
- else for (const p of s.pricing) lines.push(formatPricingOption(p));
4382
+ else for (const p of s.pricing) lines.push(formatPricingOption(p, practice));
4343
4383
  if (s.pricingMode === "CustomTiers" && s.pricingTiers && s.pricingTiers.length > 0) {
4344
4384
  lines.push("", `--- Custom Pricing Tiers (${s.pricingTiers.length}) ---`);
4345
4385
  const tiers = [...s.pricingTiers].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
@@ -4359,9 +4399,9 @@ function format$1(s) {
4359
4399
  if (s.recurringTaskCount && s.recurringTaskCount > 0) lines.push("", `Recurring tasks using this service: ${s.recurringTaskCount}`);
4360
4400
  return lines.join("\n");
4361
4401
  }
4362
- function formatPricingOption(p) {
4402
+ function formatPricingOption(p, practice) {
4363
4403
  const freq = p.frequency ?? "(no frequency)";
4364
- const price = typeof p.price === "number" ? `£${p.price.toFixed(2)}` : "(no price)";
4404
+ const price = typeof p.price === "number" ? money(p.price, practice) : "(no price)";
4365
4405
  const rangeCount = p.revenueRangeOverrides?.length ?? 0;
4366
4406
  const tierCount = p.tierOverrides?.length ?? 0;
4367
4407
  const overrides = [];
@@ -4568,6 +4608,7 @@ const ListEngagementsInputSchema = {
4568
4608
  };
4569
4609
  async function handleListEngagements(api, args) {
4570
4610
  try {
4611
+ const practice = await api.getPracticeLocale();
4571
4612
  const query = args;
4572
4613
  const result = await api.listEngagements(query);
4573
4614
  const items = result.data ?? [];
@@ -4590,7 +4631,7 @@ async function handleListEngagements(api, args) {
4590
4631
  const lines = [
4591
4632
  desc ? total > items.length ? `Found ${total} engagements matching ${desc} (showing ${items.length}):` : `Found ${items.length} engagement${items.length === 1 ? "" : "s"} matching ${desc}:` : total > items.length ? `Showing ${items.length} of ${total} engagements:` : `${items.length} engagement${items.length === 1 ? "" : "s"}:`,
4592
4633
  "",
4593
- ...items.map(formatEngagement)
4634
+ ...items.map((e) => formatEngagement(e, practice))
4594
4635
  ];
4595
4636
  if (result.hasMore) {
4596
4637
  const nextOffset = (args.offset ?? 0) + items.length;
@@ -4610,20 +4651,14 @@ async function handleListEngagements(api, args) {
4610
4651
  };
4611
4652
  }
4612
4653
  }
4613
- function formatEngagement(e) {
4614
- return `- ${e.code ?? "(no code)"} · ${e.status ?? "?"} · ${e.client ? `${e.client.name ?? "(no name)"} (${e.client.code ?? "?"})` : "(no client)"}${e.date ? ` · ${e.date}` : ""}${formatAnnualValue(e)}${e.numberOfServices !== void 0 && e.numberOfServices !== null ? ` · ${e.numberOfServices} service${e.numberOfServices === 1 ? "" : "s"}` : ""}`;
4654
+ function formatEngagement(e, practice) {
4655
+ return `- ${e.code ?? "(no code)"} · ${e.status ?? "?"} · ${e.client ? `${e.client.name ?? "(no name)"} (${e.client.code ?? "?"})` : "(no client)"}${e.date ? ` · ${e.date}` : ""}${formatAnnualValue(e, practice)}${e.numberOfServices !== void 0 && e.numberOfServices !== null ? ` · ${e.numberOfServices} service${e.numberOfServices === 1 ? "" : "s"}` : ""}`;
4615
4656
  }
4616
- function formatAnnualValue(e) {
4617
- if (e.annualTotal && e.annualTotal > 0) return ` · annual £${formatMoney(e.annualTotal)}`;
4618
- if (e.totalValue && e.totalValue > 0) return ` · total £${formatMoney(e.totalValue)}`;
4657
+ function formatAnnualValue(e, practice) {
4658
+ if (e.annualTotal && e.annualTotal > 0) return ` · annual ${money(e.annualTotal, practice)}`;
4659
+ if (e.totalValue && e.totalValue > 0) return ` · total ${money(e.totalValue, practice)}`;
4619
4660
  return "";
4620
4661
  }
4621
- function formatMoney(n) {
4622
- return n.toLocaleString("en-GB", {
4623
- minimumFractionDigits: 2,
4624
- maximumFractionDigits: 2
4625
- });
4626
- }
4627
4662
  function describeFilters$4(args) {
4628
4663
  const parts = [];
4629
4664
  if (args.status) parts.push(`status ${args.status}`);
@@ -6715,17 +6750,18 @@ function fmtMinutes$4(minutes) {
6715
6750
  const m = minutes % 60;
6716
6751
  return h > 0 ? `${h}h${m ? ` ${m}m` : ""}` : `${m}m`;
6717
6752
  }
6718
- function formatEntry(e) {
6753
+ function formatEntry(e, practice) {
6719
6754
  const who = e.user?.name ? ` · ${e.user.name}` : "";
6720
6755
  const client = e.client?.name ? ` · ${e.client.name}` : "";
6721
6756
  const task = e.taskItem?.name ? ` · task: ${e.taskItem.name}` : "";
6722
6757
  const billable = e.isBillable ? e.billedLineItemCode ? " · billable (billed)" : " · billable (unbilled)" : " · non-billable";
6723
- const rate = e.isBillable && e.hourlyRate != null ? ` @ £${e.hourlyRate}/h` : "";
6758
+ const rate = e.isBillable && e.hourlyRate != null ? ` @ ${money(e.hourlyRate, practice)}/h` : "";
6724
6759
  const desc = e.description ? `\n ${e.description.length > 150 ? `${e.description.slice(0, 150)}…` : e.description}` : "";
6725
6760
  return `- ${e.entryDate ?? "?"} · ${fmtMinutes$4(e.durationMinutes)}${who}${client}${task}${billable}${rate} (${e.code ?? "no code"})${desc}`;
6726
6761
  }
6727
6762
  async function handleListTimeEntries(api, args) {
6728
6763
  try {
6764
+ const practice = await api.getPracticeLocale();
6729
6765
  const query = args;
6730
6766
  const result = await api.listTimeEntries(query);
6731
6767
  const items = result.data ?? [];
@@ -6742,7 +6778,7 @@ async function handleListTimeEntries(api, args) {
6742
6778
  const lines = [
6743
6779
  total > items.length ? `Showing ${items.length} of ${total} time entries (${fmtMinutes$4(pageMinutes)} on this page — page again or use get_service_time_report for full totals):` : `${items.length} time entr${items.length === 1 ? "y" : "ies"}, ${fmtMinutes$4(pageMinutes)} total:`,
6744
6780
  "",
6745
- ...items.map(formatEntry)
6781
+ ...items.map((e) => formatEntry(e, practice))
6746
6782
  ];
6747
6783
  if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
6748
6784
  return { content: [{
@@ -7248,14 +7284,14 @@ async function handleLogTimeEntry(api, args) {
7248
7284
  ...args.isBillable !== void 0 ? { isBillable: args.isBillable } : {},
7249
7285
  hourlyRate: args.hourlyRate ?? null
7250
7286
  };
7251
- const entry = await api.createTimeEntry(body);
7287
+ const [practice, entry] = await Promise.all([api.getPracticeLocale(), api.createTimeEntry(body)]);
7252
7288
  const lines = [`Time entry logged: ${fmtMinutes$2(entry.durationMinutes ?? args.durationMinutes)} on ${entry.entryDate ?? args.entryDate} (${entry.code ?? "no code"})`];
7253
7289
  if (entry.user?.name) lines.push(`For: ${entry.user.name}`);
7254
7290
  const clientRef = entry.client?.name ?? args.clientCode;
7255
7291
  if (clientRef) lines.push(`Client: ${clientRef}`);
7256
7292
  const taskRef = entry.taskItem?.name ?? args.taskItemCode;
7257
7293
  if (taskRef) lines.push(`Task: ${taskRef}`);
7258
- lines.push(`Billable: ${entry.isBillable ? `yes${entry.hourlyRate != null ? ` @ £${entry.hourlyRate}/h` : ""}` : "no"}`);
7294
+ lines.push(`Billable: ${entry.isBillable ? `yes${entry.hourlyRate != null ? ` @ ${money(entry.hourlyRate, practice)}/h` : ""}` : "no"}`);
7259
7295
  if (entry.description) lines.push(`Description: ${entry.description}`);
7260
7296
  return { content: [{
7261
7297
  type: "text",
@@ -7615,17 +7651,18 @@ function lineValue(i) {
7615
7651
  function fmtDate(d) {
7616
7652
  return d ? d.slice(0, 10) : "?";
7617
7653
  }
7618
- function formatItem(i) {
7654
+ function formatItem(i, practice) {
7619
7655
  const client = i.client?.name ? `${i.client.name}${i.client.code ? ` (${i.client.code})` : ""}` : "(no client)";
7620
7656
  const service = i.clientBillableService?.name ?? i.servicePackage?.name ?? "";
7621
7657
  const qty = i.quantity && i.quantity !== 1 ? `${i.quantity} × ` : "";
7622
- const value = `£${lineValue(i).toFixed(2)}${i.vatRate ? ` (VAT: ${i.vatRate})` : ""}`;
7658
+ const value = `${money(lineValue(i), practice)}${i.vatRate ? ` (VAT: ${i.vatRate})` : ""}`;
7623
7659
  const period = i.periodStart ? ` · period ${fmtDate(i.periodStart)} → ${fmtDate(i.periodEnd)}` : "";
7624
7660
  const desc = i.description ? `\n ${i.description.length > 120 ? `${i.description.slice(0, 120)}…` : i.description}` : "";
7625
7661
  return `- [${i.status ?? "?"}] ${fmtDate(i.billingDate)} · ${client}${service ? ` · ${service}` : ""} · ${qty}${value}${period} (${i.code ?? "no code"})${desc}`;
7626
7662
  }
7627
7663
  async function handleListBillingLineItems(api, args) {
7628
7664
  try {
7665
+ const practice = await api.getPracticeLocale();
7629
7666
  const query = args;
7630
7667
  const result = await api.listBillingLineItems(query);
7631
7668
  const items = result.data ?? [];
@@ -7640,9 +7677,9 @@ async function handleListBillingLineItems(api, args) {
7640
7677
  }] };
7641
7678
  const pageValue = items.reduce((sum, i) => sum + lineValue(i), 0);
7642
7679
  const lines = [
7643
- total > items.length ? `Showing ${items.length} of ${total} billing line items (£${pageValue.toFixed(2)} net on this page — page for the rest):` : `${items.length} billing line item${items.length === 1 ? "" : "s"}, £${pageValue.toFixed(2)} net total:`,
7680
+ total > items.length ? `Showing ${items.length} of ${total} billing line items (${money(pageValue, practice)} net on this page — page for the rest):` : `${items.length} billing line item${items.length === 1 ? "" : "s"}, ${money(pageValue, practice)} net total:`,
7644
7681
  "",
7645
- ...items.map(formatItem)
7682
+ ...items.map((i) => formatItem(i, practice))
7646
7683
  ];
7647
7684
  if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
7648
7685
  return { content: [{
@@ -7882,16 +7919,11 @@ const GetEngagementPipelineSummaryInputSchema = {
7882
7919
  dateFrom: z.string().optional().describe("Window start (YYYY-MM-DD). Use with dateRange='CustomDateRange'."),
7883
7920
  dateTo: z.string().optional().describe("Window end (YYYY-MM-DD). Use with dateRange='CustomDateRange'.")
7884
7921
  };
7885
- function fmtMoney(v) {
7886
- return `£${(v ?? 0).toLocaleString("en-GB", {
7887
- minimumFractionDigits: 2,
7888
- maximumFractionDigits: 2
7889
- })}`;
7890
- }
7891
7922
  async function handleGetEngagementPipelineSummary(api, args) {
7892
7923
  try {
7893
7924
  const query = args;
7894
- const s = await api.getEngagementPipelineSummary(query);
7925
+ const [practice, s] = await Promise.all([api.getPracticeLocale(), api.getEngagementPipelineSummary(query)]);
7926
+ const fmtMoney = (v) => money(v ?? 0, practice);
7895
7927
  const window = args.dateRange ? args.dateRange === "CustomDateRange" ? `${args.dateFrom ?? "?"} to ${args.dateTo ?? "?"}` : args.dateRange : "all time";
7896
7928
  const rows = [
7897
7929
  [
@@ -8168,9 +8200,9 @@ const ListInvoiceSubmissionsInputSchema = {
8168
8200
  limit: z.number().int().min(0).max(50).optional().describe("Maximum submissions per page. Default 10, max 50. Pass 0 for count-only."),
8169
8201
  offset: z.number().int().min(0).optional().describe("Pagination offset. Default 0.")
8170
8202
  };
8171
- function formatSubmission(s) {
8203
+ function formatSubmission(s, practice) {
8172
8204
  const client = s.client?.name ? `${s.client.name}${s.client.code ? ` (${s.client.code})` : ""}` : "(no client)";
8173
- const amount = `£${(s.totalAmount ?? 0).toFixed(2)}`;
8205
+ const amount = money(s.totalAmount ?? 0, practice);
8174
8206
  const external = s.externalInvoiceNumber ? ` · invoice ${s.externalInvoiceNumber}` : "";
8175
8207
  const submitted = s.submittedAt ? ` · submitted ${s.submittedAt.slice(0, 10)}` : "";
8176
8208
  const err = s.status === "Failed" && s.errorMessage ? `\n Error: ${s.errorMessage.slice(0, 150)}` : "";
@@ -8178,6 +8210,7 @@ function formatSubmission(s) {
8178
8210
  }
8179
8211
  async function handleListInvoiceSubmissions(api, args) {
8180
8212
  try {
8213
+ const practice = await api.getPracticeLocale();
8181
8214
  const query = args;
8182
8215
  const result = await api.listInvoiceSubmissions(query);
8183
8216
  const items = result.data ?? [];
@@ -8192,9 +8225,9 @@ async function handleListInvoiceSubmissions(api, args) {
8192
8225
  }] };
8193
8226
  const pageValue = items.reduce((sum, s) => sum + (s.totalAmount ?? 0), 0);
8194
8227
  const lines = [
8195
- total > items.length ? `Showing ${items.length} of ${total} invoice submissions (£${pageValue.toFixed(2)} on this page):` : `${items.length} invoice submission${items.length === 1 ? "" : "s"}, £${pageValue.toFixed(2)} total:`,
8228
+ total > items.length ? `Showing ${items.length} of ${total} invoice submissions (${money(pageValue, practice)} on this page):` : `${items.length} invoice submission${items.length === 1 ? "" : "s"}, ${money(pageValue, practice)} total:`,
8196
8229
  "",
8197
- ...items.map(formatSubmission)
8230
+ ...items.map((s) => formatSubmission(s, practice))
8198
8231
  ];
8199
8232
  if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
8200
8233
  return { content: [{
@@ -8221,15 +8254,16 @@ const ListServicePackagesInputSchema = {
8221
8254
  limit: z.number().int().min(0).max(50).optional().describe("Maximum packages per page. Default 10, max 50. Pass 0 for count-only."),
8222
8255
  offset: z.number().int().min(0).optional().describe("Pagination offset. Default 0.")
8223
8256
  };
8224
- function formatPackage(p) {
8257
+ function formatPackage(p, practice) {
8225
8258
  const archived = p.isArchived ? " [ARCHIVED]" : "";
8226
- const pricing = p.usePackagePricing ? ` · package price £${(p.packagePrice ?? 0).toFixed(2)}${p.packageBillingFrequency ? ` ${p.packageBillingFrequency}` : ""}${p.packageSetupFee ? ` + £${p.packageSetupFee.toFixed(2)} setup` : ""}` : ` · priced per service (~£${(p.totalAnnualValue ?? 0).toFixed(2)}/yr)`;
8259
+ const pricing = p.usePackagePricing ? ` · package price ${money(p.packagePrice ?? 0, practice)}${p.packageBillingFrequency ? ` ${p.packageBillingFrequency}` : ""}${p.packageSetupFee ? ` + ${money(p.packageSetupFee, practice)} setup` : ""}` : ` · priced per service (~${money(p.totalAnnualValue ?? 0, practice)}/yr)`;
8227
8260
  const services = (p.items ?? []).map((i) => `${i.billableServiceName ?? i.billableServiceCode}${i.billingFrequency ? ` (${i.billingFrequency})` : ""}`).join(", ");
8228
8261
  const desc = p.description ? `\n ${p.description.length > 120 ? `${p.description.slice(0, 120)}…` : p.description}` : "";
8229
8262
  return `- ${p.name ?? "(unnamed)"} (${p.code ?? "no code"})${archived} · ${p.numberOfServices ?? p.items?.length ?? 0} services${pricing}${desc}${services ? `\n Includes: ${services}` : ""}`;
8230
8263
  }
8231
8264
  async function handleListServicePackages(api, args) {
8232
8265
  try {
8266
+ const practice = await api.getPracticeLocale();
8233
8267
  const query = args;
8234
8268
  const result = await api.listServicePackages(query);
8235
8269
  const items = result.data ?? [];
@@ -8245,7 +8279,7 @@ async function handleListServicePackages(api, args) {
8245
8279
  const lines = [
8246
8280
  total > items.length ? `Showing ${items.length} of ${total} service packages:` : `${items.length} service package${items.length === 1 ? "" : "s"}:`,
8247
8281
  "",
8248
- ...items.map(formatPackage)
8282
+ ...items.map((p) => formatPackage(p, practice))
8249
8283
  ];
8250
8284
  if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
8251
8285
  return { content: [{
@@ -8502,9 +8536,21 @@ function fmtMinutes(minutes) {
8502
8536
  const m = minutes % 60;
8503
8537
  return h > 0 ? `${h}h${m ? ` ${m}m` : ""}` : `${m}m`;
8504
8538
  }
8539
+ function fmtWholeCurrency(amount, practice) {
8540
+ const currency = practice.currencyCode || "GBP";
8541
+ try {
8542
+ return new Intl.NumberFormat(practice.locale || "en-GB", {
8543
+ style: "currency",
8544
+ currency,
8545
+ maximumFractionDigits: 0
8546
+ }).format(amount);
8547
+ } catch {
8548
+ return `${currency} ${Math.round(amount)}`;
8549
+ }
8550
+ }
8505
8551
  async function handleGetPracticeDashboard(api) {
8506
8552
  try {
8507
- const [overdue, dueNext7, blocked, openDocRequests, pendingForms, unassignedSteps, unbilled, pipeline] = await Promise.allSettled([
8553
+ const [practice, settled] = await Promise.all([api.getPracticeLocale(), Promise.allSettled([
8508
8554
  api.listTasks({
8509
8555
  isOverdue: true,
8510
8556
  limit: 0
@@ -8532,7 +8578,8 @@ async function handleGetPracticeDashboard(api) {
8532
8578
  }),
8533
8579
  api.getUnbilledTimeClients({ limit: 50 }),
8534
8580
  api.getEngagementPipelineSummary({})
8535
- ]);
8581
+ ])]);
8582
+ const [overdue, dueNext7, blocked, openDocRequests, pendingForms, unassignedSteps, unbilled, pipeline] = settled;
8536
8583
  const rows = [];
8537
8584
  const failures = [];
8538
8585
  const count = (r, label, hint) => {
@@ -8565,7 +8612,7 @@ async function handleGetPracticeDashboard(api) {
8565
8612
  const openValue = (p.sentTotalValue ?? 0) + (p.viewedTotalValue ?? 0);
8566
8613
  rows.push({
8567
8614
  label: "Proposals awaiting a decision",
8568
- value: openCount === 0 ? "0" : `${openCount} worth £${openValue.toLocaleString("en-GB", { maximumFractionDigits: 0 })} — get_engagement_pipeline_summary`
8615
+ value: openCount === 0 ? "0" : `${openCount} worth ${fmtWholeCurrency(openValue, practice)} — get_engagement_pipeline_summary`
8569
8616
  });
8570
8617
  } else failures.push("Proposal pipeline");
8571
8618
  const lines = [
@@ -8749,10 +8796,10 @@ async function handleAddClientService(api, args) {
8749
8796
  ...rest.autoInvoice !== void 0 ? { autoInvoice: rest.autoInvoice } : {},
8750
8797
  nextBillingDate: rest.nextBillingDate ?? null
8751
8798
  };
8752
- const created = await api.createClientService(clientCode, body);
8799
+ const [practice, created] = await Promise.all([api.getPracticeLocale(), api.createClientService(clientCode, body)]);
8753
8800
  const lines = [
8754
8801
  `Service assigned: ${created.billableService?.name?.trim() || args.billableServiceCode} on client ${clientCode} (${created.code ?? "no code"})`,
8755
- `Status: ${created.status} · ${created.billingFrequency} · effective price £${(created.effectivePrice ?? created.price ?? 0).toFixed(2)}`,
8802
+ `Status: ${created.status} · ${created.billingFrequency} · effective price ${money(created.effectivePrice ?? created.price ?? 0, practice)}`,
8756
8803
  `Start: ${created.startDate?.slice(0, 10)}${created.endDate ? ` · end ${created.endDate.slice(0, 10)}` : " · ongoing"}`
8757
8804
  ];
8758
8805
  if (created.managedByUser?.name) lines.push(`Managed by: ${created.managedByUser.name}`);
@@ -8963,7 +9010,7 @@ const CreateProposalInputSchema = {
8963
9010
  };
8964
9011
  async function handleCreateProposal(api, args) {
8965
9012
  try {
8966
- const created = await api.createEngagement({
9013
+ const [practice, created] = await Promise.all([api.getPracticeLocale(), api.createEngagement({
8967
9014
  clientCode: args.clientCode,
8968
9015
  date: args.date,
8969
9016
  type: args.type ?? "ProposalAndEngagementLetter",
@@ -8973,8 +9020,8 @@ async function handleCreateProposal(api, args) {
8973
9020
  clientBillableServiceCodes: args.clientBillableServiceCodes ?? null,
8974
9021
  proposalTemplateCode: args.proposalTemplateCode ?? null,
8975
9022
  lofETemplateCode: args.lofETemplateCode ?? null
8976
- });
8977
- const value = created.totalValue != null ? `£${created.totalValue.toLocaleString("en-GB", { minimumFractionDigits: 2 })}` : void 0;
9023
+ })]);
9024
+ const value = created.totalValue != null ? money(created.totalValue, practice) : void 0;
8978
9025
  const lines = [`Proposal drafted: ${created.code} for ${created.client?.name?.trim() || args.clientCode}`, `Status: ${created.status ?? "Unsent"} · type ${created.type ?? args.type ?? "ProposalAndEngagementLetter"}${value ? ` · total value ${value}` : ""}`];
8979
9026
  const services = created.proposalServices ?? [];
8980
9027
  if (services.length > 0) lines.push(`Services included: ${services.map((s) => s.billableService?.name ?? s.code ?? "?").join(", ")}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.5166",
3
+ "version": "0.1.0-beta.5175",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {