premiere-pro-mcp 1.12.0 → 1.12.2

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,26 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [1.12.2] - 2026-08-22
10
+
11
+ ### Fixed
12
+
13
+ - `set_effect_property` now accepts safely serialized string values as well as
14
+ numbers, unlocking MOGRT and graphic parameters that Premiere exposes as
15
+ JSON strings. Responses report parameter readback separately from render
16
+ verification.
17
+ - An empty legacy QE effect catalog now returns a clear no-mutation capability
18
+ response rather than incorrectly reporting a requested effect as missing.
19
+ When connected, the documented UXP effect catalog and transaction workflow is
20
+ the supported alternative.
21
+
22
+ ## [1.12.1] - 2026-08-22
23
+
24
+ ### Fixed
25
+
26
+ - Allowed Google Analytics collection requests to `www.google.com` in the
27
+ restrictive Content Security Policy, matching the current Google tag client.
28
+
9
29
  ## [1.12.0] - 2026-08-22
10
30
 
11
31
  ### Added
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  <div align="center">
2
2
 
3
- # Premiere Pro MCP
3
+ # MCP for Adobe Premiere Pro
4
4
 
5
5
  [![MCP Toplist](https://mcptoplist.com/badge/glama%2Fleancoderkavy%2Fpremiere-pro-mcp.svg)](https://mcptoplist.com/server/glama%2Fleancoderkavy%2Fpremiere-pro-mcp)
6
6
 
@@ -19,7 +19,7 @@
19
19
 
20
20
  ---
21
21
 
22
- ![Premiere Pro MCP turns a structured AI request into an organized local editing workflow](landing/public/marketing/premiere-pro-mcp-campaign-hero-v1.png)
22
+ ![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)
23
23
 
24
24
  ## What is this?
25
25
 
@@ -31,16 +31,17 @@ An [MCP (Model Context Protocol)](https://modelcontextprotocol.io) server that l
31
31
 
32
32
  The AI handles the entire workflow through 287 core tools spanning the supported ExtendScript, QE DOM, revisioned project-context retrieval, safe edit-planning, and connection-verification surfaces. A compatible, authenticated UXP panel adds 50 documented, capability-gated tools without replacing the production CEP bridge.
33
33
 
34
- ### Latest release: 1.12.0
34
+ ### Latest release: 1.12.2
35
35
 
36
- - **MCP safety research:** ten documented recommendations now cover subscription streams,
37
- contextual completions, workspace boundaries, resource metadata and canonical URIs.
38
- - **Trust boundaries:** the recommendations define prompt/resource-injection defenses, layered
39
- end-to-end health checks, C2PA inspection limits, and UXP external-launch safeguards.
40
- - **Verification clarity:** semantic keyframe verification is scoped separately from implementation
41
- and licensed Premiere Pro host confirmation.
36
+ - **MOGRT text and graphics:** `set_effect_property` now accepts string-backed
37
+ parameters with safe serialization and readback status.
38
+ - **Effect discovery clarity:** an empty legacy QE effect catalog now returns a
39
+ no-mutation capability response and directs connected hosts to the documented
40
+ UXP catalog/add workflow.
41
+ - **Verification clarity:** property readback remains distinct from playback or
42
+ exported-frame verification in a licensed Premiere Pro host.
42
43
 
43
- See the [v1.12.0 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.12.0)
44
+ See the [v1.12.2 release notes](https://github.com/leancoderkavy/premiere-pro-mcp/releases/tag/v1.12.2)
44
45
  for complete details. Live installation in Premiere Pro still requires host verification.
45
46
 
46
47
  ---
@@ -49,10 +50,10 @@ for complete details. Live installation in Premiere Pro still requires host veri
49
50
 
50
51
  ### Easiest supported path: Claude Desktop
51
52
 
52
- 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.12.0/premiere-pro-mcp-1.12.0.mcpb).
53
+ 1. Download the current [Claude Desktop bundle (`.mcpb`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.12.2/premiere-pro-mcp-1.12.2.mcpb).
53
54
  2. In Claude Desktop, open **Settings > Extensions > Advanced settings > Install Extension**, select the downloaded bundle, and restart Claude Desktop.
54
- 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.12.0/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.
55
- 4. Restart Premiere, open a project, then open **Window > Extensions > MCP Bridge**.
55
+ 3. Download the separate [signed Premiere connector (`.zxp`)](https://github.com/leancoderkavy/premiere-pro-mcp/releases/download/v1.12.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.
56
+ 4. Restart Premiere, open a project, then open **Window > Extensions > MCP for Adobe Premiere Pro**.
56
57
  5. In Claude, enter: `Safely check my Premiere connection with verify_premiere_connection. Make no changes.`
57
58
 
58
59
  The Claude bundle contains the local MCP server, so this route does not require Node.js. The Premiere connector is a separate required install. The first prompt is read-only and reports whether the server is installed, configured, connected, and live-verified.
@@ -259,7 +260,7 @@ Add to your VS Code MCP server configuration:
259
260
 
260
261
  1. Open (or restart) Premiere Pro
261
262
  2. The bridge starts automatically using the default temp directory (or its previously saved setting)
262
- 3. Optionally go to **Window > Extensions > MCP Bridge** to confirm the green "Running" status or change the **Temp Directory** to match your MCP client config
263
+ 3. Optionally go to **Window > Extensions > MCP for Adobe Premiere Pro** to confirm the green "Running" status or change the **Temp Directory** to match your MCP client config
263
264
  4. Ask your AI assistant to run `get_capabilities`, then `ping`, with Premiere open.
264
265
  5. For a safe first request, ask: *"What is my current Premiere Pro project and active sequence? Do not make changes."*
265
266
 
@@ -275,11 +276,11 @@ From a clone of this repository:
275
276
  ```bash
276
277
  codex plugin marketplace add .
277
278
  codex plugin add premiere-pro@premiere-pro-mcp
278
- npx -y premiere-pro-mcp@1.12.0 --install-cep
279
+ npx -y premiere-pro-mcp@1.12.2 --install-cep
279
280
  ```
280
281
 
281
282
  Restart Premiere Pro and start a new Codex session after installation. The plugin
282
- launches `premiere-pro-mcp@1.12.0` through `npx`; the separate CEP installation is
283
+ launches `premiere-pro-mcp@1.12.2` through `npx`; the separate CEP installation is
283
284
  required because the MCP server communicates with the running Premiere host through
284
285
  the local bridge.
285
286
 
@@ -299,7 +300,7 @@ For Claude Code, add this repository as a marketplace and install the plugin:
299
300
  Then install the Premiere bridge and start a new Claude Code session:
300
301
 
301
302
  ```bash
302
- npx -y premiere-pro-mcp@1.12.0 --install-cep
303
+ npx -y premiere-pro-mcp@1.12.2 --install-cep
303
304
  ```
304
305
 
305
306
  The Claude Code package lives in
@@ -453,7 +454,7 @@ argument, undo, confirmation, and live-host boundaries.
453
454
 
454
455
  ## Architecture
455
456
 
456
- ![Local-first Premiere Pro MCP workflow from AI assistant through the MCP bridge to a verified Premiere result](landing/public/marketing/premiere-pro-mcp-workflow-v1.png)
457
+ ![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)
457
458
 
458
459
  **Local (stdio):**
459
460
 
@@ -731,7 +732,7 @@ A live instance is running at **https://premiere-pro-mcp.fly.dev**.
731
732
  git clone https://github.com/leancoderkavy/premiere-pro-mcp.git
732
733
  cd premiere-pro-mcp
733
734
  fly apps create your-app-name
734
- # Required: add bearer token auth
735
+ # Required: add bearer token auth. Use a unique, high-entropy secret per deployment.
735
736
  fly secrets set MCP_AUTH_TOKEN=your-secret-token
736
737
  fly deploy --remote-only
737
738
  ```
@@ -752,6 +753,7 @@ Then connect with:
752
753
 
753
754
  > **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.
754
755
  > `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.
756
+ > 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.
755
757
 
756
758
  ---
757
759
 
@@ -768,14 +770,24 @@ Then connect with:
768
770
  | `PREMIERE_CONTEXT_DIR` | Override the local project-context storage directory | OS application-data directory |
769
771
  | `PORT` | HTTP port (HTTP/SSE transport only) | `3000` |
770
772
  | `MCP_AUTH_TOKEN` | Bearer token required by the HTTP transport | unset |
771
- | `ALLOW_UNAUTHENTICATED` | Set to `1` to run HTTP without auth (unsafe; throwaway instances only) | unset |
773
+ | `ALLOW_UNAUTHENTICATED` | Set to `1` only for local/test HTTP harnesses; it is rejected when `NODE_ENV=production` | unset |
774
+ | `MCP_MAX_REQUEST_BYTES` | Maximum HTTP MCP request body size | `1048576` |
775
+ | `MCP_HEADERS_TIMEOUT_MS` | Maximum time to receive request headers | `10000` |
776
+ | `MCP_REQUEST_TIMEOUT_MS` | Maximum time to receive an HTTP request | `60000` |
777
+ | `MCP_KEEP_ALIVE_TIMEOUT_MS` | Idle keep-alive socket timeout | `5000` |
778
+ | `MCP_MAX_REQUESTS_PER_SOCKET` | Requests permitted on one keep-alive socket | `100` |
779
+ | `MCP_MAX_CONCURRENT_REQUESTS` | In-flight authenticated MCP request ceiling | `8` |
780
+ | `MCP_RATE_LIMIT_PER_MINUTE` | Per-credential token-bucket refill rate | `120` |
781
+ | `MCP_RATE_LIMIT_BURST` | Per-credential short burst allowance | `30` |
782
+ | `MCP_MAX_RATE_LIMIT_KEYS` | In-memory rate-limit identity ceiling | `2048` |
783
+ | `MCP_TRUST_PROXY` | Set to `1` only behind a proxy that overwrites `X-Forwarded-For` | unset |
772
784
  | `POSTHOG_API_KEY` | PostHog project token; enables privacy-safe MCP usage telemetry | unset |
773
785
  | `POSTHOG_HOST` | PostHog ingestion host | `https://us.i.posthog.com` |
774
786
  | `POSTHOG_ENVIRONMENT` | Environment property attached to telemetry events | `production` |
775
787
  | `POSTHOG_DISTINCT_ID` | Optional stable anonymous server identifier | Fly machine ID or random boot ID |
776
788
 
777
789
  When PostHog is enabled, the server records `mcp_connection_attempt`,
778
- `mcp_request`, and `mcp_tool_call`. Events contain operational fields such as
790
+ `mcp_request`, `mcp_request_rejected`, and `mcp_tool_call`. Events contain operational fields such as
779
791
  method, tool name, outcome, status code, and duration. Authentication tokens,
780
792
  IP addresses, MCP arguments, project paths, media names, and tool results are
781
793
  never sent. Person profiles are disabled for these events.
@@ -865,8 +877,14 @@ by default. Enable them only by setting
865
877
 
866
878
  - **Run it locally over stdio** unless you have a specific reason not to. That's the safe default.
867
879
  - **The HTTP transport (`http-server`) requires `MCP_AUTH_TOKEN`** and refuses to start
868
- without it. It binds `0.0.0.0` and is remotely reachable, so never expose it publicly
869
- without a strong token (set `ALLOW_UNAUTHENTICATED=1` only for a throwaway public instance).
880
+ without it in production. It binds `0.0.0.0` and is remotely reachable, so never expose it publicly
881
+ without a strong token and edge controls. `ALLOW_UNAUTHENTICATED=1` is limited to non-production local/test use.
882
+ - **The HTTP transport admits only exact `/mcp` Streamable HTTP requests**, enforces
883
+ body/socket/request limits, and applies a bounded in-process per-credential rate and concurrency limit before
884
+ MCP request parsing or Premiere bridge work begins. It returns `413`, `429`, or `503` on containment failures. Configure an
885
+ upstream rate limit and request-size limit too; process-local counters do not protect a multi-machine deployment.
886
+ - **The landing CSP uses a per-response nonce for scripts**, and its static assets use explicit cache policies.
887
+ Keep the server in front of the exported landing so those controls are not bypassed by a separate static host.
870
888
  - The bridge temp directory is created private to your user (mode `0700`), and the server
871
889
  refuses to use one owned by another user — relevant on shared machines, where the CEP
872
890
  panel would otherwise execute any `cmd_*.jsx` staged there.
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.12.0" ExtensionBundleName="MCP Bridge">
2
+ <ExtensionManifest Version="7.0" ExtensionBundleId="com.mcp.premiere.bridge" ExtensionBundleVersion="1.12.2" ExtensionBundleName="MCP for Adobe Premiere Pro">
3
3
  <ExtensionList>
4
- <Extension Id="com.mcp.premiere.bridge.panel" Version="1.12.0"/>
5
- <Extension Id="com.mcp.premiere.bridge.headless" Version="1.12.0"/>
4
+ <Extension Id="com.mcp.premiere.bridge.panel" Version="1.12.2"/>
5
+ <Extension Id="com.mcp.premiere.bridge.headless" Version="1.12.2"/>
6
6
  </ExtensionList>
7
7
  <ExecutionEnvironment>
8
8
  <HostList>
@@ -31,7 +31,7 @@
31
31
  </Lifecycle>
32
32
  <UI>
33
33
  <Type>Panel</Type>
34
- <Menu>MCP Bridge</Menu>
34
+ <Menu>MCP for Adobe Premiere Pro</Menu>
35
35
  <Geometry>
36
36
  <Size>
37
37
  <Height>300</Height>
@@ -3,7 +3,7 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1">
6
- <title>MCP Bridge</title>
6
+ <title>MCP for Adobe Premiere Pro</title>
7
7
  <link rel="stylesheet" href="styles.css">
8
8
  </head>
9
9
  <body>
@@ -11,8 +11,8 @@
11
11
  <header class="panel-header">
12
12
  <div class="brand-mark" aria-hidden="true"><span>M</span></div>
13
13
  <div class="brand-copy">
14
- <h1>MCP Bridge</h1>
15
- <p>Premiere Pro connection</p>
14
+ <h1>MCP for Adobe Premiere Pro</h1>
15
+ <p>Local Premiere connection</p>
16
16
  </div>
17
17
  <div class="auto-start"><span></span>Auto-start</div>
18
18
  </header>
@@ -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.12.0</strong>
84
+ <strong id="updateTitle">Version 1.12.2</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>
@@ -416,7 +416,7 @@ function handleUpdateClick() {
416
416
  }
417
417
  } catch (e) {}
418
418
 
419
- log("MCP Bridge CEP plugin loaded");
419
+ log("MCP for Adobe Premiere Pro CEP connector loaded");
420
420
  setStatus("waiting", "Ready — click Start Bridge");
421
421
 
422
422
  // Always auto-start. The headless instance (StartOn ApplicationActivate) has no
@@ -7,7 +7,7 @@
7
7
  })(this, function () {
8
8
  "use strict";
9
9
 
10
- var CURRENT_VERSION = "1.12.0";
10
+ var CURRENT_VERSION = "1.12.2";
11
11
  var LATEST_RELEASE_API =
12
12
  "https://api.github.com/repos/leancoderkavy/premiere-pro-mcp/releases/latest";
13
13
  var RELEASES_URL =
@@ -115,6 +115,50 @@ function __findClip(nodeId) {
115
115
  return null;
116
116
  }
117
117
 
118
+ // CEP's legacy QE path can enumerate a host's effect catalog before adding an
119
+ // effect to a timeline clip. Recent Premiere builds can expose QE yet return an
120
+ // empty catalog, so distinguish that host limitation from a misspelled effect
121
+ // name. Calling addVideoEffect/addAudioEffect without a catalog entry is not a
122
+ // safe fallback; an available UXP bridge has its own documented effect workflow.
123
+ function __getQeEffectCatalog(kind) {
124
+ var label = kind === "audio" ? "audio" : "video";
125
+ if (typeof app === "undefined" || typeof app.enableQE !== "function") {
126
+ return { ok: false, error: "QE is unavailable in this Premiere build, so " + label + " effects cannot be enumerated or applied." };
127
+ }
128
+
129
+ try {
130
+ app.enableQE();
131
+ } catch (eEnable) {
132
+ return { ok: false, error: "Premiere could not enable QE for " + label + " effect discovery: " + eEnable.toString() };
133
+ }
134
+
135
+ if (typeof qe === "undefined" || !qe.project) {
136
+ return { ok: false, error: "QE did not expose a project after enableQE(), so " + label + " effects cannot be enumerated or applied." };
137
+ }
138
+
139
+ var getter = kind === "audio" ? qe.project.getAudioEffectList : qe.project.getVideoEffectList;
140
+ if (typeof getter !== "function") {
141
+ return { ok: false, error: "This Premiere QE build does not expose the " + label + " effect catalog API." };
142
+ }
143
+
144
+ var effects = null;
145
+ try {
146
+ effects = getter.call(qe.project);
147
+ } catch (eList) {
148
+ return { ok: false, error: "Premiere could not read its QE " + label + " effect catalog: " + eList.toString() };
149
+ }
150
+
151
+ var count = effects && typeof effects.numItems !== "undefined" ? Number(effects.numItems) : NaN;
152
+ if (isNaN(count) || count < 1) {
153
+ return {
154
+ ok: false,
155
+ error: "Premiere returned an empty legacy QE " + label + " effect catalog; no effect was applied. If the authenticated Premiere UXP bridge is connected, use manage_clip_effects_uxp with action 'catalog' and then 'add' instead. Existing clip components can still be inspected or edited."
156
+ };
157
+ }
158
+
159
+ return { ok: true, effects: effects, count: count };
160
+ }
161
+
118
162
  function __getAllClips(seq) {
119
163
  if (!seq) seq = app.project.activeSequence;
120
164
  if (!seq) return [];
@@ -0,0 +1,79 @@
1
+ import type http from "node:http";
2
+ export declare const MCP_HTTP_METHODS: readonly ["GET", "POST", "DELETE"];
3
+ export interface HttpAuthConfiguration {
4
+ authToken?: string;
5
+ allowUnauthenticated: boolean;
6
+ }
7
+ export interface HttpAdmissionSettings {
8
+ maxRequestBytes: number;
9
+ headersTimeoutMs: number;
10
+ requestTimeoutMs: number;
11
+ keepAliveTimeoutMs: number;
12
+ maxRequestsPerSocket: number;
13
+ maxConcurrentRequests: number;
14
+ rateLimitPerMinute: number;
15
+ rateLimitBurst: number;
16
+ maxRateLimitKeys: number;
17
+ trustProxy: boolean;
18
+ }
19
+ export interface AdmissionMetrics {
20
+ activeRequests: number;
21
+ trackedRateLimitKeys: number;
22
+ }
23
+ export type AdmissionDecision = {
24
+ accepted: true;
25
+ release: () => void;
26
+ } | {
27
+ accepted: false;
28
+ reason: "rate_limited" | "at_capacity";
29
+ statusCode: 429 | 503;
30
+ retryAfterSeconds: number;
31
+ };
32
+ /**
33
+ * Reads the public-HTTP containment settings. Invalid values fail startup so a
34
+ * typo cannot silently turn a request or socket bound into an unlimited one.
35
+ */
36
+ export declare function readHttpAdmissionSettings(env: NodeJS.ProcessEnv): HttpAdmissionSettings;
37
+ /**
38
+ * A network-reachable editor control plane must never start unauthenticated in
39
+ * production. The override remains available only for local development and
40
+ * test harnesses where it does not create a public deployment.
41
+ */
42
+ export declare function readHttpAuthConfiguration(env: NodeJS.ProcessEnv): HttpAuthConfiguration;
43
+ export declare function getRequestPathname(rawUrl: string | undefined): string | undefined;
44
+ export declare function isSupportedMcpMethod(method: string | undefined): boolean;
45
+ export declare function requestContentLength(req: Pick<http.IncomingMessage, "headers">): number | undefined;
46
+ export declare function exceedsRequestBodyLimit(req: Pick<http.IncomingMessage, "headers">, maxRequestBytes: number): boolean;
47
+ export declare class RequestBodyTooLargeError extends Error {
48
+ constructor();
49
+ }
50
+ /**
51
+ * Reads an MCP request body with a hard byte cap before it reaches the transport.
52
+ * This avoids attaching a second live data listener beside the transport, which
53
+ * can otherwise race and consume a fast chunked body before the transport does.
54
+ */
55
+ export declare function readBoundedRequestBody(req: http.IncomingMessage, maxRequestBytes: number): Promise<Buffer>;
56
+ export declare function isAuthorizedBearer(req: Pick<http.IncomingMessage, "headers">, authToken: string | undefined): boolean;
57
+ /**
58
+ * The edge is authoritative by default. Honor X-Forwarded-For only after an
59
+ * operator explicitly declares the proxy trusted; otherwise it is attacker
60
+ * input and must not be used as a rate-limit identity.
61
+ */
62
+ export declare function rateLimitIdentity(req: Pick<http.IncomingMessage, "headers" | "socket">, authorizedCredential: string | undefined, trustProxy: boolean): string;
63
+ /**
64
+ * Bounded, process-local protection for a single machine. It deliberately does
65
+ * not log or export identities. An edge/WAF remains necessary for fleet-wide
66
+ * protection across restarts and multiple instances.
67
+ */
68
+ export declare class HttpAdmissionController {
69
+ private readonly settings;
70
+ private readonly clock;
71
+ private readonly buckets;
72
+ private activeRequests;
73
+ constructor(settings: Pick<HttpAdmissionSettings, "maxConcurrentRequests" | "rateLimitPerMinute" | "rateLimitBurst" | "maxRateLimitKeys">, clock?: () => number);
74
+ acquire(identity: string): AdmissionDecision;
75
+ metrics(): AdmissionMetrics;
76
+ private getOrCreateBucket;
77
+ private pruneIdleBuckets;
78
+ }
79
+ //# sourceMappingURL=http-admission.d.ts.map
@@ -0,0 +1,231 @@
1
+ import { createHash, timingSafeEqual } from "node:crypto";
2
+ export const MCP_HTTP_METHODS = ["GET", "POST", "DELETE"];
3
+ const ONE_MINUTE_MS = 60_000;
4
+ function readBoundedInteger(env, name, fallback, minimum, maximum) {
5
+ const raw = env[name];
6
+ if (raw === undefined || raw === "")
7
+ return fallback;
8
+ if (!/^\d+$/.test(raw)) {
9
+ throw new Error(`${name} must be an integer between ${minimum} and ${maximum}`);
10
+ }
11
+ const value = Number(raw);
12
+ if (!Number.isSafeInteger(value) || value < minimum || value > maximum) {
13
+ throw new Error(`${name} must be an integer between ${minimum} and ${maximum}`);
14
+ }
15
+ return value;
16
+ }
17
+ /**
18
+ * Reads the public-HTTP containment settings. Invalid values fail startup so a
19
+ * typo cannot silently turn a request or socket bound into an unlimited one.
20
+ */
21
+ export function readHttpAdmissionSettings(env) {
22
+ const rateLimitPerMinute = readBoundedInteger(env, "MCP_RATE_LIMIT_PER_MINUTE", 120, 1, 10_000);
23
+ const rateLimitBurst = readBoundedInteger(env, "MCP_RATE_LIMIT_BURST", 30, 1, rateLimitPerMinute);
24
+ return {
25
+ maxRequestBytes: readBoundedInteger(env, "MCP_MAX_REQUEST_BYTES", 1_048_576, 1_024, 10_485_760),
26
+ headersTimeoutMs: readBoundedInteger(env, "MCP_HEADERS_TIMEOUT_MS", 10_000, 1_000, 60_000),
27
+ requestTimeoutMs: readBoundedInteger(env, "MCP_REQUEST_TIMEOUT_MS", 60_000, 1_000, 300_000),
28
+ keepAliveTimeoutMs: readBoundedInteger(env, "MCP_KEEP_ALIVE_TIMEOUT_MS", 5_000, 1_000, 60_000),
29
+ maxRequestsPerSocket: readBoundedInteger(env, "MCP_MAX_REQUESTS_PER_SOCKET", 100, 1, 10_000),
30
+ maxConcurrentRequests: readBoundedInteger(env, "MCP_MAX_CONCURRENT_REQUESTS", 8, 1, 128),
31
+ rateLimitPerMinute,
32
+ rateLimitBurst,
33
+ maxRateLimitKeys: readBoundedInteger(env, "MCP_MAX_RATE_LIMIT_KEYS", 2_048, 16, 100_000),
34
+ trustProxy: env.MCP_TRUST_PROXY === "1",
35
+ };
36
+ }
37
+ /**
38
+ * A network-reachable editor control plane must never start unauthenticated in
39
+ * production. The override remains available only for local development and
40
+ * test harnesses where it does not create a public deployment.
41
+ */
42
+ export function readHttpAuthConfiguration(env) {
43
+ const authToken = env.MCP_AUTH_TOKEN?.trim();
44
+ if (authToken)
45
+ return { authToken, allowUnauthenticated: false };
46
+ if (env.ALLOW_UNAUTHENTICATED === "1" && env.NODE_ENV !== "production") {
47
+ return { allowUnauthenticated: true };
48
+ }
49
+ throw new Error("MCP_AUTH_TOKEN is required for the HTTP transport. " +
50
+ "ALLOW_UNAUTHENTICATED=1 is permitted only outside NODE_ENV=production.");
51
+ }
52
+ export function getRequestPathname(rawUrl) {
53
+ if (!rawUrl)
54
+ return undefined;
55
+ try {
56
+ return new URL(rawUrl, "http://localhost").pathname;
57
+ }
58
+ catch {
59
+ return undefined;
60
+ }
61
+ }
62
+ export function isSupportedMcpMethod(method) {
63
+ return MCP_HTTP_METHODS.some((allowed) => allowed === method);
64
+ }
65
+ export function requestContentLength(req) {
66
+ const header = req.headers["content-length"];
67
+ const value = Array.isArray(header) ? header[0] : header;
68
+ if (value === undefined)
69
+ return undefined;
70
+ if (!/^\d+$/.test(value))
71
+ return Number.NaN;
72
+ const parsed = Number(value);
73
+ return Number.isSafeInteger(parsed) ? parsed : Number.NaN;
74
+ }
75
+ export function exceedsRequestBodyLimit(req, maxRequestBytes) {
76
+ const contentLength = requestContentLength(req);
77
+ return contentLength !== undefined && (!Number.isFinite(contentLength) || contentLength > maxRequestBytes);
78
+ }
79
+ export class RequestBodyTooLargeError extends Error {
80
+ constructor() {
81
+ super("Request body too large");
82
+ this.name = "RequestBodyTooLargeError";
83
+ }
84
+ }
85
+ /**
86
+ * Reads an MCP request body with a hard byte cap before it reaches the transport.
87
+ * This avoids attaching a second live data listener beside the transport, which
88
+ * can otherwise race and consume a fast chunked body before the transport does.
89
+ */
90
+ export function readBoundedRequestBody(req, maxRequestBytes) {
91
+ return new Promise((resolve, reject) => {
92
+ const chunks = [];
93
+ let receivedBytes = 0;
94
+ const cleanup = () => {
95
+ req.off("data", onData);
96
+ req.off("end", onEnd);
97
+ req.off("error", onError);
98
+ req.off("aborted", onAborted);
99
+ };
100
+ const onData = (chunk) => {
101
+ const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(String(chunk));
102
+ receivedBytes += buffer.length;
103
+ if (receivedBytes <= maxRequestBytes) {
104
+ chunks.push(buffer);
105
+ return;
106
+ }
107
+ cleanup();
108
+ // Drain rather than destroy so the caller can reliably send its 413.
109
+ req.resume();
110
+ reject(new RequestBodyTooLargeError());
111
+ };
112
+ const onEnd = () => {
113
+ cleanup();
114
+ resolve(Buffer.concat(chunks));
115
+ };
116
+ const onError = (error) => {
117
+ cleanup();
118
+ reject(error);
119
+ };
120
+ const onAborted = () => {
121
+ cleanup();
122
+ reject(new Error("Request aborted"));
123
+ };
124
+ req.on("data", onData);
125
+ req.once("end", onEnd);
126
+ req.once("error", onError);
127
+ req.once("aborted", onAborted);
128
+ });
129
+ }
130
+ export function isAuthorizedBearer(req, authToken) {
131
+ if (!authToken)
132
+ return true;
133
+ const header = req.headers.authorization;
134
+ const value = Array.isArray(header) ? header[0] : header ?? "";
135
+ if (!value.startsWith("Bearer "))
136
+ return false;
137
+ const provided = Buffer.from(value.slice(7));
138
+ const expected = Buffer.from(authToken);
139
+ if (provided.length !== expected.length)
140
+ return false;
141
+ return timingSafeBufferEqual(provided, expected);
142
+ }
143
+ function timingSafeBufferEqual(left, right) {
144
+ return timingSafeEqual(left, right);
145
+ }
146
+ function hashedIdentity(value) {
147
+ return createHash("sha256").update(value).digest("hex").slice(0, 32);
148
+ }
149
+ /**
150
+ * The edge is authoritative by default. Honor X-Forwarded-For only after an
151
+ * operator explicitly declares the proxy trusted; otherwise it is attacker
152
+ * input and must not be used as a rate-limit identity.
153
+ */
154
+ export function rateLimitIdentity(req, authorizedCredential, trustProxy) {
155
+ if (authorizedCredential)
156
+ return `credential:${hashedIdentity(authorizedCredential)}`;
157
+ const forwarded = req.headers["x-forwarded-for"];
158
+ const forwardedValue = Array.isArray(forwarded) ? forwarded[0] : forwarded;
159
+ const remoteAddress = trustProxy && forwardedValue
160
+ ? forwardedValue.split(",")[0].trim()
161
+ : req.socket?.remoteAddress ?? "unknown";
162
+ return `ip:${hashedIdentity(remoteAddress || "unknown")}`;
163
+ }
164
+ /**
165
+ * Bounded, process-local protection for a single machine. It deliberately does
166
+ * not log or export identities. An edge/WAF remains necessary for fleet-wide
167
+ * protection across restarts and multiple instances.
168
+ */
169
+ export class HttpAdmissionController {
170
+ settings;
171
+ clock;
172
+ buckets = new Map();
173
+ activeRequests = 0;
174
+ constructor(settings, clock = Date.now) {
175
+ this.settings = settings;
176
+ this.clock = clock;
177
+ }
178
+ acquire(identity) {
179
+ const now = this.clock();
180
+ this.pruneIdleBuckets(now);
181
+ const bucket = this.getOrCreateBucket(identity, now);
182
+ if (!bucket) {
183
+ return { accepted: false, reason: "rate_limited", statusCode: 429, retryAfterSeconds: 60 };
184
+ }
185
+ const elapsed = Math.max(0, now - bucket.updatedAt);
186
+ const refill = elapsed * (this.settings.rateLimitPerMinute / ONE_MINUTE_MS);
187
+ bucket.tokens = Math.min(this.settings.rateLimitBurst, bucket.tokens + refill);
188
+ bucket.updatedAt = now;
189
+ if (bucket.tokens < 1) {
190
+ const missing = 1 - bucket.tokens;
191
+ const retryAfterSeconds = Math.max(1, Math.ceil((missing / this.settings.rateLimitPerMinute) * 60));
192
+ return { accepted: false, reason: "rate_limited", statusCode: 429, retryAfterSeconds };
193
+ }
194
+ if (this.activeRequests >= this.settings.maxConcurrentRequests) {
195
+ return { accepted: false, reason: "at_capacity", statusCode: 503, retryAfterSeconds: 1 };
196
+ }
197
+ bucket.tokens -= 1;
198
+ this.activeRequests += 1;
199
+ let released = false;
200
+ return {
201
+ accepted: true,
202
+ release: () => {
203
+ if (released)
204
+ return;
205
+ released = true;
206
+ this.activeRequests = Math.max(0, this.activeRequests - 1);
207
+ },
208
+ };
209
+ }
210
+ metrics() {
211
+ return { activeRequests: this.activeRequests, trackedRateLimitKeys: this.buckets.size };
212
+ }
213
+ getOrCreateBucket(identity, now) {
214
+ const existing = this.buckets.get(identity);
215
+ if (existing)
216
+ return existing;
217
+ if (this.buckets.size >= this.settings.maxRateLimitKeys)
218
+ return undefined;
219
+ const bucket = { tokens: this.settings.rateLimitBurst, updatedAt: now };
220
+ this.buckets.set(identity, bucket);
221
+ return bucket;
222
+ }
223
+ pruneIdleBuckets(now) {
224
+ const maxIdleMs = Math.max(ONE_MINUTE_MS, Math.ceil((this.settings.rateLimitBurst / this.settings.rateLimitPerMinute) * ONE_MINUTE_MS) * 2);
225
+ for (const [identity, bucket] of this.buckets) {
226
+ if (now - bucket.updatedAt > maxIdleMs)
227
+ this.buckets.delete(identity);
228
+ }
229
+ }
230
+ }
231
+ //# sourceMappingURL=http-admission.js.map
@@ -1,4 +1,8 @@
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
6
  export declare const HTTP_SECURITY_HEADERS: Readonly<{
3
7
  "Content-Security-Policy": string;
4
8
  "Strict-Transport-Security": "max-age=31536000; includeSubDomains";
@@ -8,5 +12,5 @@ export declare const HTTP_SECURITY_HEADERS: Readonly<{
8
12
  "Permissions-Policy": "camera=(), microphone=(), geolocation=()";
9
13
  "Cross-Origin-Opener-Policy": "same-origin";
10
14
  }>;
11
- export declare function applyHttpSecurityHeaders(res: http.ServerResponse): void;
15
+ export declare function applyHttpSecurityHeaders(res: http.ServerResponse, options?: HttpSecurityHeaderOptions): void;
12
16
  //# sourceMappingURL=http-security.d.ts.map