@sodiumhq/mcp-pm 0.1.0-beta.4445 → 0.1.0-beta.4448
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 +1 -2
- package/dist/http.js +18 -62
- package/dist/index.js +1 -1
- package/dist/{src-BRf2g74U.mjs → src-DEbTNFXg.mjs} +143 -143
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,8 +62,7 @@ Only enable write mode with an AI client you trust — it hands the client the a
|
|
|
62
62
|
- **`get_practice_dashboard`** — one-call "state of the practice right now": overdue/blocked/due-soon work, what clients are sitting on, unassigned steps, unbilled time, open proposal pipeline. *"Give me my morning briefing."*
|
|
63
63
|
- **`get_practice_details`** — consolidated practice overview (counts, connections, settings)
|
|
64
64
|
- **`whoami`** — show the authenticated user (name, email, code), tenant (name, code, status), and practice name. Useful for verifying which account is connected.
|
|
65
|
-
- **`list_practices`** — list the practices you belong to
|
|
66
|
-
- **`set_active_practice`** — choose which practice to work in, or switch mid-session. *"Switch to Oceans."* Needed when you belong to more than one practice.
|
|
65
|
+
- **`list_practices`** — list the practices you belong to. *"Which practices can I access?"* If you belong to more than one, tools take an optional `practice` code to say which one a request is about; with a single practice it is selected automatically.
|
|
67
66
|
|
|
68
67
|
**Clients**
|
|
69
68
|
- **`list_clients`** — list and filter clients by search, status, type, assignee, services, saved filters
|
package/dist/http.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as buildServer } from "./src-
|
|
2
|
+
import { t as buildServer } from "./src-DEbTNFXg.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
|
-
import { createHash
|
|
4
|
+
import { createHash } from "node:crypto";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
6
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
7
7
|
import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
|
|
@@ -140,26 +140,6 @@ const WRITES_ENABLED = writesRaw === "true" || writesRaw === "1";
|
|
|
140
140
|
const AUTH = loadAuthConfig();
|
|
141
141
|
const OBO = AUTH ? loadOboConfig(AUTH.issuer) : null;
|
|
142
142
|
const ENDPOINT_PATH = AUTH ? new URL(AUTH.resourceUrl).pathname : process.env.MCP_ENDPOINT_PATH ?? "/mcp";
|
|
143
|
-
const sessions = /* @__PURE__ */ new Map();
|
|
144
|
-
const SESSION_IDLE_MS = Number(process.env.MCP_SESSION_IDLE_MS ?? 30 * 6e4);
|
|
145
|
-
const SWEEP_INTERVAL_MS = 6e4;
|
|
146
|
-
function touch(sessionId) {
|
|
147
|
-
const session = sessions.get(sessionId);
|
|
148
|
-
if (session) session.lastActivity = Date.now();
|
|
149
|
-
}
|
|
150
|
-
async function sweepIdleSessions() {
|
|
151
|
-
const cutoff = Date.now() - SESSION_IDLE_MS;
|
|
152
|
-
for (const [sessionId, session] of sessions) {
|
|
153
|
-
if (session.lastActivity > cutoff) continue;
|
|
154
|
-
sessions.delete(sessionId);
|
|
155
|
-
console.error(`[sodium-pm-mcp-http] session ${sessionId} swept (idle)`);
|
|
156
|
-
try {
|
|
157
|
-
await session.transport.close();
|
|
158
|
-
} catch (error) {
|
|
159
|
-
console.error(`[sodium-pm-mcp-http] error closing swept session: ${String(error)}`);
|
|
160
|
-
}
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
143
|
const MAX_BODY_BYTES = 4 * 1024 * 1024;
|
|
164
144
|
async function readJsonBody(req) {
|
|
165
145
|
const chunks = [];
|
|
@@ -182,7 +162,7 @@ function sendJsonError(res, status, message) {
|
|
|
182
162
|
id: null
|
|
183
163
|
}));
|
|
184
164
|
}
|
|
185
|
-
async function
|
|
165
|
+
async function handleMcpPost(req, res, body) {
|
|
186
166
|
const authHeader = req.headers.authorization;
|
|
187
167
|
if (!authHeader?.startsWith("Bearer ")) throw new HttpError(401, "Authorization: Bearer <token> header is required.");
|
|
188
168
|
const bearerToken = authHeader.slice(7).trim();
|
|
@@ -195,6 +175,7 @@ async function createSession(req) {
|
|
|
195
175
|
}
|
|
196
176
|
const tenantHeader = req.headers["x-sodium-tenant"];
|
|
197
177
|
const tenant = (Array.isArray(tenantHeader) ? tenantHeader[0] : tenantHeader) ?? process.env.SODIUM_TENANT;
|
|
178
|
+
const isInit = isInitializeRequest(body);
|
|
198
179
|
const server = await buildServer({
|
|
199
180
|
context: {
|
|
200
181
|
bearerToken: apiToken,
|
|
@@ -203,26 +184,17 @@ async function createSession(req) {
|
|
|
203
184
|
writesEnabled: writesAllowed
|
|
204
185
|
},
|
|
205
186
|
serverName: "Sodium Practice Management",
|
|
206
|
-
serverVersion: VERSION
|
|
187
|
+
serverVersion: VERSION,
|
|
188
|
+
includeStartupContext: isInit
|
|
207
189
|
});
|
|
208
|
-
const transport = new StreamableHTTPServerTransport({
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
transport,
|
|
213
|
-
lastActivity: Date.now()
|
|
214
|
-
});
|
|
215
|
-
console.error(`[sodium-pm-mcp-http] session ${sessionId} started (tenant: ${tenant}, live: ${sessions.size})`);
|
|
216
|
-
}
|
|
190
|
+
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: void 0 });
|
|
191
|
+
res.on("close", () => {
|
|
192
|
+
transport.close();
|
|
193
|
+
server.close();
|
|
217
194
|
});
|
|
218
|
-
|
|
219
|
-
if (transport.sessionId) {
|
|
220
|
-
sessions.delete(transport.sessionId);
|
|
221
|
-
console.error(`[sodium-pm-mcp-http] session ${transport.sessionId} closed`);
|
|
222
|
-
}
|
|
223
|
-
};
|
|
195
|
+
if (isInit) console.error(`[sodium-pm-mcp-http] initialize (tenant: ${tenant || "per-call"}, writes: ${writesAllowed})`);
|
|
224
196
|
await server.connect(transport);
|
|
225
|
-
|
|
197
|
+
await transport.handleRequest(req, res, body);
|
|
226
198
|
}
|
|
227
199
|
var HttpError = class extends Error {
|
|
228
200
|
constructor(status, message) {
|
|
@@ -235,8 +207,7 @@ async function handle(req, res) {
|
|
|
235
207
|
if (req.method === "GET" && url.pathname === "/health") {
|
|
236
208
|
res.writeHead(200, { "Content-Type": "application/json" }).end(JSON.stringify({
|
|
237
209
|
status: "ok",
|
|
238
|
-
version: VERSION
|
|
239
|
-
sessions: sessions.size
|
|
210
|
+
version: VERSION
|
|
240
211
|
}));
|
|
241
212
|
return;
|
|
242
213
|
}
|
|
@@ -248,25 +219,14 @@ async function handle(req, res) {
|
|
|
248
219
|
sendJsonError(res, 404, `Not found. MCP endpoint is ${ENDPOINT_PATH}.`);
|
|
249
220
|
return;
|
|
250
221
|
}
|
|
251
|
-
const sessionId = req.headers["mcp-session-id"];
|
|
252
|
-
const existing = typeof sessionId === "string" ? sessions.get(sessionId) : void 0;
|
|
253
|
-
if (existing && typeof sessionId === "string") {
|
|
254
|
-
touch(sessionId);
|
|
255
|
-
await existing.transport.handleRequest(req, res);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
222
|
if (req.method !== "POST") {
|
|
259
|
-
|
|
223
|
+
res.setHeader("Allow", "POST");
|
|
224
|
+
sendJsonError(res, 405, "Stateless MCP endpoint. Send requests as POST.");
|
|
260
225
|
return;
|
|
261
226
|
}
|
|
262
|
-
|
|
263
|
-
if (!isInitializeRequest(body)) {
|
|
264
|
-
sendJsonError(res, 400, "No valid MCP session. Send an initialize request first.");
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
await (await createSession(req)).handleRequest(req, res, body);
|
|
227
|
+
await handleMcpPost(req, res, await readJsonBody(req));
|
|
268
228
|
}
|
|
269
|
-
|
|
229
|
+
createServer((req, res) => {
|
|
270
230
|
handle(req, res).catch((error) => {
|
|
271
231
|
const message = error instanceof Error ? error.message : String(error);
|
|
272
232
|
console.error(`[sodium-pm-mcp-http] ${req.method} ${req.url} failed: ${message}`);
|
|
@@ -280,11 +240,7 @@ const httpServer = createServer((req, res) => {
|
|
|
280
240
|
if (AUTH && error instanceof HttpError && error.status === 401) res.setHeader("WWW-Authenticate", buildChallenge(AUTH));
|
|
281
241
|
sendJsonError(res, error instanceof HttpError ? error.status : 500, message);
|
|
282
242
|
});
|
|
283
|
-
})
|
|
284
|
-
setInterval(() => {
|
|
285
|
-
sweepIdleSessions();
|
|
286
|
-
}, SWEEP_INTERVAL_MS).unref();
|
|
287
|
-
httpServer.listen(PORT, HOST, () => {
|
|
243
|
+
}).listen(PORT, HOST, () => {
|
|
288
244
|
console.error(`[sodium-pm-mcp-http] v${VERSION} listening on http://${HOST}:${PORT}${ENDPOINT_PATH} (api: ${BASE_URL}, writes: ${WRITES_ENABLED ? "enabled" : "disabled"})`);
|
|
289
245
|
});
|
|
290
246
|
//#endregion
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { t as buildServer } from "./src-
|
|
2
|
+
import { t as buildServer } from "./src-DEbTNFXg.mjs";
|
|
3
3
|
import { createRequire } from "node:module";
|
|
4
4
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
5
5
|
import { parseArgs } from "node:util";
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
3
2
|
import { z } from "zod";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
4
|
//#region ../mcp-core/src/generated/core/bodySerializer.gen.ts
|
|
5
5
|
const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
|
|
6
6
|
Object.entries({
|
|
@@ -2233,7 +2233,7 @@ var SodiumApiClient = class {
|
|
|
2233
2233
|
}));
|
|
2234
2234
|
}
|
|
2235
2235
|
get tenant() {
|
|
2236
|
-
if (!this.activeTenant) throw new Error("No practice
|
|
2236
|
+
if (!this.activeTenant) throw new Error("No practice specified. Pass a practice code from list_practices in the tool's 'practice' argument.");
|
|
2237
2237
|
return this.activeTenant;
|
|
2238
2238
|
}
|
|
2239
2239
|
hasActiveTenant() {
|
|
@@ -3576,13 +3576,13 @@ async function buildInstructions(api, writesEnabled, selection) {
|
|
|
3576
3576
|
if (tenant.status === "fulfilled") lines.push(`Tenant: ${tenant.value.name} (${tenant.value.code})`);
|
|
3577
3577
|
if (practice.status === "fulfilled") lines.push(`Practice: ${practice.value.name}`);
|
|
3578
3578
|
if (selection.kind === "choice") {
|
|
3579
|
-
lines.push("", "PRACTICE SELECTION
|
|
3579
|
+
lines.push("", "PRACTICE SELECTION — this user belongs to more than one practice.", "Data tools take an optional 'practice' argument (a practice code). Ask the user which practice to work in, then pass its code on EVERY tool call for the rest of the conversation. Calls without it are refused with a reminder.");
|
|
3580
3580
|
if (selection.practices.length > 0) {
|
|
3581
3581
|
lines.push("Practices available:");
|
|
3582
3582
|
for (const p of selection.practices) lines.push(`- ${p.name} (${p.code})`);
|
|
3583
3583
|
} else lines.push("Call list_practices to see the available practices.");
|
|
3584
3584
|
} else if (selection.kind === "none") lines.push("", "This user does not belong to any usable practice. Tell them their Sodium account has no active practice and there is nothing to show.");
|
|
3585
|
-
else if (selection.kind === "auto") lines.push("", `Working in ${selection.practice.name} (${selection.practice.code}) — the only practice this user belongs to.
|
|
3585
|
+
else if (selection.kind === "auto") lines.push("", `Working in ${selection.practice.name} (${selection.practice.code}) — the only practice this user belongs to. It is selected automatically; no 'practice' argument is needed.`);
|
|
3586
3586
|
lines.push("", writesEnabled ? "Write mode: ENABLED. Create/update tools are available; destructive and bulk operations are not." : "Write mode: DISABLED. Read-only — tell the user to relaunch with --enable-writes if they want changes made.");
|
|
3587
3587
|
if (team.status === "fulfilled") {
|
|
3588
3588
|
const members = team.value.data ?? [];
|
|
@@ -3629,12 +3629,34 @@ async function resolveTenant(api) {
|
|
|
3629
3629
|
practices: usable
|
|
3630
3630
|
};
|
|
3631
3631
|
}
|
|
3632
|
+
async function resolveTenantForCall(api, practice) {
|
|
3633
|
+
const explicit = practice?.trim();
|
|
3634
|
+
if (explicit) {
|
|
3635
|
+
api.setActiveTenant(explicit);
|
|
3636
|
+
return { ok: true };
|
|
3637
|
+
}
|
|
3638
|
+
if (api.hasActiveTenant()) return { ok: true };
|
|
3639
|
+
const selection = await resolveTenant(api);
|
|
3640
|
+
if (selection.kind === "auto" || selection.kind === "explicit") return { ok: true };
|
|
3641
|
+
if (selection.kind === "none") return {
|
|
3642
|
+
ok: false,
|
|
3643
|
+
message: "This user does not belong to any usable practice, so there is no practice data to work with."
|
|
3644
|
+
};
|
|
3645
|
+
if (selection.practices.length === 0) return {
|
|
3646
|
+
ok: false,
|
|
3647
|
+
message: "Could not determine which practice to work in. Call list_practices, ask the user to choose, then retry with the chosen code in the 'practice' argument."
|
|
3648
|
+
};
|
|
3649
|
+
return {
|
|
3650
|
+
ok: false,
|
|
3651
|
+
message: `This user belongs to more than one practice: ${selection.practices.map((p) => `${p.name} (${p.code})`).join(", ")}. Ask which practice to work in, then retry with the chosen code in the 'practice' argument.`
|
|
3652
|
+
};
|
|
3653
|
+
}
|
|
3632
3654
|
//#endregion
|
|
3633
3655
|
//#region ../mcp-core/src/tools/list-practices.ts
|
|
3634
3656
|
async function handleListPractices(api) {
|
|
3635
3657
|
try {
|
|
3636
3658
|
const memberships = await api.listTenants();
|
|
3637
|
-
const
|
|
3659
|
+
const pinned = api.getActiveTenant();
|
|
3638
3660
|
if (memberships.length === 0) return { content: [{
|
|
3639
3661
|
type: "text",
|
|
3640
3662
|
text: "You do not belong to any practices."
|
|
@@ -3642,10 +3664,10 @@ async function handleListPractices(api) {
|
|
|
3642
3664
|
return { content: [{
|
|
3643
3665
|
type: "text",
|
|
3644
3666
|
text: `Practices you belong to:\n${memberships.map((t) => {
|
|
3645
|
-
const flags = [t.code ===
|
|
3667
|
+
const flags = [t.code === pinned ? "default" : null, isUsablePractice(t) ? null : "unavailable"].filter(Boolean);
|
|
3646
3668
|
const suffix = flags.length > 0 ? ` — ${flags.join(", ")}` : "";
|
|
3647
3669
|
return `- ${t.name} (${t.code})${suffix}`;
|
|
3648
|
-
}).join("\n")}\n\
|
|
3670
|
+
}).join("\n")}\n\nPass a code in the 'practice' argument of other tools to work in that practice.`
|
|
3649
3671
|
}] };
|
|
3650
3672
|
} catch (error) {
|
|
3651
3673
|
return {
|
|
@@ -3658,45 +3680,6 @@ async function handleListPractices(api) {
|
|
|
3658
3680
|
}
|
|
3659
3681
|
}
|
|
3660
3682
|
//#endregion
|
|
3661
|
-
//#region ../mcp-core/src/tools/set-active-practice.ts
|
|
3662
|
-
const SetActivePracticeInputSchema = { code: z.string().describe("The practice code to work in, from list_practices (e.g. 'oceans').") };
|
|
3663
|
-
async function handleSetActivePractice(api, args) {
|
|
3664
|
-
try {
|
|
3665
|
-
const memberships = await api.listTenants();
|
|
3666
|
-
const match = memberships.find((t) => t.code === args.code);
|
|
3667
|
-
if (!match) {
|
|
3668
|
-
const known = memberships.map((t) => t.code).join(", ") || "(none)";
|
|
3669
|
-
return {
|
|
3670
|
-
content: [{
|
|
3671
|
-
type: "text",
|
|
3672
|
-
text: `'${args.code}' is not a practice you belong to. Your practices: ${known}.`
|
|
3673
|
-
}],
|
|
3674
|
-
isError: true
|
|
3675
|
-
};
|
|
3676
|
-
}
|
|
3677
|
-
if (!isUsablePractice(match)) return {
|
|
3678
|
-
content: [{
|
|
3679
|
-
type: "text",
|
|
3680
|
-
text: `'${match.name}' (${match.code}) is not currently available and cannot be selected.`
|
|
3681
|
-
}],
|
|
3682
|
-
isError: true
|
|
3683
|
-
};
|
|
3684
|
-
api.setActiveTenant(match.code);
|
|
3685
|
-
return { content: [{
|
|
3686
|
-
type: "text",
|
|
3687
|
-
text: `Now working in ${match.name} (${match.code}).`
|
|
3688
|
-
}] };
|
|
3689
|
-
} catch (error) {
|
|
3690
|
-
return {
|
|
3691
|
-
content: [{
|
|
3692
|
-
type: "text",
|
|
3693
|
-
text: `Could not switch practice: ${error instanceof Error ? error.message : String(error)}`
|
|
3694
|
-
}],
|
|
3695
|
-
isError: true
|
|
3696
|
-
};
|
|
3697
|
-
}
|
|
3698
|
-
}
|
|
3699
|
-
//#endregion
|
|
3700
3683
|
//#region ../mcp-core/src/tools/get-practice-details.ts
|
|
3701
3684
|
function format$4(tenant, practice) {
|
|
3702
3685
|
const lines = [];
|
|
@@ -8759,14 +8742,39 @@ async function handleCreateSavedTaskFilter(api, args) {
|
|
|
8759
8742
|
}
|
|
8760
8743
|
//#endregion
|
|
8761
8744
|
//#region ../mcp-core/src/server.ts
|
|
8762
|
-
|
|
8745
|
+
const practiceField = z.string().optional().describe("Practice code to run this call against, from list_practices. Needed only when the user belongs to more than one practice; omit when they have exactly one — it is selected automatically.");
|
|
8746
|
+
function registerTenantTool(server, api, name, config, cb) {
|
|
8747
|
+
const inputSchema = {
|
|
8748
|
+
...config.inputSchema ?? {},
|
|
8749
|
+
practice: practiceField
|
|
8750
|
+
};
|
|
8751
|
+
server.registerTool(name, {
|
|
8752
|
+
...config,
|
|
8753
|
+
inputSchema
|
|
8754
|
+
}, async (args, extra) => {
|
|
8755
|
+
const { practice, ...rest } = args;
|
|
8756
|
+
const resolution = await resolveTenantForCall(api, typeof practice === "string" ? practice : void 0);
|
|
8757
|
+
if (!resolution.ok) return {
|
|
8758
|
+
content: [{
|
|
8759
|
+
type: "text",
|
|
8760
|
+
text: resolution.message
|
|
8761
|
+
}],
|
|
8762
|
+
isError: true
|
|
8763
|
+
};
|
|
8764
|
+
return cb(rest, extra);
|
|
8765
|
+
});
|
|
8766
|
+
}
|
|
8767
|
+
function registerWriteTool(server, api, ctx, name, config, cb) {
|
|
8763
8768
|
if (!ctx.writesEnabled) return;
|
|
8764
|
-
server
|
|
8769
|
+
registerTenantTool(server, api, name, config, cb);
|
|
8765
8770
|
}
|
|
8766
8771
|
async function buildServer(config) {
|
|
8767
8772
|
const api = new SodiumApiClient(config.context, { serverVersion: config.serverVersion });
|
|
8768
|
-
|
|
8769
|
-
|
|
8773
|
+
let instructions;
|
|
8774
|
+
if (config.includeStartupContext !== false) {
|
|
8775
|
+
const selection = api.hasActiveTenant() ? { kind: "explicit" } : await resolveTenant(api);
|
|
8776
|
+
instructions = await buildInstructions(api, config.context.writesEnabled, selection);
|
|
8777
|
+
}
|
|
8770
8778
|
const server = new McpServer({
|
|
8771
8779
|
name: config.serverName,
|
|
8772
8780
|
version: config.serverVersion,
|
|
@@ -8790,7 +8798,7 @@ async function buildServer(config) {
|
|
|
8790
8798
|
version: info.version
|
|
8791
8799
|
});
|
|
8792
8800
|
};
|
|
8793
|
-
server
|
|
8801
|
+
registerTenantTool(server, api, "get_practice_details", {
|
|
8794
8802
|
title: "Get practice details",
|
|
8795
8803
|
description: "Get a consolidated overview of the practice including name, contact details, client/service/user counts, connections, and settings. Use this when the user asks about their practice, tenant, or wants a summary of their account.",
|
|
8796
8804
|
inputSchema: {},
|
|
@@ -8802,17 +8810,20 @@ async function buildServer(config) {
|
|
|
8802
8810
|
}, () => handleGetPracticeDetails(api));
|
|
8803
8811
|
server.registerTool("whoami", {
|
|
8804
8812
|
title: "Show current user and tenant info",
|
|
8805
|
-
description: "Returns the identity of the authenticated
|
|
8806
|
-
inputSchema: {},
|
|
8813
|
+
description: "Returns the identity of the authenticated user (name, email, code), the tenant/practice the call runs against (name, code, status), and the practice name. Use this when the user asks 'who am I?', 'which tenant am I connected to?', 'what account is this?', or when debugging authentication/permission issues. The optional 'practice' argument picks the practice section shown; without it the user's lone practice is used, and the identity section still answers when no practice can be resolved.",
|
|
8814
|
+
inputSchema: { practice: practiceField },
|
|
8807
8815
|
annotations: {
|
|
8808
8816
|
readOnlyHint: true,
|
|
8809
8817
|
idempotentHint: true,
|
|
8810
8818
|
openWorldHint: true
|
|
8811
8819
|
}
|
|
8812
|
-
}, () =>
|
|
8820
|
+
}, async (args) => {
|
|
8821
|
+
await resolveTenantForCall(api, args.practice);
|
|
8822
|
+
return handleWhoami(api);
|
|
8823
|
+
});
|
|
8813
8824
|
server.registerTool("list_practices", {
|
|
8814
8825
|
title: "List the practices you can work in",
|
|
8815
|
-
description: "List the practices (tenants) the signed-in user belongs to
|
|
8826
|
+
description: "List the practices (tenants) the signed-in user belongs to. Use this when the user belongs to more than one practice and you need the codes to pass in other tools' 'practice' argument, or when the user asks 'which practices can I access?'. Practices flagged 'unavailable' cannot be worked in.",
|
|
8816
8827
|
inputSchema: {},
|
|
8817
8828
|
annotations: {
|
|
8818
8829
|
readOnlyHint: true,
|
|
@@ -8820,18 +8831,7 @@ async function buildServer(config) {
|
|
|
8820
8831
|
openWorldHint: true
|
|
8821
8832
|
}
|
|
8822
8833
|
}, () => handleListPractices(api));
|
|
8823
|
-
server
|
|
8824
|
-
title: "Choose which practice to work in",
|
|
8825
|
-
description: "Set the practice all subsequent tools operate on for this session. Required before other tools can run if the user belongs to more than one practice and none is active yet. Also use it to switch practice mid-session ('switch to Oceans', 'now look at Greggs'). Pass a practice code from list_practices. Only practices the user belongs to and that are currently available can be selected.",
|
|
8826
|
-
inputSchema: SetActivePracticeInputSchema,
|
|
8827
|
-
annotations: {
|
|
8828
|
-
readOnlyHint: false,
|
|
8829
|
-
destructiveHint: false,
|
|
8830
|
-
idempotentHint: true,
|
|
8831
|
-
openWorldHint: true
|
|
8832
|
-
}
|
|
8833
|
-
}, (args) => handleSetActivePractice(api, args));
|
|
8834
|
-
server.registerTool("list_clients", {
|
|
8834
|
+
registerTenantTool(server, api, "list_clients", {
|
|
8835
8835
|
title: "List / search / filter clients",
|
|
8836
8836
|
description: "List clients with any combination of: search (code/name/internal reference, 3+ chars), status (Active/Inactive/Prospect/LostProspect), type (PrivateLimitedCompany/PublicLimitedCompany/LimitedLiabilityPartnership/Partnership/Individual/Trust/Charity/SoleTrader), manager/partner/associate user codes, service codes, a saved filter code, sort, and pagination. Use search for 'find ACME'-style queries. Use type for 'list limited companies' (pass PrivateLimitedCompany + PublicLimitedCompany). Use status: ['Active'] to exclude prospects/inactive. Returns up to 50 clients per page — paginate via offset for more. For 'how many X?' questions, pass limit=0 to get just the total count without fetching any client data. Follow up with get_client_summary for full detail on a specific client.",
|
|
8837
8837
|
inputSchema: ListClientsInputSchema,
|
|
@@ -8841,7 +8841,7 @@ async function buildServer(config) {
|
|
|
8841
8841
|
openWorldHint: true
|
|
8842
8842
|
}
|
|
8843
8843
|
}, (args) => handleListClients(api, args));
|
|
8844
|
-
server
|
|
8844
|
+
registerTenantTool(server, api, "get_client_summary", {
|
|
8845
8845
|
title: "Get a full summary of one client",
|
|
8846
8846
|
description: "Get a consolidated overview of a single client by code: identity (name, status, type, assignments), business details (company number, incorporation date, trading name, registered address, VAT status, UTR, PAYE ref), custom fields (all user-defined fields with current values, data types, and field codes — use these codes with update_client_custom_fields), key statutory dates (year-end, accounts due, VAT return due, confirmation statement due, etc. — upcoming first, then past), all contacts, active services with pricing, overdue task count + top 5, and tasks due in the next 7 days. Use this AFTER list_clients identifies the client of interest, or when the user references a specific client by code. Also answers 'when is ACME's year-end?' / 'is ACME VAT registered?' / 'what are ACME's custom fields?' in a single call. Tolerates partial failures — if one section can't be loaded, the rest is still returned with a note about what's missing.",
|
|
8847
8847
|
inputSchema: GetClientSummaryInputSchema,
|
|
@@ -8851,7 +8851,7 @@ async function buildServer(config) {
|
|
|
8851
8851
|
openWorldHint: true
|
|
8852
8852
|
}
|
|
8853
8853
|
}, (args) => handleGetClientSummary(api, args));
|
|
8854
|
-
server
|
|
8854
|
+
registerTenantTool(server, api, "get_custom_field_details", {
|
|
8855
8855
|
title: "Get full details of a custom field definition",
|
|
8856
8856
|
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.",
|
|
8857
8857
|
inputSchema: GetCustomFieldDetailsInputSchema,
|
|
@@ -8861,7 +8861,7 @@ async function buildServer(config) {
|
|
|
8861
8861
|
openWorldHint: true
|
|
8862
8862
|
}
|
|
8863
8863
|
}, (args) => handleGetCustomFieldDetails(api, args));
|
|
8864
|
-
server
|
|
8864
|
+
registerTenantTool(server, api, "list_client_notes", {
|
|
8865
8865
|
title: "List / search notes on a client",
|
|
8866
8866
|
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).",
|
|
8867
8867
|
inputSchema: ListClientNotesInputSchema,
|
|
@@ -8871,7 +8871,7 @@ async function buildServer(config) {
|
|
|
8871
8871
|
openWorldHint: true
|
|
8872
8872
|
}
|
|
8873
8873
|
}, (args) => handleListClientNotes(api, args));
|
|
8874
|
-
server
|
|
8874
|
+
registerTenantTool(server, api, "list_tasks", {
|
|
8875
8875
|
title: "List / filter tasks across the practice",
|
|
8876
8876
|
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.",
|
|
8877
8877
|
inputSchema: ListTasksInputSchema,
|
|
@@ -8881,7 +8881,7 @@ async function buildServer(config) {
|
|
|
8881
8881
|
openWorldHint: true
|
|
8882
8882
|
}
|
|
8883
8883
|
}, (args) => handleListTasks(api, args));
|
|
8884
|
-
server
|
|
8884
|
+
registerTenantTool(server, api, "get_task_context", {
|
|
8885
8885
|
title: "Get full context for a single task",
|
|
8886
8886
|
description: "Get a consolidated view of one task by code: identity (name, code, status, overdue flag, start/due/statutory dates, time estimate, assignee), description, task-level checklist, workflow progress (groups and steps with status, assignee, and completion info — only if the task has a workflow attached), notes (pinned first, then newest first), and the parent client. Use this AFTER list_tasks identifies a task of interest, or when the user references a specific task by code. Tolerates partial failures — if notes or workflow steps can't be loaded, the task details are still returned with a note about what's missing. Only fails outright if the task itself can't be fetched (e.g. unknown code).",
|
|
8887
8887
|
inputSchema: GetTaskContextInputSchema,
|
|
@@ -8891,7 +8891,7 @@ async function buildServer(config) {
|
|
|
8891
8891
|
openWorldHint: true
|
|
8892
8892
|
}
|
|
8893
8893
|
}, (args) => handleGetTaskContext(api, args));
|
|
8894
|
-
server
|
|
8894
|
+
registerTenantTool(server, api, "list_proposals", {
|
|
8895
8895
|
title: "List / filter proposals",
|
|
8896
8896
|
description: "List proposals (pre-acceptance engagement documents) with filters: status (Unsent/Sent/Viewed/Accepted/Rejected), search (engagement code / client name / client code, 3+ chars), preset date range, sort, pagination. Use for 'proposals awaiting acceptance' (status=Sent, or run twice for Sent + Viewed), 'proposals sent to ACME' (search), 'proposals sent this month' (dateRange=ThisMonth). Returns the same underlying data as list_engagement_letters — both aliases back the same Engagement entity; the status filter is what distinguishes a proposal (Unsent/Sent/Viewed/Rejected) from a signed letter of engagement (Accepted). For 'how many X?' questions, pass limit=0 for count-only. Follow up with get_proposal_summary for full detail on one engagement.",
|
|
8897
8897
|
inputSchema: ListEngagementsInputSchema,
|
|
@@ -8901,7 +8901,7 @@ async function buildServer(config) {
|
|
|
8901
8901
|
openWorldHint: true
|
|
8902
8902
|
}
|
|
8903
8903
|
}, (args) => handleListEngagements(api, args));
|
|
8904
|
-
server
|
|
8904
|
+
registerTenantTool(server, api, "list_engagement_letters", {
|
|
8905
8905
|
title: "List / filter letters of engagement",
|
|
8906
8906
|
description: "List letters of engagement (signed/accepted or in-flight engagement documents) with filters: status (Unsent/Sent/Viewed/Accepted/Rejected), search (engagement code / client name / client code, 3+ chars), preset date range, sort, pagination. Use for 'live letters of engagement' or 'signed engagements' (status=Accepted), 'letter of engagement for ACME' (search), 'engagements signed this quarter' (status=Accepted + dateRange=ThisQuarter). Returns the same underlying data as list_proposals — both aliases back the same Engagement entity; the status filter narrows the pipeline stage. For 'how many X?' questions, pass limit=0 for count-only. Follow up with get_engagement_letter_summary for full detail on one engagement.",
|
|
8907
8907
|
inputSchema: ListEngagementsInputSchema,
|
|
@@ -8911,7 +8911,7 @@ async function buildServer(config) {
|
|
|
8911
8911
|
openWorldHint: true
|
|
8912
8912
|
}
|
|
8913
8913
|
}, (args) => handleListEngagements(api, args));
|
|
8914
|
-
server
|
|
8914
|
+
registerTenantTool(server, api, "get_proposal_summary", {
|
|
8915
8915
|
title: "Get full detail of one proposal",
|
|
8916
8916
|
description: "Get a consolidated view of a single proposal by engagement code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off / total), the proposed services with pricing, acceptance record (if accepted), PDF availability (proposal + letter of engagement), email history, and the acceptance link. REQUIRES a concrete engagement code as input — do NOT call this tool unless you already have the code. If the user describes an engagement by attribute — e.g. 'the unsent proposal', 'the proposal for ACME', 'our most recent proposal', 'the rejected one' — you MUST call list_proposals FIRST with the matching filters to look up the code, then call this tool with that code. Returns identical data to get_engagement_letter_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history may be missing with a note); only fails hard if the engagement itself can't be fetched.",
|
|
8917
8917
|
inputSchema: GetEngagementSummaryInputSchema,
|
|
@@ -8921,7 +8921,7 @@ async function buildServer(config) {
|
|
|
8921
8921
|
openWorldHint: true
|
|
8922
8922
|
}
|
|
8923
8923
|
}, (args) => handleGetEngagementSummary(api, args));
|
|
8924
|
-
server
|
|
8924
|
+
registerTenantTool(server, api, "get_engagement_letter_summary", {
|
|
8925
8925
|
title: "Get full detail of one letter of engagement",
|
|
8926
8926
|
description: "Get a consolidated view of a single letter of engagement by code: identity (code, status, type, date), client, recipient, value breakdown (annual / quarterly / monthly / one-off / total), services included with pricing, acceptance record (accepted date, whether accepted via portal or recorded manually), PDF availability (proposal + letter of engagement), email history, and the acceptance link. REQUIRES a concrete engagement code as input — do NOT call this tool unless you already have the code. If the user describes the engagement by attribute — e.g. 'ACME's letter of engagement', 'the most recent signed engagement', 'the live engagement for Smith & Co' — you MUST call list_engagement_letters FIRST with the matching filters to look up the code, then call this tool with that code. Returns identical data to get_proposal_summary — both aliases back the same Engagement entity. Tolerates partial failures (email history may be missing with a note); only fails hard if the engagement itself can't be fetched.",
|
|
8927
8927
|
inputSchema: GetEngagementSummaryInputSchema,
|
|
@@ -8931,7 +8931,7 @@ async function buildServer(config) {
|
|
|
8931
8931
|
openWorldHint: true
|
|
8932
8932
|
}
|
|
8933
8933
|
}, (args) => handleGetEngagementSummary(api, args));
|
|
8934
|
-
server
|
|
8934
|
+
registerTenantTool(server, api, "list_services", {
|
|
8935
8935
|
title: "List / search / filter the practice's service catalogue",
|
|
8936
8936
|
description: "List the practice's configured billable services with any combination of: search (3+ chars over code and name), category (Tax / Payroll / CoreAccounting / CompanySecretarial / Advisory / SoftwareAndTraining / Other — single value), clientType (PrivateLimitedCompany / PublicLimitedCompany / LimitedLiabilityPartnership / Partnership / Individual / Trust / Charity / SoleTrader — single value; matches services configured for that type plus services with no client-type restriction), isArchived (omit for all, false for active only, true for archive), sort (Name / Category / AccountingCode), and pagination. Use for: 'what services do we offer?' (no filter, isArchived=false), 'list our tax services' (category=Tax), 'what do we offer limited companies?' (clientType=PrivateLimitedCompany, isArchived=false), 'how many active services do we have?' (isArchived=false, limit=0 for count-only). Returns up to 50 per page. Follow up with get_service_details for one service's full configuration (client types, pricing options, clearance items, HMRC authorisations).",
|
|
8937
8937
|
inputSchema: ListServicesInputSchema,
|
|
@@ -8941,7 +8941,7 @@ async function buildServer(config) {
|
|
|
8941
8941
|
openWorldHint: true
|
|
8942
8942
|
}
|
|
8943
8943
|
}, (args) => handleListServices(api, args));
|
|
8944
|
-
server
|
|
8944
|
+
registerTenantTool(server, api, "get_service_details", {
|
|
8945
8945
|
title: "Get a single service's full configuration",
|
|
8946
8946
|
description: "Get a consolidated view of one billable service by code: identity (name, code, category, status, VAT rate, accounting code, description, default manager, pricing mode), applicable client types (explicit list, or 'all client types' if unrestricted), HMRC agent authorisations, pricing options (one per billing frequency with base price + override counts), custom pricing tiers (only when pricingMode=CustomTiers), professional clearance request items, and pricing-factor presence (count + factor questions only — exact factor band values are intentionally not dumped; computed pricing per client lives in the proposal tools). Use this AFTER list_services identifies the service, or when the user references a specific service by code. Enables service-audit workflows like 'is Self Assessment correctly restricted to Individual clients?' and 'what clearance items do we request for new bookkeeping clients?'",
|
|
8947
8947
|
inputSchema: GetServiceDetailsInputSchema,
|
|
@@ -8951,7 +8951,7 @@ async function buildServer(config) {
|
|
|
8951
8951
|
openWorldHint: true
|
|
8952
8952
|
}
|
|
8953
8953
|
}, (args) => handleGetServiceDetails(api, args));
|
|
8954
|
-
server
|
|
8954
|
+
registerTenantTool(server, api, "list_users", {
|
|
8955
8955
|
title: "List / search / filter tenant users",
|
|
8956
8956
|
description: "Find tenant users by name, email, role, or status. Use this when the user mentioned in a request isn't present in the startup roster (large teams have more than the top 20 active members shown there), or when filtering is needed beyond name resolution. Typical queries: 'find Jane' (search), 'list all partners' (isPartner=true), 'who's been invited but not joined yet?' (status=Invited), 'how many active users do we have?' (status=Active, limit=0). For 'how many X?' questions, pass limit=0 to get just the total count without fetching any user data.",
|
|
8957
8957
|
inputSchema: ListUsersInputSchema,
|
|
@@ -8961,7 +8961,7 @@ async function buildServer(config) {
|
|
|
8961
8961
|
openWorldHint: true
|
|
8962
8962
|
}
|
|
8963
8963
|
}, (args) => handleListUsers(api, args));
|
|
8964
|
-
server
|
|
8964
|
+
registerTenantTool(server, api, "list_contacts", {
|
|
8965
8965
|
title: "List / search contacts across the practice",
|
|
8966
8966
|
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.",
|
|
8967
8967
|
inputSchema: ListContactsInputSchema,
|
|
@@ -8971,7 +8971,7 @@ async function buildServer(config) {
|
|
|
8971
8971
|
openWorldHint: true
|
|
8972
8972
|
}
|
|
8973
8973
|
}, (args) => handleListContacts(api, args));
|
|
8974
|
-
server
|
|
8974
|
+
registerTenantTool(server, api, "get_contact", {
|
|
8975
8975
|
title: "Get full details of a contact",
|
|
8976
8976
|
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.",
|
|
8977
8977
|
inputSchema: GetContactInputSchema,
|
|
@@ -8986,49 +8986,49 @@ async function buildServer(config) {
|
|
|
8986
8986
|
idempotentHint: true,
|
|
8987
8987
|
openWorldHint: true
|
|
8988
8988
|
};
|
|
8989
|
-
server
|
|
8989
|
+
registerTenantTool(server, api, "get_service_delivery_report", {
|
|
8990
8990
|
title: "Service delivery turnaround report",
|
|
8991
8991
|
description: "Are we delivering client services as quickly as we should? Cohort = tasks COMPLETED in the window. Summary KPIs (count, avg/median turnaround from planned start to completion, on-time %, statutory compliance %, currently-overdue snapshot) plus a breakdown grouped by Service (default), Client, AssignedUser, or Month. Answers 'how fast do we turn around VAT returns?', 'which clients' work runs late?', 'is our turnaround improving month on month?' (groupBy=Month over a long window). NOT the right tool for missed deadlines — a task still open past its deadline never completed, so it isn't in this cohort; use get_deadline_compliance_report for that. For per-step bottlenecks within one service, follow up with get_service_delivery_steps. Constraints: dateRange='CustomDateRange' requires both startDate and endDate; summary always covers the whole cohort even when breakdown rows are paged.",
|
|
8992
8992
|
inputSchema: GetServiceDeliveryReportInputSchema,
|
|
8993
8993
|
annotations: reportAnnotations
|
|
8994
8994
|
}, (args) => handleGetServiceDeliveryReport(api, args));
|
|
8995
|
-
server
|
|
8995
|
+
registerTenantTool(server, api, "get_service_delivery_steps", {
|
|
8996
8996
|
title: "Per-step bottlenecks for one service",
|
|
8997
8997
|
description: "Where does the time go inside one billable service's workflow? For tasks of the given service completed in the window: average elapsed days per workflow step (measured from the previous step's completion, so queue/idle time counts), skip counts, chase counts, and each step flagged client-wait (waiting on the client: document requests, approvals, confirmations, data forms) vs internal — splitting 'we were slow' from 'the client was slow'. Use AFTER get_service_delivery_report flags a slow service, or directly for 'why do accounts jobs take so long?'. Requires serviceCode (from list_services). Note: steps are compared by name across tasks, so a renamed workflow step appears as separate rows.",
|
|
8998
8998
|
inputSchema: GetServiceDeliveryStepsInputSchema,
|
|
8999
8999
|
annotations: reportAnnotations
|
|
9000
9000
|
}, (args) => handleGetServiceDeliverySteps(api, args));
|
|
9001
|
-
server
|
|
9001
|
+
registerTenantTool(server, api, "get_deadline_compliance_report", {
|
|
9002
9002
|
title: "Deadline compliance report",
|
|
9003
9003
|
description: "Which deadlines did we miss, and which are we about to? Cohort = every non-Skipped task whose chosen deadline (internal due date by default, or statutory via deadlineType='StatutoryDueDate') FELL in the window, whatever the task's status — so misses that are still open count, unlike the completion-anchored service delivery report. Each deadline is exactly one of: met, completed late, missed & still open (the actionable ones), or not yet due (excluded from the compliance rate). Also returns a forward-looking now-snapshot of OPEN tasks bucketed by days remaining (overdue / 0-7 / 8-14 / 15-30 / 31+), which ignores the date window. Answers 'which statutory deadlines did we miss last quarter?' (deadlineType=StatutoryDueDate, dateRange=PreviousQuarter), 'what's at risk this week?' (read the upcoming buckets), 'who keeps missing deadlines?' (groupBy=AssignedUser). Constraints: dateRange='CustomDateRange' requires both startDate and endDate.",
|
|
9004
9004
|
inputSchema: GetDeadlineComplianceReportInputSchema,
|
|
9005
9005
|
annotations: reportAnnotations
|
|
9006
9006
|
}, (args) => handleGetDeadlineComplianceReport(api, args));
|
|
9007
|
-
server
|
|
9007
|
+
registerTenantTool(server, api, "get_client_responsiveness_report", {
|
|
9008
9008
|
title: "Client responsiveness report",
|
|
9009
9009
|
description: "Is it us or the client? Response times and chase pressure over CLIENT-FACING workflow steps (document requests, document approvals, client confirmations, data forms) completed in the window: how long clients take to respond (step sent to step completed), how much chasing it takes, plus a now-snapshot of steps currently waiting on a client. Group by Client (default — 'which clients are slowest to respond?'), Service, or StepType ('do clients answer confirmations faster than document requests?'). Use when slow delivery might be the client's fault rather than the practice's. Constraints: dateRange='CustomDateRange' requires both startDate and endDate.",
|
|
9010
9010
|
inputSchema: GetClientResponsivenessReportInputSchema,
|
|
9011
9011
|
annotations: reportAnnotations
|
|
9012
9012
|
}, (args) => handleGetClientResponsivenessReport(api, args));
|
|
9013
|
-
server
|
|
9013
|
+
registerTenantTool(server, api, "get_team_workload_report", {
|
|
9014
9014
|
title: "Team workload and productivity report",
|
|
9015
9015
|
description: "Who's overloaded, and who completes what? Two-sided rows per person (or team via groupBy='Team'): the open-workload NOW-snapshot (open / overdue / blocked counts — not affected by the date window) alongside completion throughput over the window (completions, average turnaround, on-time rate; completions attribute to whoever completed the task, not the assignee). Includes an Unassigned bucket. Answers 'who has capacity?', 'how much is on Jane's plate vs what she's shipping?', 'is anyone drowning in overdue work?'. Rows are bounded by team size — never paged. Constraints: dateRange='CustomDateRange' requires both startDate and endDate.",
|
|
9016
9016
|
inputSchema: GetTeamWorkloadReportInputSchema,
|
|
9017
9017
|
annotations: reportAnnotations
|
|
9018
9018
|
}, (args) => handleGetTeamWorkloadReport(api, args));
|
|
9019
|
-
server
|
|
9019
|
+
registerTenantTool(server, api, "get_wip_ageing_report", {
|
|
9020
9020
|
title: "WIP / ageing report",
|
|
9021
9021
|
description: "What's sitting untouched? A pure NOW-snapshot (the only report with no date window) of open tasks whose planned start date has arrived — how many, how old (age = planned start to today), and where they're stuck. Group by AgeBucket (default: 0-7/8-14/15-30/31-60/61+ days), Status, Service, Client, AssignedUser, or CurrentStep (the workflow step each task is sitting at — best for 'where is work stuck?'). Answers 'what's been sitting untouched the longest?', 'how much WIP do we have?', 'what's blocked right now?'.",
|
|
9022
9022
|
inputSchema: GetWipAgeingReportInputSchema,
|
|
9023
9023
|
annotations: reportAnnotations
|
|
9024
9024
|
}, (args) => handleGetWipAgeingReport(api, args));
|
|
9025
|
-
server
|
|
9025
|
+
registerTenantTool(server, api, "get_workflow_step_analysis", {
|
|
9026
9026
|
title: "Workflow step analysis report",
|
|
9027
9027
|
description: "For ONE workflow template, where does the time go across its runs? Cohort = tasks spawned from the workflow and completed in the window. Per-step duration distributions in workflow order — median/avg/p90/best/worst elapsed days (best and worst carry their task codes so outliers are one call away), active time (the step's own start-to-complete, vs elapsed which includes queueing), skip and chase counts, client-wait vs internal classification — plus summary totals. Requires workflowCode. Differs from get_service_delivery_steps by anchoring on the workflow template (homogeneous runs, richer distributions) rather than a billable service. Treat distributions over fewer than ~5 occurrences as noise (the response flags this). For the slowest individual runs, follow up with list_workflow_occurrences. Constraints: dateRange='CustomDateRange' requires both startDate and endDate.",
|
|
9028
9028
|
inputSchema: GetWorkflowStepAnalysisInputSchema,
|
|
9029
9029
|
annotations: reportAnnotations
|
|
9030
9030
|
}, (args) => handleGetWorkflowStepAnalysis(api, args));
|
|
9031
|
-
server
|
|
9031
|
+
registerTenantTool(server, api, "list_workflow_occurrences", {
|
|
9032
9032
|
title: "List a workflow's completed runs, slowest first",
|
|
9033
9033
|
description: "One row per completed run (task) of a workflow template in the window, SLOWEST FIRST: task, client, completion date, total elapsed days, and the client-wait vs internal split. The drill-down companion to get_workflow_step_analysis — use for 'show me the ten slowest VAT-return runs this quarter' or to find the outlier tasks dragging the averages. Requires workflowCode. Paged (default 10 per page). Constraints: dateRange='CustomDateRange' requires both startDate and endDate.",
|
|
9034
9034
|
inputSchema: ListWorkflowOccurrencesInputSchema,
|
|
@@ -9049,217 +9049,217 @@ async function buildServer(config) {
|
|
|
9049
9049
|
idempotentHint: true,
|
|
9050
9050
|
openWorldHint: true
|
|
9051
9051
|
};
|
|
9052
|
-
server
|
|
9052
|
+
registerTenantTool(server, api, "list_workflows", {
|
|
9053
9053
|
title: "List / search workflow templates",
|
|
9054
9054
|
description: "List the practice's workflow templates with search, sort (Name / GroupCount / StepCount / UpdatedDate), and pagination. Each row shows the code, group/step counts, and whether a client is required. This is THE way to resolve a workflow name ('our VAT workflow') to the workflowCode required by get_workflow_step_analysis, list_workflow_occurrences, and list_recurring_tasks' workflow filter. Also answers 'what workflows do we have?' and (limit=0) 'how many?'. Follow up with get_workflow for one template's full step structure.",
|
|
9055
9055
|
inputSchema: ListWorkflowsInputSchema,
|
|
9056
9056
|
annotations: lookupAnnotations
|
|
9057
9057
|
}, (args) => handleListWorkflows(api, args));
|
|
9058
|
-
server
|
|
9058
|
+
registerTenantTool(server, api, "get_workflow", {
|
|
9059
9059
|
title: "Get one workflow template's full structure",
|
|
9060
9060
|
description: "Get a single workflow template by code: the ordered groups and steps with step types (email, document request, approval, client confirmation, data form, etc.), assignment rules, auto-execute flags, dependencies, time estimates, and conditions. Answers 'what are the steps in our VAT return workflow?' and 'which steps in this workflow wait on the client?'. REQUIRES a concrete workflow code — call list_workflows first if the user named the workflow instead. For how the workflow performs in practice (durations, bottlenecks), use get_workflow_step_analysis.",
|
|
9061
9061
|
inputSchema: GetWorkflowInputSchema,
|
|
9062
9062
|
annotations: lookupAnnotations
|
|
9063
9063
|
}, (args) => handleGetWorkflow(api, args));
|
|
9064
|
-
server
|
|
9064
|
+
registerTenantTool(server, api, "list_teams", {
|
|
9065
9065
|
title: "List teams and their members",
|
|
9066
9066
|
description: "List the practice's teams with their member rosters (name + code per member). Use to resolve a team name ('the payroll team') to the team code accepted by list_tasks' team filter, create_task/update_task's assignedTeamCode, and the report tools' team cohorts — and to answer 'what teams do we have?' / 'who's in the tax team?'. Note the members list also gives you user codes, so this doubles as a name→code lookup for people organised by team.",
|
|
9067
9067
|
inputSchema: ListTeamsInputSchema,
|
|
9068
9068
|
annotations: lookupAnnotations
|
|
9069
9069
|
}, (args) => handleListTeams(api, args));
|
|
9070
|
-
server
|
|
9070
|
+
registerTenantTool(server, api, "list_task_categories", {
|
|
9071
9071
|
title: "List task categories",
|
|
9072
9072
|
description: "List the practice's task categories (code + name, in the practice's own sort order). Use to resolve a category name ('compliance work') to the categoryCode accepted by list_tasks, list_recurring_tasks, list_document_requests, list_data_form_requests, create_task/update_task, and the report tools' category cohorts. Categories are usually few — the first page is normally all of them.",
|
|
9073
9073
|
inputSchema: ListTaskCategoriesInputSchema,
|
|
9074
9074
|
annotations: lookupAnnotations
|
|
9075
9075
|
}, (args) => handleListTaskCategories(api, args));
|
|
9076
|
-
server
|
|
9076
|
+
registerTenantTool(server, api, "list_saved_task_filters", {
|
|
9077
9077
|
title: "List saved task views",
|
|
9078
9078
|
description: "List the user's saved task views/filters, showing each view's code AND the criteria it encodes (statuses, date range, users, teams, clients, categories, overdue/unassigned flags). Use when the user references a saved view by name ('run my Weekly Review', 'my compliance filter') — resolve the code here, then pass it as list_tasks' savedFilter. The echoed criteria also let you answer 'what does my Weekly Review view actually show?' without running it. Prefer menuOnly=true when guessing which view the user means — those are the ones they use daily.",
|
|
9079
9079
|
inputSchema: ListSavedTaskFiltersInputSchema,
|
|
9080
9080
|
annotations: lookupAnnotations
|
|
9081
9081
|
}, (args) => handleListSavedTaskFilters(api, args));
|
|
9082
|
-
server
|
|
9082
|
+
registerTenantTool(server, api, "list_saved_client_filters", {
|
|
9083
9083
|
title: "List saved client views",
|
|
9084
9084
|
description: "List the user's saved client views/filters, showing each view's code AND the criteria it encodes (statuses, client types, managers, partners, services, teams). Use when the user references a saved view by name ('my VAT clients view') — resolve the code here, then pass it as list_clients' savedFilter. Prefer menuOnly=true when guessing which view the user means.",
|
|
9085
9085
|
inputSchema: ListSavedClientFiltersInputSchema,
|
|
9086
9086
|
annotations: lookupAnnotations
|
|
9087
9087
|
}, (args) => handleListSavedClientFilters(api, args));
|
|
9088
|
-
server
|
|
9088
|
+
registerTenantTool(server, api, "search_companies_house", {
|
|
9089
9089
|
title: "Search Companies House",
|
|
9090
9090
|
description: "Search the Companies House public register by company name (full or partial) or company number. Returns matches with number, status, type, incorporation date, and registered address — live data from the register, not limited to the practice's clients. Use for prospect due diligence ('look up Bristol Roofing'), finding a company number before get_companies_house_profile, or checking name availability. For an EXISTING client's company number, prefer get_client_summary (business details) over searching by name.",
|
|
9091
9091
|
inputSchema: SearchCompaniesHouseInputSchema,
|
|
9092
9092
|
annotations: lookupAnnotations
|
|
9093
9093
|
}, (args) => handleSearchCompaniesHouse(api, args));
|
|
9094
|
-
server
|
|
9094
|
+
registerTenantTool(server, api, "get_companies_house_profile", {
|
|
9095
9095
|
title: "Get a company's full Companies House profile",
|
|
9096
9096
|
description: "Get the live Companies House record for one company number: legal status, incorporation date, registered office, SIC codes, accounts and confirmation-statement deadlines (with OVERDUE flags), last-filed accounts, charges/insolvency/liquidation flags, and current + resigned officers. Answers 'when are ACME's accounts due at Companies House?', 'who are the directors?', 'does this prospect have insolvency history?'. REQUIRES a company number — use search_companies_house for name lookups, or get_client_summary's business details for an existing client's number. Pass includeOfficers=false when only filing dates matter. Officers tolerate partial failure — the profile still returns if they can't be loaded.",
|
|
9097
9097
|
inputSchema: GetCompaniesHouseProfileInputSchema,
|
|
9098
9098
|
annotations: lookupAnnotations
|
|
9099
9099
|
}, (args) => handleGetCompaniesHouseProfile(api, args));
|
|
9100
|
-
server
|
|
9100
|
+
registerTenantTool(server, api, "list_document_requests", {
|
|
9101
9101
|
title: "List / filter document requests to clients",
|
|
9102
9102
|
description: "List document requests across the practice (or one client) with status, client, manager/partner, task category, search, sort, and pagination. Each row shows status, deadline, documents requested, chase history (last chased / next chase), and the linked task. THE tool for 'what are we waiting on from clients?' (status=Open, sortBy=CreatedDate), 'has ACME sent their records?' (clientCode), 'what's submitted and needs review?' (status=Submitted), 'how many open requests?' (limit=0). Statuses: Draft (not sent) → Open (waiting on client) → Submitted (client responded, awaiting review) → Accepted/Rejected. For aggregate response-time analytics use get_client_responsiveness_report instead; this is the item-level view.",
|
|
9103
9103
|
inputSchema: ListDocumentRequestsInputSchema,
|
|
9104
9104
|
annotations: lookupAnnotations
|
|
9105
9105
|
}, (args) => handleListDocumentRequests(api, args));
|
|
9106
|
-
server
|
|
9106
|
+
registerTenantTool(server, api, "list_data_form_requests", {
|
|
9107
9107
|
title: "List / filter data form (questionnaire) requests",
|
|
9108
9108
|
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).",
|
|
9109
9109
|
inputSchema: ListDataFormRequestsInputSchema,
|
|
9110
9110
|
annotations: lookupAnnotations
|
|
9111
9111
|
}, (args) => handleListDataFormRequests(api, args));
|
|
9112
|
-
server
|
|
9112
|
+
registerTenantTool(server, api, "list_time_entries", {
|
|
9113
9113
|
title: "List / filter time entries",
|
|
9114
9114
|
description: "List recorded time entries with filters: team member, client, task, preset date range (or CustomDateRange + fromDate/toDate), billable flag, billed flag, search, sort, pagination. Answers 'how much time have I logged this week?' (userCode = current user, dateRange=ThisWeek), 'time spent on ACME this month' (clientCode + dateRange=ThisMonth), 'what unbilled time do we have?' (isBillable=true, isBilled=false — the WIP question), 'biggest time entries on this job' (taskItemCode, sortBy=DurationMinutes, sortDesc=true). Each page also shows its summed duration. IMPORTANT: prefer a narrow dateRange — unwindowed queries scan all history. For 'how many entries?' pass limit=0. For aggregate per-service/per-client totals over a period prefer get_service_time_report — it sums the whole window in one call instead of paging entries.",
|
|
9115
9115
|
inputSchema: ListTimeEntriesInputSchema,
|
|
9116
9116
|
annotations: lookupAnnotations
|
|
9117
9117
|
}, (args) => handleListTimeEntries(api, args));
|
|
9118
|
-
server
|
|
9118
|
+
registerTenantTool(server, api, "get_service_time_report", {
|
|
9119
9119
|
title: "Service time report — where recorded time goes",
|
|
9120
9120
|
description: "Where does the practice's recorded time actually go? Aggregates time entries in the window: summary KPIs (total / billable / attributed / unattributed minutes, entry count) plus a breakdown by billable service — or, when serviceCode is passed, by CLIENT within that one service. Rows include estimate-vs-actual variance where estimates exist. Answers 'where did our time go last month?', 'which services are over their estimates?', 'which clients consume the most bookkeeping time?' (serviceCode for the bookkeeping service). Cohort = time entries whose entry date falls in the window (default Last30Days). For individual entries use list_time_entries; for workload/throughput by person use get_team_workload_report. Constraints: dateRange='CustomDateRange' requires both startDate and endDate.",
|
|
9121
9121
|
inputSchema: GetServiceTimeReportInputSchema,
|
|
9122
9122
|
annotations: reportAnnotations
|
|
9123
9123
|
}, (args) => handleGetServiceTimeReport(api, args));
|
|
9124
|
-
server
|
|
9124
|
+
registerTenantTool(server, api, "list_recurring_tasks", {
|
|
9125
9125
|
title: "List / filter recurring task templates",
|
|
9126
9126
|
description: "List the recurring-task templates that generate the practice's repeating work (monthly bookkeeping, quarterly VAT returns, annual accounts). Filters: active status, billable service, client, category, workflow, search, sort (Name / NextRunDate), pagination. Each row shows the recurrence pattern, linked service/workflow, assignee, and next run date. Answers 'what recurring work do we run?' (isActive=true), 'what recurring tasks does ACME have?' (clientCode), 'what generates next week?' (sortBy=NextRunDate), 'which recurring tasks use the VAT workflow?' (workflowCode from list_workflows). This lists the TEMPLATES — for the generated task instances use list_tasks with its recurring-task filter.",
|
|
9127
9127
|
inputSchema: ListRecurringTasksInputSchema,
|
|
9128
9128
|
annotations: lookupAnnotations
|
|
9129
9129
|
}, (args) => handleListRecurringTasks(api, args));
|
|
9130
|
-
server
|
|
9130
|
+
registerTenantTool(server, api, "list_sent_emails", {
|
|
9131
9131
|
title: "List emails Sodium sent (delivery audit trail)",
|
|
9132
9132
|
description: "List the emails Sodium sent on the practice's behalf — proposal/engagement emails, document-request chases, portal invitations, broadcasts — with delivery status (Queued/Processing/Sent/Failed/Cancelled), recipient, subject, date window, search, sort, pagination. THE tool for 'did the proposal email to ACME actually go out?', 'which emails failed this week?' (status=Failed + fromDate), 'what chases went to John?' (search on his address). This is the OUTBOUND SYSTEM audit trail only — for two-way email correspondence with a client (their replies, ad-hoc emails) use list_client_emails instead. Prefer a fromDate/toDate window; pass limit=0 for count-only. Follow up with get_sent_email for full recipient/error detail.",
|
|
9133
9133
|
inputSchema: ListSentEmailsInputSchema,
|
|
9134
9134
|
annotations: lookupAnnotations
|
|
9135
9135
|
}, (args) => handleListSentEmails(api, args));
|
|
9136
|
-
server
|
|
9136
|
+
registerTenantTool(server, api, "get_sent_email", {
|
|
9137
9137
|
title: "Get one sent email's delivery detail",
|
|
9138
9138
|
description: "Get the full delivery record of one system-sent email by id (from list_sent_emails): sender, all to/cc/bcc recipients, created/processed/sent timeline, retry count, last error, and attachment names. Use to diagnose 'why didn't the client get the engagement letter email?'. Note: the email BODY is not retained in the send history — only the delivery record; don't promise the user the content.",
|
|
9139
9139
|
inputSchema: GetSentEmailInputSchema,
|
|
9140
9140
|
annotations: lookupAnnotations
|
|
9141
9141
|
}, (args) => handleGetSentEmail(api, args));
|
|
9142
|
-
server
|
|
9142
|
+
registerTenantTool(server, api, "list_client_emails", {
|
|
9143
9143
|
title: "List a client's email correspondence (synced mailbox)",
|
|
9144
9144
|
description: "List email threads with one client from the practice's connected/synced mailbox: subject, latest sender, snippet, message and unread counts, attachments flag. Filters: search, direction (Inbound = from the client, Outbound = from the practice), provider folder, mailbox config, sort (default LatestMessageDate — pass sortDesc=true for newest first), pagination. Answers 'what's the latest correspondence with ACME?', 'has ACME replied about the VAT return?' (search), 'unanswered inbound email from clients'. Requires the practice to have connected a mailbox in Sodium — an empty result usually means no mailbox is synced, so say that rather than 'no emails exist'. For the delivery status of emails SODIUM sent (proposals, chases), use list_sent_emails instead.",
|
|
9145
9145
|
inputSchema: ListClientEmailsInputSchema,
|
|
9146
9146
|
annotations: lookupAnnotations
|
|
9147
9147
|
}, (args) => handleListClientEmails(api, args));
|
|
9148
|
-
server
|
|
9148
|
+
registerTenantTool(server, api, "list_aml_clients", {
|
|
9149
9149
|
title: "List AML / identity-check records",
|
|
9150
9150
|
description: "List the practice's AML (anti-money-laundering / KYC) records: each row shows the AML record id, whether it's an individual or entity, AML status (Active/Pending/Archived), risk level (Low/Normal/High), and how many linked contacts still have incomplete checks. AML records are held separately from PM client records — resolve by name with search (e.g. the client's name from list_clients). Answers 'which clients have outstanding AML checks?', 'do we have an AML record for ACME?', 'list our high-risk AML clients' (scan riskLevel in the output). Read-only — verifications are performed in the AML provider, not through this integration. Follow up with get_aml_client for per-contact statuses and risk assessments.",
|
|
9151
9151
|
inputSchema: ListAmlClientsInputSchema,
|
|
9152
9152
|
annotations: lookupAnnotations
|
|
9153
9153
|
}, (args) => handleListAmlClients(api, args));
|
|
9154
|
-
server
|
|
9154
|
+
registerTenantTool(server, api, "get_aml_client", {
|
|
9155
9155
|
title: "Get one AML record's full status",
|
|
9156
9156
|
description: "Get one AML record by id (from list_aml_clients — NOT a PM client code): the record's status and risk level, every linked contact with their onboarding status and AML check status (NotStarted/InProgress/Ready/InReview/Complete/ActionRequired/Cancelled), and the risk-assessment history (status, risk level, who prepared/reviewed, notes). Answers 'is ACME's AML complete?', 'which directors still need ID checks?', 'when was the last risk assessment reviewed and by whom?'. Perfect for onboarding-readiness checks: pair with get_client_summary and list_document_requests for a full 'are we ready to act for this client?' picture. Risk assessments tolerate partial failure — the record still returns with a note.",
|
|
9157
9157
|
inputSchema: GetAmlClientInputSchema,
|
|
9158
9158
|
annotations: lookupAnnotations
|
|
9159
9159
|
}, (args) => handleGetAmlClient(api, args));
|
|
9160
|
-
server
|
|
9160
|
+
registerTenantTool(server, api, "list_billing_line_items", {
|
|
9161
9161
|
title: "List / filter billing line items",
|
|
9162
9162
|
description: "List the billing line items Sodium has generated from client service subscriptions: status (Pending = awaiting approval, Approved = ready to invoice, Invoiced = pushed to the accounting system, Cancelled), client, service, billing-date window, sort, pagination. Each row shows value (quantity × unit price), VAT rate, billing period, and description; each page shows its summed net value. Answers 'what's waiting to be billed?' (status=Pending), 'what are we billing ACME this month?' (clientCode + billingDateFrom/To), 'how much bookkeeping revenue is queued?' (billableServiceCode + status filters), 'biggest pending lines' (sortBy=UnitPrice, sortDesc=true). For UNBILLED TIME (WIP) use list_time_entries with isBillable=true + isBilled=false — this tool is the service-subscription billing pipeline, not timesheets.",
|
|
9163
9163
|
inputSchema: ListBillingLineItemsInputSchema,
|
|
9164
9164
|
annotations: lookupAnnotations
|
|
9165
9165
|
}, (args) => handleListBillingLineItems(api, args));
|
|
9166
|
-
server
|
|
9166
|
+
registerTenantTool(server, api, "get_practice_dashboard", {
|
|
9167
9167
|
title: "Practice dashboard — state of the practice right now",
|
|
9168
9168
|
description: "One-call snapshot of what needs attention across the whole practice: overdue task count, tasks due in the next 7 days, blocked tasks, open document requests and pending data forms (what clients are sitting on), unassigned actionable workflow steps, total unbilled time, and the open proposal pipeline value. THE opening move for 'give me my morning briefing', 'what needs attention?', 'how are we doing?' — each non-zero line names the drill-down tool and filters to investigate it. Composed from count-only queries so it's cheap; sections tolerate partial failure independently. Takes no parameters.",
|
|
9169
9169
|
inputSchema: GetPracticeDashboardInputSchema,
|
|
9170
9170
|
annotations: lookupAnnotations
|
|
9171
9171
|
}, () => handleGetPracticeDashboard(api));
|
|
9172
|
-
server
|
|
9172
|
+
registerTenantTool(server, api, "get_onboarding_status", {
|
|
9173
9173
|
title: "Onboarding readiness check for one client",
|
|
9174
9174
|
description: "Are we ready to act for this client? One call assembles: engagement status (accepted letter of engagement or not), AML record matched by name (per-contact check completeness), HMRC agent authorisation statuses (granted vs outstanding), open document requests and pending data forms, and key-data completeness (company number, UTR, VAT, PAYE). Use for 'are we ready to start for Bristol Roofing?', 'what's blocking ACME's onboarding?', or a pre-work compliance check on any client. AML matching is by client NAME — verify the matched record is right. Everything after the client fetch tolerates partial failure.",
|
|
9175
9175
|
inputSchema: GetOnboardingStatusInputSchema,
|
|
9176
9176
|
annotations: lookupAnnotations
|
|
9177
9177
|
}, (args) => handleGetOnboardingStatus(api, args));
|
|
9178
|
-
server
|
|
9178
|
+
registerTenantTool(server, api, "list_pending_workflow_steps", {
|
|
9179
9179
|
title: "List actionable workflow steps across the practice",
|
|
9180
9180
|
description: "Practice-wide list of workflow steps that are actionable RIGHT NOW — the step-level 'what should I do next?'. Filters: assigned user ('my steps' — use the current user's code), assigned team, isUnassigned (work nobody owns; OR-combines with assignedUserCode), step type (Standard = internal work; DocumentRequest/DocumentApproval/ClientConfirmation/SendDataForm = client-facing), client, manager, partner, task category, search, sort (Deadline for urgency), pagination. Each row carries the task/group/step numbers that get_task_context and update_workflow_step need. Answers 'what steps are waiting on me?', 'what's unassigned?', 'which client-confirmation steps are outstanding?'. Pass limit=0 for count-only.",
|
|
9181
9181
|
inputSchema: ListPendingWorkflowStepsInputSchema,
|
|
9182
9182
|
annotations: lookupAnnotations
|
|
9183
9183
|
}, (args) => handleListPendingWorkflowSteps(api, args));
|
|
9184
|
-
server
|
|
9184
|
+
registerTenantTool(server, api, "get_client_agent_authorisations", {
|
|
9185
9185
|
title: "Get a client's HMRC agent authorisation statuses",
|
|
9186
9186
|
description: "Per-tax-regime HMRC agent authorisation status for one client (Self Assessment, Corporation Tax, PAYE, VAT, CIS, MTD variants...): NotRequested / Requested / Granted, whether each is required by the client's services, and any in-flight HMRC invitation with its expiry. Answers 'do we have SA authorisation for ACME?', 'which authorisations are missing before we can file?'. Read-only — requesting authorisations is done from the client's page in Sodium. Records only appear once the client has services that require them; an empty result on a new client is normal.",
|
|
9187
9187
|
inputSchema: GetClientAgentAuthorisationsInputSchema,
|
|
9188
9188
|
annotations: lookupAnnotations
|
|
9189
9189
|
}, (args) => handleGetClientAgentAuthorisations(api, args));
|
|
9190
|
-
server
|
|
9190
|
+
registerTenantTool(server, api, "list_unbilled_time_clients", {
|
|
9191
9191
|
title: "Unbilled time totals per client (WIP recovery)",
|
|
9192
9192
|
description: "The WIP-recovery aggregate: billable-but-unbilled time summed PER CLIENT by the API — one call answers 'which clients have unbilled time and how much?'. Optional name search and pagination (default 25/page). THE tool for 'what unbilled time do we have?', 'how much WIP is sitting on ACME?' (search), 'where's our biggest billing leak?'. Drill into one client's individual entries with list_time_entries (clientCode + isBillable=true + isBilled=false); actually billing the time is done from Sodium's Time screen. Pass limit=0 for count-only.",
|
|
9193
9193
|
inputSchema: ListUnbilledTimeClientsInputSchema,
|
|
9194
9194
|
annotations: lookupAnnotations
|
|
9195
9195
|
}, (args) => handleListUnbilledTimeClients(api, args));
|
|
9196
|
-
server
|
|
9196
|
+
registerTenantTool(server, api, "get_engagement_pipeline_summary", {
|
|
9197
9197
|
title: "Proposal pipeline totals (counts + values by status)",
|
|
9198
9198
|
description: "One-call aggregate of the engagement pipeline: counts and total values for Unsent / Sent / Viewed / Accepted / Rejected, plus the open pipeline (sent + viewed awaiting a decision). Answers 'what's our proposal pipeline worth?', 'how much did we win this quarter?' (dateRange=ThisQuarter, read the Accepted line), 'how much value is sitting unsigned?'. Omit the window for the all-time picture. Much cheaper than paging list_proposals and adding up — prefer this for any totals question; use list_proposals for the individual engagements. Constraints: dateRange='CustomDateRange' requires dateFrom and dateTo.",
|
|
9199
9199
|
inputSchema: GetEngagementPipelineSummaryInputSchema,
|
|
9200
9200
|
annotations: lookupAnnotations
|
|
9201
9201
|
}, (args) => handleGetEngagementPipelineSummary(api, args));
|
|
9202
|
-
server
|
|
9202
|
+
registerTenantTool(server, api, "list_client_documents", {
|
|
9203
9203
|
title: "List / search the document register",
|
|
9204
9204
|
description: "List documents held against clients (metadata only — titles, files, statuses; no file contents): filter by client, search (3+ chars), practice review status ('PendingReview' = uploads awaiting review), client approval status ('Pending' = sent to the client for sign-off, e.g. draft accounts), category, manager/partner, task category, sort, pagination. Answers 'do we have ACME's signed engagement letter?' (clientCode + search), 'which uploads need reviewing?' (reviewStatus=PendingReview), 'what's awaiting client approval?' (clientApprovalStatus=Pending). Files themselves are opened in Sodium or the portal — don't promise content extraction. Pass limit=0 for count-only.",
|
|
9205
9205
|
inputSchema: ListClientDocumentsInputSchema,
|
|
9206
9206
|
annotations: lookupAnnotations
|
|
9207
9207
|
}, (args) => handleListClientDocuments(api, args));
|
|
9208
|
-
server
|
|
9208
|
+
registerTenantTool(server, api, "list_data_forms", {
|
|
9209
9209
|
title: "List data form (questionnaire) templates",
|
|
9210
9210
|
description: "List the practice's data-form TEMPLATES — the questionnaires it can send to clients (SA questionnaire, onboarding forms...). Filters: search, status ('Published' = sendable, the usual filter), sort, pagination. This resolves 'the SA questionnaire' to the formCode that list_data_form_requests filters on and send_data_form requires. Each row shows category and total response count. For who's been SENT a form and who's responded, use list_data_form_requests.",
|
|
9211
9211
|
inputSchema: ListDataFormsInputSchema,
|
|
9212
9212
|
annotations: lookupAnnotations
|
|
9213
9213
|
}, (args) => handleListDataForms(api, args));
|
|
9214
|
-
server
|
|
9214
|
+
registerTenantTool(server, api, "get_task_history", {
|
|
9215
9215
|
title: "Get a task's change history (audit trail)",
|
|
9216
9216
|
description: "The full audit trail of one task, oldest first: creation, task status changes, reassignments, and workflow-step status/assignee transitions — each attributed to the team member, the client (via portal or magic link), or the system. Answers 'who changed the due date?', 'when did this move to InProgress and who did it?', 'did the client action their step themselves?'. Complements get_task_context (current state) with how it got there.",
|
|
9217
9217
|
inputSchema: GetTaskHistoryInputSchema,
|
|
9218
9218
|
annotations: lookupAnnotations
|
|
9219
9219
|
}, (args) => handleGetTaskHistory(api, args));
|
|
9220
|
-
server
|
|
9220
|
+
registerTenantTool(server, api, "list_invoice_submissions", {
|
|
9221
9221
|
title: "List invoice submissions to the accounting system",
|
|
9222
9222
|
description: "The invoice hand-off after list_billing_line_items: batches of billing lines submitted (or queued/failed/projected) to the connected accounting system, with external invoice numbers and links. Filters: status ('Failed' = didn't reach the accounting system, the actionable one; 'Projected' = forecast only), client, billing-date window, sort (TotalAmount for biggest first), pagination. Answers 'did ACME's invoice go through to Xero?', 'which invoice submissions failed and why?', 'what invoicing is forecast next month?' (status=Projected + window). Retrying failures is done in the Sodium UI. Pass limit=0 for count-only.",
|
|
9223
9223
|
inputSchema: ListInvoiceSubmissionsInputSchema,
|
|
9224
9224
|
annotations: lookupAnnotations
|
|
9225
9225
|
}, (args) => handleListInvoiceSubmissions(api, args));
|
|
9226
|
-
server
|
|
9226
|
+
registerTenantTool(server, api, "list_service_packages", {
|
|
9227
9227
|
title: "List service packages (bundled offerings)",
|
|
9228
9228
|
description: "List the practice's service packages — bundled offerings like a 'Growth package' — with contents (which services at which frequency), pricing mode (single package price vs per-service), and approximate annual value. Filters: search, contains-service (billableServiceCode array — 'which packages include bookkeeping?'), pagination. Complements list_services (individual catalogue). Applying a package to a client is done in the Sodium UI.",
|
|
9229
9229
|
inputSchema: ListServicePackagesInputSchema,
|
|
9230
9230
|
annotations: lookupAnnotations
|
|
9231
9231
|
}, (args) => handleListServicePackages(api, args));
|
|
9232
|
-
server
|
|
9232
|
+
registerTenantTool(server, api, "list_portal_users", {
|
|
9233
9233
|
title: "List client portal users and their login activity",
|
|
9234
9234
|
description: "Portal-access audit: every client-portal login (linked contact, email, active/deactivated, email-verified) with last-login date and invitation date. Filters (applied client-side — the endpoint returns everyone): activeOnly, neverLoggedIn ('who was invited but never logged in?' — the portal-adoption question). Answers 'which clients actually use the portal?', 'who needs a fresh invitation?'. Managing access and resending invitations is done from the client's contacts in Sodium.",
|
|
9235
9235
|
inputSchema: ListPortalUsersInputSchema,
|
|
9236
9236
|
annotations: lookupAnnotations
|
|
9237
9237
|
}, (args) => handleListPortalUsers(api, args));
|
|
9238
|
-
server
|
|
9238
|
+
registerTenantTool(server, api, "list_email_broadcasts", {
|
|
9239
9239
|
title: "List email broadcasts (campaigns)",
|
|
9240
9240
|
description: "List the practice's email broadcasts/campaigns with status (Draft/Queued/Sending/Completed/Failed/Cancelled), subject, audience, and progress counters (processed/total clients, failures). Answers 'what have we broadcast recently?', 'did the MTD newsletter finish sending?'. Read-only — creating and sending broadcasts stays in the Sodium UI. Follow up with get_email_broadcast for per-client delivery results.",
|
|
9241
9241
|
inputSchema: ListEmailBroadcastsInputSchema,
|
|
9242
9242
|
annotations: lookupAnnotations
|
|
9243
9243
|
}, (args) => handleListEmailBroadcasts(api, args));
|
|
9244
|
-
server
|
|
9244
|
+
registerTenantTool(server, api, "get_email_broadcast", {
|
|
9245
9245
|
title: "Get one broadcast's detail and delivery results",
|
|
9246
9246
|
description: "One campaign in full: configuration (subject, audience definition), progress counters, and per-client delivery rows — Sent / Failed / Skipped with the reason for each failure or skip. Answers 'how did the newsletter do — who didn't get it and why?'. Requires the broadcast code from list_email_broadcasts. Recipient rows are paged (recipientsLimit/recipientsOffset, default 25); pass recipientsLimit=0 for campaign detail only.",
|
|
9247
9247
|
inputSchema: GetEmailBroadcastInputSchema,
|
|
9248
9248
|
annotations: lookupAnnotations
|
|
9249
9249
|
}, (args) => handleGetEmailBroadcast(api, args));
|
|
9250
|
-
server
|
|
9250
|
+
registerTenantTool(server, api, "get_client_email_thread", {
|
|
9251
9251
|
title: "Read one email conversation (synced mailbox)",
|
|
9252
9252
|
description: "Drill into one email thread from the synced mailbox: every message in order with date, sender, read/attachment flags, and preview text, plus which clients and tasks the thread is linked to. Requires configCode AND conversationId — both shown per thread in list_client_emails output; never guess them. Answers 'what did ACME actually say in that thread?', 'show me the whole conversation about the VAT surcharge'. Message previews are snippets, not complete bodies — full bodies and attachments are opened in Sodium.",
|
|
9253
9253
|
inputSchema: GetClientEmailThreadInputSchema,
|
|
9254
9254
|
annotations: lookupAnnotations
|
|
9255
9255
|
}, (args) => handleGetClientEmailThread(api, args));
|
|
9256
|
-
server
|
|
9256
|
+
registerTenantTool(server, api, "list_client_links", {
|
|
9257
9257
|
title: "List the URLs saved on a client",
|
|
9258
9258
|
description: "The links saved against a client record — website, LinkedIn, accounting-software link, shared folder, etc. Answers 'what's ACME's website?', 'do we have a Xero link for Greggs?'. These are saved bookmarks, NOT related-client relationships. Small list, no filters needed beyond the client code.",
|
|
9259
9259
|
inputSchema: ListClientLinksInputSchema,
|
|
9260
9260
|
annotations: lookupAnnotations
|
|
9261
9261
|
}, (args) => handleListClientLinks(api, args));
|
|
9262
|
-
registerWriteTool(server, config.context, "add_client_service", {
|
|
9262
|
+
registerWriteTool(server, api, config.context, "add_client_service", {
|
|
9263
9263
|
title: "Assign a catalogue service to a client",
|
|
9264
9264
|
description: "Put a client on one of the practice's services: requires billableServiceCode (from list_services), billingFrequency (must match one of the service's configured pricing options — check get_service_details first), and startDate. Optional: status (default Active; use 'Proposed' when quoting), custom price (with overridePricing=true), pricing tier (REQUIRED for CustomTiers-priced services), setup fee, managing user, initial stage, auto-invoicing and first billing date. Use for 'put ACME on monthly bookkeeping', 'add VAT returns to Greggs from next month'. NOT exposed: pricing-factor questionnaires — services priced by factors should be configured in the Sodium UI; if the API rejects the call asking for pricing answers, say so and point the user to the UI. Check get_client_summary first to avoid assigning a duplicate service.",
|
|
9265
9265
|
inputSchema: AddClientServiceInputSchema,
|
|
@@ -9270,7 +9270,7 @@ async function buildServer(config) {
|
|
|
9270
9270
|
openWorldHint: true
|
|
9271
9271
|
}
|
|
9272
9272
|
}, (args) => handleAddClientService(api, args));
|
|
9273
|
-
registerWriteTool(server, config.context, "update_client_service_stage", {
|
|
9273
|
+
registerWriteTool(server, api, config.context, "update_client_service_stage", {
|
|
9274
9274
|
title: "Move a client's service to a different stage (kanban)",
|
|
9275
9275
|
description: "Move one client-service card to a different workflow stage — the kanban move, from chat. Requires the client code, the client's service-subscription code (from get_client_summary's services section — NOT the catalogue service code), and the target stageCode (stage codes are in get_service_details for the underlying service). Pass stageCode=null to clear the stage. Use for 'move ACME's VAT return to Records In', 'mark the bookkeeping as Finished on the board'. Note: workflows can also set stages automatically via SetServiceStage steps — a manual move may be overridden by the next automated step.",
|
|
9276
9276
|
inputSchema: UpdateClientServiceStageInputSchema,
|
|
@@ -9281,7 +9281,7 @@ async function buildServer(config) {
|
|
|
9281
9281
|
openWorldHint: true
|
|
9282
9282
|
}
|
|
9283
9283
|
}, (args) => handleUpdateClientServiceStage(api, args));
|
|
9284
|
-
registerWriteTool(server, config.context, "create_document_request", {
|
|
9284
|
+
registerWriteTool(server, api, config.context, "create_document_request", {
|
|
9285
9285
|
title: "Draft a document request for a client",
|
|
9286
9286
|
description: "Create a document request as a DRAFT — nothing is sent and the client is not notified. Requires clientCode and a title; optional client-facing description (write it as the client will read it), deadline, and a team member to notify on submission. Use for 'ask ACME for their year-end records', 'request the Jan–Mar bank statements from Greggs'. The user reviews and sends it from the client's Documents tab in Sodium — ALWAYS tell them it's drafted, not sent. Track it afterwards with list_document_requests (status=Draft).",
|
|
9287
9287
|
inputSchema: CreateDocumentRequestInputSchema,
|
|
@@ -9292,7 +9292,7 @@ async function buildServer(config) {
|
|
|
9292
9292
|
openWorldHint: true
|
|
9293
9293
|
}
|
|
9294
9294
|
}, (args) => handleCreateDocumentRequest(api, args));
|
|
9295
|
-
registerWriteTool(server, config.context, "send_data_form", {
|
|
9295
|
+
registerWriteTool(server, api, config.context, "send_data_form", {
|
|
9296
9296
|
title: "Send a data form (questionnaire) to a client",
|
|
9297
9297
|
description: "Send a published data form to a client. ⚠ SENDS IMMEDIATELY — an email goes to the client the moment this is called; there is no draft state. NEVER call this without the user explicitly confirming the send in this conversation (form + client + recipients). Requires formCode (from list_data_forms) and clientCode. Optional: client-facing message, recipient contact types or explicit addresses (only addresses the user gave or that are on the client's contacts), team member to notify on submission, autoAcceptData (default false — answers await review; only enable when explicitly asked), chase-frequency override. Confirm before calling; afterwards report exactly who it was sent to. Track responses with list_data_form_requests.",
|
|
9298
9298
|
inputSchema: SendDataFormInputSchema,
|
|
@@ -9303,7 +9303,7 @@ async function buildServer(config) {
|
|
|
9303
9303
|
openWorldHint: true
|
|
9304
9304
|
}
|
|
9305
9305
|
}, (args) => handleSendDataForm(api, args));
|
|
9306
|
-
registerWriteTool(server, config.context, "create_proposal", {
|
|
9306
|
+
registerWriteTool(server, api, config.context, "create_proposal", {
|
|
9307
9307
|
title: "Draft a proposal / letter of engagement",
|
|
9308
9308
|
description: "Create an engagement in UNSENT (draft) state — the client is NOT emailed; sending is a deliberate separate action in the Sodium UI. Requires clientCode and date. type defaults to ProposalAndEngagementLetter ('EngagementLetter' for existing clients needing only the letter). clientBillableServiceCodes are the CLIENT'S service-subscription codes (get_client_summary's services section), not catalogue codes — to propose a service the client doesn't have yet, first add_client_service with status 'Proposed', then include its new code here. Recipient defaults to the client's main contact when omitted; only use an email from the client's contacts or given by the user. Use for 'draft a proposal for Bristol Roofing covering bookkeeping and VAT'. Always report the drafted value and that it has NOT been sent; full detail via get_proposal_summary.",
|
|
9309
9309
|
inputSchema: CreateProposalInputSchema,
|
|
@@ -9314,7 +9314,7 @@ async function buildServer(config) {
|
|
|
9314
9314
|
openWorldHint: true
|
|
9315
9315
|
}
|
|
9316
9316
|
}, (args) => handleCreateProposal(api, args));
|
|
9317
|
-
registerWriteTool(server, config.context, "update_client_dates", {
|
|
9317
|
+
registerWriteTool(server, api, config.context, "update_client_dates", {
|
|
9318
9318
|
title: "Set key/statutory dates on a client",
|
|
9319
9319
|
description: "Set one or more of a client's key dates (year-end, VAT return due, PAYE/CIS filing due, pension re-enrolment...). Takes an array of { dateType, date } — only the types you pass change; pass date=null to clear one. Current values are in get_client_summary's key-dates section. Use for 'set ACME's year-end to 31 March', 'record the pension re-enrolment date'. CAUTION: for clients connected to Companies House, register-derived dates (accounts due, confirmation statement) are synced automatically and manual values may be overwritten — prefer setting only non-CH dates (YearEnd, VAT, PAYE, pension) on connected clients, and say so if asked to set a CH-derived one.",
|
|
9320
9320
|
inputSchema: UpdateClientDatesInputSchema,
|
|
@@ -9325,7 +9325,7 @@ async function buildServer(config) {
|
|
|
9325
9325
|
openWorldHint: true
|
|
9326
9326
|
}
|
|
9327
9327
|
}, (args) => handleUpdateClientDates(api, args));
|
|
9328
|
-
registerWriteTool(server, config.context, "update_client_business_details", {
|
|
9328
|
+
registerWriteTool(server, api, config.context, "update_client_business_details", {
|
|
9329
9329
|
title: "Update a client's business/tax details",
|
|
9330
9330
|
description: "Update business details on a client: trading name, postal/invoice addresses, date of trading, nature of business, UTR, PAYE reference, Accounts Office reference, and VAT registration (registered flag, number, registration date, scheme, reporting period). Only the fields you pass change — the handler fetches current details and merges, and Companies-House-derived registration data (company number, incorporation date, registered address) is never touched. Pass null to clear a clearable field. Use for 'set ACME's UTR to 1234567890', 'mark Greggs as VAT registered on the flat rate scheme, quarterly Jan/Apr/Jul/Oct'. Current values are in get_client_summary's business details.",
|
|
9331
9331
|
inputSchema: UpdateClientBusinessDetailsInputSchema,
|
|
@@ -9336,7 +9336,7 @@ async function buildServer(config) {
|
|
|
9336
9336
|
openWorldHint: true
|
|
9337
9337
|
}
|
|
9338
9338
|
}, (args) => handleUpdateClientBusinessDetails(api, args));
|
|
9339
|
-
registerWriteTool(server, config.context, "create_saved_task_filter", {
|
|
9339
|
+
registerWriteTool(server, api, config.context, "create_saved_task_filter", {
|
|
9340
9340
|
title: "Save a task query as a reusable view",
|
|
9341
9341
|
description: "Persist a task filter as a saved view the user can reopen in Sodium and rerun here (list_tasks savedFilter). The natural closer after building a filtered list in conversation — 'save that as a view called Monday Review'. Requires a name; accepts the full task-filter surface (statuses, date range + basis, users/teams/clients/categories/recurring templates, overdue/unassigned flags, projected/agenda modes, default sort). includeInMenu pins it to the user's menu; isShared makes it practice-visible (read-only to others) — both default off, only set when asked. Echo back the criteria you saved so the user can confirm they match intent.",
|
|
9342
9342
|
inputSchema: CreateSavedTaskFilterInputSchema,
|
|
@@ -9347,7 +9347,7 @@ async function buildServer(config) {
|
|
|
9347
9347
|
openWorldHint: true
|
|
9348
9348
|
}
|
|
9349
9349
|
}, (args) => handleCreateSavedTaskFilter(api, args));
|
|
9350
|
-
registerWriteTool(server, config.context, "update_workflow_step", {
|
|
9350
|
+
registerWriteTool(server, api, config.context, "update_workflow_step", {
|
|
9351
9351
|
title: "Update a workflow step (complete, skip, block, reassign)",
|
|
9352
9352
|
description: "Update one workflow step on a task: set status (Completed / Skipped / InProgress / Blocked with a blockedReason) and/or reassign to a user or team. Identify the step by taskCode + groupNumber + stepNumber exactly as shown in get_task_context's workflow section — ALWAYS call get_task_context first to confirm the numbers and current status. Use for 'mark the review step on TSK-123 as done', 'skip the client-approval step, they confirmed by phone', 'assign the bookkeeping steps to Jane'. Constraints: steps with dependencies may be rejected until prior steps complete — report the API's reason rather than retrying; auto-executing steps (emails, document requests) fire when their turn comes, so completing them manually may skip the automated action — warn the user if get_task_context showed the step as auto-executing. Step content/configuration is not editable here — that's done in the Sodium UI.",
|
|
9353
9353
|
inputSchema: UpdateWorkflowStepInputSchema,
|
|
@@ -9358,7 +9358,7 @@ async function buildServer(config) {
|
|
|
9358
9358
|
openWorldHint: true
|
|
9359
9359
|
}
|
|
9360
9360
|
}, (args) => handleUpdateWorkflowStep(api, args));
|
|
9361
|
-
registerWriteTool(server, config.context, "create_task", {
|
|
9361
|
+
registerWriteTool(server, api, config.context, "create_task", {
|
|
9362
9362
|
title: "Create a new task",
|
|
9363
9363
|
description: "Create a one-off task. Requires name, startDate, and dueDate (YYYY-MM-DD — 'due Friday' resolves against today from the startup context). Optional: description, status (defaults NotStarted), statutory due date, time estimate, client(s) (codes from list_clients), assignee (user code from the roster/list_users, or team code from list_teams), linked client-billable-service, category (list_task_categories), and a checklist (array of item texts). Use for 'create a task to chase ACME's rental schedule, due Friday', 'add a task for Jane to review the draft accounts'. Do NOT use for recurring schedules or workflow-driven tasks — those are set up in the Sodium UI. If the user gave a client or person by name, resolve codes first; ask rather than guess when multiple matches exist.",
|
|
9364
9364
|
inputSchema: CreateTaskInputSchema,
|
|
@@ -9369,7 +9369,7 @@ async function buildServer(config) {
|
|
|
9369
9369
|
openWorldHint: true
|
|
9370
9370
|
}
|
|
9371
9371
|
}, (args) => handleCreateTask(api, args));
|
|
9372
|
-
registerWriteTool(server, config.context, "update_task", {
|
|
9372
|
+
registerWriteTool(server, api, config.context, "update_task", {
|
|
9373
9373
|
title: "Update a task (status, dates, assignment, details)",
|
|
9374
9374
|
description: "Update fields on an existing task by code: status (mark Completed / InProgress / Blocked / Skipped), name, description, start/due/statutory dates, time estimate, assigned user or team, and category. Only the fields you pass change — the handler fetches the current task and preserves everything else (clients, checklist, service and recurring-task links), so it's safe to pass just { taskCode, status: 'Completed' } for 'mark it done'. Pass null to clear a clearable field (description, statutory date, estimate, assignee, team, category). Use for 'mark TSK-123 complete', 'push the deadline to Friday', 'reassign it to Jane' (resolve her code from the roster), 'unassign this task' (assignedUserCode=null). Completing a task does NOT complete its workflow steps — if get_task_context showed open steps, tell the user they remain open. Cannot delete tasks — deletion isn't available through the MCP.",
|
|
9375
9375
|
inputSchema: UpdateTaskInputSchema,
|
|
@@ -9380,7 +9380,7 @@ async function buildServer(config) {
|
|
|
9380
9380
|
openWorldHint: true
|
|
9381
9381
|
}
|
|
9382
9382
|
}, (args) => handleUpdateTask(api, args));
|
|
9383
|
-
registerWriteTool(server, config.context, "log_time_entry", {
|
|
9383
|
+
registerWriteTool(server, api, config.context, "log_time_entry", {
|
|
9384
9384
|
title: "Log a time entry",
|
|
9385
9385
|
description: "Record time spent, after the fact. Requires entryDate (YYYY-MM-DD — use today from the startup context when the user doesn't say) and durationMinutes ('2 hours' → 120). Optional: description (strongly recommended — it feeds billing narratives), client code, task code, workflow group/step numbers (from get_task_context), billable flag, hourly rate, and userCode when logging on someone else's behalf (defaults to the authenticated user). Use for 'log 2 hours on ACME for today — reviewing the draft accounts', 'add 45 minutes against the Greggs VAT task'. Billable and rate default from practice/team-member configuration — only pass them when the user is explicit. Some practices require a client or task link; if the API rejects the entry saying so, ask the user which client/task to attach.",
|
|
9386
9386
|
inputSchema: LogTimeEntryInputSchema,
|
|
@@ -9391,7 +9391,7 @@ async function buildServer(config) {
|
|
|
9391
9391
|
openWorldHint: true
|
|
9392
9392
|
}
|
|
9393
9393
|
}, (args) => handleLogTimeEntry(api, args));
|
|
9394
|
-
registerWriteTool(server, config.context, "add_task_note", {
|
|
9394
|
+
registerWriteTool(server, api, config.context, "add_task_note", {
|
|
9395
9395
|
title: "Add a note to a task",
|
|
9396
9396
|
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.",
|
|
9397
9397
|
inputSchema: AddTaskNoteInputSchema,
|
|
@@ -9402,7 +9402,7 @@ async function buildServer(config) {
|
|
|
9402
9402
|
openWorldHint: true
|
|
9403
9403
|
}
|
|
9404
9404
|
}, (args) => handleAddTaskNote(api, args));
|
|
9405
|
-
registerWriteTool(server, config.context, "create_client", {
|
|
9405
|
+
registerWriteTool(server, api, config.context, "create_client", {
|
|
9406
9406
|
title: "Create a new client",
|
|
9407
9407
|
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'.",
|
|
9408
9408
|
inputSchema: CreateClientInputSchema,
|
|
@@ -9413,7 +9413,7 @@ async function buildServer(config) {
|
|
|
9413
9413
|
openWorldHint: true
|
|
9414
9414
|
}
|
|
9415
9415
|
}, (args) => handleCreateClient(api, args));
|
|
9416
|
-
registerWriteTool(server, config.context, "add_client_note", {
|
|
9416
|
+
registerWriteTool(server, api, config.context, "add_client_note", {
|
|
9417
9417
|
title: "Add a note to a client",
|
|
9418
9418
|
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.",
|
|
9419
9419
|
inputSchema: AddClientNoteInputSchema,
|
|
@@ -9424,7 +9424,7 @@ async function buildServer(config) {
|
|
|
9424
9424
|
openWorldHint: true
|
|
9425
9425
|
}
|
|
9426
9426
|
}, (args) => handleAddClientNote(api, args));
|
|
9427
|
-
registerWriteTool(server, config.context, "update_client_custom_fields", {
|
|
9427
|
+
registerWriteTool(server, api, config.context, "update_client_custom_fields", {
|
|
9428
9428
|
title: "Update custom field values on a client",
|
|
9429
9429
|
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.",
|
|
9430
9430
|
inputSchema: SetClientCustomFieldsInputSchema,
|
|
@@ -9435,7 +9435,7 @@ async function buildServer(config) {
|
|
|
9435
9435
|
openWorldHint: true
|
|
9436
9436
|
}
|
|
9437
9437
|
}, (args) => handleSetClientCustomFields(api, args));
|
|
9438
|
-
registerWriteTool(server, config.context, "update_contact", {
|
|
9438
|
+
registerWriteTool(server, api, config.context, "update_contact", {
|
|
9439
9439
|
title: "Update a contact's details",
|
|
9440
9440
|
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 ...'.",
|
|
9441
9441
|
inputSchema: UpdateContactInputSchema,
|
|
@@ -9446,7 +9446,7 @@ async function buildServer(config) {
|
|
|
9446
9446
|
openWorldHint: true
|
|
9447
9447
|
}
|
|
9448
9448
|
}, (args) => handleUpdateContact(api, args));
|
|
9449
|
-
registerWriteTool(server, config.context, "create_contact", {
|
|
9449
|
+
registerWriteTool(server, api, config.context, "create_contact", {
|
|
9450
9450
|
title: "Create a new contact",
|
|
9451
9451
|
description: "Create a new contact record. Requires at least a last name. Optionally set title, first name, email, phone, mobile, date of birth, UTR, NI number, and address. The contact code is auto-generated. Use when the user says 'add a contact', 'create contact John Smith', 'add a new director for ACME'. Note: this creates the contact record only — to link it to a client, use the Sodium web UI.",
|
|
9452
9452
|
inputSchema: CreateContactInputSchema,
|
|
@@ -9457,7 +9457,7 @@ async function buildServer(config) {
|
|
|
9457
9457
|
openWorldHint: true
|
|
9458
9458
|
}
|
|
9459
9459
|
}, (args) => handleCreateContact(api, args));
|
|
9460
|
-
registerWriteTool(server, config.context, "link_contact_to_client", {
|
|
9460
|
+
registerWriteTool(server, api, config.context, "link_contact_to_client", {
|
|
9461
9461
|
title: "Link an existing contact to a client",
|
|
9462
9462
|
description: "Link an existing contact to a client with one or more relationship types (Main, Billing, Payroll, Accounts, Director, Psc). Optionally set a free-text role (e.g. 'Managing Director'). The contact must already exist — use create_contact first if needed, then link with this tool. Use when the user says 'add John as a director on ACME', 'link contact CON-001 to client CLI-002 as the main contact'.",
|
|
9463
9463
|
inputSchema: LinkContactToClientInputSchema,
|