@gethmy/mcp 2.5.7 → 2.7.0

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/dist/cli.js CHANGED
@@ -69,6 +69,34 @@ function formatLabels(labels) {
69
69
  return `
70
70
  **Labels:** ${labels.map((l) => l.name).join(", ")}`;
71
71
  }
72
+ function formatAttachments(attachments) {
73
+ if (!attachments || attachments.length === 0)
74
+ return "";
75
+ const lines = attachments.map((att) => {
76
+ const isImage = att.file_type.startsWith("image/");
77
+ const sizeKb = Math.max(1, Math.round(att.file_size / 1024));
78
+ const url = att.signed_url ?? "(signed URL unavailable)";
79
+ const kind = isImage ? "image" : att.file_type;
80
+ return ` - ${att.file_name} (${kind}, ${sizeKb} KB) — ${url}`;
81
+ });
82
+ return `
83
+ ## Attachments
84
+ *Signed URLs expire in ~1 hour. Re-fetch via \`harmony_get_card_attachments\` if expired.*
85
+ ${lines.join(`
86
+ `)}`;
87
+ }
88
+ function formatExternalLinks(refs) {
89
+ if (!refs || refs.length === 0)
90
+ return "";
91
+ const lines = refs.map((ref) => {
92
+ const label = ref.title?.trim() || ref.url;
93
+ return ` - ${label} — ${ref.url}`;
94
+ });
95
+ return `
96
+ ## External References
97
+ ${lines.join(`
98
+ `)}`;
99
+ }
72
100
  function formatLinkedCards(links) {
73
101
  if (!links || links.length === 0)
74
102
  return "";
@@ -102,11 +130,15 @@ function generatePrompt(options) {
102
130
  includePriority: true,
103
131
  includeLinks: true,
104
132
  includeColumn: true,
133
+ includeAttachments: true,
134
+ includeExternalLinks: true,
105
135
  ...options.contextOptions
106
136
  };
107
137
  const labels = card.labels || [];
108
138
  const subtasks = card.subtasks || [];
109
139
  const links = card.links || [];
140
+ const attachments = card.attachments || [];
141
+ const externalLinks = card.external_links || [];
110
142
  const category = inferCategoryFromLabels(labels);
111
143
  const roleFraming = getRoleFraming(category);
112
144
  const sections = [];
@@ -143,6 +175,12 @@ ${card.description}`);
143
175
  if (contextOpts.includeLinks && links.length > 0) {
144
176
  sections.push(formatLinkedCards(links));
145
177
  }
178
+ if (contextOpts.includeAttachments && attachments.length > 0) {
179
+ sections.push(formatAttachments(attachments));
180
+ }
181
+ if (contextOpts.includeExternalLinks && externalLinks.length > 0) {
182
+ sections.push(formatExternalLinks(externalLinks));
183
+ }
146
184
  sections.push(`
147
185
  ## Focus Areas`);
148
186
  roleFraming.focus.forEach((f) => {
@@ -1330,6 +1368,12 @@ class HarmonyApiClient {
1330
1368
  async listProjects(workspaceId) {
1331
1369
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
1332
1370
  }
1371
+ async archiveProject(projectId) {
1372
+ return this.request("POST", `/projects/${projectId}/archive`);
1373
+ }
1374
+ async unarchiveProject(projectId) {
1375
+ return this.request("POST", `/projects/${projectId}/unarchive`);
1376
+ }
1333
1377
  async getBoard(projectId, options) {
1334
1378
  const params = new URLSearchParams;
1335
1379
  if (options?.limit !== undefined)
@@ -1399,6 +1443,12 @@ class HarmonyApiClient {
1399
1443
  async getCardLinks(cardId) {
1400
1444
  return this.request("GET", `/cards/${cardId}/links`);
1401
1445
  }
1446
+ async getCardAttachments(cardId) {
1447
+ return this.request("GET", `/cards/${cardId}/attachments`);
1448
+ }
1449
+ async getCardExternalLinks(cardId) {
1450
+ return this.request("GET", `/cards/${cardId}/external-links`);
1451
+ }
1402
1452
  async createColumn(projectId, name) {
1403
1453
  return this.request("POST", "/columns", { projectId, name });
1404
1454
  }
@@ -1479,6 +1529,51 @@ class HarmonyApiClient {
1479
1529
  async updateMemoryEntity(entityId, updates) {
1480
1530
  return this.request("PUT", `/memory/entities/${entityId}`, updates);
1481
1531
  }
1532
+ async harmonyRecall(options) {
1533
+ const fetchLimit = Math.max(options.topK ?? 3, 50);
1534
+ let entities = [];
1535
+ if (options.query) {
1536
+ const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
1537
+ project_id: options.projectId,
1538
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1539
+ limit: fetchLimit
1540
+ });
1541
+ entities = search.entities ?? [];
1542
+ } else {
1543
+ const list = await this.listMemoryEntities({
1544
+ workspace_id: options.workspaceId,
1545
+ project_id: options.projectId,
1546
+ scope: options.scope,
1547
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1548
+ tags: options.tags,
1549
+ min_confidence: options.minConfidence,
1550
+ limit: fetchLimit
1551
+ });
1552
+ entities = list.entities ?? [];
1553
+ }
1554
+ if (options.type && options.type.length > 1) {
1555
+ const allowed = new Set(options.type);
1556
+ entities = entities.filter((e) => allowed.has(e.type));
1557
+ }
1558
+ if (options.memory_tier) {
1559
+ entities = entities.filter((e) => e.memory_tier === options.memory_tier);
1560
+ }
1561
+ if (options.scope) {
1562
+ entities = entities.filter((e) => e.scope === options.scope);
1563
+ }
1564
+ if (options.tags?.length) {
1565
+ const wanted = new Set(options.tags);
1566
+ entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
1567
+ }
1568
+ if (typeof options.minConfidence === "number") {
1569
+ const threshold = options.minConfidence;
1570
+ entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
1571
+ }
1572
+ if (options.topK !== undefined) {
1573
+ entities = entities.slice(0, options.topK);
1574
+ }
1575
+ return { entities };
1576
+ }
1482
1577
  async deleteMemoryEntity(entityId) {
1483
1578
  return this.request("DELETE", `/memory/entities/${entityId}`);
1484
1579
  }
@@ -1665,6 +1760,30 @@ class HarmonyApiClient {
1665
1760
  const msg = err instanceof Error ? err.message : String(err);
1666
1761
  console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
1667
1762
  }
1763
+ try {
1764
+ const attachmentsResult = await this.getCardAttachments(options.cardId);
1765
+ cardData.attachments = (attachmentsResult.attachments ?? []).map((att) => ({
1766
+ id: att.id,
1767
+ file_name: att.file_name,
1768
+ file_type: att.file_type,
1769
+ file_size: att.file_size,
1770
+ signed_url: att.signed_url
1771
+ }));
1772
+ } catch (err) {
1773
+ const msg = err instanceof Error ? err.message : String(err);
1774
+ console.debug(`[generateCardPrompt] getCardAttachments failed: ${msg}`);
1775
+ }
1776
+ try {
1777
+ const externalLinksResult = await this.getCardExternalLinks(options.cardId);
1778
+ cardData.external_links = (externalLinksResult.external_links ?? []).map((link) => ({
1779
+ id: link.id,
1780
+ url: link.url,
1781
+ title: link.title
1782
+ }));
1783
+ } catch (err) {
1784
+ const msg = err instanceof Error ? err.message : String(err);
1785
+ console.debug(`[generateCardPrompt] getCardExternalLinks failed: ${msg}`);
1786
+ }
1668
1787
  let columnData = null;
1669
1788
  const projectIdForBoard = options.projectId || cardData.project_id;
1670
1789
  if (projectIdForBoard) {
@@ -1785,11 +1904,8 @@ function resolveAgentIdentity(info) {
1785
1904
  var AUTO_START_TRIGGERS = new Set([
1786
1905
  "harmony_generate_prompt",
1787
1906
  "harmony_update_card",
1788
- "harmony_move_card",
1789
1907
  "harmony_create_subtask",
1790
- "harmony_toggle_subtask",
1791
- "harmony_add_label_to_card",
1792
- "harmony_remove_label_from_card"
1908
+ "harmony_toggle_subtask"
1793
1909
  ]);
1794
1910
  var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
1795
1911
  var CHECK_INTERVAL_MS = 60 * 1000;
@@ -2459,6 +2575,26 @@ var TOOLS = {
2459
2575
  required: ["cardId"]
2460
2576
  }
2461
2577
  },
2578
+ harmony_archive_project: {
2579
+ description: "Archive a project. Hides the project (and all its columns and cards) from the active workspace. Can be restored later with harmony_unarchive_project.",
2580
+ inputSchema: {
2581
+ type: "object",
2582
+ properties: {
2583
+ projectId: { type: "string", description: "Project ID to archive" }
2584
+ },
2585
+ required: ["projectId"]
2586
+ }
2587
+ },
2588
+ harmony_unarchive_project: {
2589
+ description: "Restore an archived project so it (and its content) becomes visible again.",
2590
+ inputSchema: {
2591
+ type: "object",
2592
+ properties: {
2593
+ projectId: { type: "string", description: "Project ID to unarchive" }
2594
+ },
2595
+ required: ["projectId"]
2596
+ }
2597
+ },
2462
2598
  harmony_delete_card: {
2463
2599
  description: "Delete a card",
2464
2600
  inputSchema: {
@@ -2647,6 +2783,32 @@ var TOOLS = {
2647
2783
  required: ["cardId"]
2648
2784
  }
2649
2785
  },
2786
+ harmony_get_card_attachments: {
2787
+ description: "Get all file attachments for a card with short-lived signed URLs. URLs expire after `signed_url_expires_in` seconds (default 3600). Re-fetch to refresh.",
2788
+ inputSchema: {
2789
+ type: "object",
2790
+ properties: {
2791
+ cardId: {
2792
+ type: "string",
2793
+ description: "Card UUID"
2794
+ }
2795
+ },
2796
+ required: ["cardId"]
2797
+ }
2798
+ },
2799
+ harmony_get_card_external_links: {
2800
+ description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
2801
+ inputSchema: {
2802
+ type: "object",
2803
+ properties: {
2804
+ cardId: {
2805
+ type: "string",
2806
+ description: "Card UUID"
2807
+ }
2808
+ },
2809
+ required: ["cardId"]
2810
+ }
2811
+ },
2650
2812
  harmony_create_subtask: {
2651
2813
  description: "Create a subtask on a card",
2652
2814
  inputSchema: {
@@ -3785,6 +3947,16 @@ async function handleToolCall(name, args, deps) {
3785
3947
  const result = await client3.unarchiveCard(cardId);
3786
3948
  return { success: true, ...result };
3787
3949
  }
3950
+ case "harmony_archive_project": {
3951
+ const projectId = z.string().uuid().parse(args.projectId);
3952
+ const result = await client3.archiveProject(projectId);
3953
+ return { success: true, ...result };
3954
+ }
3955
+ case "harmony_unarchive_project": {
3956
+ const projectId = z.string().uuid().parse(args.projectId);
3957
+ const result = await client3.unarchiveProject(projectId);
3958
+ return { success: true, ...result };
3959
+ }
3788
3960
  case "harmony_delete_card": {
3789
3961
  const cardId = z.string().uuid().parse(args.cardId);
3790
3962
  await client3.deleteCard(cardId);
@@ -3902,6 +4074,16 @@ async function handleToolCall(name, args, deps) {
3902
4074
  const result = await client3.getCardLinks(cardId);
3903
4075
  return result;
3904
4076
  }
4077
+ case "harmony_get_card_attachments": {
4078
+ const cardId = z.string().uuid().parse(args.cardId);
4079
+ const result = await client3.getCardAttachments(cardId);
4080
+ return result;
4081
+ }
4082
+ case "harmony_get_card_external_links": {
4083
+ const cardId = z.string().uuid().parse(args.cardId);
4084
+ const result = await client3.getCardExternalLinks(cardId);
4085
+ return result;
4086
+ }
3905
4087
  case "harmony_create_subtask": {
3906
4088
  const cardId = z.string().uuid().parse(args.cardId);
3907
4089
  const title = z.string().min(1).max(500).parse(args.title);
package/dist/index.js CHANGED
@@ -69,6 +69,34 @@ function formatLabels(labels) {
69
69
  return `
70
70
  **Labels:** ${labels.map((l) => l.name).join(", ")}`;
71
71
  }
72
+ function formatAttachments(attachments) {
73
+ if (!attachments || attachments.length === 0)
74
+ return "";
75
+ const lines = attachments.map((att) => {
76
+ const isImage = att.file_type.startsWith("image/");
77
+ const sizeKb = Math.max(1, Math.round(att.file_size / 1024));
78
+ const url = att.signed_url ?? "(signed URL unavailable)";
79
+ const kind = isImage ? "image" : att.file_type;
80
+ return ` - ${att.file_name} (${kind}, ${sizeKb} KB) — ${url}`;
81
+ });
82
+ return `
83
+ ## Attachments
84
+ *Signed URLs expire in ~1 hour. Re-fetch via \`harmony_get_card_attachments\` if expired.*
85
+ ${lines.join(`
86
+ `)}`;
87
+ }
88
+ function formatExternalLinks(refs) {
89
+ if (!refs || refs.length === 0)
90
+ return "";
91
+ const lines = refs.map((ref) => {
92
+ const label = ref.title?.trim() || ref.url;
93
+ return ` - ${label} — ${ref.url}`;
94
+ });
95
+ return `
96
+ ## External References
97
+ ${lines.join(`
98
+ `)}`;
99
+ }
72
100
  function formatLinkedCards(links) {
73
101
  if (!links || links.length === 0)
74
102
  return "";
@@ -102,11 +130,15 @@ function generatePrompt(options) {
102
130
  includePriority: true,
103
131
  includeLinks: true,
104
132
  includeColumn: true,
133
+ includeAttachments: true,
134
+ includeExternalLinks: true,
105
135
  ...options.contextOptions
106
136
  };
107
137
  const labels = card.labels || [];
108
138
  const subtasks = card.subtasks || [];
109
139
  const links = card.links || [];
140
+ const attachments = card.attachments || [];
141
+ const externalLinks = card.external_links || [];
110
142
  const category = inferCategoryFromLabels(labels);
111
143
  const roleFraming = getRoleFraming(category);
112
144
  const sections = [];
@@ -143,6 +175,12 @@ ${card.description}`);
143
175
  if (contextOpts.includeLinks && links.length > 0) {
144
176
  sections.push(formatLinkedCards(links));
145
177
  }
178
+ if (contextOpts.includeAttachments && attachments.length > 0) {
179
+ sections.push(formatAttachments(attachments));
180
+ }
181
+ if (contextOpts.includeExternalLinks && externalLinks.length > 0) {
182
+ sections.push(formatExternalLinks(externalLinks));
183
+ }
146
184
  sections.push(`
147
185
  ## Focus Areas`);
148
186
  roleFraming.focus.forEach((f) => {
@@ -1326,6 +1364,12 @@ class HarmonyApiClient {
1326
1364
  async listProjects(workspaceId) {
1327
1365
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
1328
1366
  }
1367
+ async archiveProject(projectId) {
1368
+ return this.request("POST", `/projects/${projectId}/archive`);
1369
+ }
1370
+ async unarchiveProject(projectId) {
1371
+ return this.request("POST", `/projects/${projectId}/unarchive`);
1372
+ }
1329
1373
  async getBoard(projectId, options) {
1330
1374
  const params = new URLSearchParams;
1331
1375
  if (options?.limit !== undefined)
@@ -1395,6 +1439,12 @@ class HarmonyApiClient {
1395
1439
  async getCardLinks(cardId) {
1396
1440
  return this.request("GET", `/cards/${cardId}/links`);
1397
1441
  }
1442
+ async getCardAttachments(cardId) {
1443
+ return this.request("GET", `/cards/${cardId}/attachments`);
1444
+ }
1445
+ async getCardExternalLinks(cardId) {
1446
+ return this.request("GET", `/cards/${cardId}/external-links`);
1447
+ }
1398
1448
  async createColumn(projectId, name) {
1399
1449
  return this.request("POST", "/columns", { projectId, name });
1400
1450
  }
@@ -1475,6 +1525,51 @@ class HarmonyApiClient {
1475
1525
  async updateMemoryEntity(entityId, updates) {
1476
1526
  return this.request("PUT", `/memory/entities/${entityId}`, updates);
1477
1527
  }
1528
+ async harmonyRecall(options) {
1529
+ const fetchLimit = Math.max(options.topK ?? 3, 50);
1530
+ let entities = [];
1531
+ if (options.query) {
1532
+ const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
1533
+ project_id: options.projectId,
1534
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1535
+ limit: fetchLimit
1536
+ });
1537
+ entities = search.entities ?? [];
1538
+ } else {
1539
+ const list = await this.listMemoryEntities({
1540
+ workspace_id: options.workspaceId,
1541
+ project_id: options.projectId,
1542
+ scope: options.scope,
1543
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1544
+ tags: options.tags,
1545
+ min_confidence: options.minConfidence,
1546
+ limit: fetchLimit
1547
+ });
1548
+ entities = list.entities ?? [];
1549
+ }
1550
+ if (options.type && options.type.length > 1) {
1551
+ const allowed = new Set(options.type);
1552
+ entities = entities.filter((e) => allowed.has(e.type));
1553
+ }
1554
+ if (options.memory_tier) {
1555
+ entities = entities.filter((e) => e.memory_tier === options.memory_tier);
1556
+ }
1557
+ if (options.scope) {
1558
+ entities = entities.filter((e) => e.scope === options.scope);
1559
+ }
1560
+ if (options.tags?.length) {
1561
+ const wanted = new Set(options.tags);
1562
+ entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
1563
+ }
1564
+ if (typeof options.minConfidence === "number") {
1565
+ const threshold = options.minConfidence;
1566
+ entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
1567
+ }
1568
+ if (options.topK !== undefined) {
1569
+ entities = entities.slice(0, options.topK);
1570
+ }
1571
+ return { entities };
1572
+ }
1478
1573
  async deleteMemoryEntity(entityId) {
1479
1574
  return this.request("DELETE", `/memory/entities/${entityId}`);
1480
1575
  }
@@ -1661,6 +1756,30 @@ class HarmonyApiClient {
1661
1756
  const msg = err instanceof Error ? err.message : String(err);
1662
1757
  console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
1663
1758
  }
1759
+ try {
1760
+ const attachmentsResult = await this.getCardAttachments(options.cardId);
1761
+ cardData.attachments = (attachmentsResult.attachments ?? []).map((att) => ({
1762
+ id: att.id,
1763
+ file_name: att.file_name,
1764
+ file_type: att.file_type,
1765
+ file_size: att.file_size,
1766
+ signed_url: att.signed_url
1767
+ }));
1768
+ } catch (err) {
1769
+ const msg = err instanceof Error ? err.message : String(err);
1770
+ console.debug(`[generateCardPrompt] getCardAttachments failed: ${msg}`);
1771
+ }
1772
+ try {
1773
+ const externalLinksResult = await this.getCardExternalLinks(options.cardId);
1774
+ cardData.external_links = (externalLinksResult.external_links ?? []).map((link) => ({
1775
+ id: link.id,
1776
+ url: link.url,
1777
+ title: link.title
1778
+ }));
1779
+ } catch (err) {
1780
+ const msg = err instanceof Error ? err.message : String(err);
1781
+ console.debug(`[generateCardPrompt] getCardExternalLinks failed: ${msg}`);
1782
+ }
1664
1783
  let columnData = null;
1665
1784
  const projectIdForBoard = options.projectId || cardData.project_id;
1666
1785
  if (projectIdForBoard) {
@@ -1781,11 +1900,8 @@ function resolveAgentIdentity(info) {
1781
1900
  var AUTO_START_TRIGGERS = new Set([
1782
1901
  "harmony_generate_prompt",
1783
1902
  "harmony_update_card",
1784
- "harmony_move_card",
1785
1903
  "harmony_create_subtask",
1786
- "harmony_toggle_subtask",
1787
- "harmony_add_label_to_card",
1788
- "harmony_remove_label_from_card"
1904
+ "harmony_toggle_subtask"
1789
1905
  ]);
1790
1906
  var INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000;
1791
1907
  var CHECK_INTERVAL_MS = 60 * 1000;
@@ -2455,6 +2571,26 @@ var TOOLS = {
2455
2571
  required: ["cardId"]
2456
2572
  }
2457
2573
  },
2574
+ harmony_archive_project: {
2575
+ description: "Archive a project. Hides the project (and all its columns and cards) from the active workspace. Can be restored later with harmony_unarchive_project.",
2576
+ inputSchema: {
2577
+ type: "object",
2578
+ properties: {
2579
+ projectId: { type: "string", description: "Project ID to archive" }
2580
+ },
2581
+ required: ["projectId"]
2582
+ }
2583
+ },
2584
+ harmony_unarchive_project: {
2585
+ description: "Restore an archived project so it (and its content) becomes visible again.",
2586
+ inputSchema: {
2587
+ type: "object",
2588
+ properties: {
2589
+ projectId: { type: "string", description: "Project ID to unarchive" }
2590
+ },
2591
+ required: ["projectId"]
2592
+ }
2593
+ },
2458
2594
  harmony_delete_card: {
2459
2595
  description: "Delete a card",
2460
2596
  inputSchema: {
@@ -2643,6 +2779,32 @@ var TOOLS = {
2643
2779
  required: ["cardId"]
2644
2780
  }
2645
2781
  },
2782
+ harmony_get_card_attachments: {
2783
+ description: "Get all file attachments for a card with short-lived signed URLs. URLs expire after `signed_url_expires_in` seconds (default 3600). Re-fetch to refresh.",
2784
+ inputSchema: {
2785
+ type: "object",
2786
+ properties: {
2787
+ cardId: {
2788
+ type: "string",
2789
+ description: "Card UUID"
2790
+ }
2791
+ },
2792
+ required: ["cardId"]
2793
+ }
2794
+ },
2795
+ harmony_get_card_external_links: {
2796
+ description: "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
2797
+ inputSchema: {
2798
+ type: "object",
2799
+ properties: {
2800
+ cardId: {
2801
+ type: "string",
2802
+ description: "Card UUID"
2803
+ }
2804
+ },
2805
+ required: ["cardId"]
2806
+ }
2807
+ },
2646
2808
  harmony_create_subtask: {
2647
2809
  description: "Create a subtask on a card",
2648
2810
  inputSchema: {
@@ -3781,6 +3943,16 @@ async function handleToolCall(name, args, deps) {
3781
3943
  const result = await client3.unarchiveCard(cardId);
3782
3944
  return { success: true, ...result };
3783
3945
  }
3946
+ case "harmony_archive_project": {
3947
+ const projectId = z.string().uuid().parse(args.projectId);
3948
+ const result = await client3.archiveProject(projectId);
3949
+ return { success: true, ...result };
3950
+ }
3951
+ case "harmony_unarchive_project": {
3952
+ const projectId = z.string().uuid().parse(args.projectId);
3953
+ const result = await client3.unarchiveProject(projectId);
3954
+ return { success: true, ...result };
3955
+ }
3784
3956
  case "harmony_delete_card": {
3785
3957
  const cardId = z.string().uuid().parse(args.cardId);
3786
3958
  await client3.deleteCard(cardId);
@@ -3898,6 +4070,16 @@ async function handleToolCall(name, args, deps) {
3898
4070
  const result = await client3.getCardLinks(cardId);
3899
4071
  return result;
3900
4072
  }
4073
+ case "harmony_get_card_attachments": {
4074
+ const cardId = z.string().uuid().parse(args.cardId);
4075
+ const result = await client3.getCardAttachments(cardId);
4076
+ return result;
4077
+ }
4078
+ case "harmony_get_card_external_links": {
4079
+ const cardId = z.string().uuid().parse(args.cardId);
4080
+ const result = await client3.getCardExternalLinks(cardId);
4081
+ return result;
4082
+ }
3901
4083
  case "harmony_create_subtask": {
3902
4084
  const cardId = z.string().uuid().parse(args.cardId);
3903
4085
  const title = z.string().min(1).max(500).parse(args.title);
@@ -66,6 +66,34 @@ function formatLabels(labels) {
66
66
  return `
67
67
  **Labels:** ${labels.map((l) => l.name).join(", ")}`;
68
68
  }
69
+ function formatAttachments(attachments) {
70
+ if (!attachments || attachments.length === 0)
71
+ return "";
72
+ const lines = attachments.map((att) => {
73
+ const isImage = att.file_type.startsWith("image/");
74
+ const sizeKb = Math.max(1, Math.round(att.file_size / 1024));
75
+ const url = att.signed_url ?? "(signed URL unavailable)";
76
+ const kind = isImage ? "image" : att.file_type;
77
+ return ` - ${att.file_name} (${kind}, ${sizeKb} KB) — ${url}`;
78
+ });
79
+ return `
80
+ ## Attachments
81
+ *Signed URLs expire in ~1 hour. Re-fetch via \`harmony_get_card_attachments\` if expired.*
82
+ ${lines.join(`
83
+ `)}`;
84
+ }
85
+ function formatExternalLinks(refs) {
86
+ if (!refs || refs.length === 0)
87
+ return "";
88
+ const lines = refs.map((ref) => {
89
+ const label = ref.title?.trim() || ref.url;
90
+ return ` - ${label} — ${ref.url}`;
91
+ });
92
+ return `
93
+ ## External References
94
+ ${lines.join(`
95
+ `)}`;
96
+ }
69
97
  function formatLinkedCards(links) {
70
98
  if (!links || links.length === 0)
71
99
  return "";
@@ -99,11 +127,15 @@ function generatePrompt(options) {
99
127
  includePriority: true,
100
128
  includeLinks: true,
101
129
  includeColumn: true,
130
+ includeAttachments: true,
131
+ includeExternalLinks: true,
102
132
  ...options.contextOptions
103
133
  };
104
134
  const labels = card.labels || [];
105
135
  const subtasks = card.subtasks || [];
106
136
  const links = card.links || [];
137
+ const attachments = card.attachments || [];
138
+ const externalLinks = card.external_links || [];
107
139
  const category = inferCategoryFromLabels(labels);
108
140
  const roleFraming = getRoleFraming(category);
109
141
  const sections = [];
@@ -140,6 +172,12 @@ ${card.description}`);
140
172
  if (contextOpts.includeLinks && links.length > 0) {
141
173
  sections.push(formatLinkedCards(links));
142
174
  }
175
+ if (contextOpts.includeAttachments && attachments.length > 0) {
176
+ sections.push(formatAttachments(attachments));
177
+ }
178
+ if (contextOpts.includeExternalLinks && externalLinks.length > 0) {
179
+ sections.push(formatExternalLinks(externalLinks));
180
+ }
143
181
  sections.push(`
144
182
  ## Focus Areas`);
145
183
  roleFraming.focus.forEach((f) => {
@@ -933,6 +971,12 @@ class HarmonyApiClient {
933
971
  async listProjects(workspaceId) {
934
972
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
935
973
  }
974
+ async archiveProject(projectId) {
975
+ return this.request("POST", `/projects/${projectId}/archive`);
976
+ }
977
+ async unarchiveProject(projectId) {
978
+ return this.request("POST", `/projects/${projectId}/unarchive`);
979
+ }
936
980
  async getBoard(projectId, options) {
937
981
  const params = new URLSearchParams;
938
982
  if (options?.limit !== undefined)
@@ -1002,6 +1046,12 @@ class HarmonyApiClient {
1002
1046
  async getCardLinks(cardId) {
1003
1047
  return this.request("GET", `/cards/${cardId}/links`);
1004
1048
  }
1049
+ async getCardAttachments(cardId) {
1050
+ return this.request("GET", `/cards/${cardId}/attachments`);
1051
+ }
1052
+ async getCardExternalLinks(cardId) {
1053
+ return this.request("GET", `/cards/${cardId}/external-links`);
1054
+ }
1005
1055
  async createColumn(projectId, name) {
1006
1056
  return this.request("POST", "/columns", { projectId, name });
1007
1057
  }
@@ -1082,6 +1132,51 @@ class HarmonyApiClient {
1082
1132
  async updateMemoryEntity(entityId, updates) {
1083
1133
  return this.request("PUT", `/memory/entities/${entityId}`, updates);
1084
1134
  }
1135
+ async harmonyRecall(options) {
1136
+ const fetchLimit = Math.max(options.topK ?? 3, 50);
1137
+ let entities = [];
1138
+ if (options.query) {
1139
+ const search = await this.searchMemoryEntities(options.workspaceId, options.query, {
1140
+ project_id: options.projectId,
1141
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1142
+ limit: fetchLimit
1143
+ });
1144
+ entities = search.entities ?? [];
1145
+ } else {
1146
+ const list = await this.listMemoryEntities({
1147
+ workspace_id: options.workspaceId,
1148
+ project_id: options.projectId,
1149
+ scope: options.scope,
1150
+ type: options.type?.length === 1 ? options.type[0] : undefined,
1151
+ tags: options.tags,
1152
+ min_confidence: options.minConfidence,
1153
+ limit: fetchLimit
1154
+ });
1155
+ entities = list.entities ?? [];
1156
+ }
1157
+ if (options.type && options.type.length > 1) {
1158
+ const allowed = new Set(options.type);
1159
+ entities = entities.filter((e) => allowed.has(e.type));
1160
+ }
1161
+ if (options.memory_tier) {
1162
+ entities = entities.filter((e) => e.memory_tier === options.memory_tier);
1163
+ }
1164
+ if (options.scope) {
1165
+ entities = entities.filter((e) => e.scope === options.scope);
1166
+ }
1167
+ if (options.tags?.length) {
1168
+ const wanted = new Set(options.tags);
1169
+ entities = entities.filter((e) => (e.tags ?? []).some((t) => wanted.has(t)));
1170
+ }
1171
+ if (typeof options.minConfidence === "number") {
1172
+ const threshold = options.minConfidence;
1173
+ entities = entities.filter((e) => typeof e.confidence === "number" && e.confidence >= threshold);
1174
+ }
1175
+ if (options.topK !== undefined) {
1176
+ entities = entities.slice(0, options.topK);
1177
+ }
1178
+ return { entities };
1179
+ }
1085
1180
  async deleteMemoryEntity(entityId) {
1086
1181
  return this.request("DELETE", `/memory/entities/${entityId}`);
1087
1182
  }
@@ -1268,6 +1363,30 @@ class HarmonyApiClient {
1268
1363
  const msg = err instanceof Error ? err.message : String(err);
1269
1364
  console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
1270
1365
  }
1366
+ try {
1367
+ const attachmentsResult = await this.getCardAttachments(options.cardId);
1368
+ cardData.attachments = (attachmentsResult.attachments ?? []).map((att) => ({
1369
+ id: att.id,
1370
+ file_name: att.file_name,
1371
+ file_type: att.file_type,
1372
+ file_size: att.file_size,
1373
+ signed_url: att.signed_url
1374
+ }));
1375
+ } catch (err) {
1376
+ const msg = err instanceof Error ? err.message : String(err);
1377
+ console.debug(`[generateCardPrompt] getCardAttachments failed: ${msg}`);
1378
+ }
1379
+ try {
1380
+ const externalLinksResult = await this.getCardExternalLinks(options.cardId);
1381
+ cardData.external_links = (externalLinksResult.external_links ?? []).map((link) => ({
1382
+ id: link.id,
1383
+ url: link.url,
1384
+ title: link.title
1385
+ }));
1386
+ } catch (err) {
1387
+ const msg = err instanceof Error ? err.message : String(err);
1388
+ console.debug(`[generateCardPrompt] getCardExternalLinks failed: ${msg}`);
1389
+ }
1271
1390
  let columnData = null;
1272
1391
  const projectIdForBoard = options.projectId || cardData.project_id;
1273
1392
  if (projectIdForBoard) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.5.7",
3
+ "version": "2.7.0",
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
@@ -124,6 +124,28 @@ export class HarmonyUnauthorizedError extends Error {
124
124
  }
125
125
  }
126
126
 
127
+ export interface CardAttachment {
128
+ id: string;
129
+ card_id: string;
130
+ file_name: string;
131
+ file_type: string;
132
+ file_size: number;
133
+ storage_path: string;
134
+ uploaded_by: string | null;
135
+ created_at: string;
136
+ signed_url: string | null;
137
+ signed_url_expires_in: number;
138
+ }
139
+
140
+ export interface CardExternalLinkRow {
141
+ id: string;
142
+ card_id: string;
143
+ url: string;
144
+ title: string | null;
145
+ created_by: string | null;
146
+ created_at: string;
147
+ }
148
+
127
149
  export class HarmonyApiClient {
128
150
  private apiKey: string;
129
151
  private apiUrl: string;
@@ -376,6 +398,14 @@ export class HarmonyApiClient {
376
398
  return this.request("GET", `/workspaces/${workspaceId}/projects`);
377
399
  }
378
400
 
401
+ async archiveProject(projectId: string): Promise<{ project: unknown }> {
402
+ return this.request("POST", `/projects/${projectId}/archive`);
403
+ }
404
+
405
+ async unarchiveProject(projectId: string): Promise<{ project: unknown }> {
406
+ return this.request("POST", `/projects/${projectId}/unarchive`);
407
+ }
408
+
379
409
  async getBoard(
380
410
  projectId: string,
381
411
  options?: {
@@ -524,6 +554,18 @@ export class HarmonyApiClient {
524
554
  return this.request("GET", `/cards/${cardId}/links`);
525
555
  }
526
556
 
557
+ async getCardAttachments(
558
+ cardId: string,
559
+ ): Promise<{ attachments: CardAttachment[] }> {
560
+ return this.request("GET", `/cards/${cardId}/attachments`);
561
+ }
562
+
563
+ async getCardExternalLinks(
564
+ cardId: string,
565
+ ): Promise<{ external_links: CardExternalLinkRow[] }> {
566
+ return this.request("GET", `/cards/${cardId}/external-links`);
567
+ }
568
+
527
569
  // ============ COLUMN OPERATIONS ============
528
570
 
529
571
  async createColumn(
@@ -743,15 +785,101 @@ export class HarmonyApiClient {
743
785
  scope?: string;
744
786
  type?: string;
745
787
  memory_tier?: string;
746
- // AGP lifecycle fields. Backend may not yet whitelist these — extra keys
747
- // are dropped server-side, leaving the call as a no-op for those fields.
788
+ // Supersede semantics used by Phase 1.5 review-reject back-fill to
789
+ // tombstone the original implement episode without hard-deleting it.
790
+ // The backend sets superseded_at automatically when superseded_by lands.
748
791
  superseded_by?: string | null;
792
+ superseded_at?: string | null;
749
793
  version?: number;
750
794
  },
751
795
  ): Promise<{ entity: unknown; warnings?: string[] }> {
752
796
  return this.request("PUT", `/memory/entities/${entityId}`, updates);
753
797
  }
754
798
 
799
+ /**
800
+ * Retrieve memories filtered by type/tier/scope, optionally ranked by a
801
+ * free-text query. Wraps `searchMemoryEntities` (when a query is given)
802
+ * or `listMemoryEntities` and applies client-side filters that the REST
803
+ * surface doesn't natively expose (multi-type, memory_tier).
804
+ *
805
+ * Used by the agent daemon's read hook to surface similar past episodes
806
+ * before building a new task prompt (Phase 1.5).
807
+ */
808
+ async harmonyRecall(options: {
809
+ workspaceId: string;
810
+ projectId?: string;
811
+ query?: string;
812
+ type?: string[];
813
+ memory_tier?: string;
814
+ scope?: string;
815
+ tags?: string[];
816
+ minConfidence?: number;
817
+ topK?: number;
818
+ }): Promise<{ entities: unknown[] }> {
819
+ // Over-fetch beyond topK so client-side filters (multi-type, memory_tier,
820
+ // tags) have headroom — matches the MCP server's recall path (server.ts).
821
+ const fetchLimit = Math.max(options.topK ?? 3, 50);
822
+ let entities: Array<Record<string, unknown>> = [];
823
+
824
+ if (options.query) {
825
+ // searchMemoryEntities accepts a single type — refine client-side
826
+ // when the caller passed multiple.
827
+ const search = await this.searchMemoryEntities(
828
+ options.workspaceId,
829
+ options.query,
830
+ {
831
+ project_id: options.projectId,
832
+ type: options.type?.length === 1 ? options.type[0] : undefined,
833
+ limit: fetchLimit,
834
+ },
835
+ );
836
+ entities = (search.entities ?? []) as Array<Record<string, unknown>>;
837
+ } else {
838
+ const list = await this.listMemoryEntities({
839
+ workspace_id: options.workspaceId,
840
+ project_id: options.projectId,
841
+ scope: options.scope,
842
+ type: options.type?.length === 1 ? options.type[0] : undefined,
843
+ tags: options.tags,
844
+ min_confidence: options.minConfidence,
845
+ limit: fetchLimit,
846
+ });
847
+ entities = (list.entities ?? []) as Array<Record<string, unknown>>;
848
+ }
849
+
850
+ // Client-side filters: REST surface lacks multi-type and memory_tier.
851
+ if (options.type && options.type.length > 1) {
852
+ const allowed = new Set(options.type);
853
+ entities = entities.filter((e) => allowed.has(e.type as string));
854
+ }
855
+ if (options.memory_tier) {
856
+ entities = entities.filter((e) => e.memory_tier === options.memory_tier);
857
+ }
858
+ if (options.scope) {
859
+ entities = entities.filter((e) => e.scope === options.scope);
860
+ }
861
+ if (options.tags?.length) {
862
+ const wanted = new Set(options.tags);
863
+ entities = entities.filter((e) =>
864
+ ((e.tags as string[]) ?? []).some((t) => wanted.has(t)),
865
+ );
866
+ }
867
+ if (typeof options.minConfidence === "number") {
868
+ const threshold = options.minConfidence;
869
+ entities = entities.filter(
870
+ (e) =>
871
+ typeof e.confidence === "number" &&
872
+ (e.confidence as number) >= threshold,
873
+ );
874
+ }
875
+
876
+ if (options.topK !== undefined) {
877
+ entities = entities.slice(0, options.topK);
878
+ }
879
+
880
+ return { entities };
881
+ }
882
+
755
883
  async deleteMemoryEntity(entityId: string): Promise<{ success: boolean }> {
756
884
  return this.request("DELETE", `/memory/entities/${entityId}`);
757
885
  }
@@ -1196,6 +1324,40 @@ export class HarmonyApiClient {
1196
1324
  console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
1197
1325
  }
1198
1326
 
1327
+ // Fetch attachments + external links. Best-effort — never break prompt
1328
+ // generation if storage signing or the underlying tables fail.
1329
+ try {
1330
+ const attachmentsResult = await this.getCardAttachments(options.cardId);
1331
+ cardData.attachments = (attachmentsResult.attachments ?? []).map(
1332
+ (att) => ({
1333
+ id: att.id,
1334
+ file_name: att.file_name,
1335
+ file_type: att.file_type,
1336
+ file_size: att.file_size,
1337
+ signed_url: att.signed_url,
1338
+ }),
1339
+ );
1340
+ } catch (err) {
1341
+ const msg = err instanceof Error ? err.message : String(err);
1342
+ console.debug(`[generateCardPrompt] getCardAttachments failed: ${msg}`);
1343
+ }
1344
+
1345
+ try {
1346
+ const externalLinksResult = await this.getCardExternalLinks(
1347
+ options.cardId,
1348
+ );
1349
+ cardData.external_links = (externalLinksResult.external_links ?? []).map(
1350
+ (link) => ({
1351
+ id: link.id,
1352
+ url: link.url,
1353
+ title: link.title,
1354
+ }),
1355
+ );
1356
+ } catch (err) {
1357
+ const msg = err instanceof Error ? err.message : String(err);
1358
+ console.debug(`[generateCardPrompt] getCardExternalLinks failed: ${msg}`);
1359
+ }
1360
+
1199
1361
  // Try to get column info
1200
1362
  let columnData: { name: string } | null = null;
1201
1363
  const projectIdForBoard = options.projectId || cardData.project_id;
@@ -1309,6 +1471,18 @@ interface CardPromptData {
1309
1471
  direction: "outgoing" | "incoming";
1310
1472
  }>;
1311
1473
  assignee?: { full_name?: string; email: string } | null;
1474
+ attachments?: Array<{
1475
+ id: string;
1476
+ file_name: string;
1477
+ file_type: string;
1478
+ file_size: number;
1479
+ signed_url: string | null;
1480
+ }>;
1481
+ external_links?: Array<{
1482
+ id: string;
1483
+ url: string;
1484
+ title?: string | null;
1485
+ }>;
1312
1486
  column_id?: string;
1313
1487
  project_id?: string;
1314
1488
  }
@@ -65,15 +65,19 @@ export function resolveAgentIdentity(info: ClientInfo | null): {
65
65
  return { agentIdentifier: key, agentName: displayName };
66
66
  }
67
67
 
68
- /** Tools that trigger auto-start of a session */
68
+ /**
69
+ * Tools that trigger auto-start of a session.
70
+ *
71
+ * Restricted to tools that signal real work on a card. Board-management ops
72
+ * (move, label add/remove) are excluded — they're routinely used for triage
73
+ * and would create false-positive sessions whose side effect (the auto-added
74
+ * `agent` label on the card) confuses both UI and humans.
75
+ */
69
76
  export const AUTO_START_TRIGGERS = new Set([
70
77
  "harmony_generate_prompt",
71
78
  "harmony_update_card",
72
- "harmony_move_card",
73
79
  "harmony_create_subtask",
74
80
  "harmony_toggle_subtask",
75
- "harmony_add_label_to_card",
76
- "harmony_remove_label_from_card",
77
81
  ]);
78
82
 
79
83
  export const INACTIVITY_TIMEOUT_MS = 10 * 60 * 1000; // 10 minutes
@@ -44,6 +44,8 @@ export interface PromptContextOptions {
44
44
  includePriority: boolean;
45
45
  includeLinks: boolean;
46
46
  includeColumn: boolean;
47
+ includeAttachments: boolean;
48
+ includeExternalLinks: boolean;
47
49
  }
48
50
 
49
51
  export interface RoleFraming {
@@ -307,6 +309,27 @@ function formatLabels(labels: Array<{ name: string }>): string {
307
309
  return `\n**Labels:** ${labels.map((l) => l.name).join(", ")}`;
308
310
  }
309
311
 
312
+ function formatAttachments(attachments: CardAttachmentRef[]): string {
313
+ if (!attachments || attachments.length === 0) return "";
314
+ const lines = attachments.map((att) => {
315
+ const isImage = att.file_type.startsWith("image/");
316
+ const sizeKb = Math.max(1, Math.round(att.file_size / 1024));
317
+ const url = att.signed_url ?? "(signed URL unavailable)";
318
+ const kind = isImage ? "image" : att.file_type;
319
+ return ` - ${att.file_name} (${kind}, ${sizeKb} KB) — ${url}`;
320
+ });
321
+ return `\n## Attachments\n*Signed URLs expire in ~1 hour. Re-fetch via \`harmony_get_card_attachments\` if expired.*\n${lines.join("\n")}`;
322
+ }
323
+
324
+ function formatExternalLinks(refs: CardExternalLinkRef[]): string {
325
+ if (!refs || refs.length === 0) return "";
326
+ const lines = refs.map((ref) => {
327
+ const label = ref.title?.trim() || ref.url;
328
+ return ` - ${label} — ${ref.url}`;
329
+ });
330
+ return `\n## External References\n${lines.join("\n")}`;
331
+ }
332
+
310
333
  /**
311
334
  * Format linked cards for prompt
312
335
  */
@@ -325,6 +348,20 @@ function formatLinkedCards(
325
348
  return `\n## Related Cards\n${lines.join("\n")}`;
326
349
  }
327
350
 
351
+ export interface CardAttachmentRef {
352
+ id: string;
353
+ file_name: string;
354
+ file_type: string;
355
+ file_size: number;
356
+ signed_url: string | null;
357
+ }
358
+
359
+ export interface CardExternalLinkRef {
360
+ id: string;
361
+ url: string;
362
+ title?: string | null;
363
+ }
364
+
328
365
  export interface CardData {
329
366
  id: string;
330
367
  short_id: number;
@@ -341,6 +378,8 @@ export interface CardData {
341
378
  direction: "outgoing" | "incoming";
342
379
  }>;
343
380
  assignee?: { full_name?: string; email: string } | null;
381
+ attachments?: CardAttachmentRef[];
382
+ external_links?: CardExternalLinkRef[];
344
383
  }
345
384
 
346
385
  export interface ColumnData {
@@ -397,12 +436,16 @@ export function generatePrompt(
397
436
  includePriority: true,
398
437
  includeLinks: true,
399
438
  includeColumn: true,
439
+ includeAttachments: true,
440
+ includeExternalLinks: true,
400
441
  ...options.contextOptions,
401
442
  };
402
443
 
403
444
  const labels = card.labels || [];
404
445
  const subtasks = card.subtasks || [];
405
446
  const links = card.links || [];
447
+ const attachments = card.attachments || [];
448
+ const externalLinks = card.external_links || [];
406
449
 
407
450
  const category = inferCategoryFromLabels(labels);
408
451
  const roleFraming = getRoleFraming(category);
@@ -456,6 +499,16 @@ export function generatePrompt(
456
499
  sections.push(formatLinkedCards(links));
457
500
  }
458
501
 
502
+ // Attachments (signed URLs) — fetchable image/file references
503
+ if (contextOpts.includeAttachments && attachments.length > 0) {
504
+ sections.push(formatAttachments(attachments));
505
+ }
506
+
507
+ // External URL references (docs, gists, dashboards)
508
+ if (contextOpts.includeExternalLinks && externalLinks.length > 0) {
509
+ sections.push(formatExternalLinks(externalLinks));
510
+ }
511
+
459
512
  // Focus areas
460
513
  sections.push(`\n## Focus Areas`);
461
514
  roleFraming.focus.forEach((f) => {
package/src/server.ts CHANGED
@@ -348,6 +348,28 @@ export const TOOLS = {
348
348
  required: ["cardId"],
349
349
  },
350
350
  },
351
+ harmony_archive_project: {
352
+ description:
353
+ "Archive a project. Hides the project (and all its columns and cards) from the active workspace. Can be restored later with harmony_unarchive_project.",
354
+ inputSchema: {
355
+ type: "object",
356
+ properties: {
357
+ projectId: { type: "string", description: "Project ID to archive" },
358
+ },
359
+ required: ["projectId"],
360
+ },
361
+ },
362
+ harmony_unarchive_project: {
363
+ description:
364
+ "Restore an archived project so it (and its content) becomes visible again.",
365
+ inputSchema: {
366
+ type: "object",
367
+ properties: {
368
+ projectId: { type: "string", description: "Project ID to unarchive" },
369
+ },
370
+ required: ["projectId"],
371
+ },
372
+ },
351
373
  harmony_delete_card: {
352
374
  description: "Delete a card",
353
375
  inputSchema: {
@@ -546,6 +568,34 @@ export const TOOLS = {
546
568
  required: ["cardId"],
547
569
  },
548
570
  },
571
+ harmony_get_card_attachments: {
572
+ description:
573
+ "Get all file attachments for a card with short-lived signed URLs. URLs expire after `signed_url_expires_in` seconds (default 3600). Re-fetch to refresh.",
574
+ inputSchema: {
575
+ type: "object",
576
+ properties: {
577
+ cardId: {
578
+ type: "string",
579
+ description: "Card UUID",
580
+ },
581
+ },
582
+ required: ["cardId"],
583
+ },
584
+ },
585
+ harmony_get_card_external_links: {
586
+ description:
587
+ "Get external URL references attached to a card (links to docs, gists, dashboards, etc.).",
588
+ inputSchema: {
589
+ type: "object",
590
+ properties: {
591
+ cardId: {
592
+ type: "string",
593
+ description: "Card UUID",
594
+ },
595
+ },
596
+ required: ["cardId"],
597
+ },
598
+ },
549
599
 
550
600
  // Subtask operations
551
601
  harmony_create_subtask: {
@@ -1908,6 +1958,18 @@ async function handleToolCall(
1908
1958
  return { success: true, ...result };
1909
1959
  }
1910
1960
 
1961
+ case "harmony_archive_project": {
1962
+ const projectId = z.string().uuid().parse(args.projectId);
1963
+ const result = await client.archiveProject(projectId);
1964
+ return { success: true, ...result };
1965
+ }
1966
+
1967
+ case "harmony_unarchive_project": {
1968
+ const projectId = z.string().uuid().parse(args.projectId);
1969
+ const result = await client.unarchiveProject(projectId);
1970
+ return { success: true, ...result };
1971
+ }
1972
+
1911
1973
  case "harmony_delete_card": {
1912
1974
  const cardId = z.string().uuid().parse(args.cardId);
1913
1975
  await client.deleteCard(cardId);
@@ -2074,6 +2136,18 @@ async function handleToolCall(
2074
2136
  return result;
2075
2137
  }
2076
2138
 
2139
+ case "harmony_get_card_attachments": {
2140
+ const cardId = z.string().uuid().parse(args.cardId);
2141
+ const result = await client.getCardAttachments(cardId);
2142
+ return result;
2143
+ }
2144
+
2145
+ case "harmony_get_card_external_links": {
2146
+ const cardId = z.string().uuid().parse(args.cardId);
2147
+ const result = await client.getCardExternalLinks(cardId);
2148
+ return result;
2149
+ }
2150
+
2077
2151
  // Subtask operations
2078
2152
  case "harmony_create_subtask": {
2079
2153
  const cardId = z.string().uuid().parse(args.cardId);