feat: initial commit - Jhonny Editor
- Adicionado estrutura completa do projeto - Configurado MCP server para Premiere Pro - Adicionado documentação e skills - Configurado Gitignore para o projeto
This commit is contained in:
Executable
+80
@@ -0,0 +1,80 @@
|
||||
# UXP Spike
|
||||
|
||||
A throwaway UXP plugin that answers two questions we cannot answer from the docs. It is not part
|
||||
of the MCP server and nothing depends on it.
|
||||
|
||||
## Why
|
||||
|
||||
**1. Is issue #9 fixable?**
|
||||
|
||||
Documented ExtendScript has **no frame-export method at all**. `Sequence` exposes only
|
||||
`exportAsMediaDirect`, `exportAsProject`, and `exportAsFinalCutProXML` — there is no
|
||||
`exportFramePNG`. That is why `capture_frame` reaches into the undocumented QE DOM, and why
|
||||
[#9](https://github.com/leancoderkavy/premiere-pro-mcp/issues/9) reports it returning `false` and
|
||||
writing nothing on **both** PPro 2025 and 2026. It isn't a bug we can fix; it's a hole in the API.
|
||||
|
||||
UXP has a supported `Exporter.exportSequenceFrame()`. **Probe A** finds out whether it works.
|
||||
|
||||
Frame capture is the only tool that gives an agent *visual* evidence of the timeline. Without it
|
||||
there is no way to verify a color grade, a transition, or a title actually landed. It's worth a
|
||||
spike on its own.
|
||||
|
||||
**2. Can we drop the temp-file bridge?**
|
||||
|
||||
Today the CEP panel polls a temp directory every 200ms for `.jsx` files. UXP has `WebSocket` and
|
||||
`fetch`, which would be a strict upgrade. But:
|
||||
|
||||
- Adobe's UXP docs **never mention `localhost` or `127.0.0.1`** — not once, anywhere.
|
||||
- UXP WebSockets are **client-only**: a plugin "cannot host or accept incoming connections", so
|
||||
the MCP server must be the server. (Fine — that's the direction we want anyway.)
|
||||
- macOS is documented to restrict `http://`. Whether that extends to `ws://` is **unstated**.
|
||||
- `wss://` with a self-signed cert is **explicitly broken on macOS** per Adobe's known-issues page.
|
||||
|
||||
So both obvious loopback options sit in undocumented-or-blocked territory. **Probes B–E** find out
|
||||
what actually connects. This determines our entire transport, so it's worth knowing before we
|
||||
commit to a port rather than after.
|
||||
|
||||
## Running it
|
||||
|
||||
You need Premiere Pro **25.6 or newer** (that's when UXP went GA) and the
|
||||
[UXP Developer Tool](https://developer.adobe.com/premiere-pro/uxp/plugins/) 2.2+.
|
||||
|
||||
**1. Start the spike server.** Zero dependencies, so there is nothing to install:
|
||||
|
||||
```bash
|
||||
node uxp-spike/server.mjs # listens on 127.0.0.1:7777
|
||||
```
|
||||
|
||||
**2. Enable Premiere's developer mode.** Settings → Plugins → *Enable developer mode*, then
|
||||
**restart Premiere**.
|
||||
|
||||
**3. Side-load the plugin.** In UXP Developer Tool: **Add Plugin** → select
|
||||
`uxp-spike/manifest.json` → **Load & Watch**.
|
||||
|
||||
**4. Open a project with a sequence**, put the playhead somewhere with visible picture, and open
|
||||
**Window → UXP Plugins → MCP UXP Spike**.
|
||||
|
||||
**5. Click "Run all probes."**
|
||||
|
||||
## Reading the result
|
||||
|
||||
The panel prints a JSON verdict and writes it to `/tmp/mcp-uxp-spike-report.json`. The server also
|
||||
logs every transport that reaches it.
|
||||
|
||||
The two lines that matter:
|
||||
|
||||
```
|
||||
Issue #9 (frame capture): FIXED by UXP | still broken
|
||||
Transport: websocket | http-fetch | file-bridge
|
||||
```
|
||||
|
||||
Probe A does **not** trust `exportSequenceFrame`'s return value — the QE DOM lies about this in
|
||||
both directions, so success is decided purely by whether a file exists on disk afterwards.
|
||||
|
||||
A failing probe is a **result, not an error**. "Loopback WebSocket is blocked" is exactly the kind
|
||||
of thing worth learning in an afternoon rather than three weeks into a port.
|
||||
|
||||
## Please paste the JSON verdict into the tracking issue
|
||||
|
||||
If you can run this against a real Premiere, that's genuinely the most useful thing anyone can do
|
||||
for this project right now. Neither of these questions can be settled from Adobe's documentation.
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<script src="index.js"></script>
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, "Segoe UI", sans-serif;
|
||||
font-size: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
h2 {
|
||||
font-size: 13px;
|
||||
margin: 0 0 4px;
|
||||
}
|
||||
p.sub {
|
||||
margin: 0 0 12px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.row {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
.row label {
|
||||
width: 70px;
|
||||
opacity: 0.7;
|
||||
}
|
||||
.row input {
|
||||
flex: 1;
|
||||
}
|
||||
#results {
|
||||
margin-top: 12px;
|
||||
}
|
||||
.probe {
|
||||
border-left: 3px solid #888;
|
||||
padding: 6px 8px;
|
||||
margin-bottom: 6px;
|
||||
background: rgba(127, 127, 127, 0.08);
|
||||
}
|
||||
.probe.pass {
|
||||
border-left-color: #2d9d5c;
|
||||
}
|
||||
.probe.fail {
|
||||
border-left-color: #d0402b;
|
||||
}
|
||||
.probe.run {
|
||||
border-left-color: #d8a13a;
|
||||
}
|
||||
.probe .name {
|
||||
font-weight: 600;
|
||||
}
|
||||
.probe .detail {
|
||||
opacity: 0.75;
|
||||
margin-top: 2px;
|
||||
word-break: break-all;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
#summary {
|
||||
margin-top: 12px;
|
||||
padding: 8px;
|
||||
background: rgba(127, 127, 127, 0.12);
|
||||
white-space: pre-wrap;
|
||||
word-break: break-all;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>MCP UXP Spike</h2>
|
||||
<p class="sub">
|
||||
Answers two questions: does UXP fix frame capture (issue #9), and can a UXP panel reach a
|
||||
local MCP server without the temp-file bridge?
|
||||
</p>
|
||||
|
||||
<div class="row">
|
||||
<label>Port</label>
|
||||
<input id="port" type="text" value="7777" />
|
||||
</div>
|
||||
<div class="row">
|
||||
<label>Out dir</label>
|
||||
<input id="outdir" type="text" value="/tmp/" />
|
||||
</div>
|
||||
|
||||
<sp-button id="run" variant="cta">Run all probes</sp-button>
|
||||
|
||||
<div id="results"></div>
|
||||
<div id="summary">Not run yet.</div>
|
||||
</body>
|
||||
</html>
|
||||
Executable
+304
@@ -0,0 +1,304 @@
|
||||
/*
|
||||
* MCP UXP Spike
|
||||
*
|
||||
* Two questions, answered empirically against a running Premiere Pro:
|
||||
*
|
||||
* 1. Does UXP fix frame capture?
|
||||
* Documented ExtendScript has NO frame-export method at all — Sequence only exposes
|
||||
* exportAsMediaDirect / exportAsProject / exportAsFinalCutProXML. That is why our
|
||||
* capture_frame reaches into the undocumented QE DOM, and why it returns false and
|
||||
* writes nothing on both PPro 2025 and 2026 (issue #9). UXP has a supported
|
||||
* Exporter.exportSequenceFrame(). Probe A finds out whether it actually works.
|
||||
*
|
||||
* 2. Can a UXP panel talk to a local MCP server directly?
|
||||
* Today we shuttle commands through temp files and a 200ms poller. UXP has WebSocket
|
||||
* and fetch, which would be a strict upgrade — but Adobe's docs never once mention
|
||||
* localhost or 127.0.0.1 in network.domains, macOS is documented to restrict http://,
|
||||
* and wss:// with a self-signed cert is explicitly broken on macOS. So loopback sits
|
||||
* in undocumented-or-blocked territory. Probes B-E find out what actually connects.
|
||||
*
|
||||
* Every probe reports rather than throws. A failure is a result, not an error.
|
||||
*/
|
||||
|
||||
const { entrypoints } = require("uxp");
|
||||
const ppro = require("premierepro");
|
||||
|
||||
const PROBES = [
|
||||
{ id: "frame-export", name: "A. Exporter.exportSequenceFrame()", run: probeFrameExport },
|
||||
{ id: "ws-localhost", name: "B. WebSocket ws://localhost", run: (c) => probeWebSocket(c, "localhost") },
|
||||
{ id: "ws-loopback-ip", name: "C. WebSocket ws://127.0.0.1", run: (c) => probeWebSocket(c, "127.0.0.1") },
|
||||
{ id: "fetch-http", name: "D. fetch() http://127.0.0.1", run: probeFetch },
|
||||
{ id: "fs-write", name: "E. Filesystem write (bridge fallback)", run: probeFileSystem },
|
||||
];
|
||||
|
||||
const results = {};
|
||||
|
||||
entrypoints.setup({
|
||||
plugin: { create() {}, destroy() {} },
|
||||
panels: {
|
||||
spikePanel: {
|
||||
create() {},
|
||||
show() {},
|
||||
// Adobe's docs warn that hide()/destroy() "are not working as expected yet" in
|
||||
// Premiere, so nothing important is torn down here.
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
document.addEventListener("DOMContentLoaded", () => {
|
||||
document.getElementById("run").addEventListener("click", runAll);
|
||||
});
|
||||
|
||||
async function runAll() {
|
||||
const ctx = {
|
||||
port: Number(document.getElementById("port").value) || 7777,
|
||||
outDir: document.getElementById("outdir").value || "/tmp/",
|
||||
};
|
||||
|
||||
document.getElementById("results").innerHTML = "";
|
||||
document.getElementById("summary").textContent = "Running...";
|
||||
|
||||
for (const probe of PROBES) {
|
||||
render(probe.id, probe.name, { state: "run", detail: "running..." });
|
||||
let outcome;
|
||||
try {
|
||||
outcome = await probe.run(ctx);
|
||||
} catch (e) {
|
||||
// A probe that throws is still a result — record it, don't abort the run.
|
||||
outcome = { ok: false, detail: "threw: " + errText(e) };
|
||||
}
|
||||
results[probe.id] = outcome;
|
||||
render(probe.id, probe.name, { state: outcome.ok ? "pass" : "fail", detail: outcome.detail });
|
||||
}
|
||||
|
||||
summarize(ctx);
|
||||
}
|
||||
|
||||
// --- Probe A: does UXP actually fix issue #9? ---------------------------------
|
||||
|
||||
async function probeFrameExport(ctx) {
|
||||
const project = await ppro.Project.getActiveProject();
|
||||
if (!project) return { ok: false, detail: "No active project — open one and re-run." };
|
||||
|
||||
const sequence = await project.getActiveSequence();
|
||||
if (!sequence) return { ok: false, detail: "No active sequence — open one and re-run." };
|
||||
|
||||
// getPlayerPosition() -> TickTime; getFrameSize() -> RectF, which despite the name has
|
||||
// only width/height (no x/y).
|
||||
const time = await sequence.getPlayerPosition();
|
||||
const rect = await sequence.getFrameSize();
|
||||
|
||||
const filename = "mcp-uxp-spike-frame.png";
|
||||
const returned = await ppro.Exporter.exportSequenceFrame(
|
||||
sequence,
|
||||
time,
|
||||
filename,
|
||||
ctx.outDir,
|
||||
rect.width,
|
||||
rect.height
|
||||
);
|
||||
|
||||
// The QE DOM lies about this — it returns false on builds where it works and true on
|
||||
// builds where it doesn't. So the return value is recorded but never trusted; the
|
||||
// filesystem is the only thing that decides.
|
||||
const fullPath = joinPath(ctx.outDir, filename);
|
||||
const onDisk = await fileExists(fullPath);
|
||||
|
||||
return {
|
||||
ok: onDisk,
|
||||
detail:
|
||||
"returned " + JSON.stringify(returned) +
|
||||
"\nfile on disk: " + (onDisk ? "YES — " + fullPath : "NO (" + fullPath + ")") +
|
||||
"\nsequence: " + rect.width + "x" + rect.height +
|
||||
" @ " + time.seconds + "s" +
|
||||
(onDisk
|
||||
? "\n=> UXP fixes issue #9. This is the supported frame-capture path."
|
||||
: "\n=> No file written. Check the out dir exists and is writable."),
|
||||
};
|
||||
}
|
||||
|
||||
// --- Probes B/C: can the panel open a socket to a local MCP server? -----------
|
||||
|
||||
function probeWebSocket(ctx, host) {
|
||||
const url = "ws://" + host + ":" + ctx.port;
|
||||
|
||||
return new Promise((resolve) => {
|
||||
let socket;
|
||||
let settled = false;
|
||||
|
||||
const finish = (ok, detail) => {
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
try { if (socket) socket.close(); } catch (e) { /* already gone */ }
|
||||
resolve({ ok, detail });
|
||||
};
|
||||
|
||||
// No connection attempt should hang the panel.
|
||||
const timer = setTimeout(
|
||||
() => finish(false, url + "\ntimed out after 5s — no open, no error. Treat as blocked."),
|
||||
5000
|
||||
);
|
||||
|
||||
try {
|
||||
socket = new WebSocket(url);
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
return finish(false, url + "\nconstructor threw: " + errText(e));
|
||||
}
|
||||
|
||||
socket.onopen = () => {
|
||||
try {
|
||||
socket.send(JSON.stringify({ probe: "hello", host: host }));
|
||||
} catch (e) {
|
||||
clearTimeout(timer);
|
||||
finish(false, url + "\nopened but send() threw: " + errText(e));
|
||||
}
|
||||
};
|
||||
|
||||
// Only a round-trip proves the transport. An open event alone doesn't.
|
||||
socket.onmessage = (event) => {
|
||||
clearTimeout(timer);
|
||||
finish(
|
||||
true,
|
||||
url + "\nround-trip OK. Server echoed: " + String(event.data) +
|
||||
"\n=> Loopback WebSocket works. The temp-file bridge can go."
|
||||
);
|
||||
};
|
||||
|
||||
socket.onerror = (err) => {
|
||||
clearTimeout(timer);
|
||||
finish(
|
||||
false,
|
||||
url + "\nerror: " + (errText(err) || "(no detail — UXP often gives none)") +
|
||||
"\nIs the spike server running? node uxp-spike/server.mjs"
|
||||
);
|
||||
};
|
||||
|
||||
socket.onclose = (ev) => {
|
||||
if (settled) return;
|
||||
clearTimeout(timer);
|
||||
finish(false, url + "\nclosed before any message (code " + (ev && ev.code) + ")");
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
// --- Probe D: fetch() as a fallback transport ---------------------------------
|
||||
|
||||
async function probeFetch(ctx) {
|
||||
// macOS is documented to restrict http://. If that restriction extends to loopback,
|
||||
// this fails and the answer matters as much as a pass.
|
||||
const url = "http://127.0.0.1:" + ctx.port + "/probe";
|
||||
const res = await fetch(url, {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ probe: "fetch" }),
|
||||
});
|
||||
const body = await res.text();
|
||||
|
||||
return {
|
||||
ok: res.ok,
|
||||
detail: url + "\nHTTP " + res.status + "\nbody: " + body,
|
||||
};
|
||||
}
|
||||
|
||||
// --- Probe E: the fallback we already know works ------------------------------
|
||||
|
||||
async function probeFileSystem(ctx) {
|
||||
const fs = require("fs");
|
||||
const probePath = joinPath(ctx.outDir, "mcp-uxp-spike-fs.json");
|
||||
const payload = JSON.stringify({ probe: "fs", at: new Date().toISOString() });
|
||||
|
||||
await fs.writeFile(probePath, payload, { encoding: "utf-8" });
|
||||
const readBack = await fs.readFile(probePath, { encoding: "utf-8" });
|
||||
|
||||
return {
|
||||
ok: readBack === payload,
|
||||
detail:
|
||||
probePath +
|
||||
"\nwrite+read round-trip: " + (readBack === payload ? "OK" : "MISMATCH") +
|
||||
"\n=> If B/C/D all fail, this is the transport we keep.",
|
||||
};
|
||||
}
|
||||
|
||||
// --- helpers ------------------------------------------------------------------
|
||||
|
||||
async function fileExists(path) {
|
||||
try {
|
||||
const fs = require("fs");
|
||||
await fs.lstat(path);
|
||||
return true;
|
||||
} catch (e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function joinPath(dir, name) {
|
||||
return dir.charAt(dir.length - 1) === "/" ? dir + name : dir + "/" + name;
|
||||
}
|
||||
|
||||
function errText(e) {
|
||||
if (!e) return "";
|
||||
return e.message || e.type || String(e);
|
||||
}
|
||||
|
||||
function render(id, name, { state, detail }) {
|
||||
let el = document.getElementById("probe-" + id);
|
||||
if (!el) {
|
||||
el = document.createElement("div");
|
||||
el.id = "probe-" + id;
|
||||
document.getElementById("results").appendChild(el);
|
||||
}
|
||||
el.className = "probe " + state;
|
||||
el.innerHTML = "";
|
||||
|
||||
const nameEl = document.createElement("div");
|
||||
nameEl.className = "name";
|
||||
nameEl.textContent = (state === "pass" ? "PASS " : state === "fail" ? "FAIL " : "... ") + name;
|
||||
|
||||
const detailEl = document.createElement("div");
|
||||
detailEl.className = "detail";
|
||||
detailEl.textContent = detail;
|
||||
|
||||
el.appendChild(nameEl);
|
||||
el.appendChild(detailEl);
|
||||
}
|
||||
|
||||
async function summarize(ctx) {
|
||||
const verdict = {
|
||||
ranAt: new Date().toISOString(),
|
||||
host: "premierepro",
|
||||
probes: {},
|
||||
};
|
||||
for (const p of PROBES) {
|
||||
verdict.probes[p.id] = { ok: !!(results[p.id] && results[p.id].ok), detail: results[p.id].detail };
|
||||
}
|
||||
|
||||
const frameOk = verdict.probes["frame-export"].ok;
|
||||
const socketOk = verdict.probes["ws-localhost"].ok || verdict.probes["ws-loopback-ip"].ok;
|
||||
const fetchOk = verdict.probes["fetch-http"].ok;
|
||||
|
||||
verdict.conclusions = {
|
||||
fixesIssue9: frameOk,
|
||||
canDropFileBridge: socketOk || fetchOk,
|
||||
recommendedTransport: socketOk ? "websocket" : fetchOk ? "http-fetch" : "file-bridge",
|
||||
};
|
||||
|
||||
const lines = [
|
||||
"Issue #9 (frame capture): " + (frameOk ? "FIXED by UXP" : "still broken — see probe A"),
|
||||
"Transport: " + verdict.conclusions.recommendedTransport,
|
||||
"",
|
||||
"Paste this into the spike issue:",
|
||||
JSON.stringify(verdict, null, 2),
|
||||
];
|
||||
document.getElementById("summary").textContent = lines.join("\n");
|
||||
|
||||
// Best effort — the whole point of probe E is that this still works when nothing else does.
|
||||
try {
|
||||
const fs = require("fs");
|
||||
await fs.writeFile(joinPath(ctx.outDir, "mcp-uxp-spike-report.json"), JSON.stringify(verdict, null, 2), {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
} catch (e) {
|
||||
/* the panel already shows it */
|
||||
}
|
||||
}
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"manifestVersion": 5,
|
||||
"id": "com.mcp.premiere.uxp.spike",
|
||||
"name": "MCP UXP Spike",
|
||||
"version": "0.1.0",
|
||||
"main": "index.html",
|
||||
"host": {
|
||||
"app": "premierepro",
|
||||
"minVersion": "25.6.0"
|
||||
},
|
||||
"entrypoints": [
|
||||
{
|
||||
"type": "panel",
|
||||
"id": "spikePanel",
|
||||
"label": { "default": "MCP UXP Spike" },
|
||||
"minimumSize": { "width": 380, "height": 480 },
|
||||
"preferredDockedSize": { "width": 420, "height": 640 },
|
||||
"preferredFloatingSize": { "width": 480, "height": 700 }
|
||||
}
|
||||
],
|
||||
"requiredPermissions": {
|
||||
"localFileSystem": "fullAccess",
|
||||
"network": {
|
||||
"domains": "all"
|
||||
}
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
/*
|
||||
* Spike server for the UXP probes.
|
||||
*
|
||||
* Serves HTTP and WebSocket on the same port so the person testing runs one command.
|
||||
* Deliberately zero-dependency: a spike that needs `npm install` first is a spike people
|
||||
* don't run. The WebSocket bits are a minimal RFC 6455 text-frame implementation — enough
|
||||
* to prove a round-trip, and nothing more.
|
||||
*
|
||||
* node uxp-spike/server.mjs [port]
|
||||
*/
|
||||
|
||||
import { createServer } from "node:http";
|
||||
import { createHash } from "node:crypto";
|
||||
|
||||
const PORT = Number(process.argv[2]) || 7777;
|
||||
const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; // RFC 6455
|
||||
|
||||
const seen = { http: false, ws: false };
|
||||
|
||||
const server = createServer((req, res) => {
|
||||
if (req.method === "POST" && req.url === "/probe") {
|
||||
let body = "";
|
||||
req.on("data", (chunk) => (body += chunk));
|
||||
req.on("end", () => {
|
||||
seen.http = true;
|
||||
console.log(` [http] POST /probe <- ${body}`);
|
||||
report("fetch() over http://127.0.0.1 reached the server");
|
||||
res.writeHead(200, { "Content-Type": "application/json" });
|
||||
res.end(JSON.stringify({ ok: true, echo: safeParse(body) }));
|
||||
});
|
||||
return;
|
||||
}
|
||||
res.writeHead(404).end();
|
||||
});
|
||||
|
||||
server.on("upgrade", (req, socket) => {
|
||||
const key = req.headers["sec-websocket-key"];
|
||||
if (!key) return socket.destroy();
|
||||
|
||||
const accept = createHash("sha1").update(key + GUID).digest("base64");
|
||||
socket.write(
|
||||
"HTTP/1.1 101 Switching Protocols\r\n" +
|
||||
"Upgrade: websocket\r\n" +
|
||||
"Connection: Upgrade\r\n" +
|
||||
`Sec-WebSocket-Accept: ${accept}\r\n\r\n`
|
||||
);
|
||||
|
||||
console.log(` [ws] client connected (Host: ${req.headers.host})`);
|
||||
|
||||
socket.on("data", (buf) => {
|
||||
const msg = decodeTextFrame(buf);
|
||||
if (msg === null) return; // close/ping/binary — not worth handling in a spike
|
||||
|
||||
seen.ws = true;
|
||||
console.log(` [ws] <- ${msg}`);
|
||||
report(`WebSocket round-trip works from the UXP panel (Host: ${req.headers.host})`);
|
||||
socket.write(encodeTextFrame(JSON.stringify({ ok: true, echo: safeParse(msg) })));
|
||||
});
|
||||
|
||||
socket.on("error", () => socket.destroy());
|
||||
});
|
||||
|
||||
server.listen(PORT, "127.0.0.1", () => {
|
||||
console.log(`\nUXP spike server listening on 127.0.0.1:${PORT}`);
|
||||
console.log(" WebSocket : ws://localhost:%d and ws://127.0.0.1:%d", PORT, PORT);
|
||||
console.log(" HTTP : POST http://127.0.0.1:%d/probe", PORT);
|
||||
console.log("\nNow hit 'Run all probes' in the MCP UXP Spike panel in Premiere.\n");
|
||||
});
|
||||
|
||||
function report(what) {
|
||||
console.log(`\n *** ${what} ***`);
|
||||
console.log(
|
||||
` transports reached so far: ${[seen.ws && "websocket", seen.http && "http"].filter(Boolean).join(", ") || "none"}\n`
|
||||
);
|
||||
}
|
||||
|
||||
function safeParse(s) {
|
||||
try {
|
||||
return JSON.parse(s);
|
||||
} catch {
|
||||
return s;
|
||||
}
|
||||
}
|
||||
|
||||
/** Decode a single masked client text frame. Returns null for anything else. */
|
||||
function decodeTextFrame(buf) {
|
||||
if (buf.length < 2) return null;
|
||||
|
||||
const opcode = buf[0] & 0x0f;
|
||||
if (opcode !== 0x1) return null; // text frames only
|
||||
|
||||
const masked = (buf[1] & 0x80) !== 0;
|
||||
let len = buf[1] & 0x7f;
|
||||
let offset = 2;
|
||||
|
||||
if (len === 126) {
|
||||
len = buf.readUInt16BE(2);
|
||||
offset = 4;
|
||||
} else if (len === 127) {
|
||||
len = Number(buf.readBigUInt64BE(2));
|
||||
offset = 10;
|
||||
}
|
||||
|
||||
if (!masked) return buf.subarray(offset, offset + len).toString("utf8");
|
||||
|
||||
const mask = buf.subarray(offset, offset + 4);
|
||||
const payload = buf.subarray(offset + 4, offset + 4 + len);
|
||||
const out = Buffer.allocUnsafe(payload.length);
|
||||
for (let i = 0; i < payload.length; i++) out[i] = payload[i] ^ mask[i % 4];
|
||||
return out.toString("utf8");
|
||||
}
|
||||
|
||||
/** Encode an unmasked server text frame. */
|
||||
function encodeTextFrame(text) {
|
||||
const payload = Buffer.from(text, "utf8");
|
||||
const len = payload.length;
|
||||
|
||||
let header;
|
||||
if (len < 126) {
|
||||
header = Buffer.from([0x81, len]);
|
||||
} else if (len < 65536) {
|
||||
header = Buffer.alloc(4);
|
||||
header[0] = 0x81;
|
||||
header[1] = 126;
|
||||
header.writeUInt16BE(len, 2);
|
||||
} else {
|
||||
header = Buffer.alloc(10);
|
||||
header[0] = 0x81;
|
||||
header[1] = 127;
|
||||
header.writeBigUInt64BE(BigInt(len), 2);
|
||||
}
|
||||
return Buffer.concat([header, payload]);
|
||||
}
|
||||
Reference in New Issue
Block a user