@gethmy/mcp 2.13.4 → 2.14.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/src/server.ts CHANGED
@@ -121,6 +121,65 @@ function requireExactlyOneScope(scope: {
121
121
  }
122
122
  }
123
123
 
124
+ /**
125
+ * Backward-compat dispatch shim (#605). Consolidated tools dropped the old
126
+ * names from the advertised TOOLS list (the token win), but `handleToolCall`
127
+ * still dispatches them so external integrations calling the old names keep
128
+ * working through a deprecation window. Logged once per name to stderr (never
129
+ * stdout — that carries the MCP protocol on stdio).
130
+ */
131
+ const DEPRECATED_TOOL_ALIASES: Record<string, string> = {
132
+ harmony_get_card_by_short_id: "harmony_get_card",
133
+ harmony_bulk_get_cards: "harmony_get_card",
134
+ harmony_upload_artifact: "harmony_upload",
135
+ harmony_upload_card_attachment: "harmony_upload",
136
+ harmony_request_artifact_upload_url: "harmony_request_upload_url",
137
+ harmony_request_card_attachment_upload_url: "harmony_request_upload_url",
138
+ harmony_finalize_artifact: "harmony_finalize_upload",
139
+ harmony_finalize_card_attachment: "harmony_finalize_upload",
140
+ };
141
+ const _warnedDeprecatedTools = new Set<string>();
142
+ function warnDeprecatedTool(oldName: string): void {
143
+ if (_warnedDeprecatedTools.has(oldName)) return;
144
+ _warnedDeprecatedTools.add(oldName);
145
+ const replacement = DEPRECATED_TOOL_ALIASES[oldName];
146
+ console.error(
147
+ `[harmony-mcp] Tool "${oldName}" is deprecated (still accepted) — ` +
148
+ `use "${replacement}" instead. The old name is no longer advertised and ` +
149
+ `will be removed in a future major version.`,
150
+ );
151
+ }
152
+
153
+ /**
154
+ * Removed-capability shim (#612). The legacy steps_version=1 "Automation" macro
155
+ * was retired when Harmony committed to the single stage-Playbook model. These
156
+ * tools have no v2 replacement, so — following the #605 hidden-dispatch pattern —
157
+ * they are dropped from the advertised TOOLS list but their switch cases remain,
158
+ * returning a one-time deprecation notice (warned once to stderr) instead of an
159
+ * unknown-tool error, so an integration calling the old name fails legibly.
160
+ */
161
+ const DEPRECATED_REMOVED_TOOLS: Record<string, string> = {
162
+ harmony_run_playbook:
163
+ "Playbooks are stage models run by the board and agent daemon as a card moves through their stages — there is no server-side macro to trigger. This tool no longer runs anything.",
164
+ harmony_save_card_as_playbook:
165
+ "The steps_version=1 automation macro was removed. Build a stage playbook with harmony_create_playbook instead.",
166
+ };
167
+ const _warnedRemovedTools = new Set<string>();
168
+ function deprecatedRemovedToolResult(name: string): {
169
+ success: false;
170
+ deprecated: true;
171
+ message: string;
172
+ } {
173
+ const message = DEPRECATED_REMOVED_TOOLS[name];
174
+ if (!_warnedRemovedTools.has(name)) {
175
+ _warnedRemovedTools.add(name);
176
+ console.error(
177
+ `[harmony-mcp] Tool "${name}" is deprecated and no longer functional — ${message}`,
178
+ );
179
+ }
180
+ return { success: false, deprecated: true, message };
181
+ }
182
+
124
183
  /** PUT raw bytes straight to a Supabase signed upload URL (the token rides in
125
184
  * the URL query). Used by the stdio internal handshake; the hosted server can't
126
185
  * read local disk, so there the agent drives this PUT itself via the two-step
@@ -625,49 +684,37 @@ export const TOOLS = {
625
684
  },
626
685
  },
627
686
  harmony_get_card: {
628
- description: "Get detailed information about a specific card by UUID",
629
- inputSchema: {
630
- type: "object",
631
- properties: {
632
- cardId: { type: "string", description: "Card UUID" },
633
- },
634
- required: ["cardId"],
635
- },
636
- },
637
- harmony_get_card_by_short_id: {
638
- description: "Get a card by its short ID (e.g., #42) within a project",
687
+ description:
688
+ "Fetch one or many cards. Provide exactly one of: `cardId` (UUID, full detail), " +
689
+ "`shortId` (e.g. 42, full detail), or `shortIds` (array, max 100 — compact " +
690
+ "summaries: id, shortId, title, column, priority, assignee, labels, archived, " +
691
+ "plus any short ids not found). `shortId`/`shortIds` need project context. " +
692
+ "Prefer one `shortIds` call over repeated single fetches for multiple cards.",
639
693
  inputSchema: {
640
694
  type: "object",
641
695
  properties: {
642
- projectId: {
696
+ cardId: {
643
697
  type: "string",
644
- description: "Project ID (optional if context set)",
698
+ description: "Card UUID full detail for one card.",
645
699
  },
646
700
  shortId: {
647
701
  type: "number",
648
- description: "Short ID number (e.g., 42 for card #42)",
702
+ description:
703
+ "Short ID (e.g. 42 for card #42) — full detail, project-scoped.",
649
704
  },
650
- },
651
- required: ["shortId"],
652
- },
653
- },
654
- harmony_bulk_get_cards: {
655
- description:
656
- "Fetch multiple cards by short id in one call. Returns compact summaries " +
657
- "(id, shortId, title, column, priority, assignee, labels, archived) plus " +
658
- "any short ids not found. Requires project context. Prefer this over " +
659
- "repeated harmony_get_card_by_short_id when referencing multiple cards.",
660
- inputSchema: {
661
- type: "object",
662
- properties: {
663
705
  shortIds: {
664
706
  type: "array",
665
707
  items: { type: "number" },
666
708
  description:
667
- "Card short ids, e.g. [400, 401, 402]. Max 100 per call.",
709
+ "Short IDs, e.g. [400, 401, 402] compact summaries in one call. Max 100.",
710
+ },
711
+ projectId: {
712
+ type: "string",
713
+ description:
714
+ "Project ID for shortId/shortIds (optional if context set).",
668
715
  },
669
716
  },
670
- required: ["shortIds"],
717
+ required: [],
671
718
  },
672
719
  },
673
720
  harmony_bulk_archive_cards: {
@@ -837,186 +884,162 @@ export const TOOLS = {
837
884
  required: ["cardId"],
838
885
  },
839
886
  },
840
- harmony_upload_card_attachment: {
887
+ harmony_upload: {
841
888
  description:
842
- "Upload a file attachment to a card. Provide `filePath` (local path the server reads, direct-to-storage — local/stdio mode) or `base64Data` (works everywhere, small-file fallback). Max 5MB; allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT, CSV. Returns the attachment + a signed URL. Large files on the hosted MCP server: use harmony_request_card_attachment_upload_url + harmony_finalize_card_attachment instead.",
889
+ 'Upload a file in one call. `target: "card_attachment"` attaches a file to a card ' +
890
+ "(max 5MB; PNG/JPEG/GIF/WebP/HEIC/HEIF/PDF/DOC(X)/XLS(X)/TXT/CSV) — requires cardId. " +
891
+ '`target: "artifact"` hosts a self-contained HTML doc (text/html, max 2MB) linked to ' +
892
+ "exactly one of cardId/planId/workspaceId, rendered in-app in a sandboxed iframe. Provide " +
893
+ "the bytes as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). " +
894
+ "Returns the attachment/artifact + a signed URL (artifact: use harmony_share_artifact for a " +
895
+ "public link). Large files on the hosted MCP server: use harmony_request_upload_url + " +
896
+ "harmony_finalize_upload instead.",
843
897
  inputSchema: {
844
898
  type: "object",
845
899
  properties: {
846
- cardId: { type: "string", description: "Card UUID" },
900
+ target: {
901
+ type: "string",
902
+ enum: ["card_attachment", "artifact"],
903
+ description:
904
+ "What to upload: a card file attachment or a hosted HTML artifact.",
905
+ },
906
+ cardId: {
907
+ type: "string",
908
+ description:
909
+ "Card UUID. Required for card_attachment; one of cardId/planId/workspaceId for artifact.",
910
+ },
911
+ planId: {
912
+ type: "string",
913
+ description:
914
+ "Plan UUID (artifact only — link the artifact to a plan).",
915
+ },
916
+ workspaceId: {
917
+ type: "string",
918
+ description: "Workspace UUID (artifact only — standalone artifact).",
919
+ },
847
920
  filePath: {
848
921
  type: "string",
849
922
  description:
850
- "Absolute path to a local file the MCP server process can read. Mutually exclusive with base64Data.",
923
+ "Absolute path to a local file the server can read. Mutually exclusive with base64Data.",
851
924
  },
852
925
  base64Data: {
853
926
  type: "string",
854
927
  description:
855
- "Base64-encoded file bytes (a `data:` URL prefix is accepted and stripped). Requires fileName. Mutually exclusive with filePath.",
928
+ "Base64-encoded bytes (a `data:` URL prefix is accepted and stripped). Mutually exclusive with filePath.",
856
929
  },
857
930
  fileName: {
858
931
  type: "string",
859
932
  description:
860
- "File name including extension (e.g. 'screenshot.png'). Required with base64Data; defaults to the basename of filePath otherwise.",
933
+ "File name including extension (card_attachment: required with base64Data, else defaults to the filePath basename).",
934
+ },
935
+ title: {
936
+ type: "string",
937
+ description:
938
+ "Artifact display title (defaults to the file basename).",
861
939
  },
862
940
  contentType: {
863
941
  type: "string",
864
942
  description:
865
- "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted.",
943
+ "Optional MIME type (card_attachment; inferred from the extension when omitted).",
866
944
  },
867
945
  },
868
- required: ["cardId"],
946
+ required: ["target"],
869
947
  },
870
948
  },
871
- harmony_request_card_attachment_upload_url: {
949
+ harmony_request_upload_url: {
872
950
  description:
873
- "Step 1 of the large-file / hosted-MCP upload handshake (use when the server can't read local disk). Mints a one-shot signed Storage upload URL under the card. Returns { uploadUrl, token, storagePath, fileType }. Then PUT the raw bytes to uploadUrl (no bytes through the model context) and call harmony_finalize_card_attachment with storagePath. Max 5MB; same allowed types as harmony_upload_card_attachment.",
951
+ "Step 1 of the large-file / hosted-MCP upload handshake (use when the server can't read " +
952
+ 'local disk). Mints a one-shot signed Storage upload URL. `target: "card_attachment"` ' +
953
+ '(requires cardId, fileName, size; max 5MB) or `target: "artifact"` (one of ' +
954
+ "cardId/planId/workspaceId; text/html, max 2MB). Returns { uploadUrl, token, storagePath, " +
955
+ "[fileType] }. Then PUT the raw bytes to uploadUrl (no bytes through the model context) and " +
956
+ "call harmony_finalize_upload with the storagePath.",
874
957
  inputSchema: {
875
958
  type: "object",
876
959
  properties: {
877
- cardId: { type: "string", description: "Card UUID" },
878
- fileName: {
960
+ target: {
879
961
  type: "string",
880
- description: "File name including extension (e.g. 'screenshot.png').",
962
+ enum: ["card_attachment", "artifact"],
963
+ description:
964
+ "What to upload: a card file attachment or a hosted HTML artifact.",
881
965
  },
882
- fileType: {
966
+ cardId: {
883
967
  type: "string",
884
968
  description:
885
- "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted.",
969
+ "Card UUID. Required for card_attachment; one of cardId/planId/workspaceId for artifact.",
886
970
  },
887
- size: {
888
- type: "number",
889
- description: "File size in bytes (rejected early if over 5MB).",
890
- },
891
- },
892
- required: ["cardId", "fileName", "size"],
893
- },
894
- },
895
- harmony_finalize_card_attachment: {
896
- description:
897
- "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this with storagePath to validate and register the attachment. The server re-downloads the object and enforces size + an allowlisted content-type (magic-byte sniff, never the declared type) + an optional sha256 integrity check, deleting and failing on any mismatch. Returns the attachment + a signed URL.",
898
- inputSchema: {
899
- type: "object",
900
- properties: {
901
- cardId: { type: "string", description: "Card UUID" },
902
- storagePath: {
971
+ planId: { type: "string", description: "Plan UUID (artifact only)." },
972
+ workspaceId: {
903
973
  type: "string",
904
- description:
905
- "The storagePath returned by harmony_request_card_attachment_upload_url.",
974
+ description: "Workspace UUID (artifact only).",
906
975
  },
907
976
  fileName: {
908
977
  type: "string",
909
- description: "File name including extension (e.g. 'screenshot.png').",
978
+ description:
979
+ "File name including extension (required for card_attachment).",
910
980
  },
981
+ title: { type: "string", description: "Artifact display title." },
911
982
  fileType: {
912
983
  type: "string",
913
984
  description:
914
- "Optional MIME type; inferred from the extension when omitted.",
985
+ "card_attachment MIME type (inferred from the extension when omitted).",
915
986
  },
916
- sha256: {
987
+ contentType: {
917
988
  type: "string",
918
989
  description:
919
- "Optional hex SHA-256 of the uploaded bytes; verified against the stored object.",
990
+ "artifact MIME type; only 'text/html' is accepted (the default).",
920
991
  },
921
992
  size: {
922
993
  type: "number",
923
994
  description:
924
- "Optional byte size (advisory; re-validated server-side).",
995
+ "File size in bytes (rejected early if over the target's limit).",
925
996
  },
926
997
  },
927
- required: ["cardId", "storagePath", "fileName"],
928
- },
929
- },
930
- harmony_classify_card: {
931
- description:
932
- "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the type label (feature/bug/idea). Call right after creating a card to classify it in-flow. Idempotent; never touches the user-owned `model_override`.",
933
- inputSchema: {
934
- type: "object",
935
- properties: {
936
- cardId: { type: "string", description: "Card UUID" },
937
- },
938
- required: ["cardId"],
998
+ required: ["target"],
939
999
  },
940
1000
  },
941
- harmony_upload_artifact: {
1001
+ harmony_finalize_upload: {
942
1002
  description:
943
- "Host a self-contained HTML document (design draft, diagram) and link it to a card, plan, or workspace; rendered in-app in a sandboxed iframe. Provide exactly one of cardId/planId/workspaceId, and the HTML as `filePath` (local, direct-to-storage) or `base64Data` (small-file fallback). text/html only, max 2MB. Returns the artifact + a signed URL; use harmony_share_artifact for a public link. Large files on the hosted MCP server: use harmony_request_artifact_upload_url + harmony_finalize_artifact instead.",
1003
+ "Step 2 of the upload handshake. After PUTting the bytes to the signed uploadUrl, call this " +
1004
+ "with the storagePath to validate and register. The server re-downloads the object and " +
1005
+ "enforces size + content-type (magic-byte sniff, never the declared type) + an optional " +
1006
+ 'sha256 integrity check, deleting and failing on any mismatch. `target: "card_attachment"` ' +
1007
+ '(cardId, storagePath, fileName) or `target: "artifact"` (storagePath + the same one of ' +
1008
+ "cardId/planId/workspaceId used for the upload URL). Returns the attachment/artifact + a signed URL.",
944
1009
  inputSchema: {
945
1010
  type: "object",
946
1011
  properties: {
947
- title: {
948
- type: "string",
949
- description: "Display title (defaults to the file basename).",
950
- },
951
- cardId: { type: "string", description: "Link to this card (UUID)." },
952
- planId: { type: "string", description: "Link to this plan (UUID)." },
953
- workspaceId: {
1012
+ target: {
954
1013
  type: "string",
1014
+ enum: ["card_attachment", "artifact"],
955
1015
  description:
956
- "Attach to this workspace as a standalone artifact (UUID).",
1016
+ "What to finalize: a card file attachment or a hosted HTML artifact.",
957
1017
  },
958
- filePath: {
959
- type: "string",
960
- description:
961
- "Absolute path to a local .html file the MCP server process can read. Mutually exclusive with base64Data.",
962
- },
963
- base64Data: {
1018
+ storagePath: {
964
1019
  type: "string",
965
1020
  description:
966
- "Base64-encoded HTML bytes (a `data:` URL prefix is accepted and stripped). Mutually exclusive with filePath.",
1021
+ "The storagePath returned by harmony_request_upload_url.",
967
1022
  },
968
- },
969
- required: [],
970
- },
971
- },
972
- harmony_request_artifact_upload_url: {
973
- description:
974
- "Step 1 of the large-file / hosted-MCP artifact upload handshake (use when the server can't read local disk). Mints a one-shot signed Storage upload URL. Provide exactly one of cardId/planId/workspaceId. Returns { uploadUrl, token, storagePath }. Then PUT the HTML bytes to uploadUrl and call harmony_finalize_artifact with storagePath. text/html only, max 2MB.",
975
- inputSchema: {
976
- type: "object",
977
- properties: {
978
- title: {
1023
+ cardId: {
979
1024
  type: "string",
980
1025
  description:
981
- "Display title (defaults to the file basename at finalize).",
1026
+ "Card UUID. Required for card_attachment; scope for artifact.",
982
1027
  },
983
- cardId: { type: "string", description: "Link to this card (UUID)." },
984
- planId: { type: "string", description: "Link to this plan (UUID)." },
1028
+ planId: { type: "string", description: "Plan UUID (artifact only)." },
985
1029
  workspaceId: {
986
1030
  type: "string",
987
- description:
988
- "Attach to this workspace as a standalone artifact (UUID).",
1031
+ description: "Workspace UUID (artifact only).",
989
1032
  },
990
- contentType: {
991
- type: "string",
992
- description: "MIME type; only 'text/html' is accepted (the default).",
993
- },
994
- size: {
995
- type: "number",
996
- description: "File size in bytes (rejected early if over 2MB).",
997
- },
998
- },
999
- required: [],
1000
- },
1001
- },
1002
- harmony_finalize_artifact: {
1003
- description:
1004
- "Step 2 of the artifact upload handshake. After PUTting the HTML bytes to the signed uploadUrl, call this with storagePath to validate and register the artifact. The server re-downloads the object and enforces size + text/html (magic-byte sniff) + an optional sha256 integrity check, deleting and failing on any mismatch. Pass the same one of cardId/planId/workspaceId used for the upload URL. Returns the artifact + a signed URL; use harmony_share_artifact for a public link.",
1005
- inputSchema: {
1006
- type: "object",
1007
- properties: {
1008
- storagePath: {
1033
+ fileName: {
1009
1034
  type: "string",
1010
1035
  description:
1011
- "The storagePath returned by harmony_request_artifact_upload_url.",
1036
+ "File name including extension (required for card_attachment).",
1012
1037
  },
1013
- title: { type: "string", description: "Display title." },
1014
- cardId: { type: "string", description: "Link to this card (UUID)." },
1015
- planId: { type: "string", description: "Link to this plan (UUID)." },
1016
- workspaceId: {
1038
+ title: { type: "string", description: "Artifact display title." },
1039
+ fileType: {
1017
1040
  type: "string",
1018
1041
  description:
1019
- "Attach to this workspace as a standalone artifact (UUID).",
1042
+ "card_attachment MIME type; inferred from the extension when omitted.",
1020
1043
  },
1021
1044
  sha256: {
1022
1045
  type: "string",
@@ -1029,7 +1052,18 @@ export const TOOLS = {
1029
1052
  "Optional byte size (advisory; re-validated server-side).",
1030
1053
  },
1031
1054
  },
1032
- required: ["storagePath"],
1055
+ required: ["target", "storagePath"],
1056
+ },
1057
+ },
1058
+ harmony_classify_card: {
1059
+ description:
1060
+ "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the type label (feature/bug/idea). Call right after creating a card to classify it in-flow. Idempotent; never touches the user-owned `model_override`.",
1061
+ inputSchema: {
1062
+ type: "object",
1063
+ properties: {
1064
+ cardId: { type: "string", description: "Card UUID" },
1065
+ },
1066
+ required: ["cardId"],
1033
1067
  },
1034
1068
  },
1035
1069
  harmony_share_artifact: {
@@ -1062,6 +1096,28 @@ export const TOOLS = {
1062
1096
  required: ["cardId"],
1063
1097
  },
1064
1098
  },
1099
+ harmony_add_external_link: {
1100
+ description:
1101
+ "Attach an external reference URL (e.g. a PR/MR link, doc, or dashboard) to a card. Durable — survives description edits. Use this in addition to writing the PR link into the description.",
1102
+ inputSchema: {
1103
+ type: "object",
1104
+ properties: {
1105
+ cardId: {
1106
+ type: "string",
1107
+ description: "Card UUID",
1108
+ },
1109
+ url: {
1110
+ type: "string",
1111
+ description: "The URL to attach",
1112
+ },
1113
+ title: {
1114
+ type: "string",
1115
+ description: "Optional human-readable title for the link",
1116
+ },
1117
+ },
1118
+ required: ["cardId", "url"],
1119
+ },
1120
+ },
1065
1121
 
1066
1122
  // Subtask operations
1067
1123
  harmony_create_subtask: {
@@ -1119,7 +1175,7 @@ export const TOOLS = {
1119
1175
  // Comment operations
1120
1176
  harmony_add_comment: {
1121
1177
  description:
1122
- "Post a comment on a card as the agent — converse with the human in the open: report progress, ask a question, record a decision, or note a finding, instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one.",
1178
+ "Post a comment on a card as the agent — converse with the human in the open: report progress, ask a question, record a decision, or note a finding, instead of editing the card description. Set supersedesId to correct an earlier comment, confirmsId to reaffirm one. To answer an open question, reply to it with replyToId.",
1123
1179
  inputSchema: {
1124
1180
  type: "object",
1125
1181
  properties: {
@@ -1147,6 +1203,11 @@ export const TOOLS = {
1147
1203
  type: "string",
1148
1204
  description: "Comment id this comment reaffirms",
1149
1205
  },
1206
+ replyToId: {
1207
+ type: "string",
1208
+ description:
1209
+ "Comment id this is a one-level reply to. To answer an open question, reply to it. If you also set supersedesId/confirmsId, it must equal replyToId.",
1210
+ },
1150
1211
  },
1151
1212
  required: ["cardId", "body"],
1152
1213
  },
@@ -2101,7 +2162,7 @@ export const TOOLS = {
2101
2162
 
2102
2163
  harmony_list_playbook: {
2103
2164
  description:
2104
- "List a workspace's playbooks (reusable process definitions). Returns each playbook's name, version, steps_version, and state. Read-only.",
2165
+ "List a workspace's playbooks (reusable process definitions). Returns each playbook's name, version, and state. Read-only.",
2105
2166
  inputSchema: {
2106
2167
  type: "object",
2107
2168
  properties: {
@@ -2126,24 +2187,9 @@ export const TOOLS = {
2126
2187
  },
2127
2188
  },
2128
2189
 
2129
- harmony_run_playbook: {
2130
- description:
2131
- "Run a playbook server-side and return the finalized run. Only legacy automation playbooks (steps_version 1) are runnable; stage playbooks (steps_version 2) are rejected. The server drives every step to completion.",
2132
- inputSchema: {
2133
- type: "object",
2134
- properties: {
2135
- playbookId: {
2136
- type: "string",
2137
- description: "Playbook ID to run (UUID)",
2138
- },
2139
- },
2140
- required: ["playbookId"],
2141
- },
2142
- },
2143
-
2144
2190
  harmony_create_playbook: {
2145
2191
  description:
2146
- "Create a new playbook in a workspace. Default steps_version 1 is a legacy automation macro (an array of tool steps); steps_version 2 is the Method stage model (an array of stage objects).",
2192
+ "Create a new playbook (a reusable process definition) in a workspace. A playbook is an ordered set of stages bound to board columns, run by people and the agent daemon as a card moves through them.",
2147
2193
  inputSchema: {
2148
2194
  type: "object",
2149
2195
  properties: {
@@ -2153,16 +2199,9 @@ export const TOOLS = {
2153
2199
  },
2154
2200
  name: { type: "string", description: "Playbook name" },
2155
2201
  description: { type: "string", description: "Playbook description" },
2156
- stepsVersion: {
2157
- type: "number",
2158
- enum: [1, 2],
2159
- description:
2160
- "1 = automation macro (tool steps), 2 = Method stage model (stage objects). Default 1.",
2161
- },
2162
2202
  steps: {
2163
2203
  type: "array",
2164
- description:
2165
- "Steps (steps_version 1: tool-step objects) or stages (steps_version 2: stage objects).",
2204
+ description: "The playbook's ordered stage objects.",
2166
2205
  items: { type: "object" },
2167
2206
  },
2168
2207
  },
@@ -2184,7 +2223,7 @@ export const TOOLS = {
2184
2223
  description: { type: "string", description: "New description" },
2185
2224
  steps: {
2186
2225
  type: "array",
2187
- description: "New steps (v1) or stages (v2) array.",
2226
+ description: "New ordered stage objects.",
2188
2227
  items: { type: "object" },
2189
2228
  },
2190
2229
  enabled: {
@@ -2201,22 +2240,6 @@ export const TOOLS = {
2201
2240
  },
2202
2241
  },
2203
2242
 
2204
- harmony_save_card_as_playbook: {
2205
- description:
2206
- "Save an existing card as a new steps_version 1 (automation) playbook, seeding one create-card step from the card. Returns the created playbook.",
2207
- inputSchema: {
2208
- type: "object",
2209
- properties: {
2210
- cardId: { type: "string", description: "Card ID to template (UUID)" },
2211
- name: {
2212
- type: "string",
2213
- description: "Name for the new playbook (defaults to the card title)",
2214
- },
2215
- },
2216
- required: ["cardId"],
2217
- },
2218
- },
2219
-
2220
2243
  // ============ ONBOARDING TOOLS ============
2221
2244
  harmony_signup: {
2222
2245
  description:
@@ -2807,30 +2830,41 @@ async function handleToolCall(
2807
2830
  return { success: true, ...result, count: result.cards.length };
2808
2831
  }
2809
2832
 
2833
+ // Unified card fetch (#605): cardId (full) | shortId (full) | shortIds (compact).
2834
+ // Old names dispatch here too, behind the deprecation shim.
2835
+ case "harmony_get_card_by_short_id":
2836
+ case "harmony_bulk_get_cards":
2810
2837
  case "harmony_get_card": {
2838
+ if (name !== "harmony_get_card") warnDeprecatedTool(name);
2839
+ const hasShortIds = args.shortIds != null;
2840
+ const hasShortId = args.shortId != null;
2841
+ const hasCardId = args.cardId != null;
2842
+ if ([hasShortIds, hasShortId, hasCardId].filter(Boolean).length !== 1) {
2843
+ throw new Error(
2844
+ "Provide exactly one of: cardId (UUID), shortId (number), or shortIds (number[]).",
2845
+ );
2846
+ }
2847
+ if (hasShortIds) {
2848
+ const shortIds = z
2849
+ .array(z.number().int().positive())
2850
+ .min(1)
2851
+ .max(100)
2852
+ .parse(args.shortIds);
2853
+ const projectId = getProjectId();
2854
+ const result = await client.bulkGetCards(projectId, shortIds);
2855
+ return { success: true, ...result };
2856
+ }
2857
+ if (hasShortId) {
2858
+ const shortId = z.number().int().positive().parse(args.shortId);
2859
+ const projectId = (args.projectId as string) || getProjectId();
2860
+ const result = await client.getCardByShortId(projectId, shortId);
2861
+ return { success: true, ...result };
2862
+ }
2811
2863
  const cardId = z.string().uuid().parse(args.cardId);
2812
2864
  const result = await client.getCard(cardId);
2813
2865
  return { success: true, ...result };
2814
2866
  }
2815
2867
 
2816
- case "harmony_get_card_by_short_id": {
2817
- const shortId = z.number().int().positive().parse(args.shortId);
2818
- const projectId = (args.projectId as string) || getProjectId();
2819
- const result = await client.getCardByShortId(projectId, shortId);
2820
- return { success: true, ...result };
2821
- }
2822
-
2823
- case "harmony_bulk_get_cards": {
2824
- const shortIds = z
2825
- .array(z.number().int().positive())
2826
- .min(1)
2827
- .max(100)
2828
- .parse(args.shortIds);
2829
- const projectId = getProjectId();
2830
- const result = await client.bulkGetCards(projectId, shortIds);
2831
- return { success: true, ...result };
2832
- }
2833
-
2834
2868
  case "harmony_bulk_archive_cards": {
2835
2869
  const shortIds = z
2836
2870
  .array(z.number().int().positive())
@@ -2978,71 +3012,85 @@ async function handleToolCall(
2978
3012
  return result;
2979
3013
  }
2980
3014
 
2981
- case "harmony_upload_card_attachment": {
2982
- const cardId = z.string().uuid().parse(args.cardId);
3015
+ // Unified upload (#605): target = card_attachment | artifact. Old names
3016
+ // (harmony_upload_artifact / harmony_upload_card_attachment) dispatch here
3017
+ // behind the deprecation shim; target is inferred from the old name.
3018
+ case "harmony_upload_card_attachment":
3019
+ case "harmony_upload_artifact":
3020
+ case "harmony_upload": {
3021
+ if (name !== "harmony_upload") warnDeprecatedTool(name);
3022
+ const target =
3023
+ name === "harmony_upload_card_attachment"
3024
+ ? "card_attachment"
3025
+ : name === "harmony_upload_artifact"
3026
+ ? "artifact"
3027
+ : z.enum(["card_attachment", "artifact"]).parse(args.target);
3028
+
2983
3029
  const filePath =
2984
3030
  args.filePath != null ? z.string().parse(args.filePath) : undefined;
2985
3031
  const base64Data =
2986
3032
  args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
2987
- const fileName =
2988
- args.fileName != null ? z.string().parse(args.fileName) : undefined;
2989
- const contentType =
2990
- args.contentType != null
2991
- ? z.string().parse(args.contentType)
2992
- : undefined;
2993
-
2994
3033
  if (filePath && base64Data) {
2995
3034
  throw new Error("Provide either filePath or base64Data, not both.");
2996
3035
  }
2997
3036
 
2998
- if (filePath) {
2999
- // Server can read the file → upload direct-to-storage via the handshake
3000
- // (no base64 through the model context or the edge-fn JSON body).
3001
- const bytes = await readFileForUpload(
3002
- filePath,
3003
- MAX_ATTACHMENT_SIZE,
3004
- "attachment",
3005
- );
3006
- const resolvedName = fileName || basename(filePath);
3007
- const signed = await client.requestCardAttachmentUploadUrl(cardId, {
3008
- fileName: resolvedName,
3009
- fileType: contentType,
3010
- size: bytes.byteLength,
3011
- });
3012
- await putToSignedUrl(
3013
- signed.uploadUrl,
3014
- bytes,
3015
- contentType || signed.fileType || "application/octet-stream",
3016
- );
3017
- return await client.finalizeCardAttachment(cardId, {
3018
- storagePath: signed.storagePath,
3019
- fileName: resolvedName,
3020
- fileType: contentType || signed.fileType,
3021
- sha256: sha256Hex(bytes),
3022
- size: bytes.byteLength,
3023
- });
3024
- }
3025
-
3026
- if (base64Data) {
3027
- if (!fileName) {
3028
- throw new Error("fileName is required when using base64Data.");
3029
- }
3030
- if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
3031
- throw new Error(
3032
- `File is over the 5MB attachment limit. Use the harmony_request_card_attachment_upload_url + harmony_finalize_card_attachment handshake for large files.`,
3037
+ if (target === "card_attachment") {
3038
+ const cardId = z.string().uuid().parse(args.cardId);
3039
+ const fileName =
3040
+ args.fileName != null ? z.string().parse(args.fileName) : undefined;
3041
+ const contentType =
3042
+ args.contentType != null
3043
+ ? z.string().parse(args.contentType)
3044
+ : undefined;
3045
+
3046
+ if (filePath) {
3047
+ // Server can read the file → upload direct-to-storage via the
3048
+ // handshake (no base64 through the model context or edge-fn body).
3049
+ const bytes = await readFileForUpload(
3050
+ filePath,
3051
+ MAX_ATTACHMENT_SIZE,
3052
+ "attachment",
3053
+ );
3054
+ const resolvedName = fileName || basename(filePath);
3055
+ const signed = await client.requestCardAttachmentUploadUrl(cardId, {
3056
+ fileName: resolvedName,
3057
+ fileType: contentType,
3058
+ size: bytes.byteLength,
3059
+ });
3060
+ await putToSignedUrl(
3061
+ signed.uploadUrl,
3062
+ bytes,
3063
+ contentType || signed.fileType || "application/octet-stream",
3033
3064
  );
3065
+ return await client.finalizeCardAttachment(cardId, {
3066
+ storagePath: signed.storagePath,
3067
+ fileName: resolvedName,
3068
+ fileType: contentType || signed.fileType,
3069
+ sha256: sha256Hex(bytes),
3070
+ size: bytes.byteLength,
3071
+ });
3034
3072
  }
3035
- return await client.uploadCardAttachment(cardId, {
3036
- fileName,
3037
- data: base64Data,
3038
- fileType: contentType,
3039
- });
3040
- }
3041
3073
 
3042
- throw new Error("Provide either filePath or base64Data.");
3043
- }
3074
+ if (base64Data) {
3075
+ if (!fileName) {
3076
+ throw new Error("fileName is required when using base64Data.");
3077
+ }
3078
+ if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
3079
+ throw new Error(
3080
+ `File is over the 5MB attachment limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`,
3081
+ );
3082
+ }
3083
+ return await client.uploadCardAttachment(cardId, {
3084
+ fileName,
3085
+ data: base64Data,
3086
+ fileType: contentType,
3087
+ });
3088
+ }
3044
3089
 
3045
- case "harmony_upload_artifact": {
3090
+ throw new Error("Provide either filePath or base64Data.");
3091
+ }
3092
+
3093
+ // target === "artifact"
3046
3094
  const title =
3047
3095
  args.title != null ? z.string().parse(args.title) : undefined;
3048
3096
  const cardId =
@@ -3055,17 +3103,7 @@ async function handleToolCall(
3055
3103
  : undefined;
3056
3104
  requireExactlyOneScope({ cardId, planId, workspaceId });
3057
3105
 
3058
- const filePath =
3059
- args.filePath != null ? z.string().parse(args.filePath) : undefined;
3060
- const base64Data =
3061
- args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
3062
- if (filePath && base64Data) {
3063
- throw new Error("Provide either filePath or base64Data, not both.");
3064
- }
3065
-
3066
3106
  if (filePath) {
3067
- // Server can read the file → upload direct-to-storage via the handshake
3068
- // (no base64 through the model context or the edge-fn JSON body).
3069
3107
  const bytes = await readFileForUpload(
3070
3108
  filePath,
3071
3109
  MAX_ARTIFACT_SIZE,
@@ -3095,7 +3133,7 @@ async function handleToolCall(
3095
3133
  if (base64Data) {
3096
3134
  if (base64ByteLength(base64Data) > MAX_ARTIFACT_SIZE) {
3097
3135
  throw new Error(
3098
- `Artifact is over the 2MB limit. Use the harmony_request_artifact_upload_url + harmony_finalize_artifact handshake for large files.`,
3136
+ `Artifact is over the 2MB limit. Use the harmony_request_upload_url + harmony_finalize_upload handshake for large files.`,
3099
3137
  );
3100
3138
  }
3101
3139
  return await client.uploadArtifact({
@@ -3110,7 +3148,37 @@ async function handleToolCall(
3110
3148
  throw new Error("Provide either filePath or base64Data.");
3111
3149
  }
3112
3150
 
3113
- case "harmony_request_artifact_upload_url": {
3151
+ // Unified upload handshake step 1 (#605). Old names shimmed; target inferred.
3152
+ case "harmony_request_artifact_upload_url":
3153
+ case "harmony_request_card_attachment_upload_url":
3154
+ case "harmony_request_upload_url": {
3155
+ if (name !== "harmony_request_upload_url") warnDeprecatedTool(name);
3156
+ const target =
3157
+ name === "harmony_request_card_attachment_upload_url"
3158
+ ? "card_attachment"
3159
+ : name === "harmony_request_artifact_upload_url"
3160
+ ? "artifact"
3161
+ : z.enum(["card_attachment", "artifact"]).parse(args.target);
3162
+
3163
+ if (target === "card_attachment") {
3164
+ const cardId = z.string().uuid().parse(args.cardId);
3165
+ const fileName = z.string().parse(args.fileName);
3166
+ const fileType =
3167
+ args.fileType != null ? z.string().parse(args.fileType) : undefined;
3168
+ const size = z.number().positive().parse(args.size);
3169
+ if (size > MAX_ATTACHMENT_SIZE) {
3170
+ throw new Error(
3171
+ `Declared size ${size} bytes is over the ${MAX_ATTACHMENT_SIZE}-byte (5MB) attachment limit.`,
3172
+ );
3173
+ }
3174
+ return await client.requestCardAttachmentUploadUrl(cardId, {
3175
+ fileName,
3176
+ fileType,
3177
+ size,
3178
+ });
3179
+ }
3180
+
3181
+ // target === "artifact"
3114
3182
  const title =
3115
3183
  args.title != null ? z.string().parse(args.title) : undefined;
3116
3184
  const cardId =
@@ -3143,8 +3211,40 @@ async function handleToolCall(
3143
3211
  });
3144
3212
  }
3145
3213
 
3146
- case "harmony_finalize_artifact": {
3214
+ // Unified upload handshake step 2 (#605). Old names shimmed; target inferred.
3215
+ case "harmony_finalize_artifact":
3216
+ case "harmony_finalize_card_attachment":
3217
+ case "harmony_finalize_upload": {
3218
+ if (name !== "harmony_finalize_upload") warnDeprecatedTool(name);
3219
+ const target =
3220
+ name === "harmony_finalize_card_attachment"
3221
+ ? "card_attachment"
3222
+ : name === "harmony_finalize_artifact"
3223
+ ? "artifact"
3224
+ : z.enum(["card_attachment", "artifact"]).parse(args.target);
3147
3225
  const storagePath = z.string().parse(args.storagePath);
3226
+
3227
+ if (target === "card_attachment") {
3228
+ const cardId = z.string().uuid().parse(args.cardId);
3229
+ const fileName = z.string().parse(args.fileName);
3230
+ const fileType =
3231
+ args.fileType != null ? z.string().parse(args.fileType) : undefined;
3232
+ const sha256 =
3233
+ args.sha256 != null ? z.string().parse(args.sha256) : undefined;
3234
+ const size =
3235
+ args.size != null
3236
+ ? z.number().positive().parse(args.size)
3237
+ : undefined;
3238
+ return await client.finalizeCardAttachment(cardId, {
3239
+ storagePath,
3240
+ fileName,
3241
+ fileType,
3242
+ sha256,
3243
+ size,
3244
+ });
3245
+ }
3246
+
3247
+ // target === "artifact"
3148
3248
  const title =
3149
3249
  args.title != null ? z.string().parse(args.title) : undefined;
3150
3250
  const cardId =
@@ -3171,43 +3271,6 @@ async function handleToolCall(
3171
3271
  });
3172
3272
  }
3173
3273
 
3174
- case "harmony_request_card_attachment_upload_url": {
3175
- const cardId = z.string().uuid().parse(args.cardId);
3176
- const fileName = z.string().parse(args.fileName);
3177
- const fileType =
3178
- args.fileType != null ? z.string().parse(args.fileType) : undefined;
3179
- const size = z.number().positive().parse(args.size);
3180
- if (size > MAX_ATTACHMENT_SIZE) {
3181
- throw new Error(
3182
- `Declared size ${size} bytes is over the ${MAX_ATTACHMENT_SIZE}-byte (5MB) attachment limit.`,
3183
- );
3184
- }
3185
- return await client.requestCardAttachmentUploadUrl(cardId, {
3186
- fileName,
3187
- fileType,
3188
- size,
3189
- });
3190
- }
3191
-
3192
- case "harmony_finalize_card_attachment": {
3193
- const cardId = z.string().uuid().parse(args.cardId);
3194
- const storagePath = z.string().parse(args.storagePath);
3195
- const fileName = z.string().parse(args.fileName);
3196
- const fileType =
3197
- args.fileType != null ? z.string().parse(args.fileType) : undefined;
3198
- const sha256 =
3199
- args.sha256 != null ? z.string().parse(args.sha256) : undefined;
3200
- const size =
3201
- args.size != null ? z.number().positive().parse(args.size) : undefined;
3202
- return await client.finalizeCardAttachment(cardId, {
3203
- storagePath,
3204
- fileName,
3205
- fileType,
3206
- sha256,
3207
- size,
3208
- });
3209
- }
3210
-
3211
3274
  case "harmony_share_artifact": {
3212
3275
  const artifactId = z.string().uuid().parse(args.artifactId);
3213
3276
  const expiresInDays =
@@ -3230,6 +3293,16 @@ async function handleToolCall(
3230
3293
  return result;
3231
3294
  }
3232
3295
 
3296
+ case "harmony_add_external_link": {
3297
+ const cardId = z.string().uuid().parse(args.cardId);
3298
+ const url = z.string().min(1).max(2048).parse(args.url);
3299
+ const title = args.title
3300
+ ? z.string().max(200).parse(args.title)
3301
+ : undefined;
3302
+ const result = await client.addExternalLink(cardId, url, title);
3303
+ return { success: true, ...result };
3304
+ }
3305
+
3233
3306
  case "harmony_classify_card": {
3234
3307
  const cardId = z.string().uuid().parse(args.cardId);
3235
3308
  const result = await client.classifyCard(cardId);
@@ -3307,10 +3380,15 @@ async function handleToolCall(
3307
3380
  args.confirmsId !== undefined
3308
3381
  ? z.string().uuid().parse(args.confirmsId)
3309
3382
  : undefined;
3383
+ const replyToId =
3384
+ args.replyToId !== undefined
3385
+ ? z.string().uuid().parse(args.replyToId)
3386
+ : undefined;
3310
3387
  const result = await client.addComment(cardId, body, {
3311
3388
  commentType,
3312
3389
  supersedesId,
3313
3390
  confirmsId,
3391
+ replyToId,
3314
3392
  });
3315
3393
  return { success: true, ...result };
3316
3394
  }
@@ -4717,25 +4795,19 @@ async function handleToolCall(
4717
4795
  return { success: true, playbook: result.playbook, runs: result.runs };
4718
4796
  }
4719
4797
 
4720
- case "harmony_run_playbook": {
4721
- const playbookId = z.string().uuid().parse(args.playbookId);
4722
- const result = await client.runPlaybook(playbookId);
4723
- return { success: true, run: result.run };
4724
- }
4798
+ // Deprecated (#612) — dropped from the advertised TOOLS list; the case
4799
+ // remains so an old caller gets a legible notice, not an unknown-tool error.
4800
+ case "harmony_run_playbook":
4801
+ return deprecatedRemovedToolResult("harmony_run_playbook");
4725
4802
 
4726
4803
  case "harmony_create_playbook": {
4727
4804
  const workspaceId = (args.workspaceId as string) || getWorkspaceId();
4728
4805
  const name = z.string().min(1).max(200).parse(args.name);
4729
- const stepsVersion =
4730
- args.stepsVersion !== undefined
4731
- ? z.union([z.literal(1), z.literal(2)]).parse(args.stepsVersion)
4732
- : undefined;
4733
4806
  const result = await client.createPlaybook({
4734
4807
  workspaceId,
4735
4808
  name,
4736
4809
  description: args.description as string | undefined,
4737
4810
  steps: args.steps,
4738
- stepsVersion,
4739
4811
  });
4740
4812
  return { success: true, playbook: result.playbook };
4741
4813
  }
@@ -4752,14 +4824,9 @@ async function handleToolCall(
4752
4824
  return { success: true, playbook: result.playbook };
4753
4825
  }
4754
4826
 
4755
- case "harmony_save_card_as_playbook": {
4756
- const cardId = z.string().uuid().parse(args.cardId);
4757
- const result = await client.savePlaybookFromCard({
4758
- cardId,
4759
- name: args.name as string | undefined,
4760
- });
4761
- return { success: true, playbook: result.playbook };
4762
- }
4827
+ // Deprecated (#612) — see harmony_run_playbook above.
4828
+ case "harmony_save_card_as_playbook":
4829
+ return deprecatedRemovedToolResult("harmony_save_card_as_playbook");
4763
4830
 
4764
4831
  // ============ ONBOARDING TOOLS ============
4765
4832
  case "harmony_signup": {