@sodiumhq/mcp-pm 0.1.0-beta.6357 → 0.1.0-beta.6554

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 CHANGED
@@ -92,6 +92,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
92
92
 
93
93
  **Client requests**
94
94
  - **`list_document_requests`** — document requests across the practice: status, deadline, chase history, linked task. "What are we waiting on from clients right now?"
95
+ - **`list_overdue_requests`** — document requests past their deadline and still waiting on the client, most overdue first, with chase history. "Who do we need to chase?"
95
96
  - **`list_data_form_requests`** — questionnaire/data-form requests: who's been sent what, who's responded, which answers await review. "Who still hasn't returned the SA questionnaire?"
96
97
  - **`list_data_forms`** — the questionnaire templates the practice can send.
97
98
 
@@ -162,6 +163,8 @@ Only enable write mode with an AI client you trust — it hands the client the a
162
163
  - **`create_document_request`** — draft (never send) a document request for a client. Sending stays in the Sodium UI.
163
164
  - **`send_data_form`** — send a questionnaire to a client. Sends immediately, so your assistant confirms with you first.
164
165
  - **`create_proposal`** — draft a proposal/letter of engagement in Unsent state. Sending stays in the Sodium UI.
166
+ - **`list_upcoming_key_dates`** — clients whose accounts, confirmation statement, PAYE/CIS or other key date falls in a window, soonest first. "Whose accounts are due next month?"
167
+ - **`draft_email_to_client`** — save a draft email in your own connected mailbox. Never sends; you review and send it yourself.
165
168
  - **`update_client_dates`** — set year-end, VAT, PAYE, and other key dates on a client.
166
169
  - **`update_client_business_details`** — update UTR, PAYE ref, VAT registration, trading name, addresses. Only the fields you mention change.
167
170
  - **`create_saved_task_filter`** — save a task query as a reusable view. "Save that as 'Monday Review' and pin it to my menu."
package/dist/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-BVx86BgG.mjs";
2
+ import { n as buildServer, t as buildCatalogue } from "./src-B4pIPf3c.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { SUPPORTED_PROTOCOL_VERSIONS, createMcpHandler } from "@modelcontextprotocol/server";
5
5
  import { createHash, randomUUID } from "node:crypto";
@@ -270,6 +270,47 @@ const mcpHandler = createMcpHandler((ctx) => {
270
270
  console.error(`[sodium-pm-mcp-http] handler: ${error.message}`);
271
271
  } });
272
272
  const serveMcp = toNodeHandler(mcpHandler);
273
+ let catalogueJson;
274
+ async function listRegisteredTools() {
275
+ const state = {
276
+ apiToken: "catalogue",
277
+ tenant: void 0,
278
+ writesAllowed: true,
279
+ wantsInstructions: false,
280
+ onToolCall: () => {}
281
+ };
282
+ const request = new Request("http://internal/", {
283
+ method: "POST",
284
+ headers: {
285
+ "Content-Type": "application/json",
286
+ Accept: "application/json, text/event-stream",
287
+ "MCP-Protocol-Version": "2025-06-18"
288
+ },
289
+ body: JSON.stringify({
290
+ jsonrpc: "2.0",
291
+ id: 1,
292
+ method: "tools/list",
293
+ params: {}
294
+ })
295
+ });
296
+ const text = await (await mcpHandler.fetch(request, { authInfo: {
297
+ token: "catalogue",
298
+ clientId: "catalogue",
299
+ scopes: [],
300
+ extra: { sodium: state }
301
+ } })).text();
302
+ const payload = text.trimStart().startsWith("{") ? text : text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).pop() ?? "";
303
+ const tools = JSON.parse(payload).result?.tools;
304
+ if (!tools) throw new Error("tools/list returned no tools.");
305
+ return tools;
306
+ }
307
+ function getCatalogueJson() {
308
+ catalogueJson ??= listRegisteredTools().then((tools) => JSON.stringify(buildCatalogue(tools))).catch((error) => {
309
+ catalogueJson = void 0;
310
+ throw error;
311
+ });
312
+ return catalogueJson;
313
+ }
273
314
  function jsonRpcMethod(body) {
274
315
  if (typeof body === "object" && body !== null && "method" in body) return String(body.method);
275
316
  }
@@ -393,6 +434,14 @@ async function handle(req, res) {
393
434
  }));
394
435
  return;
395
436
  }
437
+ if (req.method === "GET" && url.pathname === "/tools.json") {
438
+ const body = await getCatalogueJson();
439
+ res.writeHead(200, {
440
+ "Content-Type": "application/json",
441
+ "Cache-Control": "public, max-age=300"
442
+ }).end(body);
443
+ return;
444
+ }
396
445
  if (AUTH && req.method === "GET" && url.pathname.startsWith("/.well-known/oauth-protected-resource")) {
397
446
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(protectedResourceMetadata(AUTH)));
398
447
  return;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-BVx86BgG.mjs";
2
+ import { n as buildServer } from "./src-B4pIPf3c.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
  import { parseArgs } from "node:util";
@@ -1364,6 +1364,22 @@ const getEmailBroadcastRecipients = (options) => (options.client ?? client).get(
1364
1364
  ...options
1365
1365
  });
1366
1366
  /**
1367
+ * List Email Configs
1368
+ *
1369
+ * Returns a paginated list of email configurations the current user can access (owned or shared). EmailConnections admins see all configurations including other users' private ones.
1370
+ */
1371
+ const listEmailConfigs = (options) => (options.client ?? client).get({
1372
+ security: [{
1373
+ name: "x-api-key",
1374
+ type: "apiKey"
1375
+ }, {
1376
+ scheme: "bearer",
1377
+ type: "http"
1378
+ }],
1379
+ url: "/tenants/{tenant}/email-configs",
1380
+ ...options
1381
+ });
1382
+ /**
1367
1383
  * List Email History
1368
1384
  *
1369
1385
  * Returns a paginated list of emails sent for the tenant. Supports filtering by status, date range, search, and metadata fields. Metadata filters use the format metadataParam=key=value (e.g., metadataParam=entityType=Proposal).
@@ -1412,6 +1428,26 @@ const getMailboxConversation = (options) => (options.client ?? client).get({
1412
1428
  ...options
1413
1429
  });
1414
1430
  /**
1431
+ * Create Draft
1432
+ *
1433
+ * Creates a new draft via the email provider. The configuration must have ReadEnabled set to true. The returned UniqueId is used for subsequent Update/Send/Discard calls.
1434
+ */
1435
+ const createMailboxDraft = (options) => (options.client ?? client).post({
1436
+ security: [{
1437
+ name: "x-api-key",
1438
+ type: "apiKey"
1439
+ }, {
1440
+ scheme: "bearer",
1441
+ type: "http"
1442
+ }],
1443
+ url: "/tenants/{tenant}/email-configs/{code}/drafts",
1444
+ ...options,
1445
+ headers: {
1446
+ "Content-Type": "application/json",
1447
+ ...options.headers
1448
+ }
1449
+ });
1450
+ /**
1415
1451
  * Get engagement settings
1416
1452
  *
1417
1453
  * Returns the engagement settings for the specified tenant
@@ -3393,6 +3429,37 @@ var SodiumApiClient = class {
3393
3429
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `list emails for client ${clientCode}`);
3394
3430
  return data;
3395
3431
  }
3432
+ async findOwnDraftMailbox() {
3433
+ const correlationId = randomUUID();
3434
+ const userCode = await this.getCurrentTenantUserCode();
3435
+ const { data, error, response } = await listEmailConfigs({
3436
+ path: { tenant: this.tenant },
3437
+ query: {
3438
+ status: "Active",
3439
+ limit: 50,
3440
+ offset: 0
3441
+ },
3442
+ headers: { "X-Correlation-Id": correlationId },
3443
+ client: this.http
3444
+ });
3445
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list your connected mailboxes");
3446
+ const own = (data.data ?? []).filter((c) => c.tenantUser?.code === userCode && c.readEnabled && c.code);
3447
+ return own.find((c) => c.isDefault) ?? own[0];
3448
+ }
3449
+ async createMailboxDraft(mailboxCode, body) {
3450
+ const correlationId = randomUUID();
3451
+ const { data, error, response } = await createMailboxDraft({
3452
+ path: {
3453
+ tenant: this.tenant,
3454
+ code: mailboxCode
3455
+ },
3456
+ body,
3457
+ headers: { "X-Correlation-Id": correlationId },
3458
+ client: this.http
3459
+ });
3460
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "save the email draft");
3461
+ return data;
3462
+ }
3396
3463
  async listAmlClients(query = {}) {
3397
3464
  const correlationId = randomUUID();
3398
3465
  const { data, error, response } = await getAmlClients({
@@ -4052,7 +4119,7 @@ async function handleGetPracticeDetails(api) {
4052
4119
  }
4053
4120
  //#endregion
4054
4121
  //#region ../mcp-core/src/tools/list-clients.ts
4055
- const statusEnum$15 = z.enum([
4122
+ const statusEnum$16 = z.enum([
4056
4123
  "Active",
4057
4124
  "Inactive",
4058
4125
  "Prospect",
@@ -4071,7 +4138,7 @@ const typeEnum$1 = z.enum([
4071
4138
  const sortByEnum$20 = z.enum(["Name", "InternalReference"]);
4072
4139
  const ListClientsInputSchema = {
4073
4140
  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."),
4074
- status: z.array(statusEnum$15).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
4141
+ status: z.array(statusEnum$16).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
4075
4142
  type: z.array(typeEnum$1).optional().describe("Filter by organisation type. Use ['PrivateLimitedCompany', 'PublicLimitedCompany'] for 'limited companies'. Use ['LimitedLiabilityPartnership'] for LLPs. Defaults to all types if omitted."),
4076
4143
  managerCode: z.array(z.string()).optional().describe("Filter by assigned manager user codes."),
4077
4144
  partnerCode: z.array(z.string()).optional().describe("Filter by assigned partner user codes."),
@@ -4884,7 +4951,7 @@ function describeFilters$3(args) {
4884
4951
  }
4885
4952
  //#endregion
4886
4953
  //#region ../mcp-core/src/tools/list-tasks.ts
4887
- const statusEnum$14 = z.enum([
4954
+ const statusEnum$15 = z.enum([
4888
4955
  "NotStarted",
4889
4956
  "InProgress",
4890
4957
  "Blocked",
@@ -4945,7 +5012,7 @@ const stepTypeEnum$1 = z.enum([
4945
5012
  const ListTasksInputSchema = {
4946
5013
  user: z.array(z.string()).optional().describe("Filter by assigned user codes. For 'my tasks', this is the signed-in user's practice user code, which whoami shows. For another team member's tasks, pass their code. Omit to see tasks across all users (useful for practice managers)."),
4947
5014
  client: z.array(z.string()).optional().describe("Filter by client codes — tasks belonging to these specific clients only."),
4948
- status: z.array(statusEnum$14).optional().describe("Filter by task status. Typical: ['NotStarted', 'InProgress'] to exclude completed/skipped. Leave empty for all statuses. If the query includes NotStarted (the default for new tasks), it also needs one of: dateRange, isOverdue=true, or restrict status to non-NotStarted values only — otherwise the API rejects the call to prevent unbounded queries."),
5015
+ status: z.array(statusEnum$15).optional().describe("Filter by task status. Typical: ['NotStarted', 'InProgress'] to exclude completed/skipped. Leave empty for all statuses. If the query includes NotStarted (the default for new tasks), it also needs one of: dateRange, isOverdue=true, or restrict status to non-NotStarted values only — otherwise the API rejects the call to prevent unbounded queries."),
4949
5016
  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."),
4950
5017
  dateRange: dateRangeEnum$2.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."),
4951
5018
  startDate: z.string().optional().describe("Start of custom date range (YYYY-MM-DD). Used when dateRange='CustomDateRange'. Both startDate and endDate with a narrow window perform best — broad ranges strain the API. If startDate > endDate the API swaps them silently. Maximum allowed total range is 2 years."),
@@ -5045,7 +5112,7 @@ function describeFilters$2(args) {
5045
5112
  }
5046
5113
  //#endregion
5047
5114
  //#region ../mcp-core/src/tools/list-users.ts
5048
- const statusEnum$13 = z.enum([
5115
+ const statusEnum$14 = z.enum([
5049
5116
  "Created",
5050
5117
  "Invited",
5051
5118
  "Active",
@@ -5065,7 +5132,7 @@ const sortByEnum$17 = z.enum([
5065
5132
  ]);
5066
5133
  const ListUsersInputSchema = {
5067
5134
  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)."),
5068
- status: statusEnum$13.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."),
5135
+ status: statusEnum$14.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."),
5069
5136
  systemRole: systemRoleEnum.optional().describe("Filter by system role. Admin = full access, StandardUser = normal access, Viewer = read-only. Single value."),
5070
5137
  isClientManager: z.boolean().optional().describe("Set true to return only users flagged as client managers; false to exclude them."),
5071
5138
  isPartner: z.boolean().optional().describe("Set true to return only users flagged as partners; false to exclude them. Use for 'list partners' queries."),
@@ -5632,7 +5699,7 @@ const GetServiceDeliveryReportInputSchema = {
5632
5699
  ...reportEntityFilters,
5633
5700
  ...reportPagingFields
5634
5701
  };
5635
- function formatRow$6(r) {
5702
+ function formatRow$8(r) {
5636
5703
  const statutory = (r.statutoryCount ?? 0) > 0 ? ` · statutory ${fmtPct(r.statutoryOnTimePct)} (of ${r.statutoryCount})` : "";
5637
5704
  const worst = fmtRef(r.worstTask);
5638
5705
  return `- ${r.groupName}: ${r.completedCount} completed · avg ${fmtDays(r.avgTurnaroundDays)} / median ${fmtDays(r.medianTurnaroundDays)} · on-time ${fmtPct(r.onTimePct)}${statutory} · worst ${r.worstCaseDays ?? 0}d${worst ? ` — ${worst}` : ""}`;
@@ -5648,7 +5715,7 @@ async function handleGetServiceDeliveryReport(api, args) {
5648
5715
  `Summary: ${s?.completedCount ?? 0} completed · avg ${fmtDays(s?.avgTurnaroundDays)} / median ${fmtDays(s?.medianTurnaroundDays)} turnaround · on-time ${fmtPct(s?.onTimePct)}` + ((s?.statutoryCount ?? 0) > 0 ? ` · statutory compliance ${fmtPct(s?.statutoryOnTimePct)} (of ${s?.statutoryCount} with statutory deadlines)` : " · no statutory deadlines in period") + ` · ${s?.overdueCount ?? 0} currently overdue (now-snapshot, ignores the window)`,
5649
5716
  "",
5650
5717
  `Breakdown by ${args.groupBy ?? "Service"} (${header}):`,
5651
- ...rows.map(formatRow$6)
5718
+ ...rows.map(formatRow$8)
5652
5719
  ];
5653
5720
  if (rows.length === 0) lines.push("(no tasks completed in the window)");
5654
5721
  if (hint) lines.push("", hint);
@@ -5723,7 +5790,7 @@ const GetDeadlineComplianceReportInputSchema = {
5723
5790
  ...reportEntityFilters,
5724
5791
  ...reportPagingFields
5725
5792
  };
5726
- function formatRow$5(r) {
5793
+ function formatRow$7(r) {
5727
5794
  const worst = fmtRef(r.worstTask);
5728
5795
  const pending = (r.pendingCount ?? 0) > 0 ? ` · ${r.pendingCount} not yet due` : "";
5729
5796
  return `- ${r.groupName}: ${r.deadlineCount} deadlines · ${r.metCount} met (${fmtPct(r.compliancePct)}) · ${r.lateCount} completed late · ${r.missedOpenCount} missed & still open${pending} · avg ${fmtDays(r.avgDaysLate)} late${worst ? ` · worst ${r.worstDaysLate}d — ${worst}` : ""}`;
@@ -5742,7 +5809,7 @@ async function handleGetDeadlineComplianceReport(api, args) {
5742
5809
  "Upcoming (now-snapshot of OPEN tasks, ignores the window): " + (report.upcomingDeadlines ?? []).map((b) => `${b.count} ${b.code === "overdue" ? "overdue" : `due in ${b.code} days`}`).join(" · "),
5743
5810
  "",
5744
5811
  `Breakdown by ${args.groupBy ?? "Service"} (${header}):`,
5745
- ...rows.map(formatRow$5)
5812
+ ...rows.map(formatRow$7)
5746
5813
  ];
5747
5814
  if (rows.length === 0) lines.push(`(no ${basis} fell in the window)`);
5748
5815
  if (hint) lines.push("", hint);
@@ -5769,7 +5836,7 @@ const GetClientResponsivenessReportInputSchema = {
5769
5836
  category: reportEntityFilters.category,
5770
5837
  ...reportPagingFields
5771
5838
  };
5772
- function formatRow$4(r) {
5839
+ function formatRow$6(r) {
5773
5840
  const worst = fmtRef(r.worstTask);
5774
5841
  return `- ${r.groupName}: ${r.stepCount} steps · avg ${fmtDays(r.avgResponseDays)} / median ${fmtDays(r.medianResponseDays)} response · ${r.totalChases} chases (${fmtPct(r.chasedPct)} of steps chased)` + (r.worstCaseDays != null ? ` · worst ${fmtDays(r.worstCaseDays)}${worst ? ` — ${worst}` : ""}` : "");
5775
5842
  }
@@ -5784,7 +5851,7 @@ async function handleGetClientResponsivenessReport(api, args) {
5784
5851
  `Summary: ${s?.stepCount ?? 0} steps · avg ${fmtDays(s?.avgResponseDays)} / median ${fmtDays(s?.medianResponseDays)} response · ${s?.totalChases ?? 0} chases (${fmtPct(s?.chasedPct)} of steps chased) · ${s?.outstandingCount ?? 0} currently waiting on a client (now-snapshot${s?.oldestOutstandingDays != null ? `, oldest ${fmtDays(s.oldestOutstandingDays)}` : ""})`,
5785
5852
  "",
5786
5853
  `Breakdown by ${args.groupBy ?? "Client"} (${header}):`,
5787
- ...rows.map(formatRow$4)
5854
+ ...rows.map(formatRow$6)
5788
5855
  ];
5789
5856
  if (rows.length === 0) lines.push("(no client-facing steps completed in the window)");
5790
5857
  if (hint) lines.push("", hint);
@@ -5807,7 +5874,7 @@ const GetTeamWorkloadReportInputSchema = {
5807
5874
  category: reportEntityFilters.category,
5808
5875
  ...reportPagingFields
5809
5876
  };
5810
- function formatRow$3(r) {
5877
+ function formatRow$5(r) {
5811
5878
  const open = `${r.openCount} open (${r.overdueCount} overdue, ${r.blockedCount} blocked)`;
5812
5879
  const done = (r.completedCount ?? 0) > 0 ? `${r.completedCount} completed · avg ${fmtDays(r.avgTurnaroundDays)} · on-time ${fmtPct(r.onTimePct)}` : "0 completed";
5813
5880
  return `- ${r.groupName}: ${open} · ${done}`;
@@ -5823,7 +5890,7 @@ async function handleGetTeamWorkloadReport(api, args) {
5823
5890
  `Summary: ${s?.openCount ?? 0} open (${s?.overdueCount ?? 0} overdue, ${s?.blockedCount ?? 0} blocked, ${s?.unassignedCount ?? 0} unassigned) · ${s?.completedCount ?? 0} completed in window · avg turnaround ${fmtDays(s?.avgTurnaroundDays)}`,
5824
5891
  "",
5825
5892
  `Breakdown by ${args.groupBy ?? "User"} (busiest first, ${header}):`,
5826
- ...rows.map(formatRow$3)
5893
+ ...rows.map(formatRow$5)
5827
5894
  ];
5828
5895
  if (rows.length === 0) lines.push("(no open or completed tasks match the filters)");
5829
5896
  if (hint) lines.push("", hint);
@@ -5847,7 +5914,7 @@ const GetWipAgeingReportInputSchema = {
5847
5914
  ...reportEntityFilters,
5848
5915
  ...reportPagingFields
5849
5916
  };
5850
- function formatRow$2(r) {
5917
+ function formatRow$4(r) {
5851
5918
  const oldest = fmtRef(r.oldestTask);
5852
5919
  return `- ${r.groupName}: ${r.count} open · avg age ${fmtDays(r.avgAgeDays)} · oldest ${r.oldestAgeDays ?? 0}d${oldest ? ` — ${oldest}` : ""} · ${r.blockedCount} blocked · ${r.overdueCount} overdue`;
5853
5920
  }
@@ -5863,7 +5930,7 @@ async function handleGetWipAgeingReport(api, args) {
5863
5930
  `Summary: ${s?.openCount ?? 0} open · ${s?.notStartedCount ?? 0} not started · ${s?.blockedCount ?? 0} blocked · ${s?.overdueCount ?? 0} overdue · avg age ${fmtDays(s?.avgAgeDays)}${s?.oldestAgeDays != null ? ` · oldest ${s.oldestAgeDays}d${oldest ? ` — ${oldest}` : ""}` : ""}`,
5864
5931
  "",
5865
5932
  `Breakdown by ${args.groupBy ?? "AgeBucket"} (${header}):`,
5866
- ...rows.map(formatRow$2)
5933
+ ...rows.map(formatRow$4)
5867
5934
  ];
5868
5935
  if (rows.length === 0) lines.push("(no open work matches the filters)");
5869
5936
  if (hint) lines.push("", hint);
@@ -6742,7 +6809,7 @@ async function handleGetCompaniesHouseProfile(api, args) {
6742
6809
  }
6743
6810
  //#endregion
6744
6811
  //#region ../mcp-core/src/tools/list-document-requests.ts
6745
- const statusEnum$12 = z.enum([
6812
+ const statusEnum$13 = z.enum([
6746
6813
  "Draft",
6747
6814
  "Open",
6748
6815
  "Submitted",
@@ -6756,7 +6823,7 @@ const sortByEnum$11 = z.enum([
6756
6823
  "Deadline"
6757
6824
  ]);
6758
6825
  const ListDocumentRequestsInputSchema = {
6759
- status: statusEnum$12.optional().describe("Filter by request status. 'Open' = sent and waiting on the client (use for 'what are we waiting on from clients?'). 'Submitted' = client responded, awaiting practice review (use for 'what needs reviewing?'). 'Draft' = not yet sent. 'Accepted'/'Rejected' = reviewed. Single value — call per status for multiple."),
6826
+ status: statusEnum$13.optional().describe("Filter by request status. 'Open' = sent and waiting on the client (use for 'what are we waiting on from clients?'). 'Submitted' = client responded, awaiting practice review (use for 'what needs reviewing?'). 'Draft' = not yet sent. 'Accepted'/'Rejected' = reviewed. Single value — call per status for multiple."),
6760
6827
  clientCode: z.string().optional().describe("Filter to one client's document requests. Get the code from list_clients."),
6761
6828
  managerCode: z.string().optional().describe("Filter by the client manager's user code — 'what are MY clients waiting to send us?' for a manager."),
6762
6829
  partnerCode: z.string().optional().describe("Filter by the client partner's user code."),
@@ -6821,6 +6888,274 @@ async function handleListDocumentRequests(api, args) {
6821
6888
  }
6822
6889
  }
6823
6890
  //#endregion
6891
+ //#region ../mcp-core/src/tools/list-overdue-requests.ts
6892
+ const PAGE_SIZE = 50;
6893
+ const MAX_PAGES = 4;
6894
+ const ListOverdueRequestsInputSchema = {
6895
+ clientCode: z.string().optional().describe("Only this client's overdue requests. Get the code from list_clients."),
6896
+ managerCode: z.string().optional().describe("Only requests for clients with this manager (user code) — 'which of Jane's clients owe us documents?'."),
6897
+ partnerCode: z.string().optional().describe("Only requests for clients with this partner (user code)."),
6898
+ taskCategoryCode: z.string().optional().describe("Only requests linked to tasks in this category. Codes come from list_task_categories."),
6899
+ limit: z.number().int().min(1).max(50).optional().describe("Most overdue requests to return. Default 20, max 50.")
6900
+ };
6901
+ function daysOverdue(deadline, today) {
6902
+ return Math.floor((Date.parse(today) - Date.parse(deadline.slice(0, 10))) / 864e5);
6903
+ }
6904
+ function formatRow$3(r, today) {
6905
+ const client = r.client?.name ? ` · ${r.client.name}${r.client.code ? ` (${r.client.code})` : ""}` : "";
6906
+ const days = daysOverdue(r.deadline, today);
6907
+ const chase = r.chase?.lastChasedDate ? `last chased ${r.chase.lastChasedDate.slice(0, 10)}` : "never chased";
6908
+ const next = r.chase?.nextChaseDate ? `, next chase ${r.chase.nextChaseDate.slice(0, 10)}` : "";
6909
+ return `- ${days} day${days === 1 ? "" : "s"} overdue · ${r.title ?? "(untitled)"} (${r.code ?? "no code"})${client} · deadline ${r.deadline.slice(0, 10)} · ${chase}${next}`;
6910
+ }
6911
+ async function handleListOverdueRequests(api, args) {
6912
+ try {
6913
+ const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
6914
+ const overdue = [];
6915
+ let scanned = 0;
6916
+ let more = false;
6917
+ for (let page = 0; page < MAX_PAGES; page++) {
6918
+ const result = await api.listDocumentRequests({
6919
+ status: "Open",
6920
+ clientCode: args.clientCode,
6921
+ managerCode: args.managerCode,
6922
+ partnerCode: args.partnerCode,
6923
+ taskCategoryCode: args.taskCategoryCode,
6924
+ sortBy: "Deadline",
6925
+ sortDesc: false,
6926
+ limit: PAGE_SIZE,
6927
+ offset: page * PAGE_SIZE
6928
+ });
6929
+ const items = result.data ?? [];
6930
+ scanned += items.length;
6931
+ let reachedNotDue = false;
6932
+ for (const r of items) if (r.deadline && r.deadline.slice(0, 10) < today) overdue.push(r);
6933
+ else if (r.deadline) {
6934
+ reachedNotDue = true;
6935
+ break;
6936
+ }
6937
+ if (reachedNotDue || !result.hasMore) break;
6938
+ if (page === MAX_PAGES - 1) more = true;
6939
+ }
6940
+ if (overdue.length === 0) return { content: [{
6941
+ type: "text",
6942
+ text: more ? `No overdue document requests found in the first ${scanned} open requests checked.` : "No overdue document requests. Every open request is within its deadline or has none."
6943
+ }] };
6944
+ overdue.sort((a, b) => a.deadline.localeCompare(b.deadline));
6945
+ const limit = args.limit ?? 20;
6946
+ const shown = overdue.slice(0, limit);
6947
+ const lines = [
6948
+ overdue.length > shown.length ? `${overdue.length} overdue document requests (showing the ${shown.length} most overdue):` : `${overdue.length} overdue document request${overdue.length === 1 ? "" : "s"}, most overdue first:`,
6949
+ "",
6950
+ ...shown.map((r) => formatRow$3(r, today))
6951
+ ];
6952
+ if (more) lines.push("", `Checked the first ${scanned} open requests only; narrow with clientCode or managerCode to be sure nothing is missed.`);
6953
+ return { content: [{
6954
+ type: "text",
6955
+ text: lines.join("\n")
6956
+ }] };
6957
+ } catch (error) {
6958
+ return {
6959
+ content: [{
6960
+ type: "text",
6961
+ text: error instanceof SodiumApiError ? `Error listing overdue requests: ${error.message} (correlation: ${error.correlationId})` : `Error listing overdue requests: ${error instanceof Error ? error.message : String(error)}`
6962
+ }],
6963
+ isError: true
6964
+ };
6965
+ }
6966
+ }
6967
+ //#endregion
6968
+ //#region ../mcp-core/src/tools/list-upcoming-key-dates.ts
6969
+ const keyDateTypeEnum = z.enum([
6970
+ "AccountsNextDue",
6971
+ "ConfirmationStatementDue",
6972
+ "NextVatReturnDueAnnual",
6973
+ "NextPayeCisFilingDue",
6974
+ "PensionReEnrolmentDate",
6975
+ "CharityAnnualReturnDue",
6976
+ "CharityAccountsDue",
6977
+ "YearEnd",
6978
+ "AccountsNextPeriodStart",
6979
+ "AccountsNextPeriodEnd",
6980
+ "ConfirmationStatementNextMadeUpTo",
6981
+ "IncorporationDate",
6982
+ "AccountsLastPeriodFiled",
6983
+ "AccountsLastPeriodStart",
6984
+ "AccountsLastPeriodEnd",
6985
+ "AccountsLastMadeUpTo",
6986
+ "LastConfirmationStatementFiled",
6987
+ "LastConfirmationStatementMadeUpTo",
6988
+ "CharityLastAnnualReturnReceived",
6989
+ "CharityLastAccountsReceived",
6990
+ "CharityFinancialYearEnd"
6991
+ ]);
6992
+ const rangeEnum = z.enum([
6993
+ "Today",
6994
+ "ThisWeek",
6995
+ "NextWeek",
6996
+ "Next7Days",
6997
+ "Next30Days",
6998
+ "ThisMonth",
6999
+ "NextMonth",
7000
+ "ThisQuarter",
7001
+ "NextQuarter",
7002
+ "ThisYear",
7003
+ "NextYear",
7004
+ "ThisTaxYear",
7005
+ "NextTaxYear",
7006
+ "CustomDateRange"
7007
+ ]);
7008
+ const statusEnum$12 = z.enum([
7009
+ "Active",
7010
+ "Inactive",
7011
+ "Prospect",
7012
+ "LostProspect"
7013
+ ]);
7014
+ const ListUpcomingKeyDatesInputSchema = {
7015
+ keyDateType: keyDateTypeEnum.describe("Which key date to look at. 'AccountsNextDue' = statutory accounts filing deadline, 'ConfirmationStatementDue' = Companies House confirmation statement, 'NextVatReturnDueAnnual' = annual-scheme VAT return, 'NextPayeCisFilingDue' = PAYE/CIS filing, 'PensionReEnrolmentDate' = auto-enrolment re-enrolment, 'CharityAnnualReturnDue' / 'CharityAccountsDue' = charity filings, 'YearEnd' = accounting year end. The quarterly and monthly VAT return dates and MTD ITSA quarter dates are calculated per client and cannot be searched this way."),
7016
+ keyDateRange: rangeEnum.optional().describe("Window the date must fall in. Defaults to Next30Days. Use 'Next7Days' for 'due this week or so', 'ThisMonth' / 'NextMonth' for month views, 'ThisQuarter' / 'NextQuarter' for quarter planning. 'CustomDateRange' needs keyDateFrom and keyDateTo."),
7017
+ keyDateFrom: z.string().optional().describe("Start of the window (YYYY-MM-DD, inclusive). Only with keyDateRange='CustomDateRange'."),
7018
+ keyDateTo: z.string().optional().describe("End of the window (YYYY-MM-DD, inclusive). Only with keyDateRange='CustomDateRange'."),
7019
+ status: z.array(statusEnum$12).optional().describe("Client statuses to include. Defaults to ['Active'] so dormant clients and prospects don't clutter the list."),
7020
+ managerCode: z.array(z.string()).optional().describe("Only clients with these managers (user codes). Use for 'Jane's clients with accounts due soon'."),
7021
+ partnerCode: z.array(z.string()).optional().describe("Only clients with these partners (user codes)."),
7022
+ teamCode: z.array(z.string()).optional().describe("Only clients assigned to these teams. Codes come from list_teams."),
7023
+ limit: z.number().int().min(0).max(50).optional().describe("Clients per page. Default 25, max 50. Pass 0 for a count only ('how many clients have accounts due next month?')."),
7024
+ offset: z.number().int().min(0).optional().describe("Number of clients to skip for pagination. Default 0.")
7025
+ };
7026
+ function formatRow$2(c, dateType) {
7027
+ const date = c.keyDates?.find((d) => d.dateType === dateType)?.date;
7028
+ return `- ${date ? date.slice(0, 10) : "no date"} · ${c.name ?? "(no name)"} (${c.code ?? "no code"})`;
7029
+ }
7030
+ async function handleListUpcomingKeyDates(api, args) {
7031
+ try {
7032
+ const range = args.keyDateRange ?? "Next30Days";
7033
+ if (range === "CustomDateRange" && (!args.keyDateFrom || !args.keyDateTo)) return {
7034
+ content: [{
7035
+ type: "text",
7036
+ text: "keyDateFrom and keyDateTo are both needed when keyDateRange is CustomDateRange."
7037
+ }],
7038
+ isError: true
7039
+ };
7040
+ const limit = args.limit ?? 25;
7041
+ const query = {
7042
+ keyDateType: args.keyDateType,
7043
+ keyDateRange: range,
7044
+ keyDateFrom: range === "CustomDateRange" ? args.keyDateFrom : void 0,
7045
+ keyDateTo: range === "CustomDateRange" ? args.keyDateTo : void 0,
7046
+ status: args.status?.length ? args.status : ["Active"],
7047
+ managerCode: args.managerCode,
7048
+ partnerCode: args.partnerCode,
7049
+ teamCode: args.teamCode,
7050
+ sortBy: "KeyDate",
7051
+ sortKeyDateType: args.keyDateType,
7052
+ limit,
7053
+ offset: args.offset
7054
+ };
7055
+ const result = await api.listClients(query);
7056
+ const items = result.data ?? [];
7057
+ const total = result.totalCount ?? items.length;
7058
+ const window = range === "CustomDateRange" ? `${args.keyDateFrom} to ${args.keyDateTo}` : range;
7059
+ if (limit === 0) return { content: [{
7060
+ type: "text",
7061
+ text: `${total} client${total === 1 ? "" : "s"} with ${args.keyDateType} in ${window}.`
7062
+ }] };
7063
+ if (items.length === 0) return { content: [{
7064
+ type: "text",
7065
+ text: `No clients have ${args.keyDateType} in ${window}.`
7066
+ }] };
7067
+ const lines = [
7068
+ total > items.length ? `${total} clients have ${args.keyDateType} in ${window} (showing ${items.length}, soonest first):` : `${items.length} client${items.length === 1 ? "" : "s"} with ${args.keyDateType} in ${window}, soonest first:`,
7069
+ "",
7070
+ ...items.map((c) => formatRow$2(c, args.keyDateType))
7071
+ ];
7072
+ if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
7073
+ return { content: [{
7074
+ type: "text",
7075
+ text: lines.join("\n")
7076
+ }] };
7077
+ } catch (error) {
7078
+ return {
7079
+ content: [{
7080
+ type: "text",
7081
+ text: error instanceof SodiumApiError ? `Error listing upcoming key dates: ${error.message} (correlation: ${error.correlationId})` : `Error listing upcoming key dates: ${error instanceof Error ? error.message : String(error)}`
7082
+ }],
7083
+ isError: true
7084
+ };
7085
+ }
7086
+ }
7087
+ //#endregion
7088
+ //#region ../mcp-core/src/tools/draft-email-to-client.ts
7089
+ const recipient = z.object({
7090
+ email: z.string().email().describe("Email address."),
7091
+ name: z.string().optional().describe("Display name (optional).")
7092
+ });
7093
+ const DraftEmailToClientInputSchema = {
7094
+ to: z.array(recipient).min(1, "At least one recipient is required").describe("Who the draft is addressed to. A client's contact emails come from get_client_summary or list_contacts."),
7095
+ subject: z.string().optional().describe("Subject line."),
7096
+ body: z.string().min(1, "Email body cannot be empty").describe("The email body as HTML (paragraphs in <p>, line breaks as <br>). The user's mailbox signature is not added automatically."),
7097
+ cc: z.array(recipient).optional().describe("CC recipients.")
7098
+ };
7099
+ const noMailbox = "I couldn't save a draft because you don't have a mailbox connected to Sodium with permission to read email. Connect one under Settings > Email in Sodium, or ask a practice admin to give you access, then try again.";
7100
+ const needsReconnect = "I couldn't save a draft because your connected mailbox needs reconnecting. Reconnect it under Settings > Email in Sodium, then try again.";
7101
+ async function handleDraftEmailToClient(api, args) {
7102
+ try {
7103
+ const mailbox = await api.findOwnDraftMailbox();
7104
+ if (!mailbox?.code) return {
7105
+ content: [{
7106
+ type: "text",
7107
+ text: noMailbox
7108
+ }],
7109
+ isError: true
7110
+ };
7111
+ let result;
7112
+ try {
7113
+ result = await api.createMailboxDraft(mailbox.code, {
7114
+ subject: args.subject,
7115
+ htmlBody: args.body,
7116
+ toRecipients: args.to,
7117
+ ccRecipients: args.cc
7118
+ });
7119
+ } catch (error) {
7120
+ if (error instanceof SodiumApiError && error.statusCode === 403) return {
7121
+ content: [{
7122
+ type: "text",
7123
+ text: noMailbox
7124
+ }],
7125
+ isError: true
7126
+ };
7127
+ throw error;
7128
+ }
7129
+ if (result.reconnectRequired) return {
7130
+ content: [{
7131
+ type: "text",
7132
+ text: needsReconnect
7133
+ }],
7134
+ isError: true
7135
+ };
7136
+ if (!result.success) return {
7137
+ content: [{
7138
+ type: "text",
7139
+ text: `The draft could not be saved: ${result.errorMessage ?? "the mail provider refused it"}.`
7140
+ }],
7141
+ isError: true
7142
+ };
7143
+ const who = args.to.map((r) => r.name ?? r.email).join(", ");
7144
+ return { content: [{
7145
+ type: "text",
7146
+ text: `Draft saved in ${mailbox.fromEmail ?? "your mailbox"}, addressed to ${who}. It has not been sent; open your Drafts folder to review and send it.`
7147
+ }] };
7148
+ } catch (error) {
7149
+ return {
7150
+ content: [{
7151
+ type: "text",
7152
+ text: error instanceof SodiumApiError ? `Error saving email draft: ${error.message} (correlation: ${error.correlationId})` : `Error saving email draft: ${error instanceof Error ? error.message : String(error)}`
7153
+ }],
7154
+ isError: true
7155
+ };
7156
+ }
7157
+ }
7158
+ //#endregion
6824
7159
  //#region ../mcp-core/src/tools/list-data-form-requests.ts
6825
7160
  const statusEnum$11 = z.enum(["PendingResponse", "ResponseReceived"]);
6826
7161
  const sortByEnum$10 = z.enum(["CreatedDate", "Status"]);
@@ -9866,6 +10201,18 @@ async function buildServer(config) {
9866
10201
  inputSchema: ListDocumentRequestsInputSchema,
9867
10202
  annotations: lookupAnnotations
9868
10203
  }, (args) => handleListDocumentRequests(api, args));
10204
+ registerTenantTool(server, api, "list_overdue_requests", {
10205
+ title: "List overdue document requests",
10206
+ description: "List document requests that have been sent to clients, are still waiting on them, and are past their deadline, most overdue first. Each row shows days overdue, the client, the deadline and chase history (last chased, next chase). Optional filters: client, manager, partner, task category. Answers 'who do we need to chase?', 'which of Jane's clients are overdue sending records?', 'is ACME overdue on anything?'. Covers document requests only; data form requests have no deadline. Requests without a deadline are never overdue. For every open request regardless of deadline, use list_document_requests.",
10207
+ inputSchema: ListOverdueRequestsInputSchema,
10208
+ annotations: lookupAnnotations
10209
+ }, (args) => handleListOverdueRequests(api, args));
10210
+ registerTenantTool(server, api, "list_upcoming_key_dates", {
10211
+ title: "List clients with a key date coming up",
10212
+ description: "List clients whose chosen key date falls inside a window, soonest first, across the whole practice. Pick the date type (accounts due, confirmation statement due, annual VAT return due, PAYE/CIS filing due, pension re-enrolment, charity returns, year end and others) and a window (Next7Days, Next30Days, ThisMonth, NextQuarter, a custom range and others). Defaults to active clients and the next 30 days. Filters: manager, partner, team, status. Answers 'whose accounts are due next month?', 'which confirmation statements are due in the next fortnight?', 'how many clients have a year end this quarter?' (limit=0). Quarterly and monthly VAT return dates and MTD ITSA quarter dates are calculated per client and cannot be searched here; get_client_summary shows them for one client. To change a date, use update_client_dates.",
10213
+ inputSchema: ListUpcomingKeyDatesInputSchema,
10214
+ annotations: lookupAnnotations
10215
+ }, (args) => handleListUpcomingKeyDates(api, args));
9869
10216
  registerTenantTool(server, api, "list_data_form_requests", {
9870
10217
  title: "List / filter data form (questionnaire) requests",
9871
10218
  description: "List data-form (questionnaire) requests sent to clients — e.g. Self Assessment questionnaires, onboarding forms — with status, client, form, category, manager/partner, search, sort, and pagination. Each row shows status, sent/submitted dates, answers awaiting review, and chase history. Answers 'who still hasn't returned the SA questionnaire?' (formCode + status=PendingResponse), 'which form responses need reviewing?' (status=ResponseReceived — check pendingReviewCount per row), 'what forms has ACME been sent?' (clientCode). Statuses: PendingResponse (waiting on client) / ResponseReceived (submitted; answers may still need review).",
@@ -10237,6 +10584,17 @@ async function buildServer(config) {
10237
10584
  openWorldHint: true
10238
10585
  }
10239
10586
  }, (args) => handleAddClientNote(api, args));
10587
+ registerWriteTool(server, api, config.context, "draft_email_to_client", {
10588
+ title: "Draft an email to a client",
10589
+ description: "Save a draft email in the signed-in user's own connected mailbox (their default mailbox if they have several). Takes recipients (email, optional name), an optional subject, an HTML body and optional CC. The draft is never sent: the user reviews and sends it from their Drafts folder. Needs the user to have a mailbox connected to Sodium with permission to read email; otherwise it says so. Recipient addresses for a client come from get_client_summary or list_contacts. Attachments are not supported.",
10590
+ inputSchema: DraftEmailToClientInputSchema,
10591
+ annotations: {
10592
+ readOnlyHint: false,
10593
+ destructiveHint: false,
10594
+ idempotentHint: false,
10595
+ openWorldHint: true
10596
+ }
10597
+ }, (args) => handleDraftEmailToClient(api, args));
10240
10598
  registerWriteTool(server, api, config.context, "update_client_custom_fields", {
10241
10599
  title: "Update custom field values on a client",
10242
10600
  description: "Set or clear custom field values on a client. Accepts a map of field codes to values. Only fields included in the map are updated — omitted fields keep their current values. Pass null to clear a field. Field codes and current values are visible in the Custom Fields section of get_client_summary. Values are validated against the field's data type (Text, Number, Date, Boolean, Select, MultiSelect) before sending — invalid values return a validation error with guidance. Use this when the user says things like 'set the referral source on ACME to Google', 'update the fee review date for Smith & Co', 'clear the VAT scheme field on Greggs'. Multiple fields can be updated in a single call.",
@@ -10284,4 +10642,284 @@ async function buildServer(config) {
10284
10642
  return server;
10285
10643
  }
10286
10644
  //#endregion
10287
- export { buildServer as t };
10645
+ //#region ../mcp-core/src/tool-groups.json
10646
+ var groups = [
10647
+ {
10648
+ "name": "Clients & contacts",
10649
+ "tools": [
10650
+ "list_clients",
10651
+ "get_client_summary",
10652
+ "list_client_notes",
10653
+ "list_client_links",
10654
+ "list_contacts",
10655
+ "get_contact",
10656
+ "get_custom_field_details",
10657
+ "list_portal_users",
10658
+ "create_client",
10659
+ "create_contact",
10660
+ "update_contact",
10661
+ "link_contact_to_client",
10662
+ "update_client_business_details",
10663
+ "update_client_dates",
10664
+ "list_upcoming_key_dates",
10665
+ "update_client_custom_fields",
10666
+ "add_client_note"
10667
+ ]
10668
+ },
10669
+ {
10670
+ "name": "Tasks & workflows",
10671
+ "tools": [
10672
+ "list_tasks",
10673
+ "get_task_context",
10674
+ "get_task_history",
10675
+ "list_recurring_tasks",
10676
+ "list_workflows",
10677
+ "get_workflow",
10678
+ "list_workflow_occurrences",
10679
+ "list_pending_workflow_steps",
10680
+ "create_task",
10681
+ "update_task",
10682
+ "add_task_note",
10683
+ "update_workflow_step",
10684
+ "add_workflow_step_note"
10685
+ ]
10686
+ },
10687
+ {
10688
+ "name": "Services & pricing",
10689
+ "tools": [
10690
+ "list_services",
10691
+ "get_service_details",
10692
+ "list_service_packages",
10693
+ "list_service_categories",
10694
+ "add_client_service",
10695
+ "apply_service_package",
10696
+ "update_client_service_stage",
10697
+ "create_service_category",
10698
+ "update_service_category"
10699
+ ]
10700
+ },
10701
+ {
10702
+ "name": "Proposals & engagements",
10703
+ "tools": [
10704
+ "list_proposals",
10705
+ "get_proposal_summary",
10706
+ "list_engagement_letters",
10707
+ "get_engagement_letter_summary",
10708
+ "get_engagement_pipeline_summary",
10709
+ "create_proposal"
10710
+ ]
10711
+ },
10712
+ {
10713
+ "name": "Documents & forms",
10714
+ "tools": [
10715
+ "list_document_requests",
10716
+ "list_overdue_requests",
10717
+ "create_document_request",
10718
+ "list_client_documents",
10719
+ "list_data_forms",
10720
+ "list_data_form_requests",
10721
+ "send_data_form"
10722
+ ]
10723
+ },
10724
+ {
10725
+ "name": "Time & billing",
10726
+ "tools": [
10727
+ "list_time_entries",
10728
+ "log_time_entry",
10729
+ "list_unbilled_time_clients",
10730
+ "list_billing_line_items",
10731
+ "list_invoice_submissions",
10732
+ "get_service_time_report",
10733
+ "get_team_time_report"
10734
+ ]
10735
+ },
10736
+ {
10737
+ "name": "Email & Communication",
10738
+ "tools": [
10739
+ "list_client_emails",
10740
+ "draft_email_to_client",
10741
+ "get_client_email_thread",
10742
+ "list_sent_emails",
10743
+ "get_sent_email",
10744
+ "list_email_broadcasts",
10745
+ "get_email_broadcast"
10746
+ ]
10747
+ },
10748
+ {
10749
+ "name": "Deadlines & reports",
10750
+ "tools": [
10751
+ "get_practice_dashboard",
10752
+ "get_deadline_compliance_report",
10753
+ "get_service_delivery_report",
10754
+ "get_service_delivery_steps",
10755
+ "get_client_responsiveness_report",
10756
+ "get_team_workload_report",
10757
+ "get_wip_ageing_report",
10758
+ "get_workflow_step_analysis"
10759
+ ]
10760
+ },
10761
+ {
10762
+ "name": "Compliance & lookups",
10763
+ "tools": [
10764
+ "list_aml_clients",
10765
+ "get_aml_client",
10766
+ "get_client_agent_authorisations",
10767
+ "get_onboarding_status",
10768
+ "search_companies_house",
10769
+ "get_companies_house_profile"
10770
+ ]
10771
+ },
10772
+ {
10773
+ "name": "Practice & setup",
10774
+ "tools": [
10775
+ "get_practice_details",
10776
+ "whoami",
10777
+ "list_practices",
10778
+ "get_usage_examples",
10779
+ "list_users",
10780
+ "list_teams",
10781
+ "list_task_categories",
10782
+ "list_saved_task_filters",
10783
+ "list_saved_client_filters",
10784
+ "create_saved_task_filter"
10785
+ ]
10786
+ }
10787
+ ];
10788
+ var summaries = {
10789
+ "list_clients": "Find and filter your clients by name, status, type, manager, service and more.",
10790
+ "get_client_summary": "A full overview of one client: details, contacts, services, key dates and tasks.",
10791
+ "list_client_notes": "Read the notes on a client.",
10792
+ "list_client_links": "See the links saved against a client, such as a website or shared folder.",
10793
+ "list_contacts": "Search contacts across the practice, or list a client's contacts.",
10794
+ "get_contact": "Full details of one contact, including linked clients.",
10795
+ "get_custom_field_details": "The definition of a custom field, including its allowed options.",
10796
+ "list_portal_users": "See who has client portal access and when they last logged in.",
10797
+ "create_client": "Create a new client or prospect.",
10798
+ "create_contact": "Create a new contact.",
10799
+ "update_contact": "Update a contact's details.",
10800
+ "link_contact_to_client": "Link an existing contact to a client and set their role.",
10801
+ "update_client_business_details": "Update a client's trading name, address, VAT and tax details.",
10802
+ "update_client_dates": "Set a client's key dates, such as year end and VAT or filing due dates.",
10803
+ "update_client_custom_fields": "Update a client's custom field values.",
10804
+ "add_client_note": "Add a note to a client.",
10805
+ "list_tasks": "Find tasks by assignee, client, status, due date, category and more.",
10806
+ "get_task_context": "Everything about one task: details, notes, workflow progress and client.",
10807
+ "get_task_history": "The audit trail of a task: who changed what, and when.",
10808
+ "list_recurring_tasks": "See the recurring task templates that generate repeating work.",
10809
+ "list_workflows": "List your workflow templates.",
10810
+ "get_workflow": "The groups and steps of one workflow template.",
10811
+ "list_workflow_occurrences": "The completed runs of a workflow, slowest first.",
10812
+ "list_pending_workflow_steps": "Workflow steps ready to be worked on now, for you, your team or nobody.",
10813
+ "create_task": "Create a task for a client or the practice.",
10814
+ "update_task": "Update a task's status, dates, assignee or details.",
10815
+ "add_task_note": "Add a note to a task.",
10816
+ "update_workflow_step": "Complete, skip, block or reassign a workflow step.",
10817
+ "add_workflow_step_note": "Add a note to one step of a task's workflow.",
10818
+ "list_services": "List the services your practice offers.",
10819
+ "get_service_details": "A service's pricing, client types, stages and authorisations.",
10820
+ "list_service_packages": "List bundled service packages and what is in them.",
10821
+ "list_service_categories": "List your service categories.",
10822
+ "add_client_service": "Put a service on a client, with frequency, price and start date.",
10823
+ "apply_service_package": "Apply a service package to a client in one go.",
10824
+ "update_client_service_stage": "Move a client's service to another stage on the board.",
10825
+ "create_service_category": "Create a service category.",
10826
+ "update_service_category": "Rename, recolour or reorder a service category.",
10827
+ "list_proposals": "List proposals by status, client or date.",
10828
+ "get_proposal_summary": "Full detail of one proposal: services, pricing and acceptance.",
10829
+ "list_engagement_letters": "List letters of engagement by status, client or date.",
10830
+ "get_engagement_letter_summary": "Full detail of one letter of engagement.",
10831
+ "get_engagement_pipeline_summary": "Counts and values of proposals by stage: what is open and what is won.",
10832
+ "create_proposal": "Draft a proposal or letter of engagement for a client.",
10833
+ "list_upcoming_key_dates": "Find clients with accounts, filings or other key dates coming up.",
10834
+ "list_overdue_requests": "See document requests that are past their deadline and need chasing.",
10835
+ "draft_email_to_client": "Save a draft email in your own mailbox, ready for you to review and send.",
10836
+ "list_document_requests": "See document requests sent to clients and what is still outstanding.",
10837
+ "create_document_request": "Draft a document request for a client.",
10838
+ "list_client_documents": "List documents held against clients and their review status.",
10839
+ "list_data_forms": "List your questionnaire templates.",
10840
+ "list_data_form_requests": "See which questionnaires were sent and who has responded.",
10841
+ "send_data_form": "Send a questionnaire to a client.",
10842
+ "list_time_entries": "List recorded time by person, client, task or date.",
10843
+ "log_time_entry": "Record time spent on a client or task.",
10844
+ "list_unbilled_time_clients": "Which clients have time recorded but not yet billed.",
10845
+ "list_billing_line_items": "List billing lines waiting for approval or invoicing.",
10846
+ "list_invoice_submissions": "See invoices sent to your accounting system, and any that failed.",
10847
+ "get_service_time_report": "Where your recorded time goes, by service or client.",
10848
+ "get_team_time_report": "Hours logged against expected hours for each team member.",
10849
+ "list_client_emails": "Email threads with a client from your synced mailbox.",
10850
+ "get_client_email_thread": "Read one email thread in full.",
10851
+ "list_sent_emails": "Emails Sodium sent for you, and whether they were delivered.",
10852
+ "get_sent_email": "Delivery detail for one sent email.",
10853
+ "list_email_broadcasts": "Your email campaigns and how far they have got.",
10854
+ "get_email_broadcast": "One campaign in full, with delivery results for each client.",
10855
+ "get_practice_dashboard": "A snapshot of what needs attention across the practice right now.",
10856
+ "get_deadline_compliance_report": "Which deadlines were met, missed or are at risk.",
10857
+ "get_service_delivery_report": "How quickly services are delivered, and how often on time.",
10858
+ "get_service_delivery_steps": "Where the time goes inside one service's workflow.",
10859
+ "get_client_responsiveness_report": "How quickly clients respond to requests, and how much chasing it takes.",
10860
+ "get_team_workload_report": "Who has too much on, and what each person completes.",
10861
+ "get_wip_ageing_report": "Open work that has been sitting untouched, and for how long.",
10862
+ "get_workflow_step_analysis": "Step-by-step timings for one workflow template.",
10863
+ "list_aml_clients": "AML and identity-check records with risk level and outstanding checks.",
10864
+ "get_aml_client": "One AML record: status, linked contacts, checks and risk assessments.",
10865
+ "get_client_agent_authorisations": "HMRC agent authorisation status for a client, by tax regime.",
10866
+ "get_onboarding_status": "Is a client ready for work: engagement, AML, authorisations and data.",
10867
+ "search_companies_house": "Search Companies House by company name.",
10868
+ "get_companies_house_profile": "The live Companies House record for a company.",
10869
+ "get_practice_details": "Your practice's details, counts, connections and settings.",
10870
+ "whoami": "Who you are signed in as, and which practice you are working in.",
10871
+ "list_practices": "The practices you belong to.",
10872
+ "get_usage_examples": "Example requests to try, grouped by topic.",
10873
+ "list_users": "Your team members and their roles.",
10874
+ "list_teams": "Your teams and who is in them.",
10875
+ "list_task_categories": "List your task categories.",
10876
+ "list_saved_task_filters": "Your saved task views.",
10877
+ "list_saved_client_filters": "Your saved client views.",
10878
+ "create_saved_task_filter": "Save a task search as a reusable view."
10879
+ };
10880
+ //#endregion
10881
+ //#region ../mcp-core/src/catalogue.ts
10882
+ const SUMMARY_MAX = 200;
10883
+ const OTHER_GROUP = "Other";
10884
+ const summaryOverrides = summaries;
10885
+ function summarise(tool) {
10886
+ const override = summaryOverrides[tool.name];
10887
+ if (override) return override;
10888
+ const text = (tool.description ?? "").replace(/\s+/g, " ").trim();
10889
+ const match = text.match(/^(.+?[.!?])(?=\s+[A-Z'"(]|$)/);
10890
+ const sentence = match ? match[1] : text;
10891
+ return sentence.length > SUMMARY_MAX ? `${sentence.slice(0, SUMMARY_MAX - 1).trimEnd()}…` : sentence;
10892
+ }
10893
+ function buildCatalogue(tools) {
10894
+ const byName = new Map(tools.map((t) => [t.name, t]));
10895
+ const placed = /* @__PURE__ */ new Set();
10896
+ const groups$1 = [];
10897
+ const toEntry = (tool) => ({
10898
+ name: tool.name,
10899
+ title: tool.title ?? tool.name,
10900
+ access: tool.annotations?.readOnlyHint === true ? "read" : "write",
10901
+ summary: summarise(tool)
10902
+ });
10903
+ for (const group of groups) {
10904
+ const entries = group.tools.map((name) => byName.get(name)).filter((t) => t !== void 0).map(toEntry);
10905
+ entries.forEach((e) => placed.add(e.name));
10906
+ if (entries.length > 0) groups$1.push({
10907
+ name: group.name,
10908
+ tools: entries
10909
+ });
10910
+ }
10911
+ const unmapped = tools.filter((t) => !placed.has(t.name)).map(toEntry);
10912
+ if (unmapped.length > 0) groups$1.push({
10913
+ name: OTHER_GROUP,
10914
+ tools: unmapped
10915
+ });
10916
+ const all = groups$1.flatMap((g) => g.tools);
10917
+ return {
10918
+ toolCount: all.length,
10919
+ readCount: all.filter((t) => t.access === "read").length,
10920
+ writeCount: all.filter((t) => t.access === "write").length,
10921
+ groups: groups$1
10922
+ };
10923
+ }
10924
+ //#endregion
10925
+ export { buildServer as n, buildCatalogue as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.6357",
3
+ "version": "0.1.0-beta.6554",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {