@sodiumhq/mcp-pm 0.1.0-beta.6257 → 0.1.0-beta.6351

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/dist/http.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-CMzdZk6e.mjs";
2
+ import { t as buildServer } from "./src-CRE2Nlq7.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { SUPPORTED_PROTOCOL_VERSIONS, createMcpHandler } from "@modelcontextprotocol/server";
5
5
  import { createHash, randomUUID } from "node:crypto";
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { t as buildServer } from "./src-CMzdZk6e.mjs";
2
+ import { t as buildServer } from "./src-CRE2Nlq7.mjs";
3
3
  import { createRequire } from "node:module";
4
4
  import { serveStdio } from "@modelcontextprotocol/server/stdio";
5
5
  import { parseArgs } from "node:util";
@@ -2070,11 +2070,26 @@ const getTaskItem = (options) => (options.client ?? client).get({
2070
2070
  ...options
2071
2071
  });
2072
2072
  /**
2073
- * Update TaskItem
2073
+ * Patch TaskItem
2074
2074
  *
2075
- * Updates a TaskItem for the specified tenant.
2075
+ * Updates part of one task, following JSON Merge Patch. Send only the fields to change: a field left
2076
+ * out is not touched, so a quick action (moving the task on the calendar, a status button, a checklist
2077
+ * tick) never overwrites details it did not send, such as the description. A field sent as null is
2078
+ * cleared: description, statutory due date, time estimate, checklist, assignee, team, category, service
2079
+ * and primary client. The name, status, dates and times are required on a task, so sending one of those
2080
+ * as null is a 400.
2081
+ *
2082
+ * **Clients**: the same rules as a full update. The primary client always stays on the task, and a task
2083
+ * with clients but no primary gets its first client as primary. To remove every client, send the client
2084
+ * list and the primary client as null. Changing the primary client clears the task's service and
2085
+ * pipeline links (they belong to a client) unless a service code is sent.
2086
+ *
2087
+ * The change is saved through the same update as a full update, so dated workflow steps follow the
2088
+ * task when its dates move (unless MoveWorkflowStepDates is false), and colleagues see the change live.
2089
+ *
2090
+ * Only a saved task can be patched. A projected (upcoming) task must be materialised first.
2076
2091
  */
2077
- const updateTaskItem = (options) => (options.client ?? client).put({
2092
+ const patchTaskItem = (options) => (options.client ?? client).patch({
2078
2093
  security: [{
2079
2094
  name: "x-api-key",
2080
2095
  type: "apiKey"
@@ -3756,9 +3771,9 @@ var SodiumApiClient = class {
3756
3771
  if (error !== void 0 || !data) throw this.toError(response, error, correlationId, "create task");
3757
3772
  return data;
3758
3773
  }
3759
- async updateTask(code, body) {
3774
+ async patchTask(code, body) {
3760
3775
  const correlationId = randomUUID();
3761
- const { data, error, response } = await updateTaskItem({
3776
+ const { data, error, response } = await patchTaskItem({
3762
3777
  path: {
3763
3778
  tenant: this.tenant,
3764
3779
  code
@@ -7299,26 +7314,23 @@ const UpdateTaskInputSchema = {
7299
7314
  };
7300
7315
  async function handleUpdateTask(api, args) {
7301
7316
  try {
7302
- const current = await api.getTask(args.taskCode);
7303
7317
  const has = (key) => args[key] !== void 0;
7304
- const body = {
7305
- name: has("name") ? args.name : current.name ?? "",
7306
- description: has("description") ? args.description : current.description ?? null,
7307
- status: has("status") ? args.status : current.status ?? "NotStarted",
7308
- startDate: has("startDate") ? args.startDate : current.startDate ?? "",
7309
- dueDate: has("dueDate") ? args.dueDate : current.dueDate ?? "",
7310
- statutoryDueDate: has("statutoryDueDate") ? args.statutoryDueDate : current.statutoryDueDate ?? null,
7311
- timeEstimateMinutes: has("timeEstimateMinutes") ? args.timeEstimateMinutes : current.timeEstimateMinutes ?? null,
7312
- clientCodes: (current.clients ?? []).map((c) => c.code).filter((c) => Boolean(c)),
7313
- primaryClientCode: current.primaryClient?.code ?? null,
7314
- assignedUserCode: has("assignedUserCode") ? args.assignedUserCode : current.assignedUser?.code ?? null,
7315
- assignedTeamCode: has("assignedTeamCode") ? args.assignedTeamCode : current.assignedTeam?.code ?? null,
7316
- clientBillableServiceCode: current.clientBillableService?.code ?? null,
7317
- categoryCode: has("categoryCode") ? args.categoryCode : current.category?.code ?? null,
7318
- checklist: current.checklist ?? null,
7319
- recurringTaskCode: current.recurringTask?.code ?? null
7320
- };
7321
- const updated = await api.updateTask(args.taskCode, body);
7318
+ const body = {};
7319
+ if (has("status")) body.status = args.status;
7320
+ if (has("name")) body.name = args.name;
7321
+ if (has("description")) body.description = args.description;
7322
+ if (has("startDate")) body.startDate = args.startDate;
7323
+ if (has("dueDate")) body.dueDate = args.dueDate;
7324
+ if (has("statutoryDueDate")) body.statutoryDueDate = args.statutoryDueDate;
7325
+ if (has("timeEstimateMinutes")) body.timeEstimateMinutes = args.timeEstimateMinutes;
7326
+ if (has("assignedUserCode")) body.assignedUserCode = args.assignedUserCode;
7327
+ if (has("assignedTeamCode")) body.assignedTeamCode = args.assignedTeamCode;
7328
+ if (has("categoryCode")) body.categoryCode = args.categoryCode;
7329
+ if (Object.keys(body).length === 0) return { content: [{
7330
+ type: "text",
7331
+ text: `Task ${args.taskCode} not changed: no fields were passed to update.`
7332
+ }] };
7333
+ const updated = await api.patchTask(args.taskCode, body);
7322
7334
  const changes = [];
7323
7335
  if (has("status")) changes.push(`status → ${updated.status}`);
7324
7336
  if (has("name")) changes.push(`name → ${updated.name}`);
@@ -7332,7 +7344,7 @@ async function handleUpdateTask(api, args) {
7332
7344
  if (has("timeEstimateMinutes")) changes.push(`estimate → ${updated.timeEstimateMinutes ?? "cleared"}m`);
7333
7345
  return { content: [{
7334
7346
  type: "text",
7335
- text: [`Task updated: ${updated.name} (${updated.code})`, changes.length ? `Changed: ${changes.join(", ")}` : "No field changes were requested."].join("\n")
7347
+ text: [`Task updated: ${updated.name} (${updated.code})`, `Changed: ${changes.join(", ")}`].join("\n")
7336
7348
  }] };
7337
7349
  } catch (error) {
7338
7350
  return {
@@ -9364,15 +9376,25 @@ function captureClientInfo(server, api) {
9364
9376
  version: info.version
9365
9377
  });
9366
9378
  }
9379
+ function withAnnotationTitle(config) {
9380
+ if (!config.title) return config;
9381
+ return {
9382
+ ...config,
9383
+ annotations: {
9384
+ title: config.title,
9385
+ ...config.annotations
9386
+ }
9387
+ };
9388
+ }
9367
9389
  function registerTenantTool(server, api, name, config, cb) {
9368
9390
  const inputSchema = {
9369
9391
  ...config.inputSchema ?? {},
9370
9392
  practice: practiceField
9371
9393
  };
9372
- server.registerTool(name, {
9394
+ server.registerTool(name, withAnnotationTitle({
9373
9395
  ...config,
9374
9396
  inputSchema
9375
- }, async (args, extra) => {
9397
+ }), async (args, extra) => {
9376
9398
  const started = Date.now();
9377
9399
  captureClientInfo(server, api);
9378
9400
  const { practice, ...rest } = args;
@@ -9452,7 +9474,7 @@ async function buildServer(config) {
9452
9474
  openWorldHint: true
9453
9475
  }
9454
9476
  }, () => handleGetPracticeDetails(api));
9455
- server.registerTool("whoami", {
9477
+ server.registerTool("whoami", withAnnotationTitle({
9456
9478
  title: "Show current user and tenant info",
9457
9479
  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.",
9458
9480
  inputSchema: { practice: practiceField },
@@ -9461,12 +9483,12 @@ async function buildServer(config) {
9461
9483
  idempotentHint: true,
9462
9484
  openWorldHint: true
9463
9485
  }
9464
- }, async (args) => {
9486
+ }), async (args) => {
9465
9487
  captureClientInfo(server, api);
9466
9488
  await resolveTenantForCall(api, args.practice);
9467
9489
  return handleWhoami(api);
9468
9490
  });
9469
- server.registerTool("list_practices", {
9491
+ server.registerTool("list_practices", withAnnotationTitle({
9470
9492
  title: "List the practices you can work in",
9471
9493
  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.",
9472
9494
  inputSchema: {},
@@ -9475,7 +9497,7 @@ async function buildServer(config) {
9475
9497
  idempotentHint: true,
9476
9498
  openWorldHint: true
9477
9499
  }
9478
- }, () => {
9500
+ }), () => {
9479
9501
  captureClientInfo(server, api);
9480
9502
  return handleListPractices(api);
9481
9503
  });
@@ -9682,7 +9704,7 @@ async function buildServer(config) {
9682
9704
  inputSchema: ListWorkflowOccurrencesInputSchema,
9683
9705
  annotations: reportAnnotations
9684
9706
  }, (args) => handleListWorkflowOccurrences(api, args));
9685
- server.registerTool("get_usage_examples", {
9707
+ server.registerTool("get_usage_examples", withAnnotationTitle({
9686
9708
  title: "Show example prompts for this integration",
9687
9709
  description: "Returns a curated cookbook of example prompts this Sodium integration answers well, grouped by topic (daily-workflow, clients, tasks-and-deadlines, client-requests, time-tracking, reports, companies-house, proposals-and-engagements, making-changes). Use when the user asks 'what can you do?', 'what can I ask about my Sodium data?', 'give me some examples', or seems unsure how to get value from the integration. Also useful onboarding a new user: show the overview, then drill into the topic nearest their role. Static content — makes no API call.",
9688
9710
  inputSchema: GetUsageExamplesInputSchema,
@@ -9691,7 +9713,7 @@ async function buildServer(config) {
9691
9713
  idempotentHint: true,
9692
9714
  openWorldHint: false
9693
9715
  }
9694
- }, (args) => handleGetUsageExamples(args));
9716
+ }), (args) => handleGetUsageExamples(args));
9695
9717
  const lookupAnnotations = {
9696
9718
  readOnlyHint: true,
9697
9719
  idempotentHint: true,
@@ -10064,7 +10086,7 @@ async function buildServer(config) {
10064
10086
  }, (args) => handleCreateTask(api, args));
10065
10087
  registerWriteTool(server, api, config.context, "update_task", {
10066
10088
  title: "Update a task (status, dates, assignment, details)",
10067
- 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.",
10089
+ 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 — everything else on the task (clients, checklist, start and due times, service and recurring-task links) is left exactly as it is, 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.",
10068
10090
  inputSchema: UpdateTaskInputSchema,
10069
10091
  annotations: {
10070
10092
  readOnlyHint: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.6257",
3
+ "version": "0.1.0-beta.6351",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {