@sodiumhq/mcp-pm 0.1.0-beta.6357 → 0.1.0-beta.6554
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 +3 -0
- package/dist/http.js +50 -1
- package/dist/index.js +1 -1
- package/dist/{src-BVx86BgG.mjs → src-B4pIPf3c.mjs} +657 -19
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -92,6 +92,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
|
|
|
92
92
|
|
|
93
93
|
**Client requests**
|
|
94
94
|
- **`list_document_requests`** — document requests across the practice: status, deadline, chase history, linked task. "What are we waiting on from clients right now?"
|
|
95
|
+
- **`list_overdue_requests`** — document requests past their deadline and still waiting on the client, most overdue first, with chase history. "Who do we need to chase?"
|
|
95
96
|
- **`list_data_form_requests`** — questionnaire/data-form requests: who's been sent what, who's responded, which answers await review. "Who still hasn't returned the SA questionnaire?"
|
|
96
97
|
- **`list_data_forms`** — the questionnaire templates the practice can send.
|
|
97
98
|
|
|
@@ -162,6 +163,8 @@ Only enable write mode with an AI client you trust — it hands the client the a
|
|
|
162
163
|
- **`create_document_request`** — draft (never send) a document request for a client. Sending stays in the Sodium UI.
|
|
163
164
|
- **`send_data_form`** — send a questionnaire to a client. Sends immediately, so your assistant confirms with you first.
|
|
164
165
|
- **`create_proposal`** — draft a proposal/letter of engagement in Unsent state. Sending stays in the Sodium UI.
|
|
166
|
+
- **`list_upcoming_key_dates`** — clients whose accounts, confirmation statement, PAYE/CIS or other key date falls in a window, soonest first. "Whose accounts are due next month?"
|
|
167
|
+
- **`draft_email_to_client`** — save a draft email in your own connected mailbox. Never sends; you review and send it yourself.
|
|
165
168
|
- **`update_client_dates`** — set year-end, VAT, PAYE, and other key dates on a client.
|
|
166
169
|
- **`update_client_business_details`** — update UTR, PAYE ref, VAT registration, trading name, addresses. Only the fields you mention change.
|
|
167
170
|
- **`create_saved_task_filter`** — save a task query as a reusable view. "Save that as 'Monday Review' and pin it to my menu."
|
package/dist/http.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { n as buildServer, t as buildCatalogue } from "./src-B4pIPf3c.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";
|
|
@@ -270,6 +270,47 @@ const mcpHandler = createMcpHandler((ctx) => {
|
|
|
270
270
|
console.error(`[sodium-pm-mcp-http] handler: ${error.message}`);
|
|
271
271
|
} });
|
|
272
272
|
const serveMcp = toNodeHandler(mcpHandler);
|
|
273
|
+
let catalogueJson;
|
|
274
|
+
async function listRegisteredTools() {
|
|
275
|
+
const state = {
|
|
276
|
+
apiToken: "catalogue",
|
|
277
|
+
tenant: void 0,
|
|
278
|
+
writesAllowed: true,
|
|
279
|
+
wantsInstructions: false,
|
|
280
|
+
onToolCall: () => {}
|
|
281
|
+
};
|
|
282
|
+
const request = new Request("http://internal/", {
|
|
283
|
+
method: "POST",
|
|
284
|
+
headers: {
|
|
285
|
+
"Content-Type": "application/json",
|
|
286
|
+
Accept: "application/json, text/event-stream",
|
|
287
|
+
"MCP-Protocol-Version": "2025-06-18"
|
|
288
|
+
},
|
|
289
|
+
body: JSON.stringify({
|
|
290
|
+
jsonrpc: "2.0",
|
|
291
|
+
id: 1,
|
|
292
|
+
method: "tools/list",
|
|
293
|
+
params: {}
|
|
294
|
+
})
|
|
295
|
+
});
|
|
296
|
+
const text = await (await mcpHandler.fetch(request, { authInfo: {
|
|
297
|
+
token: "catalogue",
|
|
298
|
+
clientId: "catalogue",
|
|
299
|
+
scopes: [],
|
|
300
|
+
extra: { sodium: state }
|
|
301
|
+
} })).text();
|
|
302
|
+
const payload = text.trimStart().startsWith("{") ? text : text.split("\n").filter((l) => l.startsWith("data:")).map((l) => l.slice(5).trim()).pop() ?? "";
|
|
303
|
+
const tools = JSON.parse(payload).result?.tools;
|
|
304
|
+
if (!tools) throw new Error("tools/list returned no tools.");
|
|
305
|
+
return tools;
|
|
306
|
+
}
|
|
307
|
+
function getCatalogueJson() {
|
|
308
|
+
catalogueJson ??= listRegisteredTools().then((tools) => JSON.stringify(buildCatalogue(tools))).catch((error) => {
|
|
309
|
+
catalogueJson = void 0;
|
|
310
|
+
throw error;
|
|
311
|
+
});
|
|
312
|
+
return catalogueJson;
|
|
313
|
+
}
|
|
273
314
|
function jsonRpcMethod(body) {
|
|
274
315
|
if (typeof body === "object" && body !== null && "method" in body) return String(body.method);
|
|
275
316
|
}
|
|
@@ -393,6 +434,14 @@ async function handle(req, res) {
|
|
|
393
434
|
}));
|
|
394
435
|
return;
|
|
395
436
|
}
|
|
437
|
+
if (req.method === "GET" && url.pathname === "/tools.json") {
|
|
438
|
+
const body = await getCatalogueJson();
|
|
439
|
+
res.writeHead(200, {
|
|
440
|
+
"Content-Type": "application/json",
|
|
441
|
+
"Cache-Control": "public, max-age=300"
|
|
442
|
+
}).end(body);
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
396
445
|
if (AUTH && req.method === "GET" && url.pathname.startsWith("/.well-known/oauth-protected-resource")) {
|
|
397
446
|
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify(protectedResourceMetadata(AUTH)));
|
|
398
447
|
return;
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { n as buildServer } from "./src-B4pIPf3c.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -1364,6 +1364,22 @@ const getEmailBroadcastRecipients = (options) => (options.client ?? client).get(
|
|
|
1364
1364
|
...options
|
|
1365
1365
|
});
|
|
1366
1366
|
/**
|
|
1367
|
+
* List Email Configs
|
|
1368
|
+
*
|
|
1369
|
+
* Returns a paginated list of email configurations the current user can access (owned or shared). EmailConnections admins see all configurations including other users' private ones.
|
|
1370
|
+
*/
|
|
1371
|
+
const listEmailConfigs = (options) => (options.client ?? client).get({
|
|
1372
|
+
security: [{
|
|
1373
|
+
name: "x-api-key",
|
|
1374
|
+
type: "apiKey"
|
|
1375
|
+
}, {
|
|
1376
|
+
scheme: "bearer",
|
|
1377
|
+
type: "http"
|
|
1378
|
+
}],
|
|
1379
|
+
url: "/tenants/{tenant}/email-configs",
|
|
1380
|
+
...options
|
|
1381
|
+
});
|
|
1382
|
+
/**
|
|
1367
1383
|
* List Email History
|
|
1368
1384
|
*
|
|
1369
1385
|
* Returns a paginated list of emails sent for the tenant. Supports filtering by status, date range, search, and metadata fields. Metadata filters use the format metadataParam=key=value (e.g., metadataParam=entityType=Proposal).
|
|
@@ -1412,6 +1428,26 @@ const getMailboxConversation = (options) => (options.client ?? client).get({
|
|
|
1412
1428
|
...options
|
|
1413
1429
|
});
|
|
1414
1430
|
/**
|
|
1431
|
+
* Create Draft
|
|
1432
|
+
*
|
|
1433
|
+
* Creates a new draft via the email provider. The configuration must have ReadEnabled set to true. The returned UniqueId is used for subsequent Update/Send/Discard calls.
|
|
1434
|
+
*/
|
|
1435
|
+
const createMailboxDraft = (options) => (options.client ?? client).post({
|
|
1436
|
+
security: [{
|
|
1437
|
+
name: "x-api-key",
|
|
1438
|
+
type: "apiKey"
|
|
1439
|
+
}, {
|
|
1440
|
+
scheme: "bearer",
|
|
1441
|
+
type: "http"
|
|
1442
|
+
}],
|
|
1443
|
+
url: "/tenants/{tenant}/email-configs/{code}/drafts",
|
|
1444
|
+
...options,
|
|
1445
|
+
headers: {
|
|
1446
|
+
"Content-Type": "application/json",
|
|
1447
|
+
...options.headers
|
|
1448
|
+
}
|
|
1449
|
+
});
|
|
1450
|
+
/**
|
|
1415
1451
|
* Get engagement settings
|
|
1416
1452
|
*
|
|
1417
1453
|
* Returns the engagement settings for the specified tenant
|
|
@@ -3393,6 +3429,37 @@ var SodiumApiClient = class {
|
|
|
3393
3429
|
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `list emails for client ${clientCode}`);
|
|
3394
3430
|
return data;
|
|
3395
3431
|
}
|
|
3432
|
+
async findOwnDraftMailbox() {
|
|
3433
|
+
const correlationId = randomUUID();
|
|
3434
|
+
const userCode = await this.getCurrentTenantUserCode();
|
|
3435
|
+
const { data, error, response } = await listEmailConfigs({
|
|
3436
|
+
path: { tenant: this.tenant },
|
|
3437
|
+
query: {
|
|
3438
|
+
status: "Active",
|
|
3439
|
+
limit: 50,
|
|
3440
|
+
offset: 0
|
|
3441
|
+
},
|
|
3442
|
+
headers: { "X-Correlation-Id": correlationId },
|
|
3443
|
+
client: this.http
|
|
3444
|
+
});
|
|
3445
|
+
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "list your connected mailboxes");
|
|
3446
|
+
const own = (data.data ?? []).filter((c) => c.tenantUser?.code === userCode && c.readEnabled && c.code);
|
|
3447
|
+
return own.find((c) => c.isDefault) ?? own[0];
|
|
3448
|
+
}
|
|
3449
|
+
async createMailboxDraft(mailboxCode, body) {
|
|
3450
|
+
const correlationId = randomUUID();
|
|
3451
|
+
const { data, error, response } = await createMailboxDraft({
|
|
3452
|
+
path: {
|
|
3453
|
+
tenant: this.tenant,
|
|
3454
|
+
code: mailboxCode
|
|
3455
|
+
},
|
|
3456
|
+
body,
|
|
3457
|
+
headers: { "X-Correlation-Id": correlationId },
|
|
3458
|
+
client: this.http
|
|
3459
|
+
});
|
|
3460
|
+
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "save the email draft");
|
|
3461
|
+
return data;
|
|
3462
|
+
}
|
|
3396
3463
|
async listAmlClients(query = {}) {
|
|
3397
3464
|
const correlationId = randomUUID();
|
|
3398
3465
|
const { data, error, response } = await getAmlClients({
|
|
@@ -4052,7 +4119,7 @@ async function handleGetPracticeDetails(api) {
|
|
|
4052
4119
|
}
|
|
4053
4120
|
//#endregion
|
|
4054
4121
|
//#region ../mcp-core/src/tools/list-clients.ts
|
|
4055
|
-
const statusEnum$
|
|
4122
|
+
const statusEnum$16 = z.enum([
|
|
4056
4123
|
"Active",
|
|
4057
4124
|
"Inactive",
|
|
4058
4125
|
"Prospect",
|
|
@@ -4071,7 +4138,7 @@ const typeEnum$1 = z.enum([
|
|
|
4071
4138
|
const sortByEnum$20 = z.enum(["Name", "InternalReference"]);
|
|
4072
4139
|
const ListClientsInputSchema = {
|
|
4073
4140
|
search: z.string().min(3, "Search must be at least 3 characters when provided").optional().describe("Free-text search across client code, name, and internal reference. Minimum 3 characters. Omit to browse by filter only."),
|
|
4074
|
-
status: z.array(statusEnum$
|
|
4141
|
+
status: z.array(statusEnum$16).optional().describe("Filter by client status. Defaults to all statuses if omitted. Example: ['Active'] for active clients only."),
|
|
4075
4142
|
type: z.array(typeEnum$1).optional().describe("Filter by organisation type. Use ['PrivateLimitedCompany', 'PublicLimitedCompany'] for 'limited companies'. Use ['LimitedLiabilityPartnership'] for LLPs. Defaults to all types if omitted."),
|
|
4076
4143
|
managerCode: z.array(z.string()).optional().describe("Filter by assigned manager user codes."),
|
|
4077
4144
|
partnerCode: z.array(z.string()).optional().describe("Filter by assigned partner user codes."),
|
|
@@ -4884,7 +4951,7 @@ function describeFilters$3(args) {
|
|
|
4884
4951
|
}
|
|
4885
4952
|
//#endregion
|
|
4886
4953
|
//#region ../mcp-core/src/tools/list-tasks.ts
|
|
4887
|
-
const statusEnum$
|
|
4954
|
+
const statusEnum$15 = z.enum([
|
|
4888
4955
|
"NotStarted",
|
|
4889
4956
|
"InProgress",
|
|
4890
4957
|
"Blocked",
|
|
@@ -4945,7 +5012,7 @@ const stepTypeEnum$1 = z.enum([
|
|
|
4945
5012
|
const ListTasksInputSchema = {
|
|
4946
5013
|
user: z.array(z.string()).optional().describe("Filter by assigned user codes. For 'my tasks', this is the signed-in user's practice user code, which whoami shows. For another team member's tasks, pass their code. Omit to see tasks across all users (useful for practice managers)."),
|
|
4947
5014
|
client: z.array(z.string()).optional().describe("Filter by client codes — tasks belonging to these specific clients only."),
|
|
4948
|
-
status: z.array(statusEnum$
|
|
5015
|
+
status: z.array(statusEnum$15).optional().describe("Filter by task status. Typical: ['NotStarted', 'InProgress'] to exclude completed/skipped. Leave empty for all statuses. If the query includes NotStarted (the default for new tasks), it also needs one of: dateRange, isOverdue=true, or restrict status to non-NotStarted values only — otherwise the API rejects the call to prevent unbounded queries."),
|
|
4949
5016
|
isOverdue: z.boolean().optional().describe("Set true to return only overdue tasks (DueDate before today, not completed/skipped). When true, no date range is required. Useful for sidestepping the NotStarted + date-range requirement."),
|
|
4950
5017
|
dateRange: dateRangeEnum$2.optional().describe("Preset date range. Use 'Today' for today's tasks, 'Next7Days' for 'due this week', 'Last30Days' for recent activity. If 'CustomDateRange', also provide startDate and/or endDate. REQUIRED when querying NotStarted tasks unless you pass isOverdue=true."),
|
|
4951
5018
|
startDate: z.string().optional().describe("Start of custom date range (YYYY-MM-DD). Used when dateRange='CustomDateRange'. Both startDate and endDate with a narrow window perform best — broad ranges strain the API. If startDate > endDate the API swaps them silently. Maximum allowed total range is 2 years."),
|
|
@@ -5045,7 +5112,7 @@ function describeFilters$2(args) {
|
|
|
5045
5112
|
}
|
|
5046
5113
|
//#endregion
|
|
5047
5114
|
//#region ../mcp-core/src/tools/list-users.ts
|
|
5048
|
-
const statusEnum$
|
|
5115
|
+
const statusEnum$14 = z.enum([
|
|
5049
5116
|
"Created",
|
|
5050
5117
|
"Invited",
|
|
5051
5118
|
"Active",
|
|
@@ -5065,7 +5132,7 @@ const sortByEnum$17 = z.enum([
|
|
|
5065
5132
|
]);
|
|
5066
5133
|
const ListUsersInputSchema = {
|
|
5067
5134
|
search: z.string().min(3, "Search must be at least 3 characters when provided").optional().describe("Free-text search across user code, first name, last name, and email. Minimum 3 characters. Use for 'find Jane' when the name isn't in the startup roster (large teams, non-active users)."),
|
|
5068
|
-
status: statusEnum$
|
|
5135
|
+
status: statusEnum$14.optional().describe("Filter by a single user status. Use 'Active' for currently-working members, 'Invited' for pending invitations, 'Disabled' for offboarded users, 'Deleted' for removed users. Note: this is a single value, not an array."),
|
|
5069
5136
|
systemRole: systemRoleEnum.optional().describe("Filter by system role. Admin = full access, StandardUser = normal access, Viewer = read-only. Single value."),
|
|
5070
5137
|
isClientManager: z.boolean().optional().describe("Set true to return only users flagged as client managers; false to exclude them."),
|
|
5071
5138
|
isPartner: z.boolean().optional().describe("Set true to return only users flagged as partners; false to exclude them. Use for 'list partners' queries."),
|
|
@@ -5632,7 +5699,7 @@ const GetServiceDeliveryReportInputSchema = {
|
|
|
5632
5699
|
...reportEntityFilters,
|
|
5633
5700
|
...reportPagingFields
|
|
5634
5701
|
};
|
|
5635
|
-
function formatRow$
|
|
5702
|
+
function formatRow$8(r) {
|
|
5636
5703
|
const statutory = (r.statutoryCount ?? 0) > 0 ? ` · statutory ${fmtPct(r.statutoryOnTimePct)} (of ${r.statutoryCount})` : "";
|
|
5637
5704
|
const worst = fmtRef(r.worstTask);
|
|
5638
5705
|
return `- ${r.groupName}: ${r.completedCount} completed · avg ${fmtDays(r.avgTurnaroundDays)} / median ${fmtDays(r.medianTurnaroundDays)} · on-time ${fmtPct(r.onTimePct)}${statutory} · worst ${r.worstCaseDays ?? 0}d${worst ? ` — ${worst}` : ""}`;
|
|
@@ -5648,7 +5715,7 @@ async function handleGetServiceDeliveryReport(api, args) {
|
|
|
5648
5715
|
`Summary: ${s?.completedCount ?? 0} completed · avg ${fmtDays(s?.avgTurnaroundDays)} / median ${fmtDays(s?.medianTurnaroundDays)} turnaround · on-time ${fmtPct(s?.onTimePct)}` + ((s?.statutoryCount ?? 0) > 0 ? ` · statutory compliance ${fmtPct(s?.statutoryOnTimePct)} (of ${s?.statutoryCount} with statutory deadlines)` : " · no statutory deadlines in period") + ` · ${s?.overdueCount ?? 0} currently overdue (now-snapshot, ignores the window)`,
|
|
5649
5716
|
"",
|
|
5650
5717
|
`Breakdown by ${args.groupBy ?? "Service"} (${header}):`,
|
|
5651
|
-
...rows.map(formatRow$
|
|
5718
|
+
...rows.map(formatRow$8)
|
|
5652
5719
|
];
|
|
5653
5720
|
if (rows.length === 0) lines.push("(no tasks completed in the window)");
|
|
5654
5721
|
if (hint) lines.push("", hint);
|
|
@@ -5723,7 +5790,7 @@ const GetDeadlineComplianceReportInputSchema = {
|
|
|
5723
5790
|
...reportEntityFilters,
|
|
5724
5791
|
...reportPagingFields
|
|
5725
5792
|
};
|
|
5726
|
-
function formatRow$
|
|
5793
|
+
function formatRow$7(r) {
|
|
5727
5794
|
const worst = fmtRef(r.worstTask);
|
|
5728
5795
|
const pending = (r.pendingCount ?? 0) > 0 ? ` · ${r.pendingCount} not yet due` : "";
|
|
5729
5796
|
return `- ${r.groupName}: ${r.deadlineCount} deadlines · ${r.metCount} met (${fmtPct(r.compliancePct)}) · ${r.lateCount} completed late · ${r.missedOpenCount} missed & still open${pending} · avg ${fmtDays(r.avgDaysLate)} late${worst ? ` · worst ${r.worstDaysLate}d — ${worst}` : ""}`;
|
|
@@ -5742,7 +5809,7 @@ async function handleGetDeadlineComplianceReport(api, args) {
|
|
|
5742
5809
|
"Upcoming (now-snapshot of OPEN tasks, ignores the window): " + (report.upcomingDeadlines ?? []).map((b) => `${b.count} ${b.code === "overdue" ? "overdue" : `due in ${b.code} days`}`).join(" · "),
|
|
5743
5810
|
"",
|
|
5744
5811
|
`Breakdown by ${args.groupBy ?? "Service"} (${header}):`,
|
|
5745
|
-
...rows.map(formatRow$
|
|
5812
|
+
...rows.map(formatRow$7)
|
|
5746
5813
|
];
|
|
5747
5814
|
if (rows.length === 0) lines.push(`(no ${basis} fell in the window)`);
|
|
5748
5815
|
if (hint) lines.push("", hint);
|
|
@@ -5769,7 +5836,7 @@ const GetClientResponsivenessReportInputSchema = {
|
|
|
5769
5836
|
category: reportEntityFilters.category,
|
|
5770
5837
|
...reportPagingFields
|
|
5771
5838
|
};
|
|
5772
|
-
function formatRow$
|
|
5839
|
+
function formatRow$6(r) {
|
|
5773
5840
|
const worst = fmtRef(r.worstTask);
|
|
5774
5841
|
return `- ${r.groupName}: ${r.stepCount} steps · avg ${fmtDays(r.avgResponseDays)} / median ${fmtDays(r.medianResponseDays)} response · ${r.totalChases} chases (${fmtPct(r.chasedPct)} of steps chased)` + (r.worstCaseDays != null ? ` · worst ${fmtDays(r.worstCaseDays)}${worst ? ` — ${worst}` : ""}` : "");
|
|
5775
5842
|
}
|
|
@@ -5784,7 +5851,7 @@ async function handleGetClientResponsivenessReport(api, args) {
|
|
|
5784
5851
|
`Summary: ${s?.stepCount ?? 0} steps · avg ${fmtDays(s?.avgResponseDays)} / median ${fmtDays(s?.medianResponseDays)} response · ${s?.totalChases ?? 0} chases (${fmtPct(s?.chasedPct)} of steps chased) · ${s?.outstandingCount ?? 0} currently waiting on a client (now-snapshot${s?.oldestOutstandingDays != null ? `, oldest ${fmtDays(s.oldestOutstandingDays)}` : ""})`,
|
|
5785
5852
|
"",
|
|
5786
5853
|
`Breakdown by ${args.groupBy ?? "Client"} (${header}):`,
|
|
5787
|
-
...rows.map(formatRow$
|
|
5854
|
+
...rows.map(formatRow$6)
|
|
5788
5855
|
];
|
|
5789
5856
|
if (rows.length === 0) lines.push("(no client-facing steps completed in the window)");
|
|
5790
5857
|
if (hint) lines.push("", hint);
|
|
@@ -5807,7 +5874,7 @@ const GetTeamWorkloadReportInputSchema = {
|
|
|
5807
5874
|
category: reportEntityFilters.category,
|
|
5808
5875
|
...reportPagingFields
|
|
5809
5876
|
};
|
|
5810
|
-
function formatRow$
|
|
5877
|
+
function formatRow$5(r) {
|
|
5811
5878
|
const open = `${r.openCount} open (${r.overdueCount} overdue, ${r.blockedCount} blocked)`;
|
|
5812
5879
|
const done = (r.completedCount ?? 0) > 0 ? `${r.completedCount} completed · avg ${fmtDays(r.avgTurnaroundDays)} · on-time ${fmtPct(r.onTimePct)}` : "0 completed";
|
|
5813
5880
|
return `- ${r.groupName}: ${open} · ${done}`;
|
|
@@ -5823,7 +5890,7 @@ async function handleGetTeamWorkloadReport(api, args) {
|
|
|
5823
5890
|
`Summary: ${s?.openCount ?? 0} open (${s?.overdueCount ?? 0} overdue, ${s?.blockedCount ?? 0} blocked, ${s?.unassignedCount ?? 0} unassigned) · ${s?.completedCount ?? 0} completed in window · avg turnaround ${fmtDays(s?.avgTurnaroundDays)}`,
|
|
5824
5891
|
"",
|
|
5825
5892
|
`Breakdown by ${args.groupBy ?? "User"} (busiest first, ${header}):`,
|
|
5826
|
-
...rows.map(formatRow$
|
|
5893
|
+
...rows.map(formatRow$5)
|
|
5827
5894
|
];
|
|
5828
5895
|
if (rows.length === 0) lines.push("(no open or completed tasks match the filters)");
|
|
5829
5896
|
if (hint) lines.push("", hint);
|
|
@@ -5847,7 +5914,7 @@ const GetWipAgeingReportInputSchema = {
|
|
|
5847
5914
|
...reportEntityFilters,
|
|
5848
5915
|
...reportPagingFields
|
|
5849
5916
|
};
|
|
5850
|
-
function formatRow$
|
|
5917
|
+
function formatRow$4(r) {
|
|
5851
5918
|
const oldest = fmtRef(r.oldestTask);
|
|
5852
5919
|
return `- ${r.groupName}: ${r.count} open · avg age ${fmtDays(r.avgAgeDays)} · oldest ${r.oldestAgeDays ?? 0}d${oldest ? ` — ${oldest}` : ""} · ${r.blockedCount} blocked · ${r.overdueCount} overdue`;
|
|
5853
5920
|
}
|
|
@@ -5863,7 +5930,7 @@ async function handleGetWipAgeingReport(api, args) {
|
|
|
5863
5930
|
`Summary: ${s?.openCount ?? 0} open · ${s?.notStartedCount ?? 0} not started · ${s?.blockedCount ?? 0} blocked · ${s?.overdueCount ?? 0} overdue · avg age ${fmtDays(s?.avgAgeDays)}${s?.oldestAgeDays != null ? ` · oldest ${s.oldestAgeDays}d${oldest ? ` — ${oldest}` : ""}` : ""}`,
|
|
5864
5931
|
"",
|
|
5865
5932
|
`Breakdown by ${args.groupBy ?? "AgeBucket"} (${header}):`,
|
|
5866
|
-
...rows.map(formatRow$
|
|
5933
|
+
...rows.map(formatRow$4)
|
|
5867
5934
|
];
|
|
5868
5935
|
if (rows.length === 0) lines.push("(no open work matches the filters)");
|
|
5869
5936
|
if (hint) lines.push("", hint);
|
|
@@ -6742,7 +6809,7 @@ async function handleGetCompaniesHouseProfile(api, args) {
|
|
|
6742
6809
|
}
|
|
6743
6810
|
//#endregion
|
|
6744
6811
|
//#region ../mcp-core/src/tools/list-document-requests.ts
|
|
6745
|
-
const statusEnum$
|
|
6812
|
+
const statusEnum$13 = z.enum([
|
|
6746
6813
|
"Draft",
|
|
6747
6814
|
"Open",
|
|
6748
6815
|
"Submitted",
|
|
@@ -6756,7 +6823,7 @@ const sortByEnum$11 = z.enum([
|
|
|
6756
6823
|
"Deadline"
|
|
6757
6824
|
]);
|
|
6758
6825
|
const ListDocumentRequestsInputSchema = {
|
|
6759
|
-
status: statusEnum$
|
|
6826
|
+
status: statusEnum$13.optional().describe("Filter by request status. 'Open' = sent and waiting on the client (use for 'what are we waiting on from clients?'). 'Submitted' = client responded, awaiting practice review (use for 'what needs reviewing?'). 'Draft' = not yet sent. 'Accepted'/'Rejected' = reviewed. Single value — call per status for multiple."),
|
|
6760
6827
|
clientCode: z.string().optional().describe("Filter to one client's document requests. Get the code from list_clients."),
|
|
6761
6828
|
managerCode: z.string().optional().describe("Filter by the client manager's user code — 'what are MY clients waiting to send us?' for a manager."),
|
|
6762
6829
|
partnerCode: z.string().optional().describe("Filter by the client partner's user code."),
|
|
@@ -6821,6 +6888,274 @@ async function handleListDocumentRequests(api, args) {
|
|
|
6821
6888
|
}
|
|
6822
6889
|
}
|
|
6823
6890
|
//#endregion
|
|
6891
|
+
//#region ../mcp-core/src/tools/list-overdue-requests.ts
|
|
6892
|
+
const PAGE_SIZE = 50;
|
|
6893
|
+
const MAX_PAGES = 4;
|
|
6894
|
+
const ListOverdueRequestsInputSchema = {
|
|
6895
|
+
clientCode: z.string().optional().describe("Only this client's overdue requests. Get the code from list_clients."),
|
|
6896
|
+
managerCode: z.string().optional().describe("Only requests for clients with this manager (user code) — 'which of Jane's clients owe us documents?'."),
|
|
6897
|
+
partnerCode: z.string().optional().describe("Only requests for clients with this partner (user code)."),
|
|
6898
|
+
taskCategoryCode: z.string().optional().describe("Only requests linked to tasks in this category. Codes come from list_task_categories."),
|
|
6899
|
+
limit: z.number().int().min(1).max(50).optional().describe("Most overdue requests to return. Default 20, max 50.")
|
|
6900
|
+
};
|
|
6901
|
+
function daysOverdue(deadline, today) {
|
|
6902
|
+
return Math.floor((Date.parse(today) - Date.parse(deadline.slice(0, 10))) / 864e5);
|
|
6903
|
+
}
|
|
6904
|
+
function formatRow$3(r, today) {
|
|
6905
|
+
const client = r.client?.name ? ` · ${r.client.name}${r.client.code ? ` (${r.client.code})` : ""}` : "";
|
|
6906
|
+
const days = daysOverdue(r.deadline, today);
|
|
6907
|
+
const chase = r.chase?.lastChasedDate ? `last chased ${r.chase.lastChasedDate.slice(0, 10)}` : "never chased";
|
|
6908
|
+
const next = r.chase?.nextChaseDate ? `, next chase ${r.chase.nextChaseDate.slice(0, 10)}` : "";
|
|
6909
|
+
return `- ${days} day${days === 1 ? "" : "s"} overdue · ${r.title ?? "(untitled)"} (${r.code ?? "no code"})${client} · deadline ${r.deadline.slice(0, 10)} · ${chase}${next}`;
|
|
6910
|
+
}
|
|
6911
|
+
async function handleListOverdueRequests(api, args) {
|
|
6912
|
+
try {
|
|
6913
|
+
const today = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
6914
|
+
const overdue = [];
|
|
6915
|
+
let scanned = 0;
|
|
6916
|
+
let more = false;
|
|
6917
|
+
for (let page = 0; page < MAX_PAGES; page++) {
|
|
6918
|
+
const result = await api.listDocumentRequests({
|
|
6919
|
+
status: "Open",
|
|
6920
|
+
clientCode: args.clientCode,
|
|
6921
|
+
managerCode: args.managerCode,
|
|
6922
|
+
partnerCode: args.partnerCode,
|
|
6923
|
+
taskCategoryCode: args.taskCategoryCode,
|
|
6924
|
+
sortBy: "Deadline",
|
|
6925
|
+
sortDesc: false,
|
|
6926
|
+
limit: PAGE_SIZE,
|
|
6927
|
+
offset: page * PAGE_SIZE
|
|
6928
|
+
});
|
|
6929
|
+
const items = result.data ?? [];
|
|
6930
|
+
scanned += items.length;
|
|
6931
|
+
let reachedNotDue = false;
|
|
6932
|
+
for (const r of items) if (r.deadline && r.deadline.slice(0, 10) < today) overdue.push(r);
|
|
6933
|
+
else if (r.deadline) {
|
|
6934
|
+
reachedNotDue = true;
|
|
6935
|
+
break;
|
|
6936
|
+
}
|
|
6937
|
+
if (reachedNotDue || !result.hasMore) break;
|
|
6938
|
+
if (page === MAX_PAGES - 1) more = true;
|
|
6939
|
+
}
|
|
6940
|
+
if (overdue.length === 0) return { content: [{
|
|
6941
|
+
type: "text",
|
|
6942
|
+
text: more ? `No overdue document requests found in the first ${scanned} open requests checked.` : "No overdue document requests. Every open request is within its deadline or has none."
|
|
6943
|
+
}] };
|
|
6944
|
+
overdue.sort((a, b) => a.deadline.localeCompare(b.deadline));
|
|
6945
|
+
const limit = args.limit ?? 20;
|
|
6946
|
+
const shown = overdue.slice(0, limit);
|
|
6947
|
+
const lines = [
|
|
6948
|
+
overdue.length > shown.length ? `${overdue.length} overdue document requests (showing the ${shown.length} most overdue):` : `${overdue.length} overdue document request${overdue.length === 1 ? "" : "s"}, most overdue first:`,
|
|
6949
|
+
"",
|
|
6950
|
+
...shown.map((r) => formatRow$3(r, today))
|
|
6951
|
+
];
|
|
6952
|
+
if (more) lines.push("", `Checked the first ${scanned} open requests only; narrow with clientCode or managerCode to be sure nothing is missed.`);
|
|
6953
|
+
return { content: [{
|
|
6954
|
+
type: "text",
|
|
6955
|
+
text: lines.join("\n")
|
|
6956
|
+
}] };
|
|
6957
|
+
} catch (error) {
|
|
6958
|
+
return {
|
|
6959
|
+
content: [{
|
|
6960
|
+
type: "text",
|
|
6961
|
+
text: error instanceof SodiumApiError ? `Error listing overdue requests: ${error.message} (correlation: ${error.correlationId})` : `Error listing overdue requests: ${error instanceof Error ? error.message : String(error)}`
|
|
6962
|
+
}],
|
|
6963
|
+
isError: true
|
|
6964
|
+
};
|
|
6965
|
+
}
|
|
6966
|
+
}
|
|
6967
|
+
//#endregion
|
|
6968
|
+
//#region ../mcp-core/src/tools/list-upcoming-key-dates.ts
|
|
6969
|
+
const keyDateTypeEnum = z.enum([
|
|
6970
|
+
"AccountsNextDue",
|
|
6971
|
+
"ConfirmationStatementDue",
|
|
6972
|
+
"NextVatReturnDueAnnual",
|
|
6973
|
+
"NextPayeCisFilingDue",
|
|
6974
|
+
"PensionReEnrolmentDate",
|
|
6975
|
+
"CharityAnnualReturnDue",
|
|
6976
|
+
"CharityAccountsDue",
|
|
6977
|
+
"YearEnd",
|
|
6978
|
+
"AccountsNextPeriodStart",
|
|
6979
|
+
"AccountsNextPeriodEnd",
|
|
6980
|
+
"ConfirmationStatementNextMadeUpTo",
|
|
6981
|
+
"IncorporationDate",
|
|
6982
|
+
"AccountsLastPeriodFiled",
|
|
6983
|
+
"AccountsLastPeriodStart",
|
|
6984
|
+
"AccountsLastPeriodEnd",
|
|
6985
|
+
"AccountsLastMadeUpTo",
|
|
6986
|
+
"LastConfirmationStatementFiled",
|
|
6987
|
+
"LastConfirmationStatementMadeUpTo",
|
|
6988
|
+
"CharityLastAnnualReturnReceived",
|
|
6989
|
+
"CharityLastAccountsReceived",
|
|
6990
|
+
"CharityFinancialYearEnd"
|
|
6991
|
+
]);
|
|
6992
|
+
const rangeEnum = z.enum([
|
|
6993
|
+
"Today",
|
|
6994
|
+
"ThisWeek",
|
|
6995
|
+
"NextWeek",
|
|
6996
|
+
"Next7Days",
|
|
6997
|
+
"Next30Days",
|
|
6998
|
+
"ThisMonth",
|
|
6999
|
+
"NextMonth",
|
|
7000
|
+
"ThisQuarter",
|
|
7001
|
+
"NextQuarter",
|
|
7002
|
+
"ThisYear",
|
|
7003
|
+
"NextYear",
|
|
7004
|
+
"ThisTaxYear",
|
|
7005
|
+
"NextTaxYear",
|
|
7006
|
+
"CustomDateRange"
|
|
7007
|
+
]);
|
|
7008
|
+
const statusEnum$12 = z.enum([
|
|
7009
|
+
"Active",
|
|
7010
|
+
"Inactive",
|
|
7011
|
+
"Prospect",
|
|
7012
|
+
"LostProspect"
|
|
7013
|
+
]);
|
|
7014
|
+
const ListUpcomingKeyDatesInputSchema = {
|
|
7015
|
+
keyDateType: keyDateTypeEnum.describe("Which key date to look at. 'AccountsNextDue' = statutory accounts filing deadline, 'ConfirmationStatementDue' = Companies House confirmation statement, 'NextVatReturnDueAnnual' = annual-scheme VAT return, 'NextPayeCisFilingDue' = PAYE/CIS filing, 'PensionReEnrolmentDate' = auto-enrolment re-enrolment, 'CharityAnnualReturnDue' / 'CharityAccountsDue' = charity filings, 'YearEnd' = accounting year end. The quarterly and monthly VAT return dates and MTD ITSA quarter dates are calculated per client and cannot be searched this way."),
|
|
7016
|
+
keyDateRange: rangeEnum.optional().describe("Window the date must fall in. Defaults to Next30Days. Use 'Next7Days' for 'due this week or so', 'ThisMonth' / 'NextMonth' for month views, 'ThisQuarter' / 'NextQuarter' for quarter planning. 'CustomDateRange' needs keyDateFrom and keyDateTo."),
|
|
7017
|
+
keyDateFrom: z.string().optional().describe("Start of the window (YYYY-MM-DD, inclusive). Only with keyDateRange='CustomDateRange'."),
|
|
7018
|
+
keyDateTo: z.string().optional().describe("End of the window (YYYY-MM-DD, inclusive). Only with keyDateRange='CustomDateRange'."),
|
|
7019
|
+
status: z.array(statusEnum$12).optional().describe("Client statuses to include. Defaults to ['Active'] so dormant clients and prospects don't clutter the list."),
|
|
7020
|
+
managerCode: z.array(z.string()).optional().describe("Only clients with these managers (user codes). Use for 'Jane's clients with accounts due soon'."),
|
|
7021
|
+
partnerCode: z.array(z.string()).optional().describe("Only clients with these partners (user codes)."),
|
|
7022
|
+
teamCode: z.array(z.string()).optional().describe("Only clients assigned to these teams. Codes come from list_teams."),
|
|
7023
|
+
limit: z.number().int().min(0).max(50).optional().describe("Clients per page. Default 25, max 50. Pass 0 for a count only ('how many clients have accounts due next month?')."),
|
|
7024
|
+
offset: z.number().int().min(0).optional().describe("Number of clients to skip for pagination. Default 0.")
|
|
7025
|
+
};
|
|
7026
|
+
function formatRow$2(c, dateType) {
|
|
7027
|
+
const date = c.keyDates?.find((d) => d.dateType === dateType)?.date;
|
|
7028
|
+
return `- ${date ? date.slice(0, 10) : "no date"} · ${c.name ?? "(no name)"} (${c.code ?? "no code"})`;
|
|
7029
|
+
}
|
|
7030
|
+
async function handleListUpcomingKeyDates(api, args) {
|
|
7031
|
+
try {
|
|
7032
|
+
const range = args.keyDateRange ?? "Next30Days";
|
|
7033
|
+
if (range === "CustomDateRange" && (!args.keyDateFrom || !args.keyDateTo)) return {
|
|
7034
|
+
content: [{
|
|
7035
|
+
type: "text",
|
|
7036
|
+
text: "keyDateFrom and keyDateTo are both needed when keyDateRange is CustomDateRange."
|
|
7037
|
+
}],
|
|
7038
|
+
isError: true
|
|
7039
|
+
};
|
|
7040
|
+
const limit = args.limit ?? 25;
|
|
7041
|
+
const query = {
|
|
7042
|
+
keyDateType: args.keyDateType,
|
|
7043
|
+
keyDateRange: range,
|
|
7044
|
+
keyDateFrom: range === "CustomDateRange" ? args.keyDateFrom : void 0,
|
|
7045
|
+
keyDateTo: range === "CustomDateRange" ? args.keyDateTo : void 0,
|
|
7046
|
+
status: args.status?.length ? args.status : ["Active"],
|
|
7047
|
+
managerCode: args.managerCode,
|
|
7048
|
+
partnerCode: args.partnerCode,
|
|
7049
|
+
teamCode: args.teamCode,
|
|
7050
|
+
sortBy: "KeyDate",
|
|
7051
|
+
sortKeyDateType: args.keyDateType,
|
|
7052
|
+
limit,
|
|
7053
|
+
offset: args.offset
|
|
7054
|
+
};
|
|
7055
|
+
const result = await api.listClients(query);
|
|
7056
|
+
const items = result.data ?? [];
|
|
7057
|
+
const total = result.totalCount ?? items.length;
|
|
7058
|
+
const window = range === "CustomDateRange" ? `${args.keyDateFrom} to ${args.keyDateTo}` : range;
|
|
7059
|
+
if (limit === 0) return { content: [{
|
|
7060
|
+
type: "text",
|
|
7061
|
+
text: `${total} client${total === 1 ? "" : "s"} with ${args.keyDateType} in ${window}.`
|
|
7062
|
+
}] };
|
|
7063
|
+
if (items.length === 0) return { content: [{
|
|
7064
|
+
type: "text",
|
|
7065
|
+
text: `No clients have ${args.keyDateType} in ${window}.`
|
|
7066
|
+
}] };
|
|
7067
|
+
const lines = [
|
|
7068
|
+
total > items.length ? `${total} clients have ${args.keyDateType} in ${window} (showing ${items.length}, soonest first):` : `${items.length} client${items.length === 1 ? "" : "s"} with ${args.keyDateType} in ${window}, soonest first:`,
|
|
7069
|
+
"",
|
|
7070
|
+
...items.map((c) => formatRow$2(c, args.keyDateType))
|
|
7071
|
+
];
|
|
7072
|
+
if (result.hasMore) lines.push("", `More results available — call again with offset: ${(args.offset ?? 0) + items.length}.`);
|
|
7073
|
+
return { content: [{
|
|
7074
|
+
type: "text",
|
|
7075
|
+
text: lines.join("\n")
|
|
7076
|
+
}] };
|
|
7077
|
+
} catch (error) {
|
|
7078
|
+
return {
|
|
7079
|
+
content: [{
|
|
7080
|
+
type: "text",
|
|
7081
|
+
text: error instanceof SodiumApiError ? `Error listing upcoming key dates: ${error.message} (correlation: ${error.correlationId})` : `Error listing upcoming key dates: ${error instanceof Error ? error.message : String(error)}`
|
|
7082
|
+
}],
|
|
7083
|
+
isError: true
|
|
7084
|
+
};
|
|
7085
|
+
}
|
|
7086
|
+
}
|
|
7087
|
+
//#endregion
|
|
7088
|
+
//#region ../mcp-core/src/tools/draft-email-to-client.ts
|
|
7089
|
+
const recipient = z.object({
|
|
7090
|
+
email: z.string().email().describe("Email address."),
|
|
7091
|
+
name: z.string().optional().describe("Display name (optional).")
|
|
7092
|
+
});
|
|
7093
|
+
const DraftEmailToClientInputSchema = {
|
|
7094
|
+
to: z.array(recipient).min(1, "At least one recipient is required").describe("Who the draft is addressed to. A client's contact emails come from get_client_summary or list_contacts."),
|
|
7095
|
+
subject: z.string().optional().describe("Subject line."),
|
|
7096
|
+
body: z.string().min(1, "Email body cannot be empty").describe("The email body as HTML (paragraphs in <p>, line breaks as <br>). The user's mailbox signature is not added automatically."),
|
|
7097
|
+
cc: z.array(recipient).optional().describe("CC recipients.")
|
|
7098
|
+
};
|
|
7099
|
+
const noMailbox = "I couldn't save a draft because you don't have a mailbox connected to Sodium with permission to read email. Connect one under Settings > Email in Sodium, or ask a practice admin to give you access, then try again.";
|
|
7100
|
+
const needsReconnect = "I couldn't save a draft because your connected mailbox needs reconnecting. Reconnect it under Settings > Email in Sodium, then try again.";
|
|
7101
|
+
async function handleDraftEmailToClient(api, args) {
|
|
7102
|
+
try {
|
|
7103
|
+
const mailbox = await api.findOwnDraftMailbox();
|
|
7104
|
+
if (!mailbox?.code) return {
|
|
7105
|
+
content: [{
|
|
7106
|
+
type: "text",
|
|
7107
|
+
text: noMailbox
|
|
7108
|
+
}],
|
|
7109
|
+
isError: true
|
|
7110
|
+
};
|
|
7111
|
+
let result;
|
|
7112
|
+
try {
|
|
7113
|
+
result = await api.createMailboxDraft(mailbox.code, {
|
|
7114
|
+
subject: args.subject,
|
|
7115
|
+
htmlBody: args.body,
|
|
7116
|
+
toRecipients: args.to,
|
|
7117
|
+
ccRecipients: args.cc
|
|
7118
|
+
});
|
|
7119
|
+
} catch (error) {
|
|
7120
|
+
if (error instanceof SodiumApiError && error.statusCode === 403) return {
|
|
7121
|
+
content: [{
|
|
7122
|
+
type: "text",
|
|
7123
|
+
text: noMailbox
|
|
7124
|
+
}],
|
|
7125
|
+
isError: true
|
|
7126
|
+
};
|
|
7127
|
+
throw error;
|
|
7128
|
+
}
|
|
7129
|
+
if (result.reconnectRequired) return {
|
|
7130
|
+
content: [{
|
|
7131
|
+
type: "text",
|
|
7132
|
+
text: needsReconnect
|
|
7133
|
+
}],
|
|
7134
|
+
isError: true
|
|
7135
|
+
};
|
|
7136
|
+
if (!result.success) return {
|
|
7137
|
+
content: [{
|
|
7138
|
+
type: "text",
|
|
7139
|
+
text: `The draft could not be saved: ${result.errorMessage ?? "the mail provider refused it"}.`
|
|
7140
|
+
}],
|
|
7141
|
+
isError: true
|
|
7142
|
+
};
|
|
7143
|
+
const who = args.to.map((r) => r.name ?? r.email).join(", ");
|
|
7144
|
+
return { content: [{
|
|
7145
|
+
type: "text",
|
|
7146
|
+
text: `Draft saved in ${mailbox.fromEmail ?? "your mailbox"}, addressed to ${who}. It has not been sent; open your Drafts folder to review and send it.`
|
|
7147
|
+
}] };
|
|
7148
|
+
} catch (error) {
|
|
7149
|
+
return {
|
|
7150
|
+
content: [{
|
|
7151
|
+
type: "text",
|
|
7152
|
+
text: error instanceof SodiumApiError ? `Error saving email draft: ${error.message} (correlation: ${error.correlationId})` : `Error saving email draft: ${error instanceof Error ? error.message : String(error)}`
|
|
7153
|
+
}],
|
|
7154
|
+
isError: true
|
|
7155
|
+
};
|
|
7156
|
+
}
|
|
7157
|
+
}
|
|
7158
|
+
//#endregion
|
|
6824
7159
|
//#region ../mcp-core/src/tools/list-data-form-requests.ts
|
|
6825
7160
|
const statusEnum$11 = z.enum(["PendingResponse", "ResponseReceived"]);
|
|
6826
7161
|
const sortByEnum$10 = z.enum(["CreatedDate", "Status"]);
|
|
@@ -9866,6 +10201,18 @@ async function buildServer(config) {
|
|
|
9866
10201
|
inputSchema: ListDocumentRequestsInputSchema,
|
|
9867
10202
|
annotations: lookupAnnotations
|
|
9868
10203
|
}, (args) => handleListDocumentRequests(api, args));
|
|
10204
|
+
registerTenantTool(server, api, "list_overdue_requests", {
|
|
10205
|
+
title: "List overdue document requests",
|
|
10206
|
+
description: "List document requests that have been sent to clients, are still waiting on them, and are past their deadline, most overdue first. Each row shows days overdue, the client, the deadline and chase history (last chased, next chase). Optional filters: client, manager, partner, task category. Answers 'who do we need to chase?', 'which of Jane's clients are overdue sending records?', 'is ACME overdue on anything?'. Covers document requests only; data form requests have no deadline. Requests without a deadline are never overdue. For every open request regardless of deadline, use list_document_requests.",
|
|
10207
|
+
inputSchema: ListOverdueRequestsInputSchema,
|
|
10208
|
+
annotations: lookupAnnotations
|
|
10209
|
+
}, (args) => handleListOverdueRequests(api, args));
|
|
10210
|
+
registerTenantTool(server, api, "list_upcoming_key_dates", {
|
|
10211
|
+
title: "List clients with a key date coming up",
|
|
10212
|
+
description: "List clients whose chosen key date falls inside a window, soonest first, across the whole practice. Pick the date type (accounts due, confirmation statement due, annual VAT return due, PAYE/CIS filing due, pension re-enrolment, charity returns, year end and others) and a window (Next7Days, Next30Days, ThisMonth, NextQuarter, a custom range and others). Defaults to active clients and the next 30 days. Filters: manager, partner, team, status. Answers 'whose accounts are due next month?', 'which confirmation statements are due in the next fortnight?', 'how many clients have a year end this quarter?' (limit=0). Quarterly and monthly VAT return dates and MTD ITSA quarter dates are calculated per client and cannot be searched here; get_client_summary shows them for one client. To change a date, use update_client_dates.",
|
|
10213
|
+
inputSchema: ListUpcomingKeyDatesInputSchema,
|
|
10214
|
+
annotations: lookupAnnotations
|
|
10215
|
+
}, (args) => handleListUpcomingKeyDates(api, args));
|
|
9869
10216
|
registerTenantTool(server, api, "list_data_form_requests", {
|
|
9870
10217
|
title: "List / filter data form (questionnaire) requests",
|
|
9871
10218
|
description: "List data-form (questionnaire) requests sent to clients — e.g. Self Assessment questionnaires, onboarding forms — with status, client, form, category, manager/partner, search, sort, and pagination. Each row shows status, sent/submitted dates, answers awaiting review, and chase history. Answers 'who still hasn't returned the SA questionnaire?' (formCode + status=PendingResponse), 'which form responses need reviewing?' (status=ResponseReceived — check pendingReviewCount per row), 'what forms has ACME been sent?' (clientCode). Statuses: PendingResponse (waiting on client) / ResponseReceived (submitted; answers may still need review).",
|
|
@@ -10237,6 +10584,17 @@ async function buildServer(config) {
|
|
|
10237
10584
|
openWorldHint: true
|
|
10238
10585
|
}
|
|
10239
10586
|
}, (args) => handleAddClientNote(api, args));
|
|
10587
|
+
registerWriteTool(server, api, config.context, "draft_email_to_client", {
|
|
10588
|
+
title: "Draft an email to a client",
|
|
10589
|
+
description: "Save a draft email in the signed-in user's own connected mailbox (their default mailbox if they have several). Takes recipients (email, optional name), an optional subject, an HTML body and optional CC. The draft is never sent: the user reviews and sends it from their Drafts folder. Needs the user to have a mailbox connected to Sodium with permission to read email; otherwise it says so. Recipient addresses for a client come from get_client_summary or list_contacts. Attachments are not supported.",
|
|
10590
|
+
inputSchema: DraftEmailToClientInputSchema,
|
|
10591
|
+
annotations: {
|
|
10592
|
+
readOnlyHint: false,
|
|
10593
|
+
destructiveHint: false,
|
|
10594
|
+
idempotentHint: false,
|
|
10595
|
+
openWorldHint: true
|
|
10596
|
+
}
|
|
10597
|
+
}, (args) => handleDraftEmailToClient(api, args));
|
|
10240
10598
|
registerWriteTool(server, api, config.context, "update_client_custom_fields", {
|
|
10241
10599
|
title: "Update custom field values on a client",
|
|
10242
10600
|
description: "Set or clear custom field values on a client. Accepts a map of field codes to values. Only fields included in the map are updated — omitted fields keep their current values. Pass null to clear a field. Field codes and current values are visible in the Custom Fields section of get_client_summary. Values are validated against the field's data type (Text, Number, Date, Boolean, Select, MultiSelect) before sending — invalid values return a validation error with guidance. Use this when the user says things like 'set the referral source on ACME to Google', 'update the fee review date for Smith & Co', 'clear the VAT scheme field on Greggs'. Multiple fields can be updated in a single call.",
|
|
@@ -10284,4 +10642,284 @@ async function buildServer(config) {
|
|
|
10284
10642
|
return server;
|
|
10285
10643
|
}
|
|
10286
10644
|
//#endregion
|
|
10287
|
-
|
|
10645
|
+
//#region ../mcp-core/src/tool-groups.json
|
|
10646
|
+
var groups = [
|
|
10647
|
+
{
|
|
10648
|
+
"name": "Clients & contacts",
|
|
10649
|
+
"tools": [
|
|
10650
|
+
"list_clients",
|
|
10651
|
+
"get_client_summary",
|
|
10652
|
+
"list_client_notes",
|
|
10653
|
+
"list_client_links",
|
|
10654
|
+
"list_contacts",
|
|
10655
|
+
"get_contact",
|
|
10656
|
+
"get_custom_field_details",
|
|
10657
|
+
"list_portal_users",
|
|
10658
|
+
"create_client",
|
|
10659
|
+
"create_contact",
|
|
10660
|
+
"update_contact",
|
|
10661
|
+
"link_contact_to_client",
|
|
10662
|
+
"update_client_business_details",
|
|
10663
|
+
"update_client_dates",
|
|
10664
|
+
"list_upcoming_key_dates",
|
|
10665
|
+
"update_client_custom_fields",
|
|
10666
|
+
"add_client_note"
|
|
10667
|
+
]
|
|
10668
|
+
},
|
|
10669
|
+
{
|
|
10670
|
+
"name": "Tasks & workflows",
|
|
10671
|
+
"tools": [
|
|
10672
|
+
"list_tasks",
|
|
10673
|
+
"get_task_context",
|
|
10674
|
+
"get_task_history",
|
|
10675
|
+
"list_recurring_tasks",
|
|
10676
|
+
"list_workflows",
|
|
10677
|
+
"get_workflow",
|
|
10678
|
+
"list_workflow_occurrences",
|
|
10679
|
+
"list_pending_workflow_steps",
|
|
10680
|
+
"create_task",
|
|
10681
|
+
"update_task",
|
|
10682
|
+
"add_task_note",
|
|
10683
|
+
"update_workflow_step",
|
|
10684
|
+
"add_workflow_step_note"
|
|
10685
|
+
]
|
|
10686
|
+
},
|
|
10687
|
+
{
|
|
10688
|
+
"name": "Services & pricing",
|
|
10689
|
+
"tools": [
|
|
10690
|
+
"list_services",
|
|
10691
|
+
"get_service_details",
|
|
10692
|
+
"list_service_packages",
|
|
10693
|
+
"list_service_categories",
|
|
10694
|
+
"add_client_service",
|
|
10695
|
+
"apply_service_package",
|
|
10696
|
+
"update_client_service_stage",
|
|
10697
|
+
"create_service_category",
|
|
10698
|
+
"update_service_category"
|
|
10699
|
+
]
|
|
10700
|
+
},
|
|
10701
|
+
{
|
|
10702
|
+
"name": "Proposals & engagements",
|
|
10703
|
+
"tools": [
|
|
10704
|
+
"list_proposals",
|
|
10705
|
+
"get_proposal_summary",
|
|
10706
|
+
"list_engagement_letters",
|
|
10707
|
+
"get_engagement_letter_summary",
|
|
10708
|
+
"get_engagement_pipeline_summary",
|
|
10709
|
+
"create_proposal"
|
|
10710
|
+
]
|
|
10711
|
+
},
|
|
10712
|
+
{
|
|
10713
|
+
"name": "Documents & forms",
|
|
10714
|
+
"tools": [
|
|
10715
|
+
"list_document_requests",
|
|
10716
|
+
"list_overdue_requests",
|
|
10717
|
+
"create_document_request",
|
|
10718
|
+
"list_client_documents",
|
|
10719
|
+
"list_data_forms",
|
|
10720
|
+
"list_data_form_requests",
|
|
10721
|
+
"send_data_form"
|
|
10722
|
+
]
|
|
10723
|
+
},
|
|
10724
|
+
{
|
|
10725
|
+
"name": "Time & billing",
|
|
10726
|
+
"tools": [
|
|
10727
|
+
"list_time_entries",
|
|
10728
|
+
"log_time_entry",
|
|
10729
|
+
"list_unbilled_time_clients",
|
|
10730
|
+
"list_billing_line_items",
|
|
10731
|
+
"list_invoice_submissions",
|
|
10732
|
+
"get_service_time_report",
|
|
10733
|
+
"get_team_time_report"
|
|
10734
|
+
]
|
|
10735
|
+
},
|
|
10736
|
+
{
|
|
10737
|
+
"name": "Email & Communication",
|
|
10738
|
+
"tools": [
|
|
10739
|
+
"list_client_emails",
|
|
10740
|
+
"draft_email_to_client",
|
|
10741
|
+
"get_client_email_thread",
|
|
10742
|
+
"list_sent_emails",
|
|
10743
|
+
"get_sent_email",
|
|
10744
|
+
"list_email_broadcasts",
|
|
10745
|
+
"get_email_broadcast"
|
|
10746
|
+
]
|
|
10747
|
+
},
|
|
10748
|
+
{
|
|
10749
|
+
"name": "Deadlines & reports",
|
|
10750
|
+
"tools": [
|
|
10751
|
+
"get_practice_dashboard",
|
|
10752
|
+
"get_deadline_compliance_report",
|
|
10753
|
+
"get_service_delivery_report",
|
|
10754
|
+
"get_service_delivery_steps",
|
|
10755
|
+
"get_client_responsiveness_report",
|
|
10756
|
+
"get_team_workload_report",
|
|
10757
|
+
"get_wip_ageing_report",
|
|
10758
|
+
"get_workflow_step_analysis"
|
|
10759
|
+
]
|
|
10760
|
+
},
|
|
10761
|
+
{
|
|
10762
|
+
"name": "Compliance & lookups",
|
|
10763
|
+
"tools": [
|
|
10764
|
+
"list_aml_clients",
|
|
10765
|
+
"get_aml_client",
|
|
10766
|
+
"get_client_agent_authorisations",
|
|
10767
|
+
"get_onboarding_status",
|
|
10768
|
+
"search_companies_house",
|
|
10769
|
+
"get_companies_house_profile"
|
|
10770
|
+
]
|
|
10771
|
+
},
|
|
10772
|
+
{
|
|
10773
|
+
"name": "Practice & setup",
|
|
10774
|
+
"tools": [
|
|
10775
|
+
"get_practice_details",
|
|
10776
|
+
"whoami",
|
|
10777
|
+
"list_practices",
|
|
10778
|
+
"get_usage_examples",
|
|
10779
|
+
"list_users",
|
|
10780
|
+
"list_teams",
|
|
10781
|
+
"list_task_categories",
|
|
10782
|
+
"list_saved_task_filters",
|
|
10783
|
+
"list_saved_client_filters",
|
|
10784
|
+
"create_saved_task_filter"
|
|
10785
|
+
]
|
|
10786
|
+
}
|
|
10787
|
+
];
|
|
10788
|
+
var summaries = {
|
|
10789
|
+
"list_clients": "Find and filter your clients by name, status, type, manager, service and more.",
|
|
10790
|
+
"get_client_summary": "A full overview of one client: details, contacts, services, key dates and tasks.",
|
|
10791
|
+
"list_client_notes": "Read the notes on a client.",
|
|
10792
|
+
"list_client_links": "See the links saved against a client, such as a website or shared folder.",
|
|
10793
|
+
"list_contacts": "Search contacts across the practice, or list a client's contacts.",
|
|
10794
|
+
"get_contact": "Full details of one contact, including linked clients.",
|
|
10795
|
+
"get_custom_field_details": "The definition of a custom field, including its allowed options.",
|
|
10796
|
+
"list_portal_users": "See who has client portal access and when they last logged in.",
|
|
10797
|
+
"create_client": "Create a new client or prospect.",
|
|
10798
|
+
"create_contact": "Create a new contact.",
|
|
10799
|
+
"update_contact": "Update a contact's details.",
|
|
10800
|
+
"link_contact_to_client": "Link an existing contact to a client and set their role.",
|
|
10801
|
+
"update_client_business_details": "Update a client's trading name, address, VAT and tax details.",
|
|
10802
|
+
"update_client_dates": "Set a client's key dates, such as year end and VAT or filing due dates.",
|
|
10803
|
+
"update_client_custom_fields": "Update a client's custom field values.",
|
|
10804
|
+
"add_client_note": "Add a note to a client.",
|
|
10805
|
+
"list_tasks": "Find tasks by assignee, client, status, due date, category and more.",
|
|
10806
|
+
"get_task_context": "Everything about one task: details, notes, workflow progress and client.",
|
|
10807
|
+
"get_task_history": "The audit trail of a task: who changed what, and when.",
|
|
10808
|
+
"list_recurring_tasks": "See the recurring task templates that generate repeating work.",
|
|
10809
|
+
"list_workflows": "List your workflow templates.",
|
|
10810
|
+
"get_workflow": "The groups and steps of one workflow template.",
|
|
10811
|
+
"list_workflow_occurrences": "The completed runs of a workflow, slowest first.",
|
|
10812
|
+
"list_pending_workflow_steps": "Workflow steps ready to be worked on now, for you, your team or nobody.",
|
|
10813
|
+
"create_task": "Create a task for a client or the practice.",
|
|
10814
|
+
"update_task": "Update a task's status, dates, assignee or details.",
|
|
10815
|
+
"add_task_note": "Add a note to a task.",
|
|
10816
|
+
"update_workflow_step": "Complete, skip, block or reassign a workflow step.",
|
|
10817
|
+
"add_workflow_step_note": "Add a note to one step of a task's workflow.",
|
|
10818
|
+
"list_services": "List the services your practice offers.",
|
|
10819
|
+
"get_service_details": "A service's pricing, client types, stages and authorisations.",
|
|
10820
|
+
"list_service_packages": "List bundled service packages and what is in them.",
|
|
10821
|
+
"list_service_categories": "List your service categories.",
|
|
10822
|
+
"add_client_service": "Put a service on a client, with frequency, price and start date.",
|
|
10823
|
+
"apply_service_package": "Apply a service package to a client in one go.",
|
|
10824
|
+
"update_client_service_stage": "Move a client's service to another stage on the board.",
|
|
10825
|
+
"create_service_category": "Create a service category.",
|
|
10826
|
+
"update_service_category": "Rename, recolour or reorder a service category.",
|
|
10827
|
+
"list_proposals": "List proposals by status, client or date.",
|
|
10828
|
+
"get_proposal_summary": "Full detail of one proposal: services, pricing and acceptance.",
|
|
10829
|
+
"list_engagement_letters": "List letters of engagement by status, client or date.",
|
|
10830
|
+
"get_engagement_letter_summary": "Full detail of one letter of engagement.",
|
|
10831
|
+
"get_engagement_pipeline_summary": "Counts and values of proposals by stage: what is open and what is won.",
|
|
10832
|
+
"create_proposal": "Draft a proposal or letter of engagement for a client.",
|
|
10833
|
+
"list_upcoming_key_dates": "Find clients with accounts, filings or other key dates coming up.",
|
|
10834
|
+
"list_overdue_requests": "See document requests that are past their deadline and need chasing.",
|
|
10835
|
+
"draft_email_to_client": "Save a draft email in your own mailbox, ready for you to review and send.",
|
|
10836
|
+
"list_document_requests": "See document requests sent to clients and what is still outstanding.",
|
|
10837
|
+
"create_document_request": "Draft a document request for a client.",
|
|
10838
|
+
"list_client_documents": "List documents held against clients and their review status.",
|
|
10839
|
+
"list_data_forms": "List your questionnaire templates.",
|
|
10840
|
+
"list_data_form_requests": "See which questionnaires were sent and who has responded.",
|
|
10841
|
+
"send_data_form": "Send a questionnaire to a client.",
|
|
10842
|
+
"list_time_entries": "List recorded time by person, client, task or date.",
|
|
10843
|
+
"log_time_entry": "Record time spent on a client or task.",
|
|
10844
|
+
"list_unbilled_time_clients": "Which clients have time recorded but not yet billed.",
|
|
10845
|
+
"list_billing_line_items": "List billing lines waiting for approval or invoicing.",
|
|
10846
|
+
"list_invoice_submissions": "See invoices sent to your accounting system, and any that failed.",
|
|
10847
|
+
"get_service_time_report": "Where your recorded time goes, by service or client.",
|
|
10848
|
+
"get_team_time_report": "Hours logged against expected hours for each team member.",
|
|
10849
|
+
"list_client_emails": "Email threads with a client from your synced mailbox.",
|
|
10850
|
+
"get_client_email_thread": "Read one email thread in full.",
|
|
10851
|
+
"list_sent_emails": "Emails Sodium sent for you, and whether they were delivered.",
|
|
10852
|
+
"get_sent_email": "Delivery detail for one sent email.",
|
|
10853
|
+
"list_email_broadcasts": "Your email campaigns and how far they have got.",
|
|
10854
|
+
"get_email_broadcast": "One campaign in full, with delivery results for each client.",
|
|
10855
|
+
"get_practice_dashboard": "A snapshot of what needs attention across the practice right now.",
|
|
10856
|
+
"get_deadline_compliance_report": "Which deadlines were met, missed or are at risk.",
|
|
10857
|
+
"get_service_delivery_report": "How quickly services are delivered, and how often on time.",
|
|
10858
|
+
"get_service_delivery_steps": "Where the time goes inside one service's workflow.",
|
|
10859
|
+
"get_client_responsiveness_report": "How quickly clients respond to requests, and how much chasing it takes.",
|
|
10860
|
+
"get_team_workload_report": "Who has too much on, and what each person completes.",
|
|
10861
|
+
"get_wip_ageing_report": "Open work that has been sitting untouched, and for how long.",
|
|
10862
|
+
"get_workflow_step_analysis": "Step-by-step timings for one workflow template.",
|
|
10863
|
+
"list_aml_clients": "AML and identity-check records with risk level and outstanding checks.",
|
|
10864
|
+
"get_aml_client": "One AML record: status, linked contacts, checks and risk assessments.",
|
|
10865
|
+
"get_client_agent_authorisations": "HMRC agent authorisation status for a client, by tax regime.",
|
|
10866
|
+
"get_onboarding_status": "Is a client ready for work: engagement, AML, authorisations and data.",
|
|
10867
|
+
"search_companies_house": "Search Companies House by company name.",
|
|
10868
|
+
"get_companies_house_profile": "The live Companies House record for a company.",
|
|
10869
|
+
"get_practice_details": "Your practice's details, counts, connections and settings.",
|
|
10870
|
+
"whoami": "Who you are signed in as, and which practice you are working in.",
|
|
10871
|
+
"list_practices": "The practices you belong to.",
|
|
10872
|
+
"get_usage_examples": "Example requests to try, grouped by topic.",
|
|
10873
|
+
"list_users": "Your team members and their roles.",
|
|
10874
|
+
"list_teams": "Your teams and who is in them.",
|
|
10875
|
+
"list_task_categories": "List your task categories.",
|
|
10876
|
+
"list_saved_task_filters": "Your saved task views.",
|
|
10877
|
+
"list_saved_client_filters": "Your saved client views.",
|
|
10878
|
+
"create_saved_task_filter": "Save a task search as a reusable view."
|
|
10879
|
+
};
|
|
10880
|
+
//#endregion
|
|
10881
|
+
//#region ../mcp-core/src/catalogue.ts
|
|
10882
|
+
const SUMMARY_MAX = 200;
|
|
10883
|
+
const OTHER_GROUP = "Other";
|
|
10884
|
+
const summaryOverrides = summaries;
|
|
10885
|
+
function summarise(tool) {
|
|
10886
|
+
const override = summaryOverrides[tool.name];
|
|
10887
|
+
if (override) return override;
|
|
10888
|
+
const text = (tool.description ?? "").replace(/\s+/g, " ").trim();
|
|
10889
|
+
const match = text.match(/^(.+?[.!?])(?=\s+[A-Z'"(]|$)/);
|
|
10890
|
+
const sentence = match ? match[1] : text;
|
|
10891
|
+
return sentence.length > SUMMARY_MAX ? `${sentence.slice(0, SUMMARY_MAX - 1).trimEnd()}…` : sentence;
|
|
10892
|
+
}
|
|
10893
|
+
function buildCatalogue(tools) {
|
|
10894
|
+
const byName = new Map(tools.map((t) => [t.name, t]));
|
|
10895
|
+
const placed = /* @__PURE__ */ new Set();
|
|
10896
|
+
const groups$1 = [];
|
|
10897
|
+
const toEntry = (tool) => ({
|
|
10898
|
+
name: tool.name,
|
|
10899
|
+
title: tool.title ?? tool.name,
|
|
10900
|
+
access: tool.annotations?.readOnlyHint === true ? "read" : "write",
|
|
10901
|
+
summary: summarise(tool)
|
|
10902
|
+
});
|
|
10903
|
+
for (const group of groups) {
|
|
10904
|
+
const entries = group.tools.map((name) => byName.get(name)).filter((t) => t !== void 0).map(toEntry);
|
|
10905
|
+
entries.forEach((e) => placed.add(e.name));
|
|
10906
|
+
if (entries.length > 0) groups$1.push({
|
|
10907
|
+
name: group.name,
|
|
10908
|
+
tools: entries
|
|
10909
|
+
});
|
|
10910
|
+
}
|
|
10911
|
+
const unmapped = tools.filter((t) => !placed.has(t.name)).map(toEntry);
|
|
10912
|
+
if (unmapped.length > 0) groups$1.push({
|
|
10913
|
+
name: OTHER_GROUP,
|
|
10914
|
+
tools: unmapped
|
|
10915
|
+
});
|
|
10916
|
+
const all = groups$1.flatMap((g) => g.tools);
|
|
10917
|
+
return {
|
|
10918
|
+
toolCount: all.length,
|
|
10919
|
+
readCount: all.filter((t) => t.access === "read").length,
|
|
10920
|
+
writeCount: all.filter((t) => t.access === "write").length,
|
|
10921
|
+
groups: groups$1
|
|
10922
|
+
};
|
|
10923
|
+
}
|
|
10924
|
+
//#endregion
|
|
10925
|
+
export { buildServer as n, buildCatalogue as t };
|