@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.
Files changed (3) hide show
  1. package/README.md +1 -0
  2. package/dist/index.js +134 -10
  3. 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.2588",
3
+ "version": "0.1.0-beta.2589",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {