@sodiumhq/mcp-pm 0.1.0-beta.2767 → 0.1.0-beta.2772

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 (2) hide show
  1. package/dist/index.js +239 -6
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -441,7 +441,7 @@ const createConfig = (override = {}) => ({
441
441
  });
442
442
  //#endregion
443
443
  //#region ../mcp-core/src/generated/client/client.gen.ts
444
- const createClient = (config = {}) => {
444
+ const createClient$1 = (config = {}) => {
445
445
  let _config = mergeConfigs(createConfig(), config);
446
446
  const getConfig = () => ({ ..._config });
447
447
  const setConfig = (config) => {
@@ -620,7 +620,7 @@ const createClient = (config = {}) => {
620
620
  };
621
621
  //#endregion
622
622
  //#region ../mcp-core/src/generated/client.gen.ts
623
- const client = createClient(createConfig());
623
+ const client = createClient$1(createConfig());
624
624
  //#endregion
625
625
  //#region ../mcp-core/src/generated/sdk.gen.ts
626
626
  /**
@@ -706,6 +706,22 @@ const getClientDates = (options) => (options.client ?? client).get({
706
706
  ...options
707
707
  });
708
708
  /**
709
+ * List Notes for Client
710
+ *
711
+ * Lists all Notes for the specified client.
712
+ */
713
+ const listClientNotesForClient = (options) => (options.client ?? client).get({
714
+ security: [{
715
+ name: "x-api-key",
716
+ type: "apiKey"
717
+ }, {
718
+ scheme: "bearer",
719
+ type: "http"
720
+ }],
721
+ url: "/tenants/{tenant}/clients/{client}/clientnote",
722
+ ...options
723
+ });
724
+ /**
709
725
  * Create Note for Client
710
726
  *
711
727
  * Creates a new Note for the specified client.
@@ -761,6 +777,26 @@ const listClients = (options) => (options.client ?? client).get({
761
777
  ...options
762
778
  });
763
779
  /**
780
+ * Create Client
781
+ *
782
+ * Creates a new Client for the specified tenant.
783
+ */
784
+ const createClient = (options) => (options.client ?? client).post({
785
+ security: [{
786
+ name: "x-api-key",
787
+ type: "apiKey"
788
+ }, {
789
+ scheme: "bearer",
790
+ type: "http"
791
+ }],
792
+ url: "/tenants/{tenant}/clients",
793
+ ...options,
794
+ headers: {
795
+ "Content-Type": "application/json",
796
+ ...options.headers
797
+ }
798
+ });
799
+ /**
764
800
  * Get Client
765
801
  *
766
802
  * Gets a Client for the specified tenant.
@@ -1437,6 +1473,33 @@ var SodiumApiClient = class {
1437
1473
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `add note to client ${clientCode}`);
1438
1474
  return data;
1439
1475
  }
1476
+ async createClient(body) {
1477
+ const correlationId = randomUUID();
1478
+ const { data, error, response } = await createClient({
1479
+ path: { tenant: this.ctx.tenant },
1480
+ body,
1481
+ headers: { "X-Correlation-Id": correlationId }
1482
+ });
1483
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "create client");
1484
+ return data;
1485
+ }
1486
+ async listClientNotes(clientCode, query = {}) {
1487
+ const correlationId = randomUUID();
1488
+ const { data, error, response } = await listClientNotesForClient({
1489
+ path: {
1490
+ tenant: this.ctx.tenant,
1491
+ client: clientCode
1492
+ },
1493
+ query: {
1494
+ ...query,
1495
+ limit: query.limit ?? 10,
1496
+ offset: query.offset ?? 0
1497
+ },
1498
+ headers: { "X-Correlation-Id": correlationId }
1499
+ });
1500
+ if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `list notes for client ${clientCode}`);
1501
+ return data;
1502
+ }
1440
1503
  async listCustomFieldDefinitions(query = {}) {
1441
1504
  const correlationId = randomUUID();
1442
1505
  const { data, error, response } = await listCustomFieldDefinitions({
@@ -2194,7 +2257,7 @@ function format(input) {
2194
2257
  return (b.date ?? b.createdDate ?? "").localeCompare(da);
2195
2258
  });
2196
2259
  lines.push("", `--- Notes (${notes.length}) ---`);
2197
- for (const note of sorted.slice(0, 20)) lines.push(formatNote(note));
2260
+ for (const note of sorted.slice(0, 20)) lines.push(formatNote$1(note));
2198
2261
  if (notes.length > 20) lines.push(`... and ${notes.length - 20} more notes`);
2199
2262
  }
2200
2263
  const clients = task.clients ?? [];
@@ -2219,7 +2282,7 @@ function formatStep(step) {
2219
2282
  if (step.blockedReason) bits.push(`blocked: ${step.blockedReason}`);
2220
2283
  return `Step ${num}: ${name}${bits.length > 0 ? ` — ${bits.join(" · ")}` : ""}`;
2221
2284
  }
2222
- function formatNote(note) {
2285
+ function formatNote$1(note) {
2223
2286
  const pinPrefix = (note.pinnedLevel ?? 0) > 0 ? "[pinned] " : "";
2224
2287
  const dateStr = note.date ?? note.createdDate ?? "";
2225
2288
  const author = note.noteFromUser?.name ?? "(unknown)";
@@ -2369,7 +2432,7 @@ const categoryEnum = z.enum([
2369
2432
  "Advisory",
2370
2433
  "SoftwareAndTraining"
2371
2434
  ]);
2372
- const clientTypeEnum = z.enum([
2435
+ const clientTypeEnum$1 = z.enum([
2373
2436
  "PrivateLimitedCompany",
2374
2437
  "PublicLimitedCompany",
2375
2438
  "LimitedLiabilityPartnership",
@@ -2387,7 +2450,7 @@ const sortByEnum$2 = z.enum([
2387
2450
  const ListServicesInputSchema = {
2388
2451
  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."),
2389
2452
  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."),
2390
- 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)."),
2453
+ clientType: clientTypeEnum$1.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)."),
2391
2454
  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."),
2392
2455
  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."),
2393
2456
  sortDesc: z.boolean().optional().describe("Sort in descending order. Defaults to ascending."),
@@ -2853,6 +2916,145 @@ async function handleGetCustomFieldDetails(api, args) {
2853
2916
  }
2854
2917
  }
2855
2918
  //#endregion
2919
+ //#region ../mcp-core/src/tools/create-client.ts
2920
+ const clientTypeEnum = z.enum([
2921
+ "PrivateLimitedCompany",
2922
+ "PublicLimitedCompany",
2923
+ "LimitedLiabilityPartnership",
2924
+ "Partnership",
2925
+ "Individual",
2926
+ "Trust",
2927
+ "Charity",
2928
+ "SoleTrader"
2929
+ ]);
2930
+ const clientStatusEnum = z.enum([
2931
+ "Active",
2932
+ "Inactive",
2933
+ "Prospect",
2934
+ "LostProspect"
2935
+ ]);
2936
+ const CreateClientInputSchema = {
2937
+ name: z.string().min(1, "Client name is required").describe("The name of the client (e.g. 'ACME Ltd', 'John Smith')."),
2938
+ type: clientTypeEnum.describe("The client type. Determines which services and custom fields are applicable. Common types: PrivateLimitedCompany (most UK companies), Individual (sole traders' personal tax), SoleTrader (unincorporated businesses), Partnership, LimitedLiabilityPartnership."),
2939
+ status: clientStatusEnum.optional().describe("Client status (default: Active). Use Prospect for leads not yet onboarded."),
2940
+ email: z.string().nullable().optional().describe("Client email address."),
2941
+ telephone: z.string().nullable().optional().describe("Client telephone number."),
2942
+ internalReference: z.string().nullable().optional().describe("Internal reference number/code for the client."),
2943
+ managerCode: z.string().nullable().optional().describe("Code of the user who will manage this client. Discoverable via list_users or the team roster in the startup context."),
2944
+ partnerCode: z.string().nullable().optional().describe("Code of the partner user for this client."),
2945
+ associateCode: z.string().nullable().optional().describe("Code of the associate user for this client."),
2946
+ teamCode: z.string().nullable().optional().describe("Code of the team to assign to this client.")
2947
+ };
2948
+ async function handleCreateClient(api, args) {
2949
+ try {
2950
+ const created = await api.createClient(args);
2951
+ const lines = [
2952
+ `Created client: ${created.name} (${created.code ?? "(no code)"})`,
2953
+ `Type: ${created.type ?? args.type}`,
2954
+ `Status: ${created.status ?? "Active"}`
2955
+ ];
2956
+ if (created.email) lines.push(`Email: ${created.email}`);
2957
+ if (created.manager) lines.push(`Manager: ${created.manager.name} (${created.manager.code})`);
2958
+ if (created.partner) lines.push(`Partner: ${created.partner.name} (${created.partner.code})`);
2959
+ return { content: [{
2960
+ type: "text",
2961
+ text: lines.join("\n")
2962
+ }] };
2963
+ } catch (error) {
2964
+ return {
2965
+ content: [{
2966
+ type: "text",
2967
+ text: error instanceof SodiumApiError ? `Error creating client: ${error.message} (correlation: ${error.correlationId})` : `Error creating client: ${error instanceof Error ? error.message : String(error)}`
2968
+ }],
2969
+ isError: true
2970
+ };
2971
+ }
2972
+ }
2973
+ //#endregion
2974
+ //#region ../mcp-core/src/tools/get-contact.ts
2975
+ const GetContactInputSchema = { contactCode: z.string().min(1, "Contact code is required").describe("The contact code (identifier). Discoverable via list_contacts or the contacts section of get_client_summary.") };
2976
+ async function handleGetContact(api, args) {
2977
+ try {
2978
+ const c = await api.getContact(args.contactCode);
2979
+ const lines = [];
2980
+ const name = [
2981
+ c.title,
2982
+ c.firstName,
2983
+ c.middleName,
2984
+ c.lastName
2985
+ ].filter(Boolean).join(" ") || "(no name)";
2986
+ lines.push(`Contact: ${name} (${c.code ?? args.contactCode})`);
2987
+ if (c.email) lines.push(`Email: ${c.email}`);
2988
+ if (c.phone) lines.push(`Phone: ${c.phone}`);
2989
+ if (c.mobile) lines.push(`Mobile: ${c.mobile}`);
2990
+ if (c.address) lines.push(`Address: ${c.address}`);
2991
+ if (c.dateOfBirth) lines.push(`Date of birth: ${c.dateOfBirth}`);
2992
+ if (c.nationality) lines.push(`Nationality: ${c.nationality}`);
2993
+ if (c.maritalStatus) lines.push(`Marital status: ${c.maritalStatus}`);
2994
+ if (c.utr) lines.push(`UTR: ${c.utr}`);
2995
+ if (c.niNumber) lines.push(`NI number: ${c.niNumber}`);
2996
+ if (c.personalCode) lines.push(`Companies House person code: ${c.personalCode}`);
2997
+ if (c.isDeceased) lines.push(`Deceased: yes${c.deceasedDate ? ` (${c.deceasedDate})` : ""}`);
2998
+ if (c.clientCount !== void 0) lines.push(`Linked to ${c.clientCount} client(s)`);
2999
+ if (c.client) lines.push(`Primary client: ${c.client.name} (${c.client.code})`);
3000
+ if (c.individualClient) lines.push(`Individual client record: ${c.individualClient.name} (${c.individualClient.code})`);
3001
+ if (c.portalUser) lines.push(`Portal user: ${c.portalUser.name} (${c.portalUser.code})`);
3002
+ return { content: [{
3003
+ type: "text",
3004
+ text: lines.join("\n")
3005
+ }] };
3006
+ } catch (error) {
3007
+ return {
3008
+ content: [{
3009
+ type: "text",
3010
+ text: error instanceof SodiumApiError ? `Error getting contact: ${error.message} (correlation: ${error.correlationId})` : `Error getting contact: ${error instanceof Error ? error.message : String(error)}`
3011
+ }],
3012
+ isError: true
3013
+ };
3014
+ }
3015
+ }
3016
+ //#endregion
3017
+ //#region ../mcp-core/src/tools/list-client-notes.ts
3018
+ const sortFieldEnum$1 = z.enum(["Date", "UpdatedDate"]);
3019
+ const ListClientNotesInputSchema = {
3020
+ clientCode: z.string().min(1, "Client code is required").describe("The client code whose notes to list. Discoverable via list_clients or get_client_summary."),
3021
+ search: z.string().min(3, "Search must be at least 3 characters when provided").optional().describe("Search across note text. Minimum 3 characters."),
3022
+ authorCode: z.string().optional().describe("Filter by the user who wrote the note. Use the current user's code for 'my notes on ACME', or another user's code for 'what has Jane noted about this client?'."),
3023
+ sortBy: sortFieldEnum$1.optional().describe("Sort by Date (note date) or UpdatedDate. Default: Date descending (newest first)."),
3024
+ sortDesc: z.boolean().optional().describe("Sort descending (default: true for newest first)."),
3025
+ limit: z.number().int().min(0).max(50).optional().describe("Max results to return (default: 10, max: 50)."),
3026
+ offset: z.number().int().min(0).optional().describe("Number of records to skip for pagination.")
3027
+ };
3028
+ async function handleListClientNotes(api, args) {
3029
+ try {
3030
+ const { clientCode, ...query } = args;
3031
+ const result = await api.listClientNotes(clientCode, query);
3032
+ const notes = result.data ?? [];
3033
+ const total = result.totalCount ?? notes.length;
3034
+ if (notes.length === 0) return { content: [{
3035
+ type: "text",
3036
+ text: total === 0 ? `No notes found for client ${clientCode}.` : `${total} note(s) found, but none in this page (offset ${result.offset}).`
3037
+ }] };
3038
+ const lines = [`Notes for ${clientCode}: ${notes.length} of ${total}${result.hasMore ? " (more available)" : ""}`, ""];
3039
+ for (const n of notes) lines.push(formatNote(n));
3040
+ return { content: [{
3041
+ type: "text",
3042
+ text: lines.join("\n")
3043
+ }] };
3044
+ } catch (error) {
3045
+ return {
3046
+ content: [{
3047
+ type: "text",
3048
+ text: error instanceof SodiumApiError ? `Error listing client notes: ${error.message} (correlation: ${error.correlationId})` : `Error listing client notes: ${error instanceof Error ? error.message : String(error)}`
3049
+ }],
3050
+ isError: true
3051
+ };
3052
+ }
3053
+ }
3054
+ function formatNote(n) {
3055
+ return `- [${n.code ?? "(no code)"}] ${n.date ? n.date.slice(0, 10) : "(no date)"} by ${n.noteFromUser?.name ?? "(unknown)"}${n.pinnedLevel && n.pinnedLevel > 0 ? " [pinned]" : ""}: ${n.text ? n.text.length > 200 ? n.text.slice(0, 200) + "..." : n.text : "(empty)"}`;
3056
+ }
3057
+ //#endregion
2856
3058
  //#region ../mcp-core/src/tools/list-contacts.ts
2857
3059
  const sortFieldEnum = z.enum([
2858
3060
  "Name",
@@ -3081,6 +3283,16 @@ async function buildServer(config) {
3081
3283
  openWorldHint: true
3082
3284
  }
3083
3285
  }, (args) => handleGetCustomFieldDetails(api, args));
3286
+ server.registerTool("list_client_notes", {
3287
+ title: "List / search notes on a client",
3288
+ description: "List notes attached to a client, with optional search and author filter. Answers 'show me the notes on ACME', 'what has Jane noted about this client?', 'search notes for VAT'. Notes are returned newest first by default. Each note shows its code, date, author, pinned status, and text (truncated to 200 chars).",
3289
+ inputSchema: ListClientNotesInputSchema,
3290
+ annotations: {
3291
+ readOnlyHint: true,
3292
+ idempotentHint: true,
3293
+ openWorldHint: true
3294
+ }
3295
+ }, (args) => handleListClientNotes(api, args));
3084
3296
  server.registerTool("list_tasks", {
3085
3297
  title: "List / filter tasks across the practice",
3086
3298
  description: "List tasks with any combination of filters: assigned user(s), client(s), status, overdue flag, preset date range (Today / ThisWeek / Next7Days / CustomDateRange etc), category, team, recurring task template, saved filter, include-projected, include-workflow-steps (Agenda Mode), sort, and pagination. Use for: 'my tasks' (pass current user's code from startup context), 'Jane's overdue tasks' (user + isOverdue), 'tasks for ACME due this week' (client + dateRange=Next7Days + dateBasis=DueDate), 'what is the team working on this month' (dateRange=ThisMonth, no user filter). Returns up to 50 tasks per page. IMPORTANT constraints to avoid API errors and keep queries efficient: (1) Querying NotStarted tasks requires one of — a dateRange, isOverdue=true, or restricting status to non-NotStarted values. (2) Prefer the narrowest date range that answers the question — broad ranges (quarterly/yearly) are expensive; prefer Today / ThisWeek / Next7Days / ThisMonth over larger windows unless explicitly asked. (3) For 'oldest incomplete tasks' prefer status=['InProgress','Blocked'] with sortBy=StartDate (no date range needed), or add isOverdue=true if 'oldest overdue' is meant. (4) For 'how many X?' questions, pass limit=0 to get just the total count without fetching any task data — much cheaper than fetching a full page and counting.",
@@ -3181,6 +3393,16 @@ async function buildServer(config) {
3181
3393
  openWorldHint: true
3182
3394
  }
3183
3395
  }, (args) => handleListContacts(api, args));
3396
+ server.registerTool("get_contact", {
3397
+ title: "Get full details of a contact",
3398
+ description: "Get all fields for a single contact by code: name, title, email, phone, mobile, address, date of birth, nationality, marital status, UTR, NI number, Companies House person code, deceased status, linked clients, and portal user. Use after list_contacts identifies the contact of interest, or when the user asks for details about a specific contact. Also useful before update_contact to see current values.",
3399
+ inputSchema: GetContactInputSchema,
3400
+ annotations: {
3401
+ readOnlyHint: true,
3402
+ idempotentHint: true,
3403
+ openWorldHint: true
3404
+ }
3405
+ }, (args) => handleGetContact(api, args));
3184
3406
  registerWriteTool(server, config.context, "add_task_note", {
3185
3407
  title: "Add a note to a task",
3186
3408
  description: "Create a new note on a task. Additive — does not modify or delete existing notes. The note is attributed to the authenticated API user (the current practice member) and timestamped to 'now'. Use this when the user asks you to capture something on a task: 'add a note on the Greggs year-end task that we're waiting on the rental schedule', 'log on the task that I called John today and got voicemail'. Notes can be pinned; only pin when the user explicitly asks for it. The user can always edit or delete notes in the Sodium UI if the wording isn't right.",
@@ -3192,6 +3414,17 @@ async function buildServer(config) {
3192
3414
  openWorldHint: true
3193
3415
  }
3194
3416
  }, (args) => handleAddTaskNote(api, args));
3417
+ registerWriteTool(server, config.context, "create_client", {
3418
+ title: "Create a new client",
3419
+ description: "Create a new client record in the practice. Requires name and type (PrivateLimitedCompany, Individual, SoleTrader, etc.). Optionally set status (Active/Prospect/Inactive/LostProspect), email, telephone, internal reference, and assign a manager/partner/associate/team. The client code is auto-generated. Use when the user says 'add a new client', 'create client ACME Ltd', 'onboard a new prospect'.",
3420
+ inputSchema: CreateClientInputSchema,
3421
+ annotations: {
3422
+ readOnlyHint: false,
3423
+ destructiveHint: false,
3424
+ idempotentHint: false,
3425
+ openWorldHint: true
3426
+ }
3427
+ }, (args) => handleCreateClient(api, args));
3195
3428
  registerWriteTool(server, config.context, "add_client_note", {
3196
3429
  title: "Add a note to a client",
3197
3430
  description: "Create a new note on a client. Additive — does not modify or delete existing notes. The note is attributed to the authenticated API user and timestamped to 'now'. Use this when the user asks you to capture something on a client record: 'add a note on ACME that they mentioned expanding into Ireland', 'log on Greggs that they're switching bookkeeping software next quarter'. Client notes are the right place for persistent, client-level context; for task-specific notes use add_task_note. The user can edit or delete notes in the Sodium UI.",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.2767",
3
+ "version": "0.1.0-beta.2772",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {