@sodiumhq/mcp-pm 0.1.0-beta.2590 → 0.1.0-beta.2593

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.
Files changed (2) hide show
  1. package/dist/index.js +376 -9
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -622,6 +622,20 @@ const client = createClient(createConfig());
622
622
  //#endregion
623
623
  //#region ../mcp-core/src/generated/sdk.gen.ts
624
624
  /**
625
+ * Get BusinessDetails for a client
626
+ */
627
+ const getBusinessDetailsForClient = (options) => (options.client ?? client).get({
628
+ security: [{
629
+ name: "x-api-key",
630
+ type: "apiKey"
631
+ }, {
632
+ scheme: "bearer",
633
+ type: "http"
634
+ }],
635
+ url: "/tenants/{tenant}/clients/{client}/businessdetails",
636
+ ...options
637
+ });
638
+ /**
625
639
  * List Contacts for Client
626
640
  *
627
641
  * Lists all Contacts for the specified client.
@@ -638,6 +652,22 @@ const listClientContactsForClient = (options) => (options.client ?? client).get(
638
652
  ...options
639
653
  });
640
654
  /**
655
+ * Get Client Dates
656
+ *
657
+ * Returns all key dates for the specified client
658
+ */
659
+ const getClientDates = (options) => (options.client ?? client).get({
660
+ security: [{
661
+ name: "x-api-key",
662
+ type: "apiKey"
663
+ }, {
664
+ scheme: "bearer",
665
+ type: "http"
666
+ }],
667
+ url: "/tenants/{tenant}/clients/{code}/dates",
668
+ ...options
669
+ });
670
+ /**
641
671
  * List Client Services for Client
642
672
  *
643
673
  * Lists all Client Services for the specified client.
@@ -705,6 +735,60 @@ const getPracticeDetails = (options) => (options.client ?? client).get({
705
735
  ...options
706
736
  });
707
737
  /**
738
+ * List Notes for Task
739
+ *
740
+ * Lists notes for the specified task. By default only returns task-level notes. Set includeStepNotes=true to also include notes attached to workflow steps.
741
+ */
742
+ const listTaskItemNotes = (options) => (options.client ?? client).get({
743
+ security: [{
744
+ name: "x-api-key",
745
+ type: "apiKey"
746
+ }, {
747
+ scheme: "bearer",
748
+ type: "http"
749
+ }],
750
+ url: "/tenants/{tenant}/tasks/{taskCode}/taskitemnote",
751
+ ...options
752
+ });
753
+ /**
754
+ * Get Task Workflow Groups
755
+ *
756
+ * Retrieves comprehensive workflow progress information for a TaskItem.
757
+ *
758
+ * Returns detailed progress data including:
759
+ * - Overall completion statistics (total steps, completed steps, percentage)
760
+ * - All workflow steps with current status and assignments
761
+ * - Available steps that can be worked on next
762
+ * - Steps grouped by workflow groups for organized display
763
+ *
764
+ * Use this endpoint to:
765
+ * - Display workflow progress dashboards
766
+ * - Show step-by-step task execution status
767
+ * - Identify bottlenecks and available work
768
+ * - Track overall workflow completion
769
+ *
770
+ * Example Response:
771
+ * {
772
+ * "totalSteps": 8,
773
+ * "completedSteps": 3,
774
+ * "progressPercentage": 37.5,
775
+ * "allSteps": [...],
776
+ * "availableSteps": [...],
777
+ * "groupedSteps": [...]
778
+ * }
779
+ */
780
+ const getTaskWorkflowGroups = (options) => (options.client ?? client).get({
781
+ security: [{
782
+ name: "x-api-key",
783
+ type: "apiKey"
784
+ }, {
785
+ scheme: "bearer",
786
+ type: "http"
787
+ }],
788
+ url: "/tenants/{tenant}/tasks/{taskCode}/workflow/groups",
789
+ ...options
790
+ });
791
+ /**
708
792
  * List TaskItems
709
793
  *
710
794
  * Lists TaskItems for the given tenant.
@@ -744,6 +828,22 @@ const listTaskItems = (options) => (options.client ?? client).get({
744
828
  ...options
745
829
  });
746
830
  /**
831
+ * Get TaskItem
832
+ *
833
+ * Gets a TaskItem for the specified tenant.
834
+ */
835
+ const getTaskItem = (options) => (options.client ?? client).get({
836
+ security: [{
837
+ name: "x-api-key",
838
+ type: "apiKey"
839
+ }, {
840
+ scheme: "bearer",
841
+ type: "http"
842
+ }],
843
+ url: "/tenants/{tenant}/tasks/{code}",
844
+ ...options
845
+ });
846
+ /**
747
847
  * Get Tenant
748
848
  *
749
849
  * Retrieves the details of a tenant using its Code identifier.
@@ -891,6 +991,30 @@ var SodiumApiClient = class {
891
991
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `list services for client ${clientCode}`);
892
992
  return data;
893
993
  }
994
+ async getClientBusinessDetails(clientCode) {
995
+ const correlationId = randomUUID();
996
+ const { data, error, response } = await getBusinessDetailsForClient({
997
+ path: {
998
+ tenant: this.ctx.tenant,
999
+ client: clientCode
1000
+ },
1001
+ headers: { "X-Correlation-Id": correlationId }
1002
+ });
1003
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get business details for client ${clientCode}`);
1004
+ return data;
1005
+ }
1006
+ async getClientDates(clientCode) {
1007
+ const correlationId = randomUUID();
1008
+ const { data, error, response } = await getClientDates({
1009
+ path: {
1010
+ tenant: this.ctx.tenant,
1011
+ code: clientCode
1012
+ },
1013
+ headers: { "X-Correlation-Id": correlationId }
1014
+ });
1015
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get client dates for ${clientCode}`);
1016
+ return data;
1017
+ }
894
1018
  async listUsers(query = {}) {
895
1019
  const correlationId = randomUUID();
896
1020
  const { data, error, response } = await listTenantUsers({
@@ -919,6 +1043,47 @@ var SodiumApiClient = class {
919
1043
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list tasks");
920
1044
  return data;
921
1045
  }
1046
+ async getTask(code) {
1047
+ const correlationId = randomUUID();
1048
+ const { data, error, response } = await getTaskItem({
1049
+ path: {
1050
+ tenant: this.ctx.tenant,
1051
+ code
1052
+ },
1053
+ headers: { "X-Correlation-Id": correlationId }
1054
+ });
1055
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get task ${code}`);
1056
+ return data;
1057
+ }
1058
+ async listTaskNotes(taskCode, options) {
1059
+ const correlationId = randomUUID();
1060
+ const { data, error, response } = await listTaskItemNotes({
1061
+ path: {
1062
+ tenant: this.ctx.tenant,
1063
+ taskCode
1064
+ },
1065
+ query: {
1066
+ limit: options?.limit ?? 50,
1067
+ offset: options?.offset ?? 0,
1068
+ includeStepNotes: options?.includeStepNotes ?? false
1069
+ },
1070
+ headers: { "X-Correlation-Id": correlationId }
1071
+ });
1072
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `list notes for task ${taskCode}`);
1073
+ return data;
1074
+ }
1075
+ async getTaskWorkflowGroups(taskCode) {
1076
+ const correlationId = randomUUID();
1077
+ const { data, error, response } = await getTaskWorkflowGroups({
1078
+ path: {
1079
+ tenant: this.ctx.tenant,
1080
+ taskCode
1081
+ },
1082
+ headers: { "X-Correlation-Id": correlationId }
1083
+ });
1084
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get workflow groups for task ${taskCode}`);
1085
+ return data;
1086
+ }
922
1087
  toError(response, error, correlationId, operation) {
923
1088
  const status = response.status;
924
1089
  let message = `Failed to ${operation} (HTTP ${status})`;
@@ -972,7 +1137,7 @@ async function buildInstructions(api) {
972
1137
  }
973
1138
  //#endregion
974
1139
  //#region ../mcp-core/src/tools/get-practice-details.ts
975
- function format$1(tenant, practice) {
1140
+ function format$2(tenant, practice) {
976
1141
  const lines = [];
977
1142
  lines.push(`Practice: ${practice.name}`);
978
1143
  lines.push(`Tenant: ${tenant.name} (${tenant.code})`);
@@ -1006,7 +1171,7 @@ async function handleGetPracticeDetails(api) {
1006
1171
  const [tenant, practice] = await Promise.all([api.getTenantDetails(), api.getPracticeDetails()]);
1007
1172
  return { content: [{
1008
1173
  type: "text",
1009
- text: format$1(tenant, practice)
1174
+ text: format$2(tenant, practice)
1010
1175
  }] };
1011
1176
  } catch (error) {
1012
1177
  return {
@@ -1117,10 +1282,12 @@ function describeFilters$2(args) {
1117
1282
  //#region ../mcp-core/src/tools/get-client-summary.ts
1118
1283
  const GetClientSummaryInputSchema = { code: z.string().min(1, "Client code is required").describe("The client code (identifier). Usually discovered via list_clients first.") };
1119
1284
  async function handleGetClientSummary(api, { code }) {
1120
- const [clientResult, contactsResult, servicesResult, overdueResult, upcomingResult] = await Promise.allSettled([
1285
+ const [clientResult, contactsResult, servicesResult, businessResult, datesResult, overdueResult, upcomingResult] = await Promise.allSettled([
1121
1286
  api.getClient(code),
1122
1287
  api.listClientContacts(code),
1123
1288
  api.listClientServices(code),
1289
+ api.getClientBusinessDetails(code),
1290
+ api.getClientDates(code),
1124
1291
  api.listTasks({
1125
1292
  client: [code],
1126
1293
  isOverdue: true,
@@ -1145,15 +1312,19 @@ async function handleGetClientSummary(api, { code }) {
1145
1312
  }
1146
1313
  return { content: [{
1147
1314
  type: "text",
1148
- text: format({
1315
+ text: format$1({
1149
1316
  client: clientResult.value,
1150
1317
  contacts: extract(contactsResult),
1151
1318
  services: extract(servicesResult),
1319
+ businessDetails: businessResult.status === "fulfilled" ? businessResult.value : null,
1320
+ clientDates: datesResult.status === "fulfilled" ? datesResult.value : [],
1152
1321
  overdueTasks: extract(overdueResult),
1153
1322
  upcomingTasks: extract(upcomingResult),
1154
1323
  gaps: [
1155
1324
  contactsResult.status === "rejected" ? "contacts" : null,
1156
1325
  servicesResult.status === "rejected" ? "services" : null,
1326
+ businessResult.status === "rejected" ? "business details" : null,
1327
+ datesResult.status === "rejected" ? "key dates" : null,
1157
1328
  overdueResult.status === "rejected" ? "overdue tasks" : null,
1158
1329
  upcomingResult.status === "rejected" ? "upcoming tasks" : null
1159
1330
  ].filter((v) => v !== null)
@@ -1164,8 +1335,8 @@ function extract(result) {
1164
1335
  if (result.status !== "fulfilled") return [];
1165
1336
  return result.value.data ?? [];
1166
1337
  }
1167
- function format(input) {
1168
- const { client, contacts, services, overdueTasks, upcomingTasks, gaps } = input;
1338
+ function format$1(input) {
1339
+ const { client, contacts, services, businessDetails, clientDates, overdueTasks, upcomingTasks, gaps } = input;
1169
1340
  const lines = [];
1170
1341
  const name = client.name ?? "(no name)";
1171
1342
  const code = client.code ?? "(no code)";
@@ -1178,6 +1349,15 @@ function format(input) {
1178
1349
  if (client.manager) lines.push(`Manager: ${client.manager.name} (${client.manager.code})`);
1179
1350
  if (client.partner) lines.push(`Partner: ${client.partner.name} (${client.partner.code})`);
1180
1351
  if (client.associate) lines.push(`Associate: ${client.associate.name} (${client.associate.code})`);
1352
+ const businessLines = formatBusinessDetails(businessDetails);
1353
+ if (businessLines.length > 0) lines.push("", "--- Business Details ---", ...businessLines);
1354
+ if (clientDates.length > 0) {
1355
+ lines.push("", `--- Key Dates (${clientDates.length}) ---`);
1356
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
1357
+ const upcoming = clientDates.filter((d) => (d.date ?? "") >= today).sort((a, b) => (a.date ?? "").localeCompare(b.date ?? ""));
1358
+ const past = clientDates.filter((d) => (d.date ?? "") < today).sort((a, b) => (b.date ?? "").localeCompare(a.date ?? ""));
1359
+ for (const d of [...upcoming, ...past]) lines.push(formatClientDate(d));
1360
+ }
1181
1361
  lines.push("", `--- Contacts (${contacts.length}) ---`);
1182
1362
  if (contacts.length === 0) lines.push("No contacts.");
1183
1363
  else for (const c of contacts) {
@@ -1214,7 +1394,184 @@ function format(input) {
1214
1394
  }
1215
1395
  function formatTask$1(t) {
1216
1396
  const taskCode = t.code ?? "(no code)";
1217
- return `${t.name ?? "(unnamed)"} (${taskCode})${t.dueDate ? ` — due ${t.dueDate}` : ""}${t.assignedUser?.name ? ` — ${t.assignedUser.name}` : ""}`;
1397
+ return `${t.name ?? "(unnamed)"} (${taskCode})${t.dueDate ? ` — due ${t.dueDate}` : ""}${t.assignedUser?.name ? ` — ${t.assignedUser.name}` : ""}${t.workflow ? ` — workflow ${t.workflowStepsComplete ?? 0}/${t.workflowSteps ?? 0}` : ""}`;
1398
+ }
1399
+ function formatBusinessDetails(bd) {
1400
+ if (!bd) return [];
1401
+ const out = [];
1402
+ const company = bd.company;
1403
+ if (company?.number) {
1404
+ const status = company.status ? ` · ${company.status}` : "";
1405
+ out.push(`Company number: ${company.number}${status}`);
1406
+ }
1407
+ if (company?.incorporationDate) out.push(`Incorporated: ${company.incorporationDate}`);
1408
+ if (bd.tradingAs) out.push(`Trading as: ${bd.tradingAs}`);
1409
+ if (company?.registeredAddress) out.push(`Registered address: ${company.registeredAddress}`);
1410
+ if (bd.postalAddress && bd.postalAddress !== company?.registeredAddress) out.push(`Postal address: ${bd.postalAddress}`);
1411
+ if (bd.natureOfBusiness) out.push(`Nature of business: ${bd.natureOfBusiness}`);
1412
+ if (bd.vat) if (bd.vat.registered) out.push(`VAT: registered${bd.vat.number ? ` (${bd.vat.number})` : ""}`);
1413
+ else out.push("VAT: not registered");
1414
+ if (bd.utr) out.push(`UTR: ${bd.utr}`);
1415
+ if (bd.payeRef) out.push(`PAYE ref: ${bd.payeRef}`);
1416
+ if (bd.accountsOfficeRef) out.push(`Accounts office ref: ${bd.accountsOfficeRef}`);
1417
+ return out;
1418
+ }
1419
+ function formatClientDate(d) {
1420
+ return `- ${humanizeDateType(d.dateType ?? "")}: ${d.date ?? "(no date)"}${d.description ? ` — ${d.description}` : ""}`;
1421
+ }
1422
+ function humanizeDateType(type) {
1423
+ if (!type) return "(unknown)";
1424
+ const words = type.replace(/([a-z])([A-Z])/g, "$1 $2").replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2").split(" ");
1425
+ const acronyms = new Set([
1426
+ "VAT",
1427
+ "PAYE",
1428
+ "CIS",
1429
+ "UTR"
1430
+ ]);
1431
+ return words.map((w, i) => {
1432
+ const upper = w.toUpperCase();
1433
+ if (acronyms.has(upper)) return upper;
1434
+ return i === 0 ? w.charAt(0).toUpperCase() + w.slice(1).toLowerCase() : w.toLowerCase();
1435
+ }).join(" ");
1436
+ }
1437
+ //#endregion
1438
+ //#region ../mcp-core/src/tools/get-task-context.ts
1439
+ const GetTaskContextInputSchema = { code: z.string().min(1, "Task code is required").describe("The task code (identifier). Usually discovered via list_tasks first, or supplied directly by the user when they quote a task code.") };
1440
+ async function handleGetTaskContext(api, { code }) {
1441
+ const [taskResult, notesResult, workflowResult] = await Promise.allSettled([
1442
+ api.getTask(code),
1443
+ api.listTaskNotes(code, {
1444
+ includeStepNotes: false,
1445
+ limit: 50
1446
+ }),
1447
+ api.getTaskWorkflowGroups(code)
1448
+ ]);
1449
+ if (taskResult.status === "rejected") {
1450
+ const err = taskResult.reason;
1451
+ return {
1452
+ content: [{
1453
+ type: "text",
1454
+ text: err instanceof SodiumApiError ? `Error getting task: ${err.message} (correlation: ${err.correlationId})` : `Error getting task: ${err instanceof Error ? err.message : String(err)}`
1455
+ }],
1456
+ isError: true
1457
+ };
1458
+ }
1459
+ const task = taskResult.value;
1460
+ const notes = notesResult.status === "fulfilled" ? notesResult.value.data ?? [] : [];
1461
+ const workflowGroups = workflowResult.status === "fulfilled" ? workflowResult.value : [];
1462
+ const gaps = [];
1463
+ if (notesResult.status === "rejected") gaps.push("notes");
1464
+ if (workflowResult.status === "rejected" && task.workflow) gaps.push("workflow steps");
1465
+ return { content: [{
1466
+ type: "text",
1467
+ text: format({
1468
+ task,
1469
+ notes,
1470
+ workflowGroups,
1471
+ gaps
1472
+ })
1473
+ }] };
1474
+ }
1475
+ function format(input) {
1476
+ const { task, notes, workflowGroups, gaps } = input;
1477
+ const lines = [];
1478
+ const name = task.name ?? "(no name)";
1479
+ const code = task.code ?? "(no code)";
1480
+ lines.push(`Task: ${name} (${code})`);
1481
+ const statusBits = [];
1482
+ if (task.status) statusBits.push(task.status);
1483
+ if (task.isOverdue) statusBits.push("OVERDUE");
1484
+ if (task.isProjected) statusBits.push("projected");
1485
+ if (statusBits.length > 0) lines.push(`Status: ${statusBits.join(" · ")}`);
1486
+ const dateBits = [];
1487
+ if (task.startDate) dateBits.push(`start ${task.startDate}`);
1488
+ if (task.dueDate) dateBits.push(`due ${task.dueDate}`);
1489
+ if (task.statutoryDueDate) dateBits.push(`statutory ${task.statutoryDueDate}`);
1490
+ if (dateBits.length > 0) lines.push(`Dates: ${dateBits.join(" · ")}`);
1491
+ if (task.timeEstimate !== void 0 && task.timeEstimate !== null) lines.push(`Time estimate: ${task.timeEstimate}h`);
1492
+ if (task.assignedUser) lines.push(`Assigned user: ${formatCodeAndName(task.assignedUser)}`);
1493
+ if (task.assignedTeam) lines.push(`Assigned team: ${formatCodeAndName(task.assignedTeam)}`);
1494
+ if (task.category) lines.push(`Category: ${formatCodeAndName(task.category)}`);
1495
+ if (task.recurringTask) lines.push(`Recurring template: ${formatCodeAndName(task.recurringTask)}`);
1496
+ if (task.workflow) lines.push(`Workflow template: ${formatCodeAndName(task.workflow)}`);
1497
+ const description = task.description?.trim();
1498
+ if (description) lines.push("", "--- Description ---", description);
1499
+ const checklist = task.checklist ?? [];
1500
+ if (checklist.length > 0) {
1501
+ lines.push("", `--- Checklist (${countCompleted(checklist)}/${checklist.length}) ---`);
1502
+ const ordered = [...checklist].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
1503
+ for (const item of ordered) {
1504
+ const mark = item.isCompleted ? "[x]" : "[ ]";
1505
+ lines.push(`${mark} ${item.text ?? ""}`);
1506
+ }
1507
+ }
1508
+ if (workflowGroups.length > 0) {
1509
+ const totalSteps = task.workflowSteps ?? sumBy(workflowGroups, (g) => g.totalSteps ?? 0);
1510
+ const doneSteps = task.workflowStepsComplete ?? sumBy(workflowGroups, (g) => g.completedSteps ?? 0);
1511
+ lines.push("", `--- Workflow progress (${doneSteps}/${totalSteps} steps complete) ---`);
1512
+ for (const group of workflowGroups) {
1513
+ const gName = group.groupName ?? `Group ${group.groupNumber ?? "?"}`;
1514
+ const gTotal = group.totalSteps ?? 0;
1515
+ const gDone = group.completedSteps ?? 0;
1516
+ const deadline = group.groupDeadline ? ` — deadline ${group.groupDeadline}` : "";
1517
+ lines.push(`Group: ${gName} (${gDone}/${gTotal})${deadline}`);
1518
+ for (const step of group.steps ?? []) lines.push(` ${formatStep(step)}`);
1519
+ }
1520
+ }
1521
+ if (notes.length > 0) {
1522
+ const sorted = [...notes].sort((a, b) => {
1523
+ const pa = a.pinnedLevel ?? 0;
1524
+ const pb = b.pinnedLevel ?? 0;
1525
+ if (pa !== pb) return pb - pa;
1526
+ const da = a.date ?? a.createdDate ?? "";
1527
+ return (b.date ?? b.createdDate ?? "").localeCompare(da);
1528
+ });
1529
+ lines.push("", `--- Notes (${notes.length}) ---`);
1530
+ for (const note of sorted.slice(0, 20)) lines.push(formatNote(note));
1531
+ if (notes.length > 20) lines.push(`... and ${notes.length - 20} more notes`);
1532
+ }
1533
+ const clients = task.clients ?? [];
1534
+ const primaryCode = task.primaryClient?.code;
1535
+ const secondary = clients.filter((c) => c.code !== primaryCode);
1536
+ if (task.primaryClient || clients.length > 0) {
1537
+ lines.push("", "--- Parent client ---");
1538
+ if (task.primaryClient) lines.push(`Primary: ${formatCodeAndName(task.primaryClient)}`);
1539
+ if (secondary.length > 0) lines.push(`Also linked: ${secondary.map(formatCodeAndName).join(", ")}`);
1540
+ }
1541
+ if (gaps.length > 0) lines.push("", `Note: could not load ${gaps.join(", ")} (partial failure). Retry for a complete picture.`);
1542
+ return lines.join("\n");
1543
+ }
1544
+ function formatStep(step) {
1545
+ const num = step.stepNumber ?? "?";
1546
+ const name = step.stepName ?? "(unnamed)";
1547
+ const bits = [];
1548
+ if (step.status) bits.push(step.status);
1549
+ if (step.assignedUser?.name) bits.push(step.assignedUser.name);
1550
+ else if (step.assignedTeam?.name) bits.push(`team ${step.assignedTeam.name}`);
1551
+ if (step.completedDate) bits.push(`done ${step.completedDate}`);
1552
+ if (step.blockedReason) bits.push(`blocked: ${step.blockedReason}`);
1553
+ return `Step ${num}: ${name}${bits.length > 0 ? ` — ${bits.join(" · ")}` : ""}`;
1554
+ }
1555
+ function formatNote(note) {
1556
+ const pinPrefix = (note.pinnedLevel ?? 0) > 0 ? "[pinned] " : "";
1557
+ const dateStr = note.date ?? note.createdDate ?? "";
1558
+ const author = note.noteFromUser?.name ?? "(unknown)";
1559
+ const stepCtx = note.stepName ? ` [step: ${note.stepName}]` : "";
1560
+ const text = (note.text ?? "").replace(/\s+/g, " ").trim();
1561
+ return `${pinPrefix}${dateStr} — ${author}${stepCtx}: ${text.length > 200 ? `${text.slice(0, 200)}...` : text}`;
1562
+ }
1563
+ function formatCodeAndName(v) {
1564
+ if (!v) return "(none)";
1565
+ const name = v.name ?? "";
1566
+ const code = v.code ?? "";
1567
+ if (name && code) return `${name} (${code})`;
1568
+ return name || code || "(none)";
1569
+ }
1570
+ function countCompleted(items) {
1571
+ return items.filter((i) => i.isCompleted).length;
1572
+ }
1573
+ function sumBy(arr, fn) {
1574
+ return arr.reduce((acc, cur) => acc + fn(cur), 0);
1218
1575
  }
1219
1576
  //#endregion
1220
1577
  //#region ../mcp-core/src/tools/list-tasks.ts
@@ -1280,7 +1637,7 @@ const ListTasksInputSchema = {
1280
1637
  };
1281
1638
  function formatTask(t) {
1282
1639
  const code = t.code ?? "(no code)";
1283
- return `- ${t.name ?? "(unnamed)"} (${code})${t.isOverdue ? " [OVERDUE]" : ""}${t.dueDate ? ` · due ${t.dueDate}` : ""}${t.primaryClient ? ` · ${t.primaryClient.name}` : t.clients?.[0] ? ` · ${t.clients[0].name}` : ""}${t.assignedUser?.name ? ` · ${t.assignedUser.name}` : ""}${t.status && t.status !== "NotStarted" ? ` · ${t.status}` : ""}`;
1640
+ return `- ${t.name ?? "(unnamed)"} (${code})${t.isOverdue ? " [OVERDUE]" : ""}${t.dueDate ? ` · due ${t.dueDate}` : ""}${t.primaryClient ? ` · ${t.primaryClient.name}` : t.clients?.[0] ? ` · ${t.clients[0].name}` : ""}${t.assignedUser?.name ? ` · ${t.assignedUser.name}` : ""}${t.status && t.status !== "NotStarted" ? ` · ${t.status}` : ""}${t.workflow ? ` · workflow ${t.workflowStepsComplete ?? 0}/${t.workflowSteps ?? 0}` : ""}`;
1284
1641
  }
1285
1642
  async function handleListTasks(api, args) {
1286
1643
  try {
@@ -1466,7 +1823,7 @@ async function buildServer(config) {
1466
1823
  }, (args) => handleListClients(api, args));
1467
1824
  server.registerTool("get_client_summary", {
1468
1825
  title: "Get a full summary of one client",
1469
- description: "Get a consolidated overview of a single client by code: identity (name, status, type, assignments), all contacts, active services with pricing, overdue task count + top 5, and tasks due in the next 7 days. Use this AFTER list_clients identifies the client of interest, or when the user references a specific client by code. Tolerates partial failures — if one section can't be loaded, the rest is still returned with a note about what's missing.",
1826
+ description: "Get a consolidated overview of a single client by code: identity (name, status, type, assignments), business details (company number, incorporation date, trading name, registered address, VAT status, UTR, PAYE ref), key statutory dates (year-end, accounts due, VAT return due, confirmation statement due, etc. — upcoming first, then past), all contacts, active services with pricing, overdue task count + top 5, and tasks due in the next 7 days. Use this AFTER list_clients identifies the client of interest, or when the user references a specific client by code. Also answers 'when is ACME's year-end?' / 'is ACME VAT registered?' in a single call. Tolerates partial failures — if one section can't be loaded, the rest is still returned with a note about what's missing.",
1470
1827
  inputSchema: GetClientSummaryInputSchema,
1471
1828
  annotations: {
1472
1829
  readOnlyHint: true,
@@ -1484,6 +1841,16 @@ async function buildServer(config) {
1484
1841
  openWorldHint: true
1485
1842
  }
1486
1843
  }, (args) => handleListTasks(api, args));
1844
+ server.registerTool("get_task_context", {
1845
+ title: "Get full context for a single task",
1846
+ description: "Get a consolidated view of one task by code: identity (name, code, status, overdue flag, start/due/statutory dates, time estimate, assignee), description, task-level checklist, workflow progress (groups and steps with status, assignee, and completion info — only if the task has a workflow attached), notes (pinned first, then newest first), and the parent client. Use this AFTER list_tasks identifies a task of interest, or when the user references a specific task by code. Tolerates partial failures — if notes or workflow steps can't be loaded, the task details are still returned with a note about what's missing. Only fails outright if the task itself can't be fetched (e.g. unknown code).",
1847
+ inputSchema: GetTaskContextInputSchema,
1848
+ annotations: {
1849
+ readOnlyHint: true,
1850
+ idempotentHint: true,
1851
+ openWorldHint: true
1852
+ }
1853
+ }, (args) => handleGetTaskContext(api, args));
1487
1854
  server.registerTool("list_users", {
1488
1855
  title: "List / search / filter tenant users",
1489
1856
  description: "Find tenant users by name, email, role, or status. Use this when the user mentioned in a request isn't present in the startup roster (large teams have more than the top 20 active members shown there), or when filtering is needed beyond name resolution. Typical queries: 'find Jane' (search), 'list all partners' (isPartner=true), 'who's been invited but not joined yet?' (status=Invited), 'how many active users do we have?' (status=Active, limit=0). For 'how many X?' questions, pass limit=0 to get just the total count without fetching any user data.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.2590",
3
+ "version": "0.1.0-beta.2593",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {