@gethmy/mcp 2.6.0 → 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
  }
@@ -1710,6 +1760,30 @@ class HarmonyApiClient {
1710
1760
  const msg = err instanceof Error ? err.message : String(err);
1711
1761
  console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
1712
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
+ }
1713
1787
  let columnData = null;
1714
1788
  const projectIdForBoard = options.projectId || cardData.project_id;
1715
1789
  if (projectIdForBoard) {
@@ -2501,6 +2575,26 @@ var TOOLS = {
2501
2575
  required: ["cardId"]
2502
2576
  }
2503
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
+ },
2504
2598
  harmony_delete_card: {
2505
2599
  description: "Delete a card",
2506
2600
  inputSchema: {
@@ -2689,6 +2783,32 @@ var TOOLS = {
2689
2783
  required: ["cardId"]
2690
2784
  }
2691
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
+ },
2692
2812
  harmony_create_subtask: {
2693
2813
  description: "Create a subtask on a card",
2694
2814
  inputSchema: {
@@ -3827,6 +3947,16 @@ async function handleToolCall(name, args, deps) {
3827
3947
  const result = await client3.unarchiveCard(cardId);
3828
3948
  return { success: true, ...result };
3829
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
+ }
3830
3960
  case "harmony_delete_card": {
3831
3961
  const cardId = z.string().uuid().parse(args.cardId);
3832
3962
  await client3.deleteCard(cardId);
@@ -3944,6 +4074,16 @@ async function handleToolCall(name, args, deps) {
3944
4074
  const result = await client3.getCardLinks(cardId);
3945
4075
  return result;
3946
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
+ }
3947
4087
  case "harmony_create_subtask": {
3948
4088
  const cardId = z.string().uuid().parse(args.cardId);
3949
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
  }
@@ -1706,6 +1756,30 @@ class HarmonyApiClient {
1706
1756
  const msg = err instanceof Error ? err.message : String(err);
1707
1757
  console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
1708
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
+ }
1709
1783
  let columnData = null;
1710
1784
  const projectIdForBoard = options.projectId || cardData.project_id;
1711
1785
  if (projectIdForBoard) {
@@ -2497,6 +2571,26 @@ var TOOLS = {
2497
2571
  required: ["cardId"]
2498
2572
  }
2499
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
+ },
2500
2594
  harmony_delete_card: {
2501
2595
  description: "Delete a card",
2502
2596
  inputSchema: {
@@ -2685,6 +2779,32 @@ var TOOLS = {
2685
2779
  required: ["cardId"]
2686
2780
  }
2687
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
+ },
2688
2808
  harmony_create_subtask: {
2689
2809
  description: "Create a subtask on a card",
2690
2810
  inputSchema: {
@@ -3823,6 +3943,16 @@ async function handleToolCall(name, args, deps) {
3823
3943
  const result = await client3.unarchiveCard(cardId);
3824
3944
  return { success: true, ...result };
3825
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
+ }
3826
3956
  case "harmony_delete_card": {
3827
3957
  const cardId = z.string().uuid().parse(args.cardId);
3828
3958
  await client3.deleteCard(cardId);
@@ -3940,6 +4070,16 @@ async function handleToolCall(name, args, deps) {
3940
4070
  const result = await client3.getCardLinks(cardId);
3941
4071
  return result;
3942
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
+ }
3943
4083
  case "harmony_create_subtask": {
3944
4084
  const cardId = z.string().uuid().parse(args.cardId);
3945
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
  }
@@ -1313,6 +1363,30 @@ class HarmonyApiClient {
1313
1363
  const msg = err instanceof Error ? err.message : String(err);
1314
1364
  console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
1315
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
+ }
1316
1390
  let columnData = null;
1317
1391
  const projectIdForBoard = options.projectId || cardData.project_id;
1318
1392
  if (projectIdForBoard) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.6.0",
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(
@@ -1282,6 +1324,40 @@ export class HarmonyApiClient {
1282
1324
  console.debug(`[generateCardPrompt] getCardLinks failed: ${msg}`);
1283
1325
  }
1284
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
+
1285
1361
  // Try to get column info
1286
1362
  let columnData: { name: string } | null = null;
1287
1363
  const projectIdForBoard = options.projectId || cardData.project_id;
@@ -1395,6 +1471,18 @@ interface CardPromptData {
1395
1471
  direction: "outgoing" | "incoming";
1396
1472
  }>;
1397
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
+ }>;
1398
1486
  column_id?: string;
1399
1487
  project_id?: string;
1400
1488
  }
@@ -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);