@gethmy/mcp 2.2.1 → 2.2.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.2.1",
3
+ "version": "2.2.3",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -69,6 +69,7 @@
69
69
  "zod": "^4.3.6"
70
70
  },
71
71
  "devDependencies": {
72
+ "@harmony/memory": "workspace:*",
72
73
  "@types/node": "^25.5.0",
73
74
  "typescript": "^6.0.1"
74
75
  }
@@ -6,7 +6,7 @@
6
6
  * after 10 minutes of inactivity or when a different card is worked on.
7
7
  *
8
8
  * Agent identity is resolved from the MCP client's `initialize` handshake
9
- * (clientInfo.name), so "Claude Code", "Cursor", "Windsurf", etc. are
9
+ * (clientInfo.name), so "Claude Code", "Cursor", "Codex", etc. are
10
10
  * detected automatically — no hardcoded fallback needed.
11
11
  */
12
12
 
package/src/cli.ts CHANGED
@@ -29,6 +29,11 @@ program
29
29
  .command("serve")
30
30
  .description("Start the MCP server (stdio transport)")
31
31
  .action(async () => {
32
+ if (!isConfigured()) {
33
+ console.error("No API key configured.");
34
+ console.error("Run: npx @gethmy/mcp setup");
35
+ process.exit(1);
36
+ }
32
37
  await refreshSkills();
33
38
  const server = new HarmonyMCPServer();
34
39
  await server.run();
@@ -147,6 +152,8 @@ program
147
152
  .option("-p, --project <id>", "Set project context")
148
153
  .option("--skip-context", "Skip workspace/project selection")
149
154
  .option("--skip-docs", "Skip project docs scaffold/verification")
155
+ .option("--new", "Create a new account (skip the choice prompt)")
156
+ .option("-n, --name <name>", "Full name (for account creation)")
150
157
  .action(async (options) => {
151
158
  await runSetup({
152
159
  force: options.force,
@@ -162,6 +169,8 @@ program
162
169
  projectId: options.project,
163
170
  skipContext: options.skipContext,
164
171
  skipDocs: options.skipDocs,
172
+ newAccount: options.new,
173
+ name: options.name,
165
174
  });
166
175
  });
167
176
 
package/src/onboard.ts ADDED
@@ -0,0 +1,93 @@
1
+ import { requestWithBearer, signupUser } from "./api-client.js";
2
+ import { getApiUrl } from "./config.js";
3
+
4
+ export interface OnboardParams {
5
+ email: string;
6
+ password: string;
7
+ fullName: string;
8
+ workspaceName?: string;
9
+ projectName?: string;
10
+ template?: string;
11
+ keyName?: string;
12
+ apiUrl?: string;
13
+ }
14
+
15
+ export interface OnboardResult {
16
+ user: { id: string; email: string; full_name: string };
17
+ workspace: { id: string; name: string; slug: string; created_at: string };
18
+ project: { id: string; name: string; slug: string };
19
+ columns: unknown[];
20
+ apiKey: {
21
+ rawKey: string;
22
+ prefix: string;
23
+ };
24
+ }
25
+
26
+ export async function onboardNewUser(
27
+ params: OnboardParams,
28
+ ): Promise<OnboardResult> {
29
+ const {
30
+ email,
31
+ password,
32
+ fullName,
33
+ workspaceName = `${fullName}'s Workspace`,
34
+ projectName = "My First Board",
35
+ template = "kanban",
36
+ keyName = "mcp-agent",
37
+ apiUrl = getApiUrl(),
38
+ } = params;
39
+
40
+ // 1. Signup
41
+ const signupResult = await signupUser(apiUrl, {
42
+ email,
43
+ password,
44
+ full_name: fullName,
45
+ });
46
+ const token = signupResult.session.access_token;
47
+
48
+ // 2. Create workspace
49
+ const workspaceResult = await requestWithBearer<{
50
+ workspace: {
51
+ id: string;
52
+ name: string;
53
+ slug: string;
54
+ created_at: string;
55
+ };
56
+ }>(apiUrl, token, "POST", "/workspaces", {
57
+ name: workspaceName,
58
+ });
59
+
60
+ // 3. Create project
61
+ const projectResult = await requestWithBearer<{
62
+ project: { id: string; name: string; slug: string };
63
+ columns: unknown[];
64
+ }>(apiUrl, token, "POST", "/projects", {
65
+ workspaceId: workspaceResult.workspace.id,
66
+ name: projectName,
67
+ template,
68
+ });
69
+
70
+ // 4. Generate API key
71
+ const keyResult = await requestWithBearer<{
72
+ apiKey: {
73
+ id: string;
74
+ name: string;
75
+ prefix: string;
76
+ created_at: string;
77
+ };
78
+ rawKey: string;
79
+ }>(apiUrl, token, "POST", "/api-keys", {
80
+ name: keyName,
81
+ });
82
+
83
+ return {
84
+ user: signupResult.user,
85
+ workspace: workspaceResult.workspace,
86
+ project: projectResult.project,
87
+ columns: projectResult.columns,
88
+ apiKey: {
89
+ rawKey: keyResult.rawKey,
90
+ prefix: keyResult.apiKey.prefix,
91
+ },
92
+ };
93
+ }
package/src/server.ts CHANGED
@@ -23,7 +23,6 @@ import {
23
23
  import {
24
24
  getClient,
25
25
  type HarmonyApiClient,
26
- requestWithBearer,
27
26
  resetClient,
28
27
  signupUser,
29
28
  } from "./api-client.js";
@@ -59,6 +58,7 @@ import {
59
58
  } from "./context-assembly.js";
60
59
  import { autoExpandGraph } from "./graph-expansion.js";
61
60
  import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
61
+ import { onboardNewUser } from "./onboard.js";
62
62
  import type { PromptVariant } from "./prompt-builder.js";
63
63
 
64
64
  /**
@@ -273,18 +273,24 @@ const TOOLS = {
273
273
  },
274
274
  },
275
275
  harmony_move_card: {
276
- description: "Move a card to a different column or position",
276
+ description:
277
+ "Move a card to a different column or position. Provide either columnId (UUID) or columnName (e.g. 'Review', 'Done').",
277
278
  inputSchema: {
278
279
  type: "object",
279
280
  properties: {
280
281
  cardId: { type: "string", description: "Card ID to move" },
281
- columnId: { type: "string", description: "Target column ID" },
282
+ columnId: { type: "string", description: "Target column ID (UUID)" },
283
+ columnName: {
284
+ type: "string",
285
+ description:
286
+ "Target column name (e.g. 'To Do', 'In Progress', 'Review', 'Done'). Used if columnId is not provided.",
287
+ },
282
288
  position: {
283
289
  type: "number",
284
290
  description: "Position in column (0-indexed)",
285
291
  },
286
292
  },
287
- required: ["cardId", "columnId"],
293
+ required: ["cardId"],
288
294
  },
289
295
  },
290
296
  harmony_archive_card: {
@@ -420,14 +426,22 @@ const TOOLS = {
420
426
  },
421
427
  },
422
428
  harmony_add_label_to_card: {
423
- description: "Add a label to a card",
429
+ description:
430
+ "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
424
431
  inputSchema: {
425
432
  type: "object",
426
433
  properties: {
427
434
  cardId: { type: "string" },
428
- labelId: { type: "string" },
435
+ labelId: {
436
+ type: "string",
437
+ description: "Label ID (optional if labelName provided)",
438
+ },
439
+ labelName: {
440
+ type: "string",
441
+ description: "Label name — will look up or create if not found",
442
+ },
429
443
  },
430
- required: ["cardId", "labelId"],
444
+ required: ["cardId"],
431
445
  },
432
446
  },
433
447
  harmony_remove_label_from_card: {
@@ -1971,6 +1985,27 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
1971
1985
  });
1972
1986
  }
1973
1987
 
1988
+ /** Resolve a column name to its ID. Prefers exact match, falls back to substring. */
1989
+ async function resolveColumnByName(
1990
+ client: HarmonyApiClient,
1991
+ projectId: string,
1992
+ columnName: string,
1993
+ ): Promise<{ id: string; name: string }> {
1994
+ const board = await client.getBoard(projectId, { summary: true });
1995
+ const columns = board.columns as Array<{ id: string; name: string }>;
1996
+ const lower = columnName.toLowerCase();
1997
+ const col =
1998
+ columns.find((c) => c.name.toLowerCase() === lower) ||
1999
+ columns.find((c) => c.name.toLowerCase().includes(lower));
2000
+ if (!col) {
2001
+ const available = columns.map((c) => c.name).join(", ");
2002
+ throw new Error(
2003
+ `Column "${columnName}" not found. Available columns: ${available}`,
2004
+ );
2005
+ }
2006
+ return col;
2007
+ }
2008
+
1974
2009
  async function handleToolCall(
1975
2010
  name: string,
1976
2011
  args: Record<string, unknown>,
@@ -2041,21 +2076,41 @@ async function handleToolCall(
2041
2076
 
2042
2077
  case "harmony_move_card": {
2043
2078
  const cardId = z.string().uuid().parse(args.cardId);
2044
- const columnId = z.string().uuid().parse(args.columnId);
2045
2079
  const position =
2046
2080
  args.position !== undefined
2047
2081
  ? z.number().int().min(0).parse(args.position)
2048
2082
  : undefined;
2083
+
2084
+ // Resolve columnId — accept UUID directly or resolve from columnName
2085
+ let columnId: string;
2086
+ let resolvedProjectId: string | undefined;
2087
+ if (args.columnId) {
2088
+ columnId = z.string().uuid().parse(args.columnId);
2089
+ } else if (args.columnName) {
2090
+ const columnName = z.string().parse(args.columnName);
2091
+ const { card: cardForProject } = await client.getCard(cardId);
2092
+ resolvedProjectId = (cardForProject as { project_id?: string })
2093
+ ?.project_id;
2094
+ if (!resolvedProjectId) throw new Error("Card has no project");
2095
+ const col = await resolveColumnByName(
2096
+ client,
2097
+ resolvedProjectId,
2098
+ columnName,
2099
+ );
2100
+ columnId = col.id;
2101
+ } else {
2102
+ throw new Error("Either columnId or columnName is required");
2103
+ }
2104
+
2049
2105
  const result = await client.moveCard(cardId, columnId, position);
2050
2106
 
2051
2107
  // Auto-end active agent session when moving to Review or Done
2052
2108
  let sessionEnded = false;
2053
2109
  try {
2054
2110
  const { card } = result as { card: { project_id?: string } };
2055
- if (card?.project_id) {
2056
- const board = await client.getBoard(card.project_id, {
2057
- summary: true,
2058
- });
2111
+ const projectId = card?.project_id || resolvedProjectId;
2112
+ if (projectId) {
2113
+ const board = await client.getBoard(projectId, { summary: true });
2059
2114
  const columns = board.columns as Array<{
2060
2115
  id: string;
2061
2116
  name: string;
@@ -2178,7 +2233,47 @@ async function handleToolCall(
2178
2233
 
2179
2234
  case "harmony_add_label_to_card": {
2180
2235
  const cardId = z.string().uuid().parse(args.cardId);
2181
- const labelId = z.string().uuid().parse(args.labelId);
2236
+ let labelId = args.labelId
2237
+ ? z.string().uuid().parse(args.labelId)
2238
+ : undefined;
2239
+ const labelName = args.labelName
2240
+ ? z.string().min(1).max(100).parse(args.labelName)
2241
+ : undefined;
2242
+
2243
+ // If labelName provided without labelId, look up or create the label
2244
+ if (!labelId && labelName) {
2245
+ const { card } = await client.getCard(cardId);
2246
+ const projectId = (card as { project_id?: string }).project_id;
2247
+ if (!projectId) {
2248
+ throw new Error(
2249
+ "Cannot resolve label by name: card has no project_id",
2250
+ );
2251
+ }
2252
+ if (projectId) {
2253
+ const board = await client.getBoard(projectId, { summary: true });
2254
+ const labels = board.labels as Array<{
2255
+ id: string;
2256
+ name: string;
2257
+ }>;
2258
+ const existing = labels.find(
2259
+ (l) => l.name.toLowerCase() === labelName.toLowerCase(),
2260
+ );
2261
+ if (existing) {
2262
+ labelId = existing.id;
2263
+ } else {
2264
+ const created = await client.createLabel(projectId, {
2265
+ name: labelName,
2266
+ color: "#57b8a5",
2267
+ });
2268
+ labelId = (created as { label: { id: string } }).label.id;
2269
+ }
2270
+ }
2271
+ }
2272
+
2273
+ if (!labelId) {
2274
+ throw new Error("Either labelId or labelName must be provided");
2275
+ }
2276
+
2182
2277
  await client.addLabelToCard(cardId, labelId);
2183
2278
  return { success: true };
2184
2279
  }
@@ -2363,12 +2458,20 @@ async function handleToolCall(
2363
2458
 
2364
2459
  if (addLabels?.length) {
2365
2460
  for (const labelName of addLabels) {
2366
- const label = labels.find(
2461
+ let label = labels.find(
2367
2462
  (l) => l.name.toLowerCase() === labelName.toLowerCase(),
2368
2463
  );
2464
+ if (!label && projectId) {
2465
+ const created = await client.createLabel(projectId, {
2466
+ name: labelName,
2467
+ color: "#57b8a5",
2468
+ });
2469
+ label = (created as { label: { id: string; name: string } })
2470
+ .label;
2471
+ }
2369
2472
  if (label) {
2370
2473
  await client.addLabelToCard(cardId, label.id);
2371
- labelsAdded.push(label.name);
2474
+ labelsAdded.push(label.name ?? labelName);
2372
2475
  }
2373
2476
  }
2374
2477
  }
@@ -2571,38 +2674,30 @@ async function handleToolCall(
2571
2674
  await flushMemoryActions(client, cardId);
2572
2675
  cleanupMemorySession(cardId);
2573
2676
 
2574
- const result = await client.endAgentSession(cardId, {
2575
- status: sessionStatus,
2576
- progressPercent: endProgressPercent,
2577
- });
2677
+ // End the session — tolerate failure (e.g., session already ended or not found)
2678
+ let result: { session: unknown } = { session: null };
2679
+ let sessionEndError: string | null = null;
2680
+ try {
2681
+ result = await client.endAgentSession(cardId, {
2682
+ status: sessionStatus,
2683
+ progressPercent: endProgressPercent,
2684
+ });
2685
+ } catch (err) {
2686
+ sessionEndError =
2687
+ err instanceof Error ? err.message : "Failed to end session";
2688
+ }
2578
2689
 
2579
- // Remove from auto-session tracking
2690
+ // Remove from auto-session tracking regardless
2580
2691
  untrack(cardId);
2581
2692
 
2582
2693
  let movedTo: string | null = null;
2583
- const _learningsExtracted = 0;
2584
2694
 
2585
- // Get card info for move and learning extraction
2586
- let _cardTitle = "";
2587
- let _cardLabels: string[] = [];
2588
- let _cardDescription = "";
2589
- let _cardSubtasks: Array<{ title: string; done: boolean }> = [];
2590
2695
  try {
2591
2696
  const { card } = await client.getCard(cardId);
2592
2697
  const typedCard = card as {
2593
2698
  project_id?: string;
2594
- title?: string;
2595
- description?: string;
2596
2699
  labels?: Array<{ id: string; name: string }>;
2597
- subtasks?: Array<{ title: string; done: boolean }>;
2598
2700
  };
2599
- _cardTitle = typedCard.title || "";
2600
- _cardLabels = (typedCard.labels || []).map((l) => l.name);
2601
- _cardDescription = typedCard.description || "";
2602
- _cardSubtasks = (typedCard.subtasks || []).map((s) => ({
2603
- title: s.title,
2604
- done: s.done,
2605
- }));
2606
2701
  const projectId = typedCard.project_id;
2607
2702
 
2608
2703
  // Remove "agent" label when session is completed (not paused)
@@ -2616,20 +2711,13 @@ async function handleToolCall(
2616
2711
  }
2617
2712
 
2618
2713
  if (moveToColumn && projectId) {
2619
- const board = await client.getBoard(projectId, {
2620
- summary: true,
2621
- });
2622
- const columns = board.columns as Array<{
2623
- id: string;
2624
- name: string;
2625
- }>;
2626
- const col = columns.find((c) =>
2627
- c.name.toLowerCase().includes(moveToColumn.toLowerCase()),
2714
+ const col = await resolveColumnByName(
2715
+ client,
2716
+ projectId,
2717
+ moveToColumn,
2628
2718
  );
2629
- if (col) {
2630
- await client.moveCard(cardId, col.id);
2631
- movedTo = col.name;
2632
- }
2719
+ await client.moveCard(cardId, col.id);
2720
+ movedTo = col.name;
2633
2721
  }
2634
2722
  } catch {
2635
2723
  // Card fetch/move failed, continue
@@ -2655,6 +2743,7 @@ async function handleToolCall(
2655
2743
 
2656
2744
  return {
2657
2745
  success: true,
2746
+ ...(sessionEndError && { sessionEndError }),
2658
2747
  movedTo,
2659
2748
  learningsExtracted: pipelineResult.learningsExtracted,
2660
2749
  feedbackAdjusted: pipelineResult.feedbackAdjusted,
@@ -3773,69 +3862,31 @@ async function handleToolCall(
3773
3862
  const projectName = (args.projectName as string) || "My First Project";
3774
3863
  const template = (args.template as string) || "kanban";
3775
3864
  const keyName = (args.keyName as string) || "mcp-agent";
3776
- const apiUrl = deps.getApiUrl();
3777
3865
 
3778
- // 1. Signup
3779
- const signupResult = await signupUser(apiUrl, {
3866
+ const result = await onboardNewUser({
3780
3867
  email,
3781
3868
  password,
3782
- full_name: fullName,
3783
- });
3784
- const token = signupResult.session.access_token;
3785
-
3786
- // 2. Create workspace
3787
- const workspaceResult = await requestWithBearer<{
3788
- workspace: {
3789
- id: string;
3790
- name: string;
3791
- slug: string;
3792
- created_at: string;
3793
- };
3794
- }>(apiUrl, token, "POST", "/workspaces", {
3795
- name: workspaceName,
3796
- });
3797
-
3798
- // 3. Create project
3799
- const projectResult = await requestWithBearer<{
3800
- project: { id: string; name: string; slug: string };
3801
- columns: unknown[];
3802
- }>(apiUrl, token, "POST", "/projects", {
3803
- workspaceId: workspaceResult.workspace.id,
3804
- name: projectName,
3869
+ fullName,
3870
+ workspaceName,
3871
+ projectName,
3805
3872
  template,
3873
+ keyName,
3874
+ apiUrl: deps.getApiUrl(),
3806
3875
  });
3807
3876
 
3808
- // 4. Generate API key
3809
- const keyResult = await requestWithBearer<{
3810
- apiKey: {
3811
- id: string;
3812
- name: string;
3813
- prefix: string;
3814
- created_at: string;
3815
- };
3816
- rawKey: string;
3817
- }>(apiUrl, token, "POST", "/api-keys", {
3818
- name: keyName,
3819
- });
3820
-
3821
- // 5. Save config
3822
- deps.saveConfig({ apiKey: keyResult.rawKey });
3823
- deps.setActiveWorkspace(workspaceResult.workspace.id);
3824
- deps.setActiveProject(projectResult.project.id);
3825
-
3826
- // 6. Reset client so singleton picks up new key
3877
+ // Save config and reset client
3878
+ deps.saveConfig({ apiKey: result.apiKey.rawKey });
3879
+ deps.setActiveWorkspace(result.workspace.id);
3880
+ deps.setActiveProject(result.project.id);
3827
3881
  deps.resetClient();
3828
3882
 
3829
3883
  return {
3830
3884
  success: true,
3831
- user: signupResult.user,
3832
- workspace: workspaceResult.workspace,
3833
- project: projectResult.project,
3834
- columns: projectResult.columns,
3835
- apiKey: {
3836
- rawKey: keyResult.rawKey,
3837
- prefix: keyResult.apiKey.prefix,
3838
- },
3885
+ user: result.user,
3886
+ workspace: result.workspace,
3887
+ project: result.project,
3888
+ columns: result.columns,
3889
+ apiKey: result.apiKey,
3839
3890
  message: `Onboarding complete! Account created for ${email}. Workspace "${workspaceName}" and project "${projectName}" are ready. API key saved to config.`,
3840
3891
  };
3841
3892
  }
package/src/skills.ts CHANGED
@@ -7,7 +7,7 @@ export const SKILLS_VERSION = "3";
7
7
  const VERSION_MARKER_PREFIX = "<!-- skills-version:";
8
8
 
9
9
  /**
10
- * Legacy workflow prompt used by Codex, Cursor, Windsurf agents.
10
+ * Legacy workflow prompt used by Codex, Cursor agents.
11
11
  * Claude Code skills use the newer SKILL_DEFINITIONS content instead.
12
12
  */
13
13
  export const HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow