@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.
- package/README.md +4 -0
- package/dist/index.js +268 -18
- 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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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.",
|