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

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 +266 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -705,6 +705,60 @@ const getPracticeDetails = (options) => (options.client ?? client).get({
705
705
  ...options
706
706
  });
707
707
  /**
708
+ * List Notes for Task
709
+ *
710
+ * Lists notes for the specified task. By default only returns task-level notes. Set includeStepNotes=true to also include notes attached to workflow steps.
711
+ */
712
+ const listTaskItemNotes = (options) => (options.client ?? client).get({
713
+ security: [{
714
+ name: "x-api-key",
715
+ type: "apiKey"
716
+ }, {
717
+ scheme: "bearer",
718
+ type: "http"
719
+ }],
720
+ url: "/tenants/{tenant}/tasks/{taskCode}/taskitemnote",
721
+ ...options
722
+ });
723
+ /**
724
+ * Get Task Workflow Groups
725
+ *
726
+ * Retrieves comprehensive workflow progress information for a TaskItem.
727
+ *
728
+ * Returns detailed progress data including:
729
+ * - Overall completion statistics (total steps, completed steps, percentage)
730
+ * - All workflow steps with current status and assignments
731
+ * - Available steps that can be worked on next
732
+ * - Steps grouped by workflow groups for organized display
733
+ *
734
+ * Use this endpoint to:
735
+ * - Display workflow progress dashboards
736
+ * - Show step-by-step task execution status
737
+ * - Identify bottlenecks and available work
738
+ * - Track overall workflow completion
739
+ *
740
+ * Example Response:
741
+ * {
742
+ * "totalSteps": 8,
743
+ * "completedSteps": 3,
744
+ * "progressPercentage": 37.5,
745
+ * "allSteps": [...],
746
+ * "availableSteps": [...],
747
+ * "groupedSteps": [...]
748
+ * }
749
+ */
750
+ const getTaskWorkflowGroups = (options) => (options.client ?? client).get({
751
+ security: [{
752
+ name: "x-api-key",
753
+ type: "apiKey"
754
+ }, {
755
+ scheme: "bearer",
756
+ type: "http"
757
+ }],
758
+ url: "/tenants/{tenant}/tasks/{taskCode}/workflow/groups",
759
+ ...options
760
+ });
761
+ /**
708
762
  * List TaskItems
709
763
  *
710
764
  * Lists TaskItems for the given tenant.
@@ -744,6 +798,22 @@ const listTaskItems = (options) => (options.client ?? client).get({
744
798
  ...options
745
799
  });
746
800
  /**
801
+ * Get TaskItem
802
+ *
803
+ * Gets a TaskItem for the specified tenant.
804
+ */
805
+ const getTaskItem = (options) => (options.client ?? client).get({
806
+ security: [{
807
+ name: "x-api-key",
808
+ type: "apiKey"
809
+ }, {
810
+ scheme: "bearer",
811
+ type: "http"
812
+ }],
813
+ url: "/tenants/{tenant}/tasks/{code}",
814
+ ...options
815
+ });
816
+ /**
747
817
  * Get Tenant
748
818
  *
749
819
  * Retrieves the details of a tenant using its Code identifier.
@@ -919,6 +989,47 @@ var SodiumApiClient = class {
919
989
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list tasks");
920
990
  return data;
921
991
  }
992
+ async getTask(code) {
993
+ const correlationId = randomUUID();
994
+ const { data, error, response } = await getTaskItem({
995
+ path: {
996
+ tenant: this.ctx.tenant,
997
+ code
998
+ },
999
+ headers: { "X-Correlation-Id": correlationId }
1000
+ });
1001
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get task ${code}`);
1002
+ return data;
1003
+ }
1004
+ async listTaskNotes(taskCode, options) {
1005
+ const correlationId = randomUUID();
1006
+ const { data, error, response } = await listTaskItemNotes({
1007
+ path: {
1008
+ tenant: this.ctx.tenant,
1009
+ taskCode
1010
+ },
1011
+ query: {
1012
+ limit: options?.limit ?? 50,
1013
+ offset: options?.offset ?? 0,
1014
+ includeStepNotes: options?.includeStepNotes ?? false
1015
+ },
1016
+ headers: { "X-Correlation-Id": correlationId }
1017
+ });
1018
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `list notes for task ${taskCode}`);
1019
+ return data;
1020
+ }
1021
+ async getTaskWorkflowGroups(taskCode) {
1022
+ const correlationId = randomUUID();
1023
+ const { data, error, response } = await getTaskWorkflowGroups({
1024
+ path: {
1025
+ tenant: this.ctx.tenant,
1026
+ taskCode
1027
+ },
1028
+ headers: { "X-Correlation-Id": correlationId }
1029
+ });
1030
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get workflow groups for task ${taskCode}`);
1031
+ return data;
1032
+ }
922
1033
  toError(response, error, correlationId, operation) {
923
1034
  const status = response.status;
924
1035
  let message = `Failed to ${operation} (HTTP ${status})`;
@@ -972,7 +1083,7 @@ async function buildInstructions(api) {
972
1083
  }
973
1084
  //#endregion
974
1085
  //#region ../mcp-core/src/tools/get-practice-details.ts
975
- function format$1(tenant, practice) {
1086
+ function format$2(tenant, practice) {
976
1087
  const lines = [];
977
1088
  lines.push(`Practice: ${practice.name}`);
978
1089
  lines.push(`Tenant: ${tenant.name} (${tenant.code})`);
@@ -1006,7 +1117,7 @@ async function handleGetPracticeDetails(api) {
1006
1117
  const [tenant, practice] = await Promise.all([api.getTenantDetails(), api.getPracticeDetails()]);
1007
1118
  return { content: [{
1008
1119
  type: "text",
1009
- text: format$1(tenant, practice)
1120
+ text: format$2(tenant, practice)
1010
1121
  }] };
1011
1122
  } catch (error) {
1012
1123
  return {
@@ -1145,7 +1256,7 @@ async function handleGetClientSummary(api, { code }) {
1145
1256
  }
1146
1257
  return { content: [{
1147
1258
  type: "text",
1148
- text: format({
1259
+ text: format$1({
1149
1260
  client: clientResult.value,
1150
1261
  contacts: extract(contactsResult),
1151
1262
  services: extract(servicesResult),
@@ -1164,7 +1275,7 @@ function extract(result) {
1164
1275
  if (result.status !== "fulfilled") return [];
1165
1276
  return result.value.data ?? [];
1166
1277
  }
1167
- function format(input) {
1278
+ function format$1(input) {
1168
1279
  const { client, contacts, services, overdueTasks, upcomingTasks, gaps } = input;
1169
1280
  const lines = [];
1170
1281
  const name = client.name ?? "(no name)";
@@ -1214,7 +1325,146 @@ function format(input) {
1214
1325
  }
1215
1326
  function formatTask$1(t) {
1216
1327
  const taskCode = t.code ?? "(no code)";
1217
- return `${t.name ?? "(unnamed)"} (${taskCode})${t.dueDate ? ` — due ${t.dueDate}` : ""}${t.assignedUser?.name ? ` — ${t.assignedUser.name}` : ""}`;
1328
+ 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}` : ""}`;
1329
+ }
1330
+ //#endregion
1331
+ //#region ../mcp-core/src/tools/get-task-context.ts
1332
+ 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.") };
1333
+ async function handleGetTaskContext(api, { code }) {
1334
+ const [taskResult, notesResult, workflowResult] = await Promise.allSettled([
1335
+ api.getTask(code),
1336
+ api.listTaskNotes(code, {
1337
+ includeStepNotes: false,
1338
+ limit: 50
1339
+ }),
1340
+ api.getTaskWorkflowGroups(code)
1341
+ ]);
1342
+ if (taskResult.status === "rejected") {
1343
+ const err = taskResult.reason;
1344
+ return {
1345
+ content: [{
1346
+ type: "text",
1347
+ text: err instanceof SodiumApiError ? `Error getting task: ${err.message} (correlation: ${err.correlationId})` : `Error getting task: ${err instanceof Error ? err.message : String(err)}`
1348
+ }],
1349
+ isError: true
1350
+ };
1351
+ }
1352
+ const task = taskResult.value;
1353
+ const notes = notesResult.status === "fulfilled" ? notesResult.value.data ?? [] : [];
1354
+ const workflowGroups = workflowResult.status === "fulfilled" ? workflowResult.value : [];
1355
+ const gaps = [];
1356
+ if (notesResult.status === "rejected") gaps.push("notes");
1357
+ if (workflowResult.status === "rejected" && task.workflow) gaps.push("workflow steps");
1358
+ return { content: [{
1359
+ type: "text",
1360
+ text: format({
1361
+ task,
1362
+ notes,
1363
+ workflowGroups,
1364
+ gaps
1365
+ })
1366
+ }] };
1367
+ }
1368
+ function format(input) {
1369
+ const { task, notes, workflowGroups, gaps } = input;
1370
+ const lines = [];
1371
+ const name = task.name ?? "(no name)";
1372
+ const code = task.code ?? "(no code)";
1373
+ lines.push(`Task: ${name} (${code})`);
1374
+ const statusBits = [];
1375
+ if (task.status) statusBits.push(task.status);
1376
+ if (task.isOverdue) statusBits.push("OVERDUE");
1377
+ if (task.isProjected) statusBits.push("projected");
1378
+ if (statusBits.length > 0) lines.push(`Status: ${statusBits.join(" · ")}`);
1379
+ const dateBits = [];
1380
+ if (task.startDate) dateBits.push(`start ${task.startDate}`);
1381
+ if (task.dueDate) dateBits.push(`due ${task.dueDate}`);
1382
+ if (task.statutoryDueDate) dateBits.push(`statutory ${task.statutoryDueDate}`);
1383
+ if (dateBits.length > 0) lines.push(`Dates: ${dateBits.join(" · ")}`);
1384
+ if (task.timeEstimate !== void 0 && task.timeEstimate !== null) lines.push(`Time estimate: ${task.timeEstimate}h`);
1385
+ if (task.assignedUser) lines.push(`Assigned user: ${formatCodeAndName(task.assignedUser)}`);
1386
+ if (task.assignedTeam) lines.push(`Assigned team: ${formatCodeAndName(task.assignedTeam)}`);
1387
+ if (task.category) lines.push(`Category: ${formatCodeAndName(task.category)}`);
1388
+ if (task.recurringTask) lines.push(`Recurring template: ${formatCodeAndName(task.recurringTask)}`);
1389
+ if (task.workflow) lines.push(`Workflow template: ${formatCodeAndName(task.workflow)}`);
1390
+ const description = task.description?.trim();
1391
+ if (description) lines.push("", "--- Description ---", description);
1392
+ const checklist = task.checklist ?? [];
1393
+ if (checklist.length > 0) {
1394
+ lines.push("", `--- Checklist (${countCompleted(checklist)}/${checklist.length}) ---`);
1395
+ const ordered = [...checklist].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
1396
+ for (const item of ordered) {
1397
+ const mark = item.isCompleted ? "[x]" : "[ ]";
1398
+ lines.push(`${mark} ${item.text ?? ""}`);
1399
+ }
1400
+ }
1401
+ if (workflowGroups.length > 0) {
1402
+ const totalSteps = task.workflowSteps ?? sumBy(workflowGroups, (g) => g.totalSteps ?? 0);
1403
+ const doneSteps = task.workflowStepsComplete ?? sumBy(workflowGroups, (g) => g.completedSteps ?? 0);
1404
+ lines.push("", `--- Workflow progress (${doneSteps}/${totalSteps} steps complete) ---`);
1405
+ for (const group of workflowGroups) {
1406
+ const gName = group.groupName ?? `Group ${group.groupNumber ?? "?"}`;
1407
+ const gTotal = group.totalSteps ?? 0;
1408
+ const gDone = group.completedSteps ?? 0;
1409
+ const deadline = group.groupDeadline ? ` — deadline ${group.groupDeadline}` : "";
1410
+ lines.push(`Group: ${gName} (${gDone}/${gTotal})${deadline}`);
1411
+ for (const step of group.steps ?? []) lines.push(` ${formatStep(step)}`);
1412
+ }
1413
+ }
1414
+ if (notes.length > 0) {
1415
+ const sorted = [...notes].sort((a, b) => {
1416
+ const pa = a.pinnedLevel ?? 0;
1417
+ const pb = b.pinnedLevel ?? 0;
1418
+ if (pa !== pb) return pb - pa;
1419
+ const da = a.date ?? a.createdDate ?? "";
1420
+ return (b.date ?? b.createdDate ?? "").localeCompare(da);
1421
+ });
1422
+ lines.push("", `--- Notes (${notes.length}) ---`);
1423
+ for (const note of sorted.slice(0, 20)) lines.push(formatNote(note));
1424
+ if (notes.length > 20) lines.push(`... and ${notes.length - 20} more notes`);
1425
+ }
1426
+ const clients = task.clients ?? [];
1427
+ const primaryCode = task.primaryClient?.code;
1428
+ const secondary = clients.filter((c) => c.code !== primaryCode);
1429
+ if (task.primaryClient || clients.length > 0) {
1430
+ lines.push("", "--- Parent client ---");
1431
+ if (task.primaryClient) lines.push(`Primary: ${formatCodeAndName(task.primaryClient)}`);
1432
+ if (secondary.length > 0) lines.push(`Also linked: ${secondary.map(formatCodeAndName).join(", ")}`);
1433
+ }
1434
+ if (gaps.length > 0) lines.push("", `Note: could not load ${gaps.join(", ")} (partial failure). Retry for a complete picture.`);
1435
+ return lines.join("\n");
1436
+ }
1437
+ function formatStep(step) {
1438
+ const num = step.stepNumber ?? "?";
1439
+ const name = step.stepName ?? "(unnamed)";
1440
+ const bits = [];
1441
+ if (step.status) bits.push(step.status);
1442
+ if (step.assignedUser?.name) bits.push(step.assignedUser.name);
1443
+ else if (step.assignedTeam?.name) bits.push(`team ${step.assignedTeam.name}`);
1444
+ if (step.completedDate) bits.push(`done ${step.completedDate}`);
1445
+ if (step.blockedReason) bits.push(`blocked: ${step.blockedReason}`);
1446
+ return `Step ${num}: ${name}${bits.length > 0 ? ` — ${bits.join(" · ")}` : ""}`;
1447
+ }
1448
+ function formatNote(note) {
1449
+ const pinPrefix = (note.pinnedLevel ?? 0) > 0 ? "[pinned] " : "";
1450
+ const dateStr = note.date ?? note.createdDate ?? "";
1451
+ const author = note.noteFromUser?.name ?? "(unknown)";
1452
+ const stepCtx = note.stepName ? ` [step: ${note.stepName}]` : "";
1453
+ const text = (note.text ?? "").replace(/\s+/g, " ").trim();
1454
+ return `${pinPrefix}${dateStr} — ${author}${stepCtx}: ${text.length > 200 ? `${text.slice(0, 200)}...` : text}`;
1455
+ }
1456
+ function formatCodeAndName(v) {
1457
+ if (!v) return "(none)";
1458
+ const name = v.name ?? "";
1459
+ const code = v.code ?? "";
1460
+ if (name && code) return `${name} (${code})`;
1461
+ return name || code || "(none)";
1462
+ }
1463
+ function countCompleted(items) {
1464
+ return items.filter((i) => i.isCompleted).length;
1465
+ }
1466
+ function sumBy(arr, fn) {
1467
+ return arr.reduce((acc, cur) => acc + fn(cur), 0);
1218
1468
  }
1219
1469
  //#endregion
1220
1470
  //#region ../mcp-core/src/tools/list-tasks.ts
@@ -1280,7 +1530,7 @@ const ListTasksInputSchema = {
1280
1530
  };
1281
1531
  function formatTask(t) {
1282
1532
  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}` : ""}`;
1533
+ 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
1534
  }
1285
1535
  async function handleListTasks(api, args) {
1286
1536
  try {
@@ -1484,6 +1734,16 @@ async function buildServer(config) {
1484
1734
  openWorldHint: true
1485
1735
  }
1486
1736
  }, (args) => handleListTasks(api, args));
1737
+ server.registerTool("get_task_context", {
1738
+ title: "Get full context for a single task",
1739
+ 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).",
1740
+ inputSchema: GetTaskContextInputSchema,
1741
+ annotations: {
1742
+ readOnlyHint: true,
1743
+ idempotentHint: true,
1744
+ openWorldHint: true
1745
+ }
1746
+ }, (args) => handleGetTaskContext(api, args));
1487
1747
  server.registerTool("list_users", {
1488
1748
  title: "List / search / filter tenant users",
1489
1749
  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.2592",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {