@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.
@@ -4,14 +4,14 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
4
4
  import { CallToolRequestSchema, ListResourcesRequestSchema, ListToolsRequestSchema, ReadResourceRequestSchema, } from "@modelcontextprotocol/sdk/types.js";
5
5
  import { z } from "zod";
6
6
  import { detectContradictions, extractLearnings, extractMidSessionLearnings, } from "./active-learning.js";
7
- import { getClient, requestWithBearer, resetClient, signupUser, } from "./api-client.js";
7
+ import { getClient, resetClient, signupUser, } from "./api-client.js";
8
8
  import { AUTO_START_TRIGGERS, destroyAutoSession, initAutoSession, markExplicit, shutdownAllSessions, trackActivity, untrack, } from "./auto-session.js";
9
9
  import { getActiveProjectId, getActiveWorkspaceId, getApiUrl, getMemoryDir, getUserEmail, isConfigured, saveConfig, setActiveProject, setActiveWorkspace, } from "./config.js";
10
10
  import { consolidateMemories } from "./consolidation.js";
11
11
  import { assembleContext, cacheManifest, computeRelevanceScore, getCachedManifest, mapToContextEntity, recordContextFeedback, trackSessionAssembly, } from "./context-assembly.js";
12
12
  import { autoExpandGraph } from "./graph-expansion.js";
13
13
  import { runLifecycleMaintenance } from "./lifecycle-maintenance.js";
14
- import { generatePrompt, } from "./prompt-builder.js";
14
+ import { onboardNewUser } from "./onboard.js";
15
15
  const memorySessions = new Map();
16
16
  function initMemorySession(cardId, agentIdentifier, agentName) {
17
17
  memorySessions.set(cardId, {
@@ -169,18 +169,22 @@ const TOOLS = {
169
169
  },
170
170
  },
171
171
  harmony_move_card: {
172
- description: "Move a card to a different column or position",
172
+ description: "Move a card to a different column or position. Provide either columnId (UUID) or columnName (e.g. 'Review', 'Done').",
173
173
  inputSchema: {
174
174
  type: "object",
175
175
  properties: {
176
176
  cardId: { type: "string", description: "Card ID to move" },
177
- columnId: { type: "string", description: "Target column ID" },
177
+ columnId: { type: "string", description: "Target column ID (UUID)" },
178
+ columnName: {
179
+ type: "string",
180
+ description: "Target column name (e.g. 'To Do', 'In Progress', 'Review', 'Done'). Used if columnId is not provided.",
181
+ },
178
182
  position: {
179
183
  type: "number",
180
184
  description: "Position in column (0-indexed)",
181
185
  },
182
186
  },
183
- required: ["cardId", "columnId"],
187
+ required: ["cardId"],
184
188
  },
185
189
  },
186
190
  harmony_archive_card: {
@@ -313,14 +317,21 @@ const TOOLS = {
313
317
  },
314
318
  },
315
319
  harmony_add_label_to_card: {
316
- description: "Add a label to a card",
320
+ description: "Add a label to a card. Provide labelId directly, or labelName to look up (or auto-create) the label by name.",
317
321
  inputSchema: {
318
322
  type: "object",
319
323
  properties: {
320
324
  cardId: { type: "string" },
321
- labelId: { type: "string" },
325
+ labelId: {
326
+ type: "string",
327
+ description: "Label ID (optional if labelName provided)",
328
+ },
329
+ labelName: {
330
+ type: "string",
331
+ description: "Label name — will look up or create if not found",
332
+ },
322
333
  },
323
- required: ["cardId", "labelId"],
334
+ required: ["cardId"],
324
335
  },
325
336
  },
326
337
  harmony_remove_label_from_card: {
@@ -1704,6 +1715,19 @@ export function registerHandlers(server, deps) {
1704
1715
  throw new Error(`Unknown resource: ${uri}`);
1705
1716
  });
1706
1717
  }
1718
+ /** Resolve a column name to its ID. Prefers exact match, falls back to substring. */
1719
+ async function resolveColumnByName(client, projectId, columnName) {
1720
+ const board = await client.getBoard(projectId, { summary: true });
1721
+ const columns = board.columns;
1722
+ const lower = columnName.toLowerCase();
1723
+ const col = columns.find((c) => c.name.toLowerCase() === lower) ||
1724
+ columns.find((c) => c.name.toLowerCase().includes(lower));
1725
+ if (!col) {
1726
+ const available = columns.map((c) => c.name).join(", ");
1727
+ throw new Error(`Column "${columnName}" not found. Available columns: ${available}`);
1728
+ }
1729
+ return col;
1730
+ }
1707
1731
  async function handleToolCall(name, args, deps) {
1708
1732
  // Unauthenticated tools that don't require an API key
1709
1733
  const unauthenticatedTools = ["harmony_signup", "harmony_onboard"];
@@ -1758,19 +1782,36 @@ async function handleToolCall(name, args, deps) {
1758
1782
  }
1759
1783
  case "harmony_move_card": {
1760
1784
  const cardId = z.string().uuid().parse(args.cardId);
1761
- const columnId = z.string().uuid().parse(args.columnId);
1762
1785
  const position = args.position !== undefined
1763
1786
  ? z.number().int().min(0).parse(args.position)
1764
1787
  : undefined;
1788
+ // Resolve columnId — accept UUID directly or resolve from columnName
1789
+ let columnId;
1790
+ let resolvedProjectId;
1791
+ if (args.columnId) {
1792
+ columnId = z.string().uuid().parse(args.columnId);
1793
+ }
1794
+ else if (args.columnName) {
1795
+ const columnName = z.string().parse(args.columnName);
1796
+ const { card: cardForProject } = await client.getCard(cardId);
1797
+ resolvedProjectId = cardForProject
1798
+ ?.project_id;
1799
+ if (!resolvedProjectId)
1800
+ throw new Error("Card has no project");
1801
+ const col = await resolveColumnByName(client, resolvedProjectId, columnName);
1802
+ columnId = col.id;
1803
+ }
1804
+ else {
1805
+ throw new Error("Either columnId or columnName is required");
1806
+ }
1765
1807
  const result = await client.moveCard(cardId, columnId, position);
1766
1808
  // Auto-end active agent session when moving to Review or Done
1767
1809
  let sessionEnded = false;
1768
1810
  try {
1769
1811
  const { card } = result;
1770
- if (card?.project_id) {
1771
- const board = await client.getBoard(card.project_id, {
1772
- summary: true,
1773
- });
1812
+ const projectId = card?.project_id || resolvedProjectId;
1813
+ if (projectId) {
1814
+ const board = await client.getBoard(projectId, { summary: true });
1774
1815
  const columns = board.columns;
1775
1816
  const destCol = columns.find((c) => c.id === columnId);
1776
1817
  const colName = destCol?.name?.toLowerCase() || "";
@@ -1871,7 +1912,38 @@ async function handleToolCall(name, args, deps) {
1871
1912
  }
1872
1913
  case "harmony_add_label_to_card": {
1873
1914
  const cardId = z.string().uuid().parse(args.cardId);
1874
- const labelId = z.string().uuid().parse(args.labelId);
1915
+ let labelId = args.labelId
1916
+ ? z.string().uuid().parse(args.labelId)
1917
+ : undefined;
1918
+ const labelName = args.labelName
1919
+ ? z.string().min(1).max(100).parse(args.labelName)
1920
+ : undefined;
1921
+ // If labelName provided without labelId, look up or create the label
1922
+ if (!labelId && labelName) {
1923
+ const { card } = await client.getCard(cardId);
1924
+ const projectId = card.project_id;
1925
+ if (!projectId) {
1926
+ throw new Error("Cannot resolve label by name: card has no project_id");
1927
+ }
1928
+ if (projectId) {
1929
+ const board = await client.getBoard(projectId, { summary: true });
1930
+ const labels = board.labels;
1931
+ const existing = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
1932
+ if (existing) {
1933
+ labelId = existing.id;
1934
+ }
1935
+ else {
1936
+ const created = await client.createLabel(projectId, {
1937
+ name: labelName,
1938
+ color: "#57b8a5",
1939
+ });
1940
+ labelId = created.label.id;
1941
+ }
1942
+ }
1943
+ }
1944
+ if (!labelId) {
1945
+ throw new Error("Either labelId or labelName must be provided");
1946
+ }
1875
1947
  await client.addLabelToCard(cardId, labelId);
1876
1948
  return { success: true };
1877
1949
  }
@@ -2016,10 +2088,18 @@ async function handleToolCall(name, args, deps) {
2016
2088
  }
2017
2089
  if (addLabels?.length) {
2018
2090
  for (const labelName of addLabels) {
2019
- const label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
2091
+ let label = labels.find((l) => l.name.toLowerCase() === labelName.toLowerCase());
2092
+ if (!label && projectId) {
2093
+ const created = await client.createLabel(projectId, {
2094
+ name: labelName,
2095
+ color: "#57b8a5",
2096
+ });
2097
+ label = created
2098
+ .label;
2099
+ }
2020
2100
  if (label) {
2021
2101
  await client.addLabelToCard(cardId, label.id);
2022
- labelsAdded.push(label.name);
2102
+ labelsAdded.push(label.name ?? labelName);
2023
2103
  }
2024
2104
  }
2025
2105
  }
@@ -2177,29 +2257,25 @@ async function handleToolCall(name, args, deps) {
2177
2257
  // Final flush of any pending memory actions before ending the session
2178
2258
  await flushMemoryActions(client, cardId);
2179
2259
  cleanupMemorySession(cardId);
2180
- const result = await client.endAgentSession(cardId, {
2181
- status: sessionStatus,
2182
- progressPercent: endProgressPercent,
2183
- });
2184
- // Remove from auto-session tracking
2260
+ // End the session — tolerate failure (e.g., session already ended or not found)
2261
+ let result = { session: null };
2262
+ let sessionEndError = null;
2263
+ try {
2264
+ result = await client.endAgentSession(cardId, {
2265
+ status: sessionStatus,
2266
+ progressPercent: endProgressPercent,
2267
+ });
2268
+ }
2269
+ catch (err) {
2270
+ sessionEndError =
2271
+ err instanceof Error ? err.message : "Failed to end session";
2272
+ }
2273
+ // Remove from auto-session tracking regardless
2185
2274
  untrack(cardId);
2186
2275
  let movedTo = null;
2187
- const _learningsExtracted = 0;
2188
- // Get card info for move and learning extraction
2189
- let _cardTitle = "";
2190
- let _cardLabels = [];
2191
- let _cardDescription = "";
2192
- let _cardSubtasks = [];
2193
2276
  try {
2194
2277
  const { card } = await client.getCard(cardId);
2195
2278
  const typedCard = card;
2196
- _cardTitle = typedCard.title || "";
2197
- _cardLabels = (typedCard.labels || []).map((l) => l.name);
2198
- _cardDescription = typedCard.description || "";
2199
- _cardSubtasks = (typedCard.subtasks || []).map((s) => ({
2200
- title: s.title,
2201
- done: s.done,
2202
- }));
2203
2279
  const projectId = typedCard.project_id;
2204
2280
  // Remove "agent" label when session is completed (not paused)
2205
2281
  if (sessionStatus === "completed" && typedCard.labels?.length) {
@@ -2209,15 +2285,9 @@ async function handleToolCall(name, args, deps) {
2209
2285
  }
2210
2286
  }
2211
2287
  if (moveToColumn && projectId) {
2212
- const board = await client.getBoard(projectId, {
2213
- summary: true,
2214
- });
2215
- const columns = board.columns;
2216
- const col = columns.find((c) => c.name.toLowerCase().includes(moveToColumn.toLowerCase()));
2217
- if (col) {
2218
- await client.moveCard(cardId, col.id);
2219
- movedTo = col.name;
2220
- }
2288
+ const col = await resolveColumnByName(client, projectId, moveToColumn);
2289
+ await client.moveCard(cardId, col.id);
2290
+ movedTo = col.name;
2221
2291
  }
2222
2292
  }
2223
2293
  catch {
@@ -2228,6 +2298,7 @@ async function handleToolCall(name, args, deps) {
2228
2298
  const pipelineResult = await runEndSessionPipeline(client, deps, cardId, sessionStatus, endProgressPercent, sessionObj);
2229
2299
  return {
2230
2300
  success: true,
2301
+ ...(sessionEndError && { sessionEndError }),
2231
2302
  movedTo,
2232
2303
  learningsExtracted: pipelineResult.learningsExtracted,
2233
2304
  feedbackAdjusted: pipelineResult.feedbackAdjusted,
@@ -2261,13 +2332,10 @@ async function handleToolCall(name, args, deps) {
2261
2332
  }
2262
2333
  // Prompt generation
2263
2334
  case "harmony_generate_prompt": {
2264
- // Get card - either by UUID or short ID
2265
- let cardData;
2266
- let columnData = null;
2335
+ // Resolve card ID either directly or via short ID
2336
+ let cardId;
2267
2337
  if (args.cardId) {
2268
- const cardId = z.string().uuid().parse(args.cardId);
2269
- const cardResult = await client.getCard(cardId);
2270
- cardData = cardResult.card;
2338
+ cardId = z.string().uuid().parse(args.cardId);
2271
2339
  }
2272
2340
  else if (args.shortId !== undefined) {
2273
2341
  const shortId = z.number().int().positive().parse(args.shortId);
@@ -2276,32 +2344,12 @@ async function handleToolCall(name, args, deps) {
2276
2344
  throw new Error("Project ID required when using shortId. Use harmony_set_project_context or provide projectId.");
2277
2345
  }
2278
2346
  const cardResult = await client.getCardByShortId(projectId, shortId);
2279
- cardData = cardResult.card;
2347
+ cardId = cardResult.card.id;
2280
2348
  }
2281
2349
  else {
2282
2350
  throw new Error("Either cardId or shortId must be provided");
2283
2351
  }
2284
- // Try to get column info from board
2285
- const projectIdForBoard = args.projectId ||
2286
- getActiveProjectId() ||
2287
- cardData.project_id;
2288
- if (projectIdForBoard) {
2289
- try {
2290
- const board = await client.getBoard(projectIdForBoard, {
2291
- summary: true,
2292
- });
2293
- const columnId = cardData
2294
- .column_id;
2295
- const column = board.columns.find((col) => col.id === columnId);
2296
- if (column) {
2297
- columnData = { name: column.name };
2298
- }
2299
- }
2300
- catch {
2301
- // Column info not available, continue without it
2302
- }
2303
- }
2304
- const variant = args.variant || "execute";
2352
+ // Parse MCP-specific context options
2305
2353
  const contextOptions = {};
2306
2354
  if (args.includeSubtasks !== undefined) {
2307
2355
  contextOptions.includeSubtasks =
@@ -2316,75 +2364,21 @@ async function handleToolCall(name, args, deps) {
2316
2364
  args.includeDescription === true ||
2317
2365
  args.includeDescription === "true";
2318
2366
  }
2319
- // Assemble context using the context assembly engine
2320
- let assembledContextStr;
2321
- let assemblyId;
2322
- let memories;
2323
- try {
2324
- const workspaceId = deps.getActiveWorkspaceId();
2325
- if (workspaceId && cardData.title) {
2326
- const cardLabels = (cardData.labels || []).map((l) => l.name);
2327
- const taskContext = [cardData.title, cardData.description || ""]
2328
- .filter(Boolean)
2329
- .join(" ");
2330
- const assembled = await assembleContext({
2331
- workspaceId,
2332
- projectId: getActiveProjectId() || undefined,
2333
- taskContext,
2334
- cardLabels,
2335
- cardId: cardData.id,
2336
- client,
2337
- });
2338
- if (assembled.context) {
2339
- assembledContextStr = assembled.context;
2340
- assemblyId = assembled.manifest.assemblyId;
2341
- cacheManifest(assembled.manifest);
2342
- }
2343
- }
2344
- }
2345
- catch {
2346
- // Context assembly failed, try legacy fallback
2347
- try {
2348
- const workspaceId = deps.getActiveWorkspaceId();
2349
- if (workspaceId && cardData.title) {
2350
- const memoryResult = await client.searchMemoryEntities(workspaceId, cardData.title, {
2351
- project_id: getActiveProjectId() || undefined,
2352
- limit: 5,
2353
- });
2354
- if (memoryResult.entities?.length > 0) {
2355
- memories = memoryResult.entities.map((e) => {
2356
- const entity = e;
2357
- return {
2358
- id: entity.id,
2359
- type: entity.type,
2360
- title: entity.title,
2361
- content: entity.content,
2362
- confidence: entity.confidence,
2363
- tags: entity.tags || [],
2364
- };
2365
- });
2366
- }
2367
- }
2368
- }
2369
- catch {
2370
- // Memory fetch also failed, continue without memories
2371
- }
2372
- }
2373
- const result = generatePrompt({
2374
- card: cardData,
2375
- column: columnData,
2376
- variant,
2377
- contextOptions,
2367
+ // Delegate to the shared prompt generation pipeline
2368
+ const result = await client.generateCardPrompt({
2369
+ cardId,
2370
+ workspaceId: deps.getActiveWorkspaceId() || "",
2371
+ projectId: args.projectId || getActiveProjectId() || undefined,
2372
+ variant: args.variant || "execute",
2378
2373
  customConstraints: args.customConstraints,
2379
- memories,
2380
- assembledContext: assembledContextStr,
2381
- assemblyId,
2374
+ contextOptions,
2382
2375
  });
2376
+ // MCP-specific: cache the assembly manifest for the feedback loop
2377
+ if (result.assemblyId) {
2378
+ trackSessionAssembly(cardId, result.assemblyId);
2379
+ }
2383
2380
  return {
2384
2381
  success: true,
2385
- cardId: cardData.id,
2386
- shortId: cardData.short_id,
2387
- title: cardData.title,
2388
2382
  ...result,
2389
2383
  };
2390
2384
  }
@@ -3150,44 +3144,28 @@ async function handleToolCall(name, args, deps) {
3150
3144
  const projectName = args.projectName || "My First Project";
3151
3145
  const template = args.template || "kanban";
3152
3146
  const keyName = args.keyName || "mcp-agent";
3153
- const apiUrl = deps.getApiUrl();
3154
- // 1. Signup
3155
- const signupResult = await signupUser(apiUrl, {
3147
+ const result = await onboardNewUser({
3156
3148
  email,
3157
3149
  password,
3158
- full_name: fullName,
3159
- });
3160
- const token = signupResult.session.access_token;
3161
- // 2. Create workspace
3162
- const workspaceResult = await requestWithBearer(apiUrl, token, "POST", "/workspaces", {
3163
- name: workspaceName,
3164
- });
3165
- // 3. Create project
3166
- const projectResult = await requestWithBearer(apiUrl, token, "POST", "/projects", {
3167
- workspaceId: workspaceResult.workspace.id,
3168
- name: projectName,
3150
+ fullName,
3151
+ workspaceName,
3152
+ projectName,
3169
3153
  template,
3154
+ keyName,
3155
+ apiUrl: deps.getApiUrl(),
3170
3156
  });
3171
- // 4. Generate API key
3172
- const keyResult = await requestWithBearer(apiUrl, token, "POST", "/api-keys", {
3173
- name: keyName,
3174
- });
3175
- // 5. Save config
3176
- deps.saveConfig({ apiKey: keyResult.rawKey });
3177
- deps.setActiveWorkspace(workspaceResult.workspace.id);
3178
- deps.setActiveProject(projectResult.project.id);
3179
- // 6. Reset client so singleton picks up new key
3157
+ // Save config and reset client
3158
+ deps.saveConfig({ apiKey: result.apiKey.rawKey });
3159
+ deps.setActiveWorkspace(result.workspace.id);
3160
+ deps.setActiveProject(result.project.id);
3180
3161
  deps.resetClient();
3181
3162
  return {
3182
3163
  success: true,
3183
- user: signupResult.user,
3184
- workspace: workspaceResult.workspace,
3185
- project: projectResult.project,
3186
- columns: projectResult.columns,
3187
- apiKey: {
3188
- rawKey: keyResult.rawKey,
3189
- prefix: keyResult.apiKey.prefix,
3190
- },
3164
+ user: result.user,
3165
+ workspace: result.workspace,
3166
+ project: result.project,
3167
+ columns: result.columns,
3168
+ apiKey: result.apiKey,
3191
3169
  message: `Onboarding complete! Account created for ${email}. Workspace "${workspaceName}" and project "${projectName}" are ready. API key saved to config.`,
3192
3170
  };
3193
3171
  }
@@ -3267,11 +3245,14 @@ export class HarmonyMCPServer {
3267
3245
  const transport = new StdioServerTransport();
3268
3246
  await this.server.connect(transport);
3269
3247
  console.error("Harmony MCP server running on stdio");
3270
- // Initialize auto-session tracking
3248
+ // Initialize auto-session tracking with MCP client identity detection
3271
3249
  const configDeps = createConfigDeps();
3272
3250
  initAutoSession(async (client, cardId, status) => {
3273
3251
  await runEndSessionPipeline(client, configDeps, cardId, status);
3274
- }, () => getClient());
3252
+ }, () => getClient(), () => {
3253
+ const cv = this.server.getClientVersion();
3254
+ return cv ? { name: cv.name, version: cv.version } : null;
3255
+ });
3275
3256
  // Graceful shutdown: end all auto-sessions
3276
3257
  const handleShutdown = async () => {
3277
3258
  try {
@@ -4,7 +4,7 @@ import { areSkillsInstalled } from "./config.js";
4
4
  export const SKILLS_VERSION = "3";
5
5
  const VERSION_MARKER_PREFIX = "<!-- skills-version:";
6
6
  /**
7
- * Legacy workflow prompt used by Codex, Cursor, Windsurf agents.
7
+ * Legacy workflow prompt used by Codex, Cursor agents.
8
8
  * Claude Code skills use the newer SKILL_DEFINITIONS content instead.
9
9
  */
10
10
  export const HARMONY_WORKFLOW_PROMPT = `# Harmony Card Workflow