@gethmy/mcp 2.4.2 → 2.4.3

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 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor) to interact with yo
5
5
 
6
6
  ## Features
7
7
 
8
- - **61+ MCP Tools** for full board control, knowledge graph, and workflow plans
8
+ - **67 MCP Tools** for full board control, knowledge graph, and workflow plans
9
9
  - **Knowledge Graph Memory** - persistent memory with entity types, tiers, scopes, and typed relations
10
10
  - **Active Learning** - auto-extracts lessons, solutions, and error patterns from completed work sessions
11
11
  - **Context Assembly** - token-budget-aware memory injection into AI prompts
@@ -95,7 +95,7 @@ If you prefer to configure manually (e.g., in Claude.ai's UI):
95
95
  1. Get an API key from [Harmony](https://gethmy.com/user/keys)
96
96
  2. In Claude.ai, add a remote MCP server with URL `https://mcp.gethmy.com/mcp`
97
97
  3. Set the Authorization header to `Bearer hmy_your_key_here`
98
- 4. All 61+ Harmony tools become available in your conversation
98
+ 4. All 67 Harmony tools become available in your conversation
99
99
 
100
100
  **Session management** is automatic - sessions have a 1-hour TTL and are created/renewed transparently.
101
101
 
@@ -324,6 +324,9 @@ Store and retrieve persistent knowledge across sessions. Memories have types, ti
324
324
  - `harmony_consolidate_memories` - Cluster similar draft/episode memories and merge into reference entities (dry-run by default)
325
325
  - `harmony_backfill_embeddings` - Generate vector embeddings for entities missing them
326
326
  - `harmony_backfill_relations` - Retroactively create semantic relations across existing entities
327
+ - `harmony_cleanup_memories` - Bulk cleanup helper: flag stale drafts, dedupe boilerplate, trim low-value entries
328
+ - `harmony_audit_memories` - Surface low-confidence or low-quality memories for human review
329
+ - `harmony_purge_memories` - Hard-delete archived or flagged memories (dry-run by default)
327
330
 
328
331
  ### Context Debugging
329
332
 
package/dist/cli.js CHANGED
@@ -27094,9 +27094,20 @@ class HarmonyApiClient {
27094
27094
  },
27095
27095
  body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined)
27096
27096
  });
27097
- const data = await response.json();
27097
+ const text = await response.text();
27098
+ const responseContentType = response.headers.get("content-type") || "";
27099
+ const looksLikeJson = responseContentType.includes("application/json");
27100
+ let data = null;
27101
+ let parseError = null;
27102
+ if (text) {
27103
+ try {
27104
+ data = JSON.parse(text);
27105
+ } catch (err) {
27106
+ parseError = err instanceof Error ? err : new Error(String(err));
27107
+ }
27108
+ }
27098
27109
  if (!response.ok) {
27099
- const errorMsg = data.error || `API error: ${response.status}`;
27110
+ const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
27100
27111
  if (!isRetryableError(null, response.status)) {
27101
27112
  throw new Error(errorMsg);
27102
27113
  }
@@ -27107,6 +27118,9 @@ class HarmonyApiClient {
27107
27118
  }
27108
27119
  throw lastError;
27109
27120
  }
27121
+ if (parseError) {
27122
+ throw new Error(`API returned ${response.status} with invalid JSON body: ${parseError.message}`);
27123
+ }
27110
27124
  return data;
27111
27125
  } catch (error48) {
27112
27126
  lastError = error48 instanceof Error ? error48 : new Error(String(error48));
@@ -27255,6 +27269,9 @@ class HarmonyApiClient {
27255
27269
  async createLabel(projectId, data) {
27256
27270
  return this.request("POST", "/labels", { projectId, ...data });
27257
27271
  }
27272
+ async deleteLabel(labelId) {
27273
+ return this.request("DELETE", `/labels/${labelId}`);
27274
+ }
27258
27275
  async createSubtask(cardId, title) {
27259
27276
  return this.request("POST", "/subtasks", { cardId, title });
27260
27277
  }
@@ -28994,6 +29011,30 @@ async function onboardNewUser(params) {
28994
29011
 
28995
29012
  // src/server.ts
28996
29013
  var memorySessions = new Map;
29014
+ function parseLabelList(raw) {
29015
+ if (raw === undefined || raw === null)
29016
+ return;
29017
+ if (Array.isArray(raw)) {
29018
+ const arr = raw.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
29019
+ return arr.length ? arr : undefined;
29020
+ }
29021
+ if (typeof raw === "string") {
29022
+ const trimmed = raw.trim();
29023
+ if (!trimmed)
29024
+ return;
29025
+ if (trimmed.startsWith("[")) {
29026
+ try {
29027
+ const parsed = JSON.parse(trimmed);
29028
+ if (Array.isArray(parsed) && parsed.every((x) => typeof x === "string")) {
29029
+ const arr = parsed.map((v) => v.trim()).filter((v) => v.length > 0);
29030
+ return arr.length ? arr : undefined;
29031
+ }
29032
+ } catch {}
29033
+ }
29034
+ return [trimmed];
29035
+ }
29036
+ return;
29037
+ }
28997
29038
  function initMemorySession(cardId, agentIdentifier, agentName) {
28998
29039
  memorySessions.set(cardId, {
28999
29040
  cardId,
@@ -29271,6 +29312,16 @@ var TOOLS = {
29271
29312
  required: ["name", "color"]
29272
29313
  }
29273
29314
  },
29315
+ harmony_delete_label: {
29316
+ description: "Delete a label from a project. Also removes it from any cards that reference it.",
29317
+ inputSchema: {
29318
+ type: "object",
29319
+ properties: {
29320
+ labelId: { type: "string", description: "Label ID to delete" }
29321
+ },
29322
+ required: ["labelId"]
29323
+ }
29324
+ },
29274
29325
  harmony_add_label_to_card: {
29275
29326
  description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
29276
29327
  inputSchema: {
@@ -30924,6 +30975,11 @@ async function handleToolCall(name, args, deps) {
30924
30975
  const result = await client3.createLabel(projectId, { name: name2, color });
30925
30976
  return { success: true, ...result };
30926
30977
  }
30978
+ case "harmony_delete_label": {
30979
+ const labelId = exports_external.string().uuid().parse(args.labelId);
30980
+ const result = await client3.deleteLabel(labelId);
30981
+ return { success: true, ...result };
30982
+ }
30927
30983
  case "harmony_add_label_to_card": {
30928
30984
  const cardId = exports_external.string().uuid().parse(args.cardId);
30929
30985
  let labelId = args.labelId ? exports_external.string().uuid().parse(args.labelId) : undefined;
@@ -31059,7 +31115,7 @@ async function handleToolCall(name, args, deps) {
31059
31115
  const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
31060
31116
  const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
31061
31117
  const moveToColumn = args.moveToColumn;
31062
- const addLabels = args.addLabels;
31118
+ const addLabels = parseLabelList(args.addLabels);
31063
31119
  let movedTo = null;
31064
31120
  const labelsAdded = [];
31065
31121
  if (moveToColumn || addLabels?.length) {
package/dist/index.js CHANGED
@@ -24854,9 +24854,20 @@ class HarmonyApiClient {
24854
24854
  },
24855
24855
  body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined)
24856
24856
  });
24857
- const data = await response.json();
24857
+ const text = await response.text();
24858
+ const responseContentType = response.headers.get("content-type") || "";
24859
+ const looksLikeJson = responseContentType.includes("application/json");
24860
+ let data = null;
24861
+ let parseError = null;
24862
+ if (text) {
24863
+ try {
24864
+ data = JSON.parse(text);
24865
+ } catch (err) {
24866
+ parseError = err instanceof Error ? err : new Error(String(err));
24867
+ }
24868
+ }
24858
24869
  if (!response.ok) {
24859
- const errorMsg = data.error || `API error: ${response.status}`;
24870
+ const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
24860
24871
  if (!isRetryableError(null, response.status)) {
24861
24872
  throw new Error(errorMsg);
24862
24873
  }
@@ -24867,6 +24878,9 @@ class HarmonyApiClient {
24867
24878
  }
24868
24879
  throw lastError;
24869
24880
  }
24881
+ if (parseError) {
24882
+ throw new Error(`API returned ${response.status} with invalid JSON body: ${parseError.message}`);
24883
+ }
24870
24884
  return data;
24871
24885
  } catch (error48) {
24872
24886
  lastError = error48 instanceof Error ? error48 : new Error(String(error48));
@@ -25015,6 +25029,9 @@ class HarmonyApiClient {
25015
25029
  async createLabel(projectId, data) {
25016
25030
  return this.request("POST", "/labels", { projectId, ...data });
25017
25031
  }
25032
+ async deleteLabel(labelId) {
25033
+ return this.request("DELETE", `/labels/${labelId}`);
25034
+ }
25018
25035
  async createSubtask(cardId, title) {
25019
25036
  return this.request("POST", "/subtasks", { cardId, title });
25020
25037
  }
@@ -26754,6 +26771,30 @@ async function onboardNewUser(params) {
26754
26771
 
26755
26772
  // src/server.ts
26756
26773
  var memorySessions = new Map;
26774
+ function parseLabelList(raw) {
26775
+ if (raw === undefined || raw === null)
26776
+ return;
26777
+ if (Array.isArray(raw)) {
26778
+ const arr = raw.filter((v) => typeof v === "string").map((v) => v.trim()).filter((v) => v.length > 0);
26779
+ return arr.length ? arr : undefined;
26780
+ }
26781
+ if (typeof raw === "string") {
26782
+ const trimmed = raw.trim();
26783
+ if (!trimmed)
26784
+ return;
26785
+ if (trimmed.startsWith("[")) {
26786
+ try {
26787
+ const parsed = JSON.parse(trimmed);
26788
+ if (Array.isArray(parsed) && parsed.every((x) => typeof x === "string")) {
26789
+ const arr = parsed.map((v) => v.trim()).filter((v) => v.length > 0);
26790
+ return arr.length ? arr : undefined;
26791
+ }
26792
+ } catch {}
26793
+ }
26794
+ return [trimmed];
26795
+ }
26796
+ return;
26797
+ }
26757
26798
  function initMemorySession(cardId, agentIdentifier, agentName) {
26758
26799
  memorySessions.set(cardId, {
26759
26800
  cardId,
@@ -27031,6 +27072,16 @@ var TOOLS = {
27031
27072
  required: ["name", "color"]
27032
27073
  }
27033
27074
  },
27075
+ harmony_delete_label: {
27076
+ description: "Delete a label from a project. Also removes it from any cards that reference it.",
27077
+ inputSchema: {
27078
+ type: "object",
27079
+ properties: {
27080
+ labelId: { type: "string", description: "Label ID to delete" }
27081
+ },
27082
+ required: ["labelId"]
27083
+ }
27084
+ },
27034
27085
  harmony_add_label_to_card: {
27035
27086
  description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
27036
27087
  inputSchema: {
@@ -28684,6 +28735,11 @@ async function handleToolCall(name, args, deps) {
28684
28735
  const result = await client3.createLabel(projectId, { name: name2, color });
28685
28736
  return { success: true, ...result };
28686
28737
  }
28738
+ case "harmony_delete_label": {
28739
+ const labelId = exports_external.string().uuid().parse(args.labelId);
28740
+ const result = await client3.deleteLabel(labelId);
28741
+ return { success: true, ...result };
28742
+ }
28687
28743
  case "harmony_add_label_to_card": {
28688
28744
  const cardId = exports_external.string().uuid().parse(args.cardId);
28689
28745
  let labelId = args.labelId ? exports_external.string().uuid().parse(args.labelId) : undefined;
@@ -28819,7 +28875,7 @@ async function handleToolCall(name, args, deps) {
28819
28875
  const agentIdentifier = exports_external.string().min(1).max(100).parse(args.agentIdentifier);
28820
28876
  const agentName = exports_external.string().min(1).max(100).parse(args.agentName);
28821
28877
  const moveToColumn = args.moveToColumn;
28822
- const addLabels = args.addLabels;
28878
+ const addLabels = parseLabelList(args.addLabels);
28823
28879
  let movedTo = null;
28824
28880
  const labelsAdded = [];
28825
28881
  if (moveToColumn || addLabels?.length) {
@@ -1612,9 +1612,20 @@ class HarmonyApiClient {
1612
1612
  },
1613
1613
  body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined)
1614
1614
  });
1615
- const data = await response.json();
1615
+ const text = await response.text();
1616
+ const responseContentType = response.headers.get("content-type") || "";
1617
+ const looksLikeJson = responseContentType.includes("application/json");
1618
+ let data = null;
1619
+ let parseError = null;
1620
+ if (text) {
1621
+ try {
1622
+ data = JSON.parse(text);
1623
+ } catch (err) {
1624
+ parseError = err instanceof Error ? err : new Error(String(err));
1625
+ }
1626
+ }
1616
1627
  if (!response.ok) {
1617
- const errorMsg = data.error || `API error: ${response.status}`;
1628
+ const errorMsg = data?.error || (looksLikeJson ? null : `API error: ${response.status} (non-JSON response)`) || `API error: ${response.status}`;
1618
1629
  if (!isRetryableError(null, response.status)) {
1619
1630
  throw new Error(errorMsg);
1620
1631
  }
@@ -1625,6 +1636,9 @@ class HarmonyApiClient {
1625
1636
  }
1626
1637
  throw lastError;
1627
1638
  }
1639
+ if (parseError) {
1640
+ throw new Error(`API returned ${response.status} with invalid JSON body: ${parseError.message}`);
1641
+ }
1628
1642
  return data;
1629
1643
  } catch (error) {
1630
1644
  lastError = error instanceof Error ? error : new Error(String(error));
@@ -1773,6 +1787,9 @@ class HarmonyApiClient {
1773
1787
  async createLabel(projectId, data) {
1774
1788
  return this.request("POST", "/labels", { projectId, ...data });
1775
1789
  }
1790
+ async deleteLabel(labelId) {
1791
+ return this.request("DELETE", `/labels/${labelId}`);
1792
+ }
1776
1793
  async createSubtask(cardId, title) {
1777
1794
  return this.request("POST", "/subtasks", { cardId, title });
1778
1795
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.4.2",
3
+ "version": "2.4.3",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
package/src/api-client.ts CHANGED
@@ -176,10 +176,27 @@ export class HarmonyApiClient {
176
176
  body: options?.rawBody ?? (body ? JSON.stringify(body) : undefined),
177
177
  });
178
178
 
179
- const data = await response.json();
179
+ const text = await response.text();
180
+ const responseContentType = response.headers.get("content-type") || "";
181
+ const looksLikeJson = responseContentType.includes("application/json");
182
+
183
+ let data: ApiResponse | null = null;
184
+ let parseError: Error | null = null;
185
+ if (text) {
186
+ try {
187
+ data = JSON.parse(text);
188
+ } catch (err) {
189
+ parseError = err instanceof Error ? err : new Error(String(err));
190
+ }
191
+ }
180
192
 
181
193
  if (!response.ok) {
182
- const errorMsg = data.error || `API error: ${response.status}`;
194
+ const errorMsg =
195
+ data?.error ||
196
+ (looksLikeJson
197
+ ? null
198
+ : `API error: ${response.status} (non-JSON response)`) ||
199
+ `API error: ${response.status}`;
183
200
  if (!isRetryableError(null, response.status)) {
184
201
  throw new Error(errorMsg);
185
202
  }
@@ -191,6 +208,12 @@ export class HarmonyApiClient {
191
208
  throw lastError;
192
209
  }
193
210
 
211
+ if (parseError) {
212
+ throw new Error(
213
+ `API returned ${response.status} with invalid JSON body: ${parseError.message}`,
214
+ );
215
+ }
216
+
194
217
  return data as T;
195
218
  } catch (error) {
196
219
  lastError = error instanceof Error ? error : new Error(String(error));
@@ -455,6 +478,10 @@ export class HarmonyApiClient {
455
478
  return this.request("POST", "/labels", { projectId, ...data });
456
479
  }
457
480
 
481
+ async deleteLabel(labelId: string): Promise<{ success: boolean }> {
482
+ return this.request("DELETE", `/labels/${labelId}`);
483
+ }
484
+
458
485
  // ============ SUBTASK OPERATIONS ============
459
486
 
460
487
  async createSubtask(
package/src/server.ts CHANGED
@@ -100,6 +100,45 @@ interface MemorySessionState {
100
100
 
101
101
  const memorySessions = new Map<string, MemorySessionState>();
102
102
 
103
+ /**
104
+ * Normalize a label-list argument into `string[]`.
105
+ *
106
+ * Some MCP callers pass `addLabels` as a JSON-encoded string (e.g. `'["agent"]'`)
107
+ * instead of a real array. Iterating such a string with `for..of` would yield
108
+ * individual characters and create one bogus single-char label per character.
109
+ * This helper coerces the value into a well-formed array and drops empty entries.
110
+ */
111
+ function parseLabelList(raw: unknown): string[] | undefined {
112
+ if (raw === undefined || raw === null) return undefined;
113
+ if (Array.isArray(raw)) {
114
+ const arr = raw
115
+ .filter((v): v is string => typeof v === "string")
116
+ .map((v) => v.trim())
117
+ .filter((v) => v.length > 0);
118
+ return arr.length ? arr : undefined;
119
+ }
120
+ if (typeof raw === "string") {
121
+ const trimmed = raw.trim();
122
+ if (!trimmed) return undefined;
123
+ if (trimmed.startsWith("[")) {
124
+ try {
125
+ const parsed = JSON.parse(trimmed);
126
+ if (
127
+ Array.isArray(parsed) &&
128
+ parsed.every((x) => typeof x === "string")
129
+ ) {
130
+ const arr = parsed.map((v) => v.trim()).filter((v) => v.length > 0);
131
+ return arr.length ? arr : undefined;
132
+ }
133
+ } catch {
134
+ // fall through to single-label fallback
135
+ }
136
+ }
137
+ return [trimmed];
138
+ }
139
+ return undefined;
140
+ }
141
+
103
142
  function initMemorySession(
104
143
  cardId: string,
105
144
  agentIdentifier: string,
@@ -431,6 +470,17 @@ const TOOLS = {
431
470
  required: ["name", "color"],
432
471
  },
433
472
  },
473
+ harmony_delete_label: {
474
+ description:
475
+ "Delete a label from a project. Also removes it from any cards that reference it.",
476
+ inputSchema: {
477
+ type: "object",
478
+ properties: {
479
+ labelId: { type: "string", description: "Label ID to delete" },
480
+ },
481
+ required: ["labelId"],
482
+ },
483
+ },
434
484
  harmony_add_label_to_card: {
435
485
  description:
436
486
  "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
@@ -2412,6 +2462,12 @@ async function handleToolCall(
2412
2462
  return { success: true, ...result };
2413
2463
  }
2414
2464
 
2465
+ case "harmony_delete_label": {
2466
+ const labelId = z.string().uuid().parse(args.labelId);
2467
+ const result = await client.deleteLabel(labelId);
2468
+ return { success: true, ...result };
2469
+ }
2470
+
2415
2471
  case "harmony_add_label_to_card": {
2416
2472
  const cardId = z.string().uuid().parse(args.cardId);
2417
2473
  let labelId = args.labelId
@@ -2603,7 +2659,7 @@ async function handleToolCall(
2603
2659
  .parse(args.agentIdentifier);
2604
2660
  const agentName = z.string().min(1).max(100).parse(args.agentName);
2605
2661
  const moveToColumn = args.moveToColumn as string | undefined;
2606
- const addLabels = args.addLabels as string[] | undefined;
2662
+ const addLabels = parseLabelList(args.addLabels);
2607
2663
 
2608
2664
  let movedTo: string | null = null;
2609
2665
  const labelsAdded: string[] = [];