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