@gethmy/mcp 2.5.1 → 2.5.4

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/index.js CHANGED
@@ -1311,6 +1311,17 @@ class HarmonyApiClient {
1311
1311
  }
1312
1312
  throw lastError || new Error("Request failed after retries");
1313
1313
  }
1314
+ async fetchSkillsVersion() {
1315
+ return this.request("GET", "/skills/version");
1316
+ }
1317
+ async fetchSkill(name) {
1318
+ return this.request("GET", `/skills/${encodeURIComponent(name)}`);
1319
+ }
1320
+ async recordSkillInvocation(name) {
1321
+ try {
1322
+ await this.request("POST", "/skills/telemetry", { name });
1323
+ } catch {}
1324
+ }
1314
1325
  async listWorkspaces() {
1315
1326
  return this.request("GET", "/workspaces");
1316
1327
  }
@@ -3542,6 +3553,22 @@ var RESOURCES = [
3542
3553
  mimeType: "application/json"
3543
3554
  }
3544
3555
  ];
3556
+ async function listResourcesDynamic(deps) {
3557
+ if (!deps.isConfigured())
3558
+ return RESOURCES;
3559
+ try {
3560
+ const versionInfo = await deps.getClient().fetchSkillsVersion();
3561
+ const skillResources = versionInfo.skills.filter((name) => name !== "hmy-update-check").map((name) => ({
3562
+ uri: `harmony://skills/${name}`,
3563
+ name: `Skill: ${name}`,
3564
+ description: `Harmony skill (SKILL.md) — version ${versionInfo.version}`,
3565
+ mimeType: "text/markdown"
3566
+ }));
3567
+ return [...RESOURCES, ...skillResources];
3568
+ } catch {
3569
+ return RESOURCES;
3570
+ }
3571
+ }
3545
3572
  async function runEndSessionPipeline(client3, _deps, cardId, sessionStatus) {
3546
3573
  try {
3547
3574
  const { card } = await client3.getCard(cardId);
@@ -3609,7 +3636,7 @@ function registerHandlers(server, deps) {
3609
3636
  }
3610
3637
  });
3611
3638
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
3612
- resources: RESOURCES
3639
+ resources: await listResourcesDynamic(deps)
3613
3640
  }));
3614
3641
  server.setRequestHandler(ReadResourceRequestSchema, async (request) => {
3615
3642
  const { uri } = request.params;
@@ -3628,6 +3655,26 @@ function registerHandlers(server, deps) {
3628
3655
  ]
3629
3656
  };
3630
3657
  }
3658
+ const SKILL_URI_RE = /^harmony:\/\/skills\/([a-z0-9][a-z0-9-]*[a-z0-9])$/;
3659
+ const skillMatch = uri.match(SKILL_URI_RE);
3660
+ if (skillMatch) {
3661
+ const name = skillMatch[1];
3662
+ if (!deps.isConfigured()) {
3663
+ throw new Error(`Cannot read skill "${name}": Harmony MCP server is not configured. Run \`hmy-mcp setup\`.`);
3664
+ }
3665
+ const client3 = deps.getClient();
3666
+ client3.recordSkillInvocation(name);
3667
+ const fetched = await client3.fetchSkill(name);
3668
+ return {
3669
+ contents: [
3670
+ {
3671
+ uri,
3672
+ mimeType: "text/markdown",
3673
+ text: fetched.content
3674
+ }
3675
+ ]
3676
+ };
3677
+ }
3631
3678
  throw new Error(`Unknown resource: ${uri}`);
3632
3679
  });
3633
3680
  }
@@ -918,6 +918,17 @@ class HarmonyApiClient {
918
918
  }
919
919
  throw lastError || new Error("Request failed after retries");
920
920
  }
921
+ async fetchSkillsVersion() {
922
+ return this.request("GET", "/skills/version");
923
+ }
924
+ async fetchSkill(name) {
925
+ return this.request("GET", `/skills/${encodeURIComponent(name)}`);
926
+ }
927
+ async recordSkillInvocation(name) {
928
+ try {
929
+ await this.request("POST", "/skills/telemetry", { name });
930
+ } catch {}
931
+ }
921
932
  async listWorkspaces() {
922
933
  return this.request("GET", "/workspaces");
923
934
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.5.1",
3
+ "version": "2.5.4",
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
@@ -323,6 +323,62 @@ export class HarmonyApiClient {
323
323
  throw lastError || new Error("Request failed after retries");
324
324
  }
325
325
 
326
+ // ============ SKILLS OPERATIONS ============
327
+
328
+ /**
329
+ * GET /v1/skills/version — unauthenticated discovery endpoint.
330
+ * Returns aggregate version, the list of skill names, sha256 integrity
331
+ * hashes, and the ed25519 public key for signature verification.
332
+ */
333
+ async fetchSkillsVersion(): Promise<{
334
+ version: string;
335
+ skills: string[];
336
+ integrity: Record<string, string>;
337
+ publicKey: string;
338
+ }> {
339
+ return this.request("GET", "/skills/version");
340
+ }
341
+
342
+ /**
343
+ * GET /v1/skills/{name} — authenticated content fetch.
344
+ * Returns rendered SKILL.md (frontmatter + body) ready to install.
345
+ *
346
+ * Phase 3b: `skillVersion` (per-skill integer) is returned alongside the
347
+ * legacy aggregate `version` string so refreshSkills can compare per-skill
348
+ * instead of treating any global bump as "everything stale."
349
+ */
350
+ async fetchSkill(name: string): Promise<{
351
+ name: string;
352
+ version: string;
353
+ skillVersion?: number;
354
+ content: string;
355
+ sha256: string;
356
+ signature: string;
357
+ }> {
358
+ return this.request("GET", `/skills/${encodeURIComponent(name)}`);
359
+ }
360
+
361
+ /**
362
+ * POST /v1/skills/telemetry — fire-and-forget activation signal.
363
+ * Called from the ReadResourceRequest handler when an agent reads
364
+ * a harmony://skills/<name> resource. Increments invocation_count +
365
+ * sets last_used = now() on the matching skill_resource row.
366
+ *
367
+ * Best-effort: returns void; never throws. If the endpoint doesn't
368
+ * exist yet (pre-Phase-2 backend), or the network is flaky, the
369
+ * activation is dropped silently. The skill content read itself
370
+ * is NOT gated on this call succeeding.
371
+ */
372
+ async recordSkillInvocation(name: string): Promise<void> {
373
+ try {
374
+ await this.request("POST", "/skills/telemetry", { name });
375
+ } catch {
376
+ // Fire-and-forget: any failure (404 if endpoint not deployed yet,
377
+ // 401 if API key rotated, network timeout) is swallowed so the
378
+ // caller can proceed with serving content.
379
+ }
380
+ }
381
+
326
382
  // ============ WORKSPACE OPERATIONS ============
327
383
 
328
384
  async listWorkspaces(): Promise<{ workspaces: unknown[] }> {
package/src/server.ts CHANGED
@@ -1530,6 +1530,38 @@ export const RESOURCES = [
1530
1530
  },
1531
1531
  ];
1532
1532
 
1533
+ /**
1534
+ * Build the dynamic resource list. Skill resources (harmony://skills/<name>)
1535
+ * are derived at request time from /v1/skills/version so the list always
1536
+ * matches what the agent will get on read. Phase 1 of card #162 — the
1537
+ * ReadResourceRequest on harmony://skills/<name> is the activation signal
1538
+ * for skill telemetry.
1539
+ *
1540
+ * Offline / unauthenticated → returns just the static RESOURCES. Agents
1541
+ * that can't reach the API still see the context resource and any error
1542
+ * surfaces on read rather than list (matches the existing offline-safe
1543
+ * posture of refreshSkills()).
1544
+ */
1545
+ async function listResourcesDynamic(deps: ToolDeps): Promise<typeof RESOURCES> {
1546
+ if (!deps.isConfigured()) return RESOURCES;
1547
+ try {
1548
+ const versionInfo = await deps.getClient().fetchSkillsVersion();
1549
+ // hmy-update-check is a shell script served on the same URL but is
1550
+ // not an installable SKILL.md; don't surface it as an MCP resource.
1551
+ const skillResources = versionInfo.skills
1552
+ .filter((name) => name !== "hmy-update-check")
1553
+ .map((name) => ({
1554
+ uri: `harmony://skills/${name}`,
1555
+ name: `Skill: ${name}`,
1556
+ description: `Harmony skill (SKILL.md) — version ${versionInfo.version}`,
1557
+ mimeType: "text/markdown",
1558
+ }));
1559
+ return [...RESOURCES, ...skillResources];
1560
+ } catch {
1561
+ return RESOURCES;
1562
+ }
1563
+ }
1564
+
1533
1565
  /**
1534
1566
  * Reusable end-session pipeline.
1535
1567
  * Called by both explicit harmony_end_agent_session and auto-session timeout/card-switch.
@@ -1654,9 +1686,10 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
1654
1686
  }
1655
1687
  });
1656
1688
 
1657
- // List resources
1689
+ // List resources — dynamic so harmony://skills/<name> entries reflect
1690
+ // the live /v1/skills/version payload at request time.
1658
1691
  server.setRequestHandler(ListResourcesRequestSchema, async () => ({
1659
- resources: RESOURCES,
1692
+ resources: await listResourcesDynamic(deps),
1660
1693
  }));
1661
1694
 
1662
1695
  // Read resource
@@ -1683,6 +1716,37 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
1683
1716
  };
1684
1717
  }
1685
1718
 
1719
+ // harmony://skills/<name> — Phase 1 of card #162. This read IS the
1720
+ // activation event: agent matched the skill's description, harness
1721
+ // calls ReadResource, we fire fire-and-forget telemetry before
1722
+ // returning the body. recordSkillInvocation() swallows all errors
1723
+ // so a telemetry outage never blocks skill delivery.
1724
+ const SKILL_URI_RE = /^harmony:\/\/skills\/([a-z0-9][a-z0-9-]*[a-z0-9])$/;
1725
+ const skillMatch = uri.match(SKILL_URI_RE);
1726
+ if (skillMatch) {
1727
+ const name = skillMatch[1];
1728
+ if (!deps.isConfigured()) {
1729
+ throw new Error(
1730
+ `Cannot read skill "${name}": Harmony MCP server is not configured. Run \`hmy-mcp setup\`.`,
1731
+ );
1732
+ }
1733
+ const client = deps.getClient();
1734
+ // Fire-and-forget BEFORE awaiting the content fetch so a slow
1735
+ // telemetry endpoint never delays skill delivery. The promise
1736
+ // is intentionally dropped (caught inside recordSkillInvocation).
1737
+ void client.recordSkillInvocation(name);
1738
+ const fetched = await client.fetchSkill(name);
1739
+ return {
1740
+ contents: [
1741
+ {
1742
+ uri,
1743
+ mimeType: "text/markdown",
1744
+ text: fetched.content,
1745
+ },
1746
+ ],
1747
+ };
1748
+ }
1749
+
1686
1750
  throw new Error(`Unknown resource: ${uri}`);
1687
1751
  });
1688
1752
  }