@sodiumhq/mcp-pm 0.1.0-beta.2600 → 0.1.0-beta.2603

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.
Files changed (3) hide show
  1. package/README.md +4 -0
  2. package/dist/index.js +268 -18
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -66,6 +66,10 @@ Then ask: *"give me a summary of my practice"*.
66
66
  - **`list_proposals`** and **`list_engagement_letters`** — aliased tools for the same underlying engagement pipeline (pre-acceptance vs signed). Filter by status, client, date range.
67
67
  - **`get_proposal_summary`** and **`get_engagement_letter_summary`** — aliased drill-ins: identity + client + value breakdown + services with pricing + PDFs + email history + acceptance record
68
68
 
69
+ **Services**
70
+ - **`list_services`** — find and count services in the practice's catalogue by search, category, applicable client type, or archived status. Answers "what do we offer?", "list our tax services", "what do we offer limited companies?", "how many active services do we have?"
71
+ - **`get_service_details`** — drill into one service: identity + applicable client types + HMRC agent authorisations + pricing options per billing frequency + custom tiers + professional clearance items + pricing-factor overview. Enables service-audit workflows like "is Self Assessment correctly restricted to Individual clients?"
72
+
69
73
  **Team**
70
74
  - **`list_users`** — find team members by name, email, role, or status — supports "who is Jane?", "list all partners", "who has been invited but not joined?"
71
75
 
package/dist/index.js CHANGED
@@ -787,6 +787,38 @@ const getPracticeDetails = (options) => (options.client ?? client).get({
787
787
  ...options
788
788
  });
789
789
  /**
790
+ * List BillableServices
791
+ *
792
+ * Lists all BillableServices for the given tenant.
793
+ */
794
+ const listBillableServices = (options) => (options.client ?? client).get({
795
+ security: [{
796
+ name: "x-api-key",
797
+ type: "apiKey"
798
+ }, {
799
+ scheme: "bearer",
800
+ type: "http"
801
+ }],
802
+ url: "/tenants/{tenant}/services",
803
+ ...options
804
+ });
805
+ /**
806
+ * Get BillableService
807
+ *
808
+ * Gets a BillableService for the specified tenant.
809
+ */
810
+ const getBillableService = (options) => (options.client ?? client).get({
811
+ security: [{
812
+ name: "x-api-key",
813
+ type: "apiKey"
814
+ }, {
815
+ scheme: "bearer",
816
+ type: "http"
817
+ }],
818
+ url: "/tenants/{tenant}/services/{code}",
819
+ ...options
820
+ });
821
+ /**
790
822
  * List Notes for Task
791
823
  *
792
824
  * Lists notes for the specified task. By default only returns task-level notes. Set includeStepNotes=true to also include notes attached to workflow steps.
@@ -1099,6 +1131,32 @@ var SodiumApiClient = class {
1099
1131
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list users");
1100
1132
  return data;
1101
1133
  }
1134
+ async listServices(query = {}) {
1135
+ const correlationId = randomUUID();
1136
+ const { data, error, response } = await listBillableServices({
1137
+ path: { tenant: this.ctx.tenant },
1138
+ query: {
1139
+ ...query,
1140
+ limit: query.limit ?? 10,
1141
+ offset: query.offset ?? 0
1142
+ },
1143
+ headers: { "X-Correlation-Id": correlationId }
1144
+ });
1145
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list services");
1146
+ return data;
1147
+ }
1148
+ async getService(code) {
1149
+ const correlationId = randomUUID();
1150
+ const { data, error, response } = await getBillableService({
1151
+ path: {
1152
+ tenant: this.ctx.tenant,
1153
+ code
1154
+ },
1155
+ headers: { "X-Correlation-Id": correlationId }
1156
+ });
1157
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get service ${code}`);
1158
+ return data;
1159
+ }
1102
1160
  async listTasks(query = {}) {
1103
1161
  const correlationId = randomUUID();
1104
1162
  const { data, error, response } = await listTaskItems({
@@ -1255,7 +1313,7 @@ async function buildInstructions(api) {
1255
1313
  }
1256
1314
  //#endregion
1257
1315
  //#region ../mcp-core/src/tools/get-practice-details.ts
1258
- function format$3(tenant, practice) {
1316
+ function format$4(tenant, practice) {
1259
1317
  const lines = [];
1260
1318
  lines.push(`Practice: ${practice.name}`);
1261
1319
  lines.push(`Tenant: ${tenant.name} (${tenant.code})`);
@@ -1289,7 +1347,7 @@ async function handleGetPracticeDetails(api) {
1289
1347
  const [tenant, practice] = await Promise.all([api.getTenantDetails(), api.getPracticeDetails()]);
1290
1348
  return { content: [{
1291
1349
  type: "text",
1292
- text: format$3(tenant, practice)
1350
+ text: format$4(tenant, practice)
1293
1351
  }] };
1294
1352
  } catch (error) {
1295
1353
  return {
@@ -1319,7 +1377,7 @@ const typeEnum = z.enum([
1319
1377
  "Charity",
1320
1378
  "SoleTrader"
1321
1379
  ]);
1322
- const sortByEnum$2 = z.enum(["Name", "InternalReference"]);
1380
+ const sortByEnum$3 = z.enum(["Name", "InternalReference"]);
1323
1381
  const ListClientsInputSchema = {
1324
1382
  search: z.string().min(3, "Search must be at least 3 characters when provided").optional().describe("Free-text search across client code, name, and internal reference. Minimum 3 characters. Omit to browse by filter only."),
1325
1383
  status: z.array(statusEnum$2).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
@@ -1329,7 +1387,7 @@ const ListClientsInputSchema = {
1329
1387
  associateCode: z.array(z.string()).optional().describe("Filter by assigned associate user codes."),
1330
1388
  serviceCode: z.array(z.string()).optional().describe("Filter by billable service codes that clients have assigned."),
1331
1389
  savedFilter: z.string().optional().describe("Code of a user-saved filter to apply. Other filter parameters override fields from the saved filter."),
1332
- sortBy: sortByEnum$2.optional().describe("Field to sort by. Defaults to Name."),
1390
+ sortBy: sortByEnum$3.optional().describe("Field to sort by. Defaults to Name."),
1333
1391
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
1334
1392
  limit: z.number().int().min(0).max(50).optional().describe("Maximum number of clients to return per page. Default 10, max 50. Pass 0 to return only the total count without any client data — use this for 'how many X?' questions so the API doesn't fetch a full page just to be counted."),
1335
1393
  offset: z.number().int().min(0).optional().describe("Number of records to skip for pagination. Default 0.")
@@ -1347,20 +1405,20 @@ async function handleListClients(api, args) {
1347
1405
  const items = result.data ?? [];
1348
1406
  const total = result.totalCount ?? items.length;
1349
1407
  if (args.limit === 0) {
1350
- const desc = describeFilters$3(args);
1408
+ const desc = describeFilters$4(args);
1351
1409
  return { content: [{
1352
1410
  type: "text",
1353
1411
  text: desc ? `Total: ${total} client${total === 1 ? "" : "s"} matching ${desc}.` : `Total: ${total} client${total === 1 ? "" : "s"}.`
1354
1412
  }] };
1355
1413
  }
1356
1414
  if (items.length === 0) {
1357
- const desc = describeFilters$3(args);
1415
+ const desc = describeFilters$4(args);
1358
1416
  return { content: [{
1359
1417
  type: "text",
1360
1418
  text: desc ? `No clients match ${desc}.` : "No clients found."
1361
1419
  }] };
1362
1420
  }
1363
- const desc = describeFilters$3(args);
1421
+ const desc = describeFilters$4(args);
1364
1422
  const lines = [
1365
1423
  desc ? total > items.length ? `Found ${total} clients matching ${desc} (showing ${items.length}):` : `Found ${items.length} client${items.length === 1 ? "" : "s"} matching ${desc}:` : total > items.length ? `Showing ${items.length} of ${total} clients:` : `${items.length} client${items.length === 1 ? "" : "s"}:`,
1366
1424
  "",
@@ -1384,7 +1442,7 @@ async function handleListClients(api, args) {
1384
1442
  };
1385
1443
  }
1386
1444
  }
1387
- function describeFilters$3(args) {
1445
+ function describeFilters$4(args) {
1388
1446
  const parts = [];
1389
1447
  if (args.search) parts.push(`search "${args.search}"`);
1390
1448
  if (args.status?.length) parts.push(`status ${args.status.join("/")}`);
@@ -1430,7 +1488,7 @@ async function handleGetClientSummary(api, { code }) {
1430
1488
  }
1431
1489
  return { content: [{
1432
1490
  type: "text",
1433
- text: format$2({
1491
+ text: format$3({
1434
1492
  client: clientResult.value,
1435
1493
  contacts: extract(contactsResult),
1436
1494
  services: extract(servicesResult),
@@ -1453,7 +1511,7 @@ function extract(result) {
1453
1511
  if (result.status !== "fulfilled") return [];
1454
1512
  return result.value.data ?? [];
1455
1513
  }
1456
- function format$2(input) {
1514
+ function format$3(input) {
1457
1515
  const { client, contacts, services, businessDetails, clientDates, overdueTasks, upcomingTasks, gaps } = input;
1458
1516
  const lines = [];
1459
1517
  const name = client.name ?? "(no name)";
@@ -1570,14 +1628,14 @@ async function handleGetEngagementSummary(api, { code }) {
1570
1628
  const engagement = engagementResult.value;
1571
1629
  return { content: [{
1572
1630
  type: "text",
1573
- text: format$1({
1631
+ text: format$2({
1574
1632
  engagement,
1575
1633
  emails: emailsResult.status === "fulfilled" ? emailsResult.value : [],
1576
1634
  gaps: emailsResult.status === "rejected" ? ["email history"] : []
1577
1635
  })
1578
1636
  }] };
1579
1637
  }
1580
- function format$1(input) {
1638
+ function format$2(input) {
1581
1639
  const { engagement: e, emails, gaps } = input;
1582
1640
  const lines = [];
1583
1641
  const code = e.code ?? "(no code)";
@@ -1597,7 +1655,7 @@ function format$1(input) {
1597
1655
  const services = e.proposalServices ?? [];
1598
1656
  if (services.length > 0) {
1599
1657
  lines.push("", `--- Services (${services.length}) ---`);
1600
- for (const s of services) lines.push(`- ${formatService(s)}`);
1658
+ for (const s of services) lines.push(`- ${formatService$1(s)}`);
1601
1659
  }
1602
1660
  if (e.acceptance && e.status === "Accepted") {
1603
1661
  lines.push("", "--- Acceptance ---");
@@ -1632,7 +1690,7 @@ function formatValues(e) {
1632
1690
  if (e.totalValue && e.totalValue > 0) out.push(`Total contracted value: £${formatMoney$1(e.totalValue)}`);
1633
1691
  return out;
1634
1692
  }
1635
- function formatService(s) {
1693
+ function formatService$1(s) {
1636
1694
  const name = s.billableService?.name ?? "(unnamed)";
1637
1695
  const freq = s.billingFrequency ? ` ${s.billingFrequency}` : "";
1638
1696
  if (s.effectivePrice !== void 0 && s.effectivePrice !== null) return `${name} — £${formatMoney$1(s.effectivePrice)}${freq}`;
@@ -1648,6 +1706,83 @@ function formatMoney$1(n) {
1648
1706
  });
1649
1707
  }
1650
1708
  //#endregion
1709
+ //#region ../mcp-core/src/tools/get-service-details.ts
1710
+ const GetServiceDetailsInputSchema = { code: z.string().min(1, "Service code is required").describe("The service code (identifier). Usually discovered via list_services first.") };
1711
+ async function handleGetServiceDetails(api, { code }) {
1712
+ try {
1713
+ return { content: [{
1714
+ type: "text",
1715
+ text: format$1(await api.getService(code))
1716
+ }] };
1717
+ } catch (error) {
1718
+ return {
1719
+ content: [{
1720
+ type: "text",
1721
+ text: error instanceof SodiumApiError ? `Error getting service: ${error.message} (correlation: ${error.correlationId})` : `Error getting service: ${error instanceof Error ? error.message : String(error)}`
1722
+ }],
1723
+ isError: true
1724
+ };
1725
+ }
1726
+ }
1727
+ function format$1(s) {
1728
+ const lines = [];
1729
+ const name = s.name ?? "(no name)";
1730
+ const code = s.code ?? "(no code)";
1731
+ lines.push(`Service: ${name} (${code})`);
1732
+ const meta = [];
1733
+ if (s.category) meta.push(s.category);
1734
+ meta.push(s.isArchived ? "Archived" : "Active");
1735
+ if (s.vatRate) meta.push(`VAT: ${s.vatRate}`);
1736
+ if (s.pricingMode) meta.push(`Pricing: ${s.pricingMode}`);
1737
+ if (meta.length > 0) lines.push(meta.join(" · "));
1738
+ if (s.accountingCode) lines.push(`Accounting code: ${s.accountingCode}`);
1739
+ if (s.description) lines.push(`Description: ${s.description}`);
1740
+ if (s.defaultManagedByUser) lines.push(`Default manager: ${s.defaultManagedByUser.name} (${s.defaultManagedByUser.code})`);
1741
+ lines.push("", "--- Applicable Client Types ---");
1742
+ if (!s.clientTypes || s.clientTypes.length === 0) lines.push("All client types (no restriction configured).");
1743
+ else for (const ct of s.clientTypes) lines.push(`- ${ct}`);
1744
+ lines.push("", `--- HMRC Agent Authorisations (${s.agentAuthorisations?.length ?? 0}) ---`);
1745
+ if (!s.agentAuthorisations || s.agentAuthorisations.length === 0) lines.push("None configured.");
1746
+ else for (const a of s.agentAuthorisations) lines.push(`- ${a}`);
1747
+ lines.push("", `--- Pricing Options (${s.pricing?.length ?? 0}) ---`);
1748
+ if (!s.pricing || s.pricing.length === 0) lines.push("No pricing options configured.");
1749
+ else for (const p of s.pricing) lines.push(formatPricingOption(p));
1750
+ if (s.pricingMode === "CustomTiers" && s.pricingTiers && s.pricingTiers.length > 0) {
1751
+ lines.push("", `--- Custom Pricing Tiers (${s.pricingTiers.length}) ---`);
1752
+ const tiers = [...s.pricingTiers].sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0));
1753
+ for (const t of tiers) lines.push(formatTier(t));
1754
+ }
1755
+ lines.push("", `--- Professional Clearance Items (${s.pcrItems?.length ?? 0}) ---`);
1756
+ if (!s.pcrItems || s.pcrItems.length === 0) lines.push("None configured.");
1757
+ else for (const item of s.pcrItems) lines.push(formatCodeAndName$1(item));
1758
+ const factorCount = s.pricingFactors?.length ?? 0;
1759
+ lines.push("", "--- Pricing Factors ---");
1760
+ if (factorCount === 0) lines.push("None configured.");
1761
+ else {
1762
+ lines.push(`${factorCount} pricing factor${factorCount === 1 ? "" : "s"} configured.`);
1763
+ for (const f of s.pricingFactors ?? []) if (f.description) lines.push(`- ${f.description}`);
1764
+ lines.push("Note: exact pricing computed from factors is available via the proposal tools (get_proposal_summary / get_engagement_letter_summary) — those return effectivePrice per service.");
1765
+ }
1766
+ if (s.recurringTaskCount && s.recurringTaskCount > 0) lines.push("", `Recurring tasks using this service: ${s.recurringTaskCount}`);
1767
+ return lines.join("\n");
1768
+ }
1769
+ function formatPricingOption(p) {
1770
+ const freq = p.frequency ?? "(no frequency)";
1771
+ const price = typeof p.price === "number" ? `£${p.price.toFixed(2)}` : "(no price)";
1772
+ const rangeCount = p.revenueRangeOverrides?.length ?? 0;
1773
+ const tierCount = p.tierOverrides?.length ?? 0;
1774
+ const overrides = [];
1775
+ if (rangeCount > 0) overrides.push(`${rangeCount} revenue-range override${rangeCount === 1 ? "" : "s"}`);
1776
+ if (tierCount > 0) overrides.push(`${tierCount} tier override${tierCount === 1 ? "" : "s"}`);
1777
+ return `- ${freq}: ${price}${overrides.length > 0 ? ` (+ ${overrides.join(", ")})` : ""}`;
1778
+ }
1779
+ function formatTier(t) {
1780
+ return `- ${t.name ?? "(unnamed)"} (${t.code ?? "(no code)"})`;
1781
+ }
1782
+ function formatCodeAndName$1(item) {
1783
+ return `- ${item.name ?? "(unnamed)"} (${item.code ?? "(no code)"})`;
1784
+ }
1785
+ //#endregion
1651
1786
  //#region ../mcp-core/src/tools/get-task-context.ts
1652
1787
  const GetTaskContextInputSchema = { code: z.string().min(1, "Task code is required").describe("The task code (identifier). Usually discovered via list_tasks first, or supplied directly by the user when they quote a task code.") };
1653
1788
  async function handleGetTaskContext(api, { code }) {
@@ -1845,20 +1980,20 @@ async function handleListEngagements(api, args) {
1845
1980
  const items = result.data ?? [];
1846
1981
  const total = result.totalCount ?? items.length;
1847
1982
  if (args.limit === 0) {
1848
- const desc = describeFilters$2(args);
1983
+ const desc = describeFilters$3(args);
1849
1984
  return { content: [{
1850
1985
  type: "text",
1851
1986
  text: desc ? `Total: ${total} engagement${total === 1 ? "" : "s"} matching ${desc}.` : `Total: ${total} engagement${total === 1 ? "" : "s"}.`
1852
1987
  }] };
1853
1988
  }
1854
1989
  if (items.length === 0) {
1855
- const desc = describeFilters$2(args);
1990
+ const desc = describeFilters$3(args);
1856
1991
  return { content: [{
1857
1992
  type: "text",
1858
1993
  text: desc ? `No engagements match ${desc}.` : "No engagements found."
1859
1994
  }] };
1860
1995
  }
1861
- const desc = describeFilters$2(args);
1996
+ const desc = describeFilters$3(args);
1862
1997
  const lines = [
1863
1998
  desc ? total > items.length ? `Found ${total} engagements matching ${desc} (showing ${items.length}):` : `Found ${items.length} engagement${items.length === 1 ? "" : "s"} matching ${desc}:` : total > items.length ? `Showing ${items.length} of ${total} engagements:` : `${items.length} engagement${items.length === 1 ? "" : "s"}:`,
1864
1999
  "",
@@ -1896,7 +2031,7 @@ function formatMoney(n) {
1896
2031
  maximumFractionDigits: 2
1897
2032
  });
1898
2033
  }
1899
- function describeFilters$2(args) {
2034
+ function describeFilters$3(args) {
1900
2035
  const parts = [];
1901
2036
  if (args.status) parts.push(`status ${args.status}`);
1902
2037
  if (args.search) parts.push(`search "${args.search}"`);
@@ -1905,6 +2040,101 @@ function describeFilters$2(args) {
1905
2040
  return parts.join(", ");
1906
2041
  }
1907
2042
  //#endregion
2043
+ //#region ../mcp-core/src/tools/list-services.ts
2044
+ const categoryEnum = z.enum([
2045
+ "Other",
2046
+ "CoreAccounting",
2047
+ "Tax",
2048
+ "Payroll",
2049
+ "CompanySecretarial",
2050
+ "Advisory",
2051
+ "SoftwareAndTraining"
2052
+ ]);
2053
+ const clientTypeEnum = z.enum([
2054
+ "PrivateLimitedCompany",
2055
+ "PublicLimitedCompany",
2056
+ "LimitedLiabilityPartnership",
2057
+ "Partnership",
2058
+ "Individual",
2059
+ "Trust",
2060
+ "Charity",
2061
+ "SoleTrader"
2062
+ ]);
2063
+ const sortByEnum$2 = z.enum([
2064
+ "Name",
2065
+ "Category",
2066
+ "AccountingCode"
2067
+ ]);
2068
+ const ListServicesInputSchema = {
2069
+ search: z.string().min(3, "Search must be at least 3 characters when provided").optional().describe("Free-text search across service code and name. Minimum 3 characters. Use for 'find our VAT services' or 'does the practice have a bookkeeping service?' when the exact code isn't known."),
2070
+ category: categoryEnum.optional().describe("Filter by service category. Use 'Tax' for 'all tax services', 'Payroll' for payroll, 'CoreAccounting' for year-end / accounts / bookkeeping, 'CompanySecretarial' for confirmation statements / registered office, 'Advisory' for consulting / planning, 'SoftwareAndTraining' for software setup / training. Single value — to see multiple categories, call once per category or omit to see all."),
2071
+ clientType: clientTypeEnum.optional().describe("Filter by the client type the service applies to. Use for service-audit questions like 'which services do we offer to individuals?' or 'what do we offer private limited companies?'. Returns services configured for that client type plus any service with no client-type restriction (those apply to everyone)."),
2072
+ isArchived: z.boolean().optional().describe("Filter by archived status. Omit (default) to return everything; pass false for active services only; pass true for the archive. Most practice-manager questions ('what do we offer?') want isArchived=false."),
2073
+ sortBy: sortByEnum$2.optional().describe("Field to sort by. Defaults to Name. Use 'Category' to group the output by category, 'AccountingCode' when reconciling against a chart of accounts."),
2074
+ sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
2075
+ limit: z.number().int().min(0).max(50).optional().describe("Maximum number of services per page. Default 10, max 50. Pass 0 to return only the total count without any service data — use for 'how many services do we offer?' questions."),
2076
+ offset: z.number().int().min(0).optional().describe("Pagination offset. Default 0.")
2077
+ };
2078
+ function formatService(s) {
2079
+ const code = s.code ?? "(no code)";
2080
+ const name = s.name ?? "(unnamed)";
2081
+ const category = s.category ? ` · ${s.category}` : "";
2082
+ const pricing = s.pricingMode ? ` · ${s.pricingMode}` : "";
2083
+ return `- ${name} (${code})${s.isArchived ? " [ARCHIVED]" : ""}${category}${pricing}${s.accountingCode ? ` · a/c ${s.accountingCode}` : ""}${s.recurringTaskCount && s.recurringTaskCount > 0 ? ` · ${s.recurringTaskCount} recurring task${s.recurringTaskCount === 1 ? "" : "s"}` : ""}`;
2084
+ }
2085
+ async function handleListServices(api, args) {
2086
+ try {
2087
+ const query = args;
2088
+ const result = await api.listServices(query);
2089
+ const items = result.data ?? [];
2090
+ const total = result.totalCount ?? items.length;
2091
+ if (args.limit === 0) {
2092
+ const desc = describeFilters$2(args);
2093
+ return { content: [{
2094
+ type: "text",
2095
+ text: desc ? `Total: ${total} service${total === 1 ? "" : "s"} matching ${desc}.` : `Total: ${total} service${total === 1 ? "" : "s"}.`
2096
+ }] };
2097
+ }
2098
+ if (items.length === 0) {
2099
+ const desc = describeFilters$2(args);
2100
+ return { content: [{
2101
+ type: "text",
2102
+ text: desc ? `No services match ${desc}.` : "No services found."
2103
+ }] };
2104
+ }
2105
+ const desc = describeFilters$2(args);
2106
+ const lines = [
2107
+ desc ? total > items.length ? `Found ${total} services matching ${desc} (showing ${items.length}):` : `Found ${items.length} service${items.length === 1 ? "" : "s"} matching ${desc}:` : total > items.length ? `Showing ${items.length} of ${total} services:` : `${items.length} service${items.length === 1 ? "" : "s"}:`,
2108
+ "",
2109
+ ...items.map(formatService)
2110
+ ];
2111
+ if (result.hasMore) {
2112
+ const nextOffset = (args.offset ?? 0) + items.length;
2113
+ lines.push("", `More results available — call again with offset: ${nextOffset} to see the next page.`);
2114
+ }
2115
+ return { content: [{
2116
+ type: "text",
2117
+ text: lines.join("\n")
2118
+ }] };
2119
+ } catch (error) {
2120
+ return {
2121
+ content: [{
2122
+ type: "text",
2123
+ text: error instanceof SodiumApiError ? `Error listing services: ${error.message} (correlation: ${error.correlationId})` : `Error listing services: ${error instanceof Error ? error.message : String(error)}`
2124
+ }],
2125
+ isError: true
2126
+ };
2127
+ }
2128
+ }
2129
+ function describeFilters$2(args) {
2130
+ const parts = [];
2131
+ if (args.search) parts.push(`search "${args.search}"`);
2132
+ if (args.category) parts.push(`category ${args.category}`);
2133
+ if (args.clientType) parts.push(`clientType ${args.clientType}`);
2134
+ if (args.isArchived !== void 0) parts.push(`isArchived=${args.isArchived}`);
2135
+ return parts.join(", ");
2136
+ }
2137
+ //#endregion
1908
2138
  //#region ../mcp-core/src/tools/list-tasks.ts
1909
2139
  const statusEnum$1 = z.enum([
1910
2140
  "NotStarted",
@@ -2229,6 +2459,26 @@ async function buildServer(config) {
2229
2459
  openWorldHint: true
2230
2460
  }
2231
2461
  }, (args) => handleGetEngagementSummary(api, args));
2462
+ server.registerTool("list_services", {
2463
+ title: "List / search / filter the practice's service catalogue",
2464
+ description: "List the practice's configured billable services with any combination of: search (3+ chars over code and name), category (Tax / Payroll / CoreAccounting / CompanySecretarial / Advisory / SoftwareAndTraining / Other — single value), clientType (PrivateLimitedCompany / PublicLimitedCompany / LimitedLiabilityPartnership / Partnership / Individual / Trust / Charity / SoleTrader — single value; matches services configured for that type plus services with no client-type restriction), isArchived (omit for all, false for active only, true for archive), sort (Name / Category / AccountingCode), and pagination. Use for: 'what services do we offer?' (no filter, isArchived=false), 'list our tax services' (category=Tax), 'what do we offer limited companies?' (clientType=PrivateLimitedCompany, isArchived=false), 'how many active services do we have?' (isArchived=false, limit=0 for count-only). Returns up to 50 per page. Follow up with get_service_details for one service's full configuration (client types, pricing options, clearance items, HMRC authorisations).",
2465
+ inputSchema: ListServicesInputSchema,
2466
+ annotations: {
2467
+ readOnlyHint: true,
2468
+ idempotentHint: true,
2469
+ openWorldHint: true
2470
+ }
2471
+ }, (args) => handleListServices(api, args));
2472
+ server.registerTool("get_service_details", {
2473
+ title: "Get a single service's full configuration",
2474
+ 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?'",
2475
+ inputSchema: GetServiceDetailsInputSchema,
2476
+ annotations: {
2477
+ readOnlyHint: true,
2478
+ idempotentHint: true,
2479
+ openWorldHint: true
2480
+ }
2481
+ }, (args) => handleGetServiceDetails(api, args));
2232
2482
  server.registerTool("list_users", {
2233
2483
  title: "List / search / filter tenant users",
2234
2484
  description: "Find tenant users by name, email, role, or status. Use this when the user mentioned in a request isn't present in the startup roster (large teams have more than the top 20 active members shown there), or when filtering is needed beyond name resolution. Typical queries: 'find Jane' (search), 'list all partners' (isPartner=true), 'who's been invited but not joined yet?' (status=Invited), 'how many active users do we have?' (status=Active, limit=0). For 'how many X?' questions, pass limit=0 to get just the total count without fetching any user data.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.2600",
3
+ "version": "0.1.0-beta.2603",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {