@sodiumhq/mcp-pm 0.1.0-beta.6359 → 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 { n as buildServer, t as buildCatalogue } from "./src-B3pnHPhY.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";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { n as buildServer } from "./src-B3pnHPhY.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.",
@@ -10303,6 +10661,7 @@ var groups = [
10303
10661
  "link_contact_to_client",
10304
10662
  "update_client_business_details",
10305
10663
  "update_client_dates",
10664
+ "list_upcoming_key_dates",
10306
10665
  "update_client_custom_fields",
10307
10666
  "add_client_note"
10308
10667
  ]
@@ -10354,6 +10713,7 @@ var groups = [
10354
10713
  "name": "Documents & forms",
10355
10714
  "tools": [
10356
10715
  "list_document_requests",
10716
+ "list_overdue_requests",
10357
10717
  "create_document_request",
10358
10718
  "list_client_documents",
10359
10719
  "list_data_forms",
@@ -10377,6 +10737,7 @@ var groups = [
10377
10737
  "name": "Email & Communication",
10378
10738
  "tools": [
10379
10739
  "list_client_emails",
10740
+ "draft_email_to_client",
10380
10741
  "get_client_email_thread",
10381
10742
  "list_sent_emails",
10382
10743
  "get_sent_email",
@@ -10469,6 +10830,9 @@ var summaries = {
10469
10830
  "get_engagement_letter_summary": "Full detail of one letter of engagement.",
10470
10831
  "get_engagement_pipeline_summary": "Counts and values of proposals by stage: what is open and what is won.",
10471
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.",
10472
10836
  "list_document_requests": "See document requests sent to clients and what is still outstanding.",
10473
10837
  "create_document_request": "Draft a document request for a client.",
10474
10838
  "list_client_documents": "List documents held against clients and their review status.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.6359",
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": {