premiere-pro-mcp 1.1.0 → 1.1.2

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/CHANGELOG.md CHANGED
@@ -4,6 +4,70 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## [1.1.2] - 2026-07-11
8
+
9
+ The headline of this release is that the CEP 12 bridge fix from
10
+ [#1](https://github.com/leancoderkavy/premiere-pro-mcp/pull/1) finally ships to npm. It has been on
11
+ `main` since March but was never published, so everyone who installed with `npm install -g` still
12
+ got a bridge that returned `null` for every tool call. If that was your symptom, upgrading is the
13
+ whole fix.
14
+
15
+ ### Fixed
16
+
17
+ - **The bridge returns data again on Premiere Pro 2023+ / CEP 12.** The published `CSInterface.js`
18
+ shim called `__adobe_cep__.evalScript(script)` without forwarding the callback. CEP 9+ is
19
+ async-only, so every result was silently discarded and every tool answered
20
+ `{"success":true,"data":null}` while the panel cheerfully logged "Result: OK". The manifest was
21
+ also missing `--enable-nodejs`, leaving `require("fs")` undefined in the panel.
22
+ ([#2](https://github.com/leancoderkavy/premiere-pro-mcp/issues/2),
23
+ [#5](https://github.com/leancoderkavy/premiere-pro-mcp/issues/5),
24
+ [#8](https://github.com/leancoderkavy/premiere-pro-mcp/issues/8))
25
+
26
+ - **Markers landed at wildly wrong times.** `createMarker()` takes seconds, but was being handed
27
+ ticks — a marker requested at 2.0s was placed roughly 508 billion seconds down the timeline,
28
+ far past the end of any real sequence. `marker.end` had the same bug, and `list_markers` read
29
+ back nonsense as a result. ([#6](https://github.com/leancoderkavy/premiere-pro-mcp/issues/6))
30
+
31
+ - **`manage_proxies` and `get_encoder_presets` called ExtendScript methods that do not exist.**
32
+ `ProjectItem` has no `createProxy()` and `EncoderManager` has no `getFormatList()`, so both threw
33
+ every time. `manage_proxies` with `action: "create"` now queues a real proxy encode through Media
34
+ Encoder instead of reporting "Proxy creation started" for work that never happened, and
35
+ `get_encoder_presets` discovers presets by scanning the `.epr` files Adobe ships on disk, returning
36
+ each preset's path so it can be passed straight to `export_sequence`.
37
+ ([#7](https://github.com/leancoderkavy/premiere-pro-mcp/issues/7))
38
+
39
+ - **`capture_frame`, `export_frame`, and `freeze_frame` threw on every call.** `exportFramePNG`
40
+ exists only on the QE DOM sequence, not the public DOM one. These tools now go through the QE
41
+ sequence, and — because QE's return value is unreliable — decide success by checking that a file
42
+ actually exists on disk, falling back to a one-frame Media Encoder export. They can no longer
43
+ report success having written nothing.
44
+ ([#9](https://github.com/leancoderkavy/premiere-pro-mcp/issues/9))
45
+
46
+ - **Six tools repaired for Premiere Pro 2026** via
47
+ [#3](https://github.com/leancoderkavy/premiere-pro-mcp/pull/3): `add_audio_keyframes` (used a
48
+ nonexistent `Property.addKeyframe`, and wrote dB into a property that stores amplitude),
49
+ `color_correct` (one unsettable Lumetri property aborted the whole script and lost every other
50
+ change), `add_transition` and friends (`getVideoTransitionList()` returns empty on 2026 even
51
+ though by-name lookup works), `add_adjustment_layer` (`qeSeq.addAdjustmentLayer` was removed in
52
+ 2026), `export_sequence` (defaulted to a hardcoded macOS-only preset path), and `add_text_overlay`
53
+ (called `createCaptionTrack` with the wrong signature).
54
+
55
+ - `manage_proxies` with `action: "toggle"` reported the inverse of the state it had just set.
56
+
57
+ - The README described this repository as "a temporary fork" of itself — a fork banner that rode in
58
+ with the [#1](https://github.com/leancoderkavy/premiere-pro-mcp/pull/1) merge.
59
+
60
+ ### Notes
61
+
62
+ - The frame-export and proxy-create paths are fixed against the documented API and covered by
63
+ regression tests, but have not yet been live-verified against a running Premiere Pro. If you can
64
+ test them, reports on
65
+ [#7](https://github.com/leancoderkavy/premiere-pro-mcp/issues/7) and
66
+ [#9](https://github.com/leancoderkavy/premiere-pro-mcp/issues/9) are very welcome.
67
+ - Windows users on CEP 12 may additionally need to sign the extension (`ZXPSignCmd -sign`) — see
68
+ [#2](https://github.com/leancoderkavy/premiere-pro-mcp/issues/2) for details. That is an Adobe
69
+ signature-verification requirement, not a bug in this package.
70
+
7
71
  ## [1.0.0] - 2025-02-26
8
72
 
9
73
  ### Added
package/README.md CHANGED
@@ -476,10 +476,22 @@ All generated scripts use **ES3 syntax** (`var`, manual `for` loops, no arrow fu
476
476
 
477
477
  ### Security
478
478
 
479
- - Scripts are validated before execution blocks `eval()`, `new Function()`, `System.callSystem()`
480
- - 500KB script size limit
481
- - `send_raw_script` bypasses validation for advanced users (with explicit opt-in)
482
- - Temp directory created with restricted permissions (mode 700)
479
+ Understand the trust model before deploying this: **any client that can reach the MCP
480
+ server can run ExtendScript inside your Premiere Pro.** `execute_extendscript` and
481
+ `send_raw_script` are arbitrary-code-execution tools by design. Treat access to the server
482
+ the same way you'd treat a shell on the machine running Premiere.
483
+
484
+ - **Run it locally over stdio** unless you have a specific reason not to. That's the safe default.
485
+ - **The HTTP transport (`http-server`) requires `MCP_AUTH_TOKEN`** and refuses to start
486
+ without it. It binds `0.0.0.0` and is remotely reachable, so never expose it publicly
487
+ without a strong token (set `ALLOW_UNAUTHENTICATED=1` only for a throwaway public instance).
488
+ - The bridge temp directory is created private to your user (mode `0700`), and the server
489
+ refuses to use one owned by another user — relevant on shared machines, where the CEP
490
+ panel would otherwise execute any `cmd_*.jsx` staged there.
491
+ - There is a 500 KB script size limit, and a small regex check that rejects `eval()`,
492
+ `new Function()`, and `System.callSystem()` in tool-generated scripts. **This is a guard
493
+ rail, not a sandbox** — it is trivially bypassable and is not a security boundary. Do not
494
+ rely on it to contain untrusted input; the real boundary is who can reach the server.
483
495
 
484
496
  ### QE DOM
485
497
 
@@ -26,15 +26,10 @@ function CSInterface() {}
26
26
  */
27
27
  CSInterface.prototype.evalScript = function (script, callback) {
28
28
  if (typeof __adobe_cep__ !== "undefined") {
29
- var result = __adobe_cep__.evalScript(script);
30
- if (callback) {
31
- // CSInterface v9+ uses async callback
32
- if (typeof result === "undefined" || result === "undefined") {
33
- // v9+ path: callback is registered and called asynchronously
34
- // The __adobe_cep__.evalScript already handles the callback via internal mechanism
35
- }
36
- callback(result);
37
- }
29
+ // CEP 9+ requires the callback to be passed directly to __adobe_cep__.evalScript.
30
+ // Calling it without a callback causes the result to be silently discarded,
31
+ // making every command return null/undefined.
32
+ __adobe_cep__.evalScript(script, callback || function () {});
38
33
  } else {
39
34
  // Running outside CEP (for testing)
40
35
  console.warn("[CSInterface] Not running in CEP environment");
@@ -22,6 +22,7 @@
22
22
  <ScriptPath>./host.jsx</ScriptPath>
23
23
  <CEFCommandLine>
24
24
  <Parameter>--allow-file-access-from-files</Parameter>
25
+ <Parameter>--enable-nodejs</Parameter>
25
26
  </CEFCommandLine>
26
27
  </Resources>
27
28
  <Lifecycle>
@@ -30,9 +30,24 @@ function setStatus(state, text) {
30
30
  }
31
31
 
32
32
  // ---- File I/O via Node.js (CEP has access to Node) ----
33
- var fs = require("fs");
34
- var path = require("path");
35
- var os = require("os");
33
+ // --enable-nodejs puts `require` in the global scope on most hosts, but on some it
34
+ // lands on cep_node instead. Try both, and fail loudly rather than letting fs come
35
+ // back undefined and surface later as "Cannot read properties of undefined".
36
+ function nodeRequire(moduleName) {
37
+ if (typeof require !== "undefined") return require(moduleName);
38
+
39
+ var cepNode = typeof cep_node !== "undefined" ? cep_node : typeof window !== "undefined" ? window.cep_node : null;
40
+ if (cepNode && typeof cepNode.require === "function") return cepNode.require(moduleName);
41
+
42
+ throw new Error(
43
+ 'Node.js is not available in this CEP panel, so "' + moduleName + '" could not be loaded. ' +
44
+ "Check that CSXS/manifest.xml has <Parameter>--enable-nodejs</Parameter>, then fully quit and reopen Premiere Pro."
45
+ );
46
+ }
47
+
48
+ var fs = nodeRequire("fs");
49
+ var path = nodeRequire("path");
50
+ var os = nodeRequire("os");
36
51
  tempDir = path.join(os.tmpdir(), "premiere-mcp-bridge");
37
52
 
38
53
  function ensureDir(dir) {
@@ -127,10 +142,21 @@ function processOneCommand(cmdFileName) {
127
142
  // Check if it's already valid JSON
128
143
  var parsed = JSON.parse(result);
129
144
  response = JSON.stringify(parsed);
145
+ log("Result: OK", "ok");
130
146
  } else {
131
- response = JSON.stringify({ success: true, data: result || null });
147
+ // An empty result means evalScript gave us nothing back. That is a bridge
148
+ // failure, not a successful command with no data — reporting it as "OK" is
149
+ // what made this so hard to diagnose. Say so.
150
+ response = JSON.stringify({
151
+ success: false,
152
+ error:
153
+ "The bridge received an empty result from evalScript (got " +
154
+ (typeof result) +
155
+ "). The script may not have run. If every command does this, the CEP panel is stale — " +
156
+ "close and reopen it (a reload is not enough), or reinstall the extension.",
157
+ });
158
+ log("Result: EMPTY — evalScript returned nothing (see response file)", "err");
132
159
  }
133
- log("Result: OK", "ok");
134
160
  } catch (e) {
135
161
  // If result isn't JSON, wrap it
136
162
  if (result && result.indexOf("Error") === 0) {
@@ -1,13 +1,43 @@
1
- import { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync, readdirSync } from "node:fs";
1
+ import { mkdirSync, writeFileSync, readFileSync, unlinkSync, existsSync, readdirSync, statSync, chmodSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { tmpdir } from "node:os";
4
4
  const DEFAULT_TEMP_DIR = join(tmpdir(), "premiere-mcp-bridge");
5
5
  const POLL_INTERVAL_MS = 100;
6
6
  const DEFAULT_TIMEOUT_MS = 30000;
7
7
  let commandCounter = 0;
8
+ /**
9
+ * Create the bridge temp dir private to this user, and — critically — refuse to trust
10
+ * one we didn't create.
11
+ *
12
+ * The dir sits at a predictable, world-accessible path (e.g. /tmp/premiere-mcp-bridge)
13
+ * and the CEP panel executes ANY cmd_*.jsx it finds there, inside Premiere, as the
14
+ * logged-in user. On a shared machine another user could pre-create that path and drop
15
+ * command files, or read the res_*.json we write (which contain project data). And
16
+ * mkdirSync({recursive:true}) is a no-op on an existing dir — it does NOT re-apply the
17
+ * mode — so "create it 0o700" alone does not protect against a dir that was already there.
18
+ *
19
+ * So: if it exists, verify it's ours and lock its permissions down; if it isn't ours,
20
+ * fail loudly rather than executing whatever an attacker staged in it.
21
+ */
8
22
  function ensureDir(dir) {
9
23
  if (!existsSync(dir)) {
10
24
  mkdirSync(dir, { recursive: true, mode: 0o700 });
25
+ return;
26
+ }
27
+ // POSIX only — Windows doesn't model uid/mode the same way, and its per-user temp
28
+ // dir isn't world-writable to begin with.
29
+ if (process.platform === "win32")
30
+ return;
31
+ const st = statSync(dir);
32
+ const myUid = typeof process.getuid === "function" ? process.getuid() : undefined;
33
+ if (myUid !== undefined && st.uid !== myUid) {
34
+ throw new Error(`Bridge temp dir ${dir} is owned by uid ${st.uid}, not this user (${myUid}). ` +
35
+ `Refusing to use it — another user may have staged command files. ` +
36
+ `Set PREMIERE_TEMP_DIR to a path only you control.`);
37
+ }
38
+ // Clamp to owner-only, in case it was created with looser perms before this fix.
39
+ if ((st.mode & 0o077) !== 0) {
40
+ chmodSync(dir, 0o700);
11
41
  }
12
42
  }
13
43
  export function getTempDir(options) {
@@ -129,6 +129,263 @@ function __getAllClips(seq) {
129
129
  return clips;
130
130
  }
131
131
 
132
+ // Premiere's ExtendScript API exposes no preset/format enumeration (there is no
133
+ // encoder.getFormatList()), so presets have to be discovered by walking the .epr
134
+ // files Adobe ships on disk.
135
+
136
+ function __isMacOS() {
137
+ return !!($.os && $.os.toLowerCase().indexOf("mac") !== -1);
138
+ }
139
+
140
+ // Version-agnostic: returns install folders whose name starts with appNamePrefix,
141
+ // e.g. "Adobe Premiere Pro" -> [.../Adobe Premiere Pro 2026, .../Adobe Premiere Pro 2025]
142
+ function __adobeAppFolders(appNamePrefix) {
143
+ var base = new Folder(__isMacOS() ? "/Applications" : "C:\\\\Program Files\\\\Adobe");
144
+ if (!base.exists) return [];
145
+
146
+ var found = [];
147
+ var subs = base.getFiles(function(f) { return f instanceof Folder; });
148
+ for (var i = 0; i < subs.length; i++) {
149
+ if (subs[i].displayName.indexOf(appNamePrefix) === 0) found.push(subs[i]);
150
+ }
151
+ // Newest version first, so a 2026 preset wins over a stale 2024 one.
152
+ found.sort(function(a, b) { return a.displayName < b.displayName ? 1 : -1; });
153
+ return found;
154
+ }
155
+
156
+ function __collectEprFiles(folder, out) {
157
+ if (!folder || !folder.exists) return out;
158
+ var entries = folder.getFiles();
159
+ for (var i = 0; i < entries.length; i++) {
160
+ var entry = entries[i];
161
+ if (entry instanceof Folder) __collectEprFiles(entry, out);
162
+ else if (/\\.epr$/i.test(entry.name)) out.push(entry);
163
+ }
164
+ return out;
165
+ }
166
+
167
+ // All export presets AME ships, plus the user's own saved presets.
168
+ function __collectAllPresets() {
169
+ var roots = [];
170
+
171
+ var ame = __adobeAppFolders("Adobe Media Encoder");
172
+ for (var i = 0; i < ame.length; i++) {
173
+ roots.push(new Folder(ame[i].fsName + "/MediaIO/systempresets"));
174
+ }
175
+
176
+ var ppro = __adobeAppFolders("Adobe Premiere Pro");
177
+ for (var j = 0; j < ppro.length; j++) {
178
+ roots.push(new Folder(ppro[j].fsName + "/Settings/IngestPresets"));
179
+ }
180
+
181
+ // User-saved presets live under the Documents tree on both platforms.
182
+ var userRoot = new Folder(Folder.myDocuments.fsName + "/Adobe/Adobe Media Encoder");
183
+ if (userRoot.exists) {
184
+ var versions = userRoot.getFiles(function(f) { return f instanceof Folder; });
185
+ for (var v = 0; v < versions.length; v++) {
186
+ roots.push(new Folder(versions[v].fsName + "/Presets"));
187
+ }
188
+ }
189
+
190
+ var presets = [];
191
+ for (var r = 0; r < roots.length; r++) {
192
+ var eprs = __collectEprFiles(roots[r], []);
193
+ for (var e = 0; e < eprs.length; e++) {
194
+ presets.push({
195
+ name: decodeURI(eprs[e].displayName).replace(/\\.epr$/i, ""),
196
+ path: eprs[e].fsName,
197
+ // The parent folder is the format bucket, e.g. "48323634" (hex "H264").
198
+ format: eprs[e].parent ? decodeURI(eprs[e].parent.displayName) : ""
199
+ });
200
+ }
201
+ }
202
+ return presets;
203
+ }
204
+
205
+ // Default export preset. "48323634" is hex for "H264" — the folder name AME uses
206
+ // for the H.264 format bucket on disk.
207
+ function __findH264Preset() {
208
+ var presets = __collectAllPresets();
209
+ var candidates = [];
210
+ for (var i = 0; i < presets.length; i++) {
211
+ var haystack = (presets[i].name + " " + presets[i].format).toLowerCase();
212
+ if (haystack.indexOf("h264") !== -1 || haystack.indexOf("h.264") !== -1 || haystack.indexOf("48323634") !== -1) {
213
+ candidates.push(presets[i]);
214
+ }
215
+ }
216
+ if (!candidates.length) return "";
217
+
218
+ for (var j = 0; j < candidates.length; j++) {
219
+ if (candidates[j].name.toLowerCase().indexOf("match source - high") !== -1) return candidates[j].path;
220
+ }
221
+ return candidates[0].path;
222
+ }
223
+
224
+ function __findProxyPreset() {
225
+ var ppro = __adobeAppFolders("Adobe Premiere Pro");
226
+ for (var i = 0; i < ppro.length; i++) {
227
+ var proxyDir = new Folder(ppro[i].fsName + "/Settings/IngestPresets/Proxy");
228
+ var eprs = __collectEprFiles(proxyDir, []);
229
+ if (eprs.length) {
230
+ eprs.sort(function(a, b) { return a.displayName < b.displayName ? -1 : 1; });
231
+ return eprs[0].fsName;
232
+ }
233
+ }
234
+ return "";
235
+ }
236
+
237
+ function __findStillPreset(outputPath) {
238
+ var wantJpeg = /\\.jpe?g$/i.test(outputPath);
239
+ var needles = wantJpeg ? ["jpeg", "jpg"] : ["png"];
240
+ var presets = __collectAllPresets();
241
+
242
+ for (var n = 0; n < needles.length; n++) {
243
+ for (var i = 0; i < presets.length; i++) {
244
+ var haystack = (presets[i].name + " " + presets[i].format).toLowerCase();
245
+ if (haystack.indexOf(needles[n]) !== -1) return presets[i].path;
246
+ }
247
+ }
248
+ return "";
249
+ }
250
+
251
+ // Returns the path actually written, or "" if nothing was. Media Encoder treats a
252
+ // still export as a one-frame image *sequence* and appends a frame number to the
253
+ // filename, so an exact-path miss is not proof that nothing was written.
254
+ function __firstWrittenFile(outputPath) {
255
+ var exact = new File(outputPath);
256
+ if (exact.exists && exact.length > 0) return exact.fsName;
257
+
258
+ var dir = exact.parent;
259
+ if (!dir || !dir.exists) return "";
260
+
261
+ var fullName = decodeURI(exact.name);
262
+ var dot = fullName.lastIndexOf(".");
263
+ var base = dot === -1 ? fullName : fullName.substring(0, dot);
264
+ var ext = dot === -1 ? "" : fullName.substring(dot).toLowerCase();
265
+
266
+ var matches = dir.getFiles(function(candidate) {
267
+ if (candidate instanceof Folder) return false;
268
+ var nm = decodeURI(candidate.name);
269
+ if (nm.indexOf(base) !== 0) return false;
270
+ return ext === "" || nm.toLowerCase().substring(nm.length - ext.length) === ext;
271
+ });
272
+ if (!matches || !matches.length) return "";
273
+
274
+ // Normalize back to the caller's requested path so they get the name they asked for.
275
+ var produced = matches[0];
276
+ if (produced.length <= 0) return "";
277
+ try {
278
+ if (produced.fsName !== exact.fsName) produced.rename(fullName);
279
+ return exact.exists ? exact.fsName : produced.fsName;
280
+ } catch (e) {
281
+ return produced.fsName;
282
+ }
283
+ }
284
+
285
+ // Export a single frame to disk. Returns { ok, method, path, notes } / { ok:false, error, notes }.
286
+ //
287
+ // exportFramePNG/exportFrameJPEG do NOT exist on the public DOM sequence — only on
288
+ // the QE sequence — and even there they return false and write nothing on some
289
+ // builds. So we try QE first, verify against the filesystem rather than the return
290
+ // value, and fall back to a one-frame Media Encoder export.
291
+ function __exportStillFrame(outputPath, ticks) {
292
+ var seq = app.project.activeSequence;
293
+ if (!seq) return { ok: false, error: "No active sequence", notes: [] };
294
+
295
+ var notes = [];
296
+ var savedPos = null;
297
+ try { savedPos = seq.getPlayerPosition().ticks; } catch (e) {}
298
+
299
+ if (ticks) {
300
+ try { seq.setPlayerPosition(String(ticks)); } catch (e) { notes.push("setPlayerPosition: " + e.toString()); }
301
+ }
302
+ var atTicks = ticks;
303
+ if (!atTicks) {
304
+ try { atTicks = seq.getPlayerPosition().ticks; } catch (e) { atTicks = "0"; }
305
+ }
306
+
307
+ // Clear any stale file so that a file existing afterwards proves we wrote it.
308
+ var stale = new File(outputPath);
309
+ if (stale.exists) { try { stale.remove(); } catch (e) {} }
310
+
311
+ var wantJpeg = /\\.jpe?g$/i.test(outputPath);
312
+
313
+ // --- Path 1: QE DOM. Signature is (path, width, height) with string args. ---
314
+ try {
315
+ app.enableQE();
316
+ var qeSeq = qe.project.getActiveSequence();
317
+ if (!qeSeq) {
318
+ notes.push("QE: no active sequence");
319
+ } else {
320
+ var fn = wantJpeg ? qeSeq.exportFrameJPEG : qeSeq.exportFramePNG;
321
+ if (typeof fn !== "function") {
322
+ notes.push("QE: exportFrame" + (wantJpeg ? "JPEG" : "PNG") + " unavailable on this build");
323
+ } else {
324
+ var w = String(seq.frameSizeHorizontal);
325
+ var h = String(seq.frameSizeVertical);
326
+ try {
327
+ notes.push("QE returned " + fn.call(qeSeq, outputPath, w, h));
328
+ } catch (eArgs) {
329
+ try { notes.push("QE returned " + fn.call(qeSeq, outputPath, w)); }
330
+ catch (eArgs2) { notes.push("QE: " + eArgs2.toString()); }
331
+ }
332
+ }
333
+ }
334
+ } catch (eQE) {
335
+ notes.push("QE: " + eQE.toString());
336
+ }
337
+
338
+ var written = __firstWrittenFile(outputPath);
339
+ if (written) {
340
+ if (savedPos) { try { seq.setPlayerPosition(savedPos); } catch (e) {} }
341
+ return { ok: true, method: "qe", path: written, notes: notes };
342
+ }
343
+ notes.push("QE wrote no file; falling back to Media Encoder");
344
+
345
+ // --- Path 2: one-frame export through Media Encoder. ---
346
+ try {
347
+ var preset = __findStillPreset(outputPath);
348
+ if (!preset) {
349
+ notes.push("AME: no " + (wantJpeg ? "JPEG" : "PNG") + " still preset found on disk");
350
+ } else {
351
+ var savedIn = null, savedOut = null;
352
+ try {
353
+ savedIn = seq.getInPointAsTime().ticks;
354
+ savedOut = seq.getOutPointAsTime().ticks;
355
+ } catch (e) {}
356
+
357
+ // seq.timebase is ticks-per-frame, so in..in+timebase is exactly one frame.
358
+ var frameTicks = parseFloat(seq.timebase);
359
+ var startTicks = parseFloat(atTicks);
360
+ seq.setInPoint(String(startTicks));
361
+ seq.setOutPoint(String(startTicks + frameTicks));
362
+
363
+ try {
364
+ seq.exportAsMediaDirect(outputPath, preset, app.encoder.ENCODE_IN_TO_OUT);
365
+ notes.push("AME preset: " + preset);
366
+ } finally {
367
+ try {
368
+ if (savedIn !== null) seq.setInPoint(savedIn);
369
+ if (savedOut !== null) seq.setOutPoint(savedOut);
370
+ } catch (e) {}
371
+ }
372
+ }
373
+ } catch (eAME) {
374
+ notes.push("AME: " + eAME.toString());
375
+ }
376
+
377
+ if (savedPos) { try { seq.setPlayerPosition(savedPos); } catch (e) {} }
378
+
379
+ written = __firstWrittenFile(outputPath);
380
+ if (written) return { ok: true, method: "ame", path: written, notes: notes };
381
+
382
+ return {
383
+ ok: false,
384
+ error: "Frame export produced no file on disk. Neither the QE DOM nor Media Encoder wrote " + outputPath,
385
+ notes: notes
386
+ };
387
+ }
388
+
132
389
  function __jsonStringify(obj) {
133
390
  // ES3-compatible JSON stringify
134
391
  if (typeof JSON !== "undefined" && JSON.stringify) {
@@ -15,7 +15,10 @@
15
15
  * PORT HTTP port to listen on (default: 3000)
16
16
  * PREMIERE_TEMP_DIR Shared temp directory for the file bridge
17
17
  * PREMIERE_TIMEOUT_MS Command timeout in ms (default: 30000)
18
- * MCP_AUTH_TOKEN Optional bearer token to require on all requests
18
+ * MCP_AUTH_TOKEN Bearer token required on every /mcp request. REQUIRED — the
19
+ * server refuses to start without it, because this transport
20
+ * binds 0.0.0.0 and can drive Premiere. Set ALLOW_UNAUTHENTICATED=1
21
+ * only for a deliberately public, throwaway instance.
19
22
  */
20
23
  export {};
21
24
  //# sourceMappingURL=http-server.d.ts.map
@@ -15,14 +15,81 @@
15
15
  * PORT HTTP port to listen on (default: 3000)
16
16
  * PREMIERE_TEMP_DIR Shared temp directory for the file bridge
17
17
  * PREMIERE_TIMEOUT_MS Command timeout in ms (default: 30000)
18
- * MCP_AUTH_TOKEN Optional bearer token to require on all requests
18
+ * MCP_AUTH_TOKEN Bearer token required on every /mcp request. REQUIRED — the
19
+ * server refuses to start without it, because this transport
20
+ * binds 0.0.0.0 and can drive Premiere. Set ALLOW_UNAUTHENTICATED=1
21
+ * only for a deliberately public, throwaway instance.
19
22
  */
20
23
  import http from "node:http";
24
+ import fs from "node:fs";
25
+ import path from "node:path";
26
+ import { timingSafeEqual } from "node:crypto";
27
+ import { fileURLToPath } from "node:url";
21
28
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
22
29
  import { createServer } from "./server.js";
23
30
  import { cleanupTempDir, getTempDir } from "./bridge/file-bridge.js";
31
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
32
+ const LANDING_DIR = path.resolve(__dirname, "../landing-dist");
33
+ const MIME = {
34
+ ".html": "text/html; charset=utf-8",
35
+ ".js": "application/javascript; charset=utf-8",
36
+ ".css": "text/css; charset=utf-8",
37
+ ".json": "application/json",
38
+ ".png": "image/png",
39
+ ".svg": "image/svg+xml",
40
+ ".ico": "image/x-icon",
41
+ ".woff2": "font/woff2",
42
+ ".woff": "font/woff",
43
+ ".ttf": "font/ttf",
44
+ ".txt": "text/plain",
45
+ ".xml": "application/xml",
46
+ };
47
+ function serveLanding(req, res) {
48
+ if (!fs.existsSync(LANDING_DIR))
49
+ return false;
50
+ let urlPath = req.url?.split("?")[0] ?? "/";
51
+ if (urlPath === "/" || urlPath === "")
52
+ urlPath = "/index.html";
53
+ // Next.js trailingSlash: /about/ -> /about/index.html
54
+ if (urlPath.endsWith("/"))
55
+ urlPath += "index.html";
56
+ const filePath = path.join(LANDING_DIR, urlPath);
57
+ // Security: ensure we stay within LANDING_DIR
58
+ if (!filePath.startsWith(LANDING_DIR))
59
+ return false;
60
+ if (!fs.existsSync(filePath))
61
+ return false;
62
+ const ext = path.extname(filePath);
63
+ const contentType = MIME[ext] ?? "application/octet-stream";
64
+ res.writeHead(200, { "Content-Type": contentType });
65
+ fs.createReadStream(filePath).pipe(res);
66
+ return true;
67
+ }
24
68
  const PORT = parseInt(process.env.PORT ?? "3000", 10);
25
69
  const AUTH_TOKEN = process.env.MCP_AUTH_TOKEN;
70
+ const ALLOW_UNAUTHENTICATED = process.env.ALLOW_UNAUTHENTICATED === "1";
71
+ // Fail closed. This transport listens on 0.0.0.0 and every /mcp request can drive
72
+ // Premiere, so starting it wide open is almost never what anyone means to do. Refuse
73
+ // unless a token is set, or the operator has explicitly opted into a public instance.
74
+ if (!AUTH_TOKEN && !ALLOW_UNAUTHENTICATED) {
75
+ console.error("[premiere-pro-mcp] Refusing to start: MCP_AUTH_TOKEN is not set.\n" +
76
+ " This HTTP transport is remotely reachable and can control Premiere.\n" +
77
+ " Set MCP_AUTH_TOKEN to a strong secret, or set ALLOW_UNAUTHENTICATED=1 if you\n" +
78
+ " genuinely want a public, unauthenticated instance.");
79
+ process.exit(1);
80
+ }
81
+ /** Constant-time bearer check, so token comparison leaks no timing signal. */
82
+ function isAuthorized(req) {
83
+ if (!AUTH_TOKEN)
84
+ return true; // only reachable under ALLOW_UNAUTHENTICATED
85
+ const header = req.headers["authorization"] ?? "";
86
+ const provided = header.startsWith("Bearer ") ? header.slice(7) : "";
87
+ const a = Buffer.from(provided);
88
+ const b = Buffer.from(AUTH_TOKEN);
89
+ // timingSafeEqual throws on length mismatch, which would itself leak length — so
90
+ // guard the length check first and only compare equal-length buffers.
91
+ return a.length === b.length && timingSafeEqual(a, b);
92
+ }
26
93
  const bridgeOptions = {
27
94
  tempDir: process.env.PREMIERE_TEMP_DIR,
28
95
  timeoutMs: process.env.PREMIERE_TIMEOUT_MS
@@ -41,21 +108,19 @@ const httpServer = http.createServer(async (req, res) => {
41
108
  res.end(JSON.stringify({ status: "ok", service: "premiere-pro-mcp" }));
42
109
  return;
43
110
  }
44
- // Only handle /mcp endpoint
111
+ // Only handle /mcp endpoint; everything else goes to the landing page
45
112
  if (!req.url?.startsWith("/mcp")) {
113
+ if (serveLanding(req, res))
114
+ return;
46
115
  res.writeHead(404);
47
116
  res.end("Not found");
48
117
  return;
49
118
  }
50
- // Optional bearer token auth
51
- if (AUTH_TOKEN) {
52
- const authHeader = req.headers["authorization"] ?? "";
53
- const provided = authHeader.startsWith("Bearer ") ? authHeader.slice(7) : "";
54
- if (provided !== AUTH_TOKEN) {
55
- res.writeHead(401, { "Content-Type": "application/json" });
56
- res.end(JSON.stringify({ error: "Unauthorized" }));
57
- return;
58
- }
119
+ // Bearer token auth (constant-time; required unless ALLOW_UNAUTHENTICATED=1)
120
+ if (!isAuthorized(req)) {
121
+ res.writeHead(401, { "Content-Type": "application/json" });
122
+ res.end(JSON.stringify({ error: "Unauthorized" }));
123
+ return;
59
124
  }
60
125
  const mcpServer = createServer(bridgeOptions);
61
126
  const transport = new StreamableHTTPServerTransport({
@@ -84,7 +149,7 @@ httpServer.listen(PORT, "0.0.0.0", () => {
84
149
  console.error(`[premiere-pro-mcp] Auth: Bearer token required`);
85
150
  }
86
151
  else {
87
- console.error(`[premiere-pro-mcp] Auth: none (set MCP_AUTH_TOKEN to enable)`);
152
+ console.error(`[premiere-pro-mcp] Auth: DISABLED via ALLOW_UNAUTHENTICATED anyone can drive this server`);
88
153
  }
89
154
  });
90
155
  process.on("SIGTERM", () => {
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Workflow prompts.
3
+ *
4
+ * 269 tools is an API, not an assistant. An agent handed a flat list of 269 names has to
5
+ * rediscover, every session, that you import before you edit, that effect names must match
6
+ * this machine exactly, and that a node_id goes stale the moment the timeline shifts under
7
+ * it. These encode the running order so it doesn't have to.
8
+ *
9
+ * Each prompt is deliberately opinionated about sequencing and about verification — the
10
+ * failure mode we care most about is an agent that believes an edit landed when it didn't.
11
+ */
12
+ export interface PromptArg {
13
+ name: string;
14
+ description: string;
15
+ required?: boolean;
16
+ }
17
+ export interface PromptDef {
18
+ name: string;
19
+ description: string;
20
+ arguments: PromptArg[];
21
+ build: (args: Record<string, string>) => string;
22
+ }
23
+ export declare const PROMPTS: PromptDef[];
24
+ export declare function getPrompt(name: string): PromptDef | undefined;
25
+ //# sourceMappingURL=index.d.ts.map