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 +15 -0
- package/README.md +23 -24
- package/after-effects-cep-plugin/CSXS/manifest.xml +2 -2
- package/artifacts/MCPBridgeCEP.zxp +0 -0
- package/cep-plugin/CSXS/manifest.xml +3 -3
- package/cep-plugin/index.html +1 -1
- package/cep-plugin/updater.cjs +1 -1
- package/dist/http-security.d.ts +6 -5
- package/dist/http-security.js +10 -25
- package/dist/http-server.js +32 -251
- package/dist/tools/spot-workflows.d.ts +3 -3
- package/dist/tools/track-targeting.js +47 -14
- package/docs/supported-actions.md +1 -1
- package/package.json +3 -6
- package/public-product-manifest.json +1 -1
- package/scripts/validate-adobe-marketplace-branding.mjs +0 -8
- package/uxp-plugin/manifest.json +1 -1
- package/dist/homepage-experiment.d.ts +0 -32
- package/dist/homepage-experiment.js +0 -327
- package/dist/landing-compression.d.ts +0 -3
- package/dist/landing-compression.js +0 -16
- package/dist/landing-documents.d.ts +0 -38
- package/dist/landing-documents.js +0 -142
- package/scripts/build-workflow-starter-kit.py +0 -92
- package/scripts/generate-marketing-reference.mjs +0 -127
- package/scripts/tool-reference-data.mjs +0 -37
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
|
[](https://nodejs.org)
|
|
23
23
|
[](https://modelcontextprotocol.io/specification/2026-07-28)
|
|
24
24
|
[](https://www.npmjs.com/package/premiere-pro-mcp)
|
|
25
|
-
[](https://premiere-pro-mcp.com/)
|
|
26
26
|
[](https://www.adobe.com/products/premiere.html)
|
|
27
27
|
|
|
28
28
|
[](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
|
-

|
|
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.
|
|
113
|
+
### Latest release: 1.18.5
|
|
114
114
|
|
|
115
|
-
The published v1.18.
|
|
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
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-

|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-

|
|
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
|
-
|
|
1239
|
-
|
|
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
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
<Extension Id="com.mcp.premiere.bridge.headless" Version="1.18.
|
|
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>
|
package/cep-plugin/index.html
CHANGED
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
<section class="update-section" aria-live="polite">
|
|
82
82
|
<div class="update-copy">
|
|
83
83
|
<span class="section-label">MCP updates</span>
|
|
84
|
-
<strong id="updateTitle">Version 1.18.
|
|
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>
|
package/cep-plugin/updater.cjs
CHANGED
package/dist/http-security.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type http from "node:http";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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
|
|
16
|
+
export declare function applyHttpSecurityHeaders(res: http.ServerResponse): void;
|
|
16
17
|
//# sourceMappingURL=http-security.d.ts.map
|
package/dist/http-security.js
CHANGED
|
@@ -1,24 +1,13 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
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 '
|
|
11
|
-
"base-uri '
|
|
7
|
+
"default-src 'none'",
|
|
8
|
+
"base-uri 'none'",
|
|
12
9
|
"frame-ancestors 'none'",
|
|
13
|
-
"
|
|
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
|
|
34
|
-
const
|
|
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
|
}
|
package/dist/http-server.js
CHANGED
|
@@ -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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
-
|
|
354
|
-
res.
|
|
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"));
|