@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 +1 -1
- package/dist/index.js +1 -1
- package/dist/{src-Clf8G25g.mjs → src-CxBEbLO4.mjs} +120 -73
- package/package.json +1 -1
package/dist/http.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as buildServer } from "./src-
|
|
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-
|
|
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 [
|
|
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 ? ` —
|
|
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:
|
|
4279
|
-
if (e.quarterlyTotal && e.quarterlyTotal > 0) out.push(`Quarterly:
|
|
4280
|
-
if (e.monthlyTotal && e.monthlyTotal > 0) out.push(`Monthly:
|
|
4281
|
-
if (e.weeklyTotal && e.weeklyTotal > 0) out.push(`Weekly:
|
|
4282
|
-
if (e.oneOffTotal && e.oneOffTotal > 0) out.push(`One-off:
|
|
4283
|
-
if (e.totalValue && e.totalValue > 0) out.push(`Total contracted value:
|
|
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} —
|
|
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(
|
|
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" ?
|
|
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
|
|
4618
|
-
if (e.totalValue && e.totalValue > 0) return ` · total
|
|
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 ? ` @
|
|
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 ? ` @
|
|
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 =
|
|
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 (
|
|
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 =
|
|
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 (
|
|
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
|
|
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 [
|
|
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
|
|
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
|
|
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 ?
|
|
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(", ")}`);
|