premiere-pro-mcp 1.1.1 → 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 +64 -0
- package/README.md +16 -4
- package/cep-plugin/CSInterface.js +4 -9
- package/cep-plugin/CSXS/manifest.xml +1 -0
- package/cep-plugin/main.js +31 -5
- package/dist/bridge/file-bridge.js +31 -1
- package/dist/bridge/script-builder.js +257 -0
- package/dist/http-server.d.ts +4 -1
- package/dist/http-server.js +34 -11
- package/dist/prompts/index.d.ts +25 -0
- package/dist/prompts/index.js +243 -0
- package/dist/resources/live.d.ts +35 -0
- package/dist/resources/live.js +114 -0
- package/dist/tools/audio.js +21 -10
- package/dist/tools/effects.js +43 -18
- package/dist/tools/export.d.ts +10 -0
- package/dist/tools/export.js +82 -54
- package/dist/tools/markers.js +6 -6
- package/dist/tools/registry.d.ts +17 -0
- package/dist/tools/registry.js +66 -0
- package/dist/tools/text.d.ts +3 -7
- package/dist/tools/text.js +42 -28
- package/dist/tools/track-targeting.js +21 -25
- package/dist/tools/transitions.js +63 -30
- package/dist/tools/utility.js +52 -12
- package/package.json +1 -1
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
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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");
|
package/cep-plugin/main.js
CHANGED
|
@@ -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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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
|
-
|
|
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) {
|
package/dist/http-server.d.ts
CHANGED
|
@@ -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
|
|
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
|
package/dist/http-server.js
CHANGED
|
@@ -15,11 +15,15 @@
|
|
|
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
|
|
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";
|
|
21
24
|
import fs from "node:fs";
|
|
22
25
|
import path from "node:path";
|
|
26
|
+
import { timingSafeEqual } from "node:crypto";
|
|
23
27
|
import { fileURLToPath } from "node:url";
|
|
24
28
|
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
|
|
25
29
|
import { createServer } from "./server.js";
|
|
@@ -63,6 +67,29 @@ function serveLanding(req, res) {
|
|
|
63
67
|
}
|
|
64
68
|
const PORT = parseInt(process.env.PORT ?? "3000", 10);
|
|
65
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
|
+
}
|
|
66
93
|
const bridgeOptions = {
|
|
67
94
|
tempDir: process.env.PREMIERE_TEMP_DIR,
|
|
68
95
|
timeoutMs: process.env.PREMIERE_TIMEOUT_MS
|
|
@@ -89,15 +116,11 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
89
116
|
res.end("Not found");
|
|
90
117
|
return;
|
|
91
118
|
}
|
|
92
|
-
//
|
|
93
|
-
if (
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
res.writeHead(401, { "Content-Type": "application/json" });
|
|
98
|
-
res.end(JSON.stringify({ error: "Unauthorized" }));
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
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;
|
|
101
124
|
}
|
|
102
125
|
const mcpServer = createServer(bridgeOptions);
|
|
103
126
|
const transport = new StreamableHTTPServerTransport({
|
|
@@ -126,7 +149,7 @@ httpServer.listen(PORT, "0.0.0.0", () => {
|
|
|
126
149
|
console.error(`[premiere-pro-mcp] Auth: Bearer token required`);
|
|
127
150
|
}
|
|
128
151
|
else {
|
|
129
|
-
console.error(`[premiere-pro-mcp] Auth:
|
|
152
|
+
console.error(`[premiere-pro-mcp] Auth: DISABLED via ALLOW_UNAUTHENTICATED — anyone can drive this server`);
|
|
130
153
|
}
|
|
131
154
|
});
|
|
132
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
|