premiere-pro-mcp 1.14.2 → 1.14.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -6,6 +6,25 @@ 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.3] - 2026-08-29
10
+
11
+ ### Added
12
+
13
+ - Added an optional, fail-closed OAuth resource-server mode with RFC 9728
14
+ protected-resource metadata, remote JWKS verification, exact issuer and
15
+ audience validation, required scopes, and an explicit trusted-subject
16
+ allowlist for operator-managed HTTP deployments.
17
+
18
+ ### Security
19
+
20
+ - Added an IP-keyed admission gate before JWT verification and isolated
21
+ authenticated rate-limit identities behind random process-local keys.
22
+ - Made partial or mixed OAuth/shared-token configuration fail startup, kept the
23
+ shared token as an operator-only compatibility mode, and removed internal
24
+ admission counters from the public health response.
25
+ - Kept public desktop routing deliberately disabled: OAuth does not claim
26
+ user-to-device pairing or access to a user's local Premiere process.
27
+
9
28
  ## [1.14.2] - 2026-08-28
10
29
 
11
30
  ### Added
package/README.md CHANGED
@@ -33,21 +33,21 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that l
33
33
 
34
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.
35
35
 
36
- ### Latest release: 1.14.2
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.
49
-
50
- See the [v1.14.2 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.14.2)
36
+ ### Latest release: 1.14.3
37
+
38
+ - **Trusted-operator OAuth:** HTTP deployments can validate signed access
39
+ tokens against a pinned issuer and JWKS with exact audience, lifetime,
40
+ required-scope, and subject-allowlist checks.
41
+ - **MCP discovery:** OAuth mode publishes RFC 9728 protected-resource metadata
42
+ and returns interoperable `WWW-Authenticate` challenges.
43
+ - **Fail-closed admission:** untrusted requests are bounded before JWT work,
44
+ mixed authentication modes refuse startup, and authenticated rate-limit keys
45
+ are opaque and process-local.
46
+ - **Explicit boundary:** this release does not claim public desktop pairing or
47
+ route OAuth users to local Premiere processes; hosted use remains
48
+ operator-managed until that separate relay exists.
49
+
50
+ See the [v1.14.3 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.14.3)
51
51
  for complete details. Live installation in Premiere Pro still requires host verification.
52
52
 
53
53
  ### Current MCP protocol support
@@ -89,9 +89,9 @@ their bins, media rules, and organization rules before a facility uses one.
89
89
 
90
90
  ### Easiest supported path: Claude Desktop
91
91
 
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).
92
+ 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.14.3/premiere-pro-mcp-1.14.3.mcpb).
93
93
  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.
94
+ 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.14.3/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
95
  4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
96
96
  5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
97
97
 
@@ -338,11 +338,11 @@ From a clone of this repository:
338
338
  ```bash
339
339
  codex plugin marketplace add .
340
340
  codex plugin add premiere-pro@premiere-pro-mcp
341
- npx -y premiere-pro-mcp@1.14.2 --install-cep
341
+ npx -y premiere-pro-mcp@1.14.3 --install-cep
342
342
  ```
343
343
 
344
344
  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
345
+ launches `premiere-pro-mcp@1.14.3` through `npx`; the separate CEP installation is
346
346
  required because the MCP server communicates with the running Premiere host through
347
347
  the local bridge.
348
348
 
@@ -362,7 +362,7 @@ For Claude Code, add this repository as a marketplace and install the plugin:
362
362
  Then install the Premiere bridge and start a new Claude Code session:
363
363
 
364
364
  ```bash
365
- npx -y premiere-pro-mcp@1.14.2 --install-cep
365
+ npx -y premiere-pro-mcp@1.14.3 --install-cep
366
366
  ```
367
367
 
368
368
  The Claude Code package lives in
@@ -808,21 +808,28 @@ feature coverage, playback, rendering, or editorial correctness.
808
808
 
809
809
  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
810
 
811
- A live instance is running at **https://premiere-pro-mcp.fly.dev**.
811
+ A live operator-managed instance is running at **https://premiere-pro-mcp.fly.dev**.
812
+ It is not a public desktop relay: it cannot connect an authenticated user to
813
+ Premiere on that user's computer. Public users should use the local stdio setup
814
+ until the separate device-pairing relay is available.
812
815
 
813
- ### Connect via mcp-remote
816
+ ### Connect to an operator-managed instance
814
817
 
815
818
  ```json
816
819
  {
817
820
  "mcpServers": {
818
821
  "premiere-pro": {
819
822
  "command": "npx",
820
- "args": ["mcp-remote", "https://premiere-pro-mcp.fly.dev/mcp"]
823
+ "args": ["mcp-remote", "https://your-authorized-instance.example/mcp"]
821
824
  }
822
825
  }
823
826
  }
824
827
  ```
825
828
 
829
+ The instance must either provision an operator bearer token or use the OAuth
830
+ resource-server configuration below. The production endpoint intentionally
831
+ returns `401` to callers who have not been authorized.
832
+
826
833
  ### Self-host on Fly.io
827
834
 
828
835
  ```bash
@@ -849,6 +856,39 @@ Then connect with:
849
856
  }
850
857
  ```
851
858
 
859
+ ### Trusted-operator OAuth resource-server mode
860
+
861
+ For an identity-aware operator deployment, configure a real OAuth/OIDC authorization
862
+ server rather than distributing `MCP_AUTH_TOKEN`. The authorization server must
863
+ support the MCP client's registration model and issue signed access tokens with
864
+ an exact audience for this MCP resource.
865
+
866
+ ```bash
867
+ fly secrets set \
868
+ MCP_OAUTH_ISSUER=https://identity.example.com \
869
+ MCP_OAUTH_JWKS_URI=https://identity.example.com/.well-known/jwks.json \
870
+ MCP_OAUTH_AUDIENCE=https://your-app-name.fly.dev/mcp \
871
+ MCP_PUBLIC_URL=https://your-app-name.fly.dev \
872
+ MCP_OAUTH_REQUIRED_SCOPES=premiere:mcp \
873
+ MCP_OAUTH_ALLOWED_SUBJECTS=your-provider-user-subject
874
+ ```
875
+
876
+ OAuth mode validates the token signature, algorithm, issuer, exact audience,
877
+ expiry, issued-at time, subject, and required scopes. It publishes protected
878
+ resource metadata at `/.well-known/oauth-protected-resource/mcp` and includes
879
+ that URL in the `WWW-Authenticate` challenge. Configuration is fail-closed:
880
+ partial OAuth settings, non-HTTPS production URLs, ambiguous OAuth/shared-token
881
+ settings, and missing credentials all prevent startup.
882
+
883
+ `MCP_OAUTH_ALLOWED_SUBJECTS` is mandatory and restricts this single-bridge
884
+ deployment to explicitly trusted operator identities. This is an enforcement
885
+ boundary, not a public-user device model.
886
+
887
+ This mode authenticates trusted operators but does **not** yet implement device ownership,
888
+ desktop pairing, or per-user Premiere routing. Do not expose editor mutations as
889
+ a public multi-user service until an outbound desktop relay and durable
890
+ user/device authorization are implemented.
891
+
852
892
  > **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
893
  > `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
894
  > 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 +907,13 @@ Then connect with:
867
907
  | `PREMIERE_CONTEXT_BACKEND` | Local project-context store: `auto`, `sqlite`, `json`, or `memory` | `auto` |
868
908
  | `PREMIERE_CONTEXT_DIR` | Override the local project-context storage directory | OS application-data directory |
869
909
  | `PORT` | HTTP port (HTTP/SSE transport only) | `3000` |
870
- | `MCP_AUTH_TOKEN` | Bearer token required by the HTTP transport | unset |
910
+ | `MCP_AUTH_TOKEN` | Operator bearer token for controlled HTTP deployments; mutually exclusive with OAuth mode | unset |
911
+ | `MCP_OAUTH_ISSUER` | Exact trusted OAuth/OIDC token issuer URL | unset |
912
+ | `MCP_OAUTH_JWKS_URI` | HTTPS JWKS URL used to verify access-token signatures | unset |
913
+ | `MCP_OAUTH_AUDIENCE` | Exact MCP resource audience, normally the public `/mcp` URL | unset |
914
+ | `MCP_PUBLIC_URL` | Canonical HTTPS origin used in protected-resource discovery | unset |
915
+ | `MCP_OAUTH_REQUIRED_SCOPES` | Space- or comma-separated scopes required for `/mcp` | `premiere:mcp` |
916
+ | `MCP_OAUTH_ALLOWED_SUBJECTS` | Mandatory comma-separated token-subject allowlist for the single operator bridge | unset |
871
917
  | `ALLOW_UNAUTHENTICATED` | Set to `1` only for local/test HTTP harnesses; it is rejected when `NODE_ENV=production` | unset |
872
918
  | `MCP_MAX_REQUEST_BYTES` | Maximum HTTP MCP request body size | `1048576` |
873
919
  | `MCP_HEADERS_TIMEOUT_MS` | Maximum time to receive request headers | `10000` |
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.3" 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.3"/>
5
+ <Extension Id="com.mcp.premiere.bridge.headless" Version="1.14.3"/>
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.3</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.3";
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 {
@@ -0,0 +1,34 @@
1
+ import type http from "node:http";
2
+ import { type JWTPayload } from "jose";
3
+ import type { HttpAuthConfiguration } from "./http-admission.js";
4
+ export interface AuthenticatedPrincipal {
5
+ subject: string;
6
+ scopes: string[];
7
+ rateLimitKey: string;
8
+ }
9
+ export type AuthenticationResult = {
10
+ authenticated: true;
11
+ principal: AuthenticatedPrincipal;
12
+ } | {
13
+ authenticated: false;
14
+ error: "missing_token" | "invalid_token" | "insufficient_scope";
15
+ };
16
+ export interface OAuthResourceMetadata {
17
+ resource: string;
18
+ authorization_servers: string[];
19
+ bearer_methods_supported: ["header"];
20
+ scopes_supported: string[];
21
+ }
22
+ type JwtVerifier = (token: string) => Promise<JWTPayload>;
23
+ export declare class OAuthResourceServer {
24
+ private readonly configuration;
25
+ private readonly verifyToken;
26
+ private readonly rateLimitKeys;
27
+ constructor(configuration: NonNullable<HttpAuthConfiguration["oauth"]>, verifier?: JwtVerifier);
28
+ metadata(): OAuthResourceMetadata;
29
+ metadataUrl(): string;
30
+ challenge(error?: "missing_token" | "invalid_token" | "insufficient_scope"): string;
31
+ authenticate(req: Pick<http.IncomingMessage, "headers">): Promise<AuthenticationResult>;
32
+ }
33
+ export {};
34
+ //# sourceMappingURL=oauth-resource-server.d.ts.map
@@ -0,0 +1,91 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createRemoteJWKSet, errors, jwtVerify } from "jose";
3
+ function bearerToken(req) {
4
+ const header = req.headers.authorization;
5
+ const value = Array.isArray(header) ? header[0] : header;
6
+ if (!value?.startsWith("Bearer "))
7
+ return undefined;
8
+ const token = value.slice(7).trim();
9
+ return token && token.length <= 16_384 ? token : undefined;
10
+ }
11
+ function tokenScopes(payload) {
12
+ const scope = typeof payload.scope === "string" ? payload.scope.split(/\s+/) : [];
13
+ const scp = Array.isArray(payload.scp) ? payload.scp.filter((value) => typeof value === "string") : [];
14
+ return [...new Set([...scope, ...scp].filter(Boolean))];
15
+ }
16
+ export class OAuthResourceServer {
17
+ configuration;
18
+ verifyToken;
19
+ rateLimitKeys;
20
+ constructor(configuration, verifier) {
21
+ this.configuration = configuration;
22
+ this.rateLimitKeys = new Map(configuration.allowedSubjects.map((subject) => [subject, randomBytes(16).toString("hex")]));
23
+ if (verifier) {
24
+ this.verifyToken = verifier;
25
+ return;
26
+ }
27
+ const jwks = createRemoteJWKSet(new URL(configuration.jwksUri));
28
+ this.verifyToken = async (token) => {
29
+ const result = await jwtVerify(token, jwks, {
30
+ issuer: configuration.issuer,
31
+ audience: configuration.audience,
32
+ algorithms: ["RS256", "ES256", "EdDSA"],
33
+ clockTolerance: 5,
34
+ requiredClaims: ["sub", "exp", "iat"],
35
+ });
36
+ return result.payload;
37
+ };
38
+ }
39
+ metadata() {
40
+ return {
41
+ resource: this.configuration.audience,
42
+ authorization_servers: [this.configuration.issuer],
43
+ bearer_methods_supported: ["header"],
44
+ scopes_supported: this.configuration.requiredScopes,
45
+ };
46
+ }
47
+ metadataUrl() {
48
+ return `${this.configuration.publicUrl}/.well-known/oauth-protected-resource/mcp`;
49
+ }
50
+ challenge(error) {
51
+ const attributes = [
52
+ `resource_metadata="${this.metadataUrl()}"`,
53
+ `scope="${this.configuration.requiredScopes.join(" ")}"`,
54
+ ];
55
+ if (error && error !== "missing_token")
56
+ attributes.push(`error="${error}"`);
57
+ return `Bearer ${attributes.join(", ")}`;
58
+ }
59
+ async authenticate(req) {
60
+ const token = bearerToken(req);
61
+ if (!token)
62
+ return { authenticated: false, error: "missing_token" };
63
+ try {
64
+ const payload = await this.verifyToken(token);
65
+ if (!payload.sub || payload.sub.length > 255)
66
+ return { authenticated: false, error: "invalid_token" };
67
+ if (!this.configuration.allowedSubjects.includes(payload.sub)) {
68
+ return { authenticated: false, error: "invalid_token" };
69
+ }
70
+ const scopes = tokenScopes(payload);
71
+ if (!this.configuration.requiredScopes.every((scope) => scopes.includes(scope))) {
72
+ return { authenticated: false, error: "insufficient_scope" };
73
+ }
74
+ return {
75
+ authenticated: true,
76
+ principal: {
77
+ subject: payload.sub,
78
+ scopes,
79
+ rateLimitKey: this.rateLimitKeys.get(payload.sub),
80
+ },
81
+ };
82
+ }
83
+ catch (error) {
84
+ if (error instanceof errors.JOSEError)
85
+ return { authenticated: false, error: "invalid_token" };
86
+ // Remote JWKS/network failures must fail closed without disclosing provider details.
87
+ return { authenticated: false, error: "invalid_token" };
88
+ }
89
+ }
90
+ }
91
+ //# sourceMappingURL=oauth-resource-server.js.map
@@ -41,7 +41,8 @@ This repository targets the current Model Context Protocol revision, `2026-07-28
41
41
  | Surface | Status | Reason |
42
42
  | --- | --- | --- |
43
43
  | Tasks extension (`io.modelcontextprotocol/tasks`) | Not implemented | Current bridge calls are bounded request/response operations. Advertising durable tasks without durable, authorization-scoped storage, TTL cleanup, cancellation, and result recovery would be misleading. |
44
- | OAuth Client ID Metadata Documents and issuer validation | External authorization boundary | The hosted endpoint currently uses a fail-closed operator bearer token. It does not operate an OAuth authorization server and therefore does not advertise OAuth discovery it cannot complete. |
44
+ | OAuth resource-server authorization | Implemented for trusted operators, externally provisioned | HTTP validates signed access tokens by exact issuer, canonical audience, lifetime, allowlisted subject, and scope and publishes RFC 9728 protected-resource metadata. A separately configured authorization server owns login, consent, token issuance, and MCP client registration; this repository does not issue tokens or route public users to their own desktops. |
45
+ | OAuth Client ID Metadata Documents | Authorization-server responsibility | The resource server advertises its configured authorization server. That external service must support the registration mechanism required by the connecting MCP clients; this repository does not claim to operate CIMD or client registration. |
45
46
  | Enterprise Managed Authorization | Not implemented | No enterprise identity-policy provider is configured in this repository. |
46
47
  | MCP Apps | Not implemented | Premiere UI is delivered through CEP/UXP, not an MCP App resource. |
47
48
  | Skills over MCP | Experimental, not advertised | The repository ships client-specific local skills, but the Skills over MCP working group is still defining interoperable discovery and distribution. Local skill packaging is not claimed as protocol support. |
@@ -67,9 +68,10 @@ the official changelog:
67
68
  - **Extensions:** a declared Premiere extension plus explicit non-advertisement of
68
69
  Tasks, MCP Apps, enterprise authorization, and experimental Skills/Server Card
69
70
  surfaces that the product does not safely implement.
70
- - **Authorization and deprecations:** bearer-token authorization remains a documented
71
- external boundary; issuer/CIMD requirements apply when an OAuth authorization
72
- flow is added; new Roots, Sampling, Logging, DCR, or HTTP+SSE dependencies are not
71
+ - **Authorization and deprecations:** HTTP supports either controlled operator-token
72
+ authentication or fail-closed OAuth resource-server validation and RFC 9728
73
+ discovery; login, consent, token issuance, and CIMD remain the configured external
74
+ authorization server's responsibility; new Roots, Sampling, Logging, DCR, or HTTP+SSE dependencies are not
73
75
  introduced.
74
76
 
75
77
  ## Product capability surface
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "premiere-pro-mcp",
3
3
  "mcpName": "io.github.leancoderkavy/premiere-pro",
4
- "version": "1.14.2",
4
+ "version": "1.14.3",
5
5
  "description": "MCP server for Adobe Premiere Pro with 320 core AI video editing tools and a capability-aware UXP bridge for supported Premiere workflows.",
6
6
  "main": "dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -103,17 +103,18 @@
103
103
  "node": ">=20.19.0"
104
104
  },
105
105
  "dependencies": {
106
+ "@modelcontextprotocol/node": "^2.0.0",
107
+ "@modelcontextprotocol/server": "^2.0.0",
108
+ "jose": "^6.2.10",
106
109
  "posthog-node": "^5.51.1",
107
110
  "ws": "^8.21.1",
108
- "zod": "^4.4.3",
109
- "@modelcontextprotocol/node": "^2.0.0",
110
- "@modelcontextprotocol/server": "^2.0.0"
111
+ "zod": "^4.4.3"
111
112
  },
112
113
  "devDependencies": {
113
114
  "@adobe/eslint-plugin-premierepro": "26.3.0",
114
115
  "@adobe/premierepro": "26.3.0",
115
- "@types/node": "^26.3.0",
116
116
  "@modelcontextprotocol/client": "^2.0.0",
117
+ "@types/node": "^26.3.0",
117
118
  "@types/ws": "^8.18.1",
118
119
  "@typescript-eslint/parser": "^8.68.0",
119
120
  "@vitest/coverage-v8": "^4.1.11",
@@ -104,10 +104,12 @@ export async function validateClaudeStage(stage) {
104
104
  path.join(stage, "server", "dist", "index.js"),
105
105
  "Claude bundle stage is missing server/dist/index.js; run the TypeScript build first",
106
106
  );
107
- await assertFile(
108
- path.join(stage, "node_modules", "@modelcontextprotocol", "sdk", "package.json"),
109
- "Claude bundle stage is missing production dependencies",
110
- );
107
+ for (const dependency of Object.keys(packageJson.dependencies ?? {})) {
108
+ await assertFile(
109
+ path.join(stage, "node_modules", dependency, "package.json"),
110
+ `Claude bundle stage is missing production dependency ${dependency}`,
111
+ );
112
+ }
111
113
  return { manifest, packageJson };
112
114
  }
113
115
 
@@ -2,7 +2,7 @@
2
2
  "manifestVersion": 5,
3
3
  "id": "com.ppmcp.premiere.uxp",
4
4
  "name": "MCP for Adobe Premiere Pro",
5
- "version": "1.14.2",
5
+ "version": "1.14.3",
6
6
  "main": "index.html",
7
7
  "host": { "app": "premierepro", "minVersion": "25.6.0" },
8
8
  "entrypoints": [