@sodiumhq/mcp-pm 0.1.0-beta.6194 → 0.1.0-beta.6299

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-BB0-Ejj-.mjs";
2
+ import { t as buildServer } from "./src-b0BBk3Y8.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-BB0-Ejj-.mjs";
2
+ import { t as buildServer } from "./src-b0BBk3Y8.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
@@ -4515,6 +4530,7 @@ function format(input) {
4515
4530
  if (task.assignedUser) lines.push(`Assigned user: ${formatCodeAndName(task.assignedUser)}`);
4516
4531
  if (task.assignedTeam) lines.push(`Assigned team: ${formatCodeAndName(task.assignedTeam)}`);
4517
4532
  if (task.category) lines.push(`Category: ${formatCodeAndName(task.category)}`);
4533
+ if (task.clientBillableService) lines.push(`Service: ${formatCodeAndName(task.clientBillableService)}`);
4518
4534
  if (task.recurringTask) lines.push(`Recurring template: ${formatCodeAndName(task.recurringTask)}`);
4519
4535
  if (task.workflow) lines.push(`Workflow template: ${formatCodeAndName(task.workflow)}`);
4520
4536
  const description = task.description?.trim();
@@ -4905,7 +4921,7 @@ const ListTasksInputSchema = {
4905
4921
  };
4906
4922
  function formatTask(t) {
4907
4923
  const code = t.code ?? "(no code)";
4908
- return `- ${t.name ?? "(unnamed)"} (${code})${t.isOverdue ? " [OVERDUE]" : ""}${t.dueDate ? ` · due ${t.dueDate}` : ""}${t.primaryClient ? ` · ${t.primaryClient.name}` : t.clients?.[0] ? ` · ${t.clients[0].name}` : ""}${t.assignedUser?.name ? ` · ${t.assignedUser.name}` : ""}${t.status && t.status !== "NotStarted" ? ` · ${t.status}` : ""}${t.workflow ? ` · workflow ${t.workflowStepsComplete ?? 0}/${t.workflowSteps ?? 0}` : ""}`;
4924
+ return `- ${t.name ?? "(unnamed)"} (${code})${t.isOverdue ? " [OVERDUE]" : ""}${t.dueDate ? ` · due ${t.dueDate}` : ""}${t.statutoryDueDate ? ` · statutory ${t.statutoryDueDate}` : ""}${t.primaryClient ? ` · ${t.primaryClient.name}` : t.clients?.[0] ? ` · ${t.clients[0].name}` : ""}${t.clientBillableService?.name ? ` · service ${t.clientBillableService.name}${t.clientBillableService.code ? ` (${t.clientBillableService.code})` : ""}` : ""}${t.assignedUser?.name ? ` · ${t.assignedUser.name}` : ""}${t.status && t.status !== "NotStarted" ? ` · ${t.status}` : ""}${t.workflow ? ` · workflow ${t.workflowStepsComplete ?? 0}/${t.workflowSteps ?? 0}` : ""}`;
4909
4925
  }
4910
4926
  async function handleListTasks(api, args) {
4911
4927
  try {
@@ -7298,26 +7314,23 @@ const UpdateTaskInputSchema = {
7298
7314
  };
7299
7315
  async function handleUpdateTask(api, args) {
7300
7316
  try {
7301
- const current = await api.getTask(args.taskCode);
7302
7317
  const has = (key) => args[key] !== void 0;
7303
- const body = {
7304
- name: has("name") ? args.name : current.name ?? "",
7305
- description: has("description") ? args.description : current.description ?? null,
7306
- status: has("status") ? args.status : current.status ?? "NotStarted",
7307
- startDate: has("startDate") ? args.startDate : current.startDate ?? "",
7308
- dueDate: has("dueDate") ? args.dueDate : current.dueDate ?? "",
7309
- statutoryDueDate: has("statutoryDueDate") ? args.statutoryDueDate : current.statutoryDueDate ?? null,
7310
- timeEstimateMinutes: has("timeEstimateMinutes") ? args.timeEstimateMinutes : current.timeEstimateMinutes ?? null,
7311
- clientCodes: (current.clients ?? []).map((c) => c.code).filter((c) => Boolean(c)),
7312
- primaryClientCode: current.primaryClient?.code ?? null,
7313
- assignedUserCode: has("assignedUserCode") ? args.assignedUserCode : current.assignedUser?.code ?? null,
7314
- assignedTeamCode: has("assignedTeamCode") ? args.assignedTeamCode : current.assignedTeam?.code ?? null,
7315
- clientBillableServiceCode: current.clientBillableService?.code ?? null,
7316
- categoryCode: has("categoryCode") ? args.categoryCode : current.category?.code ?? null,
7317
- checklist: current.checklist ?? null,
7318
- recurringTaskCode: current.recurringTask?.code ?? null
7319
- };
7320
- 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);
7321
7334
  const changes = [];
7322
7335
  if (has("status")) changes.push(`status → ${updated.status}`);
7323
7336
  if (has("name")) changes.push(`name → ${updated.name}`);
@@ -7331,7 +7344,7 @@ async function handleUpdateTask(api, args) {
7331
7344
  if (has("timeEstimateMinutes")) changes.push(`estimate → ${updated.timeEstimateMinutes ?? "cleared"}m`);
7332
7345
  return { content: [{
7333
7346
  type: "text",
7334
- 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")
7335
7348
  }] };
7336
7349
  } catch (error) {
7337
7350
  return {
@@ -9520,7 +9533,7 @@ async function buildServer(config) {
9520
9533
  }, (args) => handleListClientNotes(api, args));
9521
9534
  registerTenantTool(server, api, "list_tasks", {
9522
9535
  title: "List / filter tasks across the practice",
9523
- description: "List tasks with any combination of filters: assigned user(s), unassigned, client(s), the primary client's client manager / partner (portfolio views: 'tasks across the clients Jane manages'), status, overdue flag, preset date range (Today / ThisWeek / Next7Days / CustomDateRange etc), category, team, billable service, workflow, workflow step type, current step name, recurring task template, free-text search, 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.",
9536
+ description: "List tasks with any combination of filters: assigned user(s), unassigned, client(s), the primary client's client manager / partner (portfolio views: 'tasks across the clients Jane manages'), status, overdue flag, preset date range (Today / ThisWeek / Next7Days / CustomDateRange etc), category, team, billable service, workflow, workflow step type, current step name, recurring task template, free-text search, 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; each row shows due date, statutory due date (when set), client, billable service (name + code), assignee, status and workflow progress. 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.",
9524
9537
  inputSchema: ListTasksInputSchema,
9525
9538
  annotations: {
9526
9539
  readOnlyHint: true,
@@ -9530,7 +9543,7 @@ async function buildServer(config) {
9530
9543
  }, (args) => handleListTasks(api, args));
9531
9544
  registerTenantTool(server, api, "get_task_context", {
9532
9545
  title: "Get full context for a single task",
9533
- 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).",
9546
+ description: "Get a consolidated view of one task by code: identity (name, code, status, overdue flag, start/due/statutory dates, time estimate, assignee, category, billable service, recurring/workflow templates), 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).",
9534
9547
  inputSchema: GetTaskContextInputSchema,
9535
9548
  annotations: {
9536
9549
  readOnlyHint: true,
@@ -10063,7 +10076,7 @@ async function buildServer(config) {
10063
10076
  }, (args) => handleCreateTask(api, args));
10064
10077
  registerWriteTool(server, api, config.context, "update_task", {
10065
10078
  title: "Update a task (status, dates, assignment, details)",
10066
- 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.",
10079
+ 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.",
10067
10080
  inputSchema: UpdateTaskInputSchema,
10068
10081
  annotations: {
10069
10082
  readOnlyHint: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sodiumhq/mcp-pm",
3
- "version": "0.1.0-beta.6194",
3
+ "version": "0.1.0-beta.6299",
4
4
  "description": "Sodium Practice Management MCP server — lets AI assistants interact with your Sodium tenant",
5
5
  "type": "module",
6
6
  "bin": {