goatedit-mcp 1.1.20 → 1.1.21

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.
@@ -20043,6 +20043,16 @@ var BASE_TOOLS = [
20043
20043
  },
20044
20044
  method: "getEditLog"
20045
20045
  },
20046
+ {
20047
+ name: "get_operation",
20048
+ description: 'Where an edit stands after its call timed out or the connection dropped: queued, running, done (with its reply) or failed, by the opId the error gave. "unknown" means the tab never got it, so sending it again is safe. To retry, repeat the call with that same opId: the tab runs an opId once and answers a repeat with the first reply, so a retry never makes a clip twice.',
20049
+ inputSchema: {
20050
+ type: "object",
20051
+ properties: { opId: { type: "string", description: "The id from the timeout or disconnect message." } },
20052
+ required: ["opId"]
20053
+ },
20054
+ method: "getOperation"
20055
+ },
20046
20056
  {
20047
20057
  name: "clean_unused_sequences",
20048
20058
  description: "Find nested sequences that no clip uses any more \u2014 left behind when a nest clip is removed or a shot is rebuilt \u2014 and optionally delete them with their bin items. Without apply it only lists them. A scene (a sequence once placed with add_sequence_clip) counts as a nest. Other top-level sequences, and nests reachable from one or open in a tab, are never touched; top-level ones nothing plays are listed under otherUnplayed for delete_sequence.",
@@ -22035,6 +22045,7 @@ var TOOLS_BY_NAME = new Map([
22035
22045
  var READ_ONLY_TOOLS = /* @__PURE__ */ new Set([
22036
22046
  "get_session_status",
22037
22047
  "get_design_guide",
22048
+ "get_operation",
22038
22049
  "get_project_state",
22039
22050
  "get_transcript",
22040
22051
  "render_preview_frame",
@@ -22423,6 +22434,10 @@ function withRunSequence(stepArgs, sequenceId, tool) {
22423
22434
  if (!tool || !("expectedSequenceId" in (tool.inputSchema.properties ?? {}))) return stepArgs;
22424
22435
  return { ...stepArgs, expectedSequenceId: sequenceId };
22425
22436
  }
22437
+ function withRunOp(stepArgs, runOpId, index2) {
22438
+ if (!runOpId || typeof stepArgs.opId === "string") return stepArgs;
22439
+ return { ...stepArgs, opId: `${runOpId}.${index2 + 1}` };
22440
+ }
22426
22441
 
22427
22442
  // src/tab-registry.ts
22428
22443
  function indexTabs(reports, isOpen) {
@@ -25250,13 +25265,16 @@ function resolveTab(projectId) {
25250
25265
  if (!id) throw new Error(notConnectedMessage());
25251
25266
  return tabs.get(id);
25252
25267
  }
25268
+ function unsettled(message) {
25269
+ return Object.assign(new Error(message), { unsettled: true });
25270
+ }
25253
25271
  var pendingRequests = /* @__PURE__ */ new Map();
25254
25272
  function rejectPendingOn(socket, why) {
25255
25273
  for (const [id, pending] of pendingRequests) {
25256
25274
  if (pending.socket !== socket) continue;
25257
25275
  clearTimeout(pending.timeoutId);
25258
25276
  pendingRequests.delete(id);
25259
- pending.reject(new Error(`${why} before ${pending.method} answered. Check the editor tab, then call get_timeline to see whether the edit landed.`));
25277
+ pending.reject(unsettled(`${why} before ${pending.method} answered. Check the editor tab.`));
25260
25278
  }
25261
25279
  }
25262
25280
  function explainTabError(error2) {
@@ -25397,7 +25415,7 @@ function failUpstream(why) {
25397
25415
  for (const [id, p] of upstreamPending) {
25398
25416
  clearTimeout(p.timeoutId);
25399
25417
  upstreamPending.delete(id);
25400
- p.reject(new Error(`${why} before ${p.method} answered.`));
25418
+ p.reject(unsettled(`${why} before ${p.method} answered.`));
25401
25419
  }
25402
25420
  }
25403
25421
  function connectUpstream() {
@@ -25455,7 +25473,7 @@ function sendUpstream(method, params, projectId) {
25455
25473
  const id = crypto2.randomUUID();
25456
25474
  const timeoutId = setTimeout(() => {
25457
25475
  upstreamPending.delete(id);
25458
- reject(new Error(`Request timed out: ${method}`));
25476
+ reject(unsettled(`Request timed out: ${method}`));
25459
25477
  }, REQUEST_TIMEOUT_MS);
25460
25478
  upstreamPending.set(id, { resolve, reject, timeoutId, method });
25461
25479
  ws.send(JSON.stringify({ id, method, params, ...projectId ? { projectId } : {} }));
@@ -25469,7 +25487,7 @@ async function sendToBrowser(method, params = {}, projectId) {
25469
25487
  const id = crypto2.randomUUID();
25470
25488
  const timeoutId = setTimeout(() => {
25471
25489
  pendingRequests.delete(id);
25472
- reject(new Error(`Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s: ${method}. Call get_timeline to see whether it landed.`));
25490
+ reject(unsettled(`Request timed out after ${REQUEST_TIMEOUT_MS / 1e3}s: ${method}.`));
25473
25491
  }, REQUEST_TIMEOUT_MS);
25474
25492
  pendingRequests.set(id, { resolve, reject, timeoutId, socket: tab.ws, method });
25475
25493
  tab.ws.send(JSON.stringify({ id, method, params, projectId: tab.projectId }));
@@ -25576,7 +25594,8 @@ var LOCAL_TOOLS = [
25576
25594
  continueOnError: { type: "boolean", description: "Run the remaining steps after one fails." },
25577
25595
  rollback: { type: "boolean", description: "Take a checkpoint first and restore it if any step fails, so a run either lands whole or not at all. Cannot be combined with continueOnError." },
25578
25596
  projectId: { type: "string", description: "Run every step on this project (from list_connected_projects). Use it when several agents edit different tabs at once; a step's own projectId wins." },
25579
- expectedSequenceId: { type: "string", description: "Run every step that takes expectedSequenceId on this sequence (from list_sequences), open or not. Use it when several agents edit different sequences at once; a step's own expectedSequenceId wins." }
25597
+ expectedSequenceId: { type: "string", description: "Run every step that takes expectedSequenceId on this sequence (from list_sequences), open or not. Use it when several agents edit different sequences at once; a step's own expectedSequenceId wins." },
25598
+ opId: { type: "string", description: "Only to retry a run that failed or timed out: the opId its reply or error gave, with the same calls in the same order. Steps that already finished are answered without running again." }
25580
25599
  },
25581
25600
  required: ["calls"]
25582
25601
  }
@@ -25590,6 +25609,7 @@ for (const [alias, name] of Object.entries(TOOL_ALIASES)) {
25590
25609
  if (tool) BROWSER_TOOLS_BY_NAME.set(alias, tool);
25591
25610
  }
25592
25611
  var PROJECT_FREE_TOOLS = /* @__PURE__ */ new Set(["create_project", "begin_request", "record_choice", "set_style_brand"]);
25612
+ var VIEW_TOOLS = /* @__PURE__ */ new Set(["switch_project", "open_sequence", "playback_control", "set_playhead", "set_tool"]);
25593
25613
  var SAVES_FRAMES = new Set(
25594
25614
  BROWSER_TOOLS.filter((t) => "saveToDisk" in (t.inputSchema.properties ?? {})).map((t) => t.name)
25595
25615
  );
@@ -25601,7 +25621,7 @@ server.setRequestHandler(ListToolsRequestSchema, async () => ({
25601
25621
  ]
25602
25622
  }));
25603
25623
  async function callTool(name, args) {
25604
- const { projectId: rawProjectId, ...forwarded } = args;
25624
+ const { projectId: rawProjectId, opId: rawOpId, ...forwarded } = args;
25605
25625
  const projectId = typeof rawProjectId === "string" && rawProjectId ? rawProjectId : void 0;
25606
25626
  switch (name) {
25607
25627
  case "get_design_guide":
@@ -25631,6 +25651,7 @@ async function callTool(name, args) {
25631
25651
  }
25632
25652
  const saveToDisk = SAVES_FRAMES.has(name) && forwarded.saveToDisk !== false;
25633
25653
  if (SAVES_FRAMES.has(name)) forwarded.saveToDisk = false;
25654
+ if (name === "get_operation") forwarded.opId = rawOpId;
25634
25655
  const invalid = validateArgs(tool, forwarded);
25635
25656
  if (invalid) throw new McpError(ErrorCode.InvalidParams, invalid);
25636
25657
  const target = projectId ?? chosenProjectId;
@@ -25643,11 +25664,25 @@ async function callTool(name, args) {
25643
25664
  );
25644
25665
  }
25645
25666
  }
25646
- if (name === "import_local_file") return importLocalFile(forwarded, target);
25647
- const result = await sendToBrowser(tool.method, forwarded, target);
25667
+ if (!READ_ONLY_TOOLS.has(tool.name) && !VIEW_TOOLS.has(tool.name)) {
25668
+ forwarded.opId = typeof rawOpId === "string" && rawOpId ? rawOpId : `op-${crypto2.randomUUID().slice(0, 8)}`;
25669
+ }
25670
+ if (name === "import_local_file") return withOpIdHint(importLocalFile(forwarded, target), forwarded.opId);
25671
+ const result = await withOpIdHint(sendToBrowser(tool.method, forwarded, target), forwarded.opId);
25648
25672
  const withFile = saveFileToDisk(result);
25649
25673
  return saveToDisk ? saveFramesToDisk(withFile, name) : withFile;
25650
25674
  }
25675
+ async function withOpIdHint(call, opId) {
25676
+ try {
25677
+ return await call;
25678
+ } catch (err) {
25679
+ const message = err?.message ?? String(err);
25680
+ if (typeof opId !== "string" || !err?.unsettled) throw err;
25681
+ throw new Error(
25682
+ `${message} The edit may still finish. It is operation ${opId}: call get_operation with opId "${opId}" to see whether it did, or repeat this exact call with opId "${opId}" \u2014 the tab runs an opId once, so the retry cannot make anything twice.`
25683
+ );
25684
+ }
25685
+ }
25651
25686
  async function importLocalFile(args, target) {
25652
25687
  const local = await callLocal("find_local_file", { filename: args.filename }, target).catch((err) => {
25653
25688
  if (/^Several files/.test(err?.message ?? "")) throw err;
@@ -25676,6 +25711,7 @@ async function runTools(args) {
25676
25711
  const images = [];
25677
25712
  const refs = /* @__PURE__ */ new Map();
25678
25713
  let failed = 0;
25714
+ const runOpId = args.rollback ? null : typeof args.opId === "string" && args.opId ? args.opId : `op-${crypto2.randomUUID().slice(0, 8)}`;
25679
25715
  let checkpointId;
25680
25716
  if (args.rollback) {
25681
25717
  const made = await callTool("create_checkpoint", withRunProject({ name: "run_tools rollback" }, args.projectId));
@@ -25686,7 +25722,7 @@ async function runTools(args) {
25686
25722
  const tool = typeof step?.tool === "string" ? step.tool : "";
25687
25723
  try {
25688
25724
  const stepArgs = resolveRefs(step.args && typeof step.args === "object" ? step.args : {}, refs);
25689
- const raw = await callTool(tool, withRunSequence(withRunProject(stepArgs, args.projectId), args.expectedSequenceId, BROWSER_TOOLS_BY_NAME.get(tool)));
25725
+ const raw = await callTool(tool, withRunOp(withRunSequence(withRunProject(stepArgs, args.projectId), args.expectedSequenceId, BROWSER_TOOLS_BY_NAME.get(tool)), runOpId, i));
25690
25726
  if (typeof step.as === "string" && step.as) {
25691
25727
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
25692
25728
  const { _image, _images, ...kept } = raw;
@@ -25725,6 +25761,7 @@ async function runTools(args) {
25725
25761
  return {
25726
25762
  ok: failed === 0,
25727
25763
  results,
25764
+ ...failed && runOpId ? { opId: runOpId, retry: `Repeat this run_tools call with opId "${runOpId}": finished steps answer from the tab without running again.` } : {},
25728
25765
  ...rolledBack !== void 0 ? { rolledBack } : {},
25729
25766
  ...images.length ? { _images: images } : {}
25730
25767
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goatedit-mcp",
3
- "version": "1.1.20",
3
+ "version": "1.1.21",
4
4
  "description": "Local MCP server for the GoatEdit video editor: drive your open editor tab from Claude Code or any MCP client",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",