premiere-pro-mcp 1.14.2 → 1.14.4
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 +34 -0
- package/README.md +72 -27
- package/artifacts/MCPBridgeCEP.zxp +0 -0
- package/cep-plugin/CSXS/manifest.xml +3 -3
- package/cep-plugin/index.html +1 -1
- package/cep-plugin/updater.cjs +1 -1
- package/dist/http-admission.d.ts +10 -1
- package/dist/http-admission.js +84 -7
- package/dist/http-server.d.ts +2 -0
- package/dist/http-server.js +70 -8
- package/dist/index.js +18 -5
- package/dist/intake/project-intake.d.ts +4 -0
- package/dist/intake/project-intake.js +38 -6
- package/dist/oauth-resource-server.d.ts +34 -0
- package/dist/oauth-resource-server.js +91 -0
- package/dist/resources/extendscript-reference.js +1 -1
- package/dist/tools/audio.js +20 -7
- package/dist/tools/captions.js +33 -4
- package/dist/tools/clipboard.js +101 -35
- package/dist/tools/effects.js +54 -33
- package/dist/tools/export.d.ts +42 -2
- package/dist/tools/export.js +124 -17
- package/dist/tools/health.d.ts +43 -3
- package/dist/tools/health.js +61 -6
- package/dist/tools/inspection.d.ts +53 -2
- package/dist/tools/inspection.js +71 -13
- package/dist/tools/metadata.d.ts +10 -0
- package/dist/tools/metadata.js +15 -5
- package/dist/tools/playback.js +19 -6
- package/dist/tools/project-manager.js +24 -7
- package/dist/tools/sequence.js +21 -9
- package/dist/tools/timeline.js +14 -1
- package/dist/tools/track-targeting.d.ts +4 -1
- package/dist/tools/track-targeting.js +50 -28
- package/dist/tools/transitions.js +12 -3
- package/dist/workflows/tool-packs.js +1 -0
- package/docs/mcp-2026-07-28-capabilities.md +6 -4
- package/docs/supported-actions.md +21 -20
- package/package.json +7 -6
- package/scripts/validate-distribution.mjs +6 -4
- package/uxp-plugin/commands.cjs +13 -3
- package/uxp-plugin/manifest.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,40 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [1.14.4] - 2026-08-29
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- Corrected QE razor operations to pass sequence timecode rather than ticks and
|
|
14
|
+
added regression coverage for both split and all-track cuts.
|
|
15
|
+
- Made batch effect application preflight every target, match QE clips without
|
|
16
|
+
assuming gap-free indexes, and require post-application component readback.
|
|
17
|
+
- Replaced false playback-success claims with explicit request-only results and
|
|
18
|
+
polling guidance when the legacy API cannot provide same-call verification.
|
|
19
|
+
- Added direct QE by-name effect probes when Premiere exposes an empty effect
|
|
20
|
+
catalog, while labelling bounded fallback lists as partial.
|
|
21
|
+
- Made an empty or unavailable QE audio-transition catalog fail closed instead
|
|
22
|
+
of appearing as a usable transition list.
|
|
23
|
+
|
|
24
|
+
## [1.14.3] - 2026-08-29
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- Added an optional, fail-closed OAuth resource-server mode with RFC 9728
|
|
29
|
+
protected-resource metadata, remote JWKS verification, exact issuer and
|
|
30
|
+
audience validation, required scopes, and an explicit trusted-subject
|
|
31
|
+
allowlist for operator-managed HTTP deployments.
|
|
32
|
+
|
|
33
|
+
### Security
|
|
34
|
+
|
|
35
|
+
- Added an IP-keyed admission gate before JWT verification and isolated
|
|
36
|
+
authenticated rate-limit identities behind random process-local keys.
|
|
37
|
+
- Made partial or mixed OAuth/shared-token configuration fail startup, kept the
|
|
38
|
+
shared token as an operator-only compatibility mode, and removed internal
|
|
39
|
+
admission counters from the public health response.
|
|
40
|
+
- Kept public desktop routing deliberately disabled: OAuth does not claim
|
|
41
|
+
user-to-device pairing or access to a user's local Premiere process.
|
|
42
|
+
|
|
9
43
|
## [1.14.2] - 2026-08-28
|
|
10
44
|
|
|
11
45
|
### Added
|
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**Give compatible AI assistants structured control over supported Adobe Premiere Pro workflows.**
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
321 core tools across 37 modules, 14 resources, and 11 guided workflows. A connected UXP host adds 54 capability-gated tools.
|
|
12
12
|
|
|
13
13
|
[](LICENSE)
|
|
14
14
|
[](https://nodejs.org)
|
|
@@ -31,23 +31,22 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that l
|
|
|
31
31
|
"Add the B-roll clips to V2, apply a cross dissolve between each, color correct them to match the A-roll, and export a 1080p ProRes."
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
The AI handles the entire workflow through
|
|
34
|
+
The AI handles the entire workflow through 321 core tools spanning the supported ExtendScript, QE DOM, local media and interchange analysis, revisioned project-context retrieval, safe edit-planning, project-intake preview, review handoff, and connection-verification surfaces. A compatible, authenticated UXP panel adds 54 documented, capability-gated tools without replacing the production CEP bridge.
|
|
35
35
|
|
|
36
|
-
### Latest release: 1.14.
|
|
36
|
+
### Latest release: 1.14.4
|
|
37
37
|
|
|
38
|
-
- **
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
- **
|
|
42
|
-
|
|
43
|
-
- **
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
npm CLI.
|
|
38
|
+
- **Verified editing semantics:** QE razor operations use sequence timecode and
|
|
39
|
+
batch effect application preflights every target before reporting a verified
|
|
40
|
+
component readback.
|
|
41
|
+
- **Honest playback state:** legacy playback tools report an accepted request,
|
|
42
|
+
not movement or stoppage, until a separate position readback confirms it.
|
|
43
|
+
- **Fail-closed catalogs:** empty QE audio-transition catalogs are errors, and
|
|
44
|
+
effect fallback results are explicitly bounded and partial.
|
|
45
|
+
- **Explicit boundary:** the hosted endpoint remains an operator-managed MCP
|
|
46
|
+
service; unauthenticated callers are rejected and it does not pair users to
|
|
47
|
+
local Premiere processes.
|
|
49
48
|
|
|
50
|
-
See the [v1.14.
|
|
49
|
+
See the [v1.14.4 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.14.4)
|
|
51
50
|
for complete details. Live installation in Premiere Pro still requires host verification.
|
|
52
51
|
|
|
53
52
|
### Current MCP protocol support
|
|
@@ -89,9 +88,9 @@ their bins, media rules, and organization rules before a facility uses one.
|
|
|
89
88
|
|
|
90
89
|
### Easiest supported path: Claude Desktop
|
|
91
90
|
|
|
92
|
-
1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.14.
|
|
91
|
+
1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.14.4/premiere-pro-mcp-1.14.4.mcpb).
|
|
93
92
|
2. In Claude Desktop, open **Settings > Extensions > Advanced settings > Install Extension**, select the downloaded bundle, and restart Claude Desktop.
|
|
94
|
-
3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.14.
|
|
93
|
+
3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.14.4/MCPBridgeCEP.zxp). Open it with your trusted ZXP installer. If your computer has no ZXP installer, use the npm connector installer in **Advanced setup** below.
|
|
95
94
|
4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
|
|
96
95
|
5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
|
|
97
96
|
|
|
@@ -338,11 +337,11 @@ From a clone of this repository:
|
|
|
338
337
|
```bash
|
|
339
338
|
codex plugin marketplace add .
|
|
340
339
|
codex plugin add premiere-pro@premiere-pro-mcp
|
|
341
|
-
npx -y premiere-pro-mcp@1.14.
|
|
340
|
+
npx -y premiere-pro-mcp@1.14.4 --install-cep
|
|
342
341
|
```
|
|
343
342
|
|
|
344
343
|
Restart Premiere Pro and start a new Codex session after installation. The plugin
|
|
345
|
-
launches `premiere-pro-mcp@1.14.
|
|
344
|
+
launches `premiere-pro-mcp@1.14.4` through `npx`; the separate CEP installation is
|
|
346
345
|
required because the MCP server communicates with the running Premiere host through
|
|
347
346
|
the local bridge.
|
|
348
347
|
|
|
@@ -362,7 +361,7 @@ For Claude Code, add this repository as a marketplace and install the plugin:
|
|
|
362
361
|
Then install the Premiere bridge and start a new Claude Code session:
|
|
363
362
|
|
|
364
363
|
```bash
|
|
365
|
-
npx -y premiere-pro-mcp@1.14.
|
|
364
|
+
npx -y premiere-pro-mcp@1.14.4 --install-cep
|
|
366
365
|
```
|
|
367
366
|
|
|
368
367
|
The Claude Code package lives in
|
|
@@ -401,7 +400,7 @@ installed separately.
|
|
|
401
400
|
QE-backed tools are reported as `experimental` because QE is undocumented and can vary between Premiere builds. Authority availability is reported separately from implementation support, so disabling `edit`, for example, does not incorrectly label editing tools as unsupported. Static metadata never claims that a Premiere operation succeeded; use `ping` and inspect each tool result for runtime evidence.
|
|
402
401
|
|
|
403
402
|
MCP `tools/list` is filtered to the active authority profile. The default
|
|
404
|
-
`inspect,edit,export,filesystem` profile advertises
|
|
403
|
+
`inspect,edit,export,filesystem` profile advertises 319 of the 321 registered
|
|
405
404
|
tools and omits `execute_extendscript` and `evaluate_expression`, which require
|
|
406
405
|
explicit `unsafe-script` authority. `ping` and `get_capabilities` remain visible
|
|
407
406
|
under every profile so a restricted or misconfigured server can still explain
|
|
@@ -579,7 +578,7 @@ The file-based IPC bridge is simple, reliable, and works across macOS and Window
|
|
|
579
578
|
|
|
580
579
|
---
|
|
581
580
|
|
|
582
|
-
## Tools (
|
|
581
|
+
## Tools (321 core total; 319 under the default profile; 373 with a connected UXP bridge)
|
|
583
582
|
|
|
584
583
|
The [complete supported-actions catalog](docs/supported-actions.md) lists every
|
|
585
584
|
registered core tool, the two tools restricted behind explicit `unsafe-script`
|
|
@@ -808,21 +807,28 @@ feature coverage, playback, rendering, or editorial correctness.
|
|
|
808
807
|
|
|
809
808
|
The server includes an HTTP/SSE transport (`src/http-server.ts`) for remote access via [mcp-remote](https://github.com/geelen/mcp-remote) or any MCP client that supports Streamable HTTP.
|
|
810
809
|
|
|
811
|
-
A live instance is running at **https://premiere-pro-mcp.fly.dev**.
|
|
810
|
+
A live operator-managed instance is running at **https://premiere-pro-mcp.fly.dev**.
|
|
811
|
+
It is not a public desktop relay: it cannot connect an authenticated user to
|
|
812
|
+
Premiere on that user's computer. Public users should use the local stdio setup
|
|
813
|
+
until the separate device-pairing relay is available.
|
|
812
814
|
|
|
813
|
-
### Connect
|
|
815
|
+
### Connect to an operator-managed instance
|
|
814
816
|
|
|
815
817
|
```json
|
|
816
818
|
{
|
|
817
819
|
"mcpServers": {
|
|
818
820
|
"premiere-pro": {
|
|
819
821
|
"command": "npx",
|
|
820
|
-
"args": ["mcp-remote", "https://
|
|
822
|
+
"args": ["mcp-remote", "https://your-authorized-instance.example/mcp"]
|
|
821
823
|
}
|
|
822
824
|
}
|
|
823
825
|
}
|
|
824
826
|
```
|
|
825
827
|
|
|
828
|
+
The instance must either provision an operator bearer token or use the OAuth
|
|
829
|
+
resource-server configuration below. The production endpoint intentionally
|
|
830
|
+
returns `401` to callers who have not been authorized.
|
|
831
|
+
|
|
826
832
|
### Self-host on Fly.io
|
|
827
833
|
|
|
828
834
|
```bash
|
|
@@ -849,6 +855,39 @@ Then connect with:
|
|
|
849
855
|
}
|
|
850
856
|
```
|
|
851
857
|
|
|
858
|
+
### Trusted-operator OAuth resource-server mode
|
|
859
|
+
|
|
860
|
+
For an identity-aware operator deployment, configure a real OAuth/OIDC authorization
|
|
861
|
+
server rather than distributing `MCP_AUTH_TOKEN`. The authorization server must
|
|
862
|
+
support the MCP client's registration model and issue signed access tokens with
|
|
863
|
+
an exact audience for this MCP resource.
|
|
864
|
+
|
|
865
|
+
```bash
|
|
866
|
+
fly secrets set \
|
|
867
|
+
MCP_OAUTH_ISSUER=https://identity.example.com \
|
|
868
|
+
MCP_OAUTH_JWKS_URI=https://identity.example.com/.well-known/jwks.json \
|
|
869
|
+
MCP_OAUTH_AUDIENCE=https://your-app-name.fly.dev/mcp \
|
|
870
|
+
MCP_PUBLIC_URL=https://your-app-name.fly.dev \
|
|
871
|
+
MCP_OAUTH_REQUIRED_SCOPES=premiere:mcp \
|
|
872
|
+
MCP_OAUTH_ALLOWED_SUBJECTS=your-provider-user-subject
|
|
873
|
+
```
|
|
874
|
+
|
|
875
|
+
OAuth mode validates the token signature, algorithm, issuer, exact audience,
|
|
876
|
+
expiry, issued-at time, subject, and required scopes. It publishes protected
|
|
877
|
+
resource metadata at `/.well-known/oauth-protected-resource/mcp` and includes
|
|
878
|
+
that URL in the `WWW-Authenticate` challenge. Configuration is fail-closed:
|
|
879
|
+
partial OAuth settings, non-HTTPS production URLs, ambiguous OAuth/shared-token
|
|
880
|
+
settings, and missing credentials all prevent startup.
|
|
881
|
+
|
|
882
|
+
`MCP_OAUTH_ALLOWED_SUBJECTS` is mandatory and restricts this single-bridge
|
|
883
|
+
deployment to explicitly trusted operator identities. This is an enforcement
|
|
884
|
+
boundary, not a public-user device model.
|
|
885
|
+
|
|
886
|
+
This mode authenticates trusted operators but does **not** yet implement device ownership,
|
|
887
|
+
desktop pairing, or per-user Premiere routing. Do not expose editor mutations as
|
|
888
|
+
a public multi-user service until an outbound desktop relay and durable
|
|
889
|
+
user/device authorization are implemented.
|
|
890
|
+
|
|
852
891
|
> **Note:** The file bridge still requires the CEP plugin to share the same `PREMIERE_TEMP_DIR`. For cloud deployments this means running a sync agent or using `fly proxy` / WireGuard to reach your local machine.
|
|
853
892
|
> `detect_silence` can analyze only media paths available inside the server filesystem; a desktop-only path is not automatically available to a remote Fly machine.
|
|
854
893
|
> For a shared or multi-user remote deployment, put a managed identity-aware edge in front of the server and replace the shared bearer secret with per-user authorization. The built-in limiter is intentionally process-local defense in depth, not a substitute for an edge/WAF or account system.
|
|
@@ -867,7 +906,13 @@ Then connect with:
|
|
|
867
906
|
| `PREMIERE_CONTEXT_BACKEND` | Local project-context store: `auto`, `sqlite`, `json`, or `memory` | `auto` |
|
|
868
907
|
| `PREMIERE_CONTEXT_DIR` | Override the local project-context storage directory | OS application-data directory |
|
|
869
908
|
| `PORT` | HTTP port (HTTP/SSE transport only) | `3000` |
|
|
870
|
-
| `MCP_AUTH_TOKEN` |
|
|
909
|
+
| `MCP_AUTH_TOKEN` | Operator bearer token for controlled HTTP deployments; mutually exclusive with OAuth mode | unset |
|
|
910
|
+
| `MCP_OAUTH_ISSUER` | Exact trusted OAuth/OIDC token issuer URL | unset |
|
|
911
|
+
| `MCP_OAUTH_JWKS_URI` | HTTPS JWKS URL used to verify access-token signatures | unset |
|
|
912
|
+
| `MCP_OAUTH_AUDIENCE` | Exact MCP resource audience, normally the public `/mcp` URL | unset |
|
|
913
|
+
| `MCP_PUBLIC_URL` | Canonical HTTPS origin used in protected-resource discovery | unset |
|
|
914
|
+
| `MCP_OAUTH_REQUIRED_SCOPES` | Space- or comma-separated scopes required for `/mcp` | `premiere:mcp` |
|
|
915
|
+
| `MCP_OAUTH_ALLOWED_SUBJECTS` | Mandatory comma-separated token-subject allowlist for the single operator bridge | unset |
|
|
871
916
|
| `ALLOW_UNAUTHENTICATED` | Set to `1` only for local/test HTTP harnesses; it is rejected when `NODE_ENV=production` | unset |
|
|
872
917
|
| `MCP_MAX_REQUEST_BYTES` | Maximum HTTP MCP request body size | `1048576` |
|
|
873
918
|
| `MCP_HEADERS_TIMEOUT_MS` | Maximum time to receive request headers | `10000` |
|
|
@@ -907,7 +952,7 @@ premiere-pro-mcp/
|
|
|
907
952
|
├── src/
|
|
908
953
|
│ ├── index.ts # Entry point — stdio transport setup
|
|
909
954
|
│ ├── http-server.ts # Entry point — HTTP/SSE transport (Fly.io / remote)
|
|
910
|
-
│ ├── server.ts # MCP server — registers
|
|
955
|
+
│ ├── server.ts # MCP server — registers 321 tools, filtered by authority profile
|
|
911
956
|
│ ├── bridge/
|
|
912
957
|
│ │ ├── file-bridge.ts # File-based IPC (write .jsx, poll .json)
|
|
913
958
|
│ │ └── script-builder.ts # ExtendScript generator with ES3 helpers
|
|
Binary file
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.14.
|
|
2
|
+
<ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.14.4" ExtensionBundleName="MCP for Adobe Premiere Pro">
|
|
3
3
|
<ExtensionList>
|
|
4
|
-
<Extension Id="com.mcp.premiere.bridge.panel" Version="1.14.
|
|
5
|
-
<Extension Id="com.mcp.premiere.bridge.headless" Version="1.14.
|
|
4
|
+
<Extension Id="com.mcp.premiere.bridge.panel" Version="1.14.4"/>
|
|
5
|
+
<Extension Id="com.mcp.premiere.bridge.headless" Version="1.14.4"/>
|
|
6
6
|
</ExtensionList>
|
|
7
7
|
<ExecutionEnvironment>
|
|
8
8
|
<HostList>
|
package/cep-plugin/index.html
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
<section class="update-section" aria-live="polite">
|
|
82
82
|
<div class="update-copy">
|
|
83
83
|
<span class="section-label">Connector updates</span>
|
|
84
|
-
<strong id="updateTitle">Version 1.14.
|
|
84
|
+
<strong id="updateTitle">Version 1.14.4</strong>
|
|
85
85
|
<span id="updateDetail">Checking for updates…</span>
|
|
86
86
|
</div>
|
|
87
87
|
<button id="btnUpdate" class="button button-update" onclick="handleUpdateClick()" type="button" disabled>
|
package/cep-plugin/updater.cjs
CHANGED
package/dist/http-admission.d.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
1
|
import type http from "node:http";
|
|
2
2
|
export declare const MCP_HTTP_METHODS: readonly ["GET", "POST", "DELETE"];
|
|
3
3
|
export interface HttpAuthConfiguration {
|
|
4
|
+
mode: "shared-token" | "oauth" | "unauthenticated";
|
|
4
5
|
authToken?: string;
|
|
6
|
+
oauth?: {
|
|
7
|
+
issuer: string;
|
|
8
|
+
audience: string;
|
|
9
|
+
publicUrl: string;
|
|
10
|
+
jwksUri: string;
|
|
11
|
+
requiredScopes: string[];
|
|
12
|
+
allowedSubjects: string[];
|
|
13
|
+
};
|
|
5
14
|
allowUnauthenticated: boolean;
|
|
6
15
|
}
|
|
7
16
|
export interface HttpAdmissionSettings {
|
|
@@ -59,7 +68,7 @@ export declare function isAuthorizedBearer(req: Pick<http.IncomingMessage, "head
|
|
|
59
68
|
* operator explicitly declares the proxy trusted; otherwise it is attacker
|
|
60
69
|
* input and must not be used as a rate-limit identity.
|
|
61
70
|
*/
|
|
62
|
-
export declare function rateLimitIdentity(req: Pick<http.IncomingMessage, "headers" | "socket">,
|
|
71
|
+
export declare function rateLimitIdentity(req: Pick<http.IncomingMessage, "headers" | "socket">, trustProxy: boolean): string;
|
|
63
72
|
/**
|
|
64
73
|
* Bounded, process-local protection for a single machine. It deliberately does
|
|
65
74
|
* not log or export identities. An edge/WAF remains necessary for fleet-wide
|
package/dist/http-admission.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
2
2
|
export const MCP_HTTP_METHODS = ["GET", "POST", "DELETE"];
|
|
3
3
|
const ONE_MINUTE_MS = 60_000;
|
|
4
|
+
const RATE_LIMIT_IDENTITY_KEY = randomBytes(32);
|
|
4
5
|
function readBoundedInteger(env, name, fallback, minimum, maximum) {
|
|
5
6
|
const raw = env[name];
|
|
6
7
|
if (raw === undefined || raw === "")
|
|
@@ -41,14 +42,89 @@ export function readHttpAdmissionSettings(env) {
|
|
|
41
42
|
*/
|
|
42
43
|
export function readHttpAuthConfiguration(env) {
|
|
43
44
|
const authToken = env.MCP_AUTH_TOKEN?.trim();
|
|
45
|
+
const oauthIssuer = env.MCP_OAUTH_ISSUER?.trim();
|
|
46
|
+
const oauthAudience = env.MCP_OAUTH_AUDIENCE?.trim();
|
|
47
|
+
const publicUrl = env.MCP_PUBLIC_URL?.trim();
|
|
48
|
+
const oauthJwksUri = env.MCP_OAUTH_JWKS_URI?.trim();
|
|
49
|
+
const oauthRequiredScopes = env.MCP_OAUTH_REQUIRED_SCOPES?.trim();
|
|
50
|
+
const oauthAllowedSubjects = env.MCP_OAUTH_ALLOWED_SUBJECTS?.trim();
|
|
51
|
+
const hasOAuthIntent = Boolean(oauthIssuer || oauthAudience || publicUrl || oauthJwksUri || oauthRequiredScopes || oauthAllowedSubjects);
|
|
52
|
+
if (authToken && hasOAuthIntent) {
|
|
53
|
+
throw new Error("Configure either MCP_AUTH_TOKEN or MCP_OAUTH_ISSUER, not both.");
|
|
54
|
+
}
|
|
55
|
+
if (hasOAuthIntent) {
|
|
56
|
+
if (!oauthIssuer || !oauthAudience || !publicUrl || !oauthJwksUri || !oauthAllowedSubjects) {
|
|
57
|
+
throw new Error("MCP_OAUTH_ISSUER, MCP_OAUTH_AUDIENCE, MCP_OAUTH_JWKS_URI, MCP_PUBLIC_URL, and " +
|
|
58
|
+
"MCP_OAUTH_ALLOWED_SUBJECTS are all required for OAuth.");
|
|
59
|
+
}
|
|
60
|
+
const issuer = parseSecureUrl(oauthIssuer, "MCP_OAUTH_ISSUER", env.NODE_ENV);
|
|
61
|
+
const audience = parseSecureUrl(oauthAudience, "MCP_OAUTH_AUDIENCE", env.NODE_ENV);
|
|
62
|
+
const canonicalPublicUrl = parseSecureUrl(publicUrl, "MCP_PUBLIC_URL", env.NODE_ENV);
|
|
63
|
+
const jwksUri = parseSecureUrl(oauthJwksUri, "MCP_OAUTH_JWKS_URI", env.NODE_ENV);
|
|
64
|
+
if (issuer.search) {
|
|
65
|
+
throw new Error("MCP_OAUTH_ISSUER must not contain a query.");
|
|
66
|
+
}
|
|
67
|
+
if (canonicalPublicUrl.pathname !== "/" || canonicalPublicUrl.search || canonicalPublicUrl.hash) {
|
|
68
|
+
throw new Error("MCP_PUBLIC_URL must be an origin without a path, query, or fragment.");
|
|
69
|
+
}
|
|
70
|
+
if (audience.href !== `${canonicalPublicUrl.origin}/mcp`) {
|
|
71
|
+
throw new Error("MCP_OAUTH_AUDIENCE must exactly equal MCP_PUBLIC_URL plus /mcp.");
|
|
72
|
+
}
|
|
73
|
+
const requiredScopes = (oauthRequiredScopes ?? "premiere:mcp")
|
|
74
|
+
.split(/[ ,]+/)
|
|
75
|
+
.map((scope) => scope.trim())
|
|
76
|
+
.filter(Boolean);
|
|
77
|
+
if (requiredScopes.length === 0 || requiredScopes.some((scope) => !/^[\x21\x23-\x5B\x5D-\x7E]+$/.test(scope))) {
|
|
78
|
+
throw new Error("MCP_OAUTH_REQUIRED_SCOPES must contain one or more valid OAuth scope values.");
|
|
79
|
+
}
|
|
80
|
+
const allowedSubjects = oauthAllowedSubjects
|
|
81
|
+
.split(",")
|
|
82
|
+
.map((subject) => subject.trim())
|
|
83
|
+
.filter(Boolean);
|
|
84
|
+
if (allowedSubjects.length === 0 ||
|
|
85
|
+
allowedSubjects.some((subject) => subject.length > 255 || /[\u0000-\u001F\u007F]/.test(subject))) {
|
|
86
|
+
throw new Error("MCP_OAUTH_ALLOWED_SUBJECTS must contain valid comma-separated token subjects.");
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
mode: "oauth",
|
|
90
|
+
oauth: {
|
|
91
|
+
issuer: issuer.pathname === "/" && !issuer.search ? issuer.origin : issuer.href,
|
|
92
|
+
audience: audience.href,
|
|
93
|
+
publicUrl: canonicalPublicUrl.origin,
|
|
94
|
+
jwksUri: jwksUri.href,
|
|
95
|
+
requiredScopes: [...new Set(requiredScopes)],
|
|
96
|
+
allowedSubjects: [...new Set(allowedSubjects)],
|
|
97
|
+
},
|
|
98
|
+
allowUnauthenticated: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
44
101
|
if (authToken)
|
|
45
|
-
return { authToken, allowUnauthenticated: false };
|
|
102
|
+
return { mode: "shared-token", authToken, allowUnauthenticated: false };
|
|
46
103
|
if (env.ALLOW_UNAUTHENTICATED === "1" && env.NODE_ENV !== "production") {
|
|
47
|
-
return { allowUnauthenticated: true };
|
|
104
|
+
return { mode: "unauthenticated", allowUnauthenticated: true };
|
|
48
105
|
}
|
|
49
106
|
throw new Error("MCP_AUTH_TOKEN is required for the HTTP transport. " +
|
|
50
107
|
"ALLOW_UNAUTHENTICATED=1 is permitted only outside NODE_ENV=production.");
|
|
51
108
|
}
|
|
109
|
+
function parseSecureUrl(raw, name, nodeEnv) {
|
|
110
|
+
let parsed;
|
|
111
|
+
try {
|
|
112
|
+
parsed = new URL(raw);
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
throw new Error(`${name} must be an absolute URL.`);
|
|
116
|
+
}
|
|
117
|
+
const localDevelopment = nodeEnv !== "production" &&
|
|
118
|
+
parsed.protocol === "http:" &&
|
|
119
|
+
(parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1" || parsed.hostname === "[::1]");
|
|
120
|
+
if (parsed.protocol !== "https:" && !localDevelopment) {
|
|
121
|
+
throw new Error(`${name} must use HTTPS (HTTP is allowed only for loopback development).`);
|
|
122
|
+
}
|
|
123
|
+
if (parsed.username || parsed.password || parsed.hash) {
|
|
124
|
+
throw new Error(`${name} must not contain credentials or a fragment.`);
|
|
125
|
+
}
|
|
126
|
+
return parsed;
|
|
127
|
+
}
|
|
52
128
|
export function getRequestPathname(rawUrl) {
|
|
53
129
|
if (!rawUrl)
|
|
54
130
|
return undefined;
|
|
@@ -144,16 +220,17 @@ function timingSafeBufferEqual(left, right) {
|
|
|
144
220
|
return timingSafeEqual(left, right);
|
|
145
221
|
}
|
|
146
222
|
function hashedIdentity(value) {
|
|
147
|
-
|
|
223
|
+
// A process-local keyed digest prevents network addresses from being
|
|
224
|
+
// recovered through an offline dictionary attack if a bucket key
|
|
225
|
+
// is ever observed. The key and derived identities are never persisted.
|
|
226
|
+
return createHmac("sha256", RATE_LIMIT_IDENTITY_KEY).update(value).digest("hex").slice(0, 32);
|
|
148
227
|
}
|
|
149
228
|
/**
|
|
150
229
|
* The edge is authoritative by default. Honor X-Forwarded-For only after an
|
|
151
230
|
* operator explicitly declares the proxy trusted; otherwise it is attacker
|
|
152
231
|
* input and must not be used as a rate-limit identity.
|
|
153
232
|
*/
|
|
154
|
-
export function rateLimitIdentity(req,
|
|
155
|
-
if (authorizedCredential)
|
|
156
|
-
return `credential:${hashedIdentity(authorizedCredential)}`;
|
|
233
|
+
export function rateLimitIdentity(req, trustProxy) {
|
|
157
234
|
const forwarded = req.headers["x-forwarded-for"];
|
|
158
235
|
const forwardedValue = Array.isArray(forwarded) ? forwarded[0] : forwarded;
|
|
159
236
|
const remoteAddress = trustProxy && forwardedValue
|
package/dist/http-server.d.ts
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* MCP_AUTH_TOKEN Bearer token required on every /mcp request. REQUIRED — the
|
|
19
19
|
* server refuses to start without it, because this transport
|
|
20
20
|
* binds 0.0.0.0 and can drive Premiere.
|
|
21
|
+
* MCP_OAUTH_* Alternatively configure an OAuth issuer, JWKS URI,
|
|
22
|
+
* audience, public URL, and required scopes for per-user auth.
|
|
21
23
|
* MCP_MAX_REQUEST_BYTES, MCP_*_TIMEOUT_MS, MCP_RATE_LIMIT_* and
|
|
22
24
|
* MCP_MAX_CONCURRENT_REQUESTS bound public HTTP resource use. See README.
|
|
23
25
|
*/
|
package/dist/http-server.js
CHANGED
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
* MCP_AUTH_TOKEN Bearer token required on every /mcp request. REQUIRED — the
|
|
19
19
|
* server refuses to start without it, because this transport
|
|
20
20
|
* binds 0.0.0.0 and can drive Premiere.
|
|
21
|
+
* MCP_OAUTH_* Alternatively configure an OAuth issuer, JWKS URI,
|
|
22
|
+
* audience, public URL, and required scopes for per-user auth.
|
|
21
23
|
* MCP_MAX_REQUEST_BYTES, MCP_*_TIMEOUT_MS, MCP_RATE_LIMIT_* and
|
|
22
24
|
* MCP_MAX_CONCURRENT_REQUESTS bound public HTTP resource use. See README.
|
|
23
25
|
*/
|
|
@@ -32,6 +34,7 @@ import { createServer } from "./server.js";
|
|
|
32
34
|
import { cleanupTempDir, getTempDir } from "./bridge/file-bridge.js";
|
|
33
35
|
import { getTelemetry } from "./telemetry.js";
|
|
34
36
|
import { applyHttpSecurityHeaders } from "./http-security.js";
|
|
37
|
+
import { OAuthResourceServer } from "./oauth-resource-server.js";
|
|
35
38
|
import { HttpAdmissionController, MCP_HTTP_METHODS, exceedsRequestBodyLimit, getRequestPathname, isAuthorizedBearer, isSupportedMcpMethod, readBoundedRequestBody, rateLimitIdentity, readHttpAdmissionSettings, readHttpAuthConfiguration, RequestBodyTooLargeError, } from "./http-admission.js";
|
|
36
39
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
37
40
|
const LANDING_DIR = path.resolve(__dirname, "../landing-dist");
|
|
@@ -188,6 +191,8 @@ const bridgeOptions = {
|
|
|
188
191
|
process.env.PREMIERE_MCP_TRANSPORT = "http";
|
|
189
192
|
const telemetry = getTelemetry();
|
|
190
193
|
const admission = new HttpAdmissionController(admissionSettings);
|
|
194
|
+
const preAuthAdmission = new HttpAdmissionController(admissionSettings);
|
|
195
|
+
const oauthResourceServer = httpAuth.oauth ? new OAuthResourceServer(httpAuth.oauth) : undefined;
|
|
191
196
|
const mcpHandler = createMcpHandler(() => createServer(bridgeOptions, { telemetry }), {
|
|
192
197
|
onerror: (error) => console.error("[premiere-pro-mcp] MCP handler error:", error),
|
|
193
198
|
});
|
|
@@ -211,7 +216,18 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
211
216
|
// Health check
|
|
212
217
|
if (req.method === "GET" && pathname === "/health") {
|
|
213
218
|
res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
|
|
214
|
-
res.end(JSON.stringify({ status: "ok", service: "premiere-pro-mcp"
|
|
219
|
+
res.end(JSON.stringify({ status: "ok", service: "premiere-pro-mcp" }));
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
if (req.method === "GET" &&
|
|
223
|
+
(pathname === "/.well-known/oauth-protected-resource" ||
|
|
224
|
+
pathname === "/.well-known/oauth-protected-resource/mcp") &&
|
|
225
|
+
oauthResourceServer) {
|
|
226
|
+
res.writeHead(200, {
|
|
227
|
+
"Content-Type": "application/json",
|
|
228
|
+
"Cache-Control": "public, max-age=300",
|
|
229
|
+
});
|
|
230
|
+
res.end(JSON.stringify(oauthResourceServer.metadata()));
|
|
215
231
|
return;
|
|
216
232
|
}
|
|
217
233
|
// Only handle /mcp endpoint; everything else goes to the landing page
|
|
@@ -233,18 +249,61 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
233
249
|
res.end(JSON.stringify({ error: "Request body too large" }));
|
|
234
250
|
return;
|
|
235
251
|
}
|
|
236
|
-
//
|
|
237
|
-
//
|
|
238
|
-
|
|
252
|
+
// OAuth access tokens are verified for issuer, audience, lifetime, signature,
|
|
253
|
+
// subject, and scope. The legacy shared token remains available for controlled
|
|
254
|
+
// single-operator deployments and is compared in constant time.
|
|
255
|
+
// Apply an IP-keyed gate first so untrusted JWT/JWKS work cannot bypass the
|
|
256
|
+
// same concurrency and rate bounds that protect authenticated requests.
|
|
257
|
+
const preAuthDecision = preAuthAdmission.acquire(rateLimitIdentity(req, admissionSettings.trustProxy));
|
|
258
|
+
if (!preAuthDecision.accepted) {
|
|
259
|
+
telemetry.capture("mcp_request_rejected", {
|
|
260
|
+
outcome: preAuthDecision.reason,
|
|
261
|
+
status_code: preAuthDecision.statusCode,
|
|
262
|
+
phase: "pre_auth",
|
|
263
|
+
});
|
|
264
|
+
res.writeHead(preAuthDecision.statusCode, {
|
|
265
|
+
"Content-Type": "application/json",
|
|
266
|
+
"Cache-Control": "no-store",
|
|
267
|
+
"Retry-After": String(preAuthDecision.retryAfterSeconds),
|
|
268
|
+
});
|
|
269
|
+
res.end(JSON.stringify({ error: preAuthDecision.reason === "rate_limited" ? "Too many requests" : "Service busy" }));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
let oauthAuthentication;
|
|
273
|
+
try {
|
|
274
|
+
oauthAuthentication = oauthResourceServer
|
|
275
|
+
? await oauthResourceServer.authenticate(req)
|
|
276
|
+
: undefined;
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
preAuthDecision.release();
|
|
280
|
+
}
|
|
281
|
+
const isAuthorized = oauthAuthentication
|
|
282
|
+
? oauthAuthentication.authenticated
|
|
283
|
+
: isAuthorizedBearer(req, httpAuth.authToken);
|
|
284
|
+
if (!isAuthorized) {
|
|
239
285
|
telemetry.capture("mcp_connection_attempt", {
|
|
240
286
|
outcome: "unauthorized",
|
|
241
287
|
method: req.method ?? "unknown",
|
|
242
288
|
});
|
|
243
|
-
|
|
244
|
-
|
|
289
|
+
const challenge = oauthResourceServer
|
|
290
|
+
? oauthResourceServer.challenge(oauthAuthentication?.authenticated === false ? oauthAuthentication.error : undefined)
|
|
291
|
+
: "Bearer";
|
|
292
|
+
const statusCode = oauthAuthentication?.authenticated === false && oauthAuthentication.error === "insufficient_scope"
|
|
293
|
+
? 403
|
|
294
|
+
: 401;
|
|
295
|
+
res.writeHead(statusCode, {
|
|
296
|
+
"Content-Type": "application/json",
|
|
297
|
+
"Cache-Control": "no-store",
|
|
298
|
+
"WWW-Authenticate": challenge,
|
|
299
|
+
});
|
|
300
|
+
res.end(JSON.stringify({ error: statusCode === 403 ? "Forbidden" : "Unauthorized" }));
|
|
245
301
|
return;
|
|
246
302
|
}
|
|
247
|
-
const
|
|
303
|
+
const authenticatedIdentity = oauthAuthentication?.authenticated
|
|
304
|
+
? `oauth:${oauthAuthentication.principal.rateLimitKey}`
|
|
305
|
+
: "credential:shared-operator";
|
|
306
|
+
const admissionDecision = admission.acquire(authenticatedIdentity);
|
|
248
307
|
if (!admissionDecision.accepted) {
|
|
249
308
|
telemetry.capture("mcp_request_rejected", {
|
|
250
309
|
outcome: admissionDecision.reason,
|
|
@@ -323,7 +382,10 @@ httpServer.maxRequestsPerSocket = admissionSettings.maxRequestsPerSocket;
|
|
|
323
382
|
httpServer.listen(PORT, "0.0.0.0", () => {
|
|
324
383
|
console.error(`[premiere-pro-mcp] HTTP server listening on 0.0.0.0:${PORT}`);
|
|
325
384
|
console.error(`[premiere-pro-mcp] MCP endpoint: http://0.0.0.0:${PORT}/mcp`);
|
|
326
|
-
if (
|
|
385
|
+
if (oauthResourceServer) {
|
|
386
|
+
console.error(`[premiere-pro-mcp] Auth: OAuth bearer tokens required`);
|
|
387
|
+
}
|
|
388
|
+
else if (httpAuth.authToken) {
|
|
327
389
|
console.error(`[premiere-pro-mcp] Auth: Bearer token required`);
|
|
328
390
|
}
|
|
329
391
|
else {
|
package/dist/index.js
CHANGED
|
@@ -17,11 +17,16 @@ function debugLog(message) {
|
|
|
17
17
|
console.error(`[premiere-pro-mcp] ${message}`);
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
|
+
function isLoopbackPortInUse(error) {
|
|
21
|
+
return Boolean(error
|
|
22
|
+
&& typeof error === "object"
|
|
23
|
+
&& error.code === "EADDRINUSE");
|
|
24
|
+
}
|
|
20
25
|
// Handle CLI flags
|
|
21
26
|
const args = process.argv.slice(2);
|
|
22
27
|
if (args.includes("--help") || args.includes("-h")) {
|
|
23
28
|
console.log(`
|
|
24
|
-
premiere-pro-mcp — MCP server for Adobe Premiere Pro (
|
|
29
|
+
premiere-pro-mcp — MCP server for Adobe Premiere Pro (319 default-profile tools)
|
|
25
30
|
|
|
26
31
|
Usage:
|
|
27
32
|
premiere-pro-mcp Start the MCP server (stdio transport)
|
|
@@ -133,15 +138,23 @@ async function main() {
|
|
|
133
138
|
cleanupTempDir(bridgeOptions);
|
|
134
139
|
let uxpBridge;
|
|
135
140
|
if (process.env.PREMIERE_UXP_TOKEN) {
|
|
136
|
-
|
|
141
|
+
const bridge = new UxpWebSocketBridge({
|
|
137
142
|
token: process.env.PREMIERE_UXP_TOKEN,
|
|
138
143
|
port: process.env.PREMIERE_UXP_PORT
|
|
139
144
|
? parseInt(process.env.PREMIERE_UXP_PORT, 10)
|
|
140
145
|
: undefined,
|
|
141
146
|
});
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
147
|
+
try {
|
|
148
|
+
await bridge.start();
|
|
149
|
+
uxpBridge = bridge;
|
|
150
|
+
const address = bridge.address();
|
|
151
|
+
debugLog(`UXP bridge listening on ws://${address.host}:${address.port}${address.path}`);
|
|
152
|
+
}
|
|
153
|
+
catch (error) {
|
|
154
|
+
if (!isLoopbackPortInUse(error))
|
|
155
|
+
throw error;
|
|
156
|
+
console.error("[premiere-pro-mcp] UXP bridge unavailable because its loopback port is already in use; continuing with CEP-only tools.");
|
|
157
|
+
}
|
|
145
158
|
}
|
|
146
159
|
const serverHandle = serveStdio(() => createServer(bridgeOptions, { uxpBridge, telemetry }), {
|
|
147
160
|
onerror: (error) => console.error("[premiere-pro-mcp] MCP stdio error:", error),
|
|
@@ -3,6 +3,8 @@ export declare const PROJECT_INTAKE_REPORT_SCHEMA_VERSION = 1;
|
|
|
3
3
|
export declare const MAX_PROJECT_INTAKE_ITEMS = 2000;
|
|
4
4
|
export declare const MAX_PROJECT_INTAKE_RULES = 64;
|
|
5
5
|
export declare const MAX_PROJECT_INTAKE_FINDINGS = 12200;
|
|
6
|
+
export declare const FRAME_RATE_CANONICAL_SNAP_TOLERANCE_FPS = 0.005;
|
|
7
|
+
export declare const FRAME_RATE_MATCH_TOLERANCE_FPS = 0.05;
|
|
6
8
|
export type IntakeCertainty = "observed" | "unavailable" | "not_checked";
|
|
7
9
|
export type IntakeSeverity = "error" | "warning" | "info";
|
|
8
10
|
export type IntakeStatus = "ready" | "needs_attention" | "incomplete";
|
|
@@ -46,6 +48,8 @@ export interface ProjectIntakeItem {
|
|
|
46
48
|
offline?: boolean;
|
|
47
49
|
hasProxy?: boolean;
|
|
48
50
|
frameRate?: number;
|
|
51
|
+
/** Derived during snapshot validation; never accepted as caller authority. */
|
|
52
|
+
frameRateUnsupported?: true;
|
|
49
53
|
}
|
|
50
54
|
export interface ProjectIntakeSnapshot {
|
|
51
55
|
project: {
|