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.
Files changed (42) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/README.md +72 -27
  3. package/artifacts/MCPBridgeCEP.zxp +0 -0
  4. package/cep-plugin/CSXS/manifest.xml +3 -3
  5. package/cep-plugin/index.html +1 -1
  6. package/cep-plugin/updater.cjs +1 -1
  7. package/dist/http-admission.d.ts +10 -1
  8. package/dist/http-admission.js +84 -7
  9. package/dist/http-server.d.ts +2 -0
  10. package/dist/http-server.js +70 -8
  11. package/dist/index.js +18 -5
  12. package/dist/intake/project-intake.d.ts +4 -0
  13. package/dist/intake/project-intake.js +38 -6
  14. package/dist/oauth-resource-server.d.ts +34 -0
  15. package/dist/oauth-resource-server.js +91 -0
  16. package/dist/resources/extendscript-reference.js +1 -1
  17. package/dist/tools/audio.js +20 -7
  18. package/dist/tools/captions.js +33 -4
  19. package/dist/tools/clipboard.js +101 -35
  20. package/dist/tools/effects.js +54 -33
  21. package/dist/tools/export.d.ts +42 -2
  22. package/dist/tools/export.js +124 -17
  23. package/dist/tools/health.d.ts +43 -3
  24. package/dist/tools/health.js +61 -6
  25. package/dist/tools/inspection.d.ts +53 -2
  26. package/dist/tools/inspection.js +71 -13
  27. package/dist/tools/metadata.d.ts +10 -0
  28. package/dist/tools/metadata.js +15 -5
  29. package/dist/tools/playback.js +19 -6
  30. package/dist/tools/project-manager.js +24 -7
  31. package/dist/tools/sequence.js +21 -9
  32. package/dist/tools/timeline.js +14 -1
  33. package/dist/tools/track-targeting.d.ts +4 -1
  34. package/dist/tools/track-targeting.js +50 -28
  35. package/dist/tools/transitions.js +12 -3
  36. package/dist/workflows/tool-packs.js +1 -0
  37. package/docs/mcp-2026-07-28-capabilities.md +6 -4
  38. package/docs/supported-actions.md +21 -20
  39. package/package.json +7 -6
  40. package/scripts/validate-distribution.mjs +6 -4
  41. package/uxp-plugin/commands.cjs +13 -3
  42. 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
- 320 core tools across 37 modules, 14 resources, and 11 guided workflows. A connected UXP host adds 54 capability-gated tools.
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: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
14
14
  [![Node.js](https://img.shields.io/badge/Node.js-20.19%2B-green.svg)](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 320 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.
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.2
36
+ ### Latest release: 1.14.4
37
37
 
38
- - **Focused discovery:** compatible MCP clients can select essential,
39
- inspection, delivery, or captions tool packs instead of beginning with the
40
- full catalog.
41
- - **Review handoff:** `inspect_sequence_review_report` returns a read-only,
42
- path-redacted sequence report, with marker comments only when requested.
43
- - **Interoperability:** all registered MCP tools now declare explicit output
44
- schemas; this release does not substitute automated checks for licensed-host
45
- verification.
46
- - **Reliable packaging:** npm package verification now isolates its temporary
47
- tarball, keeping the validated distribution path compatible with the current
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.2 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.14.2)
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.2/premiere-pro-mcp-1.14.2.mcpb).
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.2/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.
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.2 --install-cep
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.2` through `npx`; the separate CEP installation is
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.2 --install-cep
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 318 of the 320 registered
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 (320 core total; 318 under the default profile; 372 with a connected UXP bridge)
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 via mcp-remote
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://premiere-pro-mcp.fly.dev/mcp"]
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` | Bearer token required by the HTTP transport | unset |
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 320 tools, filtered by authority profile
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" ExtensionBundleName="MCP for Adobe Premiere Pro">
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.2"/>
5
- <Extension Id="com.mcp.premiere.bridge.headless" Version="1.14.2"/>
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>
@@ -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.2</strong>
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>
@@ -7,7 +7,7 @@
7
7
  })(this, function () {
8
8
  "use strict";
9
9
 
10
- var CURRENT_VERSION = "1.14.2";
10
+ var CURRENT_VERSION = "1.14.4";
11
11
  var LATEST_RELEASE_API =
12
12
  "https://api.github.com/repos/leancoderkavy/premiere-pro-mcp/releases/latest";
13
13
  var RELEASES_URL =
@@ -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">, authorizedCredential: string | undefined, trustProxy: boolean): string;
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
@@ -1,6 +1,7 @@
1
- import { createHash, timingSafeEqual } from "node:crypto";
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
- return createHash("sha256").update(value).digest("hex").slice(0, 32);
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, authorizedCredential, trustProxy) {
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
@@ -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
  */
@@ -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", admission: admission.metrics() }));
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
- // Bearer token auth is fail-closed in production. The comparison is constant
237
- // time for equal-length credentials and never records the provided header.
238
- if (!isAuthorizedBearer(req, httpAuth.authToken)) {
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
- res.writeHead(401, { "Content-Type": "application/json" });
244
- res.end(JSON.stringify({ error: "Unauthorized" }));
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 admissionDecision = admission.acquire(rateLimitIdentity(req, httpAuth.authToken, admissionSettings.trustProxy));
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 (httpAuth.authToken) {
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 (318 default-profile tools)
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
- uxpBridge = new UxpWebSocketBridge({
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
- await uxpBridge.start();
143
- const address = uxpBridge.address();
144
- debugLog(`UXP bridge listening on ws://${address.host}:${address.port}${address.path}`);
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: {