@gethmy/mcp 2.9.6 → 2.9.8

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,8 +5,8 @@ Enables AI coding agents (Claude Code, OpenAI Codex, Cursor) to interact with yo
5
5
 
6
6
  ## Features
7
7
 
8
- - **56 MCP Tools** for full board control, knowledge graph, and workflow plans
9
- - **5 Global Skills** — `/hmy`, `/hmy-plan`, `/hmy-cleanup`, `/hmy-standup`, `/hmy-memory-prune`, served from the DB-backed [skill hub](../../docs/skills.md) with auto-update and admin-managed versioning
8
+ - **70 MCP Tools** for full board control, knowledge graph, and workflow plans
9
+ - **7 Global Skills** — `/hmy`, `/hmy-plan`, `/hmy-cleanup`, `/hmy-standup`, `/hmy-memory-prune`, `/hmy-upgrade`, `/hmy-review`, 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
12
12
  - **Card Linking** - create relationships between cards (blocks, relates_to, duplicates, is_part_of)
@@ -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 56 Harmony tools become available in your conversation
92
+ 4. All 70 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
 
@@ -150,7 +150,7 @@ npx @gethmy/mcp serve # Start MCP server
150
150
 
151
151
  ## Skills
152
152
 
153
- Five global skills ship with the MCP server and are installed automatically by `npx @gethmy/mcp setup`. They live in the `skill_resource` Postgres table, are fetched via `GET /v1/skills/<name>`, and render-time composed with a shared auto-update preamble.
153
+ Seven global skills ship with the MCP server and are installed automatically by `npx @gethmy/mcp setup`. They live in the `skill_resource` Postgres table, are fetched via `GET /v1/skills/<name>`, and render-time composed with a shared auto-update preamble.
154
154
 
155
155
  For the full skill hub architecture (storage, versioning, auto-update, admin management), see [docs/skills.md](../../docs/skills.md).
156
156
 
package/dist/cli.js CHANGED
@@ -1385,7 +1385,6 @@ import {
1385
1385
  ReadResourceRequestSchema
1386
1386
  } from "@modelcontextprotocol/sdk/types.js";
1387
1387
  import { z } from "zod";
1388
-
1389
1388
  // ../harmony-shared/dist/cardLinks.js
1390
1389
  var LINK_TYPE_INVERSES = {
1391
1390
  relates_to: "relates_to",
@@ -1815,6 +1814,16 @@ class HarmonyApiClient {
1815
1814
  async getCardByShortId(projectId, shortId) {
1816
1815
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
1817
1816
  }
1817
+ async bulkGetCards(projectId, shortIds) {
1818
+ return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
1819
+ shortIds
1820
+ });
1821
+ }
1822
+ async bulkArchiveCards(projectId, shortIds) {
1823
+ return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
1824
+ shortIds
1825
+ });
1826
+ }
1818
1827
  async searchCards(query, options) {
1819
1828
  const params = new URLSearchParams({ q: query });
1820
1829
  if (options?.projectId) {
@@ -1849,6 +1858,9 @@ class HarmonyApiClient {
1849
1858
  async getCardExternalLinks(cardId) {
1850
1859
  return this.request("GET", `/cards/${cardId}/external-links`);
1851
1860
  }
1861
+ async classifyCard(cardId) {
1862
+ return this.request("POST", `/cards/${cardId}/classify`);
1863
+ }
1852
1864
  async createColumn(projectId, name) {
1853
1865
  return this.request("POST", "/columns", { projectId, name });
1854
1866
  }
@@ -1910,6 +1922,9 @@ class HarmonyApiClient {
1910
1922
  async flushActivityLog(cardId, data) {
1911
1923
  return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
1912
1924
  }
1925
+ async appendAgentRunEvents(cardId, data) {
1926
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
1927
+ }
1913
1928
  async getActivityLog(cardId, sessionId) {
1914
1929
  return this.request("GET", `/cards/${cardId}/agent-activity-log?sessionId=${sessionId}`);
1915
1930
  }
@@ -3680,6 +3695,34 @@ var TOOLS = {
3680
3695
  required: ["shortId"]
3681
3696
  }
3682
3697
  },
3698
+ harmony_bulk_get_cards: {
3699
+ description: "Fetch multiple cards by short id in one call. Returns compact summaries " + "(id, shortId, title, column, priority, assignee, labels, archived) plus " + "any short ids not found. Requires project context. Prefer this over " + "repeated harmony_get_card_by_short_id when referencing multiple cards.",
3700
+ inputSchema: {
3701
+ type: "object",
3702
+ properties: {
3703
+ shortIds: {
3704
+ type: "array",
3705
+ items: { type: "number" },
3706
+ description: "Card short ids, e.g. [400, 401, 402]. Max 100 per call."
3707
+ }
3708
+ },
3709
+ required: ["shortIds"]
3710
+ }
3711
+ },
3712
+ harmony_bulk_archive_cards: {
3713
+ description: "Archive (soft-delete) multiple cards by short id in one call. Best-effort: " + "returns { succeeded: number[], failed: [{shortId, reason}] }. Archived " + "cards can be restored with unarchive. Requires project context.",
3714
+ inputSchema: {
3715
+ type: "object",
3716
+ properties: {
3717
+ shortIds: {
3718
+ type: "array",
3719
+ items: { type: "number" },
3720
+ description: "Card short ids to archive, e.g. [400, 401]. Max 100 per call."
3721
+ }
3722
+ },
3723
+ required: ["shortIds"]
3724
+ }
3725
+ },
3683
3726
  harmony_create_column: {
3684
3727
  description: "Create a new column in a project",
3685
3728
  inputSchema: {
@@ -3844,6 +3887,16 @@ var TOOLS = {
3844
3887
  required: ["cardId"]
3845
3888
  }
3846
3889
  },
3890
+ harmony_classify_card: {
3891
+ description: "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), and `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the canonical type label (feature/bug/idea). Use this right after creating a card (e.g. in the `hmy-new` flow) so it's classified in-flow instead of waiting for it to surface on the web board. Idempotent — safe to re-run. Never touches the user-owned `model_override`.",
3892
+ inputSchema: {
3893
+ type: "object",
3894
+ properties: {
3895
+ cardId: { type: "string", description: "Card UUID" }
3896
+ },
3897
+ required: ["cardId"]
3898
+ }
3899
+ },
3847
3900
  harmony_get_card_external_links: {
3848
3901
  description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
3849
3902
  inputSchema: {
@@ -5169,6 +5222,18 @@ async function handleToolCall(name, args, deps) {
5169
5222
  const result = await client3.getCardByShortId(projectId, shortId);
5170
5223
  return { success: true, ...result };
5171
5224
  }
5225
+ case "harmony_bulk_get_cards": {
5226
+ const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
5227
+ const projectId = getProjectId();
5228
+ const result = await client3.bulkGetCards(projectId, shortIds);
5229
+ return { success: true, ...result };
5230
+ }
5231
+ case "harmony_bulk_archive_cards": {
5232
+ const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
5233
+ const projectId = getProjectId();
5234
+ const result = await client3.bulkArchiveCards(projectId, shortIds);
5235
+ return { success: true, ...result };
5236
+ }
5172
5237
  case "harmony_create_column": {
5173
5238
  const name2 = z.string().min(1).max(100).parse(args.name);
5174
5239
  const projectId = args.projectId || getProjectId();
@@ -5299,6 +5364,11 @@ async function handleToolCall(name, args, deps) {
5299
5364
  const result = await client3.getCardExternalLinks(cardId);
5300
5365
  return result;
5301
5366
  }
5367
+ case "harmony_classify_card": {
5368
+ const cardId = z.string().uuid().parse(args.cardId);
5369
+ const result = await client3.classifyCard(cardId);
5370
+ return result;
5371
+ }
5302
5372
  case "harmony_create_subtask": {
5303
5373
  const cardId = z.string().uuid().parse(args.cardId);
5304
5374
  const title = z.string().min(1).max(500).parse(args.title);
@@ -7473,12 +7543,23 @@ async function resolveProjectSlug(apiKey, slug) {
7473
7543
  }
7474
7544
  });
7475
7545
  if (response.status === 404)
7476
- return null;
7546
+ return { status: "not_found" };
7547
+ if (response.status === 409) {
7548
+ const data2 = await response.json().catch(() => ({}));
7549
+ return {
7550
+ status: "ambiguous",
7551
+ candidates: Array.isArray(data2.candidates) ? data2.candidates : []
7552
+ };
7553
+ }
7477
7554
  if (!response.ok) {
7478
7555
  throw new Error(`Failed to resolve project slug: ${response.status}`);
7479
7556
  }
7480
7557
  const data = await response.json();
7481
- return { workspaceId: data.workspaceId, projectId: data.projectId };
7558
+ return {
7559
+ status: "found",
7560
+ workspaceId: data.workspaceId,
7561
+ projectId: data.projectId
7562
+ };
7482
7563
  }
7483
7564
  async function getAgentFiles(agentId, cwd, installMode = "global") {
7484
7565
  const home = homedir6();
@@ -7760,12 +7841,13 @@ only for unattended CI where you accept that risk.`));
7760
7841
  options: [
7761
7842
  {
7762
7843
  value: "browser",
7763
- label: "Sign in with your browser",
7764
- hint: "recommended — secure, no key handling"
7844
+ label: "Sign in or create an account in your browser",
7845
+ hint: "recommended — new or existing users, no key handling"
7765
7846
  },
7766
7847
  {
7767
7848
  value: "create",
7768
- label: "Create a free account"
7849
+ label: "Create an account here in the terminal",
7850
+ hint: "no browser"
7769
7851
  },
7770
7852
  {
7771
7853
  value: "apikey",
@@ -8002,14 +8084,23 @@ ${colors.dim(url)}`);
8002
8084
  let selectedProjectId = selectedProjectIdFromSignup || options.projectId;
8003
8085
  let selectedWorkspaceName = selectedWorkspaceNameFromSignup;
8004
8086
  let selectedProjectName = selectedProjectNameFromSignup;
8087
+ let ambiguousCandidates;
8005
8088
  if (options.projectSlug && apiKey && (!selectedWorkspaceId || !selectedProjectId)) {
8006
8089
  spinner3.start(`Resolving project slug "${options.projectSlug}"...`);
8007
8090
  try {
8008
8091
  const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
8009
- if (resolved) {
8092
+ if (resolved.status === "found") {
8010
8093
  selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
8011
8094
  selectedProjectId = selectedProjectId || resolved.projectId;
8012
8095
  spinner3.stop(colors.success(`Resolved "${options.projectSlug}"`));
8096
+ } else if (resolved.status === "ambiguous") {
8097
+ ambiguousCandidates = resolved.candidates;
8098
+ spinner3.stop(colors.warning(`Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`));
8099
+ const list = resolved.candidates.map((c) => ` • ${c.workspaceName ?? c.workspaceId}`).join(`
8100
+ `);
8101
+ p3.log.warning(`"${options.projectSlug}" matches projects in multiple workspaces:
8102
+ ${list}
8103
+ Specify the workspace with --workspace <id>, or select one below.`);
8013
8104
  } else {
8014
8105
  spinner3.stop(colors.warning(`No project found for slug "${options.projectSlug}"`));
8015
8106
  }
@@ -8033,12 +8124,15 @@ ${colors.dim(url)}`);
8033
8124
  }
8034
8125
  if (needsContext && workspaces.length > 0) {
8035
8126
  if (!selectedWorkspaceId) {
8036
- const workspaceOptions = workspaces.map((ws) => ({
8127
+ const candidateIds = new Set((ambiguousCandidates ?? []).map((c) => c.workspaceId));
8128
+ const pickable = candidateIds.size > 0 ? workspaces.filter((ws) => candidateIds.has(ws.id)) : workspaces;
8129
+ const workspaceChoices = pickable.length > 0 ? pickable : workspaces;
8130
+ const workspaceOptions = workspaceChoices.map((ws) => ({
8037
8131
  value: ws.id,
8038
8132
  label: ws.name
8039
8133
  }));
8040
8134
  const workspaceSelection = await p3.select({
8041
- message: "Select workspace",
8135
+ message: candidateIds.size > 0 ? `Select workspace for "${options.projectSlug}"` : "Select workspace",
8042
8136
  options: workspaceOptions
8043
8137
  });
8044
8138
  if (p3.isCancel(workspaceSelection)) {
@@ -8048,6 +8142,12 @@ ${colors.dim(url)}`);
8048
8142
  selectedWorkspaceId = workspaceSelection;
8049
8143
  }
8050
8144
  selectedWorkspaceName = workspaces.find((w) => w.id === selectedWorkspaceId)?.name;
8145
+ if (!selectedProjectId) {
8146
+ const chosenCandidate = ambiguousCandidates?.find((c) => c.workspaceId === selectedWorkspaceId);
8147
+ if (chosenCandidate) {
8148
+ selectedProjectId = chosenCandidate.projectId;
8149
+ }
8150
+ }
8051
8151
  spinner3.start("Fetching projects...");
8052
8152
  let projects = [];
8053
8153
  try {
@@ -8073,6 +8173,8 @@ ${colors.dim(url)}`);
8073
8173
  }
8074
8174
  selectedProjectId = projectSelection;
8075
8175
  selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
8176
+ } else if (selectedProjectId && !selectedProjectName) {
8177
+ selectedProjectName = projects.find((p4) => p4.id === selectedProjectId)?.name;
8076
8178
  }
8077
8179
  }
8078
8180
  }
@@ -8297,6 +8399,11 @@ ${colors.dim(url)}`);
8297
8399
  console.log(` ${colors.brand("Cursor:")} MCP tools available automatically`);
8298
8400
  }
8299
8401
  console.log("");
8402
+ console.log(` ${colors.bold("Next steps:")}`);
8403
+ console.log(` 1. Open Claude Code and say: ${colors.highlight('"Show me my board"')}`);
8404
+ console.log(` 2. Create a card: ${colors.highlight('"Create a card called Auth token refresh"')}`);
8405
+ console.log(` 3. Start the daemon: ${colors.highlight("npx @gethmy/agent")}`);
8406
+ console.log("");
8300
8407
  console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
8301
8408
  console.log(` ${colors.dim("Need help? Visit https://app.gethmy.com/docs/mcp")}`);
8302
8409
  }
package/dist/index.js CHANGED
@@ -1380,7 +1380,6 @@ import {
1380
1380
  ReadResourceRequestSchema
1381
1381
  } from "@modelcontextprotocol/sdk/types.js";
1382
1382
  import { z } from "zod";
1383
-
1384
1383
  // ../harmony-shared/dist/cardLinks.js
1385
1384
  var LINK_TYPE_INVERSES = {
1386
1385
  relates_to: "relates_to",
@@ -1810,6 +1809,16 @@ class HarmonyApiClient {
1810
1809
  async getCardByShortId(projectId, shortId) {
1811
1810
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
1812
1811
  }
1812
+ async bulkGetCards(projectId, shortIds) {
1813
+ return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
1814
+ shortIds
1815
+ });
1816
+ }
1817
+ async bulkArchiveCards(projectId, shortIds) {
1818
+ return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
1819
+ shortIds
1820
+ });
1821
+ }
1813
1822
  async searchCards(query, options) {
1814
1823
  const params = new URLSearchParams({ q: query });
1815
1824
  if (options?.projectId) {
@@ -1844,6 +1853,9 @@ class HarmonyApiClient {
1844
1853
  async getCardExternalLinks(cardId) {
1845
1854
  return this.request("GET", `/cards/${cardId}/external-links`);
1846
1855
  }
1856
+ async classifyCard(cardId) {
1857
+ return this.request("POST", `/cards/${cardId}/classify`);
1858
+ }
1847
1859
  async createColumn(projectId, name) {
1848
1860
  return this.request("POST", "/columns", { projectId, name });
1849
1861
  }
@@ -1905,6 +1917,9 @@ class HarmonyApiClient {
1905
1917
  async flushActivityLog(cardId, data) {
1906
1918
  return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
1907
1919
  }
1920
+ async appendAgentRunEvents(cardId, data) {
1921
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
1922
+ }
1908
1923
  async getActivityLog(cardId, sessionId) {
1909
1924
  return this.request("GET", `/cards/${cardId}/agent-activity-log?sessionId=${sessionId}`);
1910
1925
  }
@@ -3675,6 +3690,34 @@ var TOOLS = {
3675
3690
  required: ["shortId"]
3676
3691
  }
3677
3692
  },
3693
+ harmony_bulk_get_cards: {
3694
+ description: "Fetch multiple cards by short id in one call. Returns compact summaries " + "(id, shortId, title, column, priority, assignee, labels, archived) plus " + "any short ids not found. Requires project context. Prefer this over " + "repeated harmony_get_card_by_short_id when referencing multiple cards.",
3695
+ inputSchema: {
3696
+ type: "object",
3697
+ properties: {
3698
+ shortIds: {
3699
+ type: "array",
3700
+ items: { type: "number" },
3701
+ description: "Card short ids, e.g. [400, 401, 402]. Max 100 per call."
3702
+ }
3703
+ },
3704
+ required: ["shortIds"]
3705
+ }
3706
+ },
3707
+ harmony_bulk_archive_cards: {
3708
+ description: "Archive (soft-delete) multiple cards by short id in one call. Best-effort: " + "returns { succeeded: number[], failed: [{shortId, reason}] }. Archived " + "cards can be restored with unarchive. Requires project context.",
3709
+ inputSchema: {
3710
+ type: "object",
3711
+ properties: {
3712
+ shortIds: {
3713
+ type: "array",
3714
+ items: { type: "number" },
3715
+ description: "Card short ids to archive, e.g. [400, 401]. Max 100 per call."
3716
+ }
3717
+ },
3718
+ required: ["shortIds"]
3719
+ }
3720
+ },
3678
3721
  harmony_create_column: {
3679
3722
  description: "Create a new column in a project",
3680
3723
  inputSchema: {
@@ -3839,6 +3882,16 @@ var TOOLS = {
3839
3882
  required: ["cardId"]
3840
3883
  }
3841
3884
  },
3885
+ harmony_classify_card: {
3886
+ description: "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), and `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the canonical type label (feature/bug/idea). Use this right after creating a card (e.g. in the `hmy-new` flow) so it's classified in-flow instead of waiting for it to surface on the web board. Idempotent — safe to re-run. Never touches the user-owned `model_override`.",
3887
+ inputSchema: {
3888
+ type: "object",
3889
+ properties: {
3890
+ cardId: { type: "string", description: "Card UUID" }
3891
+ },
3892
+ required: ["cardId"]
3893
+ }
3894
+ },
3842
3895
  harmony_get_card_external_links: {
3843
3896
  description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
3844
3897
  inputSchema: {
@@ -5164,6 +5217,18 @@ async function handleToolCall(name, args, deps) {
5164
5217
  const result = await client3.getCardByShortId(projectId, shortId);
5165
5218
  return { success: true, ...result };
5166
5219
  }
5220
+ case "harmony_bulk_get_cards": {
5221
+ const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
5222
+ const projectId = getProjectId();
5223
+ const result = await client3.bulkGetCards(projectId, shortIds);
5224
+ return { success: true, ...result };
5225
+ }
5226
+ case "harmony_bulk_archive_cards": {
5227
+ const shortIds = z.array(z.number().int().positive()).min(1).max(100).parse(args.shortIds);
5228
+ const projectId = getProjectId();
5229
+ const result = await client3.bulkArchiveCards(projectId, shortIds);
5230
+ return { success: true, ...result };
5231
+ }
5167
5232
  case "harmony_create_column": {
5168
5233
  const name2 = z.string().min(1).max(100).parse(args.name);
5169
5234
  const projectId = args.projectId || getProjectId();
@@ -5294,6 +5359,11 @@ async function handleToolCall(name, args, deps) {
5294
5359
  const result = await client3.getCardExternalLinks(cardId);
5295
5360
  return result;
5296
5361
  }
5362
+ case "harmony_classify_card": {
5363
+ const cardId = z.string().uuid().parse(args.cardId);
5364
+ const result = await client3.classifyCard(cardId);
5365
+ return result;
5366
+ }
5297
5367
  case "harmony_create_subtask": {
5298
5368
  const cardId = z.string().uuid().parse(args.cardId);
5299
5369
  const title = z.string().min(1).max(500).parse(args.title);
@@ -833,7 +833,6 @@ var init_oauth_refresh = __esm(() => {
833
833
  init_config();
834
834
  init_oauth_login();
835
835
  });
836
-
837
836
  // ../harmony-shared/dist/cardLinks.js
838
837
  var LINK_TYPE_INVERSES = {
839
838
  relates_to: "relates_to",
@@ -1263,6 +1262,16 @@ class HarmonyApiClient {
1263
1262
  async getCardByShortId(projectId, shortId) {
1264
1263
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
1265
1264
  }
1265
+ async bulkGetCards(projectId, shortIds) {
1266
+ return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
1267
+ shortIds
1268
+ });
1269
+ }
1270
+ async bulkArchiveCards(projectId, shortIds) {
1271
+ return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
1272
+ shortIds
1273
+ });
1274
+ }
1266
1275
  async searchCards(query, options) {
1267
1276
  const params = new URLSearchParams({ q: query });
1268
1277
  if (options?.projectId) {
@@ -1297,6 +1306,9 @@ class HarmonyApiClient {
1297
1306
  async getCardExternalLinks(cardId) {
1298
1307
  return this.request("GET", `/cards/${cardId}/external-links`);
1299
1308
  }
1309
+ async classifyCard(cardId) {
1310
+ return this.request("POST", `/cards/${cardId}/classify`);
1311
+ }
1300
1312
  async createColumn(projectId, name) {
1301
1313
  return this.request("POST", "/columns", { projectId, name });
1302
1314
  }
@@ -1358,6 +1370,9 @@ class HarmonyApiClient {
1358
1370
  async flushActivityLog(cardId, data) {
1359
1371
  return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
1360
1372
  }
1373
+ async appendAgentRunEvents(cardId, data) {
1374
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
1375
+ }
1361
1376
  async getActivityLog(cardId, sessionId) {
1362
1377
  return this.request("GET", `/cards/${cardId}/agent-activity-log?sessionId=${sessionId}`);
1363
1378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.9.6",
3
+ "version": "2.9.8",
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
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type AgentRunEventDraft,
2
3
  type Comment,
3
4
  getDisplayLinkType,
4
5
  serializeCommentThread,
@@ -151,6 +152,18 @@ export interface CardExternalLinkRow {
151
152
  created_at: string;
152
153
  }
153
154
 
155
+ /** Result of the classify-card classifier (card #415). Any field may be null
156
+ * if the LLM didn't return a usable value. `model_override` is never touched. */
157
+ export interface CardClassificationResult {
158
+ type: "feature" | "bug" | "idea" | null;
159
+ intent: "plan" | "think" | "implement" | "review" | null;
160
+ complexity_score: number | null;
161
+ model_tier: "simple" | "advanced" | "research" | null;
162
+ classified_at: string;
163
+ applied_type_label_id: string | null;
164
+ reasoning: string | null;
165
+ }
166
+
154
167
  export class HarmonyApiClient {
155
168
  private apiKey: string;
156
169
  private apiUrl: string;
@@ -570,6 +583,39 @@ export class HarmonyApiClient {
570
583
  return this.request("GET", `/projects/${projectId}/cards/${shortId}`);
571
584
  }
572
585
 
586
+ async bulkGetCards(
587
+ projectId: string,
588
+ shortIds: number[],
589
+ ): Promise<{
590
+ cards: Array<{
591
+ id: string;
592
+ shortId: number;
593
+ title: string;
594
+ column: string | null;
595
+ priority: string | null;
596
+ assignee: string | null;
597
+ labels: string[];
598
+ archived: boolean;
599
+ }>;
600
+ notFound: number[];
601
+ }> {
602
+ return this.request("POST", `/projects/${projectId}/cards/bulk-get`, {
603
+ shortIds,
604
+ });
605
+ }
606
+
607
+ async bulkArchiveCards(
608
+ projectId: string,
609
+ shortIds: number[],
610
+ ): Promise<{
611
+ succeeded: number[];
612
+ failed: Array<{ shortId: number; reason: string }>;
613
+ }> {
614
+ return this.request("POST", `/projects/${projectId}/cards/bulk-archive`, {
615
+ shortIds,
616
+ });
617
+ }
618
+
573
619
  async searchCards(
574
620
  query: string,
575
621
  options?: { projectId?: string },
@@ -635,6 +681,12 @@ export class HarmonyApiClient {
635
681
  return this.request("GET", `/cards/${cardId}/external-links`);
636
682
  }
637
683
 
684
+ async classifyCard(
685
+ cardId: string,
686
+ ): Promise<{ classification: CardClassificationResult }> {
687
+ return this.request("POST", `/cards/${cardId}/classify`);
688
+ }
689
+
638
690
  // ============ COLUMN OPERATIONS ============
639
691
 
640
692
  async createColumn(
@@ -847,6 +899,20 @@ export class HarmonyApiClient {
847
899
  return this.request("POST", `/cards/${cardId}/agent-activity-log`, data);
848
900
  }
849
901
 
902
+ /**
903
+ * Append events to a run's agent_run_events stream (card #417). Send drafts in
904
+ * chronological order — the server's seq trigger assigns the monotonic per-run order.
905
+ */
906
+ async appendAgentRunEvents(
907
+ cardId: string,
908
+ data: {
909
+ sessionId: string;
910
+ events: (AgentRunEventDraft & { createdAt?: string })[];
911
+ },
912
+ ): Promise<{ inserted: number }> {
913
+ return this.request("POST", `/cards/${cardId}/agent-run-events`, data);
914
+ }
915
+
850
916
  async getActivityLog(
851
917
  cardId: string,
852
918
  sessionId: string,
package/src/server.ts CHANGED
@@ -469,6 +469,43 @@ export const TOOLS = {
469
469
  required: ["shortId"],
470
470
  },
471
471
  },
472
+ harmony_bulk_get_cards: {
473
+ description:
474
+ "Fetch multiple cards by short id in one call. Returns compact summaries " +
475
+ "(id, shortId, title, column, priority, assignee, labels, archived) plus " +
476
+ "any short ids not found. Requires project context. Prefer this over " +
477
+ "repeated harmony_get_card_by_short_id when referencing multiple cards.",
478
+ inputSchema: {
479
+ type: "object",
480
+ properties: {
481
+ shortIds: {
482
+ type: "array",
483
+ items: { type: "number" },
484
+ description:
485
+ "Card short ids, e.g. [400, 401, 402]. Max 100 per call.",
486
+ },
487
+ },
488
+ required: ["shortIds"],
489
+ },
490
+ },
491
+ harmony_bulk_archive_cards: {
492
+ description:
493
+ "Archive (soft-delete) multiple cards by short id in one call. Best-effort: " +
494
+ "returns { succeeded: number[], failed: [{shortId, reason}] }. Archived " +
495
+ "cards can be restored with unarchive. Requires project context.",
496
+ inputSchema: {
497
+ type: "object",
498
+ properties: {
499
+ shortIds: {
500
+ type: "array",
501
+ items: { type: "number" },
502
+ description:
503
+ "Card short ids to archive, e.g. [400, 401]. Max 100 per call.",
504
+ },
505
+ },
506
+ required: ["shortIds"],
507
+ },
508
+ },
472
509
 
473
510
  // Column operations
474
511
  harmony_create_column: {
@@ -649,6 +686,17 @@ export const TOOLS = {
649
686
  required: ["cardId"],
650
687
  },
651
688
  },
689
+ harmony_classify_card: {
690
+ description:
691
+ "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), and `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the canonical type label (feature/bug/idea). Use this right after creating a card (e.g. in the `hmy-new` flow) so it's classified in-flow instead of waiting for it to surface on the web board. Idempotent — safe to re-run. Never touches the user-owned `model_override`.",
692
+ inputSchema: {
693
+ type: "object",
694
+ properties: {
695
+ cardId: { type: "string", description: "Card UUID" },
696
+ },
697
+ required: ["cardId"],
698
+ },
699
+ },
652
700
  harmony_get_card_external_links: {
653
701
  description:
654
702
  "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
@@ -2241,6 +2289,28 @@ async function handleToolCall(
2241
2289
  return { success: true, ...result };
2242
2290
  }
2243
2291
 
2292
+ case "harmony_bulk_get_cards": {
2293
+ const shortIds = z
2294
+ .array(z.number().int().positive())
2295
+ .min(1)
2296
+ .max(100)
2297
+ .parse(args.shortIds);
2298
+ const projectId = getProjectId();
2299
+ const result = await client.bulkGetCards(projectId, shortIds);
2300
+ return { success: true, ...result };
2301
+ }
2302
+
2303
+ case "harmony_bulk_archive_cards": {
2304
+ const shortIds = z
2305
+ .array(z.number().int().positive())
2306
+ .min(1)
2307
+ .max(100)
2308
+ .parse(args.shortIds);
2309
+ const projectId = getProjectId();
2310
+ const result = await client.bulkArchiveCards(projectId, shortIds);
2311
+ return { success: true, ...result };
2312
+ }
2313
+
2244
2314
  // Column operations
2245
2315
  case "harmony_create_column": {
2246
2316
  const name = z.string().min(1).max(100).parse(args.name);
@@ -2425,6 +2495,12 @@ async function handleToolCall(
2425
2495
  return result;
2426
2496
  }
2427
2497
 
2498
+ case "harmony_classify_card": {
2499
+ const cardId = z.string().uuid().parse(args.cardId);
2500
+ const result = await client.classifyCard(cardId);
2501
+ return result;
2502
+ }
2503
+
2428
2504
  // Subtask operations
2429
2505
  case "harmony_create_subtask": {
2430
2506
  const cardId = z.string().uuid().parse(args.cardId);
package/src/tui/setup.ts CHANGED
@@ -331,14 +331,31 @@ async function fetchProjects(
331
331
  return data.projects || [];
332
332
  }
333
333
 
334
+ export interface SlugCandidate {
335
+ workspaceId: string;
336
+ workspaceName: string | null;
337
+ projectId: string;
338
+ }
339
+
340
+ /**
341
+ * Outcome of resolving a project slug. `ambiguous` means the same slug exists
342
+ * in more than one of the caller's workspaces (HTTP 409) — common for bare
343
+ * web-onboarded slugs like "my-first-board" when a user belongs to several
344
+ * teams. The caller must disambiguate rather than treat it as not-found.
345
+ */
346
+ export type ResolveSlugResult =
347
+ | { status: "found"; workspaceId: string; projectId: string }
348
+ | { status: "not_found" }
349
+ | { status: "ambiguous"; candidates: SlugCandidate[] };
350
+
334
351
  /**
335
352
  * Resolve a project slug to {workspaceId, projectId}. Used by
336
353
  * `npx @gethmy/mcp setup <slug>` so users don't have to copy raw UUIDs.
337
354
  */
338
- async function resolveProjectSlug(
355
+ export async function resolveProjectSlug(
339
356
  apiKey: string,
340
357
  slug: string,
341
- ): Promise<{ workspaceId: string; projectId: string } | null> {
358
+ ): Promise<ResolveSlugResult> {
342
359
  const response = await fetch(
343
360
  `${API_URL}/v1/projects/resolve/${encodeURIComponent(slug)}`,
344
361
  {
@@ -350,13 +367,26 @@ async function resolveProjectSlug(
350
367
  },
351
368
  );
352
369
 
353
- if (response.status === 404) return null;
370
+ if (response.status === 404) return { status: "not_found" };
371
+ // 409 = slug matches projects in multiple workspaces. The body carries the
372
+ // candidate workspaces so we can tell the user which ones to pick from.
373
+ if (response.status === 409) {
374
+ const data = await response.json().catch(() => ({}));
375
+ return {
376
+ status: "ambiguous",
377
+ candidates: Array.isArray(data.candidates) ? data.candidates : [],
378
+ };
379
+ }
354
380
  if (!response.ok) {
355
381
  throw new Error(`Failed to resolve project slug: ${response.status}`);
356
382
  }
357
383
 
358
384
  const data = await response.json();
359
- return { workspaceId: data.workspaceId, projectId: data.projectId };
385
+ return {
386
+ status: "found",
387
+ workspaceId: data.workspaceId,
388
+ projectId: data.projectId,
389
+ };
360
390
  }
361
391
 
362
392
  export interface FileToWrite {
@@ -753,12 +783,13 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
753
783
  options: [
754
784
  {
755
785
  value: "browser",
756
- label: "Sign in with your browser",
757
- hint: "recommended — secure, no key handling",
786
+ label: "Sign in or create an account in your browser",
787
+ hint: "recommended — new or existing users, no key handling",
758
788
  },
759
789
  {
760
790
  value: "create",
761
- label: "Create a free account",
791
+ label: "Create an account here in the terminal",
792
+ hint: "no browser",
762
793
  },
763
794
  {
764
795
  value: "apikey",
@@ -1056,6 +1087,10 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1056
1087
  let selectedWorkspaceName: string | undefined =
1057
1088
  selectedWorkspaceNameFromSignup;
1058
1089
  let selectedProjectName: string | undefined = selectedProjectNameFromSignup;
1090
+ // When a slug is ambiguous, the candidate workspaces (each already carrying
1091
+ // the matching projectId) narrow the interactive picker below to just those
1092
+ // workspaces instead of every workspace the user belongs to.
1093
+ let ambiguousCandidates: SlugCandidate[] | undefined;
1059
1094
 
1060
1095
  // Resolve project slug shorthand (e.g. `npx @gethmy/mcp setup harmony-6590761b`).
1061
1096
  // Slug wins over --workspace/--project flags only when those aren't already set
@@ -1068,10 +1103,28 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1068
1103
  spinner.start(`Resolving project slug "${options.projectSlug}"...`);
1069
1104
  try {
1070
1105
  const resolved = await resolveProjectSlug(apiKey, options.projectSlug);
1071
- if (resolved) {
1106
+ if (resolved.status === "found") {
1072
1107
  selectedWorkspaceId = selectedWorkspaceId || resolved.workspaceId;
1073
1108
  selectedProjectId = selectedProjectId || resolved.projectId;
1074
1109
  spinner.stop(colors.success(`Resolved "${options.projectSlug}"`));
1110
+ } else if (resolved.status === "ambiguous") {
1111
+ // Same slug in multiple workspaces — don't silently pick one. Surface
1112
+ // the candidates so the user knows to specify a workspace; interactive
1113
+ // context selection below still lets them recover in a TTY, narrowed to
1114
+ // just these workspaces.
1115
+ ambiguousCandidates = resolved.candidates;
1116
+ spinner.stop(
1117
+ colors.warning(
1118
+ `Slug "${options.projectSlug}" is ambiguous — it exists in multiple workspaces`,
1119
+ ),
1120
+ );
1121
+ const list = resolved.candidates
1122
+ .map((c) => ` • ${c.workspaceName ?? c.workspaceId}`)
1123
+ .join("\n");
1124
+ p.log.warning(
1125
+ `"${options.projectSlug}" matches projects in multiple workspaces:\n${list}\n` +
1126
+ "Specify the workspace with --workspace <id>, or select one below.",
1127
+ );
1075
1128
  } else {
1076
1129
  spinner.stop(
1077
1130
  colors.warning(`No project found for slug "${options.projectSlug}"`),
@@ -1108,15 +1161,28 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1108
1161
  }
1109
1162
 
1110
1163
  if (needsContext && workspaces.length > 0) {
1111
- // Select workspace
1164
+ // Select workspace. When the slug was ambiguous, restrict the choices to
1165
+ // the candidate workspaces so the user picks among the ones that actually
1166
+ // contain the slug (falling back to all workspaces if none still match).
1112
1167
  if (!selectedWorkspaceId) {
1113
- const workspaceOptions = workspaces.map((ws) => ({
1168
+ const candidateIds = new Set(
1169
+ (ambiguousCandidates ?? []).map((c) => c.workspaceId),
1170
+ );
1171
+ const pickable =
1172
+ candidateIds.size > 0
1173
+ ? workspaces.filter((ws) => candidateIds.has(ws.id))
1174
+ : workspaces;
1175
+ const workspaceChoices = pickable.length > 0 ? pickable : workspaces;
1176
+ const workspaceOptions = workspaceChoices.map((ws) => ({
1114
1177
  value: ws.id,
1115
1178
  label: ws.name,
1116
1179
  }));
1117
1180
 
1118
1181
  const workspaceSelection = await p.select({
1119
- message: "Select workspace",
1182
+ message:
1183
+ candidateIds.size > 0
1184
+ ? `Select workspace for "${options.projectSlug}"`
1185
+ : "Select workspace",
1120
1186
  options: workspaceOptions,
1121
1187
  });
1122
1188
 
@@ -1132,6 +1198,17 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1132
1198
  (w) => w.id === selectedWorkspaceId,
1133
1199
  )?.name;
1134
1200
 
1201
+ // For an ambiguous slug, the chosen workspace already pins the project —
1202
+ // each candidate carries its projectId — so skip the project picker.
1203
+ if (!selectedProjectId) {
1204
+ const chosenCandidate = ambiguousCandidates?.find(
1205
+ (c) => c.workspaceId === selectedWorkspaceId,
1206
+ );
1207
+ if (chosenCandidate) {
1208
+ selectedProjectId = chosenCandidate.projectId;
1209
+ }
1210
+ }
1211
+
1135
1212
  // Fetch and select project
1136
1213
  spinner.start("Fetching projects...");
1137
1214
  let projects: Project[] = [];
@@ -1167,6 +1244,12 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1167
1244
  selectedProjectName = projects.find(
1168
1245
  (p) => p.id === selectedProjectId,
1169
1246
  )?.name;
1247
+ } else if (selectedProjectId && !selectedProjectName) {
1248
+ // Project was pre-resolved (e.g. from an ambiguous-slug candidate) so
1249
+ // the picker was skipped — backfill the name for the summary display.
1250
+ selectedProjectName = projects.find(
1251
+ (p) => p.id === selectedProjectId,
1252
+ )?.name;
1170
1253
  }
1171
1254
  }
1172
1255
  }
@@ -1517,6 +1600,18 @@ export async function runSetup(options: SetupOptions = {}): Promise<void> {
1517
1600
  );
1518
1601
  }
1519
1602
 
1603
+ console.log("");
1604
+ console.log(` ${colors.bold("Next steps:")}`);
1605
+ console.log(
1606
+ ` 1. Open Claude Code and say: ${colors.highlight('"Show me my board"')}`,
1607
+ );
1608
+ console.log(
1609
+ ` 2. Create a card: ${colors.highlight('"Create a card called Auth token refresh"')}`,
1610
+ );
1611
+ console.log(
1612
+ ` 3. Start the daemon: ${colors.highlight("npx @gethmy/agent")}`,
1613
+ );
1614
+
1520
1615
  console.log("");
1521
1616
  console.log(` ${colors.dim("Add to new project: npx @gethmy/mcp setup")}`);
1522
1617
  console.log(