@gethmy/mcp 3.7.0 → 3.8.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
- - **79 MCP Tools** for full board control, knowledge graph, and workflow plans
8
+ - **80 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 79 Harmony tools become available in your conversation
92
+ 4. All 80 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
@@ -31,7 +31,7 @@ function noteLegacyLocalPin(path) {
31
31
  if (warnedLegacyLocalPin)
32
32
  return;
33
33
  warnedLegacyLocalPin = true;
34
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
34
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
35
35
  }
36
36
  function noteLocalPinRename(from, to) {
37
37
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
@@ -2204,6 +2204,60 @@ var REVIEW_DISALLOWED_TOOLS = [
2204
2204
  "mcp__harmony__harmony_delete_subtask",
2205
2205
  "mcp__harmony__harmony_toggle_subtask"
2206
2206
  ];
2207
+ // ../harmony-shared/dist/runEventSanitize.js
2208
+ var REPLACEMENT = "�";
2209
+ function sanitizeRunEventString(value) {
2210
+ let out = "";
2211
+ for (let i = 0;i < value.length; i++) {
2212
+ const code = value.charCodeAt(i);
2213
+ if (code === 0)
2214
+ continue;
2215
+ if (code >= 55296 && code <= 56319) {
2216
+ const next = value.charCodeAt(i + 1);
2217
+ if (next >= 56320 && next <= 57343) {
2218
+ out += value[i] + value[i + 1];
2219
+ i++;
2220
+ continue;
2221
+ }
2222
+ out += REPLACEMENT;
2223
+ continue;
2224
+ }
2225
+ if (code >= 56320 && code <= 57343) {
2226
+ out += REPLACEMENT;
2227
+ continue;
2228
+ }
2229
+ out += value[i];
2230
+ }
2231
+ return out;
2232
+ }
2233
+ function sanitizeRunEventPayload(payload) {
2234
+ return walk(payload, new Map);
2235
+ }
2236
+ function sanitizeRunEventDraft(draft) {
2237
+ return { ...draft, payload: sanitizeRunEventPayload(draft.payload) };
2238
+ }
2239
+ function walk(value, seen) {
2240
+ if (typeof value === "string")
2241
+ return sanitizeRunEventString(value);
2242
+ if (value === null || typeof value !== "object")
2243
+ return value;
2244
+ const already = seen.get(value);
2245
+ if (already !== undefined)
2246
+ return already;
2247
+ if (Array.isArray(value)) {
2248
+ const out2 = [];
2249
+ seen.set(value, out2);
2250
+ for (const entry of value)
2251
+ out2.push(walk(entry, seen));
2252
+ return out2;
2253
+ }
2254
+ const out = {};
2255
+ seen.set(value, out);
2256
+ for (const [key, entry] of Object.entries(value)) {
2257
+ out[sanitizeRunEventString(key)] = walk(entry, seen);
2258
+ }
2259
+ return out;
2260
+ }
2207
2261
  // ../harmony-shared/dist/runRedaction.js
2208
2262
  var MAX_INPUT_CHARS = 2000;
2209
2263
  var MAX_OUTPUT_CHARS = 4000;
@@ -2724,6 +2778,15 @@ class HarmonyApiClient {
2724
2778
  async registerWorkspaceAgent(workspaceId, data) {
2725
2779
  return this.request("POST", `/workspaces/${workspaceId}/agents`, data);
2726
2780
  }
2781
+ async reportAgentConfig(workspaceId, agentId, config) {
2782
+ return this.request("POST", `/workspaces/${workspaceId}/agents/${agentId}/reported-config`, { config });
2783
+ }
2784
+ async getWorkspaceModelConfig(workspaceId) {
2785
+ return this.request("GET", `/workspaces/${workspaceId}/model-config`);
2786
+ }
2787
+ async getModelCatalog() {
2788
+ return this.request("GET", "/model-catalog");
2789
+ }
2727
2790
  async listProjects(workspaceId) {
2728
2791
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
2729
2792
  }
@@ -2872,6 +2935,9 @@ class HarmonyApiClient {
2872
2935
  title
2873
2936
  });
2874
2937
  }
2938
+ async removeExternalLink(cardId, linkId) {
2939
+ return this.request("DELETE", `/cards/${cardId}/external-links/${linkId}`);
2940
+ }
2875
2941
  async uploadArtifact(data) {
2876
2942
  return this.request("POST", "/artifacts", data);
2877
2943
  }
@@ -2964,7 +3030,10 @@ class HarmonyApiClient {
2964
3030
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
2965
3031
  }
2966
3032
  async appendAgentRunEvents(cardId, data) {
2967
- return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
3033
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, {
3034
+ ...data,
3035
+ events: data.events.map((event) => sanitizeRunEventDraft(event))
3036
+ });
2968
3037
  }
2969
3038
  async getPendingUserMessages(cardId, sessionId, sinceSeq) {
2970
3039
  return this.request("GET", `/cards/${cardId}/agent-messages?sessionId=${sessionId}&sinceSeq=${sinceSeq}`);
@@ -5654,6 +5723,23 @@ var TOOLS = {
5654
5723
  required: ["cardId", "url"]
5655
5724
  }
5656
5725
  },
5726
+ harmony_remove_external_link: {
5727
+ description: "Remove an external reference URL from a card — the counterpart to harmony_add_external_link. Takes the link id from harmony_get_card_external_links, not the URL.",
5728
+ inputSchema: {
5729
+ type: "object",
5730
+ properties: {
5731
+ cardId: {
5732
+ type: "string",
5733
+ description: "Card UUID"
5734
+ },
5735
+ linkId: {
5736
+ type: "string",
5737
+ description: "External link UUID, as returned by harmony_get_card_external_links"
5738
+ }
5739
+ },
5740
+ required: ["cardId", "linkId"]
5741
+ }
5742
+ },
5657
5743
  harmony_create_subtask: {
5658
5744
  description: "Create a subtask on a card",
5659
5745
  inputSchema: {
@@ -7548,6 +7634,11 @@ ${list}
7548
7634
  const result = await client3.addExternalLink(cardId, url, title);
7549
7635
  return { success: true, ...result };
7550
7636
  }
7637
+ case "harmony_remove_external_link": {
7638
+ const cardId = z.string().uuid().parse(args.cardId);
7639
+ const linkId = z.string().uuid().parse(args.linkId);
7640
+ return await client3.removeExternalLink(cardId, linkId);
7641
+ }
7551
7642
  case "harmony_classify_card":
7552
7643
  return deprecatedRemovedToolResult("harmony_classify_card");
7553
7644
  case "harmony_create_subtask": {
package/dist/index.js CHANGED
@@ -31,7 +31,7 @@ function noteLegacyLocalPin(path) {
31
31
  if (warnedLegacyLocalPin)
32
32
  return;
33
33
  warnedLegacyLocalPin = true;
34
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
34
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
35
35
  }
36
36
  function noteLocalPinRename(from, to) {
37
37
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
@@ -1987,6 +1987,60 @@ var REVIEW_DISALLOWED_TOOLS = [
1987
1987
  "mcp__harmony__harmony_delete_subtask",
1988
1988
  "mcp__harmony__harmony_toggle_subtask"
1989
1989
  ];
1990
+ // ../harmony-shared/dist/runEventSanitize.js
1991
+ var REPLACEMENT = "�";
1992
+ function sanitizeRunEventString(value) {
1993
+ let out = "";
1994
+ for (let i = 0;i < value.length; i++) {
1995
+ const code = value.charCodeAt(i);
1996
+ if (code === 0)
1997
+ continue;
1998
+ if (code >= 55296 && code <= 56319) {
1999
+ const next = value.charCodeAt(i + 1);
2000
+ if (next >= 56320 && next <= 57343) {
2001
+ out += value[i] + value[i + 1];
2002
+ i++;
2003
+ continue;
2004
+ }
2005
+ out += REPLACEMENT;
2006
+ continue;
2007
+ }
2008
+ if (code >= 56320 && code <= 57343) {
2009
+ out += REPLACEMENT;
2010
+ continue;
2011
+ }
2012
+ out += value[i];
2013
+ }
2014
+ return out;
2015
+ }
2016
+ function sanitizeRunEventPayload(payload) {
2017
+ return walk(payload, new Map);
2018
+ }
2019
+ function sanitizeRunEventDraft(draft) {
2020
+ return { ...draft, payload: sanitizeRunEventPayload(draft.payload) };
2021
+ }
2022
+ function walk(value, seen) {
2023
+ if (typeof value === "string")
2024
+ return sanitizeRunEventString(value);
2025
+ if (value === null || typeof value !== "object")
2026
+ return value;
2027
+ const already = seen.get(value);
2028
+ if (already !== undefined)
2029
+ return already;
2030
+ if (Array.isArray(value)) {
2031
+ const out2 = [];
2032
+ seen.set(value, out2);
2033
+ for (const entry of value)
2034
+ out2.push(walk(entry, seen));
2035
+ return out2;
2036
+ }
2037
+ const out = {};
2038
+ seen.set(value, out);
2039
+ for (const [key, entry] of Object.entries(value)) {
2040
+ out[sanitizeRunEventString(key)] = walk(entry, seen);
2041
+ }
2042
+ return out;
2043
+ }
1990
2044
  // ../harmony-shared/dist/runRedaction.js
1991
2045
  var MAX_INPUT_CHARS = 2000;
1992
2046
  var MAX_OUTPUT_CHARS = 4000;
@@ -2507,6 +2561,15 @@ class HarmonyApiClient {
2507
2561
  async registerWorkspaceAgent(workspaceId, data) {
2508
2562
  return this.request("POST", `/workspaces/${workspaceId}/agents`, data);
2509
2563
  }
2564
+ async reportAgentConfig(workspaceId, agentId, config) {
2565
+ return this.request("POST", `/workspaces/${workspaceId}/agents/${agentId}/reported-config`, { config });
2566
+ }
2567
+ async getWorkspaceModelConfig(workspaceId) {
2568
+ return this.request("GET", `/workspaces/${workspaceId}/model-config`);
2569
+ }
2570
+ async getModelCatalog() {
2571
+ return this.request("GET", "/model-catalog");
2572
+ }
2510
2573
  async listProjects(workspaceId) {
2511
2574
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
2512
2575
  }
@@ -2655,6 +2718,9 @@ class HarmonyApiClient {
2655
2718
  title
2656
2719
  });
2657
2720
  }
2721
+ async removeExternalLink(cardId, linkId) {
2722
+ return this.request("DELETE", `/cards/${cardId}/external-links/${linkId}`);
2723
+ }
2658
2724
  async uploadArtifact(data) {
2659
2725
  return this.request("POST", "/artifacts", data);
2660
2726
  }
@@ -2747,7 +2813,10 @@ class HarmonyApiClient {
2747
2813
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
2748
2814
  }
2749
2815
  async appendAgentRunEvents(cardId, data) {
2750
- return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
2816
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, {
2817
+ ...data,
2818
+ events: data.events.map((event) => sanitizeRunEventDraft(event))
2819
+ });
2751
2820
  }
2752
2821
  async getPendingUserMessages(cardId, sessionId, sinceSeq) {
2753
2822
  return this.request("GET", `/cards/${cardId}/agent-messages?sessionId=${sessionId}&sinceSeq=${sinceSeq}`);
@@ -5437,6 +5506,23 @@ var TOOLS = {
5437
5506
  required: ["cardId", "url"]
5438
5507
  }
5439
5508
  },
5509
+ harmony_remove_external_link: {
5510
+ description: "Remove an external reference URL from a card — the counterpart to harmony_add_external_link. Takes the link id from harmony_get_card_external_links, not the URL.",
5511
+ inputSchema: {
5512
+ type: "object",
5513
+ properties: {
5514
+ cardId: {
5515
+ type: "string",
5516
+ description: "Card UUID"
5517
+ },
5518
+ linkId: {
5519
+ type: "string",
5520
+ description: "External link UUID, as returned by harmony_get_card_external_links"
5521
+ }
5522
+ },
5523
+ required: ["cardId", "linkId"]
5524
+ }
5525
+ },
5440
5526
  harmony_create_subtask: {
5441
5527
  description: "Create a subtask on a card",
5442
5528
  inputSchema: {
@@ -7331,6 +7417,11 @@ ${list}
7331
7417
  const result = await client3.addExternalLink(cardId, url, title);
7332
7418
  return { success: true, ...result };
7333
7419
  }
7420
+ case "harmony_remove_external_link": {
7421
+ const cardId = z.string().uuid().parse(args.cardId);
7422
+ const linkId = z.string().uuid().parse(args.linkId);
7423
+ return await client3.removeExternalLink(cardId, linkId);
7424
+ }
7334
7425
  case "harmony_classify_card":
7335
7426
  return deprecatedRemovedToolResult("harmony_classify_card");
7336
7427
  case "harmony_create_subtask": {
@@ -32,7 +32,7 @@ function noteLegacyLocalPin(path) {
32
32
  if (warnedLegacyLocalPin)
33
33
  return;
34
34
  warnedLegacyLocalPin = true;
35
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
35
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
36
36
  }
37
37
  function noteLocalPinRename(from, to) {
38
38
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
@@ -1014,6 +1014,60 @@ var REVIEW_DISALLOWED_TOOLS = [
1014
1014
  "mcp__harmony__harmony_delete_subtask",
1015
1015
  "mcp__harmony__harmony_toggle_subtask"
1016
1016
  ];
1017
+ // ../harmony-shared/dist/runEventSanitize.js
1018
+ var REPLACEMENT = "�";
1019
+ function sanitizeRunEventString(value) {
1020
+ let out = "";
1021
+ for (let i = 0;i < value.length; i++) {
1022
+ const code = value.charCodeAt(i);
1023
+ if (code === 0)
1024
+ continue;
1025
+ if (code >= 55296 && code <= 56319) {
1026
+ const next = value.charCodeAt(i + 1);
1027
+ if (next >= 56320 && next <= 57343) {
1028
+ out += value[i] + value[i + 1];
1029
+ i++;
1030
+ continue;
1031
+ }
1032
+ out += REPLACEMENT;
1033
+ continue;
1034
+ }
1035
+ if (code >= 56320 && code <= 57343) {
1036
+ out += REPLACEMENT;
1037
+ continue;
1038
+ }
1039
+ out += value[i];
1040
+ }
1041
+ return out;
1042
+ }
1043
+ function sanitizeRunEventPayload(payload) {
1044
+ return walk(payload, new Map);
1045
+ }
1046
+ function sanitizeRunEventDraft(draft) {
1047
+ return { ...draft, payload: sanitizeRunEventPayload(draft.payload) };
1048
+ }
1049
+ function walk(value, seen) {
1050
+ if (typeof value === "string")
1051
+ return sanitizeRunEventString(value);
1052
+ if (value === null || typeof value !== "object")
1053
+ return value;
1054
+ const already = seen.get(value);
1055
+ if (already !== undefined)
1056
+ return already;
1057
+ if (Array.isArray(value)) {
1058
+ const out2 = [];
1059
+ seen.set(value, out2);
1060
+ for (const entry of value)
1061
+ out2.push(walk(entry, seen));
1062
+ return out2;
1063
+ }
1064
+ const out = {};
1065
+ seen.set(value, out);
1066
+ for (const [key, entry] of Object.entries(value)) {
1067
+ out[sanitizeRunEventString(key)] = walk(entry, seen);
1068
+ }
1069
+ return out;
1070
+ }
1017
1071
  // ../harmony-shared/dist/runRedaction.js
1018
1072
  var REDACTION_MARK = "«redacted»";
1019
1073
  var CONFIG_SCOPED_SEGMENTS = new Set([
@@ -1395,6 +1449,15 @@ class HarmonyApiClient {
1395
1449
  async registerWorkspaceAgent(workspaceId, data) {
1396
1450
  return this.request("POST", `/workspaces/${workspaceId}/agents`, data);
1397
1451
  }
1452
+ async reportAgentConfig(workspaceId, agentId, config) {
1453
+ return this.request("POST", `/workspaces/${workspaceId}/agents/${agentId}/reported-config`, { config });
1454
+ }
1455
+ async getWorkspaceModelConfig(workspaceId) {
1456
+ return this.request("GET", `/workspaces/${workspaceId}/model-config`);
1457
+ }
1458
+ async getModelCatalog() {
1459
+ return this.request("GET", "/model-catalog");
1460
+ }
1398
1461
  async listProjects(workspaceId) {
1399
1462
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
1400
1463
  }
@@ -1543,6 +1606,9 @@ class HarmonyApiClient {
1543
1606
  title
1544
1607
  });
1545
1608
  }
1609
+ async removeExternalLink(cardId, linkId) {
1610
+ return this.request("DELETE", `/cards/${cardId}/external-links/${linkId}`);
1611
+ }
1546
1612
  async uploadArtifact(data) {
1547
1613
  return this.request("POST", "/artifacts", data);
1548
1614
  }
@@ -1635,7 +1701,10 @@ class HarmonyApiClient {
1635
1701
  return this.request("DELETE", `/cards/${cardId}/agent-context`, data);
1636
1702
  }
1637
1703
  async appendAgentRunEvents(cardId, data) {
1638
- return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
1704
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, {
1705
+ ...data,
1706
+ events: data.events.map((event) => sanitizeRunEventDraft(event))
1707
+ });
1639
1708
  }
1640
1709
  async getPendingUserMessages(cardId, sessionId, sinceSeq) {
1641
1710
  return this.request("GET", `/cards/${cardId}/agent-messages?sessionId=${sessionId}&sinceSeq=${sinceSeq}`);
@@ -32,7 +32,7 @@ function noteLegacyLocalPin(path) {
32
32
  if (warnedLegacyLocalPin)
33
33
  return;
34
34
  warnedLegacyLocalPin = true;
35
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
35
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
36
36
  }
37
37
  function noteLocalPinRename(from, to) {
38
38
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
@@ -32,7 +32,7 @@ function noteLegacyLocalPin(path) {
32
32
  if (warnedLegacyLocalPin)
33
33
  return;
34
34
  warnedLegacyLocalPin = true;
35
- console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`);
35
+ console.error(`Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` + `Run \`harmony-agent doctor --fix\` in this repo to write ` + `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` + `The fallback that finds it is temporary.`);
36
36
  }
37
37
  function noteLocalPinRename(from, to) {
38
38
  console.error(`Harmony: wrote the repo pin to ${to} (the pre-#1082 ${from} is now ignored and can be deleted).`);
@@ -588,6 +588,60 @@ var REVIEW_DISALLOWED_TOOLS = [
588
588
  "mcp__harmony__harmony_delete_subtask",
589
589
  "mcp__harmony__harmony_toggle_subtask"
590
590
  ];
591
+ // ../harmony-shared/dist/runEventSanitize.js
592
+ var REPLACEMENT = "�";
593
+ function sanitizeRunEventString(value) {
594
+ let out = "";
595
+ for (let i = 0;i < value.length; i++) {
596
+ const code = value.charCodeAt(i);
597
+ if (code === 0)
598
+ continue;
599
+ if (code >= 55296 && code <= 56319) {
600
+ const next = value.charCodeAt(i + 1);
601
+ if (next >= 56320 && next <= 57343) {
602
+ out += value[i] + value[i + 1];
603
+ i++;
604
+ continue;
605
+ }
606
+ out += REPLACEMENT;
607
+ continue;
608
+ }
609
+ if (code >= 56320 && code <= 57343) {
610
+ out += REPLACEMENT;
611
+ continue;
612
+ }
613
+ out += value[i];
614
+ }
615
+ return out;
616
+ }
617
+ function sanitizeRunEventPayload(payload) {
618
+ return walk(payload, new Map);
619
+ }
620
+ function sanitizeRunEventDraft(draft) {
621
+ return { ...draft, payload: sanitizeRunEventPayload(draft.payload) };
622
+ }
623
+ function walk(value, seen) {
624
+ if (typeof value === "string")
625
+ return sanitizeRunEventString(value);
626
+ if (value === null || typeof value !== "object")
627
+ return value;
628
+ const already = seen.get(value);
629
+ if (already !== undefined)
630
+ return already;
631
+ if (Array.isArray(value)) {
632
+ const out2 = [];
633
+ seen.set(value, out2);
634
+ for (const entry of value)
635
+ out2.push(walk(entry, seen));
636
+ return out2;
637
+ }
638
+ const out = {};
639
+ seen.set(value, out);
640
+ for (const [key, entry] of Object.entries(value)) {
641
+ out[sanitizeRunEventString(key)] = walk(entry, seen);
642
+ }
643
+ return out;
644
+ }
591
645
  // ../harmony-shared/dist/runRedaction.js
592
646
  var MAX_INPUT_CHARS = 2000;
593
647
  var MAX_OUTPUT_CHARS = 4000;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "3.7.0",
4
- "description": "MCP server for Harmony, the shared surface for human–agent teams agents claim cards, report progress, and move work on your board.",
3
+ "version": "3.8.0",
4
+ "description": "MCP server for Harmony, the shared surface for human\u2013agent teams \u2014 agents claim cards, report progress, and move work on your board.",
5
5
  "publishConfig": {
6
6
  "access": "public"
7
7
  },
package/src/api-client.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  type PlaybookVersionDef,
7
7
  type StageGateEvidenceInsert,
8
8
  type StageGateEvidenceRow,
9
+ sanitizeRunEventDraft,
9
10
  serializeCommentThread,
10
11
  untrustedDataBlock,
11
12
  type WorkspaceAgent,
@@ -19,6 +20,16 @@ export interface ApiResponse<T = unknown> {
19
20
  [key: string]: T | boolean | string | undefined;
20
21
  }
21
22
 
23
+ /** One `model_catalog` row (#1104). See `getModelCatalog`. */
24
+ export interface ModelCatalogRow {
25
+ id: string;
26
+ display_name?: string | null;
27
+ status: "available" | "deprecated" | "withdrawn";
28
+ replaced_by?: string | null;
29
+ sort_order?: number;
30
+ notes?: string | null;
31
+ }
32
+
22
33
  // Retry configuration
23
34
  const RETRY_CONFIG = {
24
35
  maxRetries: 3,
@@ -559,6 +570,46 @@ export class HarmonyApiClient {
559
570
  return this.request("POST", `/workspaces/${workspaceId}/agents`, data);
560
571
  }
561
572
 
573
+ /**
574
+ * Report the effective model config this daemon resolved (#1104). Advisory:
575
+ * the board renders it, nothing routes on it, so a failure here must never
576
+ * stop a daemon from starting.
577
+ */
578
+ async reportAgentConfig(
579
+ workspaceId: string,
580
+ agentId: string,
581
+ config: unknown,
582
+ ): Promise<{ agent: WorkspaceAgent }> {
583
+ return this.request(
584
+ "POST",
585
+ `/workspaces/${workspaceId}/agents/${agentId}/reported-config`,
586
+ { config },
587
+ );
588
+ }
589
+
590
+ /**
591
+ * The workspace's own model layer (#1104) — `GET /v1/workspaces/:id/model-config`.
592
+ * `config` is `null` when the workspace has not set one. Deliberately untyped
593
+ * (`Record<string, unknown>`, not `WorkspaceModelConfig`): this client has no
594
+ * dependency on `@gethmy/agent`, so the caller (`workspace-model-cache.ts`)
595
+ * parses the blob into its own shape.
596
+ */
597
+ async getWorkspaceModelConfig(
598
+ workspaceId: string,
599
+ ): Promise<{ config: Record<string, unknown> | null }> {
600
+ return this.request("GET", `/workspaces/${workspaceId}/model-config`);
601
+ }
602
+
603
+ /**
604
+ * The platform-owned supported-model catalog (#1104) — `GET /v1/model-catalog`.
605
+ * Open to any authenticated caller; a model id is not a secret. The daemon
606
+ * derives a withdrawn→replacement map from the `withdrawn` rows here and
607
+ * hands it to `chooseImplementModel`'s `catalog` parameter.
608
+ */
609
+ async getModelCatalog(): Promise<{ models: ModelCatalogRow[] }> {
610
+ return this.request("GET", "/model-catalog");
611
+ }
612
+
562
613
  // ============ PROJECT OPERATIONS ============
563
614
 
564
615
  async listProjects(workspaceId: string): Promise<{ projects: unknown[] }> {
@@ -924,6 +975,13 @@ export class HarmonyApiClient {
924
975
  });
925
976
  }
926
977
 
978
+ async removeExternalLink(
979
+ cardId: string,
980
+ linkId: string,
981
+ ): Promise<{ success: boolean }> {
982
+ return this.request("DELETE", `/cards/${cardId}/external-links/${linkId}`);
983
+ }
984
+
927
985
  // ============ ARTIFACTS (hosted HTML documents) ============
928
986
 
929
987
  async uploadArtifact(data: {
@@ -1409,6 +1467,26 @@ export class HarmonyApiClient {
1409
1467
  /**
1410
1468
  * Append events to a run's agent_run_events stream (card #417). Send drafts in
1411
1469
  * chronological order — the server's seq trigger assigns the monotonic per-run order.
1470
+ *
1471
+ * **Every draft is sanitized here, because this is the one place all three
1472
+ * writers meet (#1110).** `agent_run_events.payload` is JSONB, and Postgres
1473
+ * refuses a JSON string carrying U+0000 or a lone surrogate — it rejects the
1474
+ * whole statement rather than truncating. Measured 75 times in the 2026-09-06
1475
+ * daemon log as `unsupported Unicode escape sequence`, and tool output is
1476
+ * exactly where such a byte comes from (a `grep` over a binary, a compiler
1477
+ * dumping a fixture).
1478
+ *
1479
+ * Three callers reach this method and they fail in different ways, which is
1480
+ * why the repair belongs here rather than at any one of them:
1481
+ * - `CliAgentRunner.enqueue` (harmony-agent) retries three times, bisects,
1482
+ * and drops the single bad event — correct handling, still a lost row.
1483
+ * - `RunEventForwarder.flush` (the MCP `PostToolUse` hook) leaves the batch
1484
+ * on disk and retries with backoff for ~10 minutes, BLOCKING every later
1485
+ * tool call queued behind it, then drops the pair.
1486
+ * - `ci-repair.ts` posts a `detail` built from CI output.
1487
+ * The runner sanitizes again on its own path, above its size check, so a NUL
1488
+ * never costs bytes against the ceiling; that is defence in depth, not a
1489
+ * duplicate — this is the boundary a fourth writer gets for free.
1412
1490
  */
1413
1491
  async appendAgentRunEvents(
1414
1492
  cardId: string,
@@ -1417,7 +1495,10 @@ export class HarmonyApiClient {
1417
1495
  events: (AgentRunEventDraft & { createdAt?: string })[];
1418
1496
  },
1419
1497
  ): Promise<{ inserted: number }> {
1420
- return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
1498
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, {
1499
+ ...data,
1500
+ events: data.events.map((event) => sanitizeRunEventDraft(event)),
1501
+ });
1421
1502
  }
1422
1503
 
1423
1504
  /**
package/src/config.ts CHANGED
@@ -105,13 +105,30 @@ function noteLegacyConfigDir(path: string): void {
105
105
  );
106
106
  }
107
107
 
108
- /** Said once when a repo pin is read under the old name. */
108
+ /**
109
+ * Said once when a repo pin is read under the old name.
110
+ *
111
+ * It names the COMMAND, not the outcome (#1108). "Rename it to `.hmy.json`"
112
+ * described a result and left every operator doing it by hand in every repo —
113
+ * eight of them on the author's machine, none migrated — while
114
+ * `harmony-agent doctor --fix` had to be found by reading the source. A notice
115
+ * for a temporary fallback is worth nothing if acting on it is the hard part.
116
+ *
117
+ * The manual rename stays beside it, for two readers the command does not
118
+ * serve: `harmony-agent` is the bin of `@gethmy/agent`, so a client running
119
+ * `@gethmy/mcp` alone does not have it — and an INSTALLED agent older than
120
+ * #1108 accepts the unknown `--fix`, prints `preflight ok` and exits 0 without
121
+ * migrating anything, which is a false green the notice must not lead a reader
122
+ * into trusting.
123
+ */
109
124
  export function noteLegacyLocalPin(path: string): void {
110
125
  if (warnedLegacyLocalPin) return;
111
126
  warnedLegacyLocalPin = true;
112
127
  console.error(
113
128
  `Harmony: this repo is pinned by ${path}, the pre-#1082 name. ` +
114
- `Rename it to ${LOCAL_CONFIG_FILENAME} the fallback that finds it is temporary.`,
129
+ `Run \`harmony-agent doctor --fix\` in this repo to write ` +
130
+ `${LOCAL_CONFIG_FILENAME}, or rename the file yourself. ` +
131
+ `The fallback that finds it is temporary.`,
115
132
  );
116
133
  }
117
134
 
package/src/server.ts CHANGED
@@ -1299,6 +1299,25 @@ export const TOOLS = {
1299
1299
  required: ["cardId", "url"],
1300
1300
  },
1301
1301
  },
1302
+ harmony_remove_external_link: {
1303
+ description:
1304
+ "Remove an external reference URL from a card — the counterpart to harmony_add_external_link. Takes the link id from harmony_get_card_external_links, not the URL.",
1305
+ inputSchema: {
1306
+ type: "object",
1307
+ properties: {
1308
+ cardId: {
1309
+ type: "string",
1310
+ description: "Card UUID",
1311
+ },
1312
+ linkId: {
1313
+ type: "string",
1314
+ description:
1315
+ "External link UUID, as returned by harmony_get_card_external_links",
1316
+ },
1317
+ },
1318
+ required: ["cardId", "linkId"],
1319
+ },
1320
+ },
1302
1321
 
1303
1322
  // Subtask operations
1304
1323
  harmony_create_subtask: {
@@ -3775,6 +3794,14 @@ export async function handleToolCall(
3775
3794
  return { success: true, ...result };
3776
3795
  }
3777
3796
 
3797
+ case "harmony_remove_external_link": {
3798
+ const cardId = z.string().uuid().parse(args.cardId);
3799
+ const linkId = z.string().uuid().parse(args.linkId);
3800
+ // The route already answers `{ success: true }`, so this returns it
3801
+ // as-is rather than re-spreading a second `success` over it.
3802
+ return await client.removeExternalLink(cardId, linkId);
3803
+ }
3804
+
3778
3805
  // Removed — dropped from the advertised TOOLS list; the case remains so an
3779
3806
  // older `hmy-new` install that still calls it gets a legible notice rather
3780
3807
  // than an unknown-tool error.