@sodiumhq/mcp-pm 0.1.0-beta.4465 → 0.1.0-beta.4579

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
@@ -2,6 +2,8 @@
2
2
 
3
3
  Model Context Protocol (MCP) server for [Sodium Practice Management](https://sodiumhq.com). Lets AI assistants like Claude Desktop, Claude Code, Cursor, and VS Code interact with your Sodium tenant.
4
4
 
5
+ > **Using a browser assistant?** claude.ai, ChatGPT, and Gemini can connect to Sodium without installing anything — add `https://mcp.sodiumhq.com` as a custom connector and sign in with your Sodium account. This npm package is the local (stdio) alternative, authenticated with an API key.
6
+
5
7
  > **Full setup guide and example prompts:** [sodiumhq.com/features/mcp](https://sodiumhq.com/features/mcp)
6
8
 
7
9
  ## Status
@@ -151,6 +153,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
151
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."
152
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."
153
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."
154
157
  - **`update_client_service_stage`** — move a client's service across the kanban board. "Move ACME's VAT return to Records In."
155
158
  - **`create_document_request`** — draft (never send) a document request for a client. Sending stays in the Sodium UI.
156
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-uJ9liBF-.mjs";
2
+ import { t as buildServer } from "./src-dwGvEajk.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-uJ9liBF-.mjs";
2
+ import { t as buildServer } from "./src-dwGvEajk.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({
@@ -3738,7 +3773,7 @@ async function handleGetPracticeDetails(api) {
3738
3773
  }
3739
3774
  //#endregion
3740
3775
  //#region ../mcp-core/src/tools/list-clients.ts
3741
- const statusEnum$14 = z.enum([
3776
+ const statusEnum$15 = z.enum([
3742
3777
  "Active",
3743
3778
  "Inactive",
3744
3779
  "Prospect",
@@ -3757,7 +3792,7 @@ const typeEnum$1 = z.enum([
3757
3792
  const sortByEnum$19 = z.enum(["Name", "InternalReference"]);
3758
3793
  const ListClientsInputSchema = {
3759
3794
  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."),
3795
+ status: z.array(statusEnum$15).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
3761
3796
  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
3797
  managerCode: z.array(z.string()).optional().describe("Filter by assigned manager user codes."),
3763
3798
  partnerCode: z.array(z.string()).optional().describe("Filter by assigned partner user codes."),
@@ -4544,7 +4579,7 @@ function describeFilters$3(args) {
4544
4579
  }
4545
4580
  //#endregion
4546
4581
  //#region ../mcp-core/src/tools/list-tasks.ts
4547
- const statusEnum$13 = z.enum([
4582
+ const statusEnum$14 = z.enum([
4548
4583
  "NotStarted",
4549
4584
  "InProgress",
4550
4585
  "Blocked",
@@ -4587,7 +4622,7 @@ const sortByEnum$17 = z.enum([
4587
4622
  const ListTasksInputSchema = {
4588
4623
  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
4624
  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."),
4625
+ 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
4626
  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
4627
  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
4628
  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 +4700,7 @@ function describeFilters$2(args) {
4665
4700
  }
4666
4701
  //#endregion
4667
4702
  //#region ../mcp-core/src/tools/list-users.ts
4668
- const statusEnum$12 = z.enum([
4703
+ const statusEnum$13 = z.enum([
4669
4704
  "Created",
4670
4705
  "Invited",
4671
4706
  "Active",
@@ -4685,7 +4720,7 @@ const sortByEnum$16 = z.enum([
4685
4720
  ]);
4686
4721
  const ListUsersInputSchema = {
4687
4722
  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."),
4723
+ 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
4724
  systemRole: systemRoleEnum.optional().describe("Filter by system role. Admin = full access, StandardUser = normal access, Viewer = read-only. Single value."),
4690
4725
  isClientManager: z.boolean().optional().describe("Set true to return only users flagged as client managers; false to exclude them."),
4691
4726
  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 +6234,7 @@ async function handleGetCompaniesHouseProfile(api, args) {
6199
6234
  }
6200
6235
  //#endregion
6201
6236
  //#region ../mcp-core/src/tools/list-document-requests.ts
6202
- const statusEnum$11 = z.enum([
6237
+ const statusEnum$12 = z.enum([
6203
6238
  "Draft",
6204
6239
  "Open",
6205
6240
  "Submitted",
@@ -6213,7 +6248,7 @@ const sortByEnum$11 = z.enum([
6213
6248
  "Deadline"
6214
6249
  ]);
6215
6250
  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."),
6251
+ 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
6252
  clientCode: z.string().optional().describe("Filter to one client's document requests. Get the code from list_clients."),
6218
6253
  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
6254
  partnerCode: z.string().optional().describe("Filter by the client partner's user code."),
@@ -6279,10 +6314,10 @@ async function handleListDocumentRequests(api, args) {
6279
6314
  }
6280
6315
  //#endregion
6281
6316
  //#region ../mcp-core/src/tools/list-data-form-requests.ts
6282
- const statusEnum$10 = z.enum(["PendingResponse", "ResponseReceived"]);
6317
+ const statusEnum$11 = z.enum(["PendingResponse", "ResponseReceived"]);
6283
6318
  const sortByEnum$10 = z.enum(["CreatedDate", "Status"]);
6284
6319
  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)."),
6320
+ 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
6321
  clientCode: z.string().optional().describe("Filter to one client's form requests. Get the code from list_clients."),
6287
6322
  formCode: z.string().optional().describe("Filter by the data-form template code — 'who still hasn't returned the SA questionnaire?'."),
6288
6323
  categoryCode: z.string().optional().describe("Filter by the form's category code."),
@@ -6667,6 +6702,7 @@ const TOPICS = {
6667
6702
  - "Set the 'Referral source' custom field on ACME to 'Google'"
6668
6703
  - "Mark the review step on the Greggs year-end task as complete" / "Skip the client-approval step — they confirmed by phone"
6669
6704
  - "Put ACME on monthly bookkeeping at £350 from the 1st" / "Move their VAT return to 'Records In' on the board"
6705
+ - "Put Bristol Roofing on the Growth package from 1 April" — the whole bundle in one step
6670
6706
  - "Draft a document request to ACME for their year-end records" (drafted — you press send)
6671
6707
  - "Send the SA questionnaire to Greggs" (sends immediately — the assistant confirms first)
6672
6708
  - "Draft a proposal for Bristol Roofing covering bookkeeping and VAT" (drafted Unsent — you review and send)
@@ -6698,7 +6734,7 @@ async function handleGetUsageExamples(args) {
6698
6734
  }
6699
6735
  //#endregion
6700
6736
  //#region ../mcp-core/src/tools/create-task.ts
6701
- const statusEnum$9 = z.enum([
6737
+ const statusEnum$10 = z.enum([
6702
6738
  "NotStarted",
6703
6739
  "InProgress",
6704
6740
  "Blocked",
@@ -6708,7 +6744,7 @@ const statusEnum$9 = z.enum([
6708
6744
  const CreateTaskInputSchema = {
6709
6745
  name: z.string().min(1).describe("The task name — short and action-oriented, e.g. 'Chase rental schedule for year-end'."),
6710
6746
  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)."),
6747
+ status: statusEnum$10.optional().describe("Initial status. Defaults to NotStarted — only override when the work is already underway (InProgress)."),
6712
6748
  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
6749
  dueDate: z.string().describe("When the task is due (YYYY-MM-DD). Must be on or after startDate."),
6714
6750
  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 +6804,7 @@ async function handleCreateTask(api, args) {
6768
6804
  }
6769
6805
  //#endregion
6770
6806
  //#region ../mcp-core/src/tools/update-task.ts
6771
- const statusEnum$8 = z.enum([
6807
+ const statusEnum$9 = z.enum([
6772
6808
  "NotStarted",
6773
6809
  "InProgress",
6774
6810
  "Blocked",
@@ -6777,7 +6813,7 @@ const statusEnum$8 = z.enum([
6777
6813
  ]);
6778
6814
  const UpdateTaskInputSchema = {
6779
6815
  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."),
6816
+ 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
6817
  name: z.string().min(1).optional().describe("New task name. Omit to keep the current name."),
6782
6818
  description: z.string().nullable().optional().describe("New description. Pass null to clear it. Omit to keep the current one."),
6783
6819
  startDate: z.string().optional().describe("New start date (YYYY-MM-DD)."),
@@ -6893,7 +6929,7 @@ async function handleLogTimeEntry(api, args) {
6893
6929
  }
6894
6930
  //#endregion
6895
6931
  //#region ../mcp-core/src/tools/list-sent-emails.ts
6896
- const statusEnum$7 = z.enum([
6932
+ const statusEnum$8 = z.enum([
6897
6933
  "Queued",
6898
6934
  "Processing",
6899
6935
  "Sent",
@@ -6906,7 +6942,7 @@ const sortByEnum$7 = z.enum([
6906
6942
  "Status"
6907
6943
  ]);
6908
6944
  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."),
6945
+ 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
6946
  search: z.string().optional().describe("Search in email subject and recipient email address — 'emails to john@acme.co.uk', 'the proposal email'."),
6911
6947
  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
6948
  toDate: z.string().optional().describe("Only emails created on or before this date (YYYY-MM-DD)."),
@@ -7189,7 +7225,7 @@ async function handleGetAmlClient(api, args) {
7189
7225
  }
7190
7226
  //#endregion
7191
7227
  //#region ../mcp-core/src/tools/list-billing-line-items.ts
7192
- const statusEnum$6 = z.enum([
7228
+ const statusEnum$7 = z.enum([
7193
7229
  "Pending",
7194
7230
  "Approved",
7195
7231
  "Invoiced",
@@ -7202,7 +7238,7 @@ const sortByEnum$5 = z.enum([
7202
7238
  "UnitPrice"
7203
7239
  ]);
7204
7240
  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."),
7241
+ 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
7242
  clientCode: z.string().optional().describe("Filter to one client's billing lines — 'what are we about to bill ACME?'."),
7207
7243
  billableServiceCode: z.string().optional().describe("Filter by the billable service — 'what bookkeeping revenue is queued?'. Get codes from list_services."),
7208
7244
  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 +7301,7 @@ async function handleListBillingLineItems(api, args) {
7265
7301
  }
7266
7302
  //#endregion
7267
7303
  //#region ../mcp-core/src/tools/update-workflow-step.ts
7268
- const statusEnum$5 = z.enum([
7304
+ const statusEnum$6 = z.enum([
7269
7305
  "NotStarted",
7270
7306
  "InProgress",
7271
7307
  "Blocked",
@@ -7276,7 +7312,7 @@ const UpdateWorkflowStepInputSchema = {
7276
7312
  taskCode: z.string().min(1).describe("The task whose workflow step to update. Get it from list_tasks or get_task_context."),
7277
7313
  groupNumber: z.number().int().min(1).describe("The workflow group number, exactly as shown in get_task_context's workflow section."),
7278
7314
  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."),
7315
+ 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
7316
  blockedReason: z.string().optional().describe("Why the step is blocked. Only meaningful with status='Blocked'."),
7281
7317
  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
7318
  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.")
@@ -7631,7 +7667,7 @@ async function handleListClientDocuments(api, args) {
7631
7667
  }
7632
7668
  //#endregion
7633
7669
  //#region ../mcp-core/src/tools/list-data-forms.ts
7634
- const statusEnum$4 = z.enum([
7670
+ const statusEnum$5 = z.enum([
7635
7671
  "Draft",
7636
7672
  "Published",
7637
7673
  "Archived"
@@ -7639,7 +7675,7 @@ const statusEnum$4 = z.enum([
7639
7675
  const sortByEnum$2 = z.enum(["Name", "Status"]);
7640
7676
  const ListDataFormsInputSchema = {
7641
7677
  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."),
7678
+ status: statusEnum$5.optional().describe("Filter by form status. 'Published' = sendable to clients (the usual filter); 'Draft' = still being built; 'Archived' = retired."),
7643
7679
  sortBy: sortByEnum$2.optional().describe("Field to sort by. Defaults to Name."),
7644
7680
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
7645
7681
  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 +7785,7 @@ async function handleGetTaskHistory(api, args) {
7749
7785
  }
7750
7786
  //#endregion
7751
7787
  //#region ../mcp-core/src/tools/list-invoice-submissions.ts
7752
- const statusEnum$3 = z.enum([
7788
+ const statusEnum$4 = z.enum([
7753
7789
  "Pending",
7754
7790
  "Submitted",
7755
7791
  "Failed",
@@ -7762,7 +7798,7 @@ const sortByEnum$1 = z.enum([
7762
7798
  "TotalAmount"
7763
7799
  ]);
7764
7800
  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."),
7801
+ 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
7802
  clientCode: z.string().optional().describe("Filter to one client's invoice submissions."),
7767
7803
  billingDateFrom: z.string().optional().describe("Only submissions with billing date on or after this date (YYYY-MM-DD)."),
7768
7804
  billingDateTo: z.string().optional().describe("Only submissions with billing date on or before this date (YYYY-MM-DD)."),
@@ -7867,7 +7903,7 @@ async function handleListServicePackages(api, args) {
7867
7903
  }
7868
7904
  //#endregion
7869
7905
  //#region ../mcp-core/src/tools/list-email-broadcasts.ts
7870
- const statusEnum$2 = z.enum([
7906
+ const statusEnum$3 = z.enum([
7871
7907
  "Draft",
7872
7908
  "Queued",
7873
7909
  "Sending",
@@ -7877,7 +7913,7 @@ const statusEnum$2 = z.enum([
7877
7913
  ]);
7878
7914
  const sortByEnum = z.enum(["CreatedDate", "Status"]);
7879
7915
  const ListEmailBroadcastsInputSchema = {
7880
- status: statusEnum$2.optional().describe("Filter by broadcast status. 'Completed' for sent campaigns, 'Draft' for unsent, 'Failed' for broadcasts that errored."),
7916
+ status: statusEnum$3.optional().describe("Filter by broadcast status. 'Completed' for sent campaigns, 'Draft' for unsent, 'Failed' for broadcasts that errored."),
7881
7917
  sortBy: sortByEnum.optional().describe("Field to sort by. Defaults to CreatedDate — pair with sortDesc=true for newest first."),
7882
7918
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
7883
7919
  limit: z.number().int().min(0).max(50).optional().describe("Maximum broadcasts per page. Default 10, max 50. Pass 0 for count-only."),
@@ -8308,7 +8344,7 @@ const frequencyEnum = z.enum([
8308
8344
  "Quarterly",
8309
8345
  "Monthly"
8310
8346
  ]);
8311
- const statusEnum$1 = z.enum([
8347
+ const statusEnum$2 = z.enum([
8312
8348
  "Active",
8313
8349
  "Inactive",
8314
8350
  "Paused",
@@ -8320,7 +8356,7 @@ const AddClientServiceInputSchema = {
8320
8356
  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
8357
  startDate: z.string().describe("When the service starts for this client (YYYY-MM-DD)."),
8322
8358
  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."),
8359
+ status: statusEnum$2.optional().describe("Service status. Defaults to Active. Use 'Proposed' when the service is being quoted but not yet agreed."),
8324
8360
  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
8361
  overridePricing: z.boolean().optional().describe("true = use the custom 'price' instead of the service's standard pricing. Must be accompanied by 'price'."),
8326
8362
  pricingTierCode: z.string().optional().describe("Required when the service uses CustomTiers pricing (see get_service_details) — the tier code to apply."),
@@ -8374,6 +8410,66 @@ async function handleAddClientService(api, args) {
8374
8410
  }
8375
8411
  }
8376
8412
  //#endregion
8413
+ //#region ../mcp-core/src/tools/apply-service-package.ts
8414
+ const statusEnum$1 = z.enum([
8415
+ "Active",
8416
+ "Inactive",
8417
+ "Paused",
8418
+ "Proposed"
8419
+ ]);
8420
+ const ApplyServicePackageInputSchema = {
8421
+ clientCode: z.string().min(1).describe("The client to apply the package to. Get the code from list_clients."),
8422
+ 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."),
8423
+ 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'."),
8424
+ 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."),
8425
+ 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.")
8426
+ };
8427
+ function formatCreatedService(s) {
8428
+ const name = s.billableService?.name?.trim() || s.billableService?.code || "(unnamed service)";
8429
+ const frequency = s.billingFrequency ? ` · ${s.billingFrequency}` : "";
8430
+ return `- ${name} (${s.code ?? "no code"})${frequency}`;
8431
+ }
8432
+ async function handleApplyServicePackage(api, args) {
8433
+ try {
8434
+ const body = {
8435
+ status: args.status ?? "Active",
8436
+ startDate: args.startDate ?? null,
8437
+ createOnboardingTasks: args.createOnboardingTasks ?? true
8438
+ };
8439
+ const created = await api.applyServicePackageToClient(args.clientCode, args.packageCode, body);
8440
+ if (created.length === 0) return { content: [{
8441
+ type: "text",
8442
+ 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.`
8443
+ }] };
8444
+ const packageName = created[0]?.servicePackage?.name?.trim() || args.packageCode;
8445
+ const startDate = created[0]?.startDate?.slice(0, 10);
8446
+ const status = created[0]?.status ?? args.status ?? "Active";
8447
+ const autoInvoiced = created.filter((s) => s.autoInvoice).length;
8448
+ const lines = [
8449
+ `Package applied: ${packageName} on client ${args.clientCode} — ${created.length} service${created.length === 1 ? "" : "s"} created`,
8450
+ `Status: ${status}${startDate ? ` · starting ${startDate}` : ""}`,
8451
+ "",
8452
+ ...created.map(formatCreatedService),
8453
+ "",
8454
+ "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."
8455
+ ];
8456
+ if (args.createOnboardingTasks === false) lines.push("", "Onboarding tasks were suppressed for this application.");
8457
+ 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.`);
8458
+ return { content: [{
8459
+ type: "text",
8460
+ text: lines.join("\n")
8461
+ }] };
8462
+ } catch (error) {
8463
+ return {
8464
+ content: [{
8465
+ type: "text",
8466
+ 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)}`
8467
+ }],
8468
+ isError: true
8469
+ };
8470
+ }
8471
+ }
8472
+ //#endregion
8377
8473
  //#region ../mcp-core/src/tools/update-client-service-stage.ts
8378
8474
  const UpdateClientServiceStageInputSchema = {
8379
8475
  clientCode: z.string().min(1).describe("The client whose service to move. Get the code from list_clients."),
@@ -8550,7 +8646,7 @@ const dateTypeEnum = z.enum([
8550
8646
  "ConfirmationStatementNextMadeUpTo",
8551
8647
  "LastConfirmationStatementFiled",
8552
8648
  "LastConfirmationStatementMadeUpTo",
8553
- "NextVatReturnDue",
8649
+ "NextVatReturnDueAnnual",
8554
8650
  "NextVatPeriodEndQuarterly",
8555
8651
  "NextVatReturnDueQuarterly",
8556
8652
  "NextVatPeriodEndMonthly",
@@ -9262,7 +9358,7 @@ async function buildServer(config) {
9262
9358
  }, (args) => handleListInvoiceSubmissions(api, args));
9263
9359
  registerTenantTool(server, api, "list_service_packages", {
9264
9360
  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.",
9361
+ 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
9362
  inputSchema: ListServicePackagesInputSchema,
9267
9363
  annotations: lookupAnnotations
9268
9364
  }, (args) => handleListServicePackages(api, args));
@@ -9307,6 +9403,17 @@ async function buildServer(config) {
9307
9403
  openWorldHint: true
9308
9404
  }
9309
9405
  }, (args) => handleAddClientService(api, args));
9406
+ registerWriteTool(server, api, config.context, "apply_service_package", {
9407
+ title: "Apply a service package to a client",
9408
+ 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.",
9409
+ inputSchema: ApplyServicePackageInputSchema,
9410
+ annotations: {
9411
+ readOnlyHint: false,
9412
+ destructiveHint: false,
9413
+ idempotentHint: false,
9414
+ openWorldHint: true
9415
+ }
9416
+ }, (args) => handleApplyServicePackage(api, args));
9310
9417
  registerWriteTool(server, api, config.context, "update_client_service_stage", {
9311
9418
  title: "Move a client's service to a different stage (kanban)",
9312
9419
  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.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.4465",
3
+ "version": "0.1.0-beta.4579",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {