@gethmy/mcp 3.2.0 → 3.3.0

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 CHANGED
@@ -5,7 +5,7 @@ Claude Code, OpenAI Codex, Cursor, and any MCP client claim cards, report progre
5
5
 
6
6
  ## Features
7
7
 
8
- - **77 MCP Tools** for full board control, knowledge graph, and workflow plans
8
+ - **79 MCP Tools** for full board control, knowledge graph, and workflow plans
9
9
  - **Global Skills** — installable in one command, served from the DB-backed [skill hub](../../docs/skills.md) with auto-update and admin-managed versioning
10
10
  - **Knowledge Graph Memory** — Phase 1 surface: hybrid retrieval (vector + lexical + RRF), session-scoped working memory, activity feed. See [docs/memory.md](../../docs/memory.md)
11
11
  - **GSD Workflow Plans** - plan/execute/verify/done lifecycle with auto card creation
@@ -89,7 +89,7 @@ If you prefer to configure manually (e.g., in Claude.ai's UI):
89
89
  1. Get an API key from [Harmony](https://gethmy.com/user/keys)
90
90
  2. In Claude.ai, add a remote MCP server with URL `https://mcp.gethmy.com/mcp`
91
91
  3. Set the Authorization header to `Bearer hmy_your_key_here`
92
- 4. All 77 Harmony tools become available in your conversation
92
+ 4. All 79 Harmony tools become available in your conversation
93
93
 
94
94
  **Session management** is automatic - sessions have a 1-hour TTL and are created/renewed transparently.
95
95
 
package/dist/cli.js CHANGED
@@ -1389,7 +1389,6 @@ var AGENT_MILESTONE_LIVENESS_MS = 30 * 60 * 1000;
1389
1389
  var AGENT_SWEEP_DAEMON_MS = 30 * 60 * 1000;
1390
1390
  var AGENT_SWEEP_INTERACTIVE_MS = 2 * 60 * 60 * 1000;
1391
1391
  var AGENT_SWEEP_PAUSED_MS = 4 * 60 * 60 * 1000;
1392
- var SWEPT_SESSION_WRITE_GRACE_MS = 60 * 60 * 1000;
1393
1392
  var ACTIVE_STATUSES = new Set(["working", "blocked", "waiting"]);
1394
1393
  // ../harmony-shared/dist/cardLinks.js
1395
1394
  var LINK_TYPE_INVERSES = {
@@ -2617,6 +2616,9 @@ ${untrustedDataBlock(planContent.trim(), {
2617
2616
  async updatePlaybook(playbookId, updates) {
2618
2617
  return this.request("PATCH", `/playbooks/${playbookId}`, updates);
2619
2618
  }
2619
+ async deletePlaybook(playbookId) {
2620
+ return this.request("DELETE", `/playbooks/${encodeURIComponent(playbookId)}`);
2621
+ }
2620
2622
  }
2621
2623
  var _promptModules = null;
2622
2624
  async function loadPromptModules() {
@@ -2854,6 +2856,36 @@ async function autoEndSession(scope, client3, cardId, status) {
2854
2856
  } catch {}
2855
2857
  }
2856
2858
 
2859
+ // src/comment-session.ts
2860
+ var RUN_SESSION_CARD_ENV = "HARMONY_AGENT_CARD_ID";
2861
+ var RUN_SESSION_ID_ENV = "HARMONY_AGENT_SESSION_ID";
2862
+ function readDeclaredRunSession(env = process.env) {
2863
+ const cardId = env[RUN_SESSION_CARD_ENV]?.trim();
2864
+ const agentSessionId = env[RUN_SESSION_ID_ENV]?.trim();
2865
+ if (!cardId || !agentSessionId)
2866
+ return null;
2867
+ return { cardId, agentSessionId };
2868
+ }
2869
+ function chooseCommentSession(args) {
2870
+ const tracked = args.tracked;
2871
+ if (tracked?.agentSessionId && tracked.scopeId === args.callerScopeId) {
2872
+ return {
2873
+ kind: "session",
2874
+ agentSessionId: tracked.agentSessionId,
2875
+ source: "tracked"
2876
+ };
2877
+ }
2878
+ const declared = args.declared;
2879
+ if (declared && declared.cardId === args.cardId) {
2880
+ return {
2881
+ kind: "session",
2882
+ agentSessionId: declared.agentSessionId,
2883
+ source: "declared"
2884
+ };
2885
+ }
2886
+ return { kind: "sessionless" };
2887
+ }
2888
+
2857
2889
  // src/server.ts
2858
2890
  init_config();
2859
2891
 
@@ -3452,6 +3484,51 @@ async function onboardNewUser(params) {
3452
3484
  };
3453
3485
  }
3454
3486
 
3487
+ // src/plan-task-link.ts
3488
+ function findPlanTask(tasks, taskId) {
3489
+ if (!taskId) {
3490
+ return { ok: false, reason: "No plan task id was given." };
3491
+ }
3492
+ if (!Array.isArray(tasks)) {
3493
+ return {
3494
+ ok: false,
3495
+ reason: "The plan returned no readable criteria list."
3496
+ };
3497
+ }
3498
+ for (const row of tasks) {
3499
+ if (!row || typeof row !== "object")
3500
+ continue;
3501
+ const candidate = row;
3502
+ if (candidate.id === taskId) {
3503
+ return { ok: true, task: candidate };
3504
+ }
3505
+ }
3506
+ return {
3507
+ ok: false,
3508
+ reason: `Plan task ${taskId} is not one of this plan's criteria. ` + `Read the plan with harmony_get_plan and use an id from its \`tasks\`.`
3509
+ };
3510
+ }
3511
+ function linkedReport(planId, task, newCardId) {
3512
+ const previous = task.card_id ?? null;
3513
+ return {
3514
+ planId,
3515
+ taskId: task.id,
3516
+ linked: true,
3517
+ criterion: task.content ?? null,
3518
+ ...previous && previous !== newCardId ? { replacedCardId: previous } : {}
3519
+ };
3520
+ }
3521
+ function unlinkedReport(planId, task, error) {
3522
+ const message = error instanceof Error ? error.message : String(error);
3523
+ return {
3524
+ planId,
3525
+ taskId: task.id,
3526
+ linked: false,
3527
+ criterion: task.content ?? null,
3528
+ error: `The card was created, but the plan criterion still does not point at it: ${message}. ` + `Repair it with harmony_link_plan_task — do not create the card again.`
3529
+ };
3530
+ }
3531
+
3455
3532
  // src/playbook-metric-warnings.ts
3456
3533
  function playbookMetricWarnings(agents, steps) {
3457
3534
  if (!Array.isArray(steps))
@@ -3949,12 +4026,13 @@ function optionalNonNegativeNumberArg(raw, field) {
3949
4026
  throw new Error(`${field} must be a non-negative number.`);
3950
4027
  return n;
3951
4028
  }
3952
- function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId) {
4029
+ function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, scopeId) {
3953
4030
  memorySessions.set(cardId, {
3954
4031
  cardId,
3955
4032
  agentIdentifier,
3956
4033
  agentName,
3957
4034
  agentSessionId,
4035
+ scopeId,
3958
4036
  memoryReadCount: 0,
3959
4037
  pendingActions: [],
3960
4038
  allActions: [],
@@ -4066,6 +4144,10 @@ var TOOLS = {
4066
4144
  type: "string",
4067
4145
  description: "Plan ID to link this card to (optional). Links the card to that plan via its plan_id."
4068
4146
  },
4147
+ planTaskId: {
4148
+ type: "string",
4149
+ description: "Id of the plan CRITERION this card is created to fulfil (optional; requires `planId`). " + "Sets both directions at once: the card's plan_id, and the criterion's card_id — the " + "return leg a card outcome needs to reach the plan. Read the ids from harmony_get_plan's " + "`tasks`. A criterion that is not in the named plan refuses the whole call, so no card is " + "created on a false premise; a failure to write the return leg AFTER the card exists " + "keeps the card and reports it in `planTask` instead."
4150
+ },
4069
4151
  attachments: {
4070
4152
  type: "array",
4071
4153
  description: "Optional reference files to attach to the new card (e.g. a screenshot from the prompt). " + "Max 5MB each; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV. Each file's bytes " + "come from `filePath` (absolute local path the server reads, preferred) or `base64Data` " + "(small-file fallback; requires fileName). NOTE: a pasted image only attaches if your " + "harness has written it to a local file you can pass as filePath — a model cannot re-emit " + "pasted image bytes into base64Data. Per-file failures never block card creation; they are " + "reported back in the result's `attachments` array so you can retry via harmony_upload.",
@@ -5543,6 +5625,29 @@ var TOOLS = {
5543
5625
  required: ["planId"]
5544
5626
  }
5545
5627
  },
5628
+ harmony_link_plan_task: {
5629
+ description: "Point a plan CRITERION at the card that fulfils it, and/or set the criterion's status. " + "A plan task is a success criterion, not a work item: `cards.plan_id` says which plan a card " + "belongs to, and this sets the return leg the plan needs to show real progress instead of " + "guessing from board-column names. Use it to repair a link, to re-point a criterion at a " + "different card, or to mark a criterion `completed` once its card actually delivered it. " + "Leave a criterion the card did NOT deliver open — an open criterion is the signal.",
5630
+ inputSchema: {
5631
+ type: "object",
5632
+ properties: {
5633
+ planId: { type: "string", description: "Plan ID owning the criterion" },
5634
+ taskId: {
5635
+ type: "string",
5636
+ description: "Criterion id, from harmony_get_plan's `tasks`. Must belong to `planId`."
5637
+ },
5638
+ cardId: {
5639
+ type: "string",
5640
+ description: "Card that fulfils this criterion. Must live in the plan's own project."
5641
+ },
5642
+ status: {
5643
+ type: "string",
5644
+ enum: ["pending", "in_progress", "completed"],
5645
+ description: "Criterion status. Set `completed` only when the card demonstrably delivered it."
5646
+ }
5647
+ },
5648
+ required: ["planId", "taskId"]
5649
+ }
5650
+ },
5546
5651
  harmony_list_playbook: {
5547
5652
  description: "List a workspace's playbooks (reusable process definitions). Returns each playbook's name, version, and state. Read-only.",
5548
5653
  inputSchema: {
@@ -5637,6 +5742,19 @@ var TOOLS = {
5637
5742
  required: ["playbookId"]
5638
5743
  }
5639
5744
  },
5745
+ harmony_delete_playbook: {
5746
+ description: "Permanently delete a playbook. IRREVERSIBLE and cascading: its version snapshots and run history are deleted with it, and every card currently running it is unbound — the card stays on the board and keeps its column, but loses its playbook, its pinned version and its stage pointer, so the agent daemon stops treating it as a stage card. Prefer harmony_update_playbook with state='deprecated' unless the playbook should never have existed: a deprecated playbook stays applicable to in-flight cards and is only hidden from new applies. Requires the playbook's creator or a workspace owner/admin — and an ARMED playbook (triggerType 'auto') takes an owner/admin even from its creator, because removing it stops the workspace's automation for everyone. Anyone else is refused and the playbook is left alone. Returns unboundCardCount (the exact number of cards it unbound) and unboundCards (up to 50 of them by id, short_id, title and the stage each one lost).",
5747
+ inputSchema: {
5748
+ type: "object",
5749
+ properties: {
5750
+ playbookId: {
5751
+ type: "string",
5752
+ description: "Playbook ID to delete (UUID)"
5753
+ }
5754
+ },
5755
+ required: ["playbookId"]
5756
+ }
5757
+ },
5640
5758
  harmony_signup: {
5641
5759
  description: "Create a new user account. Returns a JWT session for subsequent authenticated calls. No API key required.",
5642
5760
  inputSchema: {
@@ -5931,28 +6049,58 @@ async function handleToolCall(name, args, deps) {
5931
6049
  fileName: z.string().optional(),
5932
6050
  contentType: z.string().optional()
5933
6051
  })).parse(args.attachments) : [];
6052
+ const planId = args.planId ? z.string().uuid().parse(args.planId) : undefined;
6053
+ const planTaskId = args.planTaskId ? z.string().uuid().parse(args.planTaskId) : undefined;
6054
+ if (planTaskId && !planId) {
6055
+ throw new Error("planTaskId requires planId: no route resolves a plan from a criterion id alone. " + "Pass the plan the criterion belongs to — harmony_get_plan returns both.");
6056
+ }
6057
+ let criterion;
6058
+ if (planTaskId && planId) {
6059
+ const { tasks } = await client3.getPlan(planId);
6060
+ const found = findPlanTask(tasks, planTaskId);
6061
+ if (!found.ok)
6062
+ throw new Error(found.reason);
6063
+ criterion = found.task;
6064
+ }
5934
6065
  const result = await client3.createCard(projectId, {
5935
6066
  title,
5936
6067
  columnId: args.columnId,
5937
6068
  description: args.description,
5938
6069
  priority: args.priority,
5939
6070
  assigneeId: args.assigneeId,
5940
- planId: args.planId
6071
+ planId
5941
6072
  });
6073
+ const newCardId = result.card?.id;
6074
+ let planTask;
6075
+ if (criterion && planId) {
6076
+ if (!newCardId) {
6077
+ planTask = unlinkedReport(planId, criterion, new Error("no card id was returned to link against"));
6078
+ } else {
6079
+ try {
6080
+ await client3.updatePlanTask(planId, criterion.id, {
6081
+ cardId: newCardId
6082
+ });
6083
+ planTask = linkedReport(planId, criterion, newCardId);
6084
+ } catch (err) {
6085
+ planTask = unlinkedReport(planId, criterion, err);
6086
+ }
6087
+ }
6088
+ }
6089
+ const planTaskField = planTask ? { planTask } : {};
5942
6090
  if (attachments.length === 0) {
5943
- return { success: true, ...result };
6091
+ return { success: true, ...result, ...planTaskField };
5944
6092
  }
5945
- const cardId = result.card?.id;
5946
- if (!cardId) {
6093
+ if (!newCardId) {
5947
6094
  return {
5948
6095
  success: true,
5949
6096
  ...result,
6097
+ ...planTaskField,
5950
6098
  attachmentWarning: "Card created, but attachments were skipped: no card id was returned to upload against."
5951
6099
  };
5952
6100
  }
5953
6101
  const attachmentResults = await Promise.all(attachments.map(async (file) => {
5954
6102
  try {
5955
- const uploaded = await attachFileToCard(client3, cardId, file);
6103
+ const uploaded = await attachFileToCard(client3, newCardId, file);
5956
6104
  return { ok: true, attachment: uploaded.attachment };
5957
6105
  } catch (err) {
5958
6106
  return {
@@ -5962,7 +6110,12 @@ async function handleToolCall(name, args, deps) {
5962
6110
  };
5963
6111
  }
5964
6112
  }));
5965
- return { success: true, ...result, attachments: attachmentResults };
6113
+ return {
6114
+ success: true,
6115
+ ...result,
6116
+ ...planTaskField,
6117
+ attachments: attachmentResults
6118
+ };
5966
6119
  }
5967
6120
  case "harmony_update_card": {
5968
6121
  const cardId = z.string().uuid().parse(args.cardId);
@@ -6446,15 +6599,24 @@ ${list}
6446
6599
  const supersedesId = args.supersedesId !== undefined ? z.string().uuid().parse(args.supersedesId) : undefined;
6447
6600
  const confirmsId = args.confirmsId !== undefined ? z.string().uuid().parse(args.confirmsId) : undefined;
6448
6601
  const replyToId = args.replyToId !== undefined ? z.string().uuid().parse(args.replyToId) : undefined;
6449
- const agentSessionId = getMemorySession(cardId)?.agentSessionId;
6602
+ const sessionChoice = chooseCommentSession({
6603
+ cardId,
6604
+ tracked: getMemorySession(cardId),
6605
+ callerScopeId: deps.getScopeId?.(),
6606
+ declared: readDeclaredRunSession()
6607
+ });
6450
6608
  const result = await client3.addComment(cardId, body, {
6451
6609
  commentType,
6452
6610
  supersedesId,
6453
6611
  confirmsId,
6454
6612
  replyToId,
6455
- agentSessionId
6613
+ agentSessionId: sessionChoice.kind === "session" ? sessionChoice.agentSessionId : undefined
6456
6614
  });
6457
- return { success: true, ...result };
6615
+ return {
6616
+ success: true,
6617
+ ...result,
6618
+ sessionAttribution: sessionChoice.kind === "session" ? sessionChoice.source : "none"
6619
+ };
6458
6620
  }
6459
6621
  case "harmony_get_comments": {
6460
6622
  const cardId = z.string().uuid().parse(args.cardId);
@@ -6678,7 +6840,7 @@ ${options}
6678
6840
  scopeId: deps.getScopeId?.()
6679
6841
  });
6680
6842
  const agentSessionId = result.session?.id;
6681
- initMemorySession(cardId, agentIdentifier, agentName, agentSessionId);
6843
+ initMemorySession(cardId, agentIdentifier, agentName, agentSessionId, deps.getScopeId?.());
6682
6844
  return {
6683
6845
  success: true,
6684
6846
  assignedTo,
@@ -7379,6 +7541,30 @@ ${options}
7379
7541
  const result = await client3.updatePlan(planId, updates);
7380
7542
  return { success: true, plan: result.plan };
7381
7543
  }
7544
+ case "harmony_link_plan_task": {
7545
+ const planId = z.string().uuid().parse(args.planId);
7546
+ const taskId = z.string().uuid().parse(args.taskId);
7547
+ const cardId = args.cardId ? z.string().uuid().parse(args.cardId) : undefined;
7548
+ const status = args.status ? z.enum(["pending", "in_progress", "completed"]).parse(args.status) : undefined;
7549
+ if (!cardId && !status) {
7550
+ throw new Error("Nothing to do: pass cardId, status, or both.");
7551
+ }
7552
+ const { tasks } = await client3.getPlan(planId);
7553
+ const found = findPlanTask(tasks, taskId);
7554
+ if (!found.ok)
7555
+ throw new Error(found.reason);
7556
+ await client3.updatePlanTask(planId, taskId, { cardId, status });
7557
+ return {
7558
+ success: true,
7559
+ planTask: {
7560
+ planId,
7561
+ taskId,
7562
+ criterion: found.task.content ?? null,
7563
+ ...cardId ? linkedReport(planId, found.task, cardId) : { linked: found.task.card_id != null },
7564
+ ...status ? { status } : {}
7565
+ }
7566
+ };
7567
+ }
7382
7568
  case "harmony_advance_plan": {
7383
7569
  const planId = z.string().uuid().parse(args.planId);
7384
7570
  const summary = args.summary;
@@ -7466,6 +7652,16 @@ ${options}
7466
7652
  ...warnings.length > 0 ? { warnings } : {}
7467
7653
  };
7468
7654
  }
7655
+ case "harmony_delete_playbook": {
7656
+ const playbookId = z.string().uuid().parse(args.playbookId);
7657
+ const result = await client3.deletePlaybook(playbookId);
7658
+ return {
7659
+ success: true,
7660
+ playbook: result.playbook,
7661
+ unboundCardCount: result.unboundCardCount,
7662
+ unboundCards: result.unboundCards
7663
+ };
7664
+ }
7469
7665
  case "harmony_save_card_as_playbook":
7470
7666
  return deprecatedRemovedToolResult("harmony_save_card_as_playbook");
7471
7667
  case "harmony_signup": {
@@ -8643,6 +8839,7 @@ var SAFE_HARMONY_TOOLS = [
8643
8839
  "harmony_create_plan",
8644
8840
  "harmony_update_plan",
8645
8841
  "harmony_advance_plan",
8842
+ "harmony_link_plan_task",
8646
8843
  "harmony_remember",
8647
8844
  "harmony_relate",
8648
8845
  "harmony_update_memory",