import { readFile, writeFile } from "node:fs/promises"; import { resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { Client, InMemoryTransport } from "@modelcontextprotocol/client"; import { createServer } from "../dist/server.js"; const DEFAULT_CAPABILITIES = "inspect,edit,export,filesystem"; const FULL_CAPABILITIES = `${DEFAULT_CAPABILITIES},unsafe-script`; const OUTPUT_PATH = resolve("docs/supported-actions.md"); const disabledTelemetry = { enabled: false, capture() {}, async shutdown() {}, }; const mockUxpBridge = { async request() { return {}; }, getState() { return { status: "connected", connected: true }; }, }; async function collectRegisteredTools(capabilities, includeUxp) { const previousCapabilities = process.env.PREMIERE_MCP_CAPABILITIES; process.env.PREMIERE_MCP_CAPABILITIES = capabilities; const server = createServer({}, { telemetry: disabledTelemetry, ...(includeUxp ? { uxpBridge: mockUxpBridge } : {}), }); const client = new Client({ name: "supported-actions-generator", version: "1.0.0" }); const [clientTransport, serverTransport] = InMemoryTransport.createLinkedPair(); try { await Promise.all([server.connect(serverTransport), client.connect(clientTransport)]); const tools = []; let cursor; do { const response = await client.listTools(cursor ? { cursor } : undefined); tools.push(...response.tools); cursor = response.nextCursor; } while (cursor); return tools.sort((left, right) => left.name.localeCompare(right.name)); } finally { await client.close(); await server.close(); if (previousCapabilities === undefined) delete process.env.PREMIERE_MCP_CAPABILITIES; else process.env.PREMIERE_MCP_CAPABILITIES = previousCapabilities; } } function escapeCell(value) { return String(value ?? "") .replace(/\s+/g, " ") .trim() .replaceAll("|", "\\|"); } function code(value) { return `\`${String(value).replaceAll("`", "\\`")}\``; } function renderModes(tool) { const properties = tool.inputSchema?.properties ?? {}; const actionValues = properties.action?.enum; if (Array.isArray(actionValues) && actionValues.length > 0) { return actionValues.map(code).join(", "); } const modeGroups = Object.entries(properties) .filter(([, property]) => Array.isArray(property?.enum) && property.enum.length > 0) .map(([name, property]) => `${code(name)}: ${property.enum.map(code).join(", ")}`); return modeGroups.length > 0 ? modeGroups.join("; ") : "Single operation"; } function renderToolRows(tools, availability) { return tools.map((tool) => ( `| ${code(tool.name)} | ${availability} | ${renderModes(tool)} | ${escapeCell(tool.description)} |` )).join("\n"); } export async function generateSupportedActionsMarkdown() { const defaultCore = await collectRegisteredTools(DEFAULT_CAPABILITIES, false); const fullCore = await collectRegisteredTools(FULL_CAPABILITIES, false); const connectedDefault = await collectRegisteredTools(DEFAULT_CAPABILITIES, true); const defaultNames = new Set(defaultCore.map((tool) => tool.name)); const fullNames = new Set(fullCore.map((tool) => tool.name)); const restrictedCore = fullCore.filter((tool) => !defaultNames.has(tool.name)); const uxpTools = connectedDefault.filter((tool) => !defaultNames.has(tool.name)); if (uxpTools.some((tool) => fullNames.has(tool.name))) { throw new Error("The UXP additions overlap the registered core tool names."); } return `# Supported actions catalog This is the complete source-derived public action catalog for the current repository. The generator reads the same MCP registration surface used by clients, so tool names, descriptions, action enums, authority visibility, and counts stay aligned with the code. Release metadata and distributed-artifact claims remain versioned separately; this source catalog may include unreleased actions. | Surface | Count | Availability | | --- | ---: | --- | | Registered core actions | ${fullCore.length} | CEP/local server catalog; host and authority checks still apply | | Default-profile core actions | ${defaultCore.length} | Advertised with \`${DEFAULT_CAPABILITIES}\` | | Restricted core actions | ${restrictedCore.length} | Require explicit \`unsafe-script\` authority | | Authenticated UXP additions | ${uxpTools.length} | Advertised only while a compatible authenticated UXP panel is connected | | Default profile with UXP | ${connectedDefault.length} | ${defaultCore.length} core plus ${uxpTools.length} UXP tools | ## How to read support - \`tools/list\` is authoritative for what the current MCP session may call. - \`get_capabilities\` reports the full registered catalog, authority decisions, backend eligibility, and any live-host verification still required. - A listed tool is not proof that a particular Premiere installation supports every host API. The authenticated UXP capability handshake and per-call preflight remain authoritative. - CEP remains the compatibility backend. A failed UXP mutation is never automatically replayed through CEP or the undocumented QE DOM. - Automated tests establish schemas, routing, bounds, transactions, and readback contracts; they do not replace validation in a real Premiere host. ## Core actions Each core tool is one callable MCP action. “Actions or modes” records a top-level \`action\` enum when present, otherwise other top-level enum selectors, or “Single operation” when the tool has no enum-based mode. | MCP tool | Availability | Actions or modes | Description | | --- | --- | --- | --- | ${renderToolRows(defaultCore, "Default profile")} ${renderToolRows(restrictedCore, `Requires ${code("unsafe-script")}`)} ## Authenticated UXP actions These tools are additive. They appear only while the local loopback UXP bridge is authenticated and the connected host advertises the required command capabilities. | MCP tool | Availability | Actions or modes | Description | | --- | --- | --- | --- | ${renderToolRows(uxpTools, "Connected UXP")} ## Maintenance Run \`npm run docs:supported-actions\` after changing tool registration or action enums. \`npm run check\` fails when this generated page no longer matches the registered surface. `; } async function main() { const checkOnly = process.argv.slice(2).includes("--check"); const unknown = process.argv.slice(2).filter((argument) => argument !== "--check"); if (unknown.length > 0) throw new Error(`Unknown argument: ${unknown[0]}`); const markdown = await generateSupportedActionsMarkdown(); if (checkOnly) { const current = await readFile(OUTPUT_PATH, "utf8").catch(() => ""); if (current.replaceAll("\r\n", "\n") !== markdown) { throw new Error("docs/supported-actions.md is stale. Run npm run docs:supported-actions."); } process.stdout.write("Supported actions catalog is current.\n"); return; } await writeFile(OUTPUT_PATH, markdown, "utf8"); process.stdout.write(`Wrote ${OUTPUT_PATH}\n`); } if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { main().catch((error) => { process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; }); }