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

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 +191 -23
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -775,6 +775,22 @@ const getCurrentUser = (options) => (options?.client ?? client).get({
775
775
  url: "/users/me",
776
776
  ...options
777
777
  });
778
+ /**
779
+ * List TenantUsers
780
+ *
781
+ * Lists all TenantUsers for the given tenant.
782
+ */
783
+ const listTenantUsers = (options) => (options.client ?? client).get({
784
+ security: [{
785
+ name: "x-api-key",
786
+ type: "apiKey"
787
+ }, {
788
+ scheme: "bearer",
789
+ type: "http"
790
+ }],
791
+ url: "/tenants/{tenant}/users",
792
+ ...options
793
+ });
778
794
  //#endregion
779
795
  //#region ../mcp-core/src/http/client.ts
780
796
  var SodiumApiError = class extends Error {
@@ -875,6 +891,20 @@ var SodiumApiClient = class {
875
891
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `list services for client ${clientCode}`);
876
892
  return data;
877
893
  }
894
+ async listUsers(query = {}) {
895
+ const correlationId = randomUUID();
896
+ const { data, error, response } = await listTenantUsers({
897
+ path: { tenant: this.ctx.tenant },
898
+ query: {
899
+ ...query,
900
+ limit: query.limit ?? 10,
901
+ offset: query.offset ?? 0
902
+ },
903
+ headers: { "X-Correlation-Id": correlationId }
904
+ });
905
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list users");
906
+ return data;
907
+ }
878
908
  async listTasks(query = {}) {
879
909
  const correlationId = randomUUID();
880
910
  const { data, error, response } = await listTaskItems({
@@ -899,11 +929,17 @@ var SodiumApiClient = class {
899
929
  };
900
930
  //#endregion
901
931
  //#region ../mcp-core/src/context/instructions.ts
932
+ const ROSTER_CAP = 20;
902
933
  async function buildInstructions(api) {
903
- const [user, tenant, practice] = await Promise.allSettled([
934
+ const [user, tenant, practice, team] = await Promise.allSettled([
904
935
  api.getCurrentUser(),
905
936
  api.getTenantDetails(),
906
- api.getPracticeDetails()
937
+ api.getPracticeDetails(),
938
+ api.listUsers({
939
+ status: "Active",
940
+ limit: ROSTER_CAP,
941
+ sortBy: "LastName"
942
+ })
907
943
  ]);
908
944
  const now = /* @__PURE__ */ new Date();
909
945
  const lines = [
@@ -918,7 +954,20 @@ async function buildInstructions(api) {
918
954
  }
919
955
  if (tenant.status === "fulfilled") lines.push(`Tenant: ${tenant.value.name} (${tenant.value.code})`);
920
956
  if (practice.status === "fulfilled") lines.push(`Practice: ${practice.value.name}`);
921
- lines.push("", "When interpreting relative dates (today, this week, next month), use the date above.", "All codes (client, task, engagement) are string identifiers, not numeric IDs.");
957
+ if (team.status === "fulfilled") {
958
+ const members = team.value.data ?? [];
959
+ const total = team.value.totalCount ?? members.length;
960
+ if (members.length > 0) {
961
+ lines.push("", `Team members (${members.length}${total > members.length ? ` of ${total}` : ""} active):`);
962
+ for (const m of members) {
963
+ const code = m.code ?? "(no code)";
964
+ const display = m.displayName ?? [m.firstName, m.lastName].filter(Boolean).join(" ") ?? m.email ?? "(no name)";
965
+ lines.push(`- ${display} (${code})`);
966
+ }
967
+ if (total > members.length) lines.push(`... and ${total - members.length} more. Use list_users to see the rest or filter by role / status.`);
968
+ }
969
+ }
970
+ lines.push("", "When interpreting relative dates (today, this week, next month), use the date above.", "When the user names a team member (e.g. 'Jane's tasks'), resolve to their code from the team roster above before calling other tools.", "All codes (client, task, engagement, user) are string identifiers, not numeric IDs.");
922
971
  return lines.join("\n");
923
972
  }
924
973
  //#endregion
@@ -971,7 +1020,7 @@ async function handleGetPracticeDetails(api) {
971
1020
  }
972
1021
  //#endregion
973
1022
  //#region ../mcp-core/src/tools/list-clients.ts
974
- const statusEnum$1 = z.enum([
1023
+ const statusEnum$2 = z.enum([
975
1024
  "Active",
976
1025
  "Inactive",
977
1026
  "Prospect",
@@ -987,19 +1036,19 @@ const typeEnum = z.enum([
987
1036
  "Charity",
988
1037
  "SoleTrader"
989
1038
  ]);
990
- const sortByEnum$1 = z.enum(["Name", "InternalReference"]);
1039
+ const sortByEnum$2 = z.enum(["Name", "InternalReference"]);
991
1040
  const ListClientsInputSchema = {
992
1041
  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$1).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
1042
+ status: z.array(statusEnum$2).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
994
1043
  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
1044
  managerCode: z.array(z.string()).optional().describe("Filter by assigned manager user codes."),
996
1045
  partnerCode: z.array(z.string()).optional().describe("Filter by assigned partner user codes."),
997
1046
  associateCode: z.array(z.string()).optional().describe("Filter by assigned associate user codes."),
998
1047
  serviceCode: z.array(z.string()).optional().describe("Filter by billable service codes that clients have assigned."),
999
1048
  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$1.optional().describe("Field to sort by. Defaults to Name."),
1049
+ sortBy: sortByEnum$2.optional().describe("Field to sort by. Defaults to Name."),
1001
1050
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
1002
- limit: z.number().int().min(1).max(50).optional().describe("Maximum number of clients to return per page. Default 10, max 50."),
1051
+ limit: z.number().int().min(0).max(50).optional().describe("Maximum number of clients to return per page. Default 10, max 50. Pass 0 to return only the total count without any client data — use this for 'how many X?' questions so the API doesn't fetch a full page just to be counted."),
1003
1052
  offset: z.number().int().min(0).optional().describe("Number of records to skip for pagination. Default 0.")
1004
1053
  };
1005
1054
  function formatClient(c) {
@@ -1013,15 +1062,22 @@ async function handleListClients(api, args) {
1013
1062
  const query = args;
1014
1063
  const result = await api.listClients(query);
1015
1064
  const items = result.data ?? [];
1065
+ const total = result.totalCount ?? items.length;
1066
+ if (args.limit === 0) {
1067
+ const desc = describeFilters$2(args);
1068
+ return { content: [{
1069
+ type: "text",
1070
+ text: desc ? `Total: ${total} client${total === 1 ? "" : "s"} matching ${desc}.` : `Total: ${total} client${total === 1 ? "" : "s"}.`
1071
+ }] };
1072
+ }
1016
1073
  if (items.length === 0) {
1017
- const desc = describeFilters$1(args);
1074
+ const desc = describeFilters$2(args);
1018
1075
  return { content: [{
1019
1076
  type: "text",
1020
1077
  text: desc ? `No clients match ${desc}.` : "No clients found."
1021
1078
  }] };
1022
1079
  }
1023
- const total = result.totalCount ?? items.length;
1024
- const desc = describeFilters$1(args);
1080
+ const desc = describeFilters$2(args);
1025
1081
  const lines = [
1026
1082
  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
1083
  "",
@@ -1045,7 +1101,7 @@ async function handleListClients(api, args) {
1045
1101
  };
1046
1102
  }
1047
1103
  }
1048
- function describeFilters$1(args) {
1104
+ function describeFilters$2(args) {
1049
1105
  const parts = [];
1050
1106
  if (args.search) parts.push(`search "${args.search}"`);
1051
1107
  if (args.status?.length) parts.push(`status ${args.status.join("/")}`);
@@ -1162,7 +1218,7 @@ function formatTask$1(t) {
1162
1218
  }
1163
1219
  //#endregion
1164
1220
  //#region ../mcp-core/src/tools/list-tasks.ts
1165
- const statusEnum = z.enum([
1221
+ const statusEnum$1 = z.enum([
1166
1222
  "NotStarted",
1167
1223
  "InProgress",
1168
1224
  "Blocked",
@@ -1194,7 +1250,7 @@ const dateRangeEnum = z.enum([
1194
1250
  "CustomDateRange"
1195
1251
  ]);
1196
1252
  const dateBasisEnum = z.enum(["StartDate", "DueDate"]);
1197
- const sortByEnum = z.enum([
1253
+ const sortByEnum$1 = z.enum([
1198
1254
  "Name",
1199
1255
  "DueDate",
1200
1256
  "StartDate",
@@ -1205,7 +1261,7 @@ const sortByEnum = z.enum([
1205
1261
  const ListTasksInputSchema = {
1206
1262
  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
1263
  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."),
1264
+ status: z.array(statusEnum$1).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
1265
  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
1266
  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
1267
  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."),
@@ -1217,9 +1273,9 @@ const ListTasksInputSchema = {
1217
1273
  savedFilter: z.string().optional().describe("Apply a user-saved filter by its code. Other filter parameters override fields from the saved filter."),
1218
1274
  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
1275
  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."),
1276
+ sortBy: sortByEnum$1.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
1277
  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."),
1278
+ limit: z.number().int().min(0).max(50).optional().describe("Maximum number of tasks per page. Default 10, max 50. Pass 0 to return only the total count without any task data — use this for 'how many X?' questions so the API doesn't fetch a full page just to be counted."),
1223
1279
  offset: z.number().int().min(0).optional().describe("Pagination offset. Default 0.")
1224
1280
  };
1225
1281
  function formatTask(t) {
@@ -1231,15 +1287,22 @@ async function handleListTasks(api, args) {
1231
1287
  const query = args;
1232
1288
  const result = await api.listTasks(query);
1233
1289
  const items = result.data ?? [];
1290
+ const total = result.totalCount ?? items.length;
1291
+ if (args.limit === 0) {
1292
+ const desc = describeFilters$1(args);
1293
+ return { content: [{
1294
+ type: "text",
1295
+ text: desc ? `Total: ${total} task${total === 1 ? "" : "s"} matching ${desc}.` : `Total: ${total} task${total === 1 ? "" : "s"}.`
1296
+ }] };
1297
+ }
1234
1298
  if (items.length === 0) {
1235
- const desc = describeFilters(args);
1299
+ const desc = describeFilters$1(args);
1236
1300
  return { content: [{
1237
1301
  type: "text",
1238
1302
  text: desc ? `No tasks match ${desc}.` : "No tasks found."
1239
1303
  }] };
1240
1304
  }
1241
- const total = result.totalCount ?? items.length;
1242
- const desc = describeFilters(args);
1305
+ const desc = describeFilters$1(args);
1243
1306
  const lines = [
1244
1307
  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
1308
  "",
@@ -1263,7 +1326,7 @@ async function handleListTasks(api, args) {
1263
1326
  };
1264
1327
  }
1265
1328
  }
1266
- function describeFilters(args) {
1329
+ function describeFilters$1(args) {
1267
1330
  const parts = [];
1268
1331
  if (args.user?.length) parts.push(`user ${args.user.join(",")}`);
1269
1332
  if (args.client?.length) parts.push(`client ${args.client.join(",")}`);
@@ -1275,6 +1338,101 @@ function describeFilters(args) {
1275
1338
  return parts.join(", ");
1276
1339
  }
1277
1340
  //#endregion
1341
+ //#region ../mcp-core/src/tools/list-users.ts
1342
+ const statusEnum = z.enum([
1343
+ "Created",
1344
+ "Invited",
1345
+ "Active",
1346
+ "Disabled",
1347
+ "Declined",
1348
+ "Deleted"
1349
+ ]);
1350
+ const systemRoleEnum = z.enum([
1351
+ "Admin",
1352
+ "StandardUser",
1353
+ "Viewer"
1354
+ ]);
1355
+ const sortByEnum = z.enum([
1356
+ "LastName",
1357
+ "FirstName",
1358
+ "Email"
1359
+ ]);
1360
+ const ListUsersInputSchema = {
1361
+ search: z.string().min(3, "Search must be at least 3 characters when provided").optional().describe("Free-text search across user code, first name, last name, and email. Minimum 3 characters. Use for 'find Jane' when the name isn't in the startup roster (large teams, non-active users)."),
1362
+ status: statusEnum.optional().describe("Filter by a single user status. Use 'Active' for currently-working members, 'Invited' for pending invitations, 'Disabled' for offboarded users, 'Deleted' for removed users. Note: this is a single value, not an array."),
1363
+ systemRole: systemRoleEnum.optional().describe("Filter by system role. Admin = full access, StandardUser = normal access, Viewer = read-only. Single value."),
1364
+ isClientManager: z.boolean().optional().describe("Set true to return only users flagged as client managers; false to exclude them."),
1365
+ isPartner: z.boolean().optional().describe("Set true to return only users flagged as partners; false to exclude them. Use for 'list partners' queries."),
1366
+ isAssociate: z.boolean().optional().describe("Set true to return only users flagged as associates; false to exclude them."),
1367
+ sortBy: sortByEnum.optional().describe("Field to sort by. Defaults to LastName."),
1368
+ sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
1369
+ limit: z.number().int().min(0).max(50).optional().describe("Maximum number of users per page. Default 10, max 50. Pass 0 to return only the total count without any user data — use for 'how many users?' questions."),
1370
+ offset: z.number().int().min(0).optional().describe("Pagination offset. Default 0.")
1371
+ };
1372
+ function formatUser(u) {
1373
+ const code = u.code ?? "(no code)";
1374
+ const display = u.displayName ?? [u.firstName, u.lastName].filter(Boolean).join(" ") ?? u.email ?? "(no name)";
1375
+ const meta = [];
1376
+ if (u.status && u.status !== "Active") meta.push(u.status);
1377
+ if (u.email && u.email !== display) meta.push(u.email);
1378
+ if (u.isSuperAdmin) meta.push("SuperAdmin");
1379
+ return `- ${display} (${code})${meta.length > 0 ? ` — ${meta.join(" · ")}` : ""}`;
1380
+ }
1381
+ async function handleListUsers(api, args) {
1382
+ try {
1383
+ const query = args;
1384
+ const result = await api.listUsers(query);
1385
+ const items = result.data ?? [];
1386
+ const total = result.totalCount ?? items.length;
1387
+ if (args.limit === 0) {
1388
+ const desc = describeFilters(args);
1389
+ return { content: [{
1390
+ type: "text",
1391
+ text: desc ? `Total: ${total} user${total === 1 ? "" : "s"} matching ${desc}.` : `Total: ${total} user${total === 1 ? "" : "s"}.`
1392
+ }] };
1393
+ }
1394
+ if (items.length === 0) {
1395
+ const desc = describeFilters(args);
1396
+ return { content: [{
1397
+ type: "text",
1398
+ text: desc ? `No users match ${desc}.` : "No users found."
1399
+ }] };
1400
+ }
1401
+ const desc = describeFilters(args);
1402
+ const lines = [
1403
+ desc ? total > items.length ? `Found ${total} users matching ${desc} (showing ${items.length}):` : `Found ${items.length} user${items.length === 1 ? "" : "s"} matching ${desc}:` : total > items.length ? `Showing ${items.length} of ${total} users:` : `${items.length} user${items.length === 1 ? "" : "s"}:`,
1404
+ "",
1405
+ ...items.map(formatUser)
1406
+ ];
1407
+ if (result.hasMore) {
1408
+ const nextOffset = (args.offset ?? 0) + items.length;
1409
+ lines.push("", `More results available — call again with offset: ${nextOffset} to see the next page.`);
1410
+ }
1411
+ return { content: [{
1412
+ type: "text",
1413
+ text: lines.join("\n")
1414
+ }] };
1415
+ } catch (error) {
1416
+ return {
1417
+ content: [{
1418
+ type: "text",
1419
+ text: error instanceof SodiumApiError ? `Error listing users: ${error.message} (correlation: ${error.correlationId})` : `Error listing users: ${error instanceof Error ? error.message : String(error)}`
1420
+ }],
1421
+ isError: true
1422
+ };
1423
+ }
1424
+ }
1425
+ function describeFilters(args) {
1426
+ const parts = [];
1427
+ if (args.search) parts.push(`search "${args.search}"`);
1428
+ if (args.status) parts.push(`status ${args.status}`);
1429
+ if (args.systemRole) parts.push(`role ${args.systemRole}`);
1430
+ if (args.isClientManager !== void 0) parts.push(`isClientManager=${args.isClientManager}`);
1431
+ if (args.isPartner !== void 0) parts.push(`isPartner=${args.isPartner}`);
1432
+ if (args.isAssociate !== void 0) parts.push(`isAssociate=${args.isAssociate}`);
1433
+ return parts.join(", ");
1434
+ }
1435
+ //#endregion
1278
1436
  //#region ../mcp-core/src/server.ts
1279
1437
  async function buildServer(config) {
1280
1438
  const api = new SodiumApiClient(config.context);
@@ -1298,7 +1456,7 @@ async function buildServer(config) {
1298
1456
  }, () => handleGetPracticeDetails(api));
1299
1457
  server.registerTool("list_clients", {
1300
1458
  title: "List / search / filter clients",
1301
- description: "List clients with any combination of: search (code/name/internal reference, 3+ chars), status (Active/Inactive/Prospect/LostProspect), type (PrivateLimitedCompany/PublicLimitedCompany/LimitedLiabilityPartnership/Partnership/Individual/Trust/Charity/SoleTrader), manager/partner/associate user codes, service codes, a saved filter code, sort, and pagination. Use search for 'find ACME'-style queries. Use type for 'list limited companies' (pass PrivateLimitedCompany + PublicLimitedCompany). Use status: ['Active'] to exclude prospects/inactive. Returns up to 50 clients per page — paginate via offset for more. Follow up with get_client_summary for full detail on a specific client.",
1459
+ description: "List clients with any combination of: search (code/name/internal reference, 3+ chars), status (Active/Inactive/Prospect/LostProspect), type (PrivateLimitedCompany/PublicLimitedCompany/LimitedLiabilityPartnership/Partnership/Individual/Trust/Charity/SoleTrader), manager/partner/associate user codes, service codes, a saved filter code, sort, and pagination. Use search for 'find ACME'-style queries. Use type for 'list limited companies' (pass PrivateLimitedCompany + PublicLimitedCompany). Use status: ['Active'] to exclude prospects/inactive. Returns up to 50 clients per page — paginate via offset for more. For 'how many X?' questions, pass limit=0 to get just the total count without fetching any client data. Follow up with get_client_summary for full detail on a specific client.",
1302
1460
  inputSchema: ListClientsInputSchema,
1303
1461
  annotations: {
1304
1462
  readOnlyHint: true,
@@ -1318,7 +1476,7 @@ async function buildServer(config) {
1318
1476
  }, (args) => handleGetClientSummary(api, args));
1319
1477
  server.registerTool("list_tasks", {
1320
1478
  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.",
1479
+ 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. (4) For 'how many X?' questions, pass limit=0 to get just the total count without fetching any task data — much cheaper than fetching a full page and counting.",
1322
1480
  inputSchema: ListTasksInputSchema,
1323
1481
  annotations: {
1324
1482
  readOnlyHint: true,
@@ -1326,6 +1484,16 @@ async function buildServer(config) {
1326
1484
  openWorldHint: true
1327
1485
  }
1328
1486
  }, (args) => handleListTasks(api, args));
1487
+ server.registerTool("list_users", {
1488
+ title: "List / search / filter tenant users",
1489
+ 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.",
1490
+ inputSchema: ListUsersInputSchema,
1491
+ annotations: {
1492
+ readOnlyHint: true,
1493
+ idempotentHint: true,
1494
+ openWorldHint: true
1495
+ }
1496
+ }, (args) => handleListUsers(api, args));
1329
1497
  return server;
1330
1498
  }
1331
1499
  //#endregion
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.2589",
3
+ "version": "0.1.0-beta.2590",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {