@sodiumhq/mcp-pm 0.1.0-beta.4489 → 0.1.0-beta.4707

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
@@ -153,6 +153,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
153
153
  - **`update_task`** — change a task's status, dates, assignment, or details. Only the fields you mention change; everything else is preserved. "Mark TSK-123 complete", "push the deadline to Friday", "reassign it to Jane."
154
154
  - **`update_workflow_step`** — complete, skip, block, or reassign a single workflow step on a task. "Mark the review step as done", "skip the client-approval step — they confirmed by phone."
155
155
  - **`add_client_service`** — put a client on a catalogue service with frequency, pricing, and start date. "Put ACME on monthly bookkeeping."
156
+ - **`apply_service_package`** — put a client on one of the practice's bundled packages in a single call; contents and pricing follow the package definition. "Put ACME on the Growth package from 1 April."
156
157
  - **`update_client_service_stage`** — move a client's service across the kanban board. "Move ACME's VAT return to Records In."
157
158
  - **`create_document_request`** — draft (never send) a document request for a client. Sending stays in the Sodium UI.
158
159
  - **`send_data_form`** — send a questionnaire to a client. Sends immediately, so your assistant confirms with you first.
package/dist/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-Ik7PFVX1.mjs";
2
+ import { t as buildServer } from "./src-DNCbW1Ge.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { createHash, randomUUID } from "node:crypto";
5
5
  import { createServer } from "node:http";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-Ik7PFVX1.mjs";
2
+ import { t as buildServer } from "./src-DNCbW1Ge.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
5
5
  import { parseArgs } from "node:util";
@@ -1719,6 +1719,26 @@ const listServicePackages = (options) => (options.client ?? client).get({
1719
1719
  ...options
1720
1720
  });
1721
1721
  /**
1722
+ * Apply Service Package to Client
1723
+ *
1724
+ * Applies a service package to a client, creating ClientBillableService records for each item in the package.
1725
+ */
1726
+ const applyServicePackageToClient = (options) => (options.client ?? client).post({
1727
+ security: [{
1728
+ name: "x-api-key",
1729
+ type: "apiKey"
1730
+ }, {
1731
+ scheme: "bearer",
1732
+ type: "http"
1733
+ }],
1734
+ url: "/tenants/{tenant}/clients/{clientCode}/apply-service-package/{packageCode}",
1735
+ ...options,
1736
+ headers: {
1737
+ "Content-Type": "application/json",
1738
+ ...options.headers
1739
+ }
1740
+ });
1741
+ /**
1722
1742
  * List BillableServices
1723
1743
  *
1724
1744
  * Lists all BillableServices for the given tenant.
@@ -3403,6 +3423,21 @@ var SodiumApiClient = class {
3403
3423
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `assign service to client ${clientCode}`);
3404
3424
  return data;
3405
3425
  }
3426
+ async applyServicePackageToClient(clientCode, packageCode, body) {
3427
+ const correlationId = randomUUID();
3428
+ const { data, error, response } = await applyServicePackageToClient({
3429
+ path: {
3430
+ tenant: this.tenant,
3431
+ clientCode,
3432
+ packageCode
3433
+ },
3434
+ body,
3435
+ headers: { "X-Correlation-Id": correlationId },
3436
+ client: this.http
3437
+ });
3438
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `apply package ${packageCode} to client ${clientCode}`);
3439
+ return data;
3440
+ }
3406
3441
  async updateClientServiceStage(clientCode, clientServiceCode, body) {
3407
3442
  const correlationId = randomUUID();
3408
3443
  const { error, response } = await updateClientBillableServiceStage({
@@ -3542,7 +3577,7 @@ var SodiumApiClient = class {
3542
3577
  //#endregion
3543
3578
  //#region ../mcp-core/src/context/instructions.ts
3544
3579
  const ROSTER_CAP = 20;
3545
- async function buildInstructions(api, writesEnabled, selection) {
3580
+ async function buildInstructions(api, ctx, selection) {
3546
3581
  const [user, tenant, practice, team] = await Promise.allSettled([
3547
3582
  api.getCurrentUser(),
3548
3583
  api.getTenantDetails(),
@@ -3592,7 +3627,8 @@ async function buildInstructions(api, writesEnabled, selection) {
3592
3627
  } else lines.push("Call list_practices to see the available practices.");
3593
3628
  } else if (selection.kind === "none") lines.push("", "This user does not belong to any usable practice. Tell them their Sodium account has no active practice and there is nothing to show.");
3594
3629
  else if (selection.kind === "auto") lines.push("", `Working in ${selection.practice.name} (${selection.practice.code}) — the only practice this user belongs to. It is selected automatically; no 'practice' argument is needed.`);
3595
- lines.push("", writesEnabled ? "Write mode: ENABLED. Create/update tools are available; destructive and bulk operations are not." : "Write mode: DISABLED. Read-only — tell the user to relaunch with --enable-writes if they want changes made.");
3630
+ const isStdio = Boolean(ctx.apiKey) && !ctx.bearerToken;
3631
+ lines.push("", ctx.writesEnabled ? "Write mode: ENABLED. Create and update tools are available; delete and bulk operations are not exposed." : isStdio ? "Write mode: DISABLED. Read-only — tell the user to restart the server with --enable-writes (or SODIUM_ENABLE_WRITES=true) if they want changes made." : "Write mode: DISABLED. Read-only — this connection was authorised for read access only. Tell the user to reconnect the Sodium connector and approve write access if they want changes made.");
3596
3632
  if (team.status === "fulfilled") {
3597
3633
  const members = team.value.data ?? [];
3598
3634
  const total = team.value.totalCount ?? members.length;
@@ -3738,7 +3774,7 @@ async function handleGetPracticeDetails(api) {
3738
3774
  }
3739
3775
  //#endregion
3740
3776
  //#region ../mcp-core/src/tools/list-clients.ts
3741
- const statusEnum$14 = z.enum([
3777
+ const statusEnum$15 = z.enum([
3742
3778
  "Active",
3743
3779
  "Inactive",
3744
3780
  "Prospect",
@@ -3757,7 +3793,7 @@ const typeEnum$1 = z.enum([
3757
3793
  const sortByEnum$19 = z.enum(["Name", "InternalReference"]);
3758
3794
  const ListClientsInputSchema = {
3759
3795
  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."),
3760
- status: z.array(statusEnum$14).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
3796
+ status: z.array(statusEnum$15).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
3761
3797
  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."),
3762
3798
  managerCode: z.array(z.string()).optional().describe("Filter by assigned manager user codes."),
3763
3799
  partnerCode: z.array(z.string()).optional().describe("Filter by assigned partner user codes."),
@@ -4544,7 +4580,7 @@ function describeFilters$3(args) {
4544
4580
  }
4545
4581
  //#endregion
4546
4582
  //#region ../mcp-core/src/tools/list-tasks.ts
4547
- const statusEnum$13 = z.enum([
4583
+ const statusEnum$14 = z.enum([
4548
4584
  "NotStarted",
4549
4585
  "InProgress",
4550
4586
  "Blocked",
@@ -4587,7 +4623,7 @@ const sortByEnum$17 = z.enum([
4587
4623
  const ListTasksInputSchema = {
4588
4624
  user: z.array(z.string()).optional().describe("Filter by assigned user codes. For 'my tasks' use the current user's code from the startup context. For another team member's tasks, pass their code. Omit to see tasks across all users (useful for practice managers)."),
4589
4625
  client: z.array(z.string()).optional().describe("Filter by client codes — tasks belonging to these specific clients only."),
4590
- status: z.array(statusEnum$13).optional().describe("Filter by task status. Typical: ['NotStarted', 'InProgress'] to exclude completed/skipped. Leave empty for all statuses. IMPORTANT: if the query includes NotStarted (the default for new tasks), you must ALSO provide one of: dateRange, isOverdue=true, or restrict status to non-NotStarted values only — otherwise the API rejects the call to prevent unbounded queries."),
4626
+ status: z.array(statusEnum$14).optional().describe("Filter by task status. Typical: ['NotStarted', 'InProgress'] to exclude completed/skipped. Leave empty for all statuses. IMPORTANT: if the query includes NotStarted (the default for new tasks), you must ALSO provide one of: dateRange, isOverdue=true, or restrict status to non-NotStarted values only — otherwise the API rejects the call to prevent unbounded queries."),
4591
4627
  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."),
4592
4628
  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."),
4593
4629
  startDate: z.string().optional().describe("Start of custom date range (YYYY-MM-DD). Used when dateRange='CustomDateRange'. Prefer specifying both startDate and endDate with a narrow window for performance — broad ranges strain the API. If startDate > endDate the API swaps them silently. Maximum allowed total range is 2 years."),
@@ -4665,7 +4701,7 @@ function describeFilters$2(args) {
4665
4701
  }
4666
4702
  //#endregion
4667
4703
  //#region ../mcp-core/src/tools/list-users.ts
4668
- const statusEnum$12 = z.enum([
4704
+ const statusEnum$13 = z.enum([
4669
4705
  "Created",
4670
4706
  "Invited",
4671
4707
  "Active",
@@ -4685,7 +4721,7 @@ const sortByEnum$16 = z.enum([
4685
4721
  ]);
4686
4722
  const ListUsersInputSchema = {
4687
4723
  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)."),
4688
- status: statusEnum$12.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."),
4724
+ 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."),
4689
4725
  systemRole: systemRoleEnum.optional().describe("Filter by system role. Admin = full access, StandardUser = normal access, Viewer = read-only. Single value."),
4690
4726
  isClientManager: z.boolean().optional().describe("Set true to return only users flagged as client managers; false to exclude them."),
4691
4727
  isPartner: z.boolean().optional().describe("Set true to return only users flagged as partners; false to exclude them. Use for 'list partners' queries."),
@@ -6199,7 +6235,7 @@ async function handleGetCompaniesHouseProfile(api, args) {
6199
6235
  }
6200
6236
  //#endregion
6201
6237
  //#region ../mcp-core/src/tools/list-document-requests.ts
6202
- const statusEnum$11 = z.enum([
6238
+ const statusEnum$12 = z.enum([
6203
6239
  "Draft",
6204
6240
  "Open",
6205
6241
  "Submitted",
@@ -6213,7 +6249,7 @@ const sortByEnum$11 = z.enum([
6213
6249
  "Deadline"
6214
6250
  ]);
6215
6251
  const ListDocumentRequestsInputSchema = {
6216
- status: statusEnum$11.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."),
6252
+ 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."),
6217
6253
  clientCode: z.string().optional().describe("Filter to one client's document requests. Get the code from list_clients."),
6218
6254
  managerCode: z.string().optional().describe("Filter by the client manager's user code — 'what are MY clients waiting to send us?' for a manager."),
6219
6255
  partnerCode: z.string().optional().describe("Filter by the client partner's user code."),
@@ -6279,10 +6315,10 @@ async function handleListDocumentRequests(api, args) {
6279
6315
  }
6280
6316
  //#endregion
6281
6317
  //#region ../mcp-core/src/tools/list-data-form-requests.ts
6282
- const statusEnum$10 = z.enum(["PendingResponse", "ResponseReceived"]);
6318
+ const statusEnum$11 = z.enum(["PendingResponse", "ResponseReceived"]);
6283
6319
  const sortByEnum$10 = z.enum(["CreatedDate", "Status"]);
6284
6320
  const ListDataFormRequestsInputSchema = {
6285
- status: statusEnum$10.optional().describe("Filter by request status. 'PendingResponse' = sent, waiting on the client to fill the form in (use for 'which questionnaires are clients sitting on?'). 'ResponseReceived' = the client submitted — answers may still need review (see pendingReviewCount in the output)."),
6321
+ status: statusEnum$11.optional().describe("Filter by request status. 'PendingResponse' = sent, waiting on the client to fill the form in (use for 'which questionnaires are clients sitting on?'). 'ResponseReceived' = the client submitted — answers may still need review (see pendingReviewCount in the output)."),
6286
6322
  clientCode: z.string().optional().describe("Filter to one client's form requests. Get the code from list_clients."),
6287
6323
  formCode: z.string().optional().describe("Filter by the data-form template code — 'who still hasn't returned the SA questionnaire?'."),
6288
6324
  categoryCode: z.string().optional().describe("Filter by the form's category code."),
@@ -6667,6 +6703,7 @@ const TOPICS = {
6667
6703
  - "Set the 'Referral source' custom field on ACME to 'Google'"
6668
6704
  - "Mark the review step on the Greggs year-end task as complete" / "Skip the client-approval step — they confirmed by phone"
6669
6705
  - "Put ACME on monthly bookkeeping at £350 from the 1st" / "Move their VAT return to 'Records In' on the board"
6706
+ - "Put Bristol Roofing on the Growth package from 1 April" — the whole bundle in one step
6670
6707
  - "Draft a document request to ACME for their year-end records" (drafted — you press send)
6671
6708
  - "Send the SA questionnaire to Greggs" (sends immediately — the assistant confirms first)
6672
6709
  - "Draft a proposal for Bristol Roofing covering bookkeeping and VAT" (drafted Unsent — you review and send)
@@ -6698,7 +6735,7 @@ async function handleGetUsageExamples(args) {
6698
6735
  }
6699
6736
  //#endregion
6700
6737
  //#region ../mcp-core/src/tools/create-task.ts
6701
- const statusEnum$9 = z.enum([
6738
+ const statusEnum$10 = z.enum([
6702
6739
  "NotStarted",
6703
6740
  "InProgress",
6704
6741
  "Blocked",
@@ -6708,7 +6745,7 @@ const statusEnum$9 = z.enum([
6708
6745
  const CreateTaskInputSchema = {
6709
6746
  name: z.string().min(1).describe("The task name — short and action-oriented, e.g. 'Chase rental schedule for year-end'."),
6710
6747
  description: z.string().optional().describe("Longer description of what needs doing. Optional."),
6711
- status: statusEnum$9.optional().describe("Initial status. Defaults to NotStarted — only override when the work is already underway (InProgress)."),
6748
+ status: statusEnum$10.optional().describe("Initial status. Defaults to NotStarted — only override when the work is already underway (InProgress)."),
6712
6749
  startDate: z.string().describe("When work should start (YYYY-MM-DD). For ad-hoc tasks, today is usually right — get today's date from the startup context."),
6713
6750
  dueDate: z.string().describe("When the task is due (YYYY-MM-DD). Must be on or after startDate."),
6714
6751
  statutoryDueDate: z.string().optional().describe("The statutory deadline (YYYY-MM-DD), for compliance tasks with a legal filing date distinct from the internal due date."),
@@ -6768,7 +6805,7 @@ async function handleCreateTask(api, args) {
6768
6805
  }
6769
6806
  //#endregion
6770
6807
  //#region ../mcp-core/src/tools/update-task.ts
6771
- const statusEnum$8 = z.enum([
6808
+ const statusEnum$9 = z.enum([
6772
6809
  "NotStarted",
6773
6810
  "InProgress",
6774
6811
  "Blocked",
@@ -6777,7 +6814,7 @@ const statusEnum$8 = z.enum([
6777
6814
  ]);
6778
6815
  const UpdateTaskInputSchema = {
6779
6816
  taskCode: z.string().min(1).describe("The code of the task to update. Get it from list_tasks or get_task_context."),
6780
- status: statusEnum$8.optional().describe("New status. 'Completed' for 'mark it done', 'InProgress' for 'I've started it', 'Blocked' when waiting on something. Note: completing a task does NOT complete its workflow steps — mention open steps to the user if get_task_context showed any."),
6817
+ status: statusEnum$9.optional().describe("New status. 'Completed' for 'mark it done', 'InProgress' for 'I've started it', 'Blocked' when waiting on something. Note: completing a task does NOT complete its workflow steps — mention open steps to the user if get_task_context showed any."),
6781
6818
  name: z.string().min(1).optional().describe("New task name. Omit to keep the current name."),
6782
6819
  description: z.string().nullable().optional().describe("New description. Pass null to clear it. Omit to keep the current one."),
6783
6820
  startDate: z.string().optional().describe("New start date (YYYY-MM-DD)."),
@@ -6893,7 +6930,7 @@ async function handleLogTimeEntry(api, args) {
6893
6930
  }
6894
6931
  //#endregion
6895
6932
  //#region ../mcp-core/src/tools/list-sent-emails.ts
6896
- const statusEnum$7 = z.enum([
6933
+ const statusEnum$8 = z.enum([
6897
6934
  "Queued",
6898
6935
  "Processing",
6899
6936
  "Sent",
@@ -6906,7 +6943,7 @@ const sortByEnum$7 = z.enum([
6906
6943
  "Status"
6907
6944
  ]);
6908
6945
  const ListSentEmailsInputSchema = {
6909
- status: statusEnum$7.optional().describe("Filter by delivery status. 'Failed' answers 'which emails didn't get through?' — the most actionable query. 'Queued'/'Processing' show what's still going out."),
6946
+ status: statusEnum$8.optional().describe("Filter by delivery status. 'Failed' answers 'which emails didn't get through?' — the most actionable query. 'Queued'/'Processing' show what's still going out."),
6910
6947
  search: z.string().optional().describe("Search in email subject and recipient email address — 'emails to john@acme.co.uk', 'the proposal email'."),
6911
6948
  fromDate: z.string().optional().describe("Only emails created on or after this date (YYYY-MM-DD). Prefer setting a window — unbounded queries scan all history."),
6912
6949
  toDate: z.string().optional().describe("Only emails created on or before this date (YYYY-MM-DD)."),
@@ -7037,8 +7074,9 @@ function formatThread(t) {
7037
7074
  const from = t.latestFromName ?? t.latestFromEmail ?? "?";
7038
7075
  const counts = `${t.messageCount ?? 0} message${t.messageCount === 1 ? "" : "s"}${t.unreadCount ? `, ${t.unreadCount} unread` : ""}`;
7039
7076
  const attach = t.hasAttachments ? " · has attachments" : "";
7077
+ const ids = `\n configCode: ${t.configCode ?? "?"} · conversationId: ${t.conversationId ?? "?"}`;
7040
7078
  const snippet = t.snippet ? `\n ${t.snippet.length > 160 ? `${t.snippet.slice(0, 160)}…` : t.snippet}` : "";
7041
- return `- ${when} · "${t.subject ?? "(no subject)"}" · latest from ${from} · ${counts}${attach}${snippet}`;
7079
+ return `- ${when} · "${t.subject ?? "(no subject)"}" · latest from ${from} · ${counts}${attach}${ids}${snippet}`;
7042
7080
  }
7043
7081
  async function handleListClientEmails(api, args) {
7044
7082
  try {
@@ -7060,6 +7098,7 @@ async function handleListClientEmails(api, args) {
7060
7098
  ...items.map(formatThread)
7061
7099
  ];
7062
7100
  if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
7101
+ lines.push("", "Use get_client_email_thread with a thread's configCode + conversationId to read the whole conversation.");
7063
7102
  return { content: [{
7064
7103
  type: "text",
7065
7104
  text: lines.join("\n")
@@ -7075,6 +7114,12 @@ async function handleListClientEmails(api, args) {
7075
7114
  }
7076
7115
  }
7077
7116
  //#endregion
7117
+ //#region ../mcp-core/src/tools/aml-common.ts
7118
+ function isAmlNotConfigured(error) {
7119
+ return error instanceof SodiumApiError && error.statusCode === 400 && /aml connection/i.test(error.message);
7120
+ }
7121
+ const AML_NOT_CONFIGURED = "This practice has no AML provider connected in Sodium, so there are no AML/KYC records to show. Records appear here once an AML connection is set up in Sodium.";
7122
+ //#endregion
7078
7123
  //#region ../mcp-core/src/tools/list-aml-clients.ts
7079
7124
  const ListAmlClientsInputSchema = {
7080
7125
  search: z.string().min(3, "Search must be at least 3 characters when provided").optional().describe("Search across AML client name, reference, or contact details. Minimum 3 characters. Use the PM client's name to find its AML record."),
@@ -7120,6 +7165,10 @@ async function handleListAmlClients(api, args) {
7120
7165
  text: lines.join("\n")
7121
7166
  }] };
7122
7167
  } catch (error) {
7168
+ if (isAmlNotConfigured(error)) return { content: [{
7169
+ type: "text",
7170
+ text: AML_NOT_CONFIGURED
7171
+ }] };
7123
7172
  return {
7124
7173
  content: [{
7125
7174
  type: "text",
@@ -7178,6 +7227,10 @@ async function handleGetAmlClient(api, args) {
7178
7227
  text: lines.join("\n")
7179
7228
  }] };
7180
7229
  } catch (error) {
7230
+ if (isAmlNotConfigured(error)) return { content: [{
7231
+ type: "text",
7232
+ text: AML_NOT_CONFIGURED
7233
+ }] };
7181
7234
  return {
7182
7235
  content: [{
7183
7236
  type: "text",
@@ -7189,7 +7242,7 @@ async function handleGetAmlClient(api, args) {
7189
7242
  }
7190
7243
  //#endregion
7191
7244
  //#region ../mcp-core/src/tools/list-billing-line-items.ts
7192
- const statusEnum$6 = z.enum([
7245
+ const statusEnum$7 = z.enum([
7193
7246
  "Pending",
7194
7247
  "Approved",
7195
7248
  "Invoiced",
@@ -7202,7 +7255,7 @@ const sortByEnum$5 = z.enum([
7202
7255
  "UnitPrice"
7203
7256
  ]);
7204
7257
  const ListBillingLineItemsInputSchema = {
7205
- status: statusEnum$6.optional().describe("Filter by line-item status. 'Pending' = generated, awaiting approval (the 'what's waiting to be billed?' question). 'Approved' = ready for invoicing. 'Invoiced' = sent to the accounting system. Single value — call per status for multiple."),
7258
+ status: statusEnum$7.optional().describe("Filter by line-item status. 'Pending' = generated, awaiting approval (the 'what's waiting to be billed?' question). 'Approved' = ready for invoicing. 'Invoiced' = sent to the accounting system. Single value — call per status for multiple."),
7206
7259
  clientCode: z.string().optional().describe("Filter to one client's billing lines — 'what are we about to bill ACME?'."),
7207
7260
  billableServiceCode: z.string().optional().describe("Filter by the billable service — 'what bookkeeping revenue is queued?'. Get codes from list_services."),
7208
7261
  clientBillableServiceCode: z.string().optional().describe("Filter by one client's specific service subscription (the code from get_client_summary's services section). Narrower than billableServiceCode."),
@@ -7265,7 +7318,7 @@ async function handleListBillingLineItems(api, args) {
7265
7318
  }
7266
7319
  //#endregion
7267
7320
  //#region ../mcp-core/src/tools/update-workflow-step.ts
7268
- const statusEnum$5 = z.enum([
7321
+ const statusEnum$6 = z.enum([
7269
7322
  "NotStarted",
7270
7323
  "InProgress",
7271
7324
  "Blocked",
@@ -7276,7 +7329,7 @@ const UpdateWorkflowStepInputSchema = {
7276
7329
  taskCode: z.string().min(1).describe("The task whose workflow step to update. Get it from list_tasks or get_task_context."),
7277
7330
  groupNumber: z.number().int().min(1).describe("The workflow group number, exactly as shown in get_task_context's workflow section."),
7278
7331
  stepNumber: z.number().int().min(1).describe("The step number within the group, exactly as shown in get_task_context."),
7279
- status: statusEnum$5.optional().describe("New step status. 'Completed' for 'mark the review step done', 'Skipped' when the step doesn't apply, 'InProgress' when starting it, 'Blocked' with blockedReason when stuck. Steps may have dependencies — if the API rejects the change, report why rather than retrying."),
7332
+ status: statusEnum$6.optional().describe("New step status. 'Completed' for 'mark the review step done', 'Skipped' when the step doesn't apply, 'InProgress' when starting it, 'Blocked' with blockedReason when stuck. Steps may have dependencies — if the API rejects the change, report why rather than retrying."),
7280
7333
  blockedReason: z.string().optional().describe("Why the step is blocked. Only meaningful with status='Blocked'."),
7281
7334
  assignedUserCode: z.string().nullable().optional().describe("Reassign the step to this user code (resolve names via the roster or list_users). Pass null to unassign."),
7282
7335
  assignedTeamCode: z.string().nullable().optional().describe("Assign the step to this team code (from list_teams), independent of the user assignment. Pass null to clear.")
@@ -7408,13 +7461,14 @@ async function handleGetClientAgentAuthorisations(api, args) {
7408
7461
  type: "text",
7409
7462
  text: `No agent authorisation records for client ${args.clientCode}. (Authorisations appear once the client has services that require them.)`
7410
7463
  }] };
7411
- const missing = auths.filter((a) => a.isRequired && a.status !== "Granted");
7464
+ const required = auths.filter((a) => a.isRequired);
7465
+ const missing = required.filter((a) => a.status !== "Granted");
7412
7466
  const lines = [
7413
7467
  `HMRC agent authorisations for ${args.clientCode}:`,
7414
7468
  "",
7415
7469
  ...auths.map(formatAuth)
7416
7470
  ];
7417
- lines.push("", missing.length === 0 ? "All required authorisations are granted." : `⚠ ${missing.length} required authorisation${missing.length === 1 ? " is" : "s are"} not yet granted: ${missing.map((a) => a.authorisationType).join(", ")}. Requesting them is done from the client's page in Sodium.`);
7471
+ lines.push("", required.length === 0 ? "None of this client's current services require an HMRC agent authorisation." : missing.length === 0 ? "All required authorisations are granted." : `⚠ ${missing.length} required authorisation${missing.length === 1 ? " is" : "s are"} not yet granted: ${missing.map((a) => a.authorisationType).join(", ")}. Requesting them is done from the client's page in Sodium.`);
7418
7472
  return { content: [{
7419
7473
  type: "text",
7420
7474
  text: lines.join("\n")
@@ -7631,7 +7685,7 @@ async function handleListClientDocuments(api, args) {
7631
7685
  }
7632
7686
  //#endregion
7633
7687
  //#region ../mcp-core/src/tools/list-data-forms.ts
7634
- const statusEnum$4 = z.enum([
7688
+ const statusEnum$5 = z.enum([
7635
7689
  "Draft",
7636
7690
  "Published",
7637
7691
  "Archived"
@@ -7639,7 +7693,7 @@ const statusEnum$4 = z.enum([
7639
7693
  const sortByEnum$2 = z.enum(["Name", "Status"]);
7640
7694
  const ListDataFormsInputSchema = {
7641
7695
  search: z.string().optional().describe("Search over form name — 'the SA questionnaire', 'onboarding form'."),
7642
- status: statusEnum$4.optional().describe("Filter by form status. 'Published' = sendable to clients (the usual filter); 'Draft' = still being built; 'Archived' = retired."),
7696
+ status: statusEnum$5.optional().describe("Filter by form status. 'Published' = sendable to clients (the usual filter); 'Draft' = still being built; 'Archived' = retired."),
7643
7697
  sortBy: sortByEnum$2.optional().describe("Field to sort by. Defaults to Name."),
7644
7698
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
7645
7699
  limit: z.number().int().min(0).max(50).optional().describe("Maximum forms per page. Default 10, max 50. Pass 0 for count-only."),
@@ -7749,7 +7803,7 @@ async function handleGetTaskHistory(api, args) {
7749
7803
  }
7750
7804
  //#endregion
7751
7805
  //#region ../mcp-core/src/tools/list-invoice-submissions.ts
7752
- const statusEnum$3 = z.enum([
7806
+ const statusEnum$4 = z.enum([
7753
7807
  "Pending",
7754
7808
  "Submitted",
7755
7809
  "Failed",
@@ -7762,7 +7816,7 @@ const sortByEnum$1 = z.enum([
7762
7816
  "TotalAmount"
7763
7817
  ]);
7764
7818
  const ListInvoiceSubmissionsInputSchema = {
7765
- status: statusEnum$3.optional().describe("Filter by status. 'Failed' answers 'which invoices didn't reach the accounting system?' (the actionable one). 'Pending' = queued to submit; 'Submitted' = in the accounting system; 'Projected' = forecast only, not yet generated."),
7819
+ status: statusEnum$4.optional().describe("Filter by status. 'Failed' answers 'which invoices didn't reach the accounting system?' (the actionable one). 'Pending' = queued to submit; 'Submitted' = in the accounting system; 'Projected' = forecast only, not yet generated."),
7766
7820
  clientCode: z.string().optional().describe("Filter to one client's invoice submissions."),
7767
7821
  billingDateFrom: z.string().optional().describe("Only submissions with billing date on or after this date (YYYY-MM-DD)."),
7768
7822
  billingDateTo: z.string().optional().describe("Only submissions with billing date on or before this date (YYYY-MM-DD)."),
@@ -7867,7 +7921,7 @@ async function handleListServicePackages(api, args) {
7867
7921
  }
7868
7922
  //#endregion
7869
7923
  //#region ../mcp-core/src/tools/list-email-broadcasts.ts
7870
- const statusEnum$2 = z.enum([
7924
+ const statusEnum$3 = z.enum([
7871
7925
  "Draft",
7872
7926
  "Queued",
7873
7927
  "Sending",
@@ -7877,7 +7931,7 @@ const statusEnum$2 = z.enum([
7877
7931
  ]);
7878
7932
  const sortByEnum = z.enum(["CreatedDate", "Status"]);
7879
7933
  const ListEmailBroadcastsInputSchema = {
7880
- status: statusEnum$2.optional().describe("Filter by broadcast status. 'Completed' for sent campaigns, 'Draft' for unsent, 'Failed' for broadcasts that errored."),
7934
+ status: statusEnum$3.optional().describe("Filter by broadcast status. 'Completed' for sent campaigns, 'Draft' for unsent, 'Failed' for broadcasts that errored."),
7881
7935
  sortBy: sortByEnum.optional().describe("Field to sort by. Defaults to CreatedDate — pair with sortDesc=true for newest first."),
7882
7936
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
7883
7937
  limit: z.number().int().min(0).max(50).optional().describe("Maximum broadcasts per page. Default 10, max 50. Pass 0 for count-only."),
@@ -8249,7 +8303,8 @@ async function handleGetOnboardingStatus(api, args) {
8249
8303
  }
8250
8304
  lines.push("(Name-matched — verify it's the right AML record.)");
8251
8305
  }
8252
- } else if (aml.status === "rejected") failures.push("AML");
8306
+ } else if (aml.status === "rejected") if (isAmlNotConfigured(aml.reason)) lines.push(AML_NOT_CONFIGURED);
8307
+ else failures.push("AML");
8253
8308
  else lines.push("(Client name too short to search the AML register.)");
8254
8309
  lines.push("", "## HMRC agent authorisations");
8255
8310
  if (auths.status === "fulfilled") {
@@ -8308,7 +8363,7 @@ const frequencyEnum = z.enum([
8308
8363
  "Quarterly",
8309
8364
  "Monthly"
8310
8365
  ]);
8311
- const statusEnum$1 = z.enum([
8366
+ const statusEnum$2 = z.enum([
8312
8367
  "Active",
8313
8368
  "Inactive",
8314
8369
  "Paused",
@@ -8320,7 +8375,7 @@ const AddClientServiceInputSchema = {
8320
8375
  billingFrequency: frequencyEnum.describe("How the service is billed to this client. Match one of the service's configured pricing options (visible in get_service_details)."),
8321
8376
  startDate: z.string().describe("When the service starts for this client (YYYY-MM-DD)."),
8322
8377
  endDate: z.string().optional().describe("Optional end date (YYYY-MM-DD). Omit for ongoing."),
8323
- status: statusEnum$1.optional().describe("Service status. Defaults to Active. Use 'Proposed' when the service is being quoted but not yet agreed."),
8378
+ status: statusEnum$2.optional().describe("Service status. Defaults to Active. Use 'Proposed' when the service is being quoted but not yet agreed."),
8324
8379
  price: z.number().min(0).optional().describe("Custom base price for this client. Only used with overridePricing=true — omit both to use the service's standard pricing."),
8325
8380
  overridePricing: z.boolean().optional().describe("true = use the custom 'price' instead of the service's standard pricing. Must be accompanied by 'price'."),
8326
8381
  pricingTierCode: z.string().optional().describe("Required when the service uses CustomTiers pricing (see get_service_details) — the tier code to apply."),
@@ -8374,6 +8429,66 @@ async function handleAddClientService(api, args) {
8374
8429
  }
8375
8430
  }
8376
8431
  //#endregion
8432
+ //#region ../mcp-core/src/tools/apply-service-package.ts
8433
+ const statusEnum$1 = z.enum([
8434
+ "Active",
8435
+ "Inactive",
8436
+ "Paused",
8437
+ "Proposed"
8438
+ ]);
8439
+ const ApplyServicePackageInputSchema = {
8440
+ clientCode: z.string().min(1).describe("The client to apply the package to. Get the code from list_clients."),
8441
+ packageCode: z.string().min(1).describe("The service package to apply. Get the code from list_service_packages — check its contents and pricing mode there first so you can tell the user what they are about to get and how it will be billed. Archived packages are rejected by the API."),
8442
+ startDate: z.string().optional().describe("When the package takes effect for this client (YYYY-MM-DD). Defaults to today. Use for 'put them on the Growth package from the 1st'."),
8443
+ status: statusEnum$1.optional().describe("Status the package is applied at. Defaults to Active. Use 'Proposed' when it is being quoted but not yet agreed — that also suppresses onboarding tasks, which are only created for Active services."),
8444
+ createOnboardingTasks: z.boolean().optional().describe("Whether to raise onboarding tasks from each service's templates. Defaults to true, which is right for a client genuinely starting the package now. Pass false when recording an arrangement that is already up and running — migrating historical data, or backdating a package the practice has been delivering for months — so the team isn't handed onboarding work for something already onboarded.")
8445
+ };
8446
+ function formatCreatedService(s) {
8447
+ const name = s.billableService?.name?.trim() || s.billableService?.code || "(unnamed service)";
8448
+ const frequency = s.billingFrequency ? ` · ${s.billingFrequency}` : "";
8449
+ return `- ${name} (${s.code ?? "no code"})${frequency}`;
8450
+ }
8451
+ async function handleApplyServicePackage(api, args) {
8452
+ try {
8453
+ const body = {
8454
+ status: args.status ?? "Active",
8455
+ startDate: args.startDate ?? null,
8456
+ createOnboardingTasks: args.createOnboardingTasks ?? true
8457
+ };
8458
+ const created = await api.applyServicePackageToClient(args.clientCode, args.packageCode, body);
8459
+ if (created.length === 0) return { content: [{
8460
+ type: "text",
8461
+ text: `Package ${args.packageCode} was applied to ${args.clientCode} but created no services — the package appears to be empty. Check its contents with list_service_packages.`
8462
+ }] };
8463
+ const packageName = created[0]?.servicePackage?.name?.trim() || args.packageCode;
8464
+ const startDate = created[0]?.startDate?.slice(0, 10);
8465
+ const status = created[0]?.status ?? args.status ?? "Active";
8466
+ const autoInvoiced = created.filter((s) => s.autoInvoice).length;
8467
+ const lines = [
8468
+ `Package applied: ${packageName} on client ${args.clientCode} — ${created.length} service${created.length === 1 ? "" : "s"} created`,
8469
+ `Status: ${status}${startDate ? ` · starting ${startDate}` : ""}`,
8470
+ "",
8471
+ ...created.map(formatCreatedService),
8472
+ "",
8473
+ "What the client is billed follows the package's own pricing setup — either a single package price or each service priced separately. Check list_service_packages for the package's pricing, or get_client_summary for the client's resulting services."
8474
+ ];
8475
+ if (args.createOnboardingTasks === false) lines.push("", "Onboarding tasks were suppressed for this application.");
8476
+ if (autoInvoiced > 0) lines.push("", `Auto-invoicing is on for ${autoInvoiced} of these services (the practice has client billing enabled), so they will generate billing lines automatically.`);
8477
+ return { content: [{
8478
+ type: "text",
8479
+ text: lines.join("\n")
8480
+ }] };
8481
+ } catch (error) {
8482
+ return {
8483
+ content: [{
8484
+ type: "text",
8485
+ text: error instanceof SodiumApiError ? `Error applying package ${args.packageCode} to ${args.clientCode}: ${error.message} (correlation: ${error.correlationId})` : `Error applying package ${args.packageCode} to ${args.clientCode}: ${error instanceof Error ? error.message : String(error)}`
8486
+ }],
8487
+ isError: true
8488
+ };
8489
+ }
8490
+ }
8491
+ //#endregion
8377
8492
  //#region ../mcp-core/src/tools/update-client-service-stage.ts
8378
8493
  const UpdateClientServiceStageInputSchema = {
8379
8494
  clientCode: z.string().min(1).describe("The client whose service to move. Get the code from list_clients."),
@@ -8810,7 +8925,7 @@ async function buildServer(config) {
8810
8925
  let instructions;
8811
8926
  if (config.includeStartupContext !== false) {
8812
8927
  const selection = api.hasActiveTenant() ? { kind: "explicit" } : await resolveTenant(api);
8813
- instructions = await buildInstructions(api, config.context.writesEnabled, selection);
8928
+ instructions = await buildInstructions(api, config.context, selection);
8814
8929
  }
8815
8930
  const server = new McpServer({
8816
8931
  name: config.serverName,
@@ -9262,7 +9377,7 @@ async function buildServer(config) {
9262
9377
  }, (args) => handleListInvoiceSubmissions(api, args));
9263
9378
  registerTenantTool(server, api, "list_service_packages", {
9264
9379
  title: "List service packages (bundled offerings)",
9265
- description: "List the practice's service packages — bundled offerings like a 'Growth package' — with contents (which services at which frequency), pricing mode (single package price vs per-service), and approximate annual value. Filters: search, contains-service (billableServiceCode array — 'which packages include bookkeeping?'), pagination. Complements list_services (individual catalogue). Applying a package to a client is done in the Sodium UI.",
9380
+ description: "List the practice's service packages — bundled offerings like a 'Growth package' — with contents (which services at which frequency), pricing mode (single package price vs per-service), and approximate annual value. Filters: search, contains-service (billableServiceCode array — 'which packages include bookkeeping?'), pagination. Complements list_services (individual catalogue). With write mode enabled, apply_service_package puts a client on one of these.",
9266
9381
  inputSchema: ListServicePackagesInputSchema,
9267
9382
  annotations: lookupAnnotations
9268
9383
  }, (args) => handleListServicePackages(api, args));
@@ -9307,13 +9422,24 @@ async function buildServer(config) {
9307
9422
  openWorldHint: true
9308
9423
  }
9309
9424
  }, (args) => handleAddClientService(api, args));
9425
+ registerWriteTool(server, api, config.context, "apply_service_package", {
9426
+ title: "Apply a service package to a client",
9427
+ description: "Put a client on one of the practice's bundled packages in a single call. The package definition decides everything: which services the client gets, at what frequency, and whether they are billed as one package price or service by service. Requires clientCode (list_clients) and packageCode (list_service_packages — check its contents and pricing mode there first so you can tell the user what they are agreeing to). Optional: startDate (defaults to today), status (defaults to Active; use 'Proposed' when quoting), and createOnboardingTasks (defaults to true; pass false when recording an arrangement that is already running, e.g. migrating or backdating). Use for 'put ACME on the Growth package', 'add the Startup bundle to Bristol Roofing from 1 April'. IMPORTANT: (1) The API does NOT check whether the client already has this package — calling twice applies it a SECOND time. Always check get_client_summary first and tell the user if it is already there rather than applying again. (2) Prefer this over repeated add_client_service calls: only this tool links the result to the package, which is what makes package pricing and package removal work in the Sodium UI. (3) Auto-invoicing follows the practice's client-billing settings; it is not controllable per call. (4) Nothing about the package can be overridden per client — use add_client_service for a bespoke arrangement. Archived packages are rejected.",
9428
+ inputSchema: ApplyServicePackageInputSchema,
9429
+ annotations: {
9430
+ readOnlyHint: false,
9431
+ destructiveHint: false,
9432
+ idempotentHint: false,
9433
+ openWorldHint: true
9434
+ }
9435
+ }, (args) => handleApplyServicePackage(api, args));
9310
9436
  registerWriteTool(server, api, config.context, "update_client_service_stage", {
9311
9437
  title: "Move a client's service to a different stage (kanban)",
9312
9438
  description: "Move one client-service card to a different workflow stage — the kanban move, from chat. Requires the client code, the client's service-subscription code (from get_client_summary's services section — NOT the catalogue service code), and the target stageCode (stage codes are in get_service_details for the underlying service). Pass stageCode=null to clear the stage. Use for 'move ACME's VAT return to Records In', 'mark the bookkeeping as Finished on the board'. Note: workflows can also set stages automatically via SetServiceStage steps — a manual move may be overridden by the next automated step.",
9313
9439
  inputSchema: UpdateClientServiceStageInputSchema,
9314
9440
  annotations: {
9315
9441
  readOnlyHint: false,
9316
- destructiveHint: false,
9442
+ destructiveHint: true,
9317
9443
  idempotentHint: true,
9318
9444
  openWorldHint: true
9319
9445
  }
@@ -9335,7 +9461,7 @@ async function buildServer(config) {
9335
9461
  inputSchema: SendDataFormInputSchema,
9336
9462
  annotations: {
9337
9463
  readOnlyHint: false,
9338
- destructiveHint: false,
9464
+ destructiveHint: true,
9339
9465
  idempotentHint: false,
9340
9466
  openWorldHint: true
9341
9467
  }
@@ -9357,7 +9483,7 @@ async function buildServer(config) {
9357
9483
  inputSchema: UpdateClientDatesInputSchema,
9358
9484
  annotations: {
9359
9485
  readOnlyHint: false,
9360
- destructiveHint: false,
9486
+ destructiveHint: true,
9361
9487
  idempotentHint: true,
9362
9488
  openWorldHint: true
9363
9489
  }
@@ -9368,7 +9494,7 @@ async function buildServer(config) {
9368
9494
  inputSchema: UpdateClientBusinessDetailsInputSchema,
9369
9495
  annotations: {
9370
9496
  readOnlyHint: false,
9371
- destructiveHint: false,
9497
+ destructiveHint: true,
9372
9498
  idempotentHint: true,
9373
9499
  openWorldHint: true
9374
9500
  }
@@ -9390,7 +9516,7 @@ async function buildServer(config) {
9390
9516
  inputSchema: UpdateWorkflowStepInputSchema,
9391
9517
  annotations: {
9392
9518
  readOnlyHint: false,
9393
- destructiveHint: false,
9519
+ destructiveHint: true,
9394
9520
  idempotentHint: true,
9395
9521
  openWorldHint: true
9396
9522
  }
@@ -9412,7 +9538,7 @@ async function buildServer(config) {
9412
9538
  inputSchema: UpdateTaskInputSchema,
9413
9539
  annotations: {
9414
9540
  readOnlyHint: false,
9415
- destructiveHint: false,
9541
+ destructiveHint: true,
9416
9542
  idempotentHint: true,
9417
9543
  openWorldHint: true
9418
9544
  }
@@ -9467,7 +9593,7 @@ async function buildServer(config) {
9467
9593
  inputSchema: SetClientCustomFieldsInputSchema,
9468
9594
  annotations: {
9469
9595
  readOnlyHint: false,
9470
- destructiveHint: false,
9596
+ destructiveHint: true,
9471
9597
  idempotentHint: true,
9472
9598
  openWorldHint: true
9473
9599
  }
@@ -9478,7 +9604,7 @@ async function buildServer(config) {
9478
9604
  inputSchema: UpdateContactInputSchema,
9479
9605
  annotations: {
9480
9606
  readOnlyHint: false,
9481
- destructiveHint: false,
9607
+ destructiveHint: true,
9482
9608
  idempotentHint: true,
9483
9609
  openWorldHint: true
9484
9610
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.4489",
3
+ "version": "0.1.0-beta.4707",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {