@gethmy/mcp 2.2.0 → 2.2.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/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,13 +58,8 @@ import {
59
58
  } from "./context-assembly.js";
60
59
  import { autoExpandGraph } from "./graph-expansion.js";
61
60
  import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
62
- import {
63
- type CardData,
64
- type ColumnData,
65
- generatePrompt,
66
- type MemoryData,
67
- type PromptVariant,
68
- } from "./prompt-builder.js";
61
+ import { onboardNewUser } from "./onboard.js";
62
+ import type { PromptVariant } from "./prompt-builder.js";
69
63
 
70
64
  /**
71
65
  * Dependencies injected into tool handlers.
@@ -279,18 +273,24 @@ const TOOLS = {
279
273
  },
280
274
  },
281
275
  harmony_move_card: {
282
- 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').",
283
278
  inputSchema: {
284
279
  type: "object",
285
280
  properties: {
286
281
  cardId: { type: "string", description: "Card ID to move" },
287
- 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
+ },
288
288
  position: {
289
289
  type: "number",
290
290
  description: "Position in column (0-indexed)",
291
291
  },
292
292
  },
293
- required: ["cardId", "columnId"],
293
+ required: ["cardId"],
294
294
  },
295
295
  },
296
296
  harmony_archive_card: {
@@ -426,14 +426,22 @@ const TOOLS = {
426
426
  },
427
427
  },
428
428
  harmony_add_label_to_card: {
429
- 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.",
430
431
  inputSchema: {
431
432
  type: "object",
432
433
  properties: {
433
434
  cardId: { type: "string" },
434
- 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
+ },
435
443
  },
436
- required: ["cardId", "labelId"],
444
+ required: ["cardId"],
437
445
  },
438
446
  },
439
447
  harmony_remove_label_from_card: {
@@ -1977,6 +1985,27 @@ export function registerHandlers(server: Server, deps: ToolDeps): void {
1977
1985
  });
1978
1986
  }
1979
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
+
1980
2009
  async function handleToolCall(
1981
2010
  name: string,
1982
2011
  args: Record<string, unknown>,
@@ -2047,21 +2076,41 @@ async function handleToolCall(
2047
2076
 
2048
2077
  case "harmony_move_card": {
2049
2078
  const cardId = z.string().uuid().parse(args.cardId);
2050
- const columnId = z.string().uuid().parse(args.columnId);
2051
2079
  const position =
2052
2080
  args.position !== undefined
2053
2081
  ? z.number().int().min(0).parse(args.position)
2054
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
+
2055
2105
  const result = await client.moveCard(cardId, columnId, position);
2056
2106
 
2057
2107
  // Auto-end active agent session when moving to Review or Done
2058
2108
  let sessionEnded = false;
2059
2109
  try {
2060
2110
  const { card } = result as { card: { project_id?: string } };
2061
- if (card?.project_id) {
2062
- const board = await client.getBoard(card.project_id, {
2063
- summary: true,
2064
- });
2111
+ const projectId = card?.project_id || resolvedProjectId;
2112
+ if (projectId) {
2113
+ const board = await client.getBoard(projectId, { summary: true });
2065
2114
  const columns = board.columns as Array<{
2066
2115
  id: string;
2067
2116
  name: string;
@@ -2184,7 +2233,47 @@ async function handleToolCall(
2184
2233
 
2185
2234
  case "harmony_add_label_to_card": {
2186
2235
  const cardId = z.string().uuid().parse(args.cardId);
2187
- 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
+
2188
2277
  await client.addLabelToCard(cardId, labelId);
2189
2278
  return { success: true };
2190
2279
  }
@@ -2369,12 +2458,20 @@ async function handleToolCall(
2369
2458
 
2370
2459
  if (addLabels?.length) {
2371
2460
  for (const labelName of addLabels) {
2372
- const label = labels.find(
2461
+ let label = labels.find(
2373
2462
  (l) => l.name.toLowerCase() === labelName.toLowerCase(),
2374
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
+ }
2375
2472
  if (label) {
2376
2473
  await client.addLabelToCard(cardId, label.id);
2377
- labelsAdded.push(label.name);
2474
+ labelsAdded.push(label.name ?? labelName);
2378
2475
  }
2379
2476
  }
2380
2477
  }
@@ -2577,38 +2674,30 @@ async function handleToolCall(
2577
2674
  await flushMemoryActions(client, cardId);
2578
2675
  cleanupMemorySession(cardId);
2579
2676
 
2580
- const result = await client.endAgentSession(cardId, {
2581
- status: sessionStatus,
2582
- progressPercent: endProgressPercent,
2583
- });
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
+ }
2584
2689
 
2585
- // Remove from auto-session tracking
2690
+ // Remove from auto-session tracking regardless
2586
2691
  untrack(cardId);
2587
2692
 
2588
2693
  let movedTo: string | null = null;
2589
- const _learningsExtracted = 0;
2590
2694
 
2591
- // Get card info for move and learning extraction
2592
- let _cardTitle = "";
2593
- let _cardLabels: string[] = [];
2594
- let _cardDescription = "";
2595
- let _cardSubtasks: Array<{ title: string; done: boolean }> = [];
2596
2695
  try {
2597
2696
  const { card } = await client.getCard(cardId);
2598
2697
  const typedCard = card as {
2599
2698
  project_id?: string;
2600
- title?: string;
2601
- description?: string;
2602
2699
  labels?: Array<{ id: string; name: string }>;
2603
- subtasks?: Array<{ title: string; done: boolean }>;
2604
2700
  };
2605
- _cardTitle = typedCard.title || "";
2606
- _cardLabels = (typedCard.labels || []).map((l) => l.name);
2607
- _cardDescription = typedCard.description || "";
2608
- _cardSubtasks = (typedCard.subtasks || []).map((s) => ({
2609
- title: s.title,
2610
- done: s.done,
2611
- }));
2612
2701
  const projectId = typedCard.project_id;
2613
2702
 
2614
2703
  // Remove "agent" label when session is completed (not paused)
@@ -2622,20 +2711,13 @@ async function handleToolCall(
2622
2711
  }
2623
2712
 
2624
2713
  if (moveToColumn && projectId) {
2625
- const board = await client.getBoard(projectId, {
2626
- summary: true,
2627
- });
2628
- const columns = board.columns as Array<{
2629
- id: string;
2630
- name: string;
2631
- }>;
2632
- const col = columns.find((c) =>
2633
- c.name.toLowerCase().includes(moveToColumn.toLowerCase()),
2714
+ const col = await resolveColumnByName(
2715
+ client,
2716
+ projectId,
2717
+ moveToColumn,
2634
2718
  );
2635
- if (col) {
2636
- await client.moveCard(cardId, col.id);
2637
- movedTo = col.name;
2638
- }
2719
+ await client.moveCard(cardId, col.id);
2720
+ movedTo = col.name;
2639
2721
  }
2640
2722
  } catch {
2641
2723
  // Card fetch/move failed, continue
@@ -2661,6 +2743,7 @@ async function handleToolCall(
2661
2743
 
2662
2744
  return {
2663
2745
  success: true,
2746
+ ...(sessionEndError && { sessionEndError }),
2664
2747
  movedTo,
2665
2748
  learningsExtracted: pipelineResult.learningsExtracted,
2666
2749
  feedbackAdjusted: pipelineResult.feedbackAdjusted,
@@ -2699,14 +2782,11 @@ async function handleToolCall(
2699
2782
 
2700
2783
  // Prompt generation
2701
2784
  case "harmony_generate_prompt": {
2702
- // Get card - either by UUID or short ID
2703
- let cardData: CardData;
2704
- let columnData: ColumnData | null = null;
2785
+ // Resolve card ID either directly or via short ID
2786
+ let cardId: string;
2705
2787
 
2706
2788
  if (args.cardId) {
2707
- const cardId = z.string().uuid().parse(args.cardId);
2708
- const cardResult = await client.getCard(cardId);
2709
- cardData = cardResult.card as CardData;
2789
+ cardId = z.string().uuid().parse(args.cardId);
2710
2790
  } else if (args.shortId !== undefined) {
2711
2791
  const shortId = z.number().int().positive().parse(args.shortId);
2712
2792
  const projectId =
@@ -2717,35 +2797,12 @@ async function handleToolCall(
2717
2797
  );
2718
2798
  }
2719
2799
  const cardResult = await client.getCardByShortId(projectId, shortId);
2720
- cardData = cardResult.card as CardData;
2800
+ cardId = (cardResult.card as { id: string }).id;
2721
2801
  } else {
2722
2802
  throw new Error("Either cardId or shortId must be provided");
2723
2803
  }
2724
2804
 
2725
- // Try to get column info from board
2726
- const projectIdForBoard =
2727
- (args.projectId as string) ||
2728
- getActiveProjectId() ||
2729
- (cardData as unknown as { project_id: string }).project_id;
2730
- if (projectIdForBoard) {
2731
- try {
2732
- const board = await client.getBoard(projectIdForBoard, {
2733
- summary: true,
2734
- });
2735
- const columnId = (cardData as unknown as { column_id: string })
2736
- .column_id;
2737
- const column = (
2738
- board.columns as Array<{ id: string; name: string }>
2739
- ).find((col) => col.id === columnId);
2740
- if (column) {
2741
- columnData = { name: column.name };
2742
- }
2743
- } catch {
2744
- // Column info not available, continue without it
2745
- }
2746
- }
2747
-
2748
- const variant = (args.variant as PromptVariant) || "execute";
2805
+ // Parse MCP-specific context options
2749
2806
  const contextOptions: Record<string, boolean> = {};
2750
2807
  if (args.includeSubtasks !== undefined) {
2751
2808
  contextOptions.includeSubtasks =
@@ -2761,89 +2818,24 @@ async function handleToolCall(
2761
2818
  args.includeDescription === "true";
2762
2819
  }
2763
2820
 
2764
- // Assemble context using the context assembly engine
2765
- let assembledContextStr: string | undefined;
2766
- let assemblyId: string | undefined;
2767
- let memories: MemoryData[] | undefined;
2768
-
2769
- try {
2770
- const workspaceId = deps.getActiveWorkspaceId();
2771
- if (workspaceId && cardData.title) {
2772
- const cardLabels = (cardData.labels || []).map((l) => l.name);
2773
- const taskContext = [cardData.title, cardData.description || ""]
2774
- .filter(Boolean)
2775
- .join(" ");
2776
-
2777
- const assembled = await assembleContext({
2778
- workspaceId,
2779
- projectId: getActiveProjectId() || undefined,
2780
- taskContext,
2781
- cardLabels,
2782
- cardId: cardData.id,
2783
- client,
2784
- });
2785
-
2786
- if (assembled.context) {
2787
- assembledContextStr = assembled.context;
2788
- assemblyId = assembled.manifest.assemblyId;
2789
- cacheManifest(assembled.manifest);
2790
- }
2791
- }
2792
- } catch {
2793
- // Context assembly failed, try legacy fallback
2794
- try {
2795
- const workspaceId = deps.getActiveWorkspaceId();
2796
- if (workspaceId && cardData.title) {
2797
- const memoryResult = await client.searchMemoryEntities(
2798
- workspaceId,
2799
- cardData.title,
2800
- {
2801
- project_id: getActiveProjectId() || undefined,
2802
- limit: 5,
2803
- },
2804
- );
2805
- if (memoryResult.entities?.length > 0) {
2806
- memories = memoryResult.entities.map((e: unknown) => {
2807
- const entity = e as {
2808
- id: string;
2809
- type: string;
2810
- title: string;
2811
- content: string;
2812
- confidence: number;
2813
- tags: string[];
2814
- };
2815
- return {
2816
- id: entity.id,
2817
- type: entity.type,
2818
- title: entity.title,
2819
- content: entity.content,
2820
- confidence: entity.confidence,
2821
- tags: entity.tags || [],
2822
- };
2823
- });
2824
- }
2825
- }
2826
- } catch {
2827
- // Memory fetch also failed, continue without memories
2828
- }
2829
- }
2830
-
2831
- const result = generatePrompt({
2832
- card: cardData,
2833
- column: columnData,
2834
- variant,
2835
- contextOptions,
2821
+ // Delegate to the shared prompt generation pipeline
2822
+ const result = await client.generateCardPrompt({
2823
+ cardId,
2824
+ workspaceId: deps.getActiveWorkspaceId() || "",
2825
+ projectId:
2826
+ (args.projectId as string) || getActiveProjectId() || undefined,
2827
+ variant: (args.variant as PromptVariant) || "execute",
2836
2828
  customConstraints: args.customConstraints as string | undefined,
2837
- memories,
2838
- assembledContext: assembledContextStr,
2839
- assemblyId,
2829
+ contextOptions,
2840
2830
  });
2841
2831
 
2832
+ // MCP-specific: cache the assembly manifest for the feedback loop
2833
+ if (result.assemblyId) {
2834
+ trackSessionAssembly(cardId, result.assemblyId);
2835
+ }
2836
+
2842
2837
  return {
2843
2838
  success: true,
2844
- cardId: cardData.id,
2845
- shortId: cardData.short_id,
2846
- title: cardData.title,
2847
2839
  ...result,
2848
2840
  };
2849
2841
  }
@@ -3037,8 +3029,7 @@ async function handleToolCall(
3037
3029
  // Track memory write action and flush (fire-and-forget)
3038
3030
  const updateMemSession = getActiveMemorySession();
3039
3031
  if (updateMemSession) {
3040
- const updateTitle =
3041
- (updates.title as string) || entityId.slice(0, 8);
3032
+ const updateTitle = (updates.title as string) || entityId.slice(0, 8);
3042
3033
  appendMemoryAction(
3043
3034
  updateMemSession.cardId,
3044
3035
  `Updated memory: ${updateTitle}`,
@@ -3871,69 +3862,31 @@ async function handleToolCall(
3871
3862
  const projectName = (args.projectName as string) || "My First Project";
3872
3863
  const template = (args.template as string) || "kanban";
3873
3864
  const keyName = (args.keyName as string) || "mcp-agent";
3874
- const apiUrl = deps.getApiUrl();
3875
3865
 
3876
- // 1. Signup
3877
- const signupResult = await signupUser(apiUrl, {
3866
+ const result = await onboardNewUser({
3878
3867
  email,
3879
3868
  password,
3880
- full_name: fullName,
3881
- });
3882
- const token = signupResult.session.access_token;
3883
-
3884
- // 2. Create workspace
3885
- const workspaceResult = await requestWithBearer<{
3886
- workspace: {
3887
- id: string;
3888
- name: string;
3889
- slug: string;
3890
- created_at: string;
3891
- };
3892
- }>(apiUrl, token, "POST", "/workspaces", {
3893
- name: workspaceName,
3894
- });
3895
-
3896
- // 3. Create project
3897
- const projectResult = await requestWithBearer<{
3898
- project: { id: string; name: string; slug: string };
3899
- columns: unknown[];
3900
- }>(apiUrl, token, "POST", "/projects", {
3901
- workspaceId: workspaceResult.workspace.id,
3902
- name: projectName,
3869
+ fullName,
3870
+ workspaceName,
3871
+ projectName,
3903
3872
  template,
3873
+ keyName,
3874
+ apiUrl: deps.getApiUrl(),
3904
3875
  });
3905
3876
 
3906
- // 4. Generate API key
3907
- const keyResult = await requestWithBearer<{
3908
- apiKey: {
3909
- id: string;
3910
- name: string;
3911
- prefix: string;
3912
- created_at: string;
3913
- };
3914
- rawKey: string;
3915
- }>(apiUrl, token, "POST", "/api-keys", {
3916
- name: keyName,
3917
- });
3918
-
3919
- // 5. Save config
3920
- deps.saveConfig({ apiKey: keyResult.rawKey });
3921
- deps.setActiveWorkspace(workspaceResult.workspace.id);
3922
- deps.setActiveProject(projectResult.project.id);
3923
-
3924
- // 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);
3925
3881
  deps.resetClient();
3926
3882
 
3927
3883
  return {
3928
3884
  success: true,
3929
- user: signupResult.user,
3930
- workspace: workspaceResult.workspace,
3931
- project: projectResult.project,
3932
- columns: projectResult.columns,
3933
- apiKey: {
3934
- rawKey: keyResult.rawKey,
3935
- prefix: keyResult.apiKey.prefix,
3936
- },
3885
+ user: result.user,
3886
+ workspace: result.workspace,
3887
+ project: result.project,
3888
+ columns: result.columns,
3889
+ apiKey: result.apiKey,
3937
3890
  message: `Onboarding complete! Account created for ${email}. Workspace "${workspaceName}" and project "${projectName}" are ready. API key saved to config.`,
3938
3891
  };
3939
3892
  }
@@ -4053,13 +4006,17 @@ export class HarmonyMCPServer {
4053
4006
  await this.server.connect(transport);
4054
4007
  console.error("Harmony MCP server running on stdio");
4055
4008
 
4056
- // Initialize auto-session tracking
4009
+ // Initialize auto-session tracking with MCP client identity detection
4057
4010
  const configDeps = createConfigDeps();
4058
4011
  initAutoSession(
4059
4012
  async (client, cardId, status) => {
4060
4013
  await runEndSessionPipeline(client, configDeps, cardId, status);
4061
4014
  },
4062
4015
  () => getClient(),
4016
+ () => {
4017
+ const cv = this.server.getClientVersion();
4018
+ return cv ? { name: cv.name, version: cv.version } : null;
4019
+ },
4063
4020
  );
4064
4021
 
4065
4022
  // Graceful shutdown: end all auto-sessions
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