@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 +2 -2
- package/dist/cli.js +93 -2
- package/dist/index.js +93 -2
- package/dist/lib/api-client.js +71 -2
- package/dist/lib/config.js +1 -1
- package/dist/lib/oauth-refresh.js +1 -1
- package/dist/run-hook-cli.js +54 -0
- package/package.json +2 -2
- package/src/api-client.ts +82 -1
- package/src/config.ts +19 -2
- package/src/server.ts +27 -0
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
|
-
- **
|
|
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
|
|
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. ` + `
|
|
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`,
|
|
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. ` + `
|
|
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`,
|
|
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": {
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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. ` + `
|
|
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`,
|
|
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}`);
|
package/dist/lib/config.js
CHANGED
|
@@ -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. ` + `
|
|
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. ` + `
|
|
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).`);
|
package/dist/run-hook-cli.js
CHANGED
|
@@ -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.
|
|
4
|
-
"description": "MCP server for Harmony, the shared surface for human
|
|
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`,
|
|
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
|
-
/**
|
|
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
|
-
`
|
|
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.
|