@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 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-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-Clf8G25g.mjs";
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 [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}`);
@@ -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 ? ` @ £${e.hourlyRate}/h` : "";
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 default rate, then the practice default. Only pass when the user names a rate.")
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 ? ` @ £${entry.hourlyRate}/h` : ""}` : "no"}`);
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 = `£${lineValue(i).toFixed(2)}${i.vatRate ? ` (VAT: ${i.vatRate})` : ""}`;
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 (£${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:`,
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 = `£${(s.totalAmount ?? 0).toFixed(2)}`;
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 (£${pageValue.toFixed(2)} on this page):` : `${items.length} invoice submission${items.length === 1 ? "" : "s"}, £${pageValue.toFixed(2)} total:`,
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 £${(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)`;
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 [overdue, dueNext7, blocked, openDocRequests, pendingForms, unassignedSteps, unbilled, pipeline] = await Promise.allSettled([
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 £${openValue.toLocaleString("en-GB", { maximumFractionDigits: 0 })} — get_engagement_pipeline_summary`
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 £${(created.effectivePrice ?? created.price ?? 0).toFixed(2)}`,
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 ? `£${created.totalValue.toLocaleString("en-GB", { minimumFractionDigits: 2 })}` : void 0;
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,
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.5330",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {