goatedit-mcp 1.1.12 → 1.1.13

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.
@@ -19841,7 +19841,7 @@ import crypto2 from "crypto";
19841
19841
  import http from "http";
19842
19842
  import fs3 from "fs";
19843
19843
  import path3 from "path";
19844
- import { execFileSync } from "child_process";
19844
+ import { execFile, execFileSync } from "child_process";
19845
19845
  import os2 from "os";
19846
19846
  import { fileURLToPath } from "url";
19847
19847
 
@@ -20055,9 +20055,18 @@ var BASE_TOOLS = [
20055
20055
  },
20056
20056
  method: "deleteSequence"
20057
20057
  },
20058
+ {
20059
+ name: "list_projects",
20060
+ description: "The person's saved GoatEdit projects, newest first: id, name, when last changed, and whether a connected tab has it open. Use an id with open_project to work on a project that is not open yet. Read-only.",
20061
+ inputSchema: {
20062
+ type: "object",
20063
+ properties: { limit: { type: "number", description: "How many to list (default 30, at most 100)." } }
20064
+ },
20065
+ method: "listProjects"
20066
+ },
20058
20067
  {
20059
20068
  name: "list_sequences",
20060
- description: "Every sequence in the project: id, name, whether it is the one open for editing, open in a tab, a nest (made by nest_clips) or top-level, its length, how many clips it holds, and the clips that play it. Every other tool edits the open sequence only \u2014 use this to find the id to pass to open_sequence.",
20069
+ description: "Every sequence in the project: id, name, whether it is the one open for editing, open in a tab, a nest (made by nest_clips) or top-level, its length, how many clips it holds, and the clips that play it. Tools act on the open sequence unless they are given expectedSequenceId \u2014 pass one of these ids to read or edit another sequence without opening it (open_sequence moves the person's view; prefer expectedSequenceId).",
20061
20070
  inputSchema: { type: "object", properties: {} },
20062
20071
  method: "listSequences"
20063
20072
  },
@@ -21059,7 +21068,7 @@ var BASE_TOOLS = [
21059
21068
  clipId: CLIP_ID,
21060
21069
  type: {
21061
21070
  type: "string",
21062
- description: "Effect type: blur, invert, hue, saturation, brightness, contrast, exposure, grayscale, sepia, gradientMap, vignette, filmGrain, flicker, posterizeTime, lensDistortion, chromaticAberration, lensFlare, liquidGlass, opacityFx, colorCorrection, depthMap, gradientMap, glow, pixelDissolve, motionBlur, dropShadow, reflection, colorama, outline, echo, flutedGlass, lightRays, halftone, datamosh, roughenEdges, filmStock, halation, starGlow, filmDamage, lightLeak, thermal, colorBalance, duotone, levels, monochrome, channelMixer, dither, threshold, lensBlur, directionalBlur, spinBlur, zoomBlur, sharpen, cameraShake, bulge, twirl, ripple, waveWarp, polarCoords, turbulentDisplace, elasticGrid, gridSplit, cubify, glitch, vhs, ntsc, crtScreen, ledScreen, rgbSplit, jpegGlitch, ascii, macpaint, modulation, stripe, risograph, emboss, bevel, inkBleed, paperTexture, paperCutout, crossHatch, scatter, blobTracker, edgeGlow, pageTurn, lightSweep, deepGlow, twister, fill, grid, repeater, plus transform, layer3d, model3d and showcase. Call describe_effect with a type for its parameters, their ranges and what they do before setting anything but the obvious. An unknown name comes back with the full list."
21071
+ description: "Effect type: blur, invert, hue, saturation, brightness, contrast, exposure, grayscale, sepia, gradientMap, vignette, filmGrain, flicker, posterizeTime, lensDistortion, chromaticAberration, lensFlare, liquidGlass, opacityFx, colorCorrection, depthMap, gradientMap, vegas, glow, pixelDissolve, motionBlur, dropShadow, reflection, colorama, outline, echo, flutedGlass, lightRays, halftone, datamosh, roughenEdges, filmStock, halation, starGlow, filmDamage, lightLeak, thermal, colorBalance, duotone, levels, monochrome, channelMixer, dither, threshold, fourColorGradient, lensBlur, boxBlur, directionalBlur, spinBlur, zoomBlur, sharpen, cameraShake, bulge, twirl, ripple, waveWarp, polarCoords, turbulentDisplace, elasticGrid, gridSplit, cubify, glitch, vhs, ntsc, crtScreen, ledScreen, rgbSplit, jpegGlitch, ascii, macpaint, modulation, stripe, risograph, emboss, bevel, inkBleed, paperTexture, paperCutout, crossHatch, scatter, blobTracker, edgeGlow, pageTurn, lightSweep, deepGlow, twister, fill, compositeOriginal, grid, repeater, plus transform, layer3d, model3d and showcase. Call describe_effect with a type for its parameters, their ranges and what they do before setting anything but the obvious. An unknown name comes back with the full list."
21063
21072
  },
21064
21073
  params: {
21065
21074
  type: "object",
@@ -21797,8 +21806,18 @@ var SEQUENCE_EDITS = /* @__PURE__ */ new Set([
21797
21806
  "setAudioMaster",
21798
21807
  "loopClip"
21799
21808
  ]);
21809
+ var SEQUENCE_READS = /* @__PURE__ */ new Set([
21810
+ "getTimeline",
21811
+ "getClips",
21812
+ "renderFrame",
21813
+ "lintTimeline",
21814
+ "lintMotion",
21815
+ "describeMotion",
21816
+ "listCuts",
21817
+ "exportRange"
21818
+ ]);
21800
21819
  var EXPECTED_SEQUENCE = str(
21801
- "Optional guard: the sequence id this edit is meant for (from list_sequences). If the tab has a different sequence open, the call is refused instead of editing the wrong one."
21820
+ "Optional: the sequence id (from list_sequences) this call reads or edits. It does not need to be open: the edit lands in that sequence and the tab stays on whatever sequence the person is looking at, so several agents can each work on their own sequence at once. Without it, the call acts on the open sequence."
21802
21821
  );
21803
21822
  var PLACING_EDITS = /* @__PURE__ */ new Set([
21804
21823
  "addClip",
@@ -21825,7 +21844,7 @@ var NEW_TRACK_ABOVE = str(
21825
21844
  );
21826
21845
  var NEW_TRACK_BELOW = str('With trackId "__new_track__": put the new track directly below this video track.');
21827
21846
  var TOOLS = BASE_TOOLS.map((tool) => {
21828
- if (!tool.method || !SEQUENCE_EDITS.has(tool.method)) return tool;
21847
+ if (!tool.method || !(SEQUENCE_EDITS.has(tool.method) || SEQUENCE_READS.has(tool.method))) return tool;
21829
21848
  return {
21830
21849
  ...tool,
21831
21850
  inputSchema: {
@@ -21984,6 +22003,11 @@ function withRunProject(stepArgs, projectId) {
21984
22003
  if (typeof projectId !== "string" || !projectId || typeof stepArgs.projectId === "string") return stepArgs;
21985
22004
  return { ...stepArgs, projectId };
21986
22005
  }
22006
+ function withRunSequence(stepArgs, sequenceId, tool) {
22007
+ if (typeof sequenceId !== "string" || !sequenceId || typeof stepArgs.expectedSequenceId === "string") return stepArgs;
22008
+ if (!tool || !("expectedSequenceId" in (tool.inputSchema.properties ?? {}))) return stepArgs;
22009
+ return { ...stepArgs, expectedSequenceId: sequenceId };
22010
+ }
21987
22011
 
21988
22012
  // src/tab-registry.ts
21989
22013
  function indexTabs(reports, isOpen) {
@@ -21994,6 +22018,23 @@ function indexTabs(reports, isOpen) {
21994
22018
  return index;
21995
22019
  }
21996
22020
 
22021
+ // src/open-project.ts
22022
+ var DEFAULT_EDITOR = "https://ai.goatedit.com";
22023
+ function editorUrl(origin, projectId) {
22024
+ const url2 = new URL(origin || DEFAULT_EDITOR);
22025
+ url2.pathname = "/";
22026
+ url2.search = "";
22027
+ url2.hash = "";
22028
+ url2.searchParams.set("project", projectId);
22029
+ url2.searchParams.set("via", "agent");
22030
+ return url2.toString();
22031
+ }
22032
+ function openCommand(platform, url2) {
22033
+ if (platform === "darwin") return ["open", [url2]];
22034
+ if (platform === "win32") return ["cmd", ["/c", "start", "", url2]];
22035
+ return ["xdg-open", [url2]];
22036
+ }
22037
+
21997
22038
  // node_modules/@modelcontextprotocol/sdk/dist/esm/experimental/tasks/client.js
21998
22039
  var ExperimentalClientTasks = class {
21999
22040
  constructor(_client) {
@@ -24493,7 +24534,7 @@ var VERSION = (() => {
24493
24534
  }
24494
24535
  return "0.0.0";
24495
24536
  })();
24496
- var INSTRUCTIONS = `Edits a GoatEdit video project live in the user's browser tab at ai.goatedit.com. At the start of each new request from the user, call begin_request with the kind of edit, its style words and any reference videos, then get_style_profile for how this user edits; treat that style as defaults, the request wins. Call get_timeline before your first edit \u2014 clip ids change after every split, trim or delete, and a stale id is rejected. Each edit's reply carries timelineChanges (clips added, with their ids and placement; clips changed; ids removed), so take the next ids from there instead of reading the timeline again. get_timeline is lean by default; pass detail: "full" only when you need effect parameters, and trackId or from/to to read part of it. Several edits in a row go faster as one run_tools call. With more than one tab open, call list_connected_projects, then use_project to pick one. To work on several projects at once (one helper agent per video), do not use use_project \u2014 it moves every caller \u2014 but pass projectId on each call, or once on run_tools for all its steps. render_preview_frame and preview_media accept saveToDisk: true, which writes the frame to a file and returns its path instead of the image. If a tool reports the editor is not connected, ask the user to open ai.goatedit.com and switch on Local in the AI status menu.`;
24537
+ var INSTRUCTIONS = `Edits a GoatEdit video project live in the user's browser tab at ai.goatedit.com. At the start of each new request from the user, call begin_request with the kind of edit, its style words and any reference videos, then get_style_profile for how this user edits; treat that style as defaults, the request wins. Call get_timeline before your first edit \u2014 clip ids change after every split, trim or delete, and a stale id is rejected. Each edit's reply carries timelineChanges (clips added, with their ids and placement; clips changed; ids removed), so take the next ids from there instead of reading the timeline again. get_timeline is lean by default; pass detail: "full" only when you need effect parameters, and trackId or from/to to read part of it. Several edits in a row go faster as one run_tools call. With more than one tab open, call list_connected_projects, then use_project to pick one. To work on a project that is not open, find it with list_projects and call open_project. To work on several projects at once (one helper agent per video), do not use use_project \u2014 it moves every caller \u2014 but pass projectId on each call, or once on run_tools for all its steps. To read or edit a sequence other than the open one, do not call open_sequence \u2014 it moves the user's view \u2014 but pass expectedSequenceId on each call, or once on run_tools; the edit lands in that sequence while the user keeps working wherever they are. render_preview_frame and preview_media accept saveToDisk: true, which writes the frame to a file and returns its path instead of the image. If a tool reports the editor is not connected, ask the user to open ai.goatedit.com and switch on Local in the AI status menu.`;
24497
24538
  var MEDIA_TYPES = {
24498
24539
  ".mp4": { mime: "video/mp4", kind: "video" },
24499
24540
  ".m4v": { mime: "video/mp4", kind: "video" },
@@ -24750,6 +24791,8 @@ wss.on("error", () => {
24750
24791
  });
24751
24792
  var tabs = /* @__PURE__ */ new Map();
24752
24793
  var tabReports = /* @__PURE__ */ new Map();
24794
+ var tabOrigins = /* @__PURE__ */ new WeakMap();
24795
+ var lastEditorOrigin = process.env.GOATEDIT_EDITOR_URL || null;
24753
24796
  function reindexTabs() {
24754
24797
  tabs.clear();
24755
24798
  for (const [pid, r] of indexTabs(tabReports.values(), (s) => s.readyState === import_websocket.default.OPEN)) {
@@ -24858,6 +24901,10 @@ wss.on("connection", (ws, req) => {
24858
24901
  return;
24859
24902
  }
24860
24903
  console.error("[GoatEdit MCP] Browser editor connected via WebSocket");
24904
+ if (req.headers.origin) {
24905
+ tabOrigins.set(ws, req.headers.origin);
24906
+ lastEditorOrigin = req.headers.origin;
24907
+ }
24861
24908
  ws.on("message", (data) => {
24862
24909
  let message;
24863
24910
  try {
@@ -25017,8 +25064,31 @@ var LOCAL_HANDLERS = {
25017
25064
  list_connected_projects: async () => connectedProjects(),
25018
25065
  list_local_assets: async (args) => handleListLocalAssets(args),
25019
25066
  generate_voiceover: async (args, projectId) => handleGenerateVoiceover(args.text, projectId),
25020
- find_local_file: async (args) => handleFindLocalFile(args)
25067
+ find_local_file: async (args) => handleFindLocalFile(args),
25068
+ open_project: async (args) => handleOpenProject(args)
25021
25069
  };
25070
+ var OPEN_WAIT_MS = 45e3;
25071
+ async function handleOpenProject(args) {
25072
+ const projectId = typeof args?.projectId === "string" ? args.projectId.trim() : "";
25073
+ if (!projectId) throw new Error("Missing required argument: projectId (from list_projects).");
25074
+ const open = tabs.get(projectId);
25075
+ if (open && open.ws.readyState === import_websocket.default.OPEN) return { projectId, name: open.projectName, alreadyOpen: true };
25076
+ const origin = [...tabReports.keys()].map((ws) => tabOrigins.get(ws)).find(Boolean) ?? lastEditorOrigin;
25077
+ const url2 = editorUrl(origin, projectId);
25078
+ const [cmd, cmdArgs] = openCommand(process.platform, url2);
25079
+ await new Promise((resolve, reject) => {
25080
+ execFile(cmd, cmdArgs, (err) => err ? reject(new Error(`Could not open the browser (${cmd}): ${err.message}. Open ${url2} by hand.`)) : resolve());
25081
+ });
25082
+ const deadline = Date.now() + OPEN_WAIT_MS;
25083
+ while (Date.now() < deadline) {
25084
+ const tab = tabs.get(projectId);
25085
+ if (tab && tab.ws.readyState === import_websocket.default.OPEN) return { projectId, name: tab.projectName, opened: url2 };
25086
+ await new Promise((r) => setTimeout(r, 500));
25087
+ }
25088
+ throw new Error(
25089
+ `Opened ${url2}, but no tab reported project ${projectId} within ${OPEN_WAIT_MS / 1e3}s. The person may need to sign in, or switch on Local in the AI status menu of that tab. Check list_connected_projects again in a moment.`
25090
+ );
25091
+ }
25022
25092
  async function callLocal(method, args, projectId) {
25023
25093
  await ensureTransport();
25024
25094
  if (ownsPort) return LOCAL_HANDLERS[method](args, projectId ?? void 0);
@@ -25034,6 +25104,15 @@ var LOCAL_TOOLS = [
25034
25104
  description: "List the GoatEdit projects open in connected browser tabs, with which one tools act on by default.",
25035
25105
  inputSchema: { type: "object", properties: {} }
25036
25106
  },
25107
+ {
25108
+ name: "open_project",
25109
+ description: "Open a saved project (id from list_projects) in a new browser tab and wait until it connects, so you can work on it \u2014 several projects at once, one tab each, addressed with projectId. A project already open is left as it is. The tab asks the person to allow its footage folder with a popup: tell them to click Allow.",
25110
+ inputSchema: {
25111
+ type: "object",
25112
+ properties: { projectId: { type: "string", description: "The project to open." } },
25113
+ required: ["projectId"]
25114
+ }
25115
+ },
25037
25116
  {
25038
25117
  name: "use_project",
25039
25118
  description: "Pick which connected project every later tool call acts on, when more than one tab is open. Ids come from list_connected_projects.",
@@ -25081,7 +25160,8 @@ var LOCAL_TOOLS = [
25081
25160
  },
25082
25161
  continueOnError: { type: "boolean", description: "Run the remaining steps after one fails." },
25083
25162
  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." },
25084
- 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." }
25163
+ 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." },
25164
+ 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." }
25085
25165
  },
25086
25166
  required: ["calls"]
25087
25167
  }
@@ -25109,6 +25189,7 @@ async function callTool(name, args) {
25109
25189
  case "list_connected_projects":
25110
25190
  case "list_local_assets":
25111
25191
  case "generate_voiceover":
25192
+ case "open_project":
25112
25193
  return callLocal(name, forwarded, projectId);
25113
25194
  case "use_project": {
25114
25195
  const wanted = typeof forwarded.projectId === "string" ? forwarded.projectId : projectId;
@@ -25176,7 +25257,7 @@ async function runTools(args) {
25176
25257
  const tool = typeof step?.tool === "string" ? step.tool : "";
25177
25258
  try {
25178
25259
  const stepArgs = resolveRefs(step.args && typeof step.args === "object" ? step.args : {}, refs);
25179
- const raw = await callTool(tool, withRunProject(stepArgs, args.projectId));
25260
+ const raw = await callTool(tool, withRunSequence(withRunProject(stepArgs, args.projectId), args.expectedSequenceId, BROWSER_TOOLS_BY_NAME.get(tool)));
25180
25261
  if (typeof step.as === "string" && step.as) {
25181
25262
  if (raw && typeof raw === "object" && !Array.isArray(raw)) {
25182
25263
  const { _image, _images, ...kept } = raw;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "goatedit-mcp",
3
- "version": "1.1.12",
3
+ "version": "1.1.13",
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",