@sodiumhq/mcp-pm 0.1.0-beta.5330 → 0.1.0-beta.5443

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -125,7 +125,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
125
125
  **Proposals and engagements**
126
126
  - **`get_engagement_pipeline_summary`** — pipeline counts and values by status in one call. "What's our proposal pipeline worth?"
127
127
  - **`list_proposals`** and **`list_engagement_letters`** — aliased tools for the same underlying engagement pipeline (pre-acceptance vs signed). Filter by status, client, date range.
128
- - **`get_proposal_summary`** and **`get_engagement_letter_summary`** — aliased drill-ins: identity + client + value breakdown + services with pricing + PDFs + email history + acceptance record
128
+ - **`get_proposal_summary`** and **`get_engagement_letter_summary`** — aliased drill-ins: identity + client + value breakdown (including catch-up fees, under the practice's own label) + services with pricing + PDFs + email history + acceptance record
129
129
 
130
130
  **Services**
131
131
  - **`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?"
package/dist/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-BY8wr-4D.mjs";
2
+ import { t as buildServer } from "./src-CEXQpQbI.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { SUPPORTED_PROTOCOL_VERSIONS, createMcpHandler } from "@modelcontextprotocol/server";
5
5
  import { createHash, randomUUID } from "node:crypto";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-BY8wr-4D.mjs";
2
+ import { t as buildServer } from "./src-CEXQpQbI.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
  import { parseArgs } from "node:util";
@@ -1412,6 +1412,22 @@ const getMailboxConversation = (options) => (options.client ?? client).get({
1412
1412
  ...options
1413
1413
  });
1414
1414
  /**
1415
+ * Get engagement settings
1416
+ *
1417
+ * Returns the engagement settings for the specified tenant
1418
+ */
1419
+ const getEngagementSettings = (options) => (options.client ?? client).get({
1420
+ security: [{
1421
+ name: "x-api-key",
1422
+ type: "apiKey"
1423
+ }, {
1424
+ scheme: "bearer",
1425
+ type: "http"
1426
+ }],
1427
+ url: "/tenants/{tenant}/practice/engagement-settings",
1428
+ ...options
1429
+ });
1430
+ /**
1415
1431
  * List Engagements
1416
1432
  *
1417
1433
  * Lists all Engagements for the given tenant.
@@ -2678,6 +2694,16 @@ var SodiumApiClient = class {
2678
2694
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get emails for engagement ${code}`);
2679
2695
  return data;
2680
2696
  }
2697
+ async getEngagementSettings() {
2698
+ const correlationId = randomUUID();
2699
+ const { data, error, response } = await getEngagementSettings({
2700
+ path: { tenant: this.tenant },
2701
+ headers: { "X-Correlation-Id": correlationId },
2702
+ client: this.http
2703
+ });
2704
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "get engagement settings");
2705
+ return data;
2706
+ }
2681
2707
  async getTaskWorkflowGroups(taskCode) {
2682
2708
  const correlationId = randomUUID();
2683
2709
  const { data, error, response } = await getTaskWorkflowGroups({
@@ -4248,9 +4274,17 @@ function humanizeDateType(type) {
4248
4274
  }
4249
4275
  //#endregion
4250
4276
  //#region ../mcp-core/src/tools/get-engagement-summary.ts
4277
+ const DEFAULT_CATCH_UP_FEE_LABEL = "Catch-up fee";
4278
+ function labelInSentence(label) {
4279
+ return /^[A-Z][A-Z]/.test(label) ? label : label.charAt(0).toLowerCase() + label.slice(1);
4280
+ }
4251
4281
  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.") };
4252
4282
  async function handleGetEngagementSummary(api, { code }) {
4253
- const [practice, [engagementResult, emailsResult]] = await Promise.all([api.getPracticeLocale(), Promise.allSettled([api.getEngagement(code), api.getEngagementEmails(code)])]);
4283
+ const [practice, [engagementResult, emailsResult, settingsResult]] = await Promise.all([api.getPracticeLocale(), Promise.allSettled([
4284
+ api.getEngagement(code),
4285
+ api.getEngagementEmails(code),
4286
+ api.getEngagementSettings()
4287
+ ])]);
4254
4288
  if (engagementResult.status === "rejected") {
4255
4289
  const err = engagementResult.reason;
4256
4290
  return {
@@ -4262,18 +4296,24 @@ async function handleGetEngagementSummary(api, { code }) {
4262
4296
  };
4263
4297
  }
4264
4298
  const engagement = engagementResult.value;
4299
+ const emails = emailsResult.status === "fulfilled" ? emailsResult.value : [];
4300
+ const catchUpFeeLabel = settingsResult.status === "fulfilled" && settingsResult.value.catchUpFeeLabel?.trim() || DEFAULT_CATCH_UP_FEE_LABEL;
4301
+ const gaps = [];
4302
+ if (emailsResult.status === "rejected") gaps.push("email history");
4303
+ if (settingsResult.status === "rejected") gaps.push("the practice's catch-up fee label");
4265
4304
  return { content: [{
4266
4305
  type: "text",
4267
4306
  text: format$2({
4268
4307
  engagement,
4269
- emails: emailsResult.status === "fulfilled" ? emailsResult.value : [],
4270
- gaps: emailsResult.status === "rejected" ? ["email history"] : [],
4271
- practice
4308
+ emails,
4309
+ gaps,
4310
+ practice,
4311
+ catchUpFeeLabel
4272
4312
  })
4273
4313
  }] };
4274
4314
  }
4275
4315
  function format$2(input) {
4276
- const { engagement: e, emails, gaps, practice } = input;
4316
+ const { engagement: e, emails, gaps, practice, catchUpFeeLabel } = input;
4277
4317
  const lines = [];
4278
4318
  const code = e.code ?? "(no code)";
4279
4319
  const typeLabel = e.typeName ?? e.type ?? "";
@@ -4287,12 +4327,12 @@ function format$2(input) {
4287
4327
  lines.push(`Recipient: ${recipientName || "(no name)"}${email}`);
4288
4328
  }
4289
4329
  if (e.lastViewed) lines.push(`Last viewed by client: ${e.lastViewed}`);
4290
- const valueLines = formatValues(e, practice);
4330
+ const valueLines = formatValues(e, practice, catchUpFeeLabel);
4291
4331
  if (valueLines.length > 0) lines.push("", "--- Value ---", ...valueLines);
4292
4332
  const services = e.proposalServices ?? [];
4293
4333
  if (services.length > 0) {
4294
4334
  lines.push("", `--- Services (${services.length}) ---`);
4295
- for (const s of services) lines.push(`- ${formatService$1(s, practice)}`);
4335
+ for (const s of services) lines.push(`- ${formatService$1(s, practice, catchUpFeeLabel)}`);
4296
4336
  }
4297
4337
  if (e.acceptance && e.status === "Accepted") {
4298
4338
  lines.push("", "--- Acceptance ---");
@@ -4318,21 +4358,23 @@ function format$2(input) {
4318
4358
  if (gaps.length > 0) lines.push("", `Note: could not load ${gaps.join(", ")} (partial failure). Retry for a complete picture.`);
4319
4359
  return lines.join("\n");
4320
4360
  }
4321
- function formatValues(e, practice) {
4361
+ function formatValues(e, practice, catchUpFeeLabel) {
4322
4362
  const out = [];
4323
4363
  if (e.annualTotal && e.annualTotal > 0) out.push(`Annual: ${money(e.annualTotal, practice)}`);
4324
4364
  if (e.quarterlyTotal && e.quarterlyTotal > 0) out.push(`Quarterly: ${money(e.quarterlyTotal, practice)}`);
4325
4365
  if (e.monthlyTotal && e.monthlyTotal > 0) out.push(`Monthly: ${money(e.monthlyTotal, practice)}`);
4326
4366
  if (e.weeklyTotal && e.weeklyTotal > 0) out.push(`Weekly: ${money(e.weeklyTotal, practice)}`);
4327
4367
  if (e.oneOffTotal && e.oneOffTotal > 0) out.push(`One-off: ${money(e.oneOffTotal, practice)}`);
4368
+ if (e.catchUpTotal) out.push(`${catchUpFeeLabel}: ${money(e.catchUpTotal, practice)}`);
4328
4369
  if (e.totalValue && e.totalValue > 0) out.push(`Total contracted value: ${money(e.totalValue, practice)}`);
4329
4370
  return out;
4330
4371
  }
4331
- function formatService$1(s, practice) {
4372
+ function formatService$1(s, practice, catchUpFeeLabel) {
4332
4373
  const name = s.billableService?.name ?? "(unnamed)";
4333
4374
  const freq = s.billingFrequency ? ` ${s.billingFrequency}` : "";
4334
- if (s.effectivePrice !== void 0 && s.effectivePrice !== null) return `${name} — ${money(s.effectivePrice, practice)}${freq}`;
4335
- return freq ? `${name} —${freq}` : name;
4375
+ const catchUp = s.catchUp?.fee ? ` + ${money(s.catchUp.fee, practice)} ${labelInSentence(catchUpFeeLabel)} (${s.catchUp.months ?? "?"} months)` : "";
4376
+ if (s.effectivePrice !== void 0 && s.effectivePrice !== null) return `${name} — ${money(s.effectivePrice, practice)}${freq}${catchUp}`;
4377
+ return freq ? `${name} —${freq}${catchUp}` : `${name}${catchUp}`;
4336
4378
  }
4337
4379
  function formatEmail$1(em) {
4338
4380
  return `- ${em.sentDate ?? "(no date)"}: ${em.subject ?? "(no subject)"}${em.status ? ` · ${em.status}` : ""}${em.toRecipients?.length ? ` → ${em.toRecipients.join(", ")}` : ""}`;
@@ -8816,8 +8858,7 @@ const AddClientServiceInputSchema = {
8816
8858
  overrideSetupFee: z.boolean().optional().describe("true = use the custom setupFee instead of the service default."),
8817
8859
  managedByUserCode: z.string().optional().describe("User who manages this service for the client. Resolve names via the roster or list_users."),
8818
8860
  stageCode: z.string().optional().describe("Initial workflow stage for the service, if the service uses stages."),
8819
- autoInvoice: z.boolean().optional().describe("Whether the service auto-generates billing line items. Omit to use the default; only set when the user is explicit."),
8820
- nextBillingDate: z.string().optional().describe("First billing date (YYYY-MM-DD) when autoInvoice is on. Omit to default from startDate.")
8861
+ autoInvoice: z.boolean().optional().describe("Whether the service auto-generates billing line items. Omit to use the default; only set when the user is explicit.")
8821
8862
  };
8822
8863
  async function handleAddClientService(api, args) {
8823
8864
  try {
@@ -8835,8 +8876,7 @@ async function handleAddClientService(api, args) {
8835
8876
  ...rest.setupFee !== void 0 ? { setupFee: rest.setupFee } : {},
8836
8877
  managedByUserCode: rest.managedByUserCode ?? null,
8837
8878
  stageCode: rest.stageCode ?? null,
8838
- ...rest.autoInvoice !== void 0 ? { autoInvoice: rest.autoInvoice } : {},
8839
- nextBillingDate: rest.nextBillingDate ?? null
8879
+ ...rest.autoInvoice !== void 0 ? { autoInvoice: rest.autoInvoice } : {}
8840
8880
  };
8841
8881
  const [practice, created] = await Promise.all([api.getPracticeLocale(), api.createClientService(clientCode, body)]);
8842
8882
  const lines = [
@@ -8846,7 +8886,7 @@ async function handleAddClientService(api, args) {
8846
8886
  ];
8847
8887
  if (created.managedByUser?.name) lines.push(`Managed by: ${created.managedByUser.name}`);
8848
8888
  if (created.stage?.name) lines.push(`Stage: ${created.stage.name}`);
8849
- if (created.autoInvoice) lines.push(`Auto-invoicing on${created.nextBillingDate ? `, next billing ${created.nextBillingDate.slice(0, 10)}` : ""}`);
8889
+ if (created.autoInvoice) lines.push("Auto-invoicing on");
8850
8890
  return { content: [{
8851
8891
  type: "text",
8852
8892
  text: lines.join("\n")
@@ -9507,7 +9547,7 @@ async function buildServer(config) {
9507
9547
  }, (args) => handleListEngagements(api, args));
9508
9548
  registerTenantTool(server, api, "get_proposal_summary", {
9509
9549
  title: "Get full detail of one proposal",
9510
- 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.",
9550
+ description: "Get a consolidated view of a single proposal by engagement code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off incl. catch-up fees / total), the proposed services with pricing (and each service's catch-up fee share), acceptance record (if accepted), PDF availability (proposal + letter of engagement), email history, and the acceptance link. REQUIRES a concrete engagement code as input — do NOT call this tool unless you already have the code. If the user describes an engagement by attribute — e.g. 'the unsent proposal', 'the proposal for ACME', 'our most recent proposal', 'the rejected one' — you MUST call list_proposals FIRST with the matching filters to look up the code, then call this tool with that code. Returns identical data to get_engagement_letter_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history or the practice's catch-up fee label may be missing with a note); only fails hard if the engagement itself can't be fetched.",
9511
9551
  inputSchema: GetEngagementSummaryInputSchema,
9512
9552
  annotations: {
9513
9553
  readOnlyHint: true,
@@ -9517,7 +9557,7 @@ async function buildServer(config) {
9517
9557
  }, (args) => handleGetEngagementSummary(api, args));
9518
9558
  registerTenantTool(server, api, "get_engagement_letter_summary", {
9519
9559
  title: "Get full detail of one letter of engagement",
9520
- 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.",
9560
+ description: "Get a consolidated view of a single letter of engagement by code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off incl. catch-up fees / total), services included with pricing (and each service's catch-up fee share), acceptance record (accepted date, whether accepted via portal or recorded manually), PDF availability (proposal + letter of engagement), email history, and the acceptance link. REQUIRES a concrete engagement code as input — do NOT call this tool unless you already have the code. If the user describes the engagement by attribute — e.g. 'ACME's letter of engagement', 'the most recent signed engagement', 'the live engagement for Smith & Co' — you MUST call list_engagement_letters FIRST with the matching filters to look up the code, then call this tool with that code. Returns identical data to get_proposal_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history or the practice's catch-up fee label may be missing with a note); only fails hard if the engagement itself can't be fetched.",
9521
9561
  inputSchema: GetEngagementSummaryInputSchema,
9522
9562
  annotations: {
9523
9563
  readOnlyHint: true,
@@ -9867,7 +9907,7 @@ async function buildServer(config) {
9867
9907
  }, (args) => handleListClientLinks(api, args));
9868
9908
  registerWriteTool(server, api, config.context, "add_client_service", {
9869
9909
  title: "Assign a catalogue service to a client",
9870
- description: "Put a client on one of the practice's services: requires billableServiceCode (from list_services), billingFrequency (must match one of the service's configured pricing options — check get_service_details first), and startDate. Optional: status (default Active; use 'Proposed' when quoting), custom price (with overridePricing=true), pricing tier (REQUIRED for CustomTiers-priced services), setup fee, managing user, initial stage, auto-invoicing and first billing date. Use for 'put ACME on monthly bookkeeping', 'add VAT returns to Greggs from next month'. NOT exposed: pricing-factor questionnaires — services priced by factors should be configured in the Sodium UI; if the API rejects the call asking for pricing answers, say so and point the user to the UI. Check get_client_summary first to avoid assigning a duplicate service.",
9910
+ description: "Put a client on one of the practice's services: requires billableServiceCode (from list_services), billingFrequency (must match one of the service's configured pricing options — check get_service_details first), and startDate. Optional: status (default Active; use 'Proposed' when quoting), custom price (with overridePricing=true), pricing tier (REQUIRED for CustomTiers-priced services), setup fee, managing user, initial stage and auto-invoicing (billing dates come from the client's billing schedule). Use for 'put ACME on monthly bookkeeping', 'add VAT returns to Greggs from next month'. NOT exposed: pricing-factor questionnaires — services priced by factors should be configured in the Sodium UI; if the API rejects the call asking for pricing answers, say so and point the user to the UI. Check get_client_summary first to avoid assigning a duplicate service.",
9871
9911
  inputSchema: AddClientServiceInputSchema,
9872
9912
  annotations: {
9873
9913
  readOnlyHint: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.5330",
3
+ "version": "0.1.0-beta.5443",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {