@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/README.md +1 -1
- package/dist/cli.js +144 -467
- package/dist/index.js +48 -1
- package/dist/lib/api-client.js +11 -0
- package/package.json +1 -1
- package/src/api-client.ts +56 -0
- package/src/server.ts +66 -2
- package/src/skills.ts +115 -484
- package/src/tui/setup.ts +57 -40
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:
|
|
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
|
}
|
package/dist/lib/api-client.js
CHANGED
|
@@ -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
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:
|
|
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
|
}
|