@sodiumhq/mcp-pm 0.1.0-beta.2603 → 0.1.0-beta.2611
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 +12 -1
- package/dist/index.js +169 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -48,6 +48,13 @@ Then ask: *"give me a summary of my practice"*.
|
|
|
48
48
|
| `SODIUM_API_KEY` | yes | — | Your Sodium API key |
|
|
49
49
|
| `SODIUM_TENANT` | yes | — | Your tenant code |
|
|
50
50
|
| `SODIUM_API_URL` | no | `https://api.sodiumhq.com` | Override for staging/dev |
|
|
51
|
+
| `SODIUM_ENABLE_WRITES` | no | `false` | Set to `true` or `1` to allow write tools (equivalent to `--enable-writes`) |
|
|
52
|
+
|
|
53
|
+
## Write mode
|
|
54
|
+
|
|
55
|
+
Write tools are **off by default** — the server exposes only read tools unless you opt in. Enable writes by adding `--enable-writes` to `args`, or by setting `SODIUM_ENABLE_WRITES=true` (or `1`) in `env`. Destructive and bulk operations (delete, batch) stay blocked either way.
|
|
56
|
+
|
|
57
|
+
Only enable write mode with an AI client you trust — it hands the client the ability to modify data under your API key. To check whether writes are on, ask your AI assistant *"can you make changes to my Sodium data?"* — it will tell you.
|
|
51
58
|
|
|
52
59
|
## What it can do today
|
|
53
60
|
|
|
@@ -73,12 +80,16 @@ Then ask: *"give me a summary of my practice"*.
|
|
|
73
80
|
**Team**
|
|
74
81
|
- **`list_users`** — find team members by name, email, role, or status — supports "who is Jane?", "list all partners", "who has been invited but not joined?"
|
|
75
82
|
|
|
83
|
+
**Write tools** (require `--enable-writes` — see [Write mode](#write-mode))
|
|
84
|
+
- **`add_task_note`** — capture a note against a specific task. Attributed to your API user, timestamped to now.
|
|
85
|
+
- **`add_client_note`** — capture a note against a client record. Same shape as `add_task_note` but scoped to the client.
|
|
86
|
+
|
|
76
87
|
More tools land iteratively as the beta progresses.
|
|
77
88
|
|
|
78
89
|
## Requirements
|
|
79
90
|
|
|
80
91
|
- Node.js 20 or later
|
|
81
|
-
- An active Sodium Practice Management subscription
|
|
92
|
+
- An active Sodium Practice Management subscription
|
|
82
93
|
- API key and tenant code from your Sodium account
|
|
83
94
|
|
|
84
95
|
## Licence
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
4
4
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { z } from "zod";
|
|
7
|
+
import { parseArgs } from "node:util";
|
|
7
8
|
//#region ../mcp-core/src/generated/core/bodySerializer.gen.ts
|
|
8
9
|
const jsonBodySerializer = { bodySerializer: (body) => JSON.stringify(body, (_key, value) => typeof value === "bigint" ? value.toString() : value) };
|
|
9
10
|
Object.entries({
|
|
@@ -669,6 +670,26 @@ const getClientDates = (options) => (options.client ?? client).get({
|
|
|
669
670
|
...options
|
|
670
671
|
});
|
|
671
672
|
/**
|
|
673
|
+
* Create Note for Client
|
|
674
|
+
*
|
|
675
|
+
* Creates a new Note for the specified client.
|
|
676
|
+
*/
|
|
677
|
+
const createClientNoteForClient = (options) => (options.client ?? client).post({
|
|
678
|
+
security: [{
|
|
679
|
+
name: "x-api-key",
|
|
680
|
+
type: "apiKey"
|
|
681
|
+
}, {
|
|
682
|
+
scheme: "bearer",
|
|
683
|
+
type: "http"
|
|
684
|
+
}],
|
|
685
|
+
url: "/tenants/{tenant}/clients/{client}/clientnote",
|
|
686
|
+
...options,
|
|
687
|
+
headers: {
|
|
688
|
+
"Content-Type": "application/json",
|
|
689
|
+
...options.headers
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
/**
|
|
672
693
|
* List Client Services for Client
|
|
673
694
|
*
|
|
674
695
|
* Lists all Client Services for the specified client.
|
|
@@ -835,6 +856,26 @@ const listTaskItemNotes = (options) => (options.client ?? client).get({
|
|
|
835
856
|
...options
|
|
836
857
|
});
|
|
837
858
|
/**
|
|
859
|
+
* Create Note
|
|
860
|
+
*
|
|
861
|
+
* Creates a new note for the specified task.
|
|
862
|
+
*/
|
|
863
|
+
const createTaskItemNote = (options) => (options.client ?? client).post({
|
|
864
|
+
security: [{
|
|
865
|
+
name: "x-api-key",
|
|
866
|
+
type: "apiKey"
|
|
867
|
+
}, {
|
|
868
|
+
scheme: "bearer",
|
|
869
|
+
type: "http"
|
|
870
|
+
}],
|
|
871
|
+
url: "/tenants/{tenant}/tasks/{taskCode}/taskitemnote",
|
|
872
|
+
...options,
|
|
873
|
+
headers: {
|
|
874
|
+
"Content-Type": "application/json",
|
|
875
|
+
...options.headers
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
/**
|
|
838
879
|
* Get Task Workflow Groups
|
|
839
880
|
*
|
|
840
881
|
* Retrieves comprehensive workflow progress information for a TaskItem.
|
|
@@ -1250,6 +1291,32 @@ var SodiumApiClient = class {
|
|
|
1250
1291
|
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `get workflow groups for task ${taskCode}`);
|
|
1251
1292
|
return data;
|
|
1252
1293
|
}
|
|
1294
|
+
async createTaskNote(taskCode, body) {
|
|
1295
|
+
const correlationId = randomUUID();
|
|
1296
|
+
const { data, error, response } = await createTaskItemNote({
|
|
1297
|
+
path: {
|
|
1298
|
+
tenant: this.ctx.tenant,
|
|
1299
|
+
taskCode
|
|
1300
|
+
},
|
|
1301
|
+
body,
|
|
1302
|
+
headers: { "X-Correlation-Id": correlationId }
|
|
1303
|
+
});
|
|
1304
|
+
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `add note to task ${taskCode}`);
|
|
1305
|
+
return data;
|
|
1306
|
+
}
|
|
1307
|
+
async createClientNote(clientCode, body) {
|
|
1308
|
+
const correlationId = randomUUID();
|
|
1309
|
+
const { data, error, response } = await createClientNoteForClient({
|
|
1310
|
+
path: {
|
|
1311
|
+
tenant: this.ctx.tenant,
|
|
1312
|
+
client: clientCode
|
|
1313
|
+
},
|
|
1314
|
+
body,
|
|
1315
|
+
headers: { "X-Correlation-Id": correlationId }
|
|
1316
|
+
});
|
|
1317
|
+
if (error !== void 0 || !data) throw this.toError(response, error, correlationId, `add note to client ${clientCode}`);
|
|
1318
|
+
return data;
|
|
1319
|
+
}
|
|
1253
1320
|
toError(response, error, correlationId, operation) {
|
|
1254
1321
|
const status = response.status;
|
|
1255
1322
|
let message = `Failed to ${operation} (HTTP ${status})`;
|
|
@@ -1261,7 +1328,7 @@ var SodiumApiClient = class {
|
|
|
1261
1328
|
//#endregion
|
|
1262
1329
|
//#region ../mcp-core/src/context/instructions.ts
|
|
1263
1330
|
const ROSTER_CAP = 20;
|
|
1264
|
-
async function buildInstructions(api) {
|
|
1331
|
+
async function buildInstructions(api, writesEnabled) {
|
|
1265
1332
|
const [user, tenant, practice, team] = await Promise.allSettled([
|
|
1266
1333
|
api.getCurrentUser(),
|
|
1267
1334
|
api.getTenantDetails(),
|
|
@@ -1295,6 +1362,7 @@ async function buildInstructions(api) {
|
|
|
1295
1362
|
}
|
|
1296
1363
|
if (tenant.status === "fulfilled") lines.push(`Tenant: ${tenant.value.name} (${tenant.value.code})`);
|
|
1297
1364
|
if (practice.status === "fulfilled") lines.push(`Practice: ${practice.value.name}`);
|
|
1365
|
+
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.");
|
|
1298
1366
|
if (team.status === "fulfilled") {
|
|
1299
1367
|
const members = team.value.data ?? [];
|
|
1300
1368
|
const total = team.value.totalCount ?? members.length;
|
|
@@ -2351,10 +2419,74 @@ function describeFilters(args) {
|
|
|
2351
2419
|
return parts.join(", ");
|
|
2352
2420
|
}
|
|
2353
2421
|
//#endregion
|
|
2422
|
+
//#region ../mcp-core/src/tools/add-client-note.ts
|
|
2423
|
+
const AddClientNoteInputSchema = {
|
|
2424
|
+
clientCode: z.string().min(1, "Client code is required").describe("The client code (identifier) to attach the note to. Usually discovered via list_clients or get_client_summary."),
|
|
2425
|
+
text: z.string().min(1, "Note text cannot be empty").describe("The note body. Keep it concise and factual — something the user can scan later. The user can edit or delete the note in the UI if the wording isn't right."),
|
|
2426
|
+
pinnedLevel: z.number().int().min(0).max(2).optional().describe("Pin level for the note. 0 = not pinned (default), 1 = pinned to the notes section (surfaces at the top of the client's notes list), 2 = pinned to the client page (surfaces on the main client record, most prominent). Only set above 0 if the user explicitly asks for the note to be pinned; use 2 when they say 'pin to the client' / 'client-level pin' / 'pin at the top of the client page', and 1 when they just say 'pin' or 'pin in notes'.")
|
|
2427
|
+
};
|
|
2428
|
+
async function handleAddClientNote(api, args) {
|
|
2429
|
+
try {
|
|
2430
|
+
const user = await api.getCurrentUser();
|
|
2431
|
+
const note = await api.createClientNote(args.clientCode, {
|
|
2432
|
+
text: args.text,
|
|
2433
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2434
|
+
noteFromUserCode: user.code ?? "",
|
|
2435
|
+
pinnedLevel: args.pinnedLevel ?? 0
|
|
2436
|
+
});
|
|
2437
|
+
return { content: [{
|
|
2438
|
+
type: "text",
|
|
2439
|
+
text: `Added note to client ${args.clientCode} (note code: ${note.code ?? "(no code)"}).`
|
|
2440
|
+
}] };
|
|
2441
|
+
} catch (error) {
|
|
2442
|
+
return {
|
|
2443
|
+
content: [{
|
|
2444
|
+
type: "text",
|
|
2445
|
+
text: error instanceof SodiumApiError ? `Error adding client note: ${error.message} (correlation: ${error.correlationId})` : `Error adding client note: ${error instanceof Error ? error.message : String(error)}`
|
|
2446
|
+
}],
|
|
2447
|
+
isError: true
|
|
2448
|
+
};
|
|
2449
|
+
}
|
|
2450
|
+
}
|
|
2451
|
+
//#endregion
|
|
2452
|
+
//#region ../mcp-core/src/tools/add-task-note.ts
|
|
2453
|
+
const AddTaskNoteInputSchema = {
|
|
2454
|
+
taskCode: z.string().min(1, "Task code is required").describe("The task code (identifier) to attach the note to. Usually discovered via list_tasks or get_task_context."),
|
|
2455
|
+
text: z.string().min(1, "Note text cannot be empty").describe("The note body. Keep it concise and factual — something the user can scan later. The user can edit or delete the note in the UI if the wording isn't right."),
|
|
2456
|
+
pinnedLevel: z.number().int().min(0).max(2).optional().describe("Pin level for the note. 0 = not pinned (default), 1 = pinned to the notes section (surfaces at the top of the task's notes list), 2 = pinned to the task page (surfaces on the main task record, most prominent). Only set above 0 if the user explicitly asks for the note to be pinned; use 2 when they say 'pin to the task' / 'task-level pin' / 'pin at the top of the task page', and 1 when they just say 'pin' or 'pin in notes'.")
|
|
2457
|
+
};
|
|
2458
|
+
async function handleAddTaskNote(api, args) {
|
|
2459
|
+
try {
|
|
2460
|
+
const user = await api.getCurrentUser();
|
|
2461
|
+
const note = await api.createTaskNote(args.taskCode, {
|
|
2462
|
+
text: args.text,
|
|
2463
|
+
date: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2464
|
+
noteFromUserCode: user.code ?? "",
|
|
2465
|
+
pinnedLevel: args.pinnedLevel ?? 0
|
|
2466
|
+
});
|
|
2467
|
+
return { content: [{
|
|
2468
|
+
type: "text",
|
|
2469
|
+
text: `Added note to task ${args.taskCode} (note code: ${note.code ?? "(no code)"}).`
|
|
2470
|
+
}] };
|
|
2471
|
+
} catch (error) {
|
|
2472
|
+
return {
|
|
2473
|
+
content: [{
|
|
2474
|
+
type: "text",
|
|
2475
|
+
text: error instanceof SodiumApiError ? `Error adding task note: ${error.message} (correlation: ${error.correlationId})` : `Error adding task note: ${error instanceof Error ? error.message : String(error)}`
|
|
2476
|
+
}],
|
|
2477
|
+
isError: true
|
|
2478
|
+
};
|
|
2479
|
+
}
|
|
2480
|
+
}
|
|
2481
|
+
//#endregion
|
|
2354
2482
|
//#region ../mcp-core/src/server.ts
|
|
2483
|
+
function registerWriteTool(server, ctx, name, config, cb) {
|
|
2484
|
+
if (!ctx.writesEnabled) return;
|
|
2485
|
+
server.registerTool(name, config, cb);
|
|
2486
|
+
}
|
|
2355
2487
|
async function buildServer(config) {
|
|
2356
2488
|
const api = new SodiumApiClient(config.context, { serverVersion: config.serverVersion });
|
|
2357
|
-
const instructions = await buildInstructions(api);
|
|
2489
|
+
const instructions = await buildInstructions(api, config.context.writesEnabled);
|
|
2358
2490
|
const server = new McpServer({
|
|
2359
2491
|
name: config.serverName,
|
|
2360
2492
|
version: config.serverVersion
|
|
@@ -2489,6 +2621,28 @@ async function buildServer(config) {
|
|
|
2489
2621
|
openWorldHint: true
|
|
2490
2622
|
}
|
|
2491
2623
|
}, (args) => handleListUsers(api, args));
|
|
2624
|
+
registerWriteTool(server, config.context, "add_task_note", {
|
|
2625
|
+
title: "Add a note to a task",
|
|
2626
|
+
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.",
|
|
2627
|
+
inputSchema: AddTaskNoteInputSchema,
|
|
2628
|
+
annotations: {
|
|
2629
|
+
readOnlyHint: false,
|
|
2630
|
+
destructiveHint: false,
|
|
2631
|
+
idempotentHint: false,
|
|
2632
|
+
openWorldHint: true
|
|
2633
|
+
}
|
|
2634
|
+
}, (args) => handleAddTaskNote(api, args));
|
|
2635
|
+
registerWriteTool(server, config.context, "add_client_note", {
|
|
2636
|
+
title: "Add a note to a client",
|
|
2637
|
+
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.",
|
|
2638
|
+
inputSchema: AddClientNoteInputSchema,
|
|
2639
|
+
annotations: {
|
|
2640
|
+
readOnlyHint: false,
|
|
2641
|
+
destructiveHint: false,
|
|
2642
|
+
idempotentHint: false,
|
|
2643
|
+
openWorldHint: true
|
|
2644
|
+
}
|
|
2645
|
+
}, (args) => handleAddClientNote(api, args));
|
|
2492
2646
|
return server;
|
|
2493
2647
|
}
|
|
2494
2648
|
//#endregion
|
|
@@ -2499,10 +2653,21 @@ function loadContext() {
|
|
|
2499
2653
|
const baseUrl = process.env.SODIUM_API_URL ?? "https://api.sodiumhq.com";
|
|
2500
2654
|
if (!apiKey) throw new Error("SODIUM_API_KEY environment variable is required. Generate one in Sodium → Settings → API Keys.");
|
|
2501
2655
|
if (!tenant) throw new Error("SODIUM_TENANT environment variable is required. Find your tenant code in Sodium → Settings → Practice.");
|
|
2656
|
+
const { values } = parseArgs({
|
|
2657
|
+
args: process.argv.slice(2),
|
|
2658
|
+
options: { "enable-writes": {
|
|
2659
|
+
type: "boolean",
|
|
2660
|
+
default: false
|
|
2661
|
+
} },
|
|
2662
|
+
strict: false
|
|
2663
|
+
});
|
|
2664
|
+
const cliWrites = values["enable-writes"] === true;
|
|
2665
|
+
const envRaw = (process.env.SODIUM_ENABLE_WRITES ?? "").trim().toLowerCase();
|
|
2502
2666
|
return {
|
|
2503
2667
|
apiKey,
|
|
2504
2668
|
tenant,
|
|
2505
|
-
baseUrl
|
|
2669
|
+
baseUrl,
|
|
2670
|
+
writesEnabled: cliWrites || envRaw === "true" || envRaw === "1"
|
|
2506
2671
|
};
|
|
2507
2672
|
}
|
|
2508
2673
|
//#endregion
|
|
@@ -2517,7 +2682,7 @@ async function main() {
|
|
|
2517
2682
|
});
|
|
2518
2683
|
const transport = new StdioServerTransport();
|
|
2519
2684
|
await server.connect(transport);
|
|
2520
|
-
console.error(`[sodium-pm-mcp] v${VERSION} ready (tenant: ${context.tenant})`);
|
|
2685
|
+
console.error(`[sodium-pm-mcp] v${VERSION} ready (tenant: ${context.tenant}, writes: ${context.writesEnabled ? "enabled" : "disabled"})`);
|
|
2521
2686
|
}
|
|
2522
2687
|
main().catch((error) => {
|
|
2523
2688
|
const message = error instanceof Error ? error.message : String(error);
|