premiere-pro-mcp 1.18.5 → 1.18.6

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,21 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.18.6] - 2026-09-29
10
+
11
+ ### Fixed
12
+
13
+ - `set_scale_width_height` never set the height. It wrote to a "Scale Height" property that Premiere's Motion effect does not have, then reported success. With Uniform Scale off, Premiere keeps the height in Motion > Scale. The tool now writes the width to Scale Width and the height to Scale, reads all three values back, fails when any does not match, and rejects values outside 0-10000 before building a script. (#642)
14
+
15
+ ### Changed
16
+
17
+ - The website moved to its own repository, [leancoderkavy/premiere-pro-mcp-site](https://github.com/leancoderkavy/premiere-pro-mcp-site), and is served at `https://premiere-pro-mcp.com`. It reads versions and tool counts from the published npm package and syncs them on its own, so releases here no longer update website files.
18
+ - The HTTP transport no longer serves website pages, the homepage experiment, or `/api/landing-events`. It answers `/mcp`, `/health`, and OAuth protected-resource metadata only. Other `GET` and `HEAD` paths on `premiere-pro-mcp.fly.dev` or a `*.premiere-pro-mcp.com` host redirect (`308`) to the same path on `https://premiere-pro-mcp.com`; any other host gets a JSON `404`. Its Content Security Policy is now deny-all, `/health` reports the running package `version`, and the `MCP_MAX_CONCURRENT_LANDING_DOCUMENTS`, `MCP_LANDING_MAX_HTML_BYTES`, and `MCP_LANDING_HTML_CACHE_BYTES` settings are gone. The Docker image no longer builds the site.
19
+
20
+ ### Verification scope
21
+
22
+ - Automated checks validate package behavior. The `set_scale_width_height` fix follows a live report from Premiere 26.5.1 but has not been re-verified on a licensed Premiere host. The website move was verified live: premiere-pro-mcp.com serves from Vercel, and the Fly deployment answers `/health` and redirects other paths to the website.
23
+
9
24
  ## [1.18.5] - 2026-09-28
10
25
 
11
26
  ### Fixed
package/README.md CHANGED
@@ -22,7 +22,7 @@ The [completed AE render handoff](docs/after-effects-render-handoff.md) previews
22
22
  [![Node.js](https://img.shields.io/badge/Node.js-20.19%2B-green.svg)](https://nodejs.org)
23
23
  [![MCP](https://img.shields.io/badge/MCP-2026--07--28-purple.svg)](https://modelcontextprotocol.io/specification/2026-07-28)
24
24
  [![npm](https://img.shields.io/npm/v/premiere-pro-mcp.svg)](https://www.npmjs.com/package/premiere-pro-mcp)
25
- [![Fly.io](https://img.shields.io/badge/Fly.io-deployed-7C3AED.svg)](https://premiere-pro-mcp.fly.dev)
25
+ [![Website](https://img.shields.io/badge/website-premiere--pro--mcp.com-7C3AED.svg)](https://premiere-pro-mcp.com/)
26
26
  [![Premiere Pro](https://img.shields.io/badge/Premiere%20Pro-2020--2026-9999FF.svg)](https://www.adobe.com/products/premiere.html)
27
27
 
28
28
  [![npm downloads](https://img.shields.io/npm/dm/premiere-pro-mcp.svg?label=npm%20downloads)](https://www.npmjs.com/package/premiere-pro-mcp)
@@ -36,7 +36,7 @@ The [completed AE render handoff](docs/after-effects-render-handoff.md) previews
36
36
 
37
37
  ---
38
38
 
39
- ![MCP for Adobe Premiere Pro turns a structured AI request into an organized local editing workflow](landing/public/marketing/premiere-pro-mcp-campaign-hero-v1.png)
39
+ ![MCP for Adobe Premiere Pro turns a structured AI request into an organized local editing workflow](https://premiere-pro-mcp.com/marketing/premiere-pro-mcp-campaign-hero-v1.png)
40
40
 
41
41
  <details>
42
42
  <summary><strong>Table of contents</strong></summary>
@@ -110,9 +110,9 @@ The current source exposes 384 core tools for supported workflow steps spanning
110
110
 
111
111
  <a id="latest-release"></a>
112
112
 
113
- ### Latest release: 1.18.4
113
+ ### Latest release: 1.18.5
114
114
 
115
- The published v1.18.4 npm artifact contains **384 core tools**, 382 in its default profile,
115
+ The published v1.18.5 npm artifact contains **384 core tools**, 382 in its default profile,
116
116
  and 478 with a compatible UXP connection. The development catalog above can include
117
117
  unreleased work. See the [versioned facts and package provenance](https://premiere-pro-mcp.com/facts/).
118
118
 
@@ -154,7 +154,7 @@ if the connection is unavailable.
154
154
  manifest, and proof runbook make workflow and verification boundaries
155
155
  inspectable without claiming a licensed-host walkthrough occurred.
156
156
  - **Faster, clearer delivery:** immutable MCP registration work is reused
157
- safely across stateless requests, and the landing now has a lighter,
157
+ safely across stateless requests, and the website gained a lighter,
158
158
  mobile-first workflow view plus current machine-readable facts and crawl
159
159
  guidance for its public pages.
160
160
  - **Explicit boundary:** the hosted endpoint remains an operator-managed MCP
@@ -162,7 +162,7 @@ if the connection is unavailable.
162
162
  local Premiere processes. See the generated [supported action catalog](docs/supported-actions.md)
163
163
  for individual capability and verification contracts.
164
164
 
165
- See the [v1.18.4 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.18.4)
165
+ See the [v1.18.5 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.18.5)
166
166
  for complete details. Live installation in Premiere Pro still requires host verification.
167
167
 
168
168
  ### Current MCP protocol support
@@ -210,7 +210,7 @@ their bins, media rules, and organization rules before a facility uses one.
210
210
  > ### Install the published package (verify the name)
211
211
  >
212
212
  > ```bash
213
- > npm i -g premiere-pro-mcp@1.18.4
213
+ > npm i -g premiere-pro-mcp@1.18.5
214
214
  > ```
215
215
  >
216
216
  > This repository publishes only **`premiere-pro-mcp`**. A differently named package (`adobe-premiere-pro-mcp`) may also declare a `premiere-pro-mcp` executable. Before configuring a client, confirm:
@@ -218,7 +218,7 @@ their bins, media rules, and organization rules before a facility uses one.
218
218
  > | Check | Expected |
219
219
  > | --- | --- |
220
220
  > | Package name | `premiere-pro-mcp` (not `adobe-premiere-pro-mcp`) |
221
- > | Version | `1.18.4` |
221
+ > | Version | `1.18.5` |
222
222
  > | Homepage / repo | https://premiere-pro-mcp.com/ · https://github.com/leancoderkavy/premiere-pro-mcp |
223
223
  >
224
224
  > ```bash
@@ -231,9 +231,9 @@ their bins, media rules, and organization rules before a facility uses one.
231
231
 
232
232
  ### Easiest supported path: Claude Desktop
233
233
 
234
- 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.4/premiere-pro-mcp-1.18.4.mcpb).
234
+ 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.5/premiere-pro-mcp-1.18.5.mcpb).
235
235
  2. In Claude Desktop, open **Settings > Extensions > Advanced settings > Install Extension**, select the downloaded bundle, and restart Claude Desktop.
236
- 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.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.
236
+ 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.18.5/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.
237
237
  4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
238
238
  5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
239
239
 
@@ -241,7 +241,7 @@ The Claude bundle contains the local MCP server, so this route does not require
241
241
 
242
242
  ### First proof, before the first edit
243
243
 
244
- ![Illustrated local Premiere MCP workflow](landing/public/premiere-pro-mcp-demo-poster.png)
244
+ ![Illustrated local Premiere MCP workflow](https://premiere-pro-mcp.com/premiere-pro-mcp-demo-poster.png)
245
245
 
246
246
  *This is an illustrated workflow, not a Premiere panel screenshot or licensed-host proof.*
247
247
 
@@ -533,11 +533,11 @@ From a clone of this repository:
533
533
  ```bash
534
534
  codex plugin marketplace add .
535
535
  codex plugin add premiere-pro@premiere-pro-mcp
536
- npx -y premiere-pro-mcp@1.18.4 --install-cep
536
+ npx -y premiere-pro-mcp@1.18.5 --install-cep
537
537
  ```
538
538
 
539
539
  Restart Premiere Pro and start a new Codex session after installation. The plugin
540
- launches `premiere-pro-mcp@1.18.4` through `npx`; the separate CEP installation is
540
+ launches `premiere-pro-mcp@1.18.5` through `npx`; the separate CEP installation is
541
541
  required because the MCP server communicates with the running Premiere host through
542
542
  the local bridge.
543
543
 
@@ -575,7 +575,7 @@ For Claude Code, add this repository as a marketplace and install the plugin:
575
575
  Then install the Premiere bridge and start a new Claude Code session:
576
576
 
577
577
  ```bash
578
- npx -y premiere-pro-mcp@1.18.4 --install-cep
578
+ npx -y premiere-pro-mcp@1.18.5 --install-cep
579
579
  ```
580
580
 
581
581
  The Claude Code package lives in
@@ -843,7 +843,7 @@ argument, undo, confirmation, and live-host boundaries.
843
843
 
844
844
  ## Architecture
845
845
 
846
- ![Local-first MCP for Adobe Premiere Pro workflow from AI assistant through the MCP bridge to a verified Premiere result](landing/public/marketing/premiere-pro-mcp-workflow-v1.png)
846
+ ![Local-first MCP for Adobe Premiere Pro workflow from AI assistant through the MCP bridge to a verified Premiere result](https://premiere-pro-mcp.com/marketing/premiere-pro-mcp-workflow-v1.png)
847
847
 
848
848
  **Local (stdio):**
849
849
 
@@ -1235,8 +1235,10 @@ feature coverage, playback, rendering, or editorial correctness.
1235
1235
 
1236
1236
  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.
1237
1237
 
1238
- A live operator-managed instance is running at **https://premiere-pro-mcp.fly.dev**.
1239
- It is not a public desktop relay: it cannot connect an authenticated user to
1238
+ An operator-managed MCP endpoint runs at **https://premiere-pro-mcp.fly.dev/mcp**
1239
+ (health check: `/health`). The website is **https://premiere-pro-mcp.com**, served
1240
+ from a separate repository; other paths on the Fly host redirect there.
1241
+ The endpoint is not a public desktop relay: it cannot connect an authenticated user to
1240
1242
  Premiere on that user's computer. Public users should use the local stdio setup
1241
1243
  until the separate device-pairing relay is available.
1242
1244
 
@@ -1349,9 +1351,6 @@ user/device authorization are implemented.
1349
1351
  | `MCP_MAX_REQUESTS_PER_SOCKET` | Requests permitted on one keep-alive socket | `100` |
1350
1352
  | `MCP_MAX_CONCURRENT_REQUESTS` | In-flight authenticated MCP request ceiling | `8` |
1351
1353
  | `MCP_MAX_CONCURRENT_STREAMS` | Open authenticated SSE stream ceiling; isolated from operation capacity | `32` |
1352
- | `MCP_MAX_CONCURRENT_LANDING_DOCUMENTS` | Concurrent HTML reads, nonce injection and asynchronous compression; excess work returns `503` | `4` |
1353
- | `MCP_LANDING_MAX_HTML_BYTES` | Maximum source HTML document bytes | `4194304` (4 MiB) |
1354
- | `MCP_LANDING_HTML_CACHE_BYTES` | Source HTML cache memory budget; must be at least the document byte limit | `16777216` (16 MiB) |
1355
1354
  | `PREMIERE_MCP_PROJECT_BACKUP_MAX_BYTES` | Positive integer byte budget for one project backup | `2147483648` (2 GiB) |
1356
1355
  | `MCP_RATE_LIMIT_PER_MINUTE` | Per-credential token-bucket refill rate | `120` |
1357
1356
  | `MCP_RATE_LIMIT_BURST` | Per-credential short burst allowance | `30` |
@@ -1469,10 +1468,10 @@ by default. Enable them only by setting
1469
1468
  body/socket/request limits, and applies a bounded in-process per-credential rate and concurrency limit before
1470
1469
  MCP request parsing or Premiere bridge work begins. It returns `413`, `429`, or `503` on containment failures. Configure an
1471
1470
  upstream rate limit and request-size limit too; process-local counters do not protect a multi-machine deployment.
1472
- - **The landing CSP uses a per-response nonce for scripts**, and its static assets use explicit cache policies.
1473
- Keep the server in front of the exported landing so those controls are not bypassed by a separate static host.
1474
- Source HTML is cached as immutable build output; restart the HTTP process after replacing
1475
- an export. Each response still gets a fresh nonce. HEAD requests skip body rendering.
1471
+ - **The HTTP transport serves no web pages.** It answers only `/mcp`, `/health`, and OAuth
1472
+ protected-resource metadata under a deny-all Content Security Policy. Other `GET`/`HEAD` paths on
1473
+ `premiere-pro-mcp.fly.dev` or a `*.premiere-pro-mcp.com` host redirect (`308`) to
1474
+ `https://premiere-pro-mcp.com`; every other host receives a JSON `404`.
1476
1475
  - **Media scans yield between asynchronous filesystem operations** and bound total traversal:
1477
1476
  25,000 entries, 5,000 matching files, 2,000 directories, depth 32, a 1,000-directory
1478
1477
  pending queue, and a cooperative five-second budget. A stalled OS call can exceed that
@@ -1,7 +1,7 @@
1
1
  <?xml version="1.0" encoding="UTF-8"?>
2
- <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.18.5" ExtensionBundleName="MCP for Adobe After Effects">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.aftereffects.bridge" ExtensionBundleVersion="1.18.6" ExtensionBundleName="MCP for Adobe After Effects">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.18.5"/>
4
+ <Extension Id="com.mcp.aftereffects.bridge.panel" Version="1.18.6"/>
5
5
  </ExtensionList>
6
6
  <ExecutionEnvironment>
7
7
  <HostList>
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.18.5" ExtensionBundleName="MCP for Adobe Premiere Pro">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.18.6" ExtensionBundleName="MCP for Adobe Premiere Pro">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.premiere.bridge.panel" Version="1.18.5"/>
5
- <Extension Id="com.mcp.premiere.bridge.headless" Version="1.18.5"/>
4
+ <Extension Id="com.mcp.premiere.bridge.panel" Version="1.18.6"/>
5
+ <Extension Id="com.mcp.premiere.bridge.headless" Version="1.18.6"/>
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">MCP updates</span>
84
- <strong id="updateTitle">Version 1.18.5</strong>
84
+ <strong id="updateTitle">Version 1.18.6</strong>
85
85
  <span id="updateDetail">Checking the global MCP server and connector release…</span>
86
86
  </div>
87
87
  <button id="btnUpdate" class="button button-update" onclick="handleUpdateClick()" type="button" aria-describedby="updateDetail" disabled>
@@ -7,7 +7,7 @@
7
7
  })(this, function () {
8
8
  "use strict";
9
9
 
10
- var CURRENT_VERSION = "1.18.5";
10
+ var CURRENT_VERSION = "1.18.6";
11
11
  var PACKAGE_NAME = "premiere-pro-mcp";
12
12
  var LATEST_PACKAGE_API = "https://registry.npmjs.org/" + PACKAGE_NAME;
13
13
  var LATEST_RELEASE_API =
@@ -1,8 +1,9 @@
1
1
  import type http from "node:http";
2
- export interface HttpSecurityHeaderOptions {
3
- scriptNonce?: string;
4
- }
5
- export declare function buildContentSecurityPolicy(options?: HttpSecurityHeaderOptions): string;
2
+ /**
3
+ * The HTTP transport serves JSON, redirects, and MCP streams only. It never
4
+ * returns an HTML document, so the policy denies every fetch, frame, and form.
5
+ */
6
+ export declare function buildContentSecurityPolicy(): string;
6
7
  export declare const HTTP_SECURITY_HEADERS: Readonly<{
7
8
  "Content-Security-Policy": string;
8
9
  "Strict-Transport-Security": "max-age=31536000; includeSubDomains";
@@ -12,5 +13,5 @@ export declare const HTTP_SECURITY_HEADERS: Readonly<{
12
13
  "Permissions-Policy": "camera=(), microphone=(), geolocation=()";
13
14
  "Cross-Origin-Opener-Policy": "same-origin";
14
15
  }>;
15
- export declare function applyHttpSecurityHeaders(res: http.ServerResponse, options?: HttpSecurityHeaderOptions): void;
16
+ export declare function applyHttpSecurityHeaders(res: http.ServerResponse): void;
16
17
  //# sourceMappingURL=http-security.d.ts.map
@@ -1,24 +1,13 @@
1
- export function buildContentSecurityPolicy(options = {}) {
2
- const scriptSource = [
3
- "'self'",
4
- ...(options.scriptNonce ? [`'nonce-${options.scriptNonce}'`] : []),
5
- "https://www.googletagmanager.com",
6
- "https://us-assets.i.posthog.com",
7
- "https://eu-assets.i.posthog.com",
8
- ].join(" ");
1
+ /**
2
+ * The HTTP transport serves JSON, redirects, and MCP streams only. It never
3
+ * returns an HTML document, so the policy denies every fetch, frame, and form.
4
+ */
5
+ export function buildContentSecurityPolicy() {
9
6
  return [
10
- "default-src 'self'",
11
- "base-uri 'self'",
7
+ "default-src 'none'",
8
+ "base-uri 'none'",
12
9
  "frame-ancestors 'none'",
13
- "object-src 'none'",
14
- "form-action 'self'",
15
- "img-src 'self' data: https:",
16
- "media-src 'self'",
17
- "font-src 'self'",
18
- "style-src 'self' 'unsafe-inline'",
19
- `script-src ${scriptSource}`,
20
- "connect-src 'self' https://www.google.com https://www.google-analytics.com https://www.googletagmanager.com https://us.i.posthog.com https://eu.i.posthog.com https://*.posthog.com",
21
- "upgrade-insecure-requests",
10
+ "form-action 'none'",
22
11
  ].join("; ");
23
12
  }
24
13
  export const HTTP_SECURITY_HEADERS = Object.freeze({
@@ -30,12 +19,8 @@ export const HTTP_SECURITY_HEADERS = Object.freeze({
30
19
  "Permissions-Policy": "camera=(), microphone=(), geolocation=()",
31
20
  "Cross-Origin-Opener-Policy": "same-origin",
32
21
  });
33
- export function applyHttpSecurityHeaders(res, options = {}) {
34
- const headers = {
35
- ...HTTP_SECURITY_HEADERS,
36
- "Content-Security-Policy": buildContentSecurityPolicy(options),
37
- };
38
- for (const [name, value] of Object.entries(headers)) {
22
+ export function applyHttpSecurityHeaders(res) {
23
+ for (const [name, value] of Object.entries(HTTP_SECURITY_HEADERS)) {
39
24
  res.setHeader(name, value);
40
25
  }
41
26
  }
@@ -26,253 +26,43 @@
26
26
  * HTTP resource use. See README.
27
27
  */
28
28
  import http from "node:http";
29
- import fs from "node:fs";
30
- import path from "node:path";
31
- import { randomBytes } from "node:crypto";
32
- import { createGzip } from "node:zlib";
33
- import { fileURLToPath } from "node:url";
34
29
  import { toNodeHandler } from "@modelcontextprotocol/node";
35
30
  import { createMcpHandler } from "@modelcontextprotocol/server";
36
- import { createServer } from "./server.js";
31
+ import { createServer, SERVER_VERSION } from "./server.js";
37
32
  import { cleanupTempDir, getTempDir } from "./bridge/file-bridge.js";
38
33
  import { getTelemetry, telemetryErrorType } from "./telemetry.js";
39
- import { createHomepageExperiment, injectFirstPaintExposure } from "./homepage-experiment.js";
40
- import { shouldGzipLanding } from "./landing-compression.js";
41
- import { LandingDocumentRenderer, assertLandingDocumentSize, readBoundedUtf8, readLandingDocumentSettings, } from "./landing-documents.js";
42
34
  import { applyHttpSecurityHeaders } from "./http-security.js";
43
35
  import { OAuthResourceServer } from "./oauth-resource-server.js";
44
36
  import { ProjectContextRepository } from "./context/project-context-store.js";
45
37
  import { MediaWatchRegistry } from "./tools/media-watch.js";
46
38
  import { HttpAdmissionController, MCP_HTTP_METHODS, exceedsRequestBodyLimit, getRequestPathname, isAuthorizedBearer, isSupportedMcpMethod, readBoundedRequestBody, rateLimitIdentity, readHttpAdmissionSettings, readHttpAuthConfiguration, RequestBodyTooLargeError, } from "./http-admission.js";
47
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
48
- const LANDING_DIR = path.resolve(__dirname, "../landing-dist");
49
- const MIME = {
50
- ".html": "text/html; charset=utf-8",
51
- ".js": "application/javascript; charset=utf-8",
52
- ".css": "text/css; charset=utf-8",
53
- ".json": "application/json",
54
- ".png": "image/png",
55
- ".webp": "image/webp",
56
- ".mp4": "video/mp4",
57
- ".svg": "image/svg+xml",
58
- ".ico": "image/x-icon",
59
- ".woff2": "font/woff2",
60
- ".woff": "font/woff",
61
- ".ttf": "font/ttf",
62
- ".txt": "text/plain",
63
- ".xml": "application/xml",
64
- };
65
- function cacheControlForLandingAsset(urlPath, contentType) {
66
- if (contentType.startsWith("text/html"))
67
- return "no-cache, must-revalidate";
68
- if (urlPath.startsWith("/_next/static/"))
69
- return "public, max-age=31536000, immutable";
70
- return "public, max-age=86400, stale-while-revalidate=604800";
71
- }
72
- function injectScriptNonce(document, nonce) {
73
- return document.replace(/<script(?=\s|>)/gi, `<script nonce="${nonce}"`);
74
- }
75
- async function serveLanding(req, res, scriptNonce, homepageVariant) {
76
- if (req.method !== "GET" && req.method !== "HEAD")
77
- return false;
78
- if (!fs.existsSync(LANDING_DIR))
79
- return false;
80
- let urlPath;
81
- try {
82
- urlPath = decodeURIComponent(req.url?.split("?")[0] ?? "/");
83
- }
84
- catch {
85
- return false;
86
- }
87
- const requestedSegments = urlPath.split("/").filter(Boolean);
88
- const safeSegments = requestedSegments.map((segment) => path.basename(segment));
89
- if (safeSegments.some((segment, index) => (segment !== requestedSegments[index] ||
90
- segment === "." ||
91
- segment === ".." ||
92
- segment.includes("\\") ||
93
- segment.includes("\0"))))
94
- return false;
95
- // Next.js trailingSlash exports /about/ as /about/index.html.
96
- if (urlPath.endsWith("/") || safeSegments.length === 0)
97
- safeSegments.push("index.html");
98
- let filePath = path.join(LANDING_DIR, ...safeSegments);
99
- // Security: ensure we stay within LANDING_DIR
100
- const relativePath = path.relative(LANDING_DIR, filePath);
101
- if (relativePath === ".." ||
102
- relativePath.startsWith(`..${path.sep}`) ||
103
- path.isAbsolute(relativePath))
104
- return false;
105
- if (!fs.existsSync(filePath)) {
106
- // Next static exports store dynamic-route Flight payloads in nested folders
107
- // (for example, __next.blog/$d$slug/__PAGE__.txt), while the client asks
108
- // for a flattened filename (__next.blog.$d$slug.__PAGE__.txt). Map only
109
- // that generated shape after every original URL segment has passed the
110
- // containment check above. Without this, guide-to-guide navigation falls
111
- // back to a full-document load and emits avoidable 404s.
112
- const requestedFlightFile = safeSegments.at(-1);
113
- const flightParts = requestedFlightFile?.split(".") ?? [];
114
- const isNestedFlightPayload = flightParts.length >= 4 &&
115
- flightParts[0] === "__next" &&
116
- flightParts.at(-1) === "txt";
117
- if (!isNestedFlightPayload)
118
- return false;
119
- const remappedFlightPath = path.join(LANDING_DIR, ...safeSegments.slice(0, -1), `${flightParts[0]}.${flightParts[1]}`, ...flightParts.slice(2, -2), `${flightParts.at(-2)}.txt`);
120
- if (!fs.existsSync(remappedFlightPath))
121
- return false;
122
- filePath = remappedFlightPath;
123
- }
124
- let fileStats;
125
- try {
126
- fileStats = fs.statSync(filePath);
127
- // Accept extensionless Next.js routes such as /changelog without trying to
128
- // stream the directory itself. Streaming a directory emits an unhandled
129
- // EISDIR error on Linux and previously restarted the production process.
130
- if (fileStats.isDirectory()) {
131
- filePath = path.join(filePath, "index.html");
132
- if (!fs.existsSync(filePath))
133
- return false;
134
- fileStats = fs.statSync(filePath);
135
- }
136
- }
137
- catch {
138
- return false;
139
- }
140
- if (!fileStats.isFile())
39
+ /**
40
+ * The product website lives in its own repository and is served from
41
+ * https://premiere-pro-mcp.com. This process only serves MCP, health, and
42
+ * OAuth discovery. Browser traffic that reaches a known public alias of the
43
+ * hosted deployment is sent to the website; every other host gets a plain 404.
44
+ */
45
+ const PUBLIC_SITE_ORIGIN = "https://premiere-pro-mcp.com";
46
+ function isPublicSiteAlias(hostHeader) {
47
+ // Only the Host header is consulted; forwarded host headers are never trusted.
48
+ const hostname = hostHeader?.trim().toLowerCase().replace(/:\d+$/, "");
49
+ if (!hostname)
141
50
  return false;
142
- // Consolidate only real exported pages, after containment and file checks.
143
- // Keep assets, missing routes, MCP, health and OAuth discovery untouched.
144
- if (path.basename(filePath) === "index.html") {
145
- const pageDirectory = path.relative(LANDING_DIR, path.dirname(filePath));
146
- const canonicalPath = pageDirectory
147
- ? `/${pageDirectory.split(path.sep).map(encodeURIComponent).join("/")}/`
148
- : "/";
149
- const rawPath = req.url.split("?")[0];
150
- const queryIndex = req.url.indexOf("?");
151
- const query = queryIndex >= 0 ? req.url.slice(queryIndex) : "";
152
- const hostname = req.headers.host?.toLowerCase();
153
- const publicAlias = hostname === "www.premiere-pro-mcp.com" || hostname === "premiere-pro-mcp.fly.dev";
154
- if (publicAlias || rawPath !== canonicalPath) {
155
- // Never derive a redirect origin from Host or forwarded headers. Local and
156
- // self-hosted installs retain their origin; known public aliases use HTTPS.
157
- const origin = publicAlias ? "https://premiere-pro-mcp.com" : "";
158
- res.writeHead(308, {
159
- "Location": `${origin}${canonicalPath}${query}`,
160
- "Cache-Control": "public, max-age=3600",
161
- });
162
- res.end();
163
- return true;
164
- }
165
- }
166
- // Both variants are complete static documents. Select before sending HTML so
167
- // the control cannot flash, shift layout, or hydrate over the treatment.
168
- if (urlPath === "/" && homepageVariant === "test") {
169
- const treatmentPath = path.join(LANDING_DIR, "design-preview", "index.html");
170
- if (!fs.existsSync(treatmentPath))
171
- return false;
172
- filePath = treatmentPath;
173
- try {
174
- fileStats = fs.statSync(filePath);
175
- }
176
- catch {
177
- return false;
178
- }
179
- }
180
- const preview = new URL(req.url ?? "/", "http://localhost").searchParams.has("design") || urlPath.startsWith("/design-preview/");
181
- if (preview)
182
- res.setHeader("X-Robots-Tag", "noindex, follow");
183
- res.setHeader("Vary", urlPath === "/" ? "Cookie, DNT, Sec-GPC, Accept-Encoding" : "Accept-Encoding");
184
- const ext = path.extname(filePath);
185
- const contentType = MIME[ext] ?? "application/octet-stream";
186
- const compress = shouldGzipLanding(req.headers["accept-encoding"], contentType);
187
- const headers = {
188
- "Content-Type": contentType,
189
- "Cache-Control": urlPath === "/" || preview ? "private, no-store" : cacheControlForLandingAsset(urlPath, contentType),
190
- };
191
- if (contentType.startsWith("text/html")) {
192
- try {
193
- assertLandingDocumentSize(Number.isFinite(fileStats.size) ? fileStats.size : undefined, landingDocumentSettings.maxDocumentBytes);
194
- }
195
- catch (error) {
196
- console.error("[premiere-pro-mcp] Landing document read failed:", error);
197
- res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" });
198
- res.end(req.method === "HEAD" ? undefined : "Internal server error");
199
- return true;
200
- }
201
- }
202
- // HEAD validates the same trusted path and returns the same representation
203
- // headers, but never reads, injects, or compresses the response body.
204
- if (req.method === "HEAD") {
205
- if (compress)
206
- headers["Content-Encoding"] = "gzip";
207
- res.writeHead(200, headers);
208
- res.end();
209
- return true;
210
- }
211
- // A static export cannot generate per-request nonces itself. Add the nonce
212
- // at the trusted server boundary so Next bootstrap and JSON-LD scripts remain
213
- // executable without retaining script-src 'unsafe-inline'.
214
- if (contentType.startsWith("text/html")) {
215
- try {
216
- const rendered = await landingDocuments.render(filePath, Number.isFinite(fileStats.size) ? fileStats.size : undefined, (source) => injectFirstPaintExposure(injectScriptNonce(source, scriptNonce), scriptNonce, urlPath === "/" ? homepageVariant : undefined, preview), compress);
217
- if (!rendered.accepted) {
218
- res.writeHead(503, {
219
- "Content-Type": "text/plain; charset=utf-8",
220
- "Cache-Control": "no-store",
221
- "Retry-After": "1",
222
- });
223
- res.end("Service busy");
224
- return true;
225
- }
226
- if (res.destroyed || res.writableEnded)
227
- return true;
228
- if (compress)
229
- headers["Content-Encoding"] = "gzip";
230
- res.writeHead(200, headers);
231
- res.end(rendered.body);
232
- return true;
233
- }
234
- catch (error) {
235
- console.error("[premiere-pro-mcp] Landing document read failed:", error);
236
- if (res.destroyed || res.writableEnded)
237
- return true;
238
- res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8", "Cache-Control": "no-store" });
239
- res.end("Internal server error");
240
- return true;
241
- }
242
- }
243
- if (compress)
244
- headers["Content-Encoding"] = "gzip";
245
- const stream = fs.createReadStream(filePath);
246
- res.once("close", () => stream.destroy());
247
- stream.once("error", (error) => {
248
- console.error("[premiere-pro-mcp] Landing asset read failed:", error);
249
- if (!res.headersSent) {
250
- res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
251
- res.end("Internal server error");
252
- return;
253
- }
254
- res.destroy();
255
- });
256
- res.writeHead(200, headers);
257
- if (compress) {
258
- const gzip = createGzip({ level: 6 });
259
- res.once("close", () => gzip.destroy());
260
- gzip.once("error", () => res.destroy());
261
- stream.pipe(gzip).pipe(res);
262
- }
263
- else
264
- stream.pipe(res);
265
- return true;
51
+ return hostname === "premiere-pro-mcp.fly.dev" || hostname.endsWith(".premiere-pro-mcp.com");
52
+ }
53
+ function publicSiteLocation(rawUrl) {
54
+ // Parse against a fixed base so absolute-form and protocol-relative request
55
+ // targets can never change the redirect origin.
56
+ const url = new URL(rawUrl, "http://localhost");
57
+ return `${PUBLIC_SITE_ORIGIN}${url.pathname}${url.search}`;
266
58
  }
267
59
  const PORT = parseInt(process.env.PORT ?? "3000", 10);
268
60
  const HTTP_HOST = process.env.MCP_HTTP_HOST || "0.0.0.0";
269
61
  let httpAuth;
270
62
  let admissionSettings;
271
- let landingDocumentSettings;
272
63
  try {
273
64
  httpAuth = readHttpAuthConfiguration(process.env);
274
65
  admissionSettings = readHttpAdmissionSettings(process.env);
275
- landingDocumentSettings = readLandingDocumentSettings(process.env);
276
66
  }
277
67
  catch (error) {
278
68
  console.error("[premiere-pro-mcp] Refusing to start:", error instanceof Error ? error.message : error);
@@ -289,7 +79,6 @@ process.env.PREMIERE_MCP_TRANSPORT = "http";
289
79
  const telemetry = getTelemetry();
290
80
  const admission = new HttpAdmissionController(admissionSettings);
291
81
  const preAuthAdmission = new HttpAdmissionController(admissionSettings);
292
- const landingDocuments = new LandingDocumentRenderer(landingDocumentSettings, readBoundedUtf8);
293
82
  const oauthResourceServer = httpAuth.oauth ? new OAuthResourceServer(httpAuth.oauth) : undefined;
294
83
  // Streamable HTTP creates an McpServer for every request. Sharing the repository
295
84
  // keeps memory-backed context durable across those request-scoped servers and
@@ -306,25 +95,19 @@ const tempDir = getTempDir(bridgeOptions);
306
95
  console.error(`[premiere-pro-mcp] Starting HTTP server on port ${PORT}...`);
307
96
  console.error(`[premiere-pro-mcp] Temp directory: ${tempDir}`);
308
97
  cleanupTempDir(bridgeOptions);
309
- const homepageExperiment = createHomepageExperiment();
310
98
  // Each request gets its own transport+server instance (stateless per-request model)
311
99
  const httpServer = http.createServer(async (req, res) => {
312
- const scriptNonce = randomBytes(18).toString("base64");
313
- applyHttpSecurityHeaders(res, { scriptNonce });
100
+ applyHttpSecurityHeaders(res);
314
101
  const pathname = getRequestPathname(req.url);
315
102
  if (!pathname) {
316
103
  res.writeHead(400, { "Content-Type": "application/json", "Cache-Control": "no-store" });
317
104
  res.end(JSON.stringify({ error: "Malformed request URL" }));
318
105
  return;
319
106
  }
320
- if (pathname === "/api/landing-events") {
321
- await homepageExperiment.handleEvent(req, res);
322
- return;
323
- }
324
107
  // Health check
325
108
  if (req.method === "GET" && pathname === "/health") {
326
109
  res.writeHead(200, { "Content-Type": "application/json", "Cache-Control": "no-store" });
327
- res.end(JSON.stringify({ status: "ok", service: "premiere-pro-mcp" }));
110
+ res.end(JSON.stringify({ status: "ok", service: "premiere-pro-mcp", version: SERVER_VERSION }));
328
111
  return;
329
112
  }
330
113
  if (req.method === "GET" &&
@@ -338,20 +121,19 @@ const httpServer = http.createServer(async (req, res) => {
338
121
  res.end(JSON.stringify(oauthResourceServer.metadata()));
339
122
  return;
340
123
  }
341
- // Only handle /mcp endpoint; everything else goes to the landing page
124
+ // Only /mcp reaches the MCP transport. Website paths on a public alias go to
125
+ // the website; everything else is a small 404.
342
126
  if (pathname !== "/mcp") {
343
- let variant;
344
- if (pathname === "/" && req.method === "GET") {
345
- const preview = new URL(req.url ?? "/", "http://localhost").searchParams;
346
- if (preview.has("design"))
347
- variant = preview.get("design") === "test" ? "test" : "control";
348
- else if (fs.existsSync(path.join(LANDING_DIR, "design-preview", "index.html")))
349
- variant = await homepageExperiment.assign(req, res);
350
- }
351
- if (await serveLanding(req, res, scriptNonce, variant))
127
+ if ((req.method === "GET" || req.method === "HEAD") && isPublicSiteAlias(req.headers.host)) {
128
+ res.writeHead(308, {
129
+ "Location": publicSiteLocation(req.url),
130
+ "Cache-Control": "public, max-age=3600",
131
+ });
132
+ res.end();
352
133
  return;
353
- res.writeHead(404);
354
- res.end("Not found");
134
+ }
135
+ res.writeHead(404, { "Content-Type": "application/json", "Cache-Control": "no-store" });
136
+ res.end(req.method === "HEAD" ? undefined : JSON.stringify({ error: "Not found" }));
355
137
  return;
356
138
  }
357
139
  if (!isSupportedMcpMethod(req.method)) {
@@ -514,7 +296,6 @@ async function shutdown(signal) {
514
296
  await mcpHandler.close();
515
297
  await projectContextRepository.close();
516
298
  await telemetry.shutdown();
517
- await homepageExperiment.shutdown();
518
299
  process.exit(0);
519
300
  }
520
301
  process.on("SIGTERM", () => void shutdown("SIGTERM"));