@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.
- package/dist/index.js +266 -6
- 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$
|
|
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$
|
|
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.",
|