@sodiumhq/mcp-pm 0.1.0-beta.2765 → 0.1.0-beta.2767
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 +5 -0
- package/dist/index.js +272 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,6 +65,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
|
|
|
65
65
|
**Clients**
|
|
66
66
|
- **`list_clients`** — list and filter clients by search, status, type, assignee, services, saved filters
|
|
67
67
|
- **`get_client_summary`** — one-call composite for a single client: identity + contacts + active services with pricing + business details (company number, VAT, UTR, trading address) + **custom fields** (all user-defined fields with current values, data types, and field codes) + key statutory dates (year-end, accounts due, VAT return due, confirmation statement due) + overdue tasks + tasks due in next 7 days
|
|
68
|
+
- **`get_custom_field_details`** — get the full definition of a custom field by code, including allowed options for Select/MultiSelect fields. Use before setting a dropdown field to discover valid values.
|
|
68
69
|
|
|
69
70
|
**Tasks**
|
|
70
71
|
- **`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?"
|
|
@@ -78,6 +79,9 @@ Only enable write mode with an AI client you trust — it hands the client the a
|
|
|
78
79
|
- **`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?"
|
|
79
80
|
- **`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?"
|
|
80
81
|
|
|
82
|
+
**Contacts**
|
|
83
|
+
- **`list_contacts`** — search and filter contacts across the practice by name, email, phone, or client. Answers "find John Smith", "who are the contacts for ACME?", "search for jane@example.com".
|
|
84
|
+
|
|
81
85
|
**Team**
|
|
82
86
|
- **`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?"
|
|
83
87
|
|
|
@@ -85,6 +89,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
|
|
|
85
89
|
- **`add_task_note`** — capture a note against a specific task. Attributed to your API user, timestamped to now.
|
|
86
90
|
- **`add_client_note`** — capture a note against a client record. Same shape as `add_task_note` but scoped to the client.
|
|
87
91
|
- **`update_client_custom_fields`** — set or clear custom field values on a client. Accepts a map of field codes to values, validates against the field's data type (Text, Number, Date, Boolean, Select, MultiSelect). Pass null to clear a field. Field codes are discoverable via `get_client_summary`.
|
|
92
|
+
- **`update_contact`** — update a contact's details: name, email, phone, mobile, date of birth, UTR, NI number, address. Requires the contact code (discoverable via `list_contacts`).
|
|
88
93
|
|
|
89
94
|
More tools land iteratively as the beta progresses.
|
|
90
95
|
|
package/dist/index.js
CHANGED
|
@@ -777,6 +777,58 @@ const getClient = (options) => (options.client ?? client).get({
|
|
|
777
777
|
...options
|
|
778
778
|
});
|
|
779
779
|
/**
|
|
780
|
+
* List Contacts
|
|
781
|
+
*
|
|
782
|
+
* Lists all Contacts for the given tenant.
|
|
783
|
+
*/
|
|
784
|
+
const listContacts = (options) => (options.client ?? client).get({
|
|
785
|
+
security: [{
|
|
786
|
+
name: "x-api-key",
|
|
787
|
+
type: "apiKey"
|
|
788
|
+
}, {
|
|
789
|
+
scheme: "bearer",
|
|
790
|
+
type: "http"
|
|
791
|
+
}],
|
|
792
|
+
url: "/tenants/{tenant}/contacts",
|
|
793
|
+
...options
|
|
794
|
+
});
|
|
795
|
+
/**
|
|
796
|
+
* Get Contact
|
|
797
|
+
*
|
|
798
|
+
* Gets a Contact for the specified tenant.
|
|
799
|
+
*/
|
|
800
|
+
const getContact = (options) => (options.client ?? client).get({
|
|
801
|
+
security: [{
|
|
802
|
+
name: "x-api-key",
|
|
803
|
+
type: "apiKey"
|
|
804
|
+
}, {
|
|
805
|
+
scheme: "bearer",
|
|
806
|
+
type: "http"
|
|
807
|
+
}],
|
|
808
|
+
url: "/tenants/{tenant}/contacts/{code}",
|
|
809
|
+
...options
|
|
810
|
+
});
|
|
811
|
+
/**
|
|
812
|
+
* Update Contact
|
|
813
|
+
*
|
|
814
|
+
* Updates a Contact for the specified tenant.
|
|
815
|
+
*/
|
|
816
|
+
const updateContact = (options) => (options.client ?? client).put({
|
|
817
|
+
security: [{
|
|
818
|
+
name: "x-api-key",
|
|
819
|
+
type: "apiKey"
|
|
820
|
+
}, {
|
|
821
|
+
scheme: "bearer",
|
|
822
|
+
type: "http"
|
|
823
|
+
}],
|
|
824
|
+
url: "/tenants/{tenant}/contacts/{code}",
|
|
825
|
+
...options,
|
|
826
|
+
headers: {
|
|
827
|
+
"Content-Type": "application/json",
|
|
828
|
+
...options.headers
|
|
829
|
+
}
|
|
830
|
+
});
|
|
831
|
+
/**
|
|
780
832
|
* List CustomFieldDefinitions
|
|
781
833
|
*
|
|
782
834
|
* Lists all CustomFieldDefinitions for the given tenant.
|
|
@@ -793,6 +845,22 @@ const listCustomFieldDefinitions = (options) => (options.client ?? client).get({
|
|
|
793
845
|
...options
|
|
794
846
|
});
|
|
795
847
|
/**
|
|
848
|
+
* Get CustomFieldDefinition
|
|
849
|
+
*
|
|
850
|
+
* Gets a CustomFieldDefinition for the specified tenant.
|
|
851
|
+
*/
|
|
852
|
+
const getCustomFieldDefinition = (options) => (options.client ?? client).get({
|
|
853
|
+
security: [{
|
|
854
|
+
name: "x-api-key",
|
|
855
|
+
type: "apiKey"
|
|
856
|
+
}, {
|
|
857
|
+
scheme: "bearer",
|
|
858
|
+
type: "http"
|
|
859
|
+
}],
|
|
860
|
+
url: "/tenants/{tenant}/custom-fields/{code}",
|
|
861
|
+
...options
|
|
862
|
+
});
|
|
863
|
+
/**
|
|
796
864
|
* List Engagements
|
|
797
865
|
*
|
|
798
866
|
* Lists all Engagements for the given tenant.
|
|
@@ -1383,6 +1451,18 @@ var SodiumApiClient = class {
|
|
|
1383
1451
|
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list custom field definitions");
|
|
1384
1452
|
return data;
|
|
1385
1453
|
}
|
|
1454
|
+
async getCustomFieldDefinition(code) {
|
|
1455
|
+
const correlationId = randomUUID();
|
|
1456
|
+
const { data, error, response } = await getCustomFieldDefinition({
|
|
1457
|
+
path: {
|
|
1458
|
+
tenant: this.ctx.tenant,
|
|
1459
|
+
code
|
|
1460
|
+
},
|
|
1461
|
+
headers: { "X-Correlation-Id": correlationId }
|
|
1462
|
+
});
|
|
1463
|
+
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get custom field definition ${code}`);
|
|
1464
|
+
return data;
|
|
1465
|
+
}
|
|
1386
1466
|
async getClientCustomFieldValues(clientCode) {
|
|
1387
1467
|
const correlationId = randomUUID();
|
|
1388
1468
|
const { data, error, response } = await getClientCustomFieldValues({
|
|
@@ -1407,6 +1487,45 @@ var SodiumApiClient = class {
|
|
|
1407
1487
|
});
|
|
1408
1488
|
if (error !== void 0) throw this.toError(response, error, correlationId, `set custom field values for client ${clientCode}`);
|
|
1409
1489
|
}
|
|
1490
|
+
async listContacts(query = {}) {
|
|
1491
|
+
const correlationId = randomUUID();
|
|
1492
|
+
const { data, error, response } = await listContacts({
|
|
1493
|
+
path: { tenant: this.ctx.tenant },
|
|
1494
|
+
query: {
|
|
1495
|
+
...query,
|
|
1496
|
+
limit: query.limit ?? 10,
|
|
1497
|
+
offset: query.offset ?? 0
|
|
1498
|
+
},
|
|
1499
|
+
headers: { "X-Correlation-Id": correlationId }
|
|
1500
|
+
});
|
|
1501
|
+
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list contacts");
|
|
1502
|
+
return data;
|
|
1503
|
+
}
|
|
1504
|
+
async getContact(code) {
|
|
1505
|
+
const correlationId = randomUUID();
|
|
1506
|
+
const { data, error, response } = await getContact({
|
|
1507
|
+
path: {
|
|
1508
|
+
tenant: this.ctx.tenant,
|
|
1509
|
+
code
|
|
1510
|
+
},
|
|
1511
|
+
headers: { "X-Correlation-Id": correlationId }
|
|
1512
|
+
});
|
|
1513
|
+
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get contact ${code}`);
|
|
1514
|
+
return data;
|
|
1515
|
+
}
|
|
1516
|
+
async updateContact(code, body) {
|
|
1517
|
+
const correlationId = randomUUID();
|
|
1518
|
+
const { data, error, response } = await updateContact({
|
|
1519
|
+
path: {
|
|
1520
|
+
tenant: this.ctx.tenant,
|
|
1521
|
+
code
|
|
1522
|
+
},
|
|
1523
|
+
body,
|
|
1524
|
+
headers: { "X-Correlation-Id": correlationId }
|
|
1525
|
+
});
|
|
1526
|
+
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `update contact ${code}`);
|
|
1527
|
+
return data;
|
|
1528
|
+
}
|
|
1410
1529
|
toError(response, error, correlationId, operation) {
|
|
1411
1530
|
const status = response.status;
|
|
1412
1531
|
let message = `Failed to ${operation} (HTTP ${status})`;
|
|
@@ -2630,6 +2749,13 @@ async function handleSetClientCustomFields(api, args) {
|
|
|
2630
2749
|
});
|
|
2631
2750
|
const defMap = /* @__PURE__ */ new Map();
|
|
2632
2751
|
for (const d of definitions.data ?? []) if (d.code) defMap.set(d.code, d);
|
|
2752
|
+
const selectFields = entries.filter(([code, val]) => val !== null && val !== "" && (defMap.get(code)?.dataType === "Select" || defMap.get(code)?.dataType === "MultiSelect"));
|
|
2753
|
+
await Promise.all(selectFields.map(async ([code]) => {
|
|
2754
|
+
try {
|
|
2755
|
+
const full = await api.getCustomFieldDefinition(code);
|
|
2756
|
+
defMap.set(code, full);
|
|
2757
|
+
} catch {}
|
|
2758
|
+
}));
|
|
2633
2759
|
const errors = [];
|
|
2634
2760
|
for (const [fieldCode, value] of entries) {
|
|
2635
2761
|
const def = defMap.get(fieldCode);
|
|
@@ -2695,6 +2821,121 @@ function validateFieldValue(def, value) {
|
|
|
2695
2821
|
}
|
|
2696
2822
|
}
|
|
2697
2823
|
//#endregion
|
|
2824
|
+
//#region ../mcp-core/src/tools/get-custom-field-details.ts
|
|
2825
|
+
const GetCustomFieldDetailsInputSchema = { fieldCode: z.string().min(1, "Field code is required").describe("The custom field definition code. Discoverable via the Custom Fields section of get_client_summary.") };
|
|
2826
|
+
async function handleGetCustomFieldDetails(api, args) {
|
|
2827
|
+
try {
|
|
2828
|
+
const def = await api.getCustomFieldDefinition(args.fieldCode);
|
|
2829
|
+
const lines = [];
|
|
2830
|
+
lines.push(`Custom Field: ${def.label ?? "(no label)"} (${def.code ?? args.fieldCode})`);
|
|
2831
|
+
lines.push(`Data type: ${def.dataType ?? "(unknown)"}`);
|
|
2832
|
+
lines.push(`Entity type: ${def.entityType ?? "(unknown)"}`);
|
|
2833
|
+
lines.push(`Archived: ${def.isArchived ? "yes" : "no"}`);
|
|
2834
|
+
if (def.sortOrder !== void 0) lines.push(`Sort order: ${def.sortOrder}`);
|
|
2835
|
+
if (def.clientTypes && def.clientTypes.length > 0) lines.push(`Applicable client types: ${def.clientTypes.join(", ")}`);
|
|
2836
|
+
else lines.push("Applicable client types: all");
|
|
2837
|
+
if (def.options && def.options.length > 0) {
|
|
2838
|
+
lines.push("", `Allowed options (${def.options.length}):`);
|
|
2839
|
+
for (const opt of def.options) lines.push(`- ${opt}`);
|
|
2840
|
+
} else if (def.dataType === "Select" || def.dataType === "MultiSelect") lines.push("", "Allowed options: (none configured)");
|
|
2841
|
+
return { content: [{
|
|
2842
|
+
type: "text",
|
|
2843
|
+
text: lines.join("\n")
|
|
2844
|
+
}] };
|
|
2845
|
+
} catch (error) {
|
|
2846
|
+
return {
|
|
2847
|
+
content: [{
|
|
2848
|
+
type: "text",
|
|
2849
|
+
text: error instanceof SodiumApiError ? `Error getting custom field definition: ${error.message} (correlation: ${error.correlationId})` : `Error getting custom field definition: ${error instanceof Error ? error.message : String(error)}`
|
|
2850
|
+
}],
|
|
2851
|
+
isError: true
|
|
2852
|
+
};
|
|
2853
|
+
}
|
|
2854
|
+
}
|
|
2855
|
+
//#endregion
|
|
2856
|
+
//#region ../mcp-core/src/tools/list-contacts.ts
|
|
2857
|
+
const sortFieldEnum = z.enum([
|
|
2858
|
+
"Name",
|
|
2859
|
+
"FirstName",
|
|
2860
|
+
"LastName",
|
|
2861
|
+
"ClientCount"
|
|
2862
|
+
]);
|
|
2863
|
+
const ListContactsInputSchema = {
|
|
2864
|
+
search: z.string().min(3, "Search must be at least 3 characters when provided").optional().describe("Free-text search across contact code, name, email, and phone. Minimum 3 characters. Use for 'find John', 'who is jane@example.com', 'search for Smith'."),
|
|
2865
|
+
clientCode: z.string().optional().describe("Filter to contacts linked to a specific client. Use when the user asks 'who are the contacts for ACME?' or 'list contacts on client CLI001'."),
|
|
2866
|
+
sortBy: sortFieldEnum.optional().describe("Field to sort results by."),
|
|
2867
|
+
sortDesc: z.boolean().optional().describe("Sort in descending order (default: ascending)."),
|
|
2868
|
+
limit: z.number().int().min(0).max(50).optional().describe("Max results to return (default: 10, max: 50). Use limit=0 for count-only."),
|
|
2869
|
+
offset: z.number().int().min(0).optional().describe("Number of records to skip for pagination.")
|
|
2870
|
+
};
|
|
2871
|
+
async function handleListContacts(api, args) {
|
|
2872
|
+
try {
|
|
2873
|
+
const result = await api.listContacts(args);
|
|
2874
|
+
const contacts = result.data ?? [];
|
|
2875
|
+
const total = result.totalCount ?? contacts.length;
|
|
2876
|
+
if (contacts.length === 0) return { content: [{
|
|
2877
|
+
type: "text",
|
|
2878
|
+
text: total === 0 ? "No contacts found matching the criteria." : `${total} contact(s) found, but none in this page (offset ${result.offset}).`
|
|
2879
|
+
}] };
|
|
2880
|
+
const lines = [`Contacts: ${contacts.length} of ${total}${result.hasMore ? " (more available)" : ""}`, ""];
|
|
2881
|
+
for (const c of contacts) lines.push(formatContact(c));
|
|
2882
|
+
return { content: [{
|
|
2883
|
+
type: "text",
|
|
2884
|
+
text: lines.join("\n")
|
|
2885
|
+
}] };
|
|
2886
|
+
} catch (error) {
|
|
2887
|
+
return {
|
|
2888
|
+
content: [{
|
|
2889
|
+
type: "text",
|
|
2890
|
+
text: error instanceof SodiumApiError ? `Error listing contacts: ${error.message} (correlation: ${error.correlationId})` : `Error listing contacts: ${error instanceof Error ? error.message : String(error)}`
|
|
2891
|
+
}],
|
|
2892
|
+
isError: true
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
}
|
|
2896
|
+
function formatContact(c) {
|
|
2897
|
+
const code = c.code ?? "(no code)";
|
|
2898
|
+
return `- ${[c.firstName, c.lastName].filter(Boolean).join(" ") || "(no name)"} (${code})${c.email ? ` <${c.email}>` : ""}${c.phone || c.mobile ? ` | ${c.phone || c.mobile}` : ""}`;
|
|
2899
|
+
}
|
|
2900
|
+
//#endregion
|
|
2901
|
+
//#region ../mcp-core/src/tools/update-contact.ts
|
|
2902
|
+
const UpdateContactInputSchema = {
|
|
2903
|
+
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."),
|
|
2904
|
+
lastName: z.string().min(1).describe("The contact's last name (required by the API even if unchanged)."),
|
|
2905
|
+
title: z.string().nullable().optional().describe("Title (e.g. Mr, Mrs, Dr). Pass null to clear."),
|
|
2906
|
+
firstName: z.string().nullable().optional().describe("First name. Pass null to clear."),
|
|
2907
|
+
middleName: z.string().nullable().optional().describe("Middle name. Pass null to clear."),
|
|
2908
|
+
email: z.string().nullable().optional().describe("Email address. Pass null to clear."),
|
|
2909
|
+
phone: z.string().nullable().optional().describe("Landline phone number. Pass null to clear."),
|
|
2910
|
+
mobile: z.string().nullable().optional().describe("Mobile phone number. Pass null to clear."),
|
|
2911
|
+
dateOfBirth: z.string().nullable().optional().describe("Date of birth in ISO 8601 format (e.g. '1985-03-15'). Pass null to clear."),
|
|
2912
|
+
utr: z.string().nullable().optional().describe("Self Assessment UTR. Pass null to clear."),
|
|
2913
|
+
niNumber: z.string().nullable().optional().describe("National Insurance Number. Pass null to clear."),
|
|
2914
|
+
address: z.string().nullable().optional().describe("Contact address (only for Individual client type contacts). Pass null to clear.")
|
|
2915
|
+
};
|
|
2916
|
+
async function handleUpdateContact(api, args) {
|
|
2917
|
+
try {
|
|
2918
|
+
const { contactCode, ...body } = args;
|
|
2919
|
+
const updated = await api.updateContact(contactCode, body);
|
|
2920
|
+
const lines = [`Updated contact: ${[updated.firstName, updated.lastName].filter(Boolean).join(" ") || "(no name)"} (${updated.code ?? contactCode})`];
|
|
2921
|
+
if (updated.email) lines.push(`Email: ${updated.email}`);
|
|
2922
|
+
if (updated.phone) lines.push(`Phone: ${updated.phone}`);
|
|
2923
|
+
if (updated.mobile) lines.push(`Mobile: ${updated.mobile}`);
|
|
2924
|
+
return { content: [{
|
|
2925
|
+
type: "text",
|
|
2926
|
+
text: lines.join("\n")
|
|
2927
|
+
}] };
|
|
2928
|
+
} catch (error) {
|
|
2929
|
+
return {
|
|
2930
|
+
content: [{
|
|
2931
|
+
type: "text",
|
|
2932
|
+
text: error instanceof SodiumApiError ? `Error updating contact: ${error.message} (correlation: ${error.correlationId})` : `Error updating contact: ${error instanceof Error ? error.message : String(error)}`
|
|
2933
|
+
}],
|
|
2934
|
+
isError: true
|
|
2935
|
+
};
|
|
2936
|
+
}
|
|
2937
|
+
}
|
|
2938
|
+
//#endregion
|
|
2698
2939
|
//#region ../mcp-core/src/tools/whoami.ts
|
|
2699
2940
|
async function handleWhoami(api) {
|
|
2700
2941
|
try {
|
|
@@ -2830,6 +3071,16 @@ async function buildServer(config) {
|
|
|
2830
3071
|
openWorldHint: true
|
|
2831
3072
|
}
|
|
2832
3073
|
}, (args) => handleGetClientSummary(api, args));
|
|
3074
|
+
server.registerTool("get_custom_field_details", {
|
|
3075
|
+
title: "Get full details of a custom field definition",
|
|
3076
|
+
description: "Get the full definition of a single custom field by code, including allowed options for Select and MultiSelect fields. The list endpoint and get_client_summary show field codes, labels, and data types but omit the allowed options — use this tool when you need to know the valid values before setting a Select or MultiSelect field via update_client_custom_fields. Also shows applicable client types and archived status.",
|
|
3077
|
+
inputSchema: GetCustomFieldDetailsInputSchema,
|
|
3078
|
+
annotations: {
|
|
3079
|
+
readOnlyHint: true,
|
|
3080
|
+
idempotentHint: true,
|
|
3081
|
+
openWorldHint: true
|
|
3082
|
+
}
|
|
3083
|
+
}, (args) => handleGetCustomFieldDetails(api, args));
|
|
2833
3084
|
server.registerTool("list_tasks", {
|
|
2834
3085
|
title: "List / filter tasks across the practice",
|
|
2835
3086
|
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.",
|
|
@@ -2920,6 +3171,16 @@ async function buildServer(config) {
|
|
|
2920
3171
|
openWorldHint: true
|
|
2921
3172
|
}
|
|
2922
3173
|
}, (args) => handleListUsers(api, args));
|
|
3174
|
+
server.registerTool("list_contacts", {
|
|
3175
|
+
title: "List / search contacts across the practice",
|
|
3176
|
+
description: "Find contacts by free-text search (name, email, phone, code — minimum 3 characters) or filter to contacts linked to a specific client. Answers 'find John Smith', 'who are the contacts for ACME?', 'search for jane@example.com', 'how many contacts do we have?'. Supports pagination and sorting by Name, FirstName, LastName, or ClientCount.",
|
|
3177
|
+
inputSchema: ListContactsInputSchema,
|
|
3178
|
+
annotations: {
|
|
3179
|
+
readOnlyHint: true,
|
|
3180
|
+
idempotentHint: true,
|
|
3181
|
+
openWorldHint: true
|
|
3182
|
+
}
|
|
3183
|
+
}, (args) => handleListContacts(api, args));
|
|
2923
3184
|
registerWriteTool(server, config.context, "add_task_note", {
|
|
2924
3185
|
title: "Add a note to a task",
|
|
2925
3186
|
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.",
|
|
@@ -2953,6 +3214,17 @@ async function buildServer(config) {
|
|
|
2953
3214
|
openWorldHint: true
|
|
2954
3215
|
}
|
|
2955
3216
|
}, (args) => handleSetClientCustomFields(api, args));
|
|
3217
|
+
registerWriteTool(server, config.context, "update_contact", {
|
|
3218
|
+
title: "Update a contact's details",
|
|
3219
|
+
description: "Update fields on an existing contact: name, email, phone, mobile, date of birth, UTR, NI number, address. The contact's lastName is always required (even if unchanged). Only include fields you want to change — but note the API replaces the entire contact record, so omitted optional fields may be cleared. To be safe, fetch the contact via list_contacts first, then pass all current values plus your changes. Use when the user says things like 'update John's email to ...', 'change the phone number for contact CON-001', 'set Jane's date of birth to ...'.",
|
|
3220
|
+
inputSchema: UpdateContactInputSchema,
|
|
3221
|
+
annotations: {
|
|
3222
|
+
readOnlyHint: false,
|
|
3223
|
+
destructiveHint: false,
|
|
3224
|
+
idempotentHint: true,
|
|
3225
|
+
openWorldHint: true
|
|
3226
|
+
}
|
|
3227
|
+
}, (args) => handleUpdateContact(api, args));
|
|
2956
3228
|
return server;
|
|
2957
3229
|
}
|
|
2958
3230
|
//#endregion
|