@gethmy/mcp 2.12.0 → 2.13.1

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/index.js CHANGED
@@ -985,6 +985,7 @@ var init_oauth_refresh = __esm(() => {
985
985
  });
986
986
 
987
987
  // src/server.ts
988
+ import { createHash as createHash4 } from "node:crypto";
988
989
  import { readFile } from "node:fs/promises";
989
990
  import { basename } from "node:path";
990
991
  // ../memory/dist/sync.js
@@ -1491,6 +1492,9 @@ var TIMINGS = {
1491
1492
  QUERY_STALE_TIME: 1000 * 60 * 5,
1492
1493
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
1493
1494
  };
1495
+ // ../harmony-shared/dist/stageHandoff.js
1496
+ var HANDOFF_MARKER = "harmony:stage-handoff";
1497
+ var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
1494
1498
  // src/api-client.ts
1495
1499
  init_config();
1496
1500
  var RETRY_CONFIG = {
@@ -1850,12 +1854,24 @@ class HarmonyApiClient {
1850
1854
  async uploadCardAttachment(cardId, data) {
1851
1855
  return this.request("POST", `/cards/${cardId}/attachments`, data);
1852
1856
  }
1857
+ async requestCardAttachmentUploadUrl(cardId, data) {
1858
+ return this.request("POST", `/cards/${cardId}/attachment-upload-url`, data);
1859
+ }
1860
+ async finalizeCardAttachment(cardId, data) {
1861
+ return this.request("POST", `/cards/${cardId}/attachments/finalize`, data);
1862
+ }
1853
1863
  async getCardExternalLinks(cardId) {
1854
1864
  return this.request("GET", `/cards/${cardId}/external-links`);
1855
1865
  }
1856
1866
  async uploadArtifact(data) {
1857
1867
  return this.request("POST", "/artifacts", data);
1858
1868
  }
1869
+ async requestArtifactUploadUrl(data) {
1870
+ return this.request("POST", "/artifacts/upload-url", data);
1871
+ }
1872
+ async finalizeArtifact(data) {
1873
+ return this.request("POST", "/artifacts/finalize", data);
1874
+ }
1859
1875
  async createArtifactShareLink(artifactId, expiresAt) {
1860
1876
  return this.request("POST", `/artifacts/${artifactId}/share`, {
1861
1877
  expires_at: expiresAt
@@ -2333,6 +2349,12 @@ ${planContent.trim()}`;
2333
2349
  async getPlaybook(playbookId) {
2334
2350
  return this.request("GET", `/playbooks/${playbookId}`);
2335
2351
  }
2352
+ async getPlaybookVersion(playbookId, version) {
2353
+ return this.request("GET", `/playbooks/${encodeURIComponent(playbookId)}/versions/${version}`);
2354
+ }
2355
+ async recordStageGateEvidence(insert) {
2356
+ return this.request("POST", `/cards/${encodeURIComponent(insert.card_id)}/stage-gate-evidence`, insert);
2357
+ }
2336
2358
  async createPlaybook(data) {
2337
2359
  return this.request("POST", "/playbooks", data);
2338
2360
  }
@@ -3431,6 +3453,52 @@ async function refreshSkills(opts = {}) {
3431
3453
  }
3432
3454
 
3433
3455
  // src/server.ts
3456
+ var MAX_ARTIFACT_SIZE = 2 * 1024 * 1024;
3457
+ var MAX_ATTACHMENT_SIZE = 5 * 1024 * 1024;
3458
+ function base64ByteLength(base64) {
3459
+ const stripped = base64.replace(/^data:[^,]*,/, "");
3460
+ return Buffer.from(stripped, "base64").byteLength;
3461
+ }
3462
+ function sha256Hex(bytes) {
3463
+ return createHash4("sha256").update(bytes).digest("hex");
3464
+ }
3465
+ async function readFileForUpload(filePath, maxSize, kind) {
3466
+ const bytes = await readFile(filePath);
3467
+ if (bytes.byteLength === 0) {
3468
+ throw new Error(`File is empty: ${filePath}`);
3469
+ }
3470
+ if (bytes.byteLength > maxSize) {
3471
+ const mb = Math.round(maxSize / (1024 * 1024));
3472
+ throw new Error(`File is ${bytes.byteLength} bytes, over the ${maxSize}-byte (${mb}MB) ${kind} limit.`);
3473
+ }
3474
+ return bytes;
3475
+ }
3476
+ function requireExactlyOneScope(scope) {
3477
+ if ([scope.cardId, scope.planId, scope.workspaceId].filter(Boolean).length !== 1) {
3478
+ throw new Error("Provide exactly one of cardId, planId, or workspaceId.");
3479
+ }
3480
+ }
3481
+ var SIGNED_UPLOAD_TIMEOUT_MS = 30000;
3482
+ async function putToSignedUrl(uploadUrl, bytes, contentType) {
3483
+ let res;
3484
+ try {
3485
+ res = await fetch(uploadUrl, {
3486
+ method: "PUT",
3487
+ headers: { "Content-Type": contentType },
3488
+ body: bytes,
3489
+ signal: AbortSignal.timeout(SIGNED_UPLOAD_TIMEOUT_MS)
3490
+ });
3491
+ } catch (err) {
3492
+ if (err instanceof DOMException && err.name === "TimeoutError") {
3493
+ throw new Error(`Direct storage upload timed out after ${SIGNED_UPLOAD_TIMEOUT_MS}ms.`);
3494
+ }
3495
+ throw err;
3496
+ }
3497
+ if (!res.ok) {
3498
+ const detail = await res.text().catch(() => "");
3499
+ throw new Error(`Direct storage upload failed: ${res.status}${detail ? ` — ${detail}` : ""}`);
3500
+ }
3501
+ }
3434
3502
  var memorySessions = new Map;
3435
3503
  function parseLabelList(raw) {
3436
3504
  if (raw === undefined || raw === null)
@@ -3456,6 +3524,45 @@ function parseLabelList(raw) {
3456
3524
  }
3457
3525
  return;
3458
3526
  }
3527
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3528
+ function requireStringArg(raw, field, opts = {}) {
3529
+ let value;
3530
+ if (typeof raw === "string")
3531
+ value = raw.trim();
3532
+ else if (typeof raw === "number" || typeof raw === "boolean")
3533
+ value = String(raw);
3534
+ else
3535
+ throw new Error(`${field} is required and must be a string (received ${raw === undefined || raw === null ? "nothing" : typeof raw}).`);
3536
+ if (value.length === 0)
3537
+ throw new Error(`${field} is required and must not be empty.`);
3538
+ if (opts.max && value.length > opts.max)
3539
+ throw new Error(`${field} must be at most ${opts.max} characters (received ${value.length}).`);
3540
+ return value;
3541
+ }
3542
+ function requireUuidArg(raw, field) {
3543
+ const value = requireStringArg(raw, field);
3544
+ if (!UUID_RE.test(value))
3545
+ throw new Error(`${field} must be a valid UUID (received "${value}").`);
3546
+ return value;
3547
+ }
3548
+ function optionalPercentArg(raw, field) {
3549
+ if (raw === undefined || raw === null)
3550
+ return;
3551
+ const n = typeof raw === "number" ? raw : Number(raw);
3552
+ if (!Number.isFinite(n))
3553
+ throw new Error(`${field} must be a number between 0 and 100.`);
3554
+ if (n < 0 || n > 100)
3555
+ throw new Error(`${field} must be between 0 and 100 (received ${n}).`);
3556
+ return n;
3557
+ }
3558
+ function optionalNonNegativeNumberArg(raw, field) {
3559
+ if (raw === undefined || raw === null)
3560
+ return;
3561
+ const n = typeof raw === "number" ? raw : Number(raw);
3562
+ if (!Number.isFinite(n) || n < 0)
3563
+ throw new Error(`${field} must be a non-negative number.`);
3564
+ return n;
3565
+ }
3459
3566
  function initMemorySession(cardId, agentIdentifier, agentName, agentSessionId) {
3460
3567
  memorySessions.set(cardId, {
3461
3568
  cardId,
@@ -3891,7 +3998,7 @@ var TOOLS = {
3891
3998
  }
3892
3999
  },
3893
4000
  harmony_upload_card_attachment: {
3894
- description: "Upload a file attachment to a card (e.g. a pasted screenshot or a document). Provide the file either as `filePath` (a local path the MCP server can read — works in local/stdio mode) or as `base64Data` (raw base64 bytes — works everywhere, including remote mode). Max 5MB. Allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT, CSV. Returns the stored attachment with a short-lived signed URL.",
4001
+ description: "Upload a file attachment to a card (e.g. a pasted screenshot or a document). Provide the file either as `filePath` (a local path the MCP server can read — works in local/stdio mode) or as `base64Data` (raw base64 bytes — works everywhere, including remote mode). With `filePath` the server uploads direct-to-storage (no base64 through the model context); `base64Data` is the small-file fallback. Max 5MB. Allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT, CSV. Returns the stored attachment with a short-lived signed URL. For large files on the hosted MCP server (where only base64Data works), prefer the two-step harmony_request_card_attachment_upload_url + harmony_finalize_card_attachment handshake, which keeps bytes out of the model context.",
3895
4002
  inputSchema: {
3896
4003
  type: "object",
3897
4004
  properties: {
@@ -3916,6 +4023,58 @@ var TOOLS = {
3916
4023
  required: ["cardId"]
3917
4024
  }
3918
4025
  },
4026
+ harmony_request_card_attachment_upload_url: {
4027
+ description: "Step 1 of the agent-driven card-attachment upload — use this for large files or when running against the hosted MCP server (which cannot read your local disk). Mints a one-shot signed Supabase Storage upload URL for a server-chosen path under the card. Provide cardId, fileName (with extension), the byte size, and optionally fileType. Returns { uploadUrl, token, storagePath, fileType }. Next: PUT the raw bytes straight to uploadUrl (e.g. `curl -X PUT --data-binary @file.png '<uploadUrl>'`) — no bytes pass through the model context — then call harmony_finalize_card_attachment with the returned storagePath. Max 5MB; allowed: PNG, JPEG, GIF, WebP, HEIC/HEIF, PDF, DOC/DOCX, XLS/XLSX, TXT, CSV.",
4028
+ inputSchema: {
4029
+ type: "object",
4030
+ properties: {
4031
+ cardId: { type: "string", description: "Card UUID" },
4032
+ fileName: {
4033
+ type: "string",
4034
+ description: "File name including extension (e.g. 'screenshot.png')."
4035
+ },
4036
+ fileType: {
4037
+ type: "string",
4038
+ description: "Optional MIME type (e.g. 'image/png'). Inferred from the file extension when omitted."
4039
+ },
4040
+ size: {
4041
+ type: "number",
4042
+ description: "File size in bytes (rejected early if over 5MB)."
4043
+ }
4044
+ },
4045
+ required: ["cardId", "fileName", "size"]
4046
+ }
4047
+ },
4048
+ harmony_finalize_card_attachment: {
4049
+ description: "Step 2 of the agent-driven card-attachment upload. After PUTting the bytes to the signed uploadUrl from harmony_request_card_attachment_upload_url, call this with the returned 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), and — when you pass sha256 — an integrity check, deleting the object and failing on any mismatch. Returns the stored attachment with a short-lived signed URL.",
4050
+ inputSchema: {
4051
+ type: "object",
4052
+ properties: {
4053
+ cardId: { type: "string", description: "Card UUID" },
4054
+ storagePath: {
4055
+ type: "string",
4056
+ description: "The storagePath returned by harmony_request_card_attachment_upload_url."
4057
+ },
4058
+ fileName: {
4059
+ type: "string",
4060
+ description: "File name including extension (e.g. 'screenshot.png')."
4061
+ },
4062
+ fileType: {
4063
+ type: "string",
4064
+ description: "Optional MIME type; inferred from the extension when omitted."
4065
+ },
4066
+ sha256: {
4067
+ type: "string",
4068
+ description: "Optional hex SHA-256 of the uploaded bytes; verified against the stored object."
4069
+ },
4070
+ size: {
4071
+ type: "number",
4072
+ description: "Optional byte size (advisory; re-validated server-side)."
4073
+ }
4074
+ },
4075
+ required: ["cardId", "storagePath", "fileName"]
4076
+ }
4077
+ },
3919
4078
  harmony_classify_card: {
3920
4079
  description: "Classify a card with the LLM classifier: sets `intent` (plan/think/implement/review), `complexity_score` (0-10), and `model_tier` (simple/advanced/research), stamps `classified_at`, and applies the canonical type label (feature/bug/idea). Use this right after creating a card (e.g. in the `hmy-new` flow) so it's classified in-flow instead of waiting for it to surface on the web board. Idempotent — safe to re-run. Never touches the user-owned `model_override`.",
3921
4080
  inputSchema: {
@@ -3927,7 +4086,7 @@ var TOOLS = {
3927
4086
  }
3928
4087
  },
3929
4088
  harmony_upload_artifact: {
3930
- description: "Host a self-contained HTML document (e.g. a visual design draft or diagram) in Harmony and link it to a card, a plan, or a workspace. The file is stored privately and rendered in-app inside a sandboxed cross-origin iframe. Provide exactly one of cardId, planId, or workspaceId. Supply the HTML as `filePath` (a local path the MCP server can read) or `base64Data`. Only text/html, max 2MB. Returns the stored artifact with a short-lived signed URL; call harmony_share_artifact to mint a public link.",
4089
+ description: "Host a self-contained HTML document (e.g. a visual design draft or diagram) in Harmony and link it to a card, a plan, or a workspace. The file is stored privately and rendered in-app inside a sandboxed cross-origin iframe. Provide exactly one of cardId, planId, or workspaceId. Supply the HTML as `filePath` (a local path the MCP server can read — uploaded direct-to-storage, no base64 through the model context) or `base64Data` (the small-file fallback). Only text/html, max 2MB. Returns the stored artifact with a short-lived signed URL; call harmony_share_artifact to mint a public link. For large files on the hosted MCP server (where only base64Data works), prefer the two-step harmony_request_artifact_upload_url + harmony_finalize_artifact handshake, which keeps bytes out of the model context.",
3931
4090
  inputSchema: {
3932
4091
  type: "object",
3933
4092
  properties: {
@@ -3953,6 +4112,61 @@ var TOOLS = {
3953
4112
  required: []
3954
4113
  }
3955
4114
  },
4115
+ harmony_request_artifact_upload_url: {
4116
+ description: "Step 1 of the agent-driven artifact upload — use this for large files or when running against the hosted MCP server (which cannot read your local disk), instead of streaming base64 through the model context. Mints a one-shot signed Supabase Storage upload URL for a server-chosen path. Provide exactly one of cardId/planId/workspaceId, optionally a title and the byte size. Returns { uploadUrl, token, storagePath }. Next: PUT the raw HTML bytes straight to uploadUrl (e.g. `curl -X PUT --data-binary @doc.html '<uploadUrl>'`), then call harmony_finalize_artifact with the returned storagePath. Only text/html, max 2MB.",
4117
+ inputSchema: {
4118
+ type: "object",
4119
+ properties: {
4120
+ title: {
4121
+ type: "string",
4122
+ description: "Display title (defaults to the file basename at finalize)."
4123
+ },
4124
+ cardId: { type: "string", description: "Link to this card (UUID)." },
4125
+ planId: { type: "string", description: "Link to this plan (UUID)." },
4126
+ workspaceId: {
4127
+ type: "string",
4128
+ description: "Attach to this workspace as a standalone artifact (UUID)."
4129
+ },
4130
+ contentType: {
4131
+ type: "string",
4132
+ description: "MIME type; only 'text/html' is accepted (the default)."
4133
+ },
4134
+ size: {
4135
+ type: "number",
4136
+ description: "File size in bytes (rejected early if over 2MB)."
4137
+ }
4138
+ },
4139
+ required: []
4140
+ }
4141
+ },
4142
+ harmony_finalize_artifact: {
4143
+ description: "Step 2 of the agent-driven artifact upload. After PUTting the HTML bytes to the signed uploadUrl from harmony_request_artifact_upload_url, call this with the returned storagePath to validate and register the artifact. The server re-downloads the object and enforces size, the text/html content-type (magic-byte sniff), and — when you pass sha256 — an integrity check, deleting the object and failing on any mismatch. Provide the same one of cardId/planId/workspaceId used for the upload URL. Returns the stored artifact with a short-lived signed URL; call harmony_share_artifact to mint a public link.",
4144
+ inputSchema: {
4145
+ type: "object",
4146
+ properties: {
4147
+ storagePath: {
4148
+ type: "string",
4149
+ description: "The storagePath returned by harmony_request_artifact_upload_url."
4150
+ },
4151
+ title: { type: "string", description: "Display title." },
4152
+ cardId: { type: "string", description: "Link to this card (UUID)." },
4153
+ planId: { type: "string", description: "Link to this plan (UUID)." },
4154
+ workspaceId: {
4155
+ type: "string",
4156
+ description: "Attach to this workspace as a standalone artifact (UUID)."
4157
+ },
4158
+ sha256: {
4159
+ type: "string",
4160
+ description: "Optional hex SHA-256 of the uploaded bytes; verified against the stored object."
4161
+ },
4162
+ size: {
4163
+ type: "number",
4164
+ description: "Optional byte size (advisory; re-validated server-side)."
4165
+ }
4166
+ },
4167
+ required: ["storagePath"]
4168
+ }
4169
+ },
3956
4170
  harmony_share_artifact: {
3957
4171
  description: "Create a public, unauthenticated share link for a hosted artifact. Anyone with the link can view the rendered HTML without a Harmony account. Returns the share token and the full public URL.",
3958
4172
  inputSchema: {
@@ -5196,7 +5410,6 @@ function registerHandlers(server, deps) {
5196
5410
  const { name, arguments: args } = request.params;
5197
5411
  const toolArgs = args || {};
5198
5412
  const cardIdArg = toolArgs.cardId;
5199
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
5200
5413
  if (cardIdArg && UUID_RE.test(cardIdArg) && deps.isConfigured()) {
5201
5414
  const isAutoStartTrigger = AUTO_START_TRIGGERS.has(name);
5202
5415
  const cv = server.getClientVersion?.();
@@ -5550,70 +5763,157 @@ async function handleToolCall(name, args, deps) {
5550
5763
  const cardId = z.string().uuid().parse(args.cardId);
5551
5764
  const filePath = args.filePath != null ? z.string().parse(args.filePath) : undefined;
5552
5765
  const base64Data = args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
5553
- let fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
5766
+ const fileName = args.fileName != null ? z.string().parse(args.fileName) : undefined;
5554
5767
  const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
5555
5768
  if (filePath && base64Data) {
5556
5769
  throw new Error("Provide either filePath or base64Data, not both.");
5557
5770
  }
5558
- let data;
5559
5771
  if (filePath) {
5560
- const bytes = await readFile(filePath);
5561
- if (bytes.byteLength === 0) {
5562
- throw new Error(`File is empty: ${filePath}`);
5563
- }
5564
- data = bytes.toString("base64");
5565
- fileName = fileName || basename(filePath);
5566
- } else if (base64Data) {
5772
+ const bytes = await readFileForUpload(filePath, MAX_ATTACHMENT_SIZE, "attachment");
5773
+ const resolvedName = fileName || basename(filePath);
5774
+ const signed = await client3.requestCardAttachmentUploadUrl(cardId, {
5775
+ fileName: resolvedName,
5776
+ fileType: contentType,
5777
+ size: bytes.byteLength
5778
+ });
5779
+ await putToSignedUrl(signed.uploadUrl, bytes, contentType || signed.fileType || "application/octet-stream");
5780
+ return await client3.finalizeCardAttachment(cardId, {
5781
+ storagePath: signed.storagePath,
5782
+ fileName: resolvedName,
5783
+ fileType: contentType || signed.fileType,
5784
+ sha256: sha256Hex(bytes),
5785
+ size: bytes.byteLength
5786
+ });
5787
+ }
5788
+ if (base64Data) {
5567
5789
  if (!fileName) {
5568
5790
  throw new Error("fileName is required when using base64Data.");
5569
5791
  }
5570
- data = base64Data;
5571
- } else {
5572
- throw new Error("Provide either filePath or base64Data.");
5792
+ if (base64ByteLength(base64Data) > MAX_ATTACHMENT_SIZE) {
5793
+ throw new Error(`File is over the 5MB attachment limit. Use the harmony_request_card_attachment_upload_url + harmony_finalize_card_attachment handshake for large files.`);
5794
+ }
5795
+ return await client3.uploadCardAttachment(cardId, {
5796
+ fileName,
5797
+ data: base64Data,
5798
+ fileType: contentType
5799
+ });
5573
5800
  }
5574
- const result = await client3.uploadCardAttachment(cardId, {
5575
- fileName,
5576
- data,
5577
- fileType: contentType
5578
- });
5579
- return result;
5801
+ throw new Error("Provide either filePath or base64Data.");
5580
5802
  }
5581
5803
  case "harmony_upload_artifact": {
5582
5804
  const title = args.title != null ? z.string().parse(args.title) : undefined;
5583
5805
  const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
5584
5806
  const planId = args.planId != null ? z.string().uuid().parse(args.planId) : undefined;
5585
5807
  const workspaceId = args.workspaceId != null ? z.string().uuid().parse(args.workspaceId) : undefined;
5586
- const anchors = [cardId, planId, workspaceId].filter(Boolean);
5587
- if (anchors.length !== 1) {
5588
- throw new Error("Provide exactly one of cardId, planId, or workspaceId.");
5589
- }
5808
+ requireExactlyOneScope({ cardId, planId, workspaceId });
5590
5809
  const filePath = args.filePath != null ? z.string().parse(args.filePath) : undefined;
5591
5810
  const base64Data = args.base64Data != null ? z.string().parse(args.base64Data) : undefined;
5592
5811
  if (filePath && base64Data) {
5593
5812
  throw new Error("Provide either filePath or base64Data, not both.");
5594
5813
  }
5595
- let data;
5596
- let inferredTitle = title;
5597
5814
  if (filePath) {
5598
- const bytes = await readFile(filePath);
5599
- if (bytes.byteLength === 0) {
5600
- throw new Error(`File is empty: ${filePath}`);
5815
+ const bytes = await readFileForUpload(filePath, MAX_ARTIFACT_SIZE, "artifact");
5816
+ const resolvedTitle = title || basename(filePath);
5817
+ const signed = await client3.requestArtifactUploadUrl({
5818
+ title: resolvedTitle,
5819
+ cardId,
5820
+ planId,
5821
+ workspaceId,
5822
+ contentType: "text/html",
5823
+ size: bytes.byteLength
5824
+ });
5825
+ await putToSignedUrl(signed.uploadUrl, bytes, "text/html");
5826
+ return await client3.finalizeArtifact({
5827
+ storagePath: signed.storagePath,
5828
+ sha256: sha256Hex(bytes),
5829
+ size: bytes.byteLength,
5830
+ title: resolvedTitle,
5831
+ cardId,
5832
+ planId,
5833
+ workspaceId
5834
+ });
5835
+ }
5836
+ if (base64Data) {
5837
+ if (base64ByteLength(base64Data) > MAX_ARTIFACT_SIZE) {
5838
+ throw new Error(`Artifact is over the 2MB limit. Use the harmony_request_artifact_upload_url + harmony_finalize_artifact handshake for large files.`);
5601
5839
  }
5602
- data = bytes.toString("base64");
5603
- inferredTitle = inferredTitle || basename(filePath);
5604
- } else if (base64Data) {
5605
- data = base64Data;
5606
- } else {
5607
- throw new Error("Provide either filePath or base64Data.");
5840
+ return await client3.uploadArtifact({
5841
+ title,
5842
+ cardId,
5843
+ planId,
5844
+ workspaceId,
5845
+ data: base64Data
5846
+ });
5608
5847
  }
5609
- const result = await client3.uploadArtifact({
5610
- title: inferredTitle,
5848
+ throw new Error("Provide either filePath or base64Data.");
5849
+ }
5850
+ case "harmony_request_artifact_upload_url": {
5851
+ const title = args.title != null ? z.string().parse(args.title) : undefined;
5852
+ const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
5853
+ const planId = args.planId != null ? z.string().uuid().parse(args.planId) : undefined;
5854
+ const workspaceId = args.workspaceId != null ? z.string().uuid().parse(args.workspaceId) : undefined;
5855
+ requireExactlyOneScope({ cardId, planId, workspaceId });
5856
+ const contentType = args.contentType != null ? z.string().parse(args.contentType) : undefined;
5857
+ const size = args.size != null ? z.number().positive().parse(args.size) : undefined;
5858
+ if (size != null && size > MAX_ARTIFACT_SIZE) {
5859
+ throw new Error(`Declared size ${size} bytes is over the ${MAX_ARTIFACT_SIZE}-byte (2MB) artifact limit.`);
5860
+ }
5861
+ return await client3.requestArtifactUploadUrl({
5862
+ title,
5611
5863
  cardId,
5612
5864
  planId,
5613
5865
  workspaceId,
5614
- data
5866
+ contentType,
5867
+ size
5868
+ });
5869
+ }
5870
+ case "harmony_finalize_artifact": {
5871
+ const storagePath = z.string().parse(args.storagePath);
5872
+ const title = args.title != null ? z.string().parse(args.title) : undefined;
5873
+ const cardId = args.cardId != null ? z.string().uuid().parse(args.cardId) : undefined;
5874
+ const planId = args.planId != null ? z.string().uuid().parse(args.planId) : undefined;
5875
+ const workspaceId = args.workspaceId != null ? z.string().uuid().parse(args.workspaceId) : undefined;
5876
+ requireExactlyOneScope({ cardId, planId, workspaceId });
5877
+ const sha256 = args.sha256 != null ? z.string().parse(args.sha256) : undefined;
5878
+ const size = args.size != null ? z.number().positive().parse(args.size) : undefined;
5879
+ return await client3.finalizeArtifact({
5880
+ storagePath,
5881
+ sha256,
5882
+ size,
5883
+ title,
5884
+ cardId,
5885
+ planId,
5886
+ workspaceId
5887
+ });
5888
+ }
5889
+ case "harmony_request_card_attachment_upload_url": {
5890
+ const cardId = z.string().uuid().parse(args.cardId);
5891
+ const fileName = z.string().parse(args.fileName);
5892
+ const fileType = args.fileType != null ? z.string().parse(args.fileType) : undefined;
5893
+ const size = z.number().positive().parse(args.size);
5894
+ if (size > MAX_ATTACHMENT_SIZE) {
5895
+ throw new Error(`Declared size ${size} bytes is over the ${MAX_ATTACHMENT_SIZE}-byte (5MB) attachment limit.`);
5896
+ }
5897
+ return await client3.requestCardAttachmentUploadUrl(cardId, {
5898
+ fileName,
5899
+ fileType,
5900
+ size
5901
+ });
5902
+ }
5903
+ case "harmony_finalize_card_attachment": {
5904
+ const cardId = z.string().uuid().parse(args.cardId);
5905
+ const storagePath = z.string().parse(args.storagePath);
5906
+ const fileName = z.string().parse(args.fileName);
5907
+ const fileType = args.fileType != null ? z.string().parse(args.fileType) : undefined;
5908
+ const sha256 = args.sha256 != null ? z.string().parse(args.sha256) : undefined;
5909
+ const size = args.size != null ? z.number().positive().parse(args.size) : undefined;
5910
+ return await client3.finalizeCardAttachment(cardId, {
5911
+ storagePath,
5912
+ fileName,
5913
+ fileType,
5914
+ sha256,
5915
+ size
5615
5916
  });
5616
- return result;
5617
5917
  }
5618
5918
  case "harmony_share_artifact": {
5619
5919
  const artifactId = z.string().uuid().parse(args.artifactId);
@@ -5775,9 +6075,11 @@ async function handleToolCall(name, args, deps) {
5775
6075
  return { success: true, ...result };
5776
6076
  }
5777
6077
  case "harmony_start_agent_session": {
5778
- const cardId = z.string().uuid().parse(args.cardId);
5779
- const agentIdentifier = z.string().min(1).max(100).parse(args.agentIdentifier);
5780
- const agentName = z.string().min(1).max(100).parse(args.agentName);
6078
+ const cardId = requireUuidArg(args.cardId, "cardId");
6079
+ const agentIdentifier = requireStringArg(args.agentIdentifier, "agentIdentifier", { max: 100 });
6080
+ const agentName = requireStringArg(args.agentName, "agentName", {
6081
+ max: 100
6082
+ });
5781
6083
  const moveToColumn = args.moveToColumn;
5782
6084
  const addLabels = parseLabelList(args.addLabels);
5783
6085
  let movedTo = null;
@@ -5838,7 +6140,7 @@ async function handleToolCall(name, args, deps) {
5838
6140
  agentName,
5839
6141
  status: "working",
5840
6142
  currentTask: args.currentTask,
5841
- estimatedMinutesRemaining: args.estimatedMinutesRemaining,
6143
+ estimatedMinutesRemaining: optionalNonNegativeNumberArg(args.estimatedMinutesRemaining, "estimatedMinutesRemaining"),
5842
6144
  steerable: args.steerable === true || args.steerable === "true" ? true : undefined
5843
6145
  });
5844
6146
  markExplicit(cardId, {
@@ -5858,10 +6160,12 @@ async function handleToolCall(name, args, deps) {
5858
6160
  };
5859
6161
  }
5860
6162
  case "harmony_update_agent_progress": {
5861
- const cardId = z.string().uuid().parse(args.cardId);
5862
- const agentIdentifier = z.string().min(1).max(100).parse(args.agentIdentifier);
5863
- const agentName = z.string().min(1).max(100).parse(args.agentName);
5864
- const progressPercent = args.progressPercent !== undefined ? z.number().min(0).max(100).parse(args.progressPercent) : undefined;
6163
+ const cardId = requireUuidArg(args.cardId, "cardId");
6164
+ const agentIdentifier = requireStringArg(args.agentIdentifier, "agentIdentifier", { max: 100 });
6165
+ const agentName = requireStringArg(args.agentName, "agentName", {
6166
+ max: 100
6167
+ });
6168
+ const progressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
5865
6169
  const callerActions = args.actions;
5866
6170
  const now = new Date().toISOString();
5867
6171
  const callerRecentActions = [
@@ -5886,17 +6190,17 @@ async function handleToolCall(name, args, deps) {
5886
6190
  progressPercent,
5887
6191
  currentTask: args.currentTask,
5888
6192
  blockers: args.blockers,
5889
- estimatedMinutesRemaining: args.estimatedMinutesRemaining,
6193
+ estimatedMinutesRemaining: optionalNonNegativeNumberArg(args.estimatedMinutesRemaining, "estimatedMinutesRemaining"),
5890
6194
  ...mergedRecentActions && { recentActions: mergedRecentActions },
5891
6195
  ...runActivity.length > 0 && { runActivity }
5892
6196
  });
5893
6197
  return { success: true, midSessionLearnings: 0, ...result };
5894
6198
  }
5895
6199
  case "harmony_end_agent_session": {
5896
- const cardId = z.string().uuid().parse(args.cardId);
6200
+ const cardId = requireUuidArg(args.cardId, "cardId");
5897
6201
  const moveToColumn = args.moveToColumn;
5898
6202
  const sessionStatus = args.status || "completed";
5899
- const endProgressPercent = args.progressPercent !== undefined ? z.number().min(0).max(100).parse(args.progressPercent) : undefined;
6203
+ const endProgressPercent = optionalPercentArg(args.progressPercent, "progressPercent");
5900
6204
  await flushMemoryActions(client3, cardId);
5901
6205
  cleanupMemorySession(cardId);
5902
6206
  let result = { session: null };
@@ -944,6 +944,9 @@ var TIMINGS = {
944
944
  QUERY_STALE_TIME: 1000 * 60 * 5,
945
945
  QUERY_GC_TIME: 1000 * 60 * 60 * 24
946
946
  };
947
+ // ../harmony-shared/dist/stageHandoff.js
948
+ var HANDOFF_MARKER = "harmony:stage-handoff";
949
+ var HANDOFF_BLOCK_RE = new RegExp("```json\\s*\\n//\\s*" + HANDOFF_MARKER + "\\s*\\n([\\s\\S]*?)\\n```", "m");
947
950
  // src/api-client.ts
948
951
  init_config();
949
952
  var RETRY_CONFIG = {
@@ -1303,12 +1306,24 @@ class HarmonyApiClient {
1303
1306
  async uploadCardAttachment(cardId, data) {
1304
1307
  return this.request("POST", `/cards/${cardId}/attachments`, data);
1305
1308
  }
1309
+ async requestCardAttachmentUploadUrl(cardId, data) {
1310
+ return this.request("POST", `/cards/${cardId}/attachment-upload-url`, data);
1311
+ }
1312
+ async finalizeCardAttachment(cardId, data) {
1313
+ return this.request("POST", `/cards/${cardId}/attachments/finalize`, data);
1314
+ }
1306
1315
  async getCardExternalLinks(cardId) {
1307
1316
  return this.request("GET", `/cards/${cardId}/external-links`);
1308
1317
  }
1309
1318
  async uploadArtifact(data) {
1310
1319
  return this.request("POST", "/artifacts", data);
1311
1320
  }
1321
+ async requestArtifactUploadUrl(data) {
1322
+ return this.request("POST", "/artifacts/upload-url", data);
1323
+ }
1324
+ async finalizeArtifact(data) {
1325
+ return this.request("POST", "/artifacts/finalize", data);
1326
+ }
1312
1327
  async createArtifactShareLink(artifactId, expiresAt) {
1313
1328
  return this.request("POST", `/artifacts/${artifactId}/share`, {
1314
1329
  expires_at: expiresAt
@@ -1786,6 +1801,12 @@ ${planContent.trim()}`;
1786
1801
  async getPlaybook(playbookId) {
1787
1802
  return this.request("GET", `/playbooks/${playbookId}`);
1788
1803
  }
1804
+ async getPlaybookVersion(playbookId, version) {
1805
+ return this.request("GET", `/playbooks/${encodeURIComponent(playbookId)}/versions/${version}`);
1806
+ }
1807
+ async recordStageGateEvidence(insert) {
1808
+ return this.request("POST", `/cards/${encodeURIComponent(insert.card_id)}/stage-gate-evidence`, insert);
1809
+ }
1789
1810
  async createPlaybook(data) {
1790
1811
  return this.request("POST", "/playbooks", data);
1791
1812
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gethmy/mcp",
3
- "version": "2.12.0",
3
+ "version": "2.13.1",
4
4
  "description": "MCP server for Harmony Kanban board - enables AI coding agents to manage your boards",
5
5
  "publishConfig": {
6
6
  "access": "public"