@gethmy/mcp 2.8.0 → 2.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +617 -276
- package/dist/index.js +525 -5
- package/dist/lib/api-client.js +121 -0
- package/package.json +2 -2
- package/src/api-client.ts +66 -1
- package/src/cli.ts +2 -2
- package/src/hmy-config.ts +70 -0
- package/src/server.ts +158 -3
- package/src/skills.ts +115 -19
- package/src/tui/setup.ts +8 -8
package/dist/lib/api-client.js
CHANGED
|
@@ -724,6 +724,87 @@ function getDisplayLinkType(linkType, direction) {
|
|
|
724
724
|
return linkType;
|
|
725
725
|
return LINK_TYPE_INVERSES[linkType];
|
|
726
726
|
}
|
|
727
|
+
// ../harmony-shared/dist/commentSerializer.js
|
|
728
|
+
var CONFLICT_INSTRUCTION = "When two comments conflict, prefer the latest created_at, UNLESS a later " + "comment explicitly confirms or restates the earlier finding. Evaluate " + "substance, not just recency. Cite the comment id(s) you relied on.";
|
|
729
|
+
function authorLabel(c) {
|
|
730
|
+
if (c.author_type === "agent")
|
|
731
|
+
return "AI agent";
|
|
732
|
+
return c.author?.full_name || c.author?.email || "teammate";
|
|
733
|
+
}
|
|
734
|
+
function criticalIds(comments) {
|
|
735
|
+
const keep = new Set;
|
|
736
|
+
for (const c of comments) {
|
|
737
|
+
if (c.comment_type === "decision")
|
|
738
|
+
keep.add(c.id);
|
|
739
|
+
if (c.supersedes_id) {
|
|
740
|
+
keep.add(c.id);
|
|
741
|
+
keep.add(c.supersedes_id);
|
|
742
|
+
}
|
|
743
|
+
if (c.confirms_id) {
|
|
744
|
+
keep.add(c.id);
|
|
745
|
+
keep.add(c.confirms_id);
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return keep;
|
|
749
|
+
}
|
|
750
|
+
function serializeCommentThread(comments, options = {}) {
|
|
751
|
+
const { heading = "Conversation", includeInstructions = true, activity = [], maxComments } = options;
|
|
752
|
+
const visible = comments.filter((c) => !c.deleted_at).slice().sort((a, b) => a.created_at.localeCompare(b.created_at));
|
|
753
|
+
if (visible.length === 0)
|
|
754
|
+
return "";
|
|
755
|
+
const indexById = new Map;
|
|
756
|
+
visible.forEach((c, i) => {
|
|
757
|
+
indexById.set(c.id, i + 1);
|
|
758
|
+
});
|
|
759
|
+
let rendered = visible;
|
|
760
|
+
let elidedCount = 0;
|
|
761
|
+
if (maxComments && visible.length > maxComments) {
|
|
762
|
+
const keep = criticalIds(visible);
|
|
763
|
+
const recentThreshold = visible.length - maxComments;
|
|
764
|
+
rendered = visible.filter((c, i) => i >= recentThreshold || keep.has(c.id));
|
|
765
|
+
elidedCount = visible.length - rendered.length;
|
|
766
|
+
}
|
|
767
|
+
const ref = (id) => {
|
|
768
|
+
const n = indexById.get(id);
|
|
769
|
+
return n ? `#${n}` : `#${id.slice(0, 8)}`;
|
|
770
|
+
};
|
|
771
|
+
const lines = [];
|
|
772
|
+
if (elidedCount > 0) {
|
|
773
|
+
lines.push({
|
|
774
|
+
at: visible[0]?.created_at ?? "",
|
|
775
|
+
text: `(${elidedCount} earlier comment(s) omitted for brevity)`
|
|
776
|
+
});
|
|
777
|
+
}
|
|
778
|
+
for (const c of rendered) {
|
|
779
|
+
const tags = [];
|
|
780
|
+
if (c.edited_at)
|
|
781
|
+
tags.push("edited");
|
|
782
|
+
if (c.supersedes_id)
|
|
783
|
+
tags.push(`supersedes ${ref(c.supersedes_id)}`);
|
|
784
|
+
if (c.confirms_id)
|
|
785
|
+
tags.push(`confirms ${ref(c.confirms_id)}`);
|
|
786
|
+
if (c.resolved_at)
|
|
787
|
+
tags.push("resolved");
|
|
788
|
+
const tagStr = tags.length ? ` | ${tags.join(" | ")}` : "";
|
|
789
|
+
const header = `[${ref(c.id)} | ${c.author_type} | ${authorLabel(c)} | ${c.comment_type} | ${c.created_at}${tagStr}]`;
|
|
790
|
+
lines.push({ at: c.created_at, text: `${header}
|
|
791
|
+
${c.body.trim()}` });
|
|
792
|
+
}
|
|
793
|
+
for (const a of activity) {
|
|
794
|
+
const actor = a.actor ? `${a.actor} ` : "";
|
|
795
|
+
lines.push({ at: a.at, text: `· (system) ${a.at} — ${actor}${a.text}` });
|
|
796
|
+
}
|
|
797
|
+
lines.sort((a, b) => a.at.localeCompare(b.at));
|
|
798
|
+
const body = lines.map((l) => l.text).join(`
|
|
799
|
+
|
|
800
|
+
`);
|
|
801
|
+
const instruction = includeInstructions ? `
|
|
802
|
+
|
|
803
|
+
${CONFLICT_INSTRUCTION}` : "";
|
|
804
|
+
return `## ${heading} (oldest → newest)
|
|
805
|
+
|
|
806
|
+
${body}${instruction}`;
|
|
807
|
+
}
|
|
727
808
|
// ../harmony-shared/dist/constants.js
|
|
728
809
|
var TIMINGS = {
|
|
729
810
|
SEARCH_DEBOUNCE: 300,
|
|
@@ -1076,6 +1157,28 @@ class HarmonyApiClient {
|
|
|
1076
1157
|
async deleteSubtask(subtaskId) {
|
|
1077
1158
|
return this.request("DELETE", `/subtasks/${subtaskId}`);
|
|
1078
1159
|
}
|
|
1160
|
+
async addComment(cardId, body, opts) {
|
|
1161
|
+
return this.request("POST", `/cards/${cardId}/comments`, {
|
|
1162
|
+
body,
|
|
1163
|
+
authorType: "agent",
|
|
1164
|
+
commentType: opts?.commentType,
|
|
1165
|
+
supersedesId: opts?.supersedesId,
|
|
1166
|
+
confirmsId: opts?.confirmsId,
|
|
1167
|
+
agentSessionId: opts?.agentSessionId
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
async getComments(cardId, opts) {
|
|
1171
|
+
const qs = new URLSearchParams;
|
|
1172
|
+
if (opts?.limit != null)
|
|
1173
|
+
qs.set("limit", String(opts.limit));
|
|
1174
|
+
if (opts?.offset != null)
|
|
1175
|
+
qs.set("offset", String(opts.offset));
|
|
1176
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
1177
|
+
return this.request("GET", `/cards/${cardId}/comments${suffix}`);
|
|
1178
|
+
}
|
|
1179
|
+
async updateComment(commentId, updates) {
|
|
1180
|
+
return this.request("PATCH", `/comments/${commentId}`, updates);
|
|
1181
|
+
}
|
|
1079
1182
|
async startAgentSession(cardId, data) {
|
|
1080
1183
|
return this.request("POST", `/cards/${cardId}/agent-context`, data);
|
|
1081
1184
|
}
|
|
@@ -1433,6 +1536,24 @@ class HarmonyApiClient {
|
|
|
1433
1536
|
assembledContext: assembledContextStr,
|
|
1434
1537
|
assemblyId
|
|
1435
1538
|
});
|
|
1539
|
+
try {
|
|
1540
|
+
const { comments } = await this.getComments(options.cardId, {
|
|
1541
|
+
limit: 200
|
|
1542
|
+
});
|
|
1543
|
+
if (Array.isArray(comments) && comments.length > 0) {
|
|
1544
|
+
const section = serializeCommentThread(comments, {
|
|
1545
|
+
heading: "Comments",
|
|
1546
|
+
maxComments: 40
|
|
1547
|
+
});
|
|
1548
|
+
if (section)
|
|
1549
|
+
result.prompt = `${result.prompt}
|
|
1550
|
+
|
|
1551
|
+
${section}`;
|
|
1552
|
+
}
|
|
1553
|
+
} catch (err) {
|
|
1554
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1555
|
+
console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
|
|
1556
|
+
}
|
|
1436
1557
|
try {
|
|
1437
1558
|
await this.recordPromptHistory({
|
|
1438
1559
|
cardId: cardData.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gethmy/mcp",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.2",
|
|
4
4
|
"description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"serve:remote": "bun src/remote.ts",
|
|
61
61
|
"dev": "bun --watch src/index.ts",
|
|
62
62
|
"test": "bun run test:unit && bun run test:integration",
|
|
63
|
-
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts",
|
|
63
|
+
"test:unit": "bun test src/__tests__/active-learning.test.ts src/__tests__/context-assembly.test.ts src/__tests__/prompt-builder.test.ts src/__tests__/memory-audit.test.ts src/__tests__/skills.test.ts src/__tests__/hmy-config.test.ts src/__tests__/tool-dispatch.test.ts src/__tests__/mcp-integration.test.ts",
|
|
64
64
|
"test:integration": "bun test src/__tests__/integration-memory-system.test.ts src/__tests__/integration-memory-crud.test.ts",
|
|
65
65
|
"typecheck": "tsc --noEmit",
|
|
66
66
|
"prepublishOnly": "bun run build"
|
package/src/api-client.ts
CHANGED
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
type Comment,
|
|
3
|
+
getDisplayLinkType,
|
|
4
|
+
serializeCommentThread,
|
|
5
|
+
} from "@harmony/shared";
|
|
2
6
|
import { getApiKey, getApiUrl } from "./config.js";
|
|
3
7
|
|
|
4
8
|
export interface ApiResponse<T = unknown> {
|
|
@@ -454,6 +458,7 @@ export class HarmonyApiClient {
|
|
|
454
458
|
description?: string;
|
|
455
459
|
priority?: string;
|
|
456
460
|
assigneeId?: string;
|
|
461
|
+
planId?: string;
|
|
457
462
|
},
|
|
458
463
|
): Promise<{ card: unknown }> {
|
|
459
464
|
return this.request("POST", "/cards", { projectId, ...data });
|
|
@@ -616,6 +621,46 @@ export class HarmonyApiClient {
|
|
|
616
621
|
return this.request("DELETE", `/subtasks/${subtaskId}`);
|
|
617
622
|
}
|
|
618
623
|
|
|
624
|
+
// ============ COMMENT OPERATIONS ============
|
|
625
|
+
|
|
626
|
+
async addComment(
|
|
627
|
+
cardId: string,
|
|
628
|
+
body: string,
|
|
629
|
+
opts?: {
|
|
630
|
+
commentType?: string;
|
|
631
|
+
supersedesId?: string;
|
|
632
|
+
confirmsId?: string;
|
|
633
|
+
agentSessionId?: string;
|
|
634
|
+
},
|
|
635
|
+
): Promise<{ comment: unknown }> {
|
|
636
|
+
return this.request("POST", `/cards/${cardId}/comments`, {
|
|
637
|
+
body,
|
|
638
|
+
authorType: "agent",
|
|
639
|
+
commentType: opts?.commentType,
|
|
640
|
+
supersedesId: opts?.supersedesId,
|
|
641
|
+
confirmsId: opts?.confirmsId,
|
|
642
|
+
agentSessionId: opts?.agentSessionId,
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
async getComments(
|
|
647
|
+
cardId: string,
|
|
648
|
+
opts?: { limit?: number; offset?: number },
|
|
649
|
+
): Promise<{ comments: unknown[] }> {
|
|
650
|
+
const qs = new URLSearchParams();
|
|
651
|
+
if (opts?.limit != null) qs.set("limit", String(opts.limit));
|
|
652
|
+
if (opts?.offset != null) qs.set("offset", String(opts.offset));
|
|
653
|
+
const suffix = qs.toString() ? `?${qs.toString()}` : "";
|
|
654
|
+
return this.request("GET", `/cards/${cardId}/comments${suffix}`);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
async updateComment(
|
|
658
|
+
commentId: string,
|
|
659
|
+
updates: { body?: string; pinned?: boolean; resolve?: boolean },
|
|
660
|
+
): Promise<{ comment: unknown }> {
|
|
661
|
+
return this.request("PATCH", `/comments/${commentId}`, updates);
|
|
662
|
+
}
|
|
663
|
+
|
|
619
664
|
// ============ AGENT CONTEXT OPERATIONS ============
|
|
620
665
|
|
|
621
666
|
async startAgentSession(
|
|
@@ -1447,6 +1492,26 @@ export class HarmonyApiClient {
|
|
|
1447
1492
|
assemblyId,
|
|
1448
1493
|
});
|
|
1449
1494
|
|
|
1495
|
+
// Append the card's comment thread (people + agents). This is the central
|
|
1496
|
+
// injection point: every caller of harmony_generate_prompt (daemon, Claude
|
|
1497
|
+
// Code, in-app builder) gets the recency-ordered thread + conflict rule.
|
|
1498
|
+
// Best-effort — never fail prompt generation on a comments fetch error.
|
|
1499
|
+
try {
|
|
1500
|
+
const { comments } = await this.getComments(options.cardId, {
|
|
1501
|
+
limit: 200,
|
|
1502
|
+
});
|
|
1503
|
+
if (Array.isArray(comments) && comments.length > 0) {
|
|
1504
|
+
const section = serializeCommentThread(comments as Comment[], {
|
|
1505
|
+
heading: "Comments",
|
|
1506
|
+
maxComments: 40,
|
|
1507
|
+
});
|
|
1508
|
+
if (section) result.prompt = `${result.prompt}\n\n${section}`;
|
|
1509
|
+
}
|
|
1510
|
+
} catch (err) {
|
|
1511
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1512
|
+
console.debug(`[generateCardPrompt] comments fetch failed: ${msg}`);
|
|
1513
|
+
}
|
|
1514
|
+
|
|
1450
1515
|
// AGP P2: persist a session-linked snapshot. Best-effort — never fail
|
|
1451
1516
|
// prompt generation just because logging didn't land.
|
|
1452
1517
|
try {
|
package/src/cli.ts
CHANGED
|
@@ -34,9 +34,9 @@ program
|
|
|
34
34
|
console.error("Run: npx @gethmy/mcp setup");
|
|
35
35
|
process.exit(1);
|
|
36
36
|
}
|
|
37
|
-
await refreshSkills();
|
|
37
|
+
const { updated } = await refreshSkills();
|
|
38
38
|
const server = new HarmonyMCPServer();
|
|
39
|
-
await server.run();
|
|
39
|
+
await server.run({ skillsUpdated: updated });
|
|
40
40
|
});
|
|
41
41
|
|
|
42
42
|
program
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* User-facing knobs for the skill auto-update, stored at `~/.hmy/config.yaml`.
|
|
7
|
+
*
|
|
8
|
+
* This file used to be read only by the bash auto-update preamble. Now that
|
|
9
|
+
* the MCP server owns auto-update (refreshSkills at `serve` startup), the
|
|
10
|
+
* server reads it directly. We intentionally avoid a YAML dependency: the file
|
|
11
|
+
* is a flat `key: value` list, so a tiny line parser is enough and keeps the
|
|
12
|
+
* install footprint small.
|
|
13
|
+
*/
|
|
14
|
+
export interface HmyConfig {
|
|
15
|
+
/** Master switch. `update_check: false` disables auto-update entirely. */
|
|
16
|
+
updateCheck: boolean;
|
|
17
|
+
/**
|
|
18
|
+
* Freeze to a specific skills version. When set, the server skips
|
|
19
|
+
* auto-update so the user stays on whatever is currently installed.
|
|
20
|
+
*/
|
|
21
|
+
pin: string | null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const DEFAULTS: HmyConfig = { updateCheck: true, pin: null };
|
|
25
|
+
|
|
26
|
+
export function getHmyConfigPath(): string {
|
|
27
|
+
return join(homedir(), ".hmy", "config.yaml");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Read `~/.hmy/config.yaml`, falling back to defaults on any error. */
|
|
31
|
+
export function loadHmyConfig(): HmyConfig {
|
|
32
|
+
const path = getHmyConfigPath();
|
|
33
|
+
if (!existsSync(path)) return { ...DEFAULTS };
|
|
34
|
+
try {
|
|
35
|
+
return parseHmyConfig(readFileSync(path, "utf-8"));
|
|
36
|
+
} catch {
|
|
37
|
+
return { ...DEFAULTS };
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Parse the flat `key: value` config body. Unknown keys are ignored;
|
|
43
|
+
* surrounding quotes and inline comments after a `#` are stripped. Exported
|
|
44
|
+
* for tests.
|
|
45
|
+
*/
|
|
46
|
+
export function parseHmyConfig(text: string): HmyConfig {
|
|
47
|
+
const cfg: HmyConfig = { ...DEFAULTS };
|
|
48
|
+
for (const rawLine of text.split("\n")) {
|
|
49
|
+
const line = rawLine.trim();
|
|
50
|
+
if (!line || line.startsWith("#")) continue;
|
|
51
|
+
const sep = line.indexOf(":");
|
|
52
|
+
if (sep === -1) continue;
|
|
53
|
+
const key = line.slice(0, sep).trim();
|
|
54
|
+
let value = line.slice(sep + 1).trim();
|
|
55
|
+
// Drop trailing inline comment, then surrounding quotes.
|
|
56
|
+
const hash = value.indexOf(" #");
|
|
57
|
+
if (hash !== -1) value = value.slice(0, hash).trim();
|
|
58
|
+
value = value.replace(/^["']|["']$/g, "");
|
|
59
|
+
switch (key) {
|
|
60
|
+
case "update_check":
|
|
61
|
+
cfg.updateCheck = value !== "false";
|
|
62
|
+
break;
|
|
63
|
+
case "pin":
|
|
64
|
+
case "pin_version":
|
|
65
|
+
cfg.pin = value || null;
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return cfg;
|
|
70
|
+
}
|
package/src/server.ts
CHANGED
|
@@ -48,6 +48,7 @@ import {
|
|
|
48
48
|
sessionScopeFor,
|
|
49
49
|
} from "./memory-session.js";
|
|
50
50
|
import { onboardNewUser } from "./onboard.js";
|
|
51
|
+
import { stripSkillPreamble } from "./skills.js";
|
|
51
52
|
|
|
52
53
|
/**
|
|
53
54
|
* Dependencies injected into tool handlers.
|
|
@@ -286,6 +287,11 @@ export const TOOLS = {
|
|
|
286
287
|
description: "Priority level",
|
|
287
288
|
},
|
|
288
289
|
assigneeId: { type: "string", description: "Assignee user ID" },
|
|
290
|
+
planId: {
|
|
291
|
+
type: "string",
|
|
292
|
+
description:
|
|
293
|
+
"Plan ID to link this card to (optional). Links the card to that plan via its plan_id.",
|
|
294
|
+
},
|
|
289
295
|
},
|
|
290
296
|
required: ["title"],
|
|
291
297
|
},
|
|
@@ -630,6 +636,72 @@ export const TOOLS = {
|
|
|
630
636
|
},
|
|
631
637
|
},
|
|
632
638
|
|
|
639
|
+
// Comment operations
|
|
640
|
+
harmony_add_comment: {
|
|
641
|
+
description:
|
|
642
|
+
"Post a comment on a card as the agent. Use this to converse with the human in the open: report progress, ask a question, record a decision, or note a finding — instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one. When the thread conflicts, prefer the latest comment unless a later one confirms an earlier finding; cite the comment id(s) you relied on.",
|
|
643
|
+
inputSchema: {
|
|
644
|
+
type: "object",
|
|
645
|
+
properties: {
|
|
646
|
+
cardId: { type: "string", description: "Card UUID to comment on" },
|
|
647
|
+
body: { type: "string", description: "Comment body (Markdown)" },
|
|
648
|
+
commentType: {
|
|
649
|
+
type: "string",
|
|
650
|
+
enum: [
|
|
651
|
+
"message",
|
|
652
|
+
"progress",
|
|
653
|
+
"question",
|
|
654
|
+
"blocker",
|
|
655
|
+
"decision",
|
|
656
|
+
"summary",
|
|
657
|
+
"finding",
|
|
658
|
+
],
|
|
659
|
+
description:
|
|
660
|
+
"Type of comment. 'question'/'blocker' signal you need a human; default 'message'.",
|
|
661
|
+
},
|
|
662
|
+
supersedesId: {
|
|
663
|
+
type: "string",
|
|
664
|
+
description: "Comment id this comment corrects/updates",
|
|
665
|
+
},
|
|
666
|
+
confirmsId: {
|
|
667
|
+
type: "string",
|
|
668
|
+
description: "Comment id this comment reaffirms",
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
required: ["cardId", "body"],
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
harmony_get_comments: {
|
|
675
|
+
description:
|
|
676
|
+
"Get the comment thread on a card (oldest → newest). Returns human + agent comments with author, type, edited/resolved state, and supersede/confirm links. Read this before acting so you weigh later comments over earlier ones they contradict.",
|
|
677
|
+
inputSchema: {
|
|
678
|
+
type: "object",
|
|
679
|
+
properties: {
|
|
680
|
+
cardId: { type: "string" },
|
|
681
|
+
limit: { type: "number" },
|
|
682
|
+
offset: { type: "number" },
|
|
683
|
+
},
|
|
684
|
+
required: ["cardId"],
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
harmony_update_comment: {
|
|
688
|
+
description:
|
|
689
|
+
"Update one of your own comments: edit the body, pin it, or resolve a question/blocker once it has been answered.",
|
|
690
|
+
inputSchema: {
|
|
691
|
+
type: "object",
|
|
692
|
+
properties: {
|
|
693
|
+
commentId: { type: "string" },
|
|
694
|
+
body: { type: "string" },
|
|
695
|
+
pinned: { type: "boolean" },
|
|
696
|
+
resolve: {
|
|
697
|
+
type: "boolean",
|
|
698
|
+
description: "Mark (true) or clear (false) the resolved state",
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
required: ["commentId"],
|
|
702
|
+
},
|
|
703
|
+
},
|
|
704
|
+
|
|
633
705
|
// Context operations
|
|
634
706
|
harmony_list_workspaces: {
|
|
635
707
|
description: "List all workspaces the user has access to",
|
|
@@ -1781,7 +1853,9 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
|
|
|
1781
1853
|
{
|
|
1782
1854
|
uri,
|
|
1783
1855
|
mimeType: "text/markdown",
|
|
1784
|
-
|
|
1856
|
+
// Strip the legacy auto-update bash block if a stale render still
|
|
1857
|
+
// carries it — the agent shouldn't be handed curl→chmod→exec.
|
|
1858
|
+
text: stripSkillPreamble(fetched.content),
|
|
1785
1859
|
},
|
|
1786
1860
|
],
|
|
1787
1861
|
};
|
|
@@ -1863,6 +1937,7 @@ async function handleToolCall(
|
|
|
1863
1937
|
description: args.description as string | undefined,
|
|
1864
1938
|
priority: args.priority as string | undefined,
|
|
1865
1939
|
assigneeId: args.assigneeId as string | undefined,
|
|
1940
|
+
planId: args.planId as string | undefined,
|
|
1866
1941
|
});
|
|
1867
1942
|
return { success: true, ...result };
|
|
1868
1943
|
}
|
|
@@ -2168,6 +2243,71 @@ async function handleToolCall(
|
|
|
2168
2243
|
return { success: true };
|
|
2169
2244
|
}
|
|
2170
2245
|
|
|
2246
|
+
// Comment operations
|
|
2247
|
+
case "harmony_add_comment": {
|
|
2248
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
2249
|
+
const body = z.string().min(1).max(10_000).parse(args.body);
|
|
2250
|
+
const commentType =
|
|
2251
|
+
args.commentType !== undefined
|
|
2252
|
+
? z
|
|
2253
|
+
.enum([
|
|
2254
|
+
"message",
|
|
2255
|
+
"progress",
|
|
2256
|
+
"question",
|
|
2257
|
+
"blocker",
|
|
2258
|
+
"decision",
|
|
2259
|
+
"summary",
|
|
2260
|
+
"finding",
|
|
2261
|
+
])
|
|
2262
|
+
.parse(args.commentType)
|
|
2263
|
+
: undefined;
|
|
2264
|
+
const supersedesId =
|
|
2265
|
+
args.supersedesId !== undefined
|
|
2266
|
+
? z.string().uuid().parse(args.supersedesId)
|
|
2267
|
+
: undefined;
|
|
2268
|
+
const confirmsId =
|
|
2269
|
+
args.confirmsId !== undefined
|
|
2270
|
+
? z.string().uuid().parse(args.confirmsId)
|
|
2271
|
+
: undefined;
|
|
2272
|
+
const result = await client.addComment(cardId, body, {
|
|
2273
|
+
commentType,
|
|
2274
|
+
supersedesId,
|
|
2275
|
+
confirmsId,
|
|
2276
|
+
});
|
|
2277
|
+
return { success: true, ...result };
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
case "harmony_get_comments": {
|
|
2281
|
+
const cardId = z.string().uuid().parse(args.cardId);
|
|
2282
|
+
const limit =
|
|
2283
|
+
args.limit !== undefined
|
|
2284
|
+
? z.number().int().min(1).max(500).parse(args.limit)
|
|
2285
|
+
: undefined;
|
|
2286
|
+
const offset =
|
|
2287
|
+
args.offset !== undefined
|
|
2288
|
+
? z.number().int().min(0).parse(args.offset)
|
|
2289
|
+
: undefined;
|
|
2290
|
+
const result = await client.getComments(cardId, { limit, offset });
|
|
2291
|
+
return { success: true, ...result };
|
|
2292
|
+
}
|
|
2293
|
+
|
|
2294
|
+
case "harmony_update_comment": {
|
|
2295
|
+
const commentId = z.string().uuid().parse(args.commentId);
|
|
2296
|
+
const updates: { body?: string; pinned?: boolean; resolve?: boolean } =
|
|
2297
|
+
{};
|
|
2298
|
+
if (args.body !== undefined) {
|
|
2299
|
+
updates.body = z.string().min(1).max(10_000).parse(args.body);
|
|
2300
|
+
}
|
|
2301
|
+
if (args.pinned !== undefined) {
|
|
2302
|
+
updates.pinned = z.boolean().parse(args.pinned);
|
|
2303
|
+
}
|
|
2304
|
+
if (args.resolve !== undefined) {
|
|
2305
|
+
updates.resolve = z.boolean().parse(args.resolve);
|
|
2306
|
+
}
|
|
2307
|
+
const result = await client.updateComment(commentId, updates);
|
|
2308
|
+
return { success: true, ...result };
|
|
2309
|
+
}
|
|
2310
|
+
|
|
2171
2311
|
// Context operations
|
|
2172
2312
|
case "harmony_list_workspaces": {
|
|
2173
2313
|
const result = await client.listWorkspaces();
|
|
@@ -3383,17 +3523,32 @@ export class HarmonyMCPServer {
|
|
|
3383
3523
|
constructor() {
|
|
3384
3524
|
this.server = new Server(
|
|
3385
3525
|
{ name: "@gethmy/mcp", version: "2.0.0" },
|
|
3386
|
-
|
|
3526
|
+
// resources.listChanged lets us notify the client when a startup skill
|
|
3527
|
+
// refresh rewrites SKILL.md files, so it re-reads them this session.
|
|
3528
|
+
{ capabilities: { tools: {}, resources: { listChanged: true } } },
|
|
3387
3529
|
);
|
|
3388
3530
|
|
|
3389
3531
|
registerHandlers(this.server, createConfigDeps());
|
|
3390
3532
|
}
|
|
3391
3533
|
|
|
3392
|
-
|
|
3534
|
+
/**
|
|
3535
|
+
* @param opts.skillsUpdated Set when the pre-connect skill refresh
|
|
3536
|
+
* (cli.ts) rewrote files. Triggers a resources/list_changed notification
|
|
3537
|
+
* once the transport is connected so the client picks up the new skills.
|
|
3538
|
+
*/
|
|
3539
|
+
async run(opts: { skillsUpdated?: boolean } = {}) {
|
|
3393
3540
|
const transport = new StdioServerTransport();
|
|
3394
3541
|
await this.server.connect(transport);
|
|
3395
3542
|
console.error("Harmony MCP server running on stdio");
|
|
3396
3543
|
|
|
3544
|
+
if (opts.skillsUpdated) {
|
|
3545
|
+
// Client is connected now — tell it the skill resources changed.
|
|
3546
|
+
this.server.sendResourceListChanged().catch(() => {
|
|
3547
|
+
// Best-effort: a missed notification just means the new skills load
|
|
3548
|
+
// on the next session instead of this one.
|
|
3549
|
+
});
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3397
3552
|
// Initialize auto-session tracking with MCP client identity detection
|
|
3398
3553
|
const configDeps = createConfigDeps();
|
|
3399
3554
|
initAutoSession(
|