@sodiumhq/mcp-pm 0.1.0-beta.2588 → 0.1.0-beta.2589
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/README.md +1 -0
- package/dist/index.js +134 -10
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,6 +50,7 @@ Then ask: *"give me a summary of my practice"*.
|
|
|
50
50
|
- **`get_practice_details`** — consolidated practice overview (counts, connections, settings)
|
|
51
51
|
- **`list_clients`** — list and filter clients by search, status, type, assignee, services, saved filters
|
|
52
52
|
- **`get_client_summary`** — one-call composite: client identity + contacts + active services + overdue + upcoming tasks
|
|
53
|
+
- **`list_tasks`** — list and filter tasks by user (including "my tasks"), client, status, date range, overdue, category, team — covers both individual and practice-manager workflows
|
|
53
54
|
|
|
54
55
|
More tools land iteratively as the beta progresses.
|
|
55
56
|
|
package/dist/index.js
CHANGED
|
@@ -971,7 +971,7 @@ async function handleGetPracticeDetails(api) {
|
|
|
971
971
|
}
|
|
972
972
|
//#endregion
|
|
973
973
|
//#region ../mcp-core/src/tools/list-clients.ts
|
|
974
|
-
const statusEnum = z.enum([
|
|
974
|
+
const statusEnum$1 = z.enum([
|
|
975
975
|
"Active",
|
|
976
976
|
"Inactive",
|
|
977
977
|
"Prospect",
|
|
@@ -987,17 +987,17 @@ const typeEnum = z.enum([
|
|
|
987
987
|
"Charity",
|
|
988
988
|
"SoleTrader"
|
|
989
989
|
]);
|
|
990
|
-
const sortByEnum = z.enum(["Name", "InternalReference"]);
|
|
990
|
+
const sortByEnum$1 = z.enum(["Name", "InternalReference"]);
|
|
991
991
|
const ListClientsInputSchema = {
|
|
992
992
|
search: z.string().min(3, "Search must be at least 3 characters when provided").optional().describe("Free-text search across client code, name, and internal reference. Minimum 3 characters. Omit to browse by filter only."),
|
|
993
|
-
status: z.array(statusEnum).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
|
|
993
|
+
status: z.array(statusEnum$1).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
|
|
994
994
|
type: z.array(typeEnum).optional().describe("Filter by organisation type. Use ['PrivateLimitedCompany', 'PublicLimitedCompany'] for 'limited companies'. Use ['LimitedLiabilityPartnership'] for LLPs. Defaults to all types if omitted."),
|
|
995
995
|
managerCode: z.array(z.string()).optional().describe("Filter by assigned manager user codes."),
|
|
996
996
|
partnerCode: z.array(z.string()).optional().describe("Filter by assigned partner user codes."),
|
|
997
997
|
associateCode: z.array(z.string()).optional().describe("Filter by assigned associate user codes."),
|
|
998
998
|
serviceCode: z.array(z.string()).optional().describe("Filter by billable service codes that clients have assigned."),
|
|
999
999
|
savedFilter: z.string().optional().describe("Code of a user-saved filter to apply. Other filter parameters override fields from the saved filter."),
|
|
1000
|
-
sortBy: sortByEnum.optional().describe("Field to sort by. Defaults to Name."),
|
|
1000
|
+
sortBy: sortByEnum$1.optional().describe("Field to sort by. Defaults to Name."),
|
|
1001
1001
|
sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
|
|
1002
1002
|
limit: z.number().int().min(1).max(50).optional().describe("Maximum number of clients to return per page. Default 10, max 50."),
|
|
1003
1003
|
offset: z.number().int().min(0).optional().describe("Number of records to skip for pagination. Default 0.")
|
|
@@ -1014,14 +1014,14 @@ async function handleListClients(api, args) {
|
|
|
1014
1014
|
const result = await api.listClients(query);
|
|
1015
1015
|
const items = result.data ?? [];
|
|
1016
1016
|
if (items.length === 0) {
|
|
1017
|
-
const desc = describeFilters(args);
|
|
1017
|
+
const desc = describeFilters$1(args);
|
|
1018
1018
|
return { content: [{
|
|
1019
1019
|
type: "text",
|
|
1020
1020
|
text: desc ? `No clients match ${desc}.` : "No clients found."
|
|
1021
1021
|
}] };
|
|
1022
1022
|
}
|
|
1023
1023
|
const total = result.totalCount ?? items.length;
|
|
1024
|
-
const desc = describeFilters(args);
|
|
1024
|
+
const desc = describeFilters$1(args);
|
|
1025
1025
|
const lines = [
|
|
1026
1026
|
desc ? total > items.length ? `Found ${total} clients matching ${desc} (showing ${items.length}):` : `Found ${items.length} client${items.length === 1 ? "" : "s"} matching ${desc}:` : total > items.length ? `Showing ${items.length} of ${total} clients:` : `${items.length} client${items.length === 1 ? "" : "s"}:`,
|
|
1027
1027
|
"",
|
|
@@ -1045,7 +1045,7 @@ async function handleListClients(api, args) {
|
|
|
1045
1045
|
};
|
|
1046
1046
|
}
|
|
1047
1047
|
}
|
|
1048
|
-
function describeFilters(args) {
|
|
1048
|
+
function describeFilters$1(args) {
|
|
1049
1049
|
const parts = [];
|
|
1050
1050
|
if (args.search) parts.push(`search "${args.search}"`);
|
|
1051
1051
|
if (args.status?.length) parts.push(`status ${args.status.join("/")}`);
|
|
@@ -1145,22 +1145,136 @@ function format(input) {
|
|
|
1145
1145
|
lines.push("", "--- Tasks ---");
|
|
1146
1146
|
lines.push(`Overdue: ${overdueTasks.length}`);
|
|
1147
1147
|
if (overdueTasks.length > 0) {
|
|
1148
|
-
for (const t of overdueTasks.slice(0, 5)) lines.push(` - ${formatTask(t)}`);
|
|
1148
|
+
for (const t of overdueTasks.slice(0, 5)) lines.push(` - ${formatTask$1(t)}`);
|
|
1149
1149
|
if (overdueTasks.length > 5) lines.push(` ... and ${overdueTasks.length - 5} more`);
|
|
1150
1150
|
}
|
|
1151
1151
|
lines.push(`Due in next 7 days: ${upcomingTasks.length}`);
|
|
1152
1152
|
if (upcomingTasks.length > 0) {
|
|
1153
|
-
for (const t of upcomingTasks.slice(0, 5)) lines.push(` - ${formatTask(t)}`);
|
|
1153
|
+
for (const t of upcomingTasks.slice(0, 5)) lines.push(` - ${formatTask$1(t)}`);
|
|
1154
1154
|
if (upcomingTasks.length > 5) lines.push(` ... and ${upcomingTasks.length - 5} more`);
|
|
1155
1155
|
}
|
|
1156
1156
|
if (gaps.length > 0) lines.push("", `Note: could not load ${gaps.join(", ")} (partial failure). Retry for a complete picture.`);
|
|
1157
1157
|
return lines.join("\n");
|
|
1158
1158
|
}
|
|
1159
|
-
function formatTask(t) {
|
|
1159
|
+
function formatTask$1(t) {
|
|
1160
1160
|
const taskCode = t.code ?? "(no code)";
|
|
1161
1161
|
return `${t.name ?? "(unnamed)"} (${taskCode})${t.dueDate ? ` — due ${t.dueDate}` : ""}${t.assignedUser?.name ? ` — ${t.assignedUser.name}` : ""}`;
|
|
1162
1162
|
}
|
|
1163
1163
|
//#endregion
|
|
1164
|
+
//#region ../mcp-core/src/tools/list-tasks.ts
|
|
1165
|
+
const statusEnum = z.enum([
|
|
1166
|
+
"NotStarted",
|
|
1167
|
+
"InProgress",
|
|
1168
|
+
"Blocked",
|
|
1169
|
+
"Completed",
|
|
1170
|
+
"Skipped"
|
|
1171
|
+
]);
|
|
1172
|
+
const dateRangeEnum = z.enum([
|
|
1173
|
+
"Today",
|
|
1174
|
+
"ThisWeek",
|
|
1175
|
+
"NextWeek",
|
|
1176
|
+
"Last7Days",
|
|
1177
|
+
"Next7Days",
|
|
1178
|
+
"Last30Days",
|
|
1179
|
+
"Next30Days",
|
|
1180
|
+
"PreviousMonth",
|
|
1181
|
+
"ThisMonth",
|
|
1182
|
+
"NextMonth",
|
|
1183
|
+
"PreviousQuarter",
|
|
1184
|
+
"ThisQuarter",
|
|
1185
|
+
"NextQuarter",
|
|
1186
|
+
"PreviousYear",
|
|
1187
|
+
"ThisYear",
|
|
1188
|
+
"NextYear",
|
|
1189
|
+
"YearToDate",
|
|
1190
|
+
"PreviousFinancialYear",
|
|
1191
|
+
"ThisFinancialYear",
|
|
1192
|
+
"NextFinancialYear",
|
|
1193
|
+
"FinancialYearToDate",
|
|
1194
|
+
"CustomDateRange"
|
|
1195
|
+
]);
|
|
1196
|
+
const dateBasisEnum = z.enum(["StartDate", "DueDate"]);
|
|
1197
|
+
const sortByEnum = z.enum([
|
|
1198
|
+
"Name",
|
|
1199
|
+
"DueDate",
|
|
1200
|
+
"StartDate",
|
|
1201
|
+
"Category",
|
|
1202
|
+
"ClientName",
|
|
1203
|
+
"AssignedUser"
|
|
1204
|
+
]);
|
|
1205
|
+
const ListTasksInputSchema = {
|
|
1206
|
+
user: z.array(z.string()).optional().describe("Filter by assigned user codes. For 'my tasks' use the current user's code from the startup context. For another team member's tasks, pass their code. Omit to see tasks across all users (useful for practice managers)."),
|
|
1207
|
+
client: z.array(z.string()).optional().describe("Filter by client codes — tasks belonging to these specific clients only."),
|
|
1208
|
+
status: z.array(statusEnum).optional().describe("Filter by task status. Typical: ['NotStarted', 'InProgress'] to exclude completed/skipped. Leave empty for all statuses. IMPORTANT: if the query includes NotStarted (the default for new tasks), you must ALSO provide one of: dateRange, isOverdue=true, or restrict status to non-NotStarted values only — otherwise the API rejects the call to prevent unbounded queries."),
|
|
1209
|
+
isOverdue: z.boolean().optional().describe("Set true to return only overdue tasks (DueDate before today, not completed/skipped). When true, no date range is required. Useful for sidestepping the NotStarted + date-range requirement."),
|
|
1210
|
+
dateRange: dateRangeEnum.optional().describe("Preset date range. Use 'Today' for today's tasks, 'Next7Days' for 'due this week', 'Last30Days' for recent activity. If 'CustomDateRange', also provide startDate and/or endDate. REQUIRED when querying NotStarted tasks unless you pass isOverdue=true."),
|
|
1211
|
+
startDate: z.string().optional().describe("Start of custom date range (YYYY-MM-DD). Used when dateRange='CustomDateRange'. Prefer specifying both startDate and endDate with a narrow window for performance — broad ranges strain the API. If startDate > endDate the API swaps them silently. Maximum allowed total range is 2 years."),
|
|
1212
|
+
endDate: z.string().optional().describe("End of custom date range (YYYY-MM-DD). Used when dateRange='CustomDateRange'. Pair with startDate to define a narrow window — avoid open-ended ranges."),
|
|
1213
|
+
dateBasis: dateBasisEnum.optional().describe("Which date field to apply the date range filter to. Use 'DueDate' for 'what's due when' questions; 'StartDate' (default) for 'what's been worked on'."),
|
|
1214
|
+
category: z.array(z.string()).optional().describe("Filter by task category codes (e.g. tax, audit, compliance categories)."),
|
|
1215
|
+
team: z.array(z.string()).optional().describe("Filter by team codes (if the practice uses teams to group users)."),
|
|
1216
|
+
recurringTask: z.array(z.string()).optional().describe("Filter by recurring-task template codes — returns all instances generated from those templates."),
|
|
1217
|
+
savedFilter: z.string().optional().describe("Apply a user-saved filter by its code. Other filter parameters override fields from the saved filter."),
|
|
1218
|
+
includeProjected: z.boolean().optional().describe("Set true to include projected (virtual, not-yet-materialised) tasks in the results. Projected tasks are always NotStarted. Useful for 'what's coming up' queries."),
|
|
1219
|
+
includeWorkflowSteps: z.boolean().optional().describe("Set true for Agenda Mode: returns both tasks AND their workflow steps as individual rows. Each step row has its parent task's properties plus step-specific details. Useful for 'what's the next action on X' or when workflow step granularity matters."),
|
|
1220
|
+
sortBy: sortByEnum.optional().describe("Field to sort by. Defaults to StartDate. Use 'DueDate' for 'what's due soonest' ordering. Combine with sortDesc for 'oldest/newest' ordering."),
|
|
1221
|
+
sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
|
|
1222
|
+
limit: z.number().int().min(1).max(50).optional().describe("Maximum number of tasks per page. Default 10, max 50."),
|
|
1223
|
+
offset: z.number().int().min(0).optional().describe("Pagination offset. Default 0.")
|
|
1224
|
+
};
|
|
1225
|
+
function formatTask(t) {
|
|
1226
|
+
const code = t.code ?? "(no code)";
|
|
1227
|
+
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}` : ""}`;
|
|
1228
|
+
}
|
|
1229
|
+
async function handleListTasks(api, args) {
|
|
1230
|
+
try {
|
|
1231
|
+
const query = args;
|
|
1232
|
+
const result = await api.listTasks(query);
|
|
1233
|
+
const items = result.data ?? [];
|
|
1234
|
+
if (items.length === 0) {
|
|
1235
|
+
const desc = describeFilters(args);
|
|
1236
|
+
return { content: [{
|
|
1237
|
+
type: "text",
|
|
1238
|
+
text: desc ? `No tasks match ${desc}.` : "No tasks found."
|
|
1239
|
+
}] };
|
|
1240
|
+
}
|
|
1241
|
+
const total = result.totalCount ?? items.length;
|
|
1242
|
+
const desc = describeFilters(args);
|
|
1243
|
+
const lines = [
|
|
1244
|
+
desc ? total > items.length ? `Found ${total} tasks matching ${desc} (showing ${items.length}):` : `Found ${items.length} task${items.length === 1 ? "" : "s"} matching ${desc}:` : total > items.length ? `Showing ${items.length} of ${total} tasks:` : `${items.length} task${items.length === 1 ? "" : "s"}:`,
|
|
1245
|
+
"",
|
|
1246
|
+
...items.map(formatTask)
|
|
1247
|
+
];
|
|
1248
|
+
if (result.hasMore) {
|
|
1249
|
+
const nextOffset = (args.offset ?? 0) + items.length;
|
|
1250
|
+
lines.push("", `More results available — call again with offset: ${nextOffset} to see the next page.`);
|
|
1251
|
+
}
|
|
1252
|
+
return { content: [{
|
|
1253
|
+
type: "text",
|
|
1254
|
+
text: lines.join("\n")
|
|
1255
|
+
}] };
|
|
1256
|
+
} catch (error) {
|
|
1257
|
+
return {
|
|
1258
|
+
content: [{
|
|
1259
|
+
type: "text",
|
|
1260
|
+
text: error instanceof SodiumApiError ? `Error listing tasks: ${error.message} (correlation: ${error.correlationId})` : `Error listing tasks: ${error instanceof Error ? error.message : String(error)}`
|
|
1261
|
+
}],
|
|
1262
|
+
isError: true
|
|
1263
|
+
};
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
function describeFilters(args) {
|
|
1267
|
+
const parts = [];
|
|
1268
|
+
if (args.user?.length) parts.push(`user ${args.user.join(",")}`);
|
|
1269
|
+
if (args.client?.length) parts.push(`client ${args.client.join(",")}`);
|
|
1270
|
+
if (args.status?.length) parts.push(`status ${args.status.join("/")}`);
|
|
1271
|
+
if (args.isOverdue) parts.push("overdue");
|
|
1272
|
+
if (args.dateRange) parts.push(`date ${args.dateRange}${args.dateBasis ? ` (${args.dateBasis})` : ""}${args.dateRange === "CustomDateRange" && args.startDate && args.endDate ? ` ${args.startDate} to ${args.endDate}` : ""}`);
|
|
1273
|
+
if (args.category?.length) parts.push(`category ${args.category.join(",")}`);
|
|
1274
|
+
if (args.team?.length) parts.push(`team ${args.team.join(",")}`);
|
|
1275
|
+
return parts.join(", ");
|
|
1276
|
+
}
|
|
1277
|
+
//#endregion
|
|
1164
1278
|
//#region ../mcp-core/src/server.ts
|
|
1165
1279
|
async function buildServer(config) {
|
|
1166
1280
|
const api = new SodiumApiClient(config.context);
|
|
@@ -1202,6 +1316,16 @@ async function buildServer(config) {
|
|
|
1202
1316
|
openWorldHint: true
|
|
1203
1317
|
}
|
|
1204
1318
|
}, (args) => handleGetClientSummary(api, args));
|
|
1319
|
+
server.registerTool("list_tasks", {
|
|
1320
|
+
title: "List / filter tasks across the practice",
|
|
1321
|
+
description: "List tasks with any combination of filters: assigned user(s), client(s), status, overdue flag, preset date range (Today / ThisWeek / Next7Days / CustomDateRange etc), category, team, recurring task template, saved filter, include-projected, include-workflow-steps (Agenda Mode), sort, and pagination. Use for: 'my tasks' (pass current user's code from startup context), 'Jane's overdue tasks' (user + isOverdue), 'tasks for ACME due this week' (client + dateRange=Next7Days + dateBasis=DueDate), 'what is the team working on this month' (dateRange=ThisMonth, no user filter). Returns up to 50 tasks per page. IMPORTANT constraints to avoid API errors and keep queries efficient: (1) Querying NotStarted tasks requires one of — a dateRange, isOverdue=true, or restricting status to non-NotStarted values. (2) Prefer the narrowest date range that answers the question — broad ranges (quarterly/yearly) are expensive; prefer Today / ThisWeek / Next7Days / ThisMonth over larger windows unless explicitly asked. (3) For 'oldest incomplete tasks' prefer status=['InProgress','Blocked'] with sortBy=StartDate (no date range needed), or add isOverdue=true if 'oldest overdue' is meant.",
|
|
1322
|
+
inputSchema: ListTasksInputSchema,
|
|
1323
|
+
annotations: {
|
|
1324
|
+
readOnlyHint: true,
|
|
1325
|
+
idempotentHint: true,
|
|
1326
|
+
openWorldHint: true
|
|
1327
|
+
}
|
|
1328
|
+
}, (args) => handleListTasks(api, args));
|
|
1205
1329
|
return server;
|
|
1206
1330
|
}
|
|
1207
1331
|
//#endregion
|