@sodiumhq/mcp-pm 0.1.0-beta.5166 → 0.1.0-beta.5330
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-BY8wr-4D.mjs} +170 -78
- 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-BY8wr-4D.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-BY8wr-4D.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({
|
|
@@ -1325,7 +1350,7 @@ const getEmailBroadcast = (options) => (options.client ?? client).get({
|
|
|
1325
1350
|
/**
|
|
1326
1351
|
* List Broadcast Recipients
|
|
1327
1352
|
*
|
|
1328
|
-
* Returns the paged recipient list. For drafts the recipients are resolved live (pre-send view including skipped clients with reasons, e.g. no resolvable contact or email preference not set); after sending, the stored snapshot with per-client delivery status is returned.
|
|
1353
|
+
* Returns the paged recipient list, optionally filtered by client name via the search parameter. For drafts the recipients are resolved live (pre-send view including skipped clients with reasons, e.g. no resolvable contact or email preference not set); after sending, the stored snapshot with per-client delivery status is returned.
|
|
1329
1354
|
*/
|
|
1330
1355
|
const getEmailBroadcastRecipients = (options) => (options.client ?? client).get({
|
|
1331
1356
|
security: [{
|
|
@@ -1689,7 +1714,7 @@ const getServiceTimeReport = (options) => (options.client ?? client).get({
|
|
|
1689
1714
|
/**
|
|
1690
1715
|
* Get Team Time Report
|
|
1691
1716
|
*
|
|
1692
|
-
* Hours logged per team member over the window against the hours they were expected to log, so under-logging and over-logging both stand out without checking person by person. Members who logged nothing still get a row. Expectations come from each member's expected week (or the practice standard), pro-rated across working days and capped at today when the window runs past it. Short windows also carry a per-day breakdown. Pass teamCode to narrow to one team. Requires time tracking to be in use.
|
|
1717
|
+
* Hours logged per team member over the window against the hours they were expected to log, so under-logging and over-logging both stand out without checking person by person. Members who logged nothing still get a row. Expectations come from each member's expected week (or the practice standard), pro-rated across working days and capped at today when the window runs past it. Short windows also carry a per-day breakdown, and every window carries each member's split by task category. Pass teamCode to narrow to one team, or categoryCode to narrow every figure to one task category (which drops the expectation, since a working pattern covers all work rather than one category's). Requires time tracking to be in use.
|
|
1693
1718
|
*/
|
|
1694
1719
|
const getTeamTimeReport = (options) => (options.client ?? client).get({
|
|
1695
1720
|
security: [{
|
|
@@ -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. Users restricted to their own time see a rollup of their own entries only. 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}`);
|
|
@@ -4776,7 +4811,25 @@ const sortByEnum$18 = z.enum([
|
|
|
4776
4811
|
"StartDate",
|
|
4777
4812
|
"Category",
|
|
4778
4813
|
"ClientName",
|
|
4779
|
-
"AssignedUser"
|
|
4814
|
+
"AssignedUser",
|
|
4815
|
+
"StatutoryDueDate",
|
|
4816
|
+
"Stage",
|
|
4817
|
+
"TimeInStage",
|
|
4818
|
+
"ClientManager",
|
|
4819
|
+
"ClientPartner",
|
|
4820
|
+
"ClientAssociate"
|
|
4821
|
+
]);
|
|
4822
|
+
const stepTypeEnum$1 = z.enum([
|
|
4823
|
+
"Standard",
|
|
4824
|
+
"SendEmail",
|
|
4825
|
+
"DocumentRequest",
|
|
4826
|
+
"DocumentApproval",
|
|
4827
|
+
"ClientConfirmation",
|
|
4828
|
+
"SendDataForm",
|
|
4829
|
+
"SetServiceStage",
|
|
4830
|
+
"UpdateClientData",
|
|
4831
|
+
"SetPipelineStage",
|
|
4832
|
+
"DocumentUpload"
|
|
4780
4833
|
]);
|
|
4781
4834
|
const ListTasksInputSchema = {
|
|
4782
4835
|
user: z.array(z.string()).optional().describe("Filter by assigned user codes. For 'my tasks' use the current user's code from the startup context. For another team member's tasks, pass their code. Omit to see tasks across all users (useful for practice managers)."),
|
|
@@ -4790,6 +4843,16 @@ const ListTasksInputSchema = {
|
|
|
4790
4843
|
category: z.array(z.string()).optional().describe("Filter by task category codes (e.g. tax, audit, compliance categories)."),
|
|
4791
4844
|
team: z.array(z.string()).optional().describe("Filter by team codes (if the practice uses teams to group users)."),
|
|
4792
4845
|
recurringTask: z.array(z.string()).optional().describe("Filter by recurring-task template codes — returns all instances generated from those templates."),
|
|
4846
|
+
clientManager: z.array(z.string()).optional().describe("Filter by the PRIMARY client's client manager (user codes from list_users). Use for 'tasks for the clients I manage' / 'work on Jane's client portfolio' — this is the client's manager, NOT the task assignee (use `user` for that). Tasks with no client never match."),
|
|
4847
|
+
clientPartner: z.array(z.string()).optional().describe("Filter by the PRIMARY client's partner (user codes from list_users). Use for 'everything across the partner's clients'. Not the task assignee. Tasks with no client never match."),
|
|
4848
|
+
clientAssociate: z.array(z.string()).optional().describe("Filter by the PRIMARY client's associate (user codes from list_users). Only meaningful if the practice uses the associate role — many do not, and some rename it. Not the task assignee. Tasks with no client never match."),
|
|
4849
|
+
service: z.array(z.string()).optional().describe("Filter by billable service codes (from list_services) — tasks linked, via the client's service assignment, to these services. Use for 'all VAT return tasks' style questions."),
|
|
4850
|
+
workflow: z.array(z.string()).optional().describe("Filter by workflow codes (from list_workflows) — tasks spawned from these workflows."),
|
|
4851
|
+
stepType: z.array(stepTypeEnum$1).optional().describe("Filter to tasks that contain at least one (non-omitted) workflow step of these types — e.g. ['DocumentRequest'] for 'which tasks are waiting on client documents'."),
|
|
4852
|
+
currentStep: z.array(z.string()).optional().describe("Filter to tasks whose CURRENT workflow step has one of these exact names (as shown in get_workflow). Use for 'everything sitting at Partner review'."),
|
|
4853
|
+
isUnassigned: z.boolean().optional().describe("Include tasks with no assignee. Alone: only unassigned tasks. Combined with `user`: unassigned OR assigned to those users. Use for 'what needs an owner'."),
|
|
4854
|
+
isAutomationDisabled: z.boolean().optional().describe("true = only tasks with automation switched off; false = only those with it on. Task previews are excluded either way. Rarely needed."),
|
|
4855
|
+
search: z.string().min(3).optional().describe("Free-text search across task name, task code and primary client name (minimum 3 characters). Projected tasks are excluded when set. Prefer `client` when the client code is known."),
|
|
4793
4856
|
savedFilter: z.string().optional().describe("Apply a user-saved filter by its code. Other filter parameters override fields from the saved filter."),
|
|
4794
4857
|
includeProjected: z.boolean().optional().describe("Set true to include projected (virtual, not-yet-materialised) tasks in the results. Projected tasks are always NotStarted. Useful for 'what's coming up' queries."),
|
|
4795
4858
|
includeWorkflowSteps: z.boolean().optional().describe("Set true for Agenda Mode: returns both tasks AND their workflow steps as individual rows. Each step row has its parent task's properties plus step-specific details. Useful for 'what's the next action on X' or when workflow step granularity matters."),
|
|
@@ -4855,6 +4918,18 @@ function describeFilters$2(args) {
|
|
|
4855
4918
|
if (args.dateRange) parts.push(`date ${args.dateRange}${args.dateBasis ? ` (${args.dateBasis})` : ""}${args.dateRange === "CustomDateRange" && args.startDate && args.endDate ? ` ${args.startDate} to ${args.endDate}` : ""}`);
|
|
4856
4919
|
if (args.category?.length) parts.push(`category ${args.category.join(",")}`);
|
|
4857
4920
|
if (args.team?.length) parts.push(`team ${args.team.join(",")}`);
|
|
4921
|
+
if (args.clientManager?.length) parts.push(`client manager ${args.clientManager.join(",")}`);
|
|
4922
|
+
if (args.clientPartner?.length) parts.push(`client partner ${args.clientPartner.join(",")}`);
|
|
4923
|
+
if (args.clientAssociate?.length) parts.push(`client associate ${args.clientAssociate.join(",")}`);
|
|
4924
|
+
if (args.service?.length) parts.push(`service ${args.service.join(",")}`);
|
|
4925
|
+
if (args.workflow?.length) parts.push(`workflow ${args.workflow.join(",")}`);
|
|
4926
|
+
if (args.stepType?.length) parts.push(`step type ${args.stepType.join("/")}`);
|
|
4927
|
+
if (args.currentStep?.length) parts.push(`current step ${args.currentStep.join("/")}`);
|
|
4928
|
+
if (args.isUnassigned) parts.push("unassigned");
|
|
4929
|
+
if (args.isAutomationDisabled !== void 0) parts.push(`automation ${args.isAutomationDisabled ? "off" : "on"}`);
|
|
4930
|
+
if (args.search) parts.push(`search "${args.search}"`);
|
|
4931
|
+
if (args.recurringTask?.length) parts.push(`recurring task ${args.recurringTask.join(",")}`);
|
|
4932
|
+
if (args.savedFilter) parts.push(`saved filter ${args.savedFilter}`);
|
|
4858
4933
|
return parts.join(", ");
|
|
4859
4934
|
}
|
|
4860
4935
|
//#endregion
|
|
@@ -6715,17 +6790,18 @@ function fmtMinutes$4(minutes) {
|
|
|
6715
6790
|
const m = minutes % 60;
|
|
6716
6791
|
return h > 0 ? `${h}h${m ? ` ${m}m` : ""}` : `${m}m`;
|
|
6717
6792
|
}
|
|
6718
|
-
function formatEntry(e) {
|
|
6793
|
+
function formatEntry(e, practice) {
|
|
6719
6794
|
const who = e.user?.name ? ` · ${e.user.name}` : "";
|
|
6720
6795
|
const client = e.client?.name ? ` · ${e.client.name}` : "";
|
|
6721
6796
|
const task = e.taskItem?.name ? ` · task: ${e.taskItem.name}` : "";
|
|
6722
6797
|
const billable = e.isBillable ? e.billedLineItemCode ? " · billable (billed)" : " · billable (unbilled)" : " · non-billable";
|
|
6723
|
-
const rate = e.isBillable && e.hourlyRate != null ? ` @
|
|
6798
|
+
const rate = e.isBillable && e.hourlyRate != null ? ` @ ${money(e.hourlyRate, practice)}/h` : "";
|
|
6724
6799
|
const desc = e.description ? `\n ${e.description.length > 150 ? `${e.description.slice(0, 150)}…` : e.description}` : "";
|
|
6725
6800
|
return `- ${e.entryDate ?? "?"} · ${fmtMinutes$4(e.durationMinutes)}${who}${client}${task}${billable}${rate} (${e.code ?? "no code"})${desc}`;
|
|
6726
6801
|
}
|
|
6727
6802
|
async function handleListTimeEntries(api, args) {
|
|
6728
6803
|
try {
|
|
6804
|
+
const practice = await api.getPracticeLocale();
|
|
6729
6805
|
const query = args;
|
|
6730
6806
|
const result = await api.listTimeEntries(query);
|
|
6731
6807
|
const items = result.data ?? [];
|
|
@@ -6742,7 +6818,7 @@ async function handleListTimeEntries(api, args) {
|
|
|
6742
6818
|
const lines = [
|
|
6743
6819
|
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
6820
|
"",
|
|
6745
|
-
...items.map(formatEntry)
|
|
6821
|
+
...items.map((e) => formatEntry(e, practice))
|
|
6746
6822
|
];
|
|
6747
6823
|
if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
|
|
6748
6824
|
return { content: [{
|
|
@@ -6965,6 +7041,8 @@ const TOPICS = {
|
|
|
6965
7041
|
|
|
6966
7042
|
- "Show Jane's overdue tasks" / "What's blocked right now?"
|
|
6967
7043
|
- "Tasks for ACME due in the next 7 days"
|
|
7044
|
+
- "What's outstanding across the clients Jane manages?" — clientManager filter (the client's manager, not the assignee)
|
|
7045
|
+
- "Save that as a view called 'My portfolio'" — create_saved_task_filter with clientManagerCodes
|
|
6968
7046
|
- "How many tasks are due this month?" (count-only, no listing)
|
|
6969
7047
|
- "Show me the task TSK-123 in full" — checklist, workflow progress, notes, client
|
|
6970
7048
|
- "What are the steps in our VAT return workflow?"
|
|
@@ -7227,7 +7305,7 @@ const LogTimeEntryInputSchema = {
|
|
|
7227
7305
|
workflowGroupNumber: z.number().int().optional().describe("Workflow group number of the step the time relates to. Requires taskItemCode and workflowStepNumber. Get numbers from get_task_context."),
|
|
7228
7306
|
workflowStepNumber: z.number().int().optional().describe("Workflow step number the time relates to. Requires taskItemCode and workflowGroupNumber."),
|
|
7229
7307
|
isBillable: z.boolean().optional().describe("Whether the time can be billed to the client. Defaults to the practice's configuration — only pass when the user is explicit ('log 30 non-chargeable minutes')."),
|
|
7230
|
-
hourlyRate: z.number().min(0).optional().describe("Charge-out rate per hour. Omit to use the team member's
|
|
7308
|
+
hourlyRate: z.number().min(0).optional().describe("Charge-out rate per hour. Omit to use the team member's rate effective on the entry date (rates are date-effective, so a backdated entry gets the rate that applied then), then the practice default. Only pass when the user names a rate.")
|
|
7231
7309
|
};
|
|
7232
7310
|
function fmtMinutes$2(minutes) {
|
|
7233
7311
|
const h = Math.floor(minutes / 60);
|
|
@@ -7248,14 +7326,14 @@ async function handleLogTimeEntry(api, args) {
|
|
|
7248
7326
|
...args.isBillable !== void 0 ? { isBillable: args.isBillable } : {},
|
|
7249
7327
|
hourlyRate: args.hourlyRate ?? null
|
|
7250
7328
|
};
|
|
7251
|
-
const entry = await api.createTimeEntry(body);
|
|
7329
|
+
const [practice, entry] = await Promise.all([api.getPracticeLocale(), api.createTimeEntry(body)]);
|
|
7252
7330
|
const lines = [`Time entry logged: ${fmtMinutes$2(entry.durationMinutes ?? args.durationMinutes)} on ${entry.entryDate ?? args.entryDate} (${entry.code ?? "no code"})`];
|
|
7253
7331
|
if (entry.user?.name) lines.push(`For: ${entry.user.name}`);
|
|
7254
7332
|
const clientRef = entry.client?.name ?? args.clientCode;
|
|
7255
7333
|
if (clientRef) lines.push(`Client: ${clientRef}`);
|
|
7256
7334
|
const taskRef = entry.taskItem?.name ?? args.taskItemCode;
|
|
7257
7335
|
if (taskRef) lines.push(`Task: ${taskRef}`);
|
|
7258
|
-
lines.push(`Billable: ${entry.isBillable ? `yes${entry.hourlyRate != null ? ` @
|
|
7336
|
+
lines.push(`Billable: ${entry.isBillable ? `yes${entry.hourlyRate != null ? ` @ ${money(entry.hourlyRate, practice)}/h` : ""}` : "no"}`);
|
|
7259
7337
|
if (entry.description) lines.push(`Description: ${entry.description}`);
|
|
7260
7338
|
return { content: [{
|
|
7261
7339
|
type: "text",
|
|
@@ -7615,17 +7693,18 @@ function lineValue(i) {
|
|
|
7615
7693
|
function fmtDate(d) {
|
|
7616
7694
|
return d ? d.slice(0, 10) : "?";
|
|
7617
7695
|
}
|
|
7618
|
-
function formatItem(i) {
|
|
7696
|
+
function formatItem(i, practice) {
|
|
7619
7697
|
const client = i.client?.name ? `${i.client.name}${i.client.code ? ` (${i.client.code})` : ""}` : "(no client)";
|
|
7620
7698
|
const service = i.clientBillableService?.name ?? i.servicePackage?.name ?? "";
|
|
7621
7699
|
const qty = i.quantity && i.quantity !== 1 ? `${i.quantity} × ` : "";
|
|
7622
|
-
const value =
|
|
7700
|
+
const value = `${money(lineValue(i), practice)}${i.vatRate ? ` (VAT: ${i.vatRate})` : ""}`;
|
|
7623
7701
|
const period = i.periodStart ? ` · period ${fmtDate(i.periodStart)} → ${fmtDate(i.periodEnd)}` : "";
|
|
7624
7702
|
const desc = i.description ? `\n ${i.description.length > 120 ? `${i.description.slice(0, 120)}…` : i.description}` : "";
|
|
7625
7703
|
return `- [${i.status ?? "?"}] ${fmtDate(i.billingDate)} · ${client}${service ? ` · ${service}` : ""} · ${qty}${value}${period} (${i.code ?? "no code"})${desc}`;
|
|
7626
7704
|
}
|
|
7627
7705
|
async function handleListBillingLineItems(api, args) {
|
|
7628
7706
|
try {
|
|
7707
|
+
const practice = await api.getPracticeLocale();
|
|
7629
7708
|
const query = args;
|
|
7630
7709
|
const result = await api.listBillingLineItems(query);
|
|
7631
7710
|
const items = result.data ?? [];
|
|
@@ -7640,9 +7719,9 @@ async function handleListBillingLineItems(api, args) {
|
|
|
7640
7719
|
}] };
|
|
7641
7720
|
const pageValue = items.reduce((sum, i) => sum + lineValue(i), 0);
|
|
7642
7721
|
const lines = [
|
|
7643
|
-
total > items.length ? `Showing ${items.length} of ${total} billing line items (
|
|
7722
|
+
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
7723
|
"",
|
|
7645
|
-
...items.map(formatItem)
|
|
7724
|
+
...items.map((i) => formatItem(i, practice))
|
|
7646
7725
|
];
|
|
7647
7726
|
if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
|
|
7648
7727
|
return { content: [{
|
|
@@ -7882,16 +7961,11 @@ const GetEngagementPipelineSummaryInputSchema = {
|
|
|
7882
7961
|
dateFrom: z.string().optional().describe("Window start (YYYY-MM-DD). Use with dateRange='CustomDateRange'."),
|
|
7883
7962
|
dateTo: z.string().optional().describe("Window end (YYYY-MM-DD). Use with dateRange='CustomDateRange'.")
|
|
7884
7963
|
};
|
|
7885
|
-
function fmtMoney(v) {
|
|
7886
|
-
return `£${(v ?? 0).toLocaleString("en-GB", {
|
|
7887
|
-
minimumFractionDigits: 2,
|
|
7888
|
-
maximumFractionDigits: 2
|
|
7889
|
-
})}`;
|
|
7890
|
-
}
|
|
7891
7964
|
async function handleGetEngagementPipelineSummary(api, args) {
|
|
7892
7965
|
try {
|
|
7893
7966
|
const query = args;
|
|
7894
|
-
const s = await api.getEngagementPipelineSummary(query);
|
|
7967
|
+
const [practice, s] = await Promise.all([api.getPracticeLocale(), api.getEngagementPipelineSummary(query)]);
|
|
7968
|
+
const fmtMoney = (v) => money(v ?? 0, practice);
|
|
7895
7969
|
const window = args.dateRange ? args.dateRange === "CustomDateRange" ? `${args.dateFrom ?? "?"} to ${args.dateTo ?? "?"}` : args.dateRange : "all time";
|
|
7896
7970
|
const rows = [
|
|
7897
7971
|
[
|
|
@@ -8168,9 +8242,9 @@ const ListInvoiceSubmissionsInputSchema = {
|
|
|
8168
8242
|
limit: z.number().int().min(0).max(50).optional().describe("Maximum submissions per page. Default 10, max 50. Pass 0 for count-only."),
|
|
8169
8243
|
offset: z.number().int().min(0).optional().describe("Pagination offset. Default 0.")
|
|
8170
8244
|
};
|
|
8171
|
-
function formatSubmission(s) {
|
|
8245
|
+
function formatSubmission(s, practice) {
|
|
8172
8246
|
const client = s.client?.name ? `${s.client.name}${s.client.code ? ` (${s.client.code})` : ""}` : "(no client)";
|
|
8173
|
-
const amount =
|
|
8247
|
+
const amount = money(s.totalAmount ?? 0, practice);
|
|
8174
8248
|
const external = s.externalInvoiceNumber ? ` · invoice ${s.externalInvoiceNumber}` : "";
|
|
8175
8249
|
const submitted = s.submittedAt ? ` · submitted ${s.submittedAt.slice(0, 10)}` : "";
|
|
8176
8250
|
const err = s.status === "Failed" && s.errorMessage ? `\n Error: ${s.errorMessage.slice(0, 150)}` : "";
|
|
@@ -8178,6 +8252,7 @@ function formatSubmission(s) {
|
|
|
8178
8252
|
}
|
|
8179
8253
|
async function handleListInvoiceSubmissions(api, args) {
|
|
8180
8254
|
try {
|
|
8255
|
+
const practice = await api.getPracticeLocale();
|
|
8181
8256
|
const query = args;
|
|
8182
8257
|
const result = await api.listInvoiceSubmissions(query);
|
|
8183
8258
|
const items = result.data ?? [];
|
|
@@ -8192,9 +8267,9 @@ async function handleListInvoiceSubmissions(api, args) {
|
|
|
8192
8267
|
}] };
|
|
8193
8268
|
const pageValue = items.reduce((sum, s) => sum + (s.totalAmount ?? 0), 0);
|
|
8194
8269
|
const lines = [
|
|
8195
|
-
total > items.length ? `Showing ${items.length} of ${total} invoice submissions (
|
|
8270
|
+
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
8271
|
"",
|
|
8197
|
-
...items.map(formatSubmission)
|
|
8272
|
+
...items.map((s) => formatSubmission(s, practice))
|
|
8198
8273
|
];
|
|
8199
8274
|
if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
|
|
8200
8275
|
return { content: [{
|
|
@@ -8221,15 +8296,16 @@ const ListServicePackagesInputSchema = {
|
|
|
8221
8296
|
limit: z.number().int().min(0).max(50).optional().describe("Maximum packages per page. Default 10, max 50. Pass 0 for count-only."),
|
|
8222
8297
|
offset: z.number().int().min(0).optional().describe("Pagination offset. Default 0.")
|
|
8223
8298
|
};
|
|
8224
|
-
function formatPackage(p) {
|
|
8299
|
+
function formatPackage(p, practice) {
|
|
8225
8300
|
const archived = p.isArchived ? " [ARCHIVED]" : "";
|
|
8226
|
-
const pricing = p.usePackagePricing ? ` · package price
|
|
8301
|
+
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
8302
|
const services = (p.items ?? []).map((i) => `${i.billableServiceName ?? i.billableServiceCode}${i.billingFrequency ? ` (${i.billingFrequency})` : ""}`).join(", ");
|
|
8228
8303
|
const desc = p.description ? `\n ${p.description.length > 120 ? `${p.description.slice(0, 120)}…` : p.description}` : "";
|
|
8229
8304
|
return `- ${p.name ?? "(unnamed)"} (${p.code ?? "no code"})${archived} · ${p.numberOfServices ?? p.items?.length ?? 0} services${pricing}${desc}${services ? `\n Includes: ${services}` : ""}`;
|
|
8230
8305
|
}
|
|
8231
8306
|
async function handleListServicePackages(api, args) {
|
|
8232
8307
|
try {
|
|
8308
|
+
const practice = await api.getPracticeLocale();
|
|
8233
8309
|
const query = args;
|
|
8234
8310
|
const result = await api.listServicePackages(query);
|
|
8235
8311
|
const items = result.data ?? [];
|
|
@@ -8245,7 +8321,7 @@ async function handleListServicePackages(api, args) {
|
|
|
8245
8321
|
const lines = [
|
|
8246
8322
|
total > items.length ? `Showing ${items.length} of ${total} service packages:` : `${items.length} service package${items.length === 1 ? "" : "s"}:`,
|
|
8247
8323
|
"",
|
|
8248
|
-
...items.map(formatPackage)
|
|
8324
|
+
...items.map((p) => formatPackage(p, practice))
|
|
8249
8325
|
];
|
|
8250
8326
|
if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
|
|
8251
8327
|
return { content: [{
|
|
@@ -8502,9 +8578,21 @@ function fmtMinutes(minutes) {
|
|
|
8502
8578
|
const m = minutes % 60;
|
|
8503
8579
|
return h > 0 ? `${h}h${m ? ` ${m}m` : ""}` : `${m}m`;
|
|
8504
8580
|
}
|
|
8581
|
+
function fmtWholeCurrency(amount, practice) {
|
|
8582
|
+
const currency = practice.currencyCode || "GBP";
|
|
8583
|
+
try {
|
|
8584
|
+
return new Intl.NumberFormat(practice.locale || "en-GB", {
|
|
8585
|
+
style: "currency",
|
|
8586
|
+
currency,
|
|
8587
|
+
maximumFractionDigits: 0
|
|
8588
|
+
}).format(amount);
|
|
8589
|
+
} catch {
|
|
8590
|
+
return `${currency} ${Math.round(amount)}`;
|
|
8591
|
+
}
|
|
8592
|
+
}
|
|
8505
8593
|
async function handleGetPracticeDashboard(api) {
|
|
8506
8594
|
try {
|
|
8507
|
-
const [
|
|
8595
|
+
const [practice, settled] = await Promise.all([api.getPracticeLocale(), Promise.allSettled([
|
|
8508
8596
|
api.listTasks({
|
|
8509
8597
|
isOverdue: true,
|
|
8510
8598
|
limit: 0
|
|
@@ -8532,7 +8620,8 @@ async function handleGetPracticeDashboard(api) {
|
|
|
8532
8620
|
}),
|
|
8533
8621
|
api.getUnbilledTimeClients({ limit: 50 }),
|
|
8534
8622
|
api.getEngagementPipelineSummary({})
|
|
8535
|
-
]);
|
|
8623
|
+
])]);
|
|
8624
|
+
const [overdue, dueNext7, blocked, openDocRequests, pendingForms, unassignedSteps, unbilled, pipeline] = settled;
|
|
8536
8625
|
const rows = [];
|
|
8537
8626
|
const failures = [];
|
|
8538
8627
|
const count = (r, label, hint) => {
|
|
@@ -8565,7 +8654,7 @@ async function handleGetPracticeDashboard(api) {
|
|
|
8565
8654
|
const openValue = (p.sentTotalValue ?? 0) + (p.viewedTotalValue ?? 0);
|
|
8566
8655
|
rows.push({
|
|
8567
8656
|
label: "Proposals awaiting a decision",
|
|
8568
|
-
value: openCount === 0 ? "0" : `${openCount} worth
|
|
8657
|
+
value: openCount === 0 ? "0" : `${openCount} worth ${fmtWholeCurrency(openValue, practice)} — get_engagement_pipeline_summary`
|
|
8569
8658
|
});
|
|
8570
8659
|
} else failures.push("Proposal pipeline");
|
|
8571
8660
|
const lines = [
|
|
@@ -8749,10 +8838,10 @@ async function handleAddClientService(api, args) {
|
|
|
8749
8838
|
...rest.autoInvoice !== void 0 ? { autoInvoice: rest.autoInvoice } : {},
|
|
8750
8839
|
nextBillingDate: rest.nextBillingDate ?? null
|
|
8751
8840
|
};
|
|
8752
|
-
const created = await api.createClientService(clientCode, body);
|
|
8841
|
+
const [practice, created] = await Promise.all([api.getPracticeLocale(), api.createClientService(clientCode, body)]);
|
|
8753
8842
|
const lines = [
|
|
8754
8843
|
`Service assigned: ${created.billableService?.name?.trim() || args.billableServiceCode} on client ${clientCode} (${created.code ?? "no code"})`,
|
|
8755
|
-
`Status: ${created.status} · ${created.billingFrequency} · effective price
|
|
8844
|
+
`Status: ${created.status} · ${created.billingFrequency} · effective price ${money(created.effectivePrice ?? created.price ?? 0, practice)}`,
|
|
8756
8845
|
`Start: ${created.startDate?.slice(0, 10)}${created.endDate ? ` · end ${created.endDate.slice(0, 10)}` : " · ongoing"}`
|
|
8757
8846
|
];
|
|
8758
8847
|
if (created.managedByUser?.name) lines.push(`Managed by: ${created.managedByUser.name}`);
|
|
@@ -8963,7 +9052,7 @@ const CreateProposalInputSchema = {
|
|
|
8963
9052
|
};
|
|
8964
9053
|
async function handleCreateProposal(api, args) {
|
|
8965
9054
|
try {
|
|
8966
|
-
const created = await api.createEngagement({
|
|
9055
|
+
const [practice, created] = await Promise.all([api.getPracticeLocale(), api.createEngagement({
|
|
8967
9056
|
clientCode: args.clientCode,
|
|
8968
9057
|
date: args.date,
|
|
8969
9058
|
type: args.type ?? "ProposalAndEngagementLetter",
|
|
@@ -8973,8 +9062,8 @@ async function handleCreateProposal(api, args) {
|
|
|
8973
9062
|
clientBillableServiceCodes: args.clientBillableServiceCodes ?? null,
|
|
8974
9063
|
proposalTemplateCode: args.proposalTemplateCode ?? null,
|
|
8975
9064
|
lofETemplateCode: args.lofETemplateCode ?? null
|
|
8976
|
-
});
|
|
8977
|
-
const value = created.totalValue != null ?
|
|
9065
|
+
})]);
|
|
9066
|
+
const value = created.totalValue != null ? money(created.totalValue, practice) : void 0;
|
|
8978
9067
|
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
9068
|
const services = created.proposalServices ?? [];
|
|
8980
9069
|
if (services.length > 0) lines.push(`Services included: ${services.map((s) => s.billableService?.name ?? s.code ?? "?").join(", ")}`);
|
|
@@ -9178,6 +9267,9 @@ const CreateSavedTaskFilterInputSchema = {
|
|
|
9178
9267
|
isUnassigned: z.boolean().optional().describe("Include unassigned tasks (OR-combined with userCodes)."),
|
|
9179
9268
|
userCodes: z.array(z.string()).optional().describe("Filter to these users' tasks. For 'my ...' views use the current user's code."),
|
|
9180
9269
|
teamCodes: z.array(z.string()).optional().describe("Filter to these teams (codes from list_teams)."),
|
|
9270
|
+
clientManagerCodes: z.array(z.string()).optional().describe("Filter to tasks whose primary client has one of these users as its client manager (user codes from list_users). Clientless tasks never match. Use for 'tasks for the clients I manage' style views."),
|
|
9271
|
+
clientPartnerCodes: z.array(z.string()).optional().describe("Filter to tasks whose primary client has one of these users as its partner (user codes from list_users). Clientless tasks never match."),
|
|
9272
|
+
clientAssociateCodes: z.array(z.string()).optional().describe("Filter to tasks whose primary client has one of these users as its associate (user codes from list_users). Only meaningful if the practice uses the associate role. Clientless tasks never match."),
|
|
9181
9273
|
clientCodes: z.array(z.string()).optional().describe("Filter to these clients."),
|
|
9182
9274
|
categoryCodes: z.array(z.string()).optional().describe("Filter to these task categories (codes from list_task_categories)."),
|
|
9183
9275
|
recurringTaskCodes: z.array(z.string()).optional().describe("Filter to tasks generated by these recurring templates (codes from list_recurring_tasks)."),
|
|
@@ -9375,7 +9467,7 @@ async function buildServer(config) {
|
|
|
9375
9467
|
}, (args) => handleListClientNotes(api, args));
|
|
9376
9468
|
registerTenantTool(server, api, "list_tasks", {
|
|
9377
9469
|
title: "List / filter tasks across the practice",
|
|
9378
|
-
description: "List tasks with any combination of filters: assigned user(s), client(s), status, overdue flag, preset date range (Today / ThisWeek / Next7Days / CustomDateRange etc), category, team, recurring task template, saved filter, include-projected, include-workflow-steps (Agenda Mode), sort, and pagination. Use for: 'my tasks' (pass current user's code from startup context), 'Jane's overdue tasks' (user + isOverdue), 'tasks for ACME due this week' (client + dateRange=Next7Days + dateBasis=DueDate), 'what is the team working on this month' (dateRange=ThisMonth, no user filter). Returns up to 50 tasks per page. IMPORTANT constraints to avoid API errors and keep queries efficient: (1) Querying NotStarted tasks requires one of — a dateRange, isOverdue=true, or restricting status to non-NotStarted values. (2) Prefer the narrowest date range that answers the question — broad ranges (quarterly/yearly) are expensive; prefer Today / ThisWeek / Next7Days / ThisMonth over larger windows unless explicitly asked. (3) For 'oldest incomplete tasks' prefer status=['InProgress','Blocked'] with sortBy=StartDate (no date range needed), or add isOverdue=true if 'oldest overdue' is meant. (4) For 'how many X?' questions, pass limit=0 to get just the total count without fetching any task data — much cheaper than fetching a full page and counting.",
|
|
9470
|
+
description: "List tasks with any combination of filters: assigned user(s), unassigned, client(s), the primary client's client manager / partner (portfolio views: 'tasks across the clients Jane manages'), status, overdue flag, preset date range (Today / ThisWeek / Next7Days / CustomDateRange etc), category, team, billable service, workflow, workflow step type, current step name, recurring task template, free-text search, saved filter, include-projected, include-workflow-steps (Agenda Mode), sort, and pagination. Use for: 'my tasks' (pass current user's code from startup context), 'Jane's overdue tasks' (user + isOverdue), 'tasks for ACME due this week' (client + dateRange=Next7Days + dateBasis=DueDate), 'what is the team working on this month' (dateRange=ThisMonth, no user filter). Returns up to 50 tasks per page. IMPORTANT constraints to avoid API errors and keep queries efficient: (1) Querying NotStarted tasks requires one of — a dateRange, isOverdue=true, or restricting status to non-NotStarted values. (2) Prefer the narrowest date range that answers the question — broad ranges (quarterly/yearly) are expensive; prefer Today / ThisWeek / Next7Days / ThisMonth over larger windows unless explicitly asked. (3) For 'oldest incomplete tasks' prefer status=['InProgress','Blocked'] with sortBy=StartDate (no date range needed), or add isOverdue=true if 'oldest overdue' is meant. (4) For 'how many X?' questions, pass limit=0 to get just the total count without fetching any task data — much cheaper than fetching a full page and counting.",
|
|
9379
9471
|
inputSchema: ListTasksInputSchema,
|
|
9380
9472
|
annotations: {
|
|
9381
9473
|
readOnlyHint: true,
|