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

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/dist/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src--vw5xenq.mjs";
2
+ import { n as buildServer, t as buildCatalogue } from "./src-B3pnHPhY.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { SUPPORTED_PROTOCOL_VERSIONS, createMcpHandler } from "@modelcontextprotocol/server";
5
5
  import { createHash, randomUUID } from "node:crypto";
@@ -270,6 +270,47 @@ const mcpHandler = createMcpHandler((ctx) => {
270
270
  console.error(`[sodium-pm-mcp-http] handler: ${error.message}`);
271
271
  } });
272
272
  const serveMcp = toNodeHandler(mcpHandler);
273
+ let catalogueJson;
274
+ async function listRegisteredTools() {
275
+ const state = {
276
+ apiToken: "catalogue",
277
+ tenant: void 0,
278
+ writesAllowed: true,
279
+ wantsInstructions: false,
280
+ onToolCall: () => {}
281
+ };
282
+ const request = new Request("http://internal/", {
283
+ method: "POST",
284
+ headers: {
285
+ "Content-Type": "application/json",
286
+ Accept: "application/json, text/event-stream",
287
+ "MCP-Protocol-Version": "2025-06-18"
288
+ },
289
+ body: JSON.stringify({
290
+ jsonrpc: "2.0",
291
+ id: 1,
292
+ method: "tools/list",
293
+ params: {}
294
+ })
295
+ });
296
+ const text = await (await mcpHandler.fetch(request, { authInfo: {
297
+ token: "catalogue",
298
+ clientId: "catalogue",
299
+ scopes: [],
300
+ extra: { sodium: state }
301
+ } })).text();
302
+ const payload = text.trimStart().startsWith("{") ? text : text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).pop() ?? "";
303
+ const tools = JSON.parse(payload).result?.tools;
304
+ if (!tools) throw new Error("tools/list returned no tools.");
305
+ return tools;
306
+ }
307
+ function getCatalogueJson() {
308
+ catalogueJson ??= listRegisteredTools().then((tools) => JSON.stringify(buildCatalogue(tools))).catch((error) => {
309
+ catalogueJson = void 0;
310
+ throw error;
311
+ });
312
+ return catalogueJson;
313
+ }
273
314
  function jsonRpcMethod(body) {
274
315
  if (typeof body === "object" && body !== null && "method" in body) return String(body.method);
275
316
  }
@@ -393,6 +434,14 @@ async function handle(req, res) {
393
434
  }));
394
435
  return;
395
436
  }
437
+ if (req.method === "GET" && url.pathname === "/tools.json") {
438
+ const body = await getCatalogueJson();
439
+ res.writeHead(200, {
440
+ "Content-Type": "application/json",
441
+ "Cache-Control": "public, max-age=300"
442
+ }).end(body);
443
+ return;
444
+ }
396
445
  if (AUTH && req.method === "GET" && url.pathname.startsWith("/.well-known/oauth-protected-resource")) {
397
446
  res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(protectedResourceMetadata(AUTH)));
398
447
  return;
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src--vw5xenq.mjs";
2
+ import { n as buildServer } from "./src-B3pnHPhY.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
  import { parseArgs } from "node:util";
@@ -4342,7 +4342,7 @@ const DEFAULT_CATCH_UP_FEE_LABEL = "Catch-up fee";
4342
4342
  function labelInSentence(label) {
4343
4343
  return /^[A-Z][A-Z]/.test(label) ? label : label.charAt(0).toLowerCase() + label.slice(1);
4344
4344
  }
4345
- const GetEngagementSummaryInputSchema = { code: z.string().min(1, "Engagement code is required").describe("REQUIRED: The engagement code (identifier, e.g. 'ENG-123'). This is the unique code of a specific engagement — NOT a description like 'the unsent proposal' or 'ACME's proposal'. If the user describes an engagement by attribute (status, client, date) rather than by code, you MUST call list_proposals or list_engagement_letters first (with the appropriate filters) to find the engagement's code, then pass that code here.") };
4345
+ const GetEngagementSummaryInputSchema = { code: z.string().min(1, "Engagement code is required").describe("REQUIRED: The engagement code (identifier, e.g. 'ENG-123'). This is the unique code of a specific engagement — NOT a description like 'the unsent proposal' or 'ACME's proposal'. list_proposals and list_engagement_letters return engagement codes and filter by status, client and date.") };
4346
4346
  async function handleGetEngagementSummary(api, { code }) {
4347
4347
  const [practice, [engagementResult, emailsResult, settingsResult]] = await Promise.all([api.getPracticeLocale(), Promise.allSettled([
4348
4348
  api.getEngagement(code),
@@ -4943,12 +4943,12 @@ const stepTypeEnum$1 = z.enum([
4943
4943
  "DocumentUpload"
4944
4944
  ]);
4945
4945
  const ListTasksInputSchema = {
4946
- 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)."),
4946
+ user: z.array(z.string()).optional().describe("Filter by assigned user codes. For 'my tasks', this is the signed-in user's practice user code, which whoami shows. For another team member's tasks, pass their code. Omit to see tasks across all users (useful for practice managers)."),
4947
4947
  client: z.array(z.string()).optional().describe("Filter by client codes — tasks belonging to these specific clients only."),
4948
- status: z.array(statusEnum$14).optional().describe("Filter by task status. Typical: ['NotStarted', 'InProgress'] to exclude completed/skipped. Leave empty for all statuses. 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."),
4948
+ status: z.array(statusEnum$14).optional().describe("Filter by task status. Typical: ['NotStarted', 'InProgress'] to exclude completed/skipped. Leave empty for all statuses. If the query includes NotStarted (the default for new tasks), it also needs one of: dateRange, isOverdue=true, or restrict status to non-NotStarted values only — otherwise the API rejects the call to prevent unbounded queries."),
4949
4949
  isOverdue: z.boolean().optional().describe("Set true to return only overdue tasks (DueDate before today, not completed/skipped). When true, no date range is required. Useful for sidestepping the NotStarted + date-range requirement."),
4950
4950
  dateRange: dateRangeEnum$2.optional().describe("Preset date range. Use 'Today' for today's tasks, 'Next7Days' for 'due this week', 'Last30Days' for recent activity. If 'CustomDateRange', also provide startDate and/or endDate. REQUIRED when querying NotStarted tasks unless you pass isOverdue=true."),
4951
- 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."),
4951
+ startDate: z.string().optional().describe("Start of custom date range (YYYY-MM-DD). Used when dateRange='CustomDateRange'. Both startDate and endDate with a narrow window perform best — broad ranges strain the API. If startDate > endDate the API swaps them silently. Maximum allowed total range is 2 years."),
4952
4952
  endDate: z.string().optional().describe("End of custom date range (YYYY-MM-DD). Used when dateRange='CustomDateRange'. Pair with startDate to define a narrow window — avoid open-ended ranges."),
4953
4953
  dateBasis: dateBasisEnum$1.optional().describe("Which date field to apply the date range filter to. Use 'DueDate' for 'what's due when' questions; 'StartDate' (default) for 'what's been worked on'."),
4954
4954
  category: z.array(z.string()).optional().describe("Filter by task category codes (e.g. tax, audit, compliance categories)."),
@@ -4963,7 +4963,7 @@ const ListTasksInputSchema = {
4963
4963
  currentStep: z.array(z.string()).optional().describe("Filter to tasks whose CURRENT workflow step has one of these exact names (as shown in get_workflow). Use for 'everything sitting at Partner review'."),
4964
4964
  isUnassigned: z.boolean().optional().describe("Include tasks with no assignee. Alone: only unassigned tasks. Combined with `user`: unassigned OR assigned to those users. Use for 'what needs an owner'."),
4965
4965
  isAutomationDisabled: z.boolean().optional().describe("true = only tasks with automation switched off; false = only those with it on. Task previews are excluded either way. Rarely needed."),
4966
- search: z.string().min(3).optional().describe("Free-text search across task name, task code and primary client name (minimum 3 characters). Projected tasks are excluded when set. Prefer `client` when the client code is known."),
4966
+ search: z.string().min(3).optional().describe("Free-text search across task name, task code and primary client name (minimum 3 characters). Projected tasks are excluded when set. `client` is more precise when the client code is known."),
4967
4967
  savedFilter: z.string().optional().describe("Apply a user-saved filter by its code. Other filter parameters override fields from the saved filter."),
4968
4968
  includeProjected: z.boolean().optional().describe("Set true to include projected (virtual, not-yet-materialised) tasks in the results. Projected tasks are always NotStarted. Useful for 'what's coming up' queries."),
4969
4969
  includeWorkflowSteps: z.boolean().optional().describe("Set true for Agenda Mode: returns both tasks AND their workflow steps as individual rows. Each step row has its parent task's properties plus step-specific details. Useful for 'what's the next action on X' or when workflow step granularity matters."),
@@ -5376,7 +5376,7 @@ const CreateClientInputSchema = {
5376
5376
  email: z.string().nullable().optional().describe("Client email address."),
5377
5377
  telephone: z.string().nullable().optional().describe("Client telephone number."),
5378
5378
  internalReference: z.string().nullable().optional().describe("Internal reference number/code for the client."),
5379
- managerCode: z.string().nullable().optional().describe("Code of the user who will manage this client. Discoverable via list_users or the team roster in the startup context."),
5379
+ managerCode: z.string().nullable().optional().describe("Code of the user who will manage this client. Codes are listed by list_users and shown in the team roster."),
5380
5380
  partnerCode: z.string().nullable().optional().describe("Code of the partner user for this client."),
5381
5381
  associateCode: z.string().nullable().optional().describe("Code of the associate user for this client."),
5382
5382
  teamCode: z.string().nullable().optional().describe("Code of the team to assign to this client.")
@@ -5569,7 +5569,7 @@ function reportWindowFields(windowNoun) {
5569
5569
  const reportEntityFilters = {
5570
5570
  service: z.array(z.string()).optional().describe("Restrict the cohort to these billable service code(s). Use for 'how are we doing on VAT returns?' — get codes from list_services."),
5571
5571
  client: z.array(z.string()).optional().describe("Restrict the cohort to these client code(s). Use for a single-client view — get codes from list_clients."),
5572
- user: z.array(z.string()).optional().describe("Restrict the cohort to tasks assigned to these user code(s). For 'my …' questions use the current user's code from the startup context."),
5572
+ user: z.array(z.string()).optional().describe("Restrict the cohort to tasks assigned to these user code(s). For 'my …' questions, this is the signed-in user's practice user code, which whoami shows."),
5573
5573
  team: z.array(z.string()).optional().describe("Restrict the cohort to tasks assigned to these team code(s)."),
5574
5574
  category: z.array(z.string()).optional().describe("Restrict the cohort to these task category code(s).")
5575
5575
  };
@@ -6487,7 +6487,7 @@ async function handleUpdateServiceCategory(api, args) {
6487
6487
  //#region ../mcp-core/src/tools/list-saved-task-filters.ts
6488
6488
  const sortByEnum$13 = z.enum(["Name", "UpdatedDate"]);
6489
6489
  const ListSavedTaskFiltersInputSchema = {
6490
- menuOnly: z.boolean().optional().describe("Only return filters the user has pinned to their menu. These are the views they use daily, so prefer menuOnly=true when guessing which saved view the user means."),
6490
+ menuOnly: z.boolean().optional().describe("Only return filters the user has pinned to their menu. These are the views they use daily."),
6491
6491
  sortBy: sortByEnum$13.optional().describe("Field to sort by. Defaults to Name. Use UpdatedDate + sortDesc=true for recently edited views."),
6492
6492
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
6493
6493
  limit: z.number().int().min(0).max(50).optional().describe("Maximum filters per page. Default 10, max 50. Pass 0 for count-only."),
@@ -6552,7 +6552,7 @@ async function handleListSavedTaskFilters(api, args) {
6552
6552
  const sortByEnum$12 = z.enum(["Name", "UpdatedDate"]);
6553
6553
  const ListSavedClientFiltersInputSchema = {
6554
6554
  search: z.string().optional().describe("Search across saved view name and description."),
6555
- menuOnly: z.boolean().optional().describe("Only return filters the user has pinned to their menu. Prefer menuOnly=true when guessing which saved view the user means."),
6555
+ menuOnly: z.boolean().optional().describe("Only return filters the user has pinned to their menu. These are the views they use daily."),
6556
6556
  sortBy: sortByEnum$12.optional().describe("Field to sort by. Defaults to Name. Use UpdatedDate + sortDesc=true for recently edited views."),
6557
6557
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
6558
6558
  limit: z.number().int().min(0).max(50).optional().describe("Maximum filters per page. Default 10, max 50. Pass 0 for count-only."),
@@ -6915,10 +6915,10 @@ const sortByEnum$9 = z.enum([
6915
6915
  "IsBillable"
6916
6916
  ]);
6917
6917
  const ListTimeEntriesInputSchema = {
6918
- userCode: z.string().optional().describe("Filter by team member code. For 'my time this week' use the current user's code from the startup context. Omit for practice-wide time."),
6918
+ userCode: z.string().optional().describe("Filter by team member code. For 'my time this week', this is the signed-in user's practice user code, which whoami shows. Omit for practice-wide time."),
6919
6919
  clientCode: z.string().optional().describe("Filter by linked client code — 'how much time have we spent on ACME?'."),
6920
6920
  taskItemCode: z.string().optional().describe("Filter by linked task code — 'time logged against the Greggs year-end task'."),
6921
- dateRange: dateRangeEnum$1.optional().describe("Preset date range for the entry window. Use 'Today' / 'ThisWeek' / 'ThisMonth' for recent-time questions; 'CustomDateRange' with fromDate + toDate for explicit windows. Prefer a narrow range — time queries without a window scan everything."),
6921
+ dateRange: dateRangeEnum$1.optional().describe("Preset date range for the entry window. Use 'Today' / 'ThisWeek' / 'ThisMonth' for recent-time questions; 'CustomDateRange' with fromDate + toDate for explicit windows. Narrow ranges are faster — time queries without a window scan everything."),
6922
6922
  fromDate: z.string().optional().describe("Window start (YYYY-MM-DD), inclusive. Use with dateRange='CustomDateRange'."),
6923
6923
  toDate: z.string().optional().describe("Window end (YYYY-MM-DD), inclusive. Use with dateRange='CustomDateRange'."),
6924
6924
  isBillable: z.boolean().optional().describe("Filter by billable flag. true for 'billable time only', false for internal/non-chargeable time."),
@@ -7320,13 +7320,13 @@ const CreateTaskInputSchema = {
7320
7320
  name: z.string().min(1).describe("The task name — short and action-oriented, e.g. 'Chase rental schedule for year-end'."),
7321
7321
  description: z.string().optional().describe("Longer description of what needs doing. Optional."),
7322
7322
  status: statusEnum$10.optional().describe("Initial status. Defaults to NotStarted — only override when the work is already underway (InProgress)."),
7323
- 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."),
7323
+ startDate: z.string().describe("When work should start (YYYY-MM-DD). For ad-hoc tasks, today's date is usually right."),
7324
7324
  dueDate: z.string().describe("When the task is due (YYYY-MM-DD). Must be on or after startDate."),
7325
7325
  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."),
7326
7326
  timeEstimateMinutes: z.number().int().min(1).optional().describe("Estimated effort in whole minutes."),
7327
7327
  clientCodes: z.array(z.string()).optional().describe("Client code(s) the task is for. Get codes from list_clients. Omit for internal practice work."),
7328
7328
  primaryClientCode: z.string().optional().describe("Which of clientCodes is the primary client. Only needed when passing more than one client."),
7329
- assignedUserCode: z.string().optional().describe("User code to assign the task to. For 'create a task for me', use the current user's code from the startup context. Resolve names via the roster or list_users."),
7329
+ assignedUserCode: z.string().optional().describe("User code to assign the task to. For 'create a task for me', this is the signed-in user's practice user code, which whoami shows. Names resolve to codes via the roster or list_users."),
7330
7330
  assignedTeamCode: z.string().optional().describe("Team code to assign to instead of an individual. Get codes from list_teams. Don't pass both this and assignedUserCode unless the user asked for both."),
7331
7331
  clientBillableServiceCode: z.string().optional().describe("The client-billable-service code linking this task to a service the client subscribes to (visible in get_client_summary's services section)."),
7332
7332
  categoryCode: z.string().optional().describe("Task category code. Get codes from list_task_categories."),
@@ -7446,7 +7446,7 @@ async function handleUpdateTask(api, args) {
7446
7446
  //#endregion
7447
7447
  //#region ../mcp-core/src/tools/log-time-entry.ts
7448
7448
  const LogTimeEntryInputSchema = {
7449
- entryDate: z.string().describe("The day the work was done (YYYY-MM-DD). For 'log 2 hours on ACME' with no date given, use today from the startup context."),
7449
+ entryDate: z.string().describe("The day the work was done (YYYY-MM-DD). For 'log 2 hours on ACME' with no date given, today's date applies."),
7450
7450
  durationMinutes: z.number().int().min(1).describe("How long the work took, in whole minutes. Convert user phrasing: '2 hours' → 120, 'half an hour' → 30."),
7451
7451
  description: z.string().optional().describe("What the time was spent on. Strongly recommended — it's what appears on billing narratives."),
7452
7452
  userCode: z.string().optional().describe("Who did the work. Defaults to the authenticated user — only pass this when logging time on someone else's behalf."),
@@ -7516,7 +7516,7 @@ const sortByEnum$7 = z.enum([
7516
7516
  const ListSentEmailsInputSchema = {
7517
7517
  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."),
7518
7518
  search: z.string().optional().describe("Search in email subject and recipient email address — 'emails to john@acme.co.uk', 'the proposal email'."),
7519
- 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."),
7519
+ fromDate: z.string().optional().describe("Only emails created on or after this date (YYYY-MM-DD). A window keeps results small; unbounded queries scan all history."),
7520
7520
  toDate: z.string().optional().describe("Only emails created on or before this date (YYYY-MM-DD)."),
7521
7521
  metadataParam: z.array(z.string()).optional().describe("Filter by metadata stored with the email, each entry as 'key=value' (e.g. ['entityType=Proposal']). Only use when you know the metadata key from a previous result — otherwise omit."),
7522
7522
  sortBy: sortByEnum$7.optional().describe("Field to sort by. Defaults to CreatedDate — pair with sortDesc=true for newest first."),
@@ -7830,7 +7830,7 @@ const ListBillingLineItemsInputSchema = {
7830
7830
  clientCode: z.string().optional().describe("Filter to one client's billing lines — 'what are we about to bill ACME?'."),
7831
7831
  billableServiceCode: z.string().optional().describe("Filter by the billable service — 'what bookkeeping revenue is queued?'. Get codes from list_services."),
7832
7832
  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."),
7833
- billingDateFrom: z.string().optional().describe("Only lines with billing date on or after this date (YYYY-MM-DD). Prefer a window for revenue questions."),
7833
+ billingDateFrom: z.string().optional().describe("Only lines with billing date on or after this date (YYYY-MM-DD). A window keeps revenue questions fast."),
7834
7834
  billingDateTo: z.string().optional().describe("Only lines with billing date on or before this date (YYYY-MM-DD)."),
7835
7835
  sortBy: sortByEnum$5.optional().describe("Field to sort by. Defaults to BillingDate. Use UnitPrice + sortDesc=true for the biggest lines first."),
7836
7836
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
@@ -7960,7 +7960,7 @@ const sortByEnum$4 = z.enum([
7960
7960
  "StepType"
7961
7961
  ]);
7962
7962
  const ListPendingWorkflowStepsInputSchema = {
7963
- assignedUserCode: z.string().optional().describe("Filter to steps assigned to this user. For 'what steps are waiting on me?' use the current user's code from the startup context."),
7963
+ assignedUserCode: z.string().optional().describe("Filter to steps assigned to this user. For 'what steps are waiting on me?', this is the signed-in user's practice user code, which whoami shows."),
7964
7964
  assignedTeamCode: z.string().optional().describe("Filter to steps assigned to this team (codes from list_teams)."),
7965
7965
  isUnassigned: z.boolean().optional().describe("true = only steps with no assigned user — 'what work is nobody picking up?'. Combines with assignedUserCode as OR (mine + unassigned)."),
7966
7966
  stepType: stepTypeEnum.optional().describe("Filter by step type. 'Standard' = internal work steps; 'DocumentRequest'/'DocumentApproval'/'ClientConfirmation'/'SendDataForm' = client-facing steps."),
@@ -9189,7 +9189,7 @@ async function handleSendDataForm(api, args) {
9189
9189
  const typeEnum = z.enum(["ProposalAndEngagementLetter", "EngagementLetter"]);
9190
9190
  const CreateProposalInputSchema = {
9191
9191
  clientCode: z.string().min(1).describe("The client the proposal is for. Get the code from list_clients."),
9192
- date: z.string().describe("The engagement date (YYYY-MM-DD) — usually today, from the startup context."),
9192
+ date: z.string().describe("The engagement date (YYYY-MM-DD) — usually today's date."),
9193
9193
  type: typeEnum.optional().describe("'ProposalAndEngagementLetter' (default) generates both documents; 'EngagementLetter' for existing clients who only need the letter."),
9194
9194
  recipientFirstName: z.string().optional().describe("Recipient's first name. Usually the client's main contact — check get_client_summary."),
9195
9195
  recipientLastName: z.string().optional().describe("Recipient's last name."),
@@ -10284,4 +10284,278 @@ async function buildServer(config) {
10284
10284
  return server;
10285
10285
  }
10286
10286
  //#endregion
10287
- export { buildServer as t };
10287
+ //#region ../mcp-core/src/tool-groups.json
10288
+ var groups = [
10289
+ {
10290
+ "name": "Clients & contacts",
10291
+ "tools": [
10292
+ "list_clients",
10293
+ "get_client_summary",
10294
+ "list_client_notes",
10295
+ "list_client_links",
10296
+ "list_contacts",
10297
+ "get_contact",
10298
+ "get_custom_field_details",
10299
+ "list_portal_users",
10300
+ "create_client",
10301
+ "create_contact",
10302
+ "update_contact",
10303
+ "link_contact_to_client",
10304
+ "update_client_business_details",
10305
+ "update_client_dates",
10306
+ "update_client_custom_fields",
10307
+ "add_client_note"
10308
+ ]
10309
+ },
10310
+ {
10311
+ "name": "Tasks & workflows",
10312
+ "tools": [
10313
+ "list_tasks",
10314
+ "get_task_context",
10315
+ "get_task_history",
10316
+ "list_recurring_tasks",
10317
+ "list_workflows",
10318
+ "get_workflow",
10319
+ "list_workflow_occurrences",
10320
+ "list_pending_workflow_steps",
10321
+ "create_task",
10322
+ "update_task",
10323
+ "add_task_note",
10324
+ "update_workflow_step",
10325
+ "add_workflow_step_note"
10326
+ ]
10327
+ },
10328
+ {
10329
+ "name": "Services & pricing",
10330
+ "tools": [
10331
+ "list_services",
10332
+ "get_service_details",
10333
+ "list_service_packages",
10334
+ "list_service_categories",
10335
+ "add_client_service",
10336
+ "apply_service_package",
10337
+ "update_client_service_stage",
10338
+ "create_service_category",
10339
+ "update_service_category"
10340
+ ]
10341
+ },
10342
+ {
10343
+ "name": "Proposals & engagements",
10344
+ "tools": [
10345
+ "list_proposals",
10346
+ "get_proposal_summary",
10347
+ "list_engagement_letters",
10348
+ "get_engagement_letter_summary",
10349
+ "get_engagement_pipeline_summary",
10350
+ "create_proposal"
10351
+ ]
10352
+ },
10353
+ {
10354
+ "name": "Documents & forms",
10355
+ "tools": [
10356
+ "list_document_requests",
10357
+ "create_document_request",
10358
+ "list_client_documents",
10359
+ "list_data_forms",
10360
+ "list_data_form_requests",
10361
+ "send_data_form"
10362
+ ]
10363
+ },
10364
+ {
10365
+ "name": "Time & billing",
10366
+ "tools": [
10367
+ "list_time_entries",
10368
+ "log_time_entry",
10369
+ "list_unbilled_time_clients",
10370
+ "list_billing_line_items",
10371
+ "list_invoice_submissions",
10372
+ "get_service_time_report",
10373
+ "get_team_time_report"
10374
+ ]
10375
+ },
10376
+ {
10377
+ "name": "Email & Communication",
10378
+ "tools": [
10379
+ "list_client_emails",
10380
+ "get_client_email_thread",
10381
+ "list_sent_emails",
10382
+ "get_sent_email",
10383
+ "list_email_broadcasts",
10384
+ "get_email_broadcast"
10385
+ ]
10386
+ },
10387
+ {
10388
+ "name": "Deadlines & reports",
10389
+ "tools": [
10390
+ "get_practice_dashboard",
10391
+ "get_deadline_compliance_report",
10392
+ "get_service_delivery_report",
10393
+ "get_service_delivery_steps",
10394
+ "get_client_responsiveness_report",
10395
+ "get_team_workload_report",
10396
+ "get_wip_ageing_report",
10397
+ "get_workflow_step_analysis"
10398
+ ]
10399
+ },
10400
+ {
10401
+ "name": "Compliance & lookups",
10402
+ "tools": [
10403
+ "list_aml_clients",
10404
+ "get_aml_client",
10405
+ "get_client_agent_authorisations",
10406
+ "get_onboarding_status",
10407
+ "search_companies_house",
10408
+ "get_companies_house_profile"
10409
+ ]
10410
+ },
10411
+ {
10412
+ "name": "Practice & setup",
10413
+ "tools": [
10414
+ "get_practice_details",
10415
+ "whoami",
10416
+ "list_practices",
10417
+ "get_usage_examples",
10418
+ "list_users",
10419
+ "list_teams",
10420
+ "list_task_categories",
10421
+ "list_saved_task_filters",
10422
+ "list_saved_client_filters",
10423
+ "create_saved_task_filter"
10424
+ ]
10425
+ }
10426
+ ];
10427
+ var summaries = {
10428
+ "list_clients": "Find and filter your clients by name, status, type, manager, service and more.",
10429
+ "get_client_summary": "A full overview of one client: details, contacts, services, key dates and tasks.",
10430
+ "list_client_notes": "Read the notes on a client.",
10431
+ "list_client_links": "See the links saved against a client, such as a website or shared folder.",
10432
+ "list_contacts": "Search contacts across the practice, or list a client's contacts.",
10433
+ "get_contact": "Full details of one contact, including linked clients.",
10434
+ "get_custom_field_details": "The definition of a custom field, including its allowed options.",
10435
+ "list_portal_users": "See who has client portal access and when they last logged in.",
10436
+ "create_client": "Create a new client or prospect.",
10437
+ "create_contact": "Create a new contact.",
10438
+ "update_contact": "Update a contact's details.",
10439
+ "link_contact_to_client": "Link an existing contact to a client and set their role.",
10440
+ "update_client_business_details": "Update a client's trading name, address, VAT and tax details.",
10441
+ "update_client_dates": "Set a client's key dates, such as year end and VAT or filing due dates.",
10442
+ "update_client_custom_fields": "Update a client's custom field values.",
10443
+ "add_client_note": "Add a note to a client.",
10444
+ "list_tasks": "Find tasks by assignee, client, status, due date, category and more.",
10445
+ "get_task_context": "Everything about one task: details, notes, workflow progress and client.",
10446
+ "get_task_history": "The audit trail of a task: who changed what, and when.",
10447
+ "list_recurring_tasks": "See the recurring task templates that generate repeating work.",
10448
+ "list_workflows": "List your workflow templates.",
10449
+ "get_workflow": "The groups and steps of one workflow template.",
10450
+ "list_workflow_occurrences": "The completed runs of a workflow, slowest first.",
10451
+ "list_pending_workflow_steps": "Workflow steps ready to be worked on now, for you, your team or nobody.",
10452
+ "create_task": "Create a task for a client or the practice.",
10453
+ "update_task": "Update a task's status, dates, assignee or details.",
10454
+ "add_task_note": "Add a note to a task.",
10455
+ "update_workflow_step": "Complete, skip, block or reassign a workflow step.",
10456
+ "add_workflow_step_note": "Add a note to one step of a task's workflow.",
10457
+ "list_services": "List the services your practice offers.",
10458
+ "get_service_details": "A service's pricing, client types, stages and authorisations.",
10459
+ "list_service_packages": "List bundled service packages and what is in them.",
10460
+ "list_service_categories": "List your service categories.",
10461
+ "add_client_service": "Put a service on a client, with frequency, price and start date.",
10462
+ "apply_service_package": "Apply a service package to a client in one go.",
10463
+ "update_client_service_stage": "Move a client's service to another stage on the board.",
10464
+ "create_service_category": "Create a service category.",
10465
+ "update_service_category": "Rename, recolour or reorder a service category.",
10466
+ "list_proposals": "List proposals by status, client or date.",
10467
+ "get_proposal_summary": "Full detail of one proposal: services, pricing and acceptance.",
10468
+ "list_engagement_letters": "List letters of engagement by status, client or date.",
10469
+ "get_engagement_letter_summary": "Full detail of one letter of engagement.",
10470
+ "get_engagement_pipeline_summary": "Counts and values of proposals by stage: what is open and what is won.",
10471
+ "create_proposal": "Draft a proposal or letter of engagement for a client.",
10472
+ "list_document_requests": "See document requests sent to clients and what is still outstanding.",
10473
+ "create_document_request": "Draft a document request for a client.",
10474
+ "list_client_documents": "List documents held against clients and their review status.",
10475
+ "list_data_forms": "List your questionnaire templates.",
10476
+ "list_data_form_requests": "See which questionnaires were sent and who has responded.",
10477
+ "send_data_form": "Send a questionnaire to a client.",
10478
+ "list_time_entries": "List recorded time by person, client, task or date.",
10479
+ "log_time_entry": "Record time spent on a client or task.",
10480
+ "list_unbilled_time_clients": "Which clients have time recorded but not yet billed.",
10481
+ "list_billing_line_items": "List billing lines waiting for approval or invoicing.",
10482
+ "list_invoice_submissions": "See invoices sent to your accounting system, and any that failed.",
10483
+ "get_service_time_report": "Where your recorded time goes, by service or client.",
10484
+ "get_team_time_report": "Hours logged against expected hours for each team member.",
10485
+ "list_client_emails": "Email threads with a client from your synced mailbox.",
10486
+ "get_client_email_thread": "Read one email thread in full.",
10487
+ "list_sent_emails": "Emails Sodium sent for you, and whether they were delivered.",
10488
+ "get_sent_email": "Delivery detail for one sent email.",
10489
+ "list_email_broadcasts": "Your email campaigns and how far they have got.",
10490
+ "get_email_broadcast": "One campaign in full, with delivery results for each client.",
10491
+ "get_practice_dashboard": "A snapshot of what needs attention across the practice right now.",
10492
+ "get_deadline_compliance_report": "Which deadlines were met, missed or are at risk.",
10493
+ "get_service_delivery_report": "How quickly services are delivered, and how often on time.",
10494
+ "get_service_delivery_steps": "Where the time goes inside one service's workflow.",
10495
+ "get_client_responsiveness_report": "How quickly clients respond to requests, and how much chasing it takes.",
10496
+ "get_team_workload_report": "Who has too much on, and what each person completes.",
10497
+ "get_wip_ageing_report": "Open work that has been sitting untouched, and for how long.",
10498
+ "get_workflow_step_analysis": "Step-by-step timings for one workflow template.",
10499
+ "list_aml_clients": "AML and identity-check records with risk level and outstanding checks.",
10500
+ "get_aml_client": "One AML record: status, linked contacts, checks and risk assessments.",
10501
+ "get_client_agent_authorisations": "HMRC agent authorisation status for a client, by tax regime.",
10502
+ "get_onboarding_status": "Is a client ready for work: engagement, AML, authorisations and data.",
10503
+ "search_companies_house": "Search Companies House by company name.",
10504
+ "get_companies_house_profile": "The live Companies House record for a company.",
10505
+ "get_practice_details": "Your practice's details, counts, connections and settings.",
10506
+ "whoami": "Who you are signed in as, and which practice you are working in.",
10507
+ "list_practices": "The practices you belong to.",
10508
+ "get_usage_examples": "Example requests to try, grouped by topic.",
10509
+ "list_users": "Your team members and their roles.",
10510
+ "list_teams": "Your teams and who is in them.",
10511
+ "list_task_categories": "List your task categories.",
10512
+ "list_saved_task_filters": "Your saved task views.",
10513
+ "list_saved_client_filters": "Your saved client views.",
10514
+ "create_saved_task_filter": "Save a task search as a reusable view."
10515
+ };
10516
+ //#endregion
10517
+ //#region ../mcp-core/src/catalogue.ts
10518
+ const SUMMARY_MAX = 200;
10519
+ const OTHER_GROUP = "Other";
10520
+ const summaryOverrides = summaries;
10521
+ function summarise(tool) {
10522
+ const override = summaryOverrides[tool.name];
10523
+ if (override) return override;
10524
+ const text = (tool.description ?? "").replace(/\s+/g, " ").trim();
10525
+ const match = text.match(/^(.+?[.!?])(?=\s+[A-Z'"(]|$)/);
10526
+ const sentence = match ? match[1] : text;
10527
+ return sentence.length > SUMMARY_MAX ? `${sentence.slice(0, SUMMARY_MAX - 1).trimEnd()}…` : sentence;
10528
+ }
10529
+ function buildCatalogue(tools) {
10530
+ const byName = new Map(tools.map((t) => [t.name, t]));
10531
+ const placed = /* @__PURE__ */ new Set();
10532
+ const groups$1 = [];
10533
+ const toEntry = (tool) => ({
10534
+ name: tool.name,
10535
+ title: tool.title ?? tool.name,
10536
+ access: tool.annotations?.readOnlyHint === true ? "read" : "write",
10537
+ summary: summarise(tool)
10538
+ });
10539
+ for (const group of groups) {
10540
+ const entries = group.tools.map((name) => byName.get(name)).filter((t) => t !== void 0).map(toEntry);
10541
+ entries.forEach((e) => placed.add(e.name));
10542
+ if (entries.length > 0) groups$1.push({
10543
+ name: group.name,
10544
+ tools: entries
10545
+ });
10546
+ }
10547
+ const unmapped = tools.filter((t) => !placed.has(t.name)).map(toEntry);
10548
+ if (unmapped.length > 0) groups$1.push({
10549
+ name: OTHER_GROUP,
10550
+ tools: unmapped
10551
+ });
10552
+ const all = groups$1.flatMap((g) => g.tools);
10553
+ return {
10554
+ toolCount: all.length,
10555
+ readCount: all.filter((t) => t.access === "read").length,
10556
+ writeCount: all.filter((t) => t.access === "write").length,
10557
+ groups: groups$1
10558
+ };
10559
+ }
10560
+ //#endregion
10561
+ export { buildServer as n, buildCatalogue as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.6355",
3
+ "version": "0.1.0-beta.6359",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {