@sodiumhq/mcp-pm 0.1.0-beta.2593 → 0.1.0-beta.2596

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 +15 -2
  2. package/dist/index.js +350 -8
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -47,10 +47,23 @@ Then ask: *"give me a summary of my practice"*.
47
47
 
48
48
  ## What it can do today
49
49
 
50
+ **Practice**
50
51
  - **`get_practice_details`** — consolidated practice overview (counts, connections, settings)
52
+
53
+ **Clients**
51
54
  - **`list_clients`** — list and filter clients by search, status, type, assignee, services, saved filters
52
- - **`get_client_summary`** — one-call composite: client identity + contacts + active services + overdue + upcoming tasks
53
- - **`list_tasks`** — list and filter tasks by user (including "my tasks"), client, status, date range, overdue, category, team — covers both individual and practice-manager workflows
55
+ - **`get_client_summary`** — one-call composite for a single client: identity + contacts + active services with pricing + business details (company number, VAT, UTR, trading address) + key statutory dates (year-end, accounts due, VAT return due, confirmation statement due) + overdue tasks + tasks due in next 7 days
56
+
57
+ **Tasks**
58
+ - **`list_tasks`** — find and count tasks by assignee (including "my tasks"), client, status, overdue, date range, category, team, or workflow. Answers "what's on my plate?", "what's Jane working on?", "what's overdue for ACME?", "how many tasks are due this week?"
59
+ - **`get_task_context`** — one-call composite for a single task: details + notes + workflow steps + primary client + category
60
+
61
+ **Proposals and engagements**
62
+ - **`list_proposals`** and **`list_engagement_letters`** — aliased tools for the same underlying engagement pipeline (pre-acceptance vs signed). Filter by status, client, date range.
63
+ - **`get_proposal_summary`** and **`get_engagement_letter_summary`** — aliased drill-ins: identity + client + value breakdown + services with pricing + PDFs + email history + acceptance record
64
+
65
+ **Team**
66
+ - **`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?"
54
67
 
55
68
  More tools land iteratively as the beta progresses.
56
69
 
package/dist/index.js CHANGED
@@ -719,6 +719,57 @@ const getClient = (options) => (options.client ?? client).get({
719
719
  ...options
720
720
  });
721
721
  /**
722
+ * List Engagements
723
+ *
724
+ * Lists all Engagements for the given tenant.
725
+ */
726
+ const listEngagements = (options) => (options.client ?? client).get({
727
+ security: [{
728
+ name: "x-api-key",
729
+ type: "apiKey"
730
+ }, {
731
+ scheme: "bearer",
732
+ type: "http"
733
+ }],
734
+ url: "/tenants/{tenant}/engagements",
735
+ ...options
736
+ });
737
+ /**
738
+ * Get Engagement
739
+ *
740
+ * Gets a Engagement for the specified tenant.
741
+ */
742
+ const getEngagement = (options) => (options.client ?? client).get({
743
+ security: [{
744
+ name: "x-api-key",
745
+ type: "apiKey"
746
+ }, {
747
+ scheme: "bearer",
748
+ type: "http"
749
+ }],
750
+ url: "/tenants/{tenant}/engagements/{code}",
751
+ ...options
752
+ });
753
+ /**
754
+ * Get Engagement Emails
755
+ *
756
+ * Gets all emails that have been sent for a specific engagement.
757
+ *
758
+ * Returns a list of emails with their message IDs and sent dates.
759
+ * The list is ordered by most recent first.
760
+ */
761
+ const getEngagementEmails = (options) => (options.client ?? client).get({
762
+ security: [{
763
+ name: "x-api-key",
764
+ type: "apiKey"
765
+ }, {
766
+ scheme: "bearer",
767
+ type: "http"
768
+ }],
769
+ url: "/tenants/{tenant}/engagements/{code}/email",
770
+ ...options
771
+ });
772
+ /**
722
773
  * Get Practice Details
723
774
  *
724
775
  * Returns the practice details for the specified tenant
@@ -1072,6 +1123,44 @@ var SodiumApiClient = class {
1072
1123
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `list notes for task ${taskCode}`);
1073
1124
  return data;
1074
1125
  }
1126
+ async listEngagements(query = {}) {
1127
+ const correlationId = randomUUID();
1128
+ const { data, error, response } = await listEngagements({
1129
+ path: { tenant: this.ctx.tenant },
1130
+ query: {
1131
+ ...query,
1132
+ limit: query.limit ?? 10,
1133
+ offset: query.offset ?? 0
1134
+ },
1135
+ headers: { "X-Correlation-Id": correlationId }
1136
+ });
1137
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list engagements");
1138
+ return data;
1139
+ }
1140
+ async getEngagement(code) {
1141
+ const correlationId = randomUUID();
1142
+ const { data, error, response } = await getEngagement({
1143
+ path: {
1144
+ tenant: this.ctx.tenant,
1145
+ code
1146
+ },
1147
+ headers: { "X-Correlation-Id": correlationId }
1148
+ });
1149
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get engagement ${code}`);
1150
+ return data;
1151
+ }
1152
+ async getEngagementEmails(code) {
1153
+ const correlationId = randomUUID();
1154
+ const { data, error, response } = await getEngagementEmails({
1155
+ path: {
1156
+ tenant: this.ctx.tenant,
1157
+ code
1158
+ },
1159
+ headers: { "X-Correlation-Id": correlationId }
1160
+ });
1161
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get emails for engagement ${code}`);
1162
+ return data;
1163
+ }
1075
1164
  async getTaskWorkflowGroups(taskCode) {
1076
1165
  const correlationId = randomUUID();
1077
1166
  const { data, error, response } = await getTaskWorkflowGroups({
@@ -1137,7 +1226,7 @@ async function buildInstructions(api) {
1137
1226
  }
1138
1227
  //#endregion
1139
1228
  //#region ../mcp-core/src/tools/get-practice-details.ts
1140
- function format$2(tenant, practice) {
1229
+ function format$3(tenant, practice) {
1141
1230
  const lines = [];
1142
1231
  lines.push(`Practice: ${practice.name}`);
1143
1232
  lines.push(`Tenant: ${tenant.name} (${tenant.code})`);
@@ -1171,7 +1260,7 @@ async function handleGetPracticeDetails(api) {
1171
1260
  const [tenant, practice] = await Promise.all([api.getTenantDetails(), api.getPracticeDetails()]);
1172
1261
  return { content: [{
1173
1262
  type: "text",
1174
- text: format$2(tenant, practice)
1263
+ text: format$3(tenant, practice)
1175
1264
  }] };
1176
1265
  } catch (error) {
1177
1266
  return {
@@ -1229,20 +1318,20 @@ async function handleListClients(api, args) {
1229
1318
  const items = result.data ?? [];
1230
1319
  const total = result.totalCount ?? items.length;
1231
1320
  if (args.limit === 0) {
1232
- const desc = describeFilters$2(args);
1321
+ const desc = describeFilters$3(args);
1233
1322
  return { content: [{
1234
1323
  type: "text",
1235
1324
  text: desc ? `Total: ${total} client${total === 1 ? "" : "s"} matching ${desc}.` : `Total: ${total} client${total === 1 ? "" : "s"}.`
1236
1325
  }] };
1237
1326
  }
1238
1327
  if (items.length === 0) {
1239
- const desc = describeFilters$2(args);
1328
+ const desc = describeFilters$3(args);
1240
1329
  return { content: [{
1241
1330
  type: "text",
1242
1331
  text: desc ? `No clients match ${desc}.` : "No clients found."
1243
1332
  }] };
1244
1333
  }
1245
- const desc = describeFilters$2(args);
1334
+ const desc = describeFilters$3(args);
1246
1335
  const lines = [
1247
1336
  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"}:`,
1248
1337
  "",
@@ -1266,7 +1355,7 @@ async function handleListClients(api, args) {
1266
1355
  };
1267
1356
  }
1268
1357
  }
1269
- function describeFilters$2(args) {
1358
+ function describeFilters$3(args) {
1270
1359
  const parts = [];
1271
1360
  if (args.search) parts.push(`search "${args.search}"`);
1272
1361
  if (args.status?.length) parts.push(`status ${args.status.join("/")}`);
@@ -1312,7 +1401,7 @@ async function handleGetClientSummary(api, { code }) {
1312
1401
  }
1313
1402
  return { content: [{
1314
1403
  type: "text",
1315
- text: format$1({
1404
+ text: format$2({
1316
1405
  client: clientResult.value,
1317
1406
  contacts: extract(contactsResult),
1318
1407
  services: extract(servicesResult),
@@ -1335,7 +1424,7 @@ function extract(result) {
1335
1424
  if (result.status !== "fulfilled") return [];
1336
1425
  return result.value.data ?? [];
1337
1426
  }
1338
- function format$1(input) {
1427
+ function format$2(input) {
1339
1428
  const { client, contacts, services, businessDetails, clientDates, overdueTasks, upcomingTasks, gaps } = input;
1340
1429
  const lines = [];
1341
1430
  const name = client.name ?? "(no name)";
@@ -1435,6 +1524,101 @@ function humanizeDateType(type) {
1435
1524
  }).join(" ");
1436
1525
  }
1437
1526
  //#endregion
1527
+ //#region ../mcp-core/src/tools/get-engagement-summary.ts
1528
+ 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.") };
1529
+ async function handleGetEngagementSummary(api, { code }) {
1530
+ const [engagementResult, emailsResult] = await Promise.allSettled([api.getEngagement(code), api.getEngagementEmails(code)]);
1531
+ if (engagementResult.status === "rejected") {
1532
+ const err = engagementResult.reason;
1533
+ return {
1534
+ content: [{
1535
+ type: "text",
1536
+ text: err instanceof SodiumApiError ? `Error getting engagement: ${err.message} (correlation: ${err.correlationId})` : `Error getting engagement: ${err instanceof Error ? err.message : String(err)}`
1537
+ }],
1538
+ isError: true
1539
+ };
1540
+ }
1541
+ const engagement = engagementResult.value;
1542
+ return { content: [{
1543
+ type: "text",
1544
+ text: format$1({
1545
+ engagement,
1546
+ emails: emailsResult.status === "fulfilled" ? emailsResult.value : [],
1547
+ gaps: emailsResult.status === "rejected" ? ["email history"] : []
1548
+ })
1549
+ }] };
1550
+ }
1551
+ function format$1(input) {
1552
+ const { engagement: e, emails, gaps } = input;
1553
+ const lines = [];
1554
+ const code = e.code ?? "(no code)";
1555
+ const typeLabel = e.typeName ?? e.type ?? "";
1556
+ lines.push(`Engagement: ${code}${typeLabel ? ` (${typeLabel})` : ""}`);
1557
+ if (e.status) lines.push(`Status: ${e.status}`);
1558
+ if (e.date) lines.push(`Date: ${e.date}`);
1559
+ if (e.client) lines.push(`Client: ${e.client.name ?? "(no name)"} (${e.client.code ?? "?"})`);
1560
+ const recipientName = [e.recipientFirstName, e.recipientLastName].filter(Boolean).join(" ");
1561
+ if (recipientName || e.recipientEmail) {
1562
+ const email = e.recipientEmail ? ` <${e.recipientEmail}>` : "";
1563
+ lines.push(`Recipient: ${recipientName || "(no name)"}${email}`);
1564
+ }
1565
+ if (e.lastViewed) lines.push(`Last viewed by client: ${e.lastViewed}`);
1566
+ const valueLines = formatValues(e);
1567
+ if (valueLines.length > 0) lines.push("", "--- Value ---", ...valueLines);
1568
+ const services = e.proposalServices ?? [];
1569
+ if (services.length > 0) {
1570
+ lines.push("", `--- Services (${services.length}) ---`);
1571
+ for (const s of services) lines.push(`- ${formatService(s)}`);
1572
+ }
1573
+ if (e.acceptance && e.status === "Accepted") {
1574
+ lines.push("", "--- Acceptance ---");
1575
+ if (e.acceptance.acceptedDate) lines.push(`Accepted on: ${e.acceptance.acceptedDate}`);
1576
+ if (e.acceptance.manuallyAccepted) lines.push("Accepted manually (recorded by practice, not via portal)");
1577
+ if (e.acceptance.acceptedIpAddress && !e.acceptance.manuallyAccepted) lines.push(`Client IP: ${e.acceptance.acceptedIpAddress}`);
1578
+ }
1579
+ const pdfs = [];
1580
+ if (e.hasProposalPdf) pdfs.push("proposal PDF uploaded");
1581
+ if (e.hasLofEPdf) pdfs.push("letter of engagement PDF uploaded");
1582
+ if (pdfs.length === 0) pdfs.push("no PDFs uploaded yet");
1583
+ lines.push("", `--- Documents ---`, ...pdfs.map((p) => `- ${p}`));
1584
+ if (emails.length > 0) {
1585
+ const sorted = [...emails].sort((a, b) => {
1586
+ const da = a.sentDate ?? "";
1587
+ return (b.sentDate ?? "").localeCompare(da);
1588
+ });
1589
+ lines.push("", `--- Email history (${emails.length}) ---`);
1590
+ for (const em of sorted.slice(0, 10)) lines.push(formatEmail(em));
1591
+ if (emails.length > 10) lines.push(`... and ${emails.length - 10} more`);
1592
+ }
1593
+ if (e.link) lines.push("", `Acceptance link: ${e.link}`);
1594
+ if (gaps.length > 0) lines.push("", `Note: could not load ${gaps.join(", ")} (partial failure). Retry for a complete picture.`);
1595
+ return lines.join("\n");
1596
+ }
1597
+ function formatValues(e) {
1598
+ const out = [];
1599
+ if (e.annualTotal && e.annualTotal > 0) out.push(`Annual: £${formatMoney$1(e.annualTotal)}`);
1600
+ if (e.quarterlyTotal && e.quarterlyTotal > 0) out.push(`Quarterly: £${formatMoney$1(e.quarterlyTotal)}`);
1601
+ if (e.monthlyTotal && e.monthlyTotal > 0) out.push(`Monthly: £${formatMoney$1(e.monthlyTotal)}`);
1602
+ if (e.oneOffTotal && e.oneOffTotal > 0) out.push(`One-off: £${formatMoney$1(e.oneOffTotal)}`);
1603
+ if (e.totalValue && e.totalValue > 0) out.push(`Total contracted value: £${formatMoney$1(e.totalValue)}`);
1604
+ return out;
1605
+ }
1606
+ function formatService(s) {
1607
+ const name = s.billableService?.name ?? "(unnamed)";
1608
+ const freq = s.billingFrequency ? ` ${s.billingFrequency}` : "";
1609
+ if (s.effectivePrice !== void 0 && s.effectivePrice !== null) return `${name} — £${formatMoney$1(s.effectivePrice)}${freq}`;
1610
+ return freq ? `${name} —${freq}` : name;
1611
+ }
1612
+ function formatEmail(em) {
1613
+ return `- ${em.sentDate ?? "(no date)"}: ${em.subject ?? "(no subject)"}${em.status ? ` · ${em.status}` : ""}${em.toRecipients?.length ? ` → ${em.toRecipients.join(", ")}` : ""}`;
1614
+ }
1615
+ function formatMoney$1(n) {
1616
+ return n.toLocaleString("en-GB", {
1617
+ minimumFractionDigits: 2,
1618
+ maximumFractionDigits: 2
1619
+ });
1620
+ }
1621
+ //#endregion
1438
1622
  //#region ../mcp-core/src/tools/get-task-context.ts
1439
1623
  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.") };
1440
1624
  async function handleGetTaskContext(api, { code }) {
@@ -1574,6 +1758,124 @@ function sumBy(arr, fn) {
1574
1758
  return arr.reduce((acc, cur) => acc + fn(cur), 0);
1575
1759
  }
1576
1760
  //#endregion
1761
+ //#region ../mcp-core/src/tools/list-engagements.ts
1762
+ const EngagementStatusEnum = z.enum([
1763
+ "Unsent",
1764
+ "Sent",
1765
+ "Viewed",
1766
+ "Accepted",
1767
+ "Rejected"
1768
+ ]);
1769
+ const EngagementSortFieldEnum = z.enum([
1770
+ "Client",
1771
+ "Code",
1772
+ "Date",
1773
+ "Status",
1774
+ "NumberOfServices",
1775
+ "TotalValue"
1776
+ ]);
1777
+ const DateRangeEnum = z.enum([
1778
+ "Today",
1779
+ "ThisWeek",
1780
+ "NextWeek",
1781
+ "Last7Days",
1782
+ "Next7Days",
1783
+ "Last30Days",
1784
+ "Next30Days",
1785
+ "PreviousMonth",
1786
+ "ThisMonth",
1787
+ "NextMonth",
1788
+ "PreviousQuarter",
1789
+ "ThisQuarter",
1790
+ "NextQuarter",
1791
+ "PreviousYear",
1792
+ "ThisYear",
1793
+ "NextYear",
1794
+ "YearToDate",
1795
+ "PreviousFinancialYear",
1796
+ "ThisFinancialYear",
1797
+ "NextFinancialYear",
1798
+ "FinancialYearToDate",
1799
+ "CustomDateRange"
1800
+ ]);
1801
+ const ListEngagementsInputSchema = {
1802
+ status: EngagementStatusEnum.optional().describe("Filter by engagement status. Unsent = draft, not yet sent. Sent = delivered, awaiting client action. Viewed = client opened but not yet responded. Accepted = client signed / agreed. Rejected = client declined. For 'proposals awaiting acceptance' use status=Sent (or run twice: Sent then Viewed). For 'signed letters of engagement' use status=Accepted. Omit to see every status."),
1803
+ search: z.string().min(3, "Search must be at least 3 characters").optional().describe("Search across engagement code, client name, and client code. Minimum 3 characters. Use for 'ACME's engagement' / 'proposal for Smith & Co' queries where you know the client name but not the engagement code."),
1804
+ dateRange: DateRangeEnum.optional().describe("Preset date range filtering by engagement date. Use Today / ThisWeek / Last7Days / Last30Days / ThisMonth / ThisQuarter / ThisYear etc. Use CustomDateRange with dateFrom+dateTo for arbitrary bounds. Omit to include engagements from any date."),
1805
+ dateFrom: z.string().optional().describe("Inclusive start date (YYYY-MM-DD). ONLY valid when dateRange=CustomDateRange — ignored otherwise."),
1806
+ dateTo: z.string().optional().describe("Inclusive end date (YYYY-MM-DD). ONLY valid when dateRange=CustomDateRange — ignored otherwise."),
1807
+ sortBy: EngagementSortFieldEnum.optional().describe("Field to sort by. Use Date for 'most recent proposals' (with sortDesc=true), TotalValue for 'highest-value engagements', Status for grouping by pipeline stage. Default server-side."),
1808
+ sortDesc: z.boolean().optional().describe("Sort descending when true, ascending when false/omitted. Pair with sortBy=Date and sortDesc=true for 'most recent first'."),
1809
+ limit: z.number().int().min(0).max(50).optional().describe("Max records to return (default 10, max 50). Pass limit=0 for 'how many proposals are awaiting acceptance?' / 'how many engagements did we send this month?' — returns just the total count without fetching any engagement data."),
1810
+ offset: z.number().int().min(0).optional().describe("Skip this many records. Use with limit for pagination.")
1811
+ };
1812
+ async function handleListEngagements(api, args) {
1813
+ try {
1814
+ const query = args;
1815
+ const result = await api.listEngagements(query);
1816
+ const items = result.data ?? [];
1817
+ const total = result.totalCount ?? items.length;
1818
+ if (args.limit === 0) {
1819
+ const desc = describeFilters$2(args);
1820
+ return { content: [{
1821
+ type: "text",
1822
+ text: desc ? `Total: ${total} engagement${total === 1 ? "" : "s"} matching ${desc}.` : `Total: ${total} engagement${total === 1 ? "" : "s"}.`
1823
+ }] };
1824
+ }
1825
+ if (items.length === 0) {
1826
+ const desc = describeFilters$2(args);
1827
+ return { content: [{
1828
+ type: "text",
1829
+ text: desc ? `No engagements match ${desc}.` : "No engagements found."
1830
+ }] };
1831
+ }
1832
+ const desc = describeFilters$2(args);
1833
+ const lines = [
1834
+ 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"}:`,
1835
+ "",
1836
+ ...items.map(formatEngagement)
1837
+ ];
1838
+ if (result.hasMore) {
1839
+ const nextOffset = (args.offset ?? 0) + items.length;
1840
+ lines.push("", `More results available — call again with offset: ${nextOffset} to see the next page.`);
1841
+ }
1842
+ return { content: [{
1843
+ type: "text",
1844
+ text: lines.join("\n")
1845
+ }] };
1846
+ } catch (error) {
1847
+ return {
1848
+ content: [{
1849
+ type: "text",
1850
+ text: error instanceof SodiumApiError ? `Error listing engagements: ${error.message} (correlation: ${error.correlationId})` : `Error listing engagements: ${error instanceof Error ? error.message : String(error)}`
1851
+ }],
1852
+ isError: true
1853
+ };
1854
+ }
1855
+ }
1856
+ function formatEngagement(e) {
1857
+ return `- ${e.code ?? "(no code)"} · ${e.status ?? "?"} · ${e.client ? `${e.client.name ?? "(no name)"} (${e.client.code ?? "?"})` : "(no client)"}${e.date ? ` · ${e.date}` : ""}${formatAnnualValue(e)}${e.numberOfServices !== void 0 && e.numberOfServices !== null ? ` · ${e.numberOfServices} service${e.numberOfServices === 1 ? "" : "s"}` : ""}`;
1858
+ }
1859
+ function formatAnnualValue(e) {
1860
+ if (e.annualTotal && e.annualTotal > 0) return ` · annual £${formatMoney(e.annualTotal)}`;
1861
+ if (e.totalValue && e.totalValue > 0) return ` · total £${formatMoney(e.totalValue)}`;
1862
+ return "";
1863
+ }
1864
+ function formatMoney(n) {
1865
+ return n.toLocaleString("en-GB", {
1866
+ minimumFractionDigits: 2,
1867
+ maximumFractionDigits: 2
1868
+ });
1869
+ }
1870
+ function describeFilters$2(args) {
1871
+ const parts = [];
1872
+ if (args.status) parts.push(`status ${args.status}`);
1873
+ if (args.search) parts.push(`search "${args.search}"`);
1874
+ if (args.dateRange) parts.push(`date ${args.dateRange}${args.dateRange === "CustomDateRange" && args.dateFrom && args.dateTo ? ` ${args.dateFrom} to ${args.dateTo}` : ""}`);
1875
+ if (args.sortBy) parts.push(`sort ${args.sortBy}${args.sortDesc ? " desc" : ""}`);
1876
+ return parts.join(", ");
1877
+ }
1878
+ //#endregion
1577
1879
  //#region ../mcp-core/src/tools/list-tasks.ts
1578
1880
  const statusEnum$1 = z.enum([
1579
1881
  "NotStarted",
@@ -1851,6 +2153,46 @@ async function buildServer(config) {
1851
2153
  openWorldHint: true
1852
2154
  }
1853
2155
  }, (args) => handleGetTaskContext(api, args));
2156
+ server.registerTool("list_proposals", {
2157
+ title: "List / filter proposals",
2158
+ description: "List proposals (pre-acceptance engagement documents) with filters: status (Unsent/Sent/Viewed/Accepted/Rejected), search (engagement code / client name / client code, 3+ chars), preset date range, sort, pagination. Use for 'proposals awaiting acceptance' (status=Sent, or run twice for Sent + Viewed), 'proposals sent to ACME' (search), 'proposals sent this month' (dateRange=ThisMonth). Returns the same underlying data as list_engagement_letters — both aliases back the same Engagement entity; the status filter is what distinguishes a proposal (Unsent/Sent/Viewed/Rejected) from a signed letter of engagement (Accepted). For 'how many X?' questions, pass limit=0 for count-only. Follow up with get_proposal_summary for full detail on one engagement.",
2159
+ inputSchema: ListEngagementsInputSchema,
2160
+ annotations: {
2161
+ readOnlyHint: true,
2162
+ idempotentHint: true,
2163
+ openWorldHint: true
2164
+ }
2165
+ }, (args) => handleListEngagements(api, args));
2166
+ server.registerTool("list_engagement_letters", {
2167
+ title: "List / filter letters of engagement",
2168
+ description: "List letters of engagement (signed/accepted or in-flight engagement documents) with filters: status (Unsent/Sent/Viewed/Accepted/Rejected), search (engagement code / client name / client code, 3+ chars), preset date range, sort, pagination. Use for 'live letters of engagement' or 'signed engagements' (status=Accepted), 'letter of engagement for ACME' (search), 'engagements signed this quarter' (status=Accepted + dateRange=ThisQuarter). Returns the same underlying data as list_proposals — both aliases back the same Engagement entity; the status filter narrows the pipeline stage. For 'how many X?' questions, pass limit=0 for count-only. Follow up with get_engagement_letter_summary for full detail on one engagement.",
2169
+ inputSchema: ListEngagementsInputSchema,
2170
+ annotations: {
2171
+ readOnlyHint: true,
2172
+ idempotentHint: true,
2173
+ openWorldHint: true
2174
+ }
2175
+ }, (args) => handleListEngagements(api, args));
2176
+ server.registerTool("get_proposal_summary", {
2177
+ title: "Get full detail of one proposal",
2178
+ description: "Get a consolidated view of a single proposal by engagement code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off / total), the proposed services with pricing, acceptance record (if accepted), PDF availability (proposal + letter of engagement), email history, and the acceptance link. REQUIRES a concrete engagement code as input — do NOT call this tool unless you already have the code. If the user describes an engagement by attribute — e.g. 'the unsent proposal', 'the proposal for ACME', 'our most recent proposal', 'the rejected one' — you MUST call list_proposals FIRST with the matching filters to look up the code, then call this tool with that code. Returns identical data to get_engagement_letter_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history may be missing with a note); only fails hard if the engagement itself can't be fetched.",
2179
+ inputSchema: GetEngagementSummaryInputSchema,
2180
+ annotations: {
2181
+ readOnlyHint: true,
2182
+ idempotentHint: true,
2183
+ openWorldHint: true
2184
+ }
2185
+ }, (args) => handleGetEngagementSummary(api, args));
2186
+ server.registerTool("get_engagement_letter_summary", {
2187
+ title: "Get full detail of one letter of engagement",
2188
+ description: "Get a consolidated view of a single letter of engagement by code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off / total), services included with pricing, acceptance record (accepted date, whether accepted via portal or recorded manually), PDF availability (proposal + letter of engagement), email history, and the acceptance link. REQUIRES a concrete engagement code as input — do NOT call this tool unless you already have the code. If the user describes the engagement by attribute — e.g. 'ACME's letter of engagement', 'the most recent signed engagement', 'the live engagement for Smith & Co' — you MUST call list_engagement_letters FIRST with the matching filters to look up the code, then call this tool with that code. Returns identical data to get_proposal_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history may be missing with a note); only fails hard if the engagement itself can't be fetched.",
2189
+ inputSchema: GetEngagementSummaryInputSchema,
2190
+ annotations: {
2191
+ readOnlyHint: true,
2192
+ idempotentHint: true,
2193
+ openWorldHint: true
2194
+ }
2195
+ }, (args) => handleGetEngagementSummary(api, args));
1854
2196
  server.registerTool("list_users", {
1855
2197
  title: "List / search / filter tenant users",
1856
2198
  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.2593",
3
+ "version": "0.1.0-beta.2596",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {