@sodiumhq/mcp-pm 0.1.0-beta.6353 → 0.1.0-beta.6355

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
@@ -168,6 +168,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
168
168
  - **`log_time_entry`** — record time after the fact, optionally against a client, task, or workflow step. "Log 2 hours on ACME for today — reviewing the draft accounts."
169
169
  - **`add_task_note`** — capture a note against a specific task. Attributed to your API user, timestamped to now.
170
170
  - **`add_client_note`** — capture a note against a client record. Same shape as `add_task_note` but scoped to the client.
171
+ - **`add_workflow_step_note`** — capture a note against one workflow step of a task, so it shows on that step in Sodium.
171
172
  - **`update_client_custom_fields`** — set or clear custom field values on a client. Accepts a map of field codes to values, validates against the field's data type (Text, Number, Date, Boolean, Select, MultiSelect). Pass null to clear a field. Field codes are discoverable via `get_client_summary`.
172
173
  - **`update_contact`** — update a contact's details: name, preferred name (used in email greetings), email, phone, mobile, date of birth, UTR, NI number, address. Requires the contact code (discoverable via `list_contacts`).
173
174
 
package/dist/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-C2ETa30c.mjs";
2
+ import { t as buildServer } from "./src--vw5xenq.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 { t as buildServer } from "./src-C2ETa30c.mjs";
2
+ import { t as buildServer } from "./src--vw5xenq.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
  import { parseArgs } from "node:util";
@@ -1904,6 +1904,26 @@ const createTaskItemNote = (options) => (options.client ?? client).post({
1904
1904
  }
1905
1905
  });
1906
1906
  /**
1907
+ * Create Note for Workflow Step
1908
+ *
1909
+ * Creates a new note for the specified workflow step.
1910
+ */
1911
+ const createStepNote = (options) => (options.client ?? client).post({
1912
+ security: [{
1913
+ name: "x-api-key",
1914
+ type: "apiKey"
1915
+ }, {
1916
+ scheme: "bearer",
1917
+ type: "http"
1918
+ }],
1919
+ url: "/tenants/{tenant}/tasks/{taskCode}/groups/{groupNumber}/steps/{stepNumber}/notes",
1920
+ ...options,
1921
+ headers: {
1922
+ "Content-Type": "application/json",
1923
+ ...options.headers
1924
+ }
1925
+ });
1926
+ /**
1907
1927
  * Get Task Workflow Groups
1908
1928
  *
1909
1929
  * Retrieves comprehensive workflow progress information for a TaskItem.
@@ -2756,6 +2776,22 @@ var SodiumApiClient = class {
2756
2776
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `add note to task ${taskCode}`);
2757
2777
  return data;
2758
2778
  }
2779
+ async createStepNote(taskCode, groupNumber, stepNumber, body) {
2780
+ const correlationId = randomUUID();
2781
+ const { data, error, response } = await createStepNote({
2782
+ path: {
2783
+ tenant: this.tenant,
2784
+ taskCode,
2785
+ groupNumber,
2786
+ stepNumber
2787
+ },
2788
+ body,
2789
+ headers: { "X-Correlation-Id": correlationId },
2790
+ client: this.http
2791
+ });
2792
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `add note to step ${groupNumber}.${stepNumber} of task ${taskCode}`);
2793
+ return data;
2794
+ }
2759
2795
  async createClientNote(clientCode, body) {
2760
2796
  const correlationId = randomUUID();
2761
2797
  const { data, error, response } = await createClientNoteForClient({
@@ -4231,7 +4267,9 @@ function format$3(input) {
4231
4267
  const svcName = s.billableService?.name ?? "(unnamed)";
4232
4268
  const freq = s.billingFrequency ? ` ${s.billingFrequency}` : "";
4233
4269
  const price = s.effectivePrice !== void 0 ? ` — ${money(s.effectivePrice, practice)}${freq}` : freq ? ` —${freq}` : "";
4234
- lines.push(`- ${svcName}${price}`);
4270
+ const svcCode = s.code ? ` (client service code: ${s.code})` : "";
4271
+ const stage = s.stage?.name ? ` · stage: ${s.stage.name}${s.stage.code ? ` (${s.stage.code})` : ""}` : "";
4272
+ lines.push(`- ${svcName}${svcCode}${price}${stage}`);
4235
4273
  }
4236
4274
  lines.push("", "--- Tasks ---");
4237
4275
  lines.push(`Overdue: ${overdueTasks.length}`);
@@ -4442,6 +4480,9 @@ function format$1(s, practice) {
4442
4480
  lines.push("", "--- Applicable Client Types ---");
4443
4481
  if (!s.clientTypes || s.clientTypes.length === 0) lines.push("All client types (no restriction configured).");
4444
4482
  else for (const ct of s.clientTypes) lines.push(`- ${ct}`);
4483
+ lines.push("", `--- Stages (${s.stages?.length ?? 0}) ---`);
4484
+ if (!s.stages || s.stages.length === 0) lines.push("No stages configured.");
4485
+ else for (const st of s.stages) lines.push(`- ${st.name ?? "(no name)"} (${st.code ?? "(no code)"})`);
4445
4486
  lines.push("", `--- HMRC Agent Authorisations (${s.agentAuthorisations?.length ?? 0}) ---`);
4446
4487
  if (!s.agentAuthorisations || s.agentAuthorisations.length === 0) lines.push("None configured.");
4447
4488
  else for (const a of s.agentAuthorisations) lines.push(`- ${a}`);
@@ -4560,11 +4601,12 @@ function format(input) {
4560
4601
  const doneSteps = task.workflowStepsComplete ?? sumBy(workflowGroups, (g) => g.completedSteps ?? 0);
4561
4602
  lines.push("", `--- Workflow progress (${doneSteps}/${totalSteps} steps complete) ---`);
4562
4603
  for (const group of workflowGroups) {
4563
- const gName = group.groupName ?? `Group ${group.groupNumber ?? "?"}`;
4604
+ const gName = group.groupName ?? "(unnamed)";
4605
+ const gNumber = group.groupNumber ?? "?";
4564
4606
  const gTotal = group.totalSteps ?? 0;
4565
4607
  const gDone = group.completedSteps ?? 0;
4566
4608
  const deadline = group.groupDeadline ? ` — deadline ${group.groupDeadline}` : "";
4567
- lines.push(`Group: ${gName} (${gDone}/${gTotal})${deadline}`);
4609
+ lines.push(`Group ${gNumber}: ${gName} (${gDone}/${gTotal})${deadline}`);
4568
4610
  for (const step of group.steps ?? []) lines.push(` ${formatStep$4(step)}`);
4569
4611
  }
4570
4612
  }
@@ -5155,6 +5197,38 @@ async function handleAddTaskNote(api, args) {
5155
5197
  }
5156
5198
  }
5157
5199
  //#endregion
5200
+ //#region ../mcp-core/src/tools/add-workflow-step-note.ts
5201
+ const AddWorkflowStepNoteInputSchema = {
5202
+ taskCode: z.string().min(1, "Task code is required").describe("The task that holds the workflow. Usually discovered via list_tasks or get_task_context."),
5203
+ groupNumber: z.number().int().min(1).describe("The workflow group number the step sits in (1-based). Shown as 'Group N' in get_task_context's workflow section."),
5204
+ stepNumber: z.number().int().min(1).describe("The step number within that group (1-based). Shown as 'Step N' under each group in get_task_context's workflow section."),
5205
+ text: z.string().min(1, "Note text cannot be empty").describe("The note body. Keep it concise and factual — something the user can scan later. The user can edit or delete the note in the UI if the wording isn't right."),
5206
+ pinnedLevel: z.number().int().min(0).max(2).optional().describe("Pin level for the note. 0 = not pinned (default), 1 = pinned to the notes section, 2 = pinned to the top of the page. Only set above 0 if the user explicitly asks for the note to be pinned.")
5207
+ };
5208
+ async function handleAddWorkflowStepNote(api, args) {
5209
+ try {
5210
+ const authorCode = await api.getCurrentTenantUserCode();
5211
+ const note = await api.createStepNote(args.taskCode, args.groupNumber, args.stepNumber, {
5212
+ text: args.text,
5213
+ date: (/* @__PURE__ */ new Date()).toISOString(),
5214
+ noteFromUserCode: authorCode,
5215
+ pinnedLevel: args.pinnedLevel ?? 0
5216
+ });
5217
+ return { content: [{
5218
+ type: "text",
5219
+ text: `Added note to step ${args.groupNumber}.${args.stepNumber} of task ${args.taskCode} (note code: ${note.code ?? "(no code)"}).`
5220
+ }] };
5221
+ } catch (error) {
5222
+ return {
5223
+ content: [{
5224
+ type: "text",
5225
+ text: error instanceof SodiumApiError ? `Error adding step note: ${error.message} (correlation: ${error.correlationId})` : `Error adding step note: ${error instanceof Error ? error.message : String(error)}`
5226
+ }],
5227
+ isError: true
5228
+ };
5229
+ }
5230
+ }
5231
+ //#endregion
5158
5232
  //#region ../mcp-core/src/tools/set-client-custom-fields.ts
5159
5233
  const SetClientCustomFieldsInputSchema = {
5160
5234
  clientCode: z.string().min(1, "Client code is required").describe("The client code (identifier). Usually discovered via list_clients or get_client_summary."),
@@ -9526,7 +9600,7 @@ async function buildServer(config) {
9526
9600
  }, (args) => handleListClients(api, args));
9527
9601
  registerTenantTool(server, api, "get_client_summary", {
9528
9602
  title: "Get a full summary of one client",
9529
- description: "Get a consolidated overview of a single client by code: identity (name, status, type, assignments), business details (company number, incorporation date, trading name, registered address, VAT status, UTR, PAYE ref), custom fields (all user-defined fields with current values, data types, and field codes — use these codes with update_client_custom_fields), key statutory dates (year-end, accounts due, VAT return due, confirmation statement due, etc. — upcoming first, then past), all contacts, active services with pricing, overdue task count + top 5, and tasks due in the next 7 days. Use this AFTER list_clients identifies the client of interest, or when the user references a specific client by code. Also answers 'when is ACME's year-end?' / 'is ACME VAT registered?' / 'what are ACME's custom fields?' in a single call. Tolerates partial failures — if one section can't be loaded, the rest is still returned with a note about what's missing.",
9603
+ description: "Get a consolidated overview of a single client by code: identity (name, status, type, assignments), business details (company number, incorporation date, trading name, registered address, VAT status, UTR, PAYE ref), custom fields (all user-defined fields with current values, data types, and field codes — use these codes with update_client_custom_fields), key statutory dates (year-end, accounts due, VAT return due, confirmation statement due, etc. — upcoming first, then past), all contacts, active services with pricing, overdue task count + top 5, and tasks due in the next 7 days. Takes a client code, as returned by list_clients or given by the user. Also answers 'when is ACME's year-end?' / 'is ACME VAT registered?' / 'what are ACME's custom fields?' in a single call. Tolerates partial failures — if one section can't be loaded, the rest is still returned with a note about what's missing.",
9530
9604
  inputSchema: GetClientSummaryInputSchema,
9531
9605
  annotations: {
9532
9606
  readOnlyHint: true,
@@ -9556,7 +9630,7 @@ async function buildServer(config) {
9556
9630
  }, (args) => handleListClientNotes(api, args));
9557
9631
  registerTenantTool(server, api, "list_tasks", {
9558
9632
  title: "List / filter tasks across the practice",
9559
- description: "List tasks with any combination of filters: assigned user(s), unassigned, client(s), the primary client's client manager / partner (portfolio views: 'tasks across the clients Jane manages'), status, overdue flag, preset date range (Today / ThisWeek / Next7Days / CustomDateRange etc), category, team, billable service, workflow, workflow step type, current step name, recurring task template, free-text search, saved filter, include-projected, include-workflow-steps (Agenda Mode), sort, and pagination. Use for: 'my tasks' (pass current user's code from startup context), 'Jane's overdue tasks' (user + isOverdue), 'tasks for ACME due this week' (client + dateRange=Next7Days + dateBasis=DueDate), 'what is the team working on this month' (dateRange=ThisMonth, no user filter). Returns up to 50 tasks per page; each row shows due date, statutory due date (when set), client, billable service (name + code), assignee, status and workflow progress. IMPORTANT constraints to avoid API errors and keep queries efficient: (1) Querying NotStarted tasks requires one of — a dateRange, isOverdue=true, or restricting status to non-NotStarted values. (2) Prefer the narrowest date range that answers the question — broad ranges (quarterly/yearly) are expensive; prefer Today / ThisWeek / Next7Days / ThisMonth over larger windows unless explicitly asked. (3) For 'oldest incomplete tasks' prefer status=['InProgress','Blocked'] with sortBy=StartDate (no date range needed), or add isOverdue=true if 'oldest overdue' is meant. (4) For 'how many X?' questions, pass limit=0 to get just the total count without fetching any task data — much cheaper than fetching a full page and counting.",
9633
+ description: "List tasks with any combination of filters: assigned user(s), unassigned, client(s), the primary client's client manager / partner (portfolio views: 'tasks across the clients Jane manages'), status, overdue flag, preset date range (Today / ThisWeek / Next7Days / CustomDateRange etc), category, team, billable service, workflow, workflow step type, current step name, recurring task template, free-text search, saved filter, include-projected, include-workflow-steps (Agenda Mode), sort, and pagination. Use for: 'my tasks' (user = the signed-in user's practice user code, which whoami shows), 'Jane's overdue tasks' (user + isOverdue), 'tasks for ACME due this week' (client + dateRange=Next7Days + dateBasis=DueDate), 'what is the team working on this month' (dateRange=ThisMonth, no user filter). Returns up to 50 tasks per page; each row shows due date, statutory due date (when set), client, billable service (name + code), assignee, status and workflow progress. Constraints that avoid API errors and keep queries efficient: (1) Querying NotStarted tasks requires one of — a dateRange, isOverdue=true, or restricting status to non-NotStarted values. (2) Narrow date ranges are cheaper: broad ranges (quarterly/yearly) are expensive, and Today / ThisWeek / Next7Days / ThisMonth answer most questions. (3) For 'oldest incomplete tasks', status=['InProgress','Blocked'] with sortBy=StartDate needs no date range; add isOverdue=true if 'oldest overdue' is meant. (4) For 'how many X?' questions, pass limit=0 to get just the total count without fetching any task data — much cheaper than fetching a full page and counting.",
9560
9634
  inputSchema: ListTasksInputSchema,
9561
9635
  annotations: {
9562
9636
  readOnlyHint: true,
@@ -9566,7 +9640,7 @@ async function buildServer(config) {
9566
9640
  }, (args) => handleListTasks(api, args));
9567
9641
  registerTenantTool(server, api, "get_task_context", {
9568
9642
  title: "Get full context for a single task",
9569
- description: "Get a consolidated view of one task by code: identity (name, code, status, overdue flag, start/due/statutory dates, time estimate, assignee, category, billable service, recurring/workflow templates), description, task-level checklist, workflow progress (groups and steps with status, assignee, and completion info — only if the task has a workflow attached), notes (pinned first, then newest first), and the parent client. Use this AFTER list_tasks identifies a task of interest, or when the user references a specific task by code. Tolerates partial failures — if notes or workflow steps can't be loaded, the task details are still returned with a note about what's missing. Only fails outright if the task itself can't be fetched (e.g. unknown code).",
9643
+ description: "Get a consolidated view of one task by code: identity (name, code, status, overdue flag, start/due/statutory dates, time estimate, assignee, category, billable service, recurring/workflow templates), description, task-level checklist, workflow progress (groups and steps with status, assignee, and completion info — only if the task has a workflow attached), notes (pinned first, then newest first), and the parent client. Takes a task code, as returned by list_tasks or given by the user. Tolerates partial failures — if notes or workflow steps can't be loaded, the task details are still returned with a note about what's missing. Only fails outright if the task itself can't be fetched (e.g. unknown code).",
9570
9644
  inputSchema: GetTaskContextInputSchema,
9571
9645
  annotations: {
9572
9646
  readOnlyHint: true,
@@ -9596,7 +9670,7 @@ async function buildServer(config) {
9596
9670
  }, (args) => handleListEngagements(api, args));
9597
9671
  registerTenantTool(server, api, "get_proposal_summary", {
9598
9672
  title: "Get full detail of one proposal",
9599
- description: "Get a consolidated view of a single proposal by engagement code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off incl. catch-up fees / total), the proposed services with pricing (and each service's catch-up fee share), acceptance record (if accepted), PDF availability (proposal + letter of engagement), email history, and the acceptance link. REQUIRES a concrete engagement code as input — do NOT call this tool unless you already have the code. If the user describes an engagement by attribute — e.g. 'the unsent proposal', 'the proposal for ACME', 'our most recent proposal', 'the rejected one' — you MUST call list_proposals FIRST with the matching filters to look up the code, then call this tool with that code. Returns identical data to get_engagement_letter_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history or the practice's catch-up fee label may be missing with a note); only fails hard if the engagement itself can't be fetched.",
9673
+ description: "Get a consolidated view of a single proposal by engagement code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off incl. catch-up fees / total), the proposed services with pricing (and each service's catch-up fee share), acceptance record (if accepted), PDF availability (proposal + letter of engagement), email history, and the acceptance link. Requires a concrete engagement code as input. list_proposals returns the codes and filters by status, client and date, which covers descriptions such as 'the unsent proposal', 'the proposal for ACME', 'our most recent proposal' or 'the rejected one'. Returns identical data to get_engagement_letter_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history or the practice's catch-up fee label may be missing with a note); only fails hard if the engagement itself can't be fetched.",
9600
9674
  inputSchema: GetEngagementSummaryInputSchema,
9601
9675
  annotations: {
9602
9676
  readOnlyHint: true,
@@ -9606,7 +9680,7 @@ async function buildServer(config) {
9606
9680
  }, (args) => handleGetEngagementSummary(api, args));
9607
9681
  registerTenantTool(server, api, "get_engagement_letter_summary", {
9608
9682
  title: "Get full detail of one letter of engagement",
9609
- description: "Get a consolidated view of a single letter of engagement by code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off incl. catch-up fees / total), services included with pricing (and each service's catch-up fee share), acceptance record (accepted date, whether accepted via portal or recorded manually), PDF availability (proposal + letter of engagement), email history, and the acceptance link. REQUIRES a concrete engagement code as input — do NOT call this tool unless you already have the code. If the user describes the engagement by attribute — e.g. 'ACME's letter of engagement', 'the most recent signed engagement', 'the live engagement for Smith & Co' — you MUST call list_engagement_letters FIRST with the matching filters to look up the code, then call this tool with that code. Returns identical data to get_proposal_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history or the practice's catch-up fee label may be missing with a note); only fails hard if the engagement itself can't be fetched.",
9683
+ description: "Get a consolidated view of a single letter of engagement by code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off incl. catch-up fees / total), services included with pricing (and each service's catch-up fee share), acceptance record (accepted date, whether accepted via portal or recorded manually), PDF availability (proposal + letter of engagement), email history, and the acceptance link. Requires a concrete engagement code as input. list_engagement_letters returns the codes and filters by status, search text and date, which covers descriptions such as 'ACME's letter of engagement', 'the most recent signed engagement' or 'the live engagement for Smith & Co'. Returns identical data to get_proposal_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history or the practice's catch-up fee label may be missing with a note); only fails hard if the engagement itself can't be fetched.",
9610
9684
  inputSchema: GetEngagementSummaryInputSchema,
9611
9685
  annotations: {
9612
9686
  readOnlyHint: true,
@@ -9626,7 +9700,7 @@ async function buildServer(config) {
9626
9700
  }, (args) => handleListServices(api, args));
9627
9701
  registerTenantTool(server, api, "get_service_details", {
9628
9702
  title: "Get a single service's full configuration",
9629
- description: "Get a consolidated view of one billable service by code: identity (name, code, category, status, VAT rate, accounting code, description, default manager, pricing mode), applicable client types (explicit list, or 'all client types' if unrestricted), HMRC agent authorisations, pricing options (one per billing frequency with base price + override counts), custom pricing tiers (only when pricingMode=CustomTiers), professional clearance request items, and pricing-factor presence (count + factor questions only — exact factor band values are intentionally not dumped; computed pricing per client lives in the proposal tools). Use this AFTER list_services identifies the service, or when the user references a specific service by code. Enables service-audit workflows like 'is Self Assessment correctly restricted to Individual clients?' and 'what clearance items do we request for new bookkeeping clients?'",
9703
+ description: "Get a consolidated view of one billable service by code: identity (name, code, category, status, VAT rate, accounting code, description, default manager, pricing mode), applicable client types (explicit list, or 'all client types' if unrestricted), HMRC agent authorisations, pricing options (one per billing frequency with base price + override counts), custom pricing tiers (only when pricingMode=CustomTiers), professional clearance request items, and pricing-factor presence (count + factor questions only — exact factor band values are intentionally not dumped; computed pricing per client lives in the proposal tools). Takes a service code, as returned by list_services or given by the user. Enables service-audit workflows like 'is Self Assessment correctly restricted to Individual clients?' and 'what clearance items do we request for new bookkeeping clients?'",
9630
9704
  inputSchema: GetServiceDetailsInputSchema,
9631
9705
  annotations: {
9632
9706
  readOnlyHint: true,
@@ -9671,13 +9745,13 @@ async function buildServer(config) {
9671
9745
  };
9672
9746
  registerTenantTool(server, api, "get_service_delivery_report", {
9673
9747
  title: "Service delivery turnaround report",
9674
- description: "Are we delivering client services as quickly as we should? Cohort = tasks COMPLETED in the window. Summary KPIs (count, avg/median turnaround from planned start to completion, on-time %, statutory compliance %, currently-overdue snapshot) plus a breakdown grouped by Service (default), Client, AssignedUser, or Month. Answers 'how fast do we turn around VAT returns?', 'which clients' work runs late?', 'is our turnaround improving month on month?' (groupBy=Month over a long window). NOT the right tool for missed deadlines — a task still open past its deadline never completed, so it isn't in this cohort; use get_deadline_compliance_report for that. For per-step bottlenecks within one service, follow up with get_service_delivery_steps. Constraints: dateRange='CustomDateRange' requires both startDate and endDate; summary always covers the whole cohort even when breakdown rows are paged.",
9748
+ description: "Are we delivering client services as quickly as we should? Cohort = tasks COMPLETED in the window. Summary KPIs (count, avg/median turnaround from planned start to completion, on-time %, statutory compliance %, currently-overdue snapshot) plus a breakdown grouped by Service (default), Client, AssignedUser, or Month. Answers 'how fast do we turn around VAT returns?', 'which clients' work runs late?', 'is our turnaround improving month on month?' (groupBy=Month over a long window). It does not cover missed deadlines — a task still open past its deadline never completed, so it isn't in this cohort; get_deadline_compliance_report covers those. For per-step bottlenecks within one service, follow up with get_service_delivery_steps. Constraints: dateRange='CustomDateRange' requires both startDate and endDate; summary always covers the whole cohort even when breakdown rows are paged.",
9675
9749
  inputSchema: GetServiceDeliveryReportInputSchema,
9676
9750
  annotations: reportAnnotations
9677
9751
  }, (args) => handleGetServiceDeliveryReport(api, args));
9678
9752
  registerTenantTool(server, api, "get_service_delivery_steps", {
9679
9753
  title: "Per-step bottlenecks for one service",
9680
- description: "Where does the time go inside one billable service's workflow? For tasks of the given service completed in the window: average elapsed days per workflow step (measured from the previous step's completion, so queue/idle time counts), skip counts, chase counts, and each step flagged client-wait (waiting on the client: document requests, approvals, confirmations, data forms) vs internal — splitting 'we were slow' from 'the client was slow'. Use AFTER get_service_delivery_report flags a slow service, or directly for 'why do accounts jobs take so long?'. Requires serviceCode (from list_services). Note: steps are compared by name across tasks, so a renamed workflow step appears as separate rows.",
9754
+ description: "Where does the time go inside one billable service's workflow? For tasks of the given service completed in the window: average elapsed days per workflow step (measured from the previous step's completion, so queue/idle time counts), skip counts, chase counts, and each step flagged client-wait (waiting on the client: document requests, approvals, confirmations, data forms) vs internal — splitting 'we were slow' from 'the client was slow'. Follows on from get_service_delivery_report when it flags a slow service, and answers 'why do accounts jobs take so long?' directly. Requires serviceCode (from list_services). Note: steps are compared by name across tasks, so a renamed workflow step appears as separate rows.",
9681
9755
  inputSchema: GetServiceDeliveryStepsInputSchema,
9682
9756
  annotations: reportAnnotations
9683
9757
  }, (args) => handleGetServiceDeliverySteps(api, args));
@@ -9740,7 +9814,7 @@ async function buildServer(config) {
9740
9814
  }, (args) => handleListWorkflows(api, args));
9741
9815
  registerTenantTool(server, api, "get_workflow", {
9742
9816
  title: "Get one workflow template's full structure",
9743
- description: "Get a single workflow template by code: the ordered groups and steps with step types (email, document request, approval, client confirmation, data form, etc.), assignment rules, auto-execute flags, dependencies, time estimates, and conditions. Answers 'what are the steps in our VAT return workflow?' and 'which steps in this workflow wait on the client?'. REQUIRES a concrete workflow code — call list_workflows first if the user named the workflow instead. For how the workflow performs in practice (durations, bottlenecks), use get_workflow_step_analysis.",
9817
+ description: "Get a single workflow template by code: the ordered groups and steps with step types (email, document request, approval, client confirmation, data form, etc.), assignment rules, auto-execute flags, dependencies, time estimates, and conditions. Answers 'what are the steps in our VAT return workflow?' and 'which steps in this workflow wait on the client?'. Requires a concrete workflow code, as listed by list_workflows. For how the workflow performs in practice (durations, bottlenecks), use get_workflow_step_analysis.",
9744
9818
  inputSchema: GetWorkflowInputSchema,
9745
9819
  annotations: lookupAnnotations
9746
9820
  }, (args) => handleGetWorkflow(api, args));
@@ -9764,25 +9838,25 @@ async function buildServer(config) {
9764
9838
  }, (args) => handleListServiceCategories(api, args));
9765
9839
  registerTenantTool(server, api, "list_saved_task_filters", {
9766
9840
  title: "List saved task views",
9767
- description: "List the user's saved task views/filters, showing each view's code AND the criteria it encodes (statuses, date range, users, teams, clients, categories, overdue/unassigned flags). Use when the user references a saved view by name ('run my Weekly Review', 'my compliance filter') — resolve the code here, then pass it as list_tasks' savedFilter. The echoed criteria also let you answer 'what does my Weekly Review view actually show?' without running it. Prefer menuOnly=true when guessing which view the user means — those are the ones they use daily.",
9841
+ description: "List the user's saved task views/filters, showing each view's code AND the criteria it encodes (statuses, date range, users, teams, clients, categories, overdue/unassigned flags). Use when the user references a saved view by name ('run my Weekly Review', 'my compliance filter') — resolve the code here, then pass it as list_tasks' savedFilter. The echoed criteria also let you answer 'what does my Weekly Review view actually show?' without running it. menuOnly=true returns only the views pinned to the user's menu, which are the ones they use daily.",
9768
9842
  inputSchema: ListSavedTaskFiltersInputSchema,
9769
9843
  annotations: lookupAnnotations
9770
9844
  }, (args) => handleListSavedTaskFilters(api, args));
9771
9845
  registerTenantTool(server, api, "list_saved_client_filters", {
9772
9846
  title: "List saved client views",
9773
- description: "List the user's saved client views/filters, showing each view's code AND the criteria it encodes (statuses, client types, managers, partners, services, teams). Use when the user references a saved view by name ('my VAT clients view') — resolve the code here, then pass it as list_clients' savedFilter. Prefer menuOnly=true when guessing which view the user means.",
9847
+ description: "List the user's saved client views/filters, showing each view's code AND the criteria it encodes (statuses, client types, managers, partners, services, teams). Use when the user references a saved view by name ('my VAT clients view') — resolve the code here, then pass it as list_clients' savedFilter. menuOnly=true returns only the views pinned to the user's menu.",
9774
9848
  inputSchema: ListSavedClientFiltersInputSchema,
9775
9849
  annotations: lookupAnnotations
9776
9850
  }, (args) => handleListSavedClientFilters(api, args));
9777
9851
  registerTenantTool(server, api, "search_companies_house", {
9778
9852
  title: "Search Companies House",
9779
- description: "Search the Companies House public register by company name (full or partial) or company number. Returns matches with number, status, type, incorporation date, and registered address — live data from the register, not limited to the practice's clients. Use for prospect due diligence ('look up Bristol Roofing'), finding a company number before get_companies_house_profile, or checking name availability. For an EXISTING client's company number, prefer get_client_summary (business details) over searching by name.",
9853
+ description: "Search the Companies House public register by company name (full or partial) or company number. Returns matches with number, status, type, incorporation date, and registered address — live data from the register, not limited to the practice's clients. Use for prospect due diligence ('look up Bristol Roofing'), finding a company number before get_companies_house_profile, or checking name availability. An existing client's company number is in get_client_summary's business details.",
9780
9854
  inputSchema: SearchCompaniesHouseInputSchema,
9781
9855
  annotations: lookupAnnotations
9782
9856
  }, (args) => handleSearchCompaniesHouse(api, args));
9783
9857
  registerTenantTool(server, api, "get_companies_house_profile", {
9784
9858
  title: "Get a company's full Companies House profile",
9785
- description: "Get the live Companies House record for one company number: legal status, incorporation date, registered office, SIC codes, accounts and confirmation-statement deadlines (with OVERDUE flags), last-filed accounts, charges/insolvency/liquidation flags, and current + resigned officers. Answers 'when are ACME's accounts due at Companies House?', 'who are the directors?', 'does this prospect have insolvency history?'. REQUIRES a company number — use search_companies_house for name lookups, or get_client_summary's business details for an existing client's number. Pass includeOfficers=false when only filing dates matter. Officers tolerate partial failure — the profile still returns if they can't be loaded.",
9859
+ description: "Get the live Companies House record for one company number: legal status, incorporation date, registered office, SIC codes, accounts and confirmation-statement deadlines (with OVERDUE flags), last-filed accounts, charges/insolvency/liquidation flags, and current + resigned officers. Answers 'when are ACME's accounts due at Companies House?', 'who are the directors?', 'does this prospect have insolvency history?'. Requires a company number: search_companies_house finds one by name, and get_client_summary's business details holds an existing client's. Pass includeOfficers=false when only filing dates matter. Officers tolerate partial failure — the profile still returns if they can't be loaded.",
9786
9860
  inputSchema: GetCompaniesHouseProfileInputSchema,
9787
9861
  annotations: lookupAnnotations
9788
9862
  }, (args) => handleGetCompaniesHouseProfile(api, args));
@@ -9800,7 +9874,7 @@ async function buildServer(config) {
9800
9874
  }, (args) => handleListDataFormRequests(api, args));
9801
9875
  registerTenantTool(server, api, "list_time_entries", {
9802
9876
  title: "List / filter time entries",
9803
- description: "List recorded time entries with filters: team member, client, task, preset date range (or CustomDateRange + fromDate/toDate), billable flag, billed flag, search, sort, pagination. Answers 'how much time have I logged this week?' (userCode = current user, dateRange=ThisWeek), 'time spent on ACME this month' (clientCode + dateRange=ThisMonth), 'what unbilled time do we have?' (isBillable=true, isBilled=false — the WIP question), 'biggest time entries on this job' (taskItemCode, sortBy=DurationMinutes, sortDesc=true). Each page also shows its summed duration. IMPORTANT: prefer a narrow dateRange — unwindowed queries scan all history. For 'how many entries?' pass limit=0. For aggregate per-service/per-client totals over a period prefer get_service_time_report — it sums the whole window in one call instead of paging entries.",
9877
+ description: "List recorded time entries with filters: team member, client, task, preset date range (or CustomDateRange + fromDate/toDate), billable flag, billed flag, search, sort, pagination. Answers 'how much time have I logged this week?' (userCode = current user, dateRange=ThisWeek), 'time spent on ACME this month' (clientCode + dateRange=ThisMonth), 'what unbilled time do we have?' (isBillable=true, isBilled=false — the WIP question), 'biggest time entries on this job' (taskItemCode, sortBy=DurationMinutes, sortDesc=true). Each page also shows its summed duration. Unwindowed queries scan all history, so a narrow dateRange is much faster. For 'how many entries?' pass limit=0. get_service_time_report gives aggregate per-service/per-client totals over a period, summing the whole window in one call instead of paging entries.",
9804
9878
  inputSchema: ListTimeEntriesInputSchema,
9805
9879
  annotations: lookupAnnotations
9806
9880
  }, (args) => handleListTimeEntries(api, args));
@@ -9824,7 +9898,7 @@ async function buildServer(config) {
9824
9898
  }, (args) => handleListRecurringTasks(api, args));
9825
9899
  registerTenantTool(server, api, "list_sent_emails", {
9826
9900
  title: "List emails Sodium sent (delivery audit trail)",
9827
- description: "List the emails Sodium sent on the practice's behalf — proposal/engagement emails, document-request chases, portal invitations, broadcasts — with delivery status (Queued/Processing/Sent/Failed/Cancelled), recipient, subject, date window, search, sort, pagination. THE tool for 'did the proposal email to ACME actually go out?', 'which emails failed this week?' (status=Failed + fromDate), 'what chases went to John?' (search on his address). This is the OUTBOUND SYSTEM audit trail only — for two-way email correspondence with a client (their replies, ad-hoc emails) use list_client_emails instead. Prefer a fromDate/toDate window; pass limit=0 for count-only. Follow up with get_sent_email for full recipient/error detail.",
9901
+ description: "List the emails Sodium sent on the practice's behalf — proposal/engagement emails, document-request chases, portal invitations, broadcasts — with delivery status (Queued/Processing/Sent/Failed/Cancelled), recipient, subject, date window, search, sort, pagination. THE tool for 'did the proposal email to ACME actually go out?', 'which emails failed this week?' (status=Failed + fromDate), 'what chases went to John?' (search on his address). This is the OUTBOUND SYSTEM audit trail only — for two-way email correspondence with a client (their replies, ad-hoc emails) use list_client_emails instead. A fromDate/toDate window keeps results small; limit=0 returns a count only. Follow up with get_sent_email for full recipient/error detail.",
9828
9902
  inputSchema: ListSentEmailsInputSchema,
9829
9903
  annotations: lookupAnnotations
9830
9904
  }, (args) => handleListSentEmails(api, args));
@@ -9890,7 +9964,7 @@ async function buildServer(config) {
9890
9964
  }, (args) => handleListUnbilledTimeClients(api, args));
9891
9965
  registerTenantTool(server, api, "get_engagement_pipeline_summary", {
9892
9966
  title: "Proposal pipeline totals (counts + values by status)",
9893
- description: "One-call aggregate of the engagement pipeline: counts and total values for Unsent / Sent / Viewed / Accepted / Rejected, plus the open pipeline (sent + viewed awaiting a decision). Answers 'what's our proposal pipeline worth?', 'how much did we win this quarter?' (dateRange=ThisQuarter, read the Accepted line), 'how much value is sitting unsigned?'. Omit the window for the all-time picture. Much cheaper than paging list_proposals and adding up — prefer this for any totals question; use list_proposals for the individual engagements. Constraints: dateRange='CustomDateRange' requires dateFrom and dateTo.",
9967
+ description: "One-call aggregate of the engagement pipeline: counts and total values for Unsent / Sent / Viewed / Accepted / Rejected, plus the open pipeline (sent + viewed awaiting a decision). Answers 'what's our proposal pipeline worth?', 'how much did we win this quarter?' (dateRange=ThisQuarter, read the Accepted line), 'how much value is sitting unsigned?'. Omit the window for the all-time picture. Much cheaper than paging list_proposals and adding up, so it suits any totals question; list_proposals lists the individual engagements. Constraints: dateRange='CustomDateRange' requires dateFrom and dateTo.",
9894
9968
  inputSchema: GetEngagementPipelineSummaryInputSchema,
9895
9969
  annotations: lookupAnnotations
9896
9970
  }, (args) => handleGetEngagementPipelineSummary(api, args));
@@ -9944,7 +10018,7 @@ async function buildServer(config) {
9944
10018
  }, (args) => handleGetEmailBroadcast(api, args));
9945
10019
  registerTenantTool(server, api, "get_client_email_thread", {
9946
10020
  title: "Read one email conversation (synced mailbox)",
9947
- description: "Drill into one email thread from the synced mailbox: every message in order with date, sender, read/attachment flags, and preview text, plus which clients and tasks the thread is linked to. Requires configCode AND conversationId — both shown per thread in list_client_emails output; never guess them. Answers 'what did ACME actually say in that thread?', 'show me the whole conversation about the VAT surcharge'. Message previews are snippets, not complete bodies — full bodies and attachments are opened in Sodium.",
10021
+ description: "Drill into one email thread from the synced mailbox: every message in order with date, sender, read/attachment flags, and preview text, plus which clients and tasks the thread is linked to. Requires configCode AND conversationId, both shown per thread in list_client_emails output. Answers 'what did ACME actually say in that thread?', 'show me the whole conversation about the VAT surcharge'. Message previews are snippets, not complete bodies — full bodies and attachments are opened in Sodium.",
9948
10022
  inputSchema: GetClientEmailThreadInputSchema,
9949
10023
  annotations: lookupAnnotations
9950
10024
  }, (args) => handleGetClientEmailThread(api, args));
@@ -9989,7 +10063,7 @@ async function buildServer(config) {
9989
10063
  }, (args) => handleUpdateServiceCategory(api, args));
9990
10064
  registerWriteTool(server, api, config.context, "apply_service_package", {
9991
10065
  title: "Apply a service package to a client",
9992
- 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.",
10066
+ 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'. Notes: (1) The API does not check whether the client already has this package, so calling twice applies it a second time; get_client_summary shows whether it is already there. (2) Unlike 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.",
9993
10067
  inputSchema: ApplyServicePackageInputSchema,
9994
10068
  annotations: {
9995
10069
  readOnlyHint: false,
@@ -10011,7 +10085,7 @@ async function buildServer(config) {
10011
10085
  }, (args) => handleUpdateClientServiceStage(api, args));
10012
10086
  registerWriteTool(server, api, config.context, "create_document_request", {
10013
10087
  title: "Draft a document request for a client",
10014
- description: "Create a document request as a DRAFT — nothing is sent and the client is not notified. Requires clientCode and a title; optional client-facing description (write it as the client will read it), deadline, and a team member to notify on submission. Use for 'ask ACME for their year-end records', 'request the Jan–Mar bank statements from Greggs'. The user reviews and sends it from the client's Documents tab in Sodium — ALWAYS tell them it's drafted, not sent. Track it afterwards with list_document_requests (status=Draft).",
10088
+ description: "Create a document request as a DRAFT — nothing is sent and the client is not notified. Requires clientCode and a title; optional client-facing description (write it as the client will read it), deadline, and a team member to notify on submission. Use for 'ask ACME for their year-end records', 'request the Jan–Mar bank statements from Greggs'. It is saved as a draft and not sent; the user reviews and sends it from the client's Documents tab in Sodium. Track it afterwards with list_document_requests (status=Draft).",
10015
10089
  inputSchema: CreateDocumentRequestInputSchema,
10016
10090
  annotations: {
10017
10091
  readOnlyHint: false,
@@ -10033,7 +10107,7 @@ async function buildServer(config) {
10033
10107
  }, (args) => handleSendDataForm(api, args));
10034
10108
  registerWriteTool(server, api, config.context, "create_proposal", {
10035
10109
  title: "Draft a proposal / letter of engagement",
10036
- description: "Create an engagement in UNSENT (draft) state — the client is NOT emailed; sending is a deliberate separate action in the Sodium UI. Requires clientCode and date. type defaults to ProposalAndEngagementLetter ('EngagementLetter' for existing clients needing only the letter). clientBillableServiceCodes are the CLIENT'S service-subscription codes (get_client_summary's services section), not catalogue codes — to propose a service the client doesn't have yet, first add_client_service with status 'Proposed', then include its new code here. Recipient defaults to the client's main contact when omitted; only use an email from the client's contacts or given by the user. Use for 'draft a proposal for Bristol Roofing covering bookkeeping and VAT'. Always report the drafted value and that it has NOT been sent; full detail via get_proposal_summary.",
10110
+ description: "Create an engagement in UNSENT (draft) state — the client is NOT emailed; sending is a deliberate separate action in the Sodium UI. Requires clientCode and date. type defaults to ProposalAndEngagementLetter ('EngagementLetter' for existing clients needing only the letter). clientBillableServiceCodes are the CLIENT'S service-subscription codes (get_client_summary's services section), not catalogue codes — to propose a service the client doesn't have yet, first add_client_service with status 'Proposed', then include its new code here. Recipient defaults to the client's main contact when omitted; only use an email from the client's contacts or given by the user. Use for 'draft a proposal for Bristol Roofing covering bookkeeping and VAT'. The result states the drafted value and that it has not been sent; full detail via get_proposal_summary.",
10037
10111
  inputSchema: CreateProposalInputSchema,
10038
10112
  annotations: {
10039
10113
  readOnlyHint: false,
@@ -10044,7 +10118,7 @@ async function buildServer(config) {
10044
10118
  }, (args) => handleCreateProposal(api, args));
10045
10119
  registerWriteTool(server, api, config.context, "update_client_dates", {
10046
10120
  title: "Set key/statutory dates on a client",
10047
- description: "Set one or more of a client's key dates (year-end, VAT return due, PAYE/CIS filing due, pension re-enrolment...). Takes an array of { dateType, date } — only the types you pass change; pass date=null to clear one. Current values are in get_client_summary's key-dates section. Use for 'set ACME's year-end to 31 March', 'record the pension re-enrolment date'. CAUTION: for clients connected to Companies House, register-derived dates (accounts due, confirmation statement) are synced automatically and manual values may be overwritten — prefer setting only non-CH dates (YearEnd, VAT, PAYE, pension) on connected clients, and say so if asked to set a CH-derived one.",
10121
+ description: "Set one or more of a client's key dates (year-end, VAT return due, PAYE/CIS filing due, pension re-enrolment...). Takes an array of { dateType, date } — only the types you pass change; pass date=null to clear one. Current values are in get_client_summary's key-dates section. Use for 'set ACME's year-end to 31 March', 'record the pension re-enrolment date'. For clients connected to Companies House, register-derived dates (accounts due, confirmation statement) are synced automatically and manual values may be overwritten; YearEnd, VAT, PAYE and pension dates are safe to set on connected clients.",
10048
10122
  inputSchema: UpdateClientDatesInputSchema,
10049
10123
  annotations: {
10050
10124
  readOnlyHint: false,
@@ -10077,7 +10151,7 @@ async function buildServer(config) {
10077
10151
  }, (args) => handleCreateSavedTaskFilter(api, args));
10078
10152
  registerWriteTool(server, api, config.context, "update_workflow_step", {
10079
10153
  title: "Update a workflow step (complete, skip, block, reassign)",
10080
- description: "Update one workflow step on a task: set status (Completed / Skipped / InProgress / Blocked with a blockedReason) and/or reassign to a user or team. Identify the step by taskCode + groupNumber + stepNumber exactly as shown in get_task_context's workflow section — ALWAYS call get_task_context first to confirm the numbers and current status. Use for 'mark the review step on TSK-123 as done', 'skip the client-approval step, they confirmed by phone', 'assign the bookkeeping steps to Jane'. Constraints: steps with dependencies may be rejected until prior steps complete — report the API's reason rather than retrying; auto-executing steps (emails, document requests) fire when their turn comes, so completing them manually may skip the automated action — warn the user if get_task_context showed the step as auto-executing. Step content/configuration is not editable here — that's done in the Sodium UI.",
10154
+ description: "Update one workflow step on a task: set status (Completed / Skipped / InProgress / Blocked with a blockedReason) and/or reassign to a user or team. Identify the step by taskCode + groupNumber + stepNumber exactly as shown in get_task_context's workflow section, which also shows each step's current status. Use for 'mark the review step on TSK-123 as done', 'skip the client-approval step, they confirmed by phone', 'assign the bookkeeping steps to Jane'. Constraints: steps with dependencies may be rejected until prior steps complete — report the API's reason rather than retrying; auto-executing steps (emails, document requests) fire when their turn comes, so completing them manually may skip the automated action — warn the user if get_task_context showed the step as auto-executing. Step content/configuration is not editable here — that's done in the Sodium UI.",
10081
10155
  inputSchema: UpdateWorkflowStepInputSchema,
10082
10156
  annotations: {
10083
10157
  readOnlyHint: false,
@@ -10088,7 +10162,7 @@ async function buildServer(config) {
10088
10162
  }, (args) => handleUpdateWorkflowStep(api, args));
10089
10163
  registerWriteTool(server, api, config.context, "create_task", {
10090
10164
  title: "Create a new task",
10091
- description: "Create a one-off task. Requires name, startDate, and dueDate (YYYY-MM-DD — 'due Friday' resolves against today from the startup context). Optional: description, status (defaults NotStarted), statutory due date, time estimate, client(s) (codes from list_clients), assignee (user code from the roster/list_users, or team code from list_teams), linked client-billable-service, category (list_task_categories), and a checklist (array of item texts). Use for 'create a task to chase ACME's rental schedule, due Friday', 'add a task for Jane to review the draft accounts'. Do NOT use for recurring schedules or workflow-driven tasks — those are set up in the Sodium UI. If the user gave a client or person by name, resolve codes first; ask rather than guess when multiple matches exist.",
10165
+ description: "Create a one-off task. Requires name, startDate, and dueDate (YYYY-MM-DD — 'due Friday' resolves against today's date). Optional: description, status (defaults NotStarted), statutory due date, time estimate, client(s) (codes from list_clients), assignee (user code from the roster/list_users, or team code from list_teams), linked client-billable-service, category (list_task_categories), and a checklist (array of item texts). Use for 'create a task to chase ACME's rental schedule, due Friday', 'add a task for Jane to review the draft accounts'. Do NOT use for recurring schedules or workflow-driven tasks — those are set up in the Sodium UI. If the user gave a client or person by name, resolve codes first; ask rather than guess when multiple matches exist.",
10092
10166
  inputSchema: CreateTaskInputSchema,
10093
10167
  annotations: {
10094
10168
  readOnlyHint: false,
@@ -10110,7 +10184,7 @@ async function buildServer(config) {
10110
10184
  }, (args) => handleUpdateTask(api, args));
10111
10185
  registerWriteTool(server, api, config.context, "log_time_entry", {
10112
10186
  title: "Log a time entry",
10113
- description: "Record time spent, after the fact. Requires entryDate (YYYY-MM-DD — use today from the startup context when the user doesn't say) and durationMinutes ('2 hours' → 120). Optional: description (strongly recommended — it feeds billing narratives), client code, task code, workflow group/step numbers (from get_task_context), billable flag, hourly rate, and userCode when logging on someone else's behalf (defaults to the authenticated user). Use for 'log 2 hours on ACME for today — reviewing the draft accounts', 'add 45 minutes against the Greggs VAT task'. Billable and rate default from practice/team-member configuration — only pass them when the user is explicit. Some practices require a client or task link; if the API rejects the entry saying so, ask the user which client/task to attach.",
10187
+ description: "Record time spent, after the fact. Requires entryDate (YYYY-MM-DD — today's date applies when none is given) and durationMinutes ('2 hours' → 120). Optional: description (strongly recommended — it feeds billing narratives), client code, task code, workflow group/step numbers (from get_task_context), billable flag, hourly rate, and userCode when logging on someone else's behalf (defaults to the authenticated user). Use for 'log 2 hours on ACME for today — reviewing the draft accounts', 'add 45 minutes against the Greggs VAT task'. Billable and rate default from practice/team-member configuration — only pass them when the user is explicit. Some practices require a client or task link; if the API rejects the entry saying so, ask the user which client/task to attach.",
10114
10188
  inputSchema: LogTimeEntryInputSchema,
10115
10189
  annotations: {
10116
10190
  readOnlyHint: false,
@@ -10130,6 +10204,17 @@ async function buildServer(config) {
10130
10204
  openWorldHint: true
10131
10205
  }
10132
10206
  }, (args) => handleAddTaskNote(api, args));
10207
+ registerWriteTool(server, api, config.context, "add_workflow_step_note", {
10208
+ title: "Add a note to a workflow step",
10209
+ description: "Create a new note on one step of a task's workflow, so it shows on that step in Sodium. Additive — does not modify or delete existing notes. The note is attributed to the signed-in user and timestamped to 'now'. Use this when the user names a step: 'add a note on step 1 of the Greggs accounts task that we're waiting on the bank statements'. Needs the task code plus the group and step numbers, which get_task_context shows for each workflow step. For a note on the whole task use add_task_note. The user can edit or delete notes in the Sodium UI.",
10210
+ inputSchema: AddWorkflowStepNoteInputSchema,
10211
+ annotations: {
10212
+ readOnlyHint: false,
10213
+ destructiveHint: false,
10214
+ idempotentHint: false,
10215
+ openWorldHint: true
10216
+ }
10217
+ }, (args) => handleAddWorkflowStepNote(api, args));
10133
10218
  registerWriteTool(server, api, config.context, "create_client", {
10134
10219
  title: "Create a new client",
10135
10220
  description: "Create a new client record in the practice. Requires name and type (PrivateLimitedCompany, Individual, SoleTrader, etc.). Optionally set status (Active/Prospect/Inactive/LostProspect), email, telephone, internal reference, and assign a manager/partner/associate/team. The client code is auto-generated. Use when the user says 'add a new client', 'create client ACME Ltd', 'onboard a new prospect'.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.6353",
3
+ "version": "0.1.0-beta.6355",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {