diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json new file mode 100644 index 0000000..89f316b --- /dev/null +++ b/.claude-plugin/marketplace.json @@ -0,0 +1,18 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-marketplace.json", + "name": "makemyclip", + "description": "MakeMyClip's Claude Code plugins.", + "owner": { + "name": "MakeMyClip", + "url": "https://github.com/MakeMyClip" + }, + "plugins": [ + { + "name": "makemyclip-editor", + "source": "./", + "description": "Local video editor Claude drives: remove silence, edit by transcript, add captions, export to Final Cut Pro, DaVinci Resolve or Premiere Pro.", + "category": "media", + "tags": ["video", "video-editing", "ffmpeg", "captions", "transcription"] + } + ] +} diff --git a/.claude-plugin/plugin.json b/.claude-plugin/plugin.json new file mode 100644 index 0000000..a103c43 --- /dev/null +++ b/.claude-plugin/plugin.json @@ -0,0 +1,23 @@ +{ + "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", + "name": "makemyclip-editor", + "displayName": "MakeMyClip Editor", + "description": "Edit videos by chatting: remove silence and repeated takes, edit by transcript with a local whisper.cpp, add captions, render an mp4 or export to Final Cut Pro, DaVinci Resolve or Premiere Pro. Runs FFmpeg on your machine; no API key.", + "author": { + "name": "MakeMyClip", + "url": "https://makemyclip.com" + }, + "homepage": "https://github.com/MakeMyClip/editor#readme", + "repository": "https://github.com/MakeMyClip/editor", + "license": "MIT", + "keywords": [ + "video", + "video-editing", + "ffmpeg", + "captions", + "subtitles", + "transcription", + "silence-removal", + "fcpxml" + ] +} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dd3fce8..9d3ec53 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,3 +35,6 @@ jobs: - run: pnpm test - run: pnpm build + + # The Claude Desktop extension: validates the manifest and packs it. + - run: pnpm build:mcpb diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2f43acd..4d8b7be 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -13,7 +13,7 @@ jobs: contents: write # write CHANGELOG, create GitHub releases issues: write # comment on released issues pull-requests: write # comment on released PRs - id-token: write # npm Trusted Publishing via OIDC + id-token: write # npm Trusted Publishing and MCP Registry login via OIDC steps: - name: Checkout uses: actions/checkout@v7 @@ -43,6 +43,30 @@ jobs: - name: Build run: pnpm build + # The MCP Registry's CLI, verified against its release signature. The + # release step runs it (as semantic-release's successCmd) once npm has + # the new version and the .mcpb is on the GitHub release. + - name: Install cosign + uses: sigstore/cosign-installer@6f9f17788090df1f26f669e9d70d6ae9567deba6 # v4.1.2 + + - name: Install mcp-publisher + env: + MCP_PUBLISHER_VERSION: v1.8.8 + working-directory: ${{ runner.temp }} + run: | + set -euo pipefail + archive="mcp-publisher_linux_amd64.tar.gz" + base_url="https://github.com/modelcontextprotocol/registry/releases/download/${MCP_PUBLISHER_VERSION}" + curl --fail --location --silent --show-error --output "$archive" "$base_url/$archive" + curl --fail --location --silent --show-error --output "$archive.sigstore.json" "$base_url/$archive.sigstore.json" + cosign verify-blob \ + --bundle "$archive.sigstore.json" \ + --certificate-identity "https://github.com/modelcontextprotocol/registry/.github/workflows/release.yml@refs/tags/${MCP_PUBLISHER_VERSION}" \ + --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \ + "$archive" + tar xzf "$archive" mcp-publisher + echo "$RUNNER_TEMP" >> "$GITHUB_PATH" + - name: Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitignore b/.gitignore index 872d5f6..0bf3c31 100644 --- a/.gitignore +++ b/.gitignore @@ -141,3 +141,7 @@ dist vite.config.js.timestamp-* vite.config.ts.timestamp-* .vite/ + +# Claude Desktop extension build (pnpm build:mcpb) and release assets +/build/ +/release/ diff --git a/.releaserc.json b/.releaserc.json index 66b0e03..778a55b 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -7,14 +7,27 @@ "preset": "angular", "releaseRules": [ { "type": "chore", "scope": "deps", "release": "patch" }, - { "type": "chore", "scope": "security", "release": "patch" } + { "type": "chore", "scope": "security", "release": "patch" }, + { "type": "feat", "release": "patch" } ] } ], "@semantic-release/release-notes-generator", ["@semantic-release/changelog", { "changelogFile": "CHANGELOG.md" }], ["@semantic-release/npm", { "tarballDir": "release", "npmPublish": false }], - ["@semantic-release/exec", { "publishCmd": "npm publish --provenance --access public" }], - "@semantic-release/github" + [ + "@semantic-release/exec", + { + "prepareCmd": "pnpm build:mcpb && pnpm registry:prepare", + "publishCmd": "npm publish --provenance --access public", + "successCmd": "pnpm registry:publish" + } + ], + [ + "@semantic-release/github", + { + "assets": [{ "path": "release/*.mcpb", "label": "Claude Desktop extension (.mcpb)" }] + } + ] ] } diff --git a/AGENTS.md b/AGENTS.md index b9df9ba..18b35f3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -97,7 +97,7 @@ Each tool lives in `src/tools/.ts` and exports: Rules: - One tool per file. - Zod schema is the single source of input truth. -- Handlers return a typed result — `{ path, ... }` for tools that write a file (`ingest.ts`, `inspect.ts`, `split.ts`, `undo.ts` and `delete-op.ts` return other shapes) — never raw FFmpeg output. +- Handlers return a typed result — `{ path, ... }` for tools that write a file (`ingest.ts`, `inspect.ts`, `split.ts`, `undo.ts`, `delete-op.ts` and `doctor.ts` return other shapes) — never raw FFmpeg output. - Handlers must be idempotent given the same input + workspace state. ## FFmpeg diff --git a/README.md b/README.md index cd8153b..025bab7 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,13 @@ All of it works from chat (the Claude Code skill or the MCP server), the CLI, an npx skills add MakeMyClip/editor ``` +Or install it as a **Claude Code plugin** from this repo's marketplace, inside Claude Code: + +```text +/plugin marketplace add MakeMyClip/editor +/plugin install makemyclip-editor@makemyclip +``` + **As a CLI** (for scripts and CI): ```bash @@ -68,6 +75,13 @@ clip --help clip ui # opens http://127.0.0.1:5573 ``` +**As a Claude Desktop extension** (one click, no Node or config file): + +1. Install FFmpeg: `brew install ffmpeg` on macOS, `winget install Gyan.FFmpeg` on Windows. +2. Download [`makemyclip-editor.mcpb`](https://github.com/MakeMyClip/editor/releases/latest/download/makemyclip-editor.mcpb) and double-click it, or drag it into Claude Desktop. + +Put your videos in the extension's **Media folder** (by default `Documents/MakeMyClip`); the editor reads and writes only there. The extension doesn't include FFmpeg (see [License](#license)): it finds the one Homebrew, MacPorts, winget, Scoop or Chocolatey installed, or you pick it in the extension's settings, along with the optional whisper model. + **As an MCP server** (drive it from Claude Desktop / Cursor / any MCP client — no API key, on your existing Claude): ```jsonc @@ -84,7 +98,7 @@ clip ui # opens http://127.0.0.1:5573 } ``` -The Claude Code skill auto-discovers triggers and shells out via `npx -y` on demand. The MCP server exposes the timeline tools (show / edit / check / transcribe / undo / redo / export) over stdio; media stays confined to `MAKEMYCLIP_WORKSPACE`, and `MAKEMYCLIP_WHISPER_MODEL` is only needed for transcription and captions. Everything runs locally and offline — no API key. +It is listed in the [official MCP Registry](https://registry.modelcontextprotocol.io/v0/servers?search=io.github.MakeMyClip/editor) as `io.github.MakeMyClip/editor`, so MCP clients that browse the registry can install it from there. The Claude Code skill auto-discovers triggers and shells out via `npx -y` on demand. The MCP server exposes the timeline tools (show / edit / check / transcribe / undo / redo / export) and a `doctor` setup check over stdio; media stays confined to `MAKEMYCLIP_WORKSPACE`, and `MAKEMYCLIP_WHISPER_MODEL` is only needed for transcription and captions. Everything runs locally and offline — no API key. **Optional: local transcription** (for editing by transcript, removing retakes, and captions): @@ -95,6 +109,14 @@ export MAKEMYCLIP_WHISPER_MODEL=/path/to/ggml-base.en.bin Download a model such as `ggml-base.en.bin` from [huggingface.co/ggerganov/whisper.cpp](https://huggingface.co/ggerganov/whisper.cpp), and set `MAKEMYCLIP_WHISPER_PATH` if `whisper-cli` isn't on your `PATH`. The editor never downloads a model itself. +**Check your setup:** + +```bash +npx -y @makemyclip/editor doctor # add --json for a machine-readable report +``` + +It checks Node, which FFmpeg runs and whether it can draw text and captions, the workspace, a caption font, and the optional whisper.cpp and model, and prints the fix for anything missing. In Claude Desktop or another MCP client, ask "is my setup OK?" and Claude runs the same check. + --- ## Quick start: clean up a talking-head recording @@ -258,7 +280,7 @@ Every edit is undoable (`clip timeline undo` / `redo`, `clip timeline log`), and - **Language:** TypeScript (Node 24+) and React 19 (browser UI) - **Timeline schema:** [Zod](https://zod.dev/) (shared with the [MakeMyClip.com](https://makemyclip.com) web app) - **Subprocess:** [execa](https://github.com/sindresorhus/execa) — args as an array, no shell injection -- **FFmpeg:** bundled via `ffmpeg-static`, with `MAKEMYCLIP_FFMPEG_PATH` override or system-binary fallback +- **FFmpeg:** downloaded by `ffmpeg-static` when the npm package installs, with a `MAKEMYCLIP_FFMPEG_PATH` override and an installed FFmpeg as the fallback (the Claude Desktop extension always uses an installed one) - **Transcription:** optional, user-installed [whisper.cpp](https://github.com/ggml-org/whisper.cpp), run as a subprocess - **MCP:** [`@modelcontextprotocol/sdk`](https://modelcontextprotocol.io/) — the `clip mcp` server (Claude Desktop / any MCP client) - **UI:** Hono server + Vite + React, plain CSS @@ -271,7 +293,7 @@ Every edit is undoable (`clip timeline undo` / `redo`, `clip timeline log`), and ### Setup & installation **Do I need FFmpeg installed separately?** -No. MakeMyClip Editor bundles FFmpeg via `ffmpeg-static`, so installing the npm package or running `npx skills add MakeMyClip/editor` is all you need. For your own FFmpeg build (LGPL-only, custom codecs, hardware acceleration), set `MAKEMYCLIP_FFMPEG_PATH` to its path and the bundled binary is ignored. +Not for the npm package, the CLI or the Claude Code skill: `ffmpeg-static` downloads an FFmpeg build for your platform when the package installs. The Claude Desktop extension is the exception: it doesn't include FFmpeg, so install it first (`brew install ffmpeg` or `winget install Gyan.FFmpeg`). For your own FFmpeg build (LGPL-only, custom codecs, hardware acceleration), set `MAKEMYCLIP_FFMPEG_PATH` to its path and the downloaded binary is ignored. `clip doctor` shows which FFmpeg runs. **What platforms does it run on?** macOS, Linux, and Windows — anywhere Node 24+ runs. The browser UI binds to `127.0.0.1` and needs no internet connection. @@ -317,8 +339,8 @@ Every edit appends to `session.json` and produces a new output file in the works **Is it free?** Yes — MIT-licensed, free forever for local editing. No paid tier, no signup, no upsell in the editor. Paid AI-generation features (voice synthesis, music, stock footage) will live on [MakeMyClip.com](https://makemyclip.com) when they ship, behind a separate `@makemyclip/generation` package. -**What's the license situation with the bundled FFmpeg?** -The MakeMyClip Editor source code is [MIT](./LICENSE). The bundled FFmpeg binary (via `ffmpeg-static`) is GPL-licensed because it includes codecs like libx264 and libx265. Subprocess invocation keeps GPL terms confined to the binary, not your application code. For LGPL-only or custom-build requirements (e.g. iOS App Store), set `MAKEMYCLIP_FFMPEG_PATH` to your own binary. +**What's the license situation with FFmpeg?** +The MakeMyClip Editor source code is [MIT](./LICENSE). FFmpeg is a separate program the editor runs as a subprocess, so its license covers the binary, not your application code. The build `ffmpeg-static` downloads is GPLv3 on Intel Macs, Windows and Linux. On Apple Silicon it is a build with nonfree parts that FFmpeg marks as not redistributable, so the Claude Desktop extension ships no FFmpeg at all. For LGPL-only or custom-build requirements (e.g. iOS App Store), set `MAKEMYCLIP_FFMPEG_PATH` to your own binary. **How is this different from running FFmpeg directly?** FFmpeg is one binary with a complex CLI; MakeMyClip Editor is 19 named tools with typed inputs, a session log, snapshot/undo, a browser UI, and an agent-callable API. You're still running FFmpeg under the hood — but with a catalog any LLM can call, input validation that prevents bad commands, and a log you can inspect or version-control. @@ -407,4 +429,9 @@ Built on the work of [FFmpeg](https://ffmpeg.org/), [whisper.cpp](https://github ## License -[MIT](./LICENSE) for the editor source; GPL for the bundled FFmpeg binary (subprocess-isolated). Use it, fork it, ship it. +[MIT](./LICENSE) for the editor source. FFmpeg is a separate program, run as a subprocess under its own license: + +- **npm package, CLI, Claude Code skill:** `ffmpeg-static` downloads an FFmpeg build from its GitHub releases when the package installs. The Intel macOS, Windows and Linux builds are GPLv3. The Apple Silicon build includes nonfree parts and isn't redistributable. +- **Claude Desktop extension (`.mcpb`):** includes no FFmpeg. It runs the one you install. + +Set `MAKEMYCLIP_FFMPEG_PATH` to use an FFmpeg of your choice. Use it, fork it, ship it. diff --git a/RELEASING.md b/RELEASING.md index 08750ee..cbb72f1 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -19,11 +19,18 @@ When the workflow publishes, it: 1. Reads commits since the last tag, decides the next version 2. Updates `CHANGELOG.md` for the release notes 3. Publishes to npm at `@makemyclip/editor` with provenance attestation -4. Creates a GitHub Release with auto-generated notes +4. Creates a GitHub Release with auto-generated notes, with the Claude Desktop extension `makemyclip-editor.mcpb` attached (built by `pnpm build:mcpb`) 5. Tags the commit as `v` +6. Lists the version in the official MCP Registry as `io.github.MakeMyClip/editor` (`pnpm registry:publish`, after npm has it and the `.mcpb` is on the release) The package shows a verified publisher badge on npmjs.com because of provenance. +### MCP Registry + +`server.json` holds the listing. At release, `pnpm registry:prepare` stamps it with the version and adds the `.mcpb` (its release URL and SHA-256); that change stays in the release job's checkout. The registry checks that the npm version's `package.json` has `"mcpName": "io.github.MakeMyClip/editor"`, case included, and that the `.mcpb` URL is reachable. The release job logs in with GitHub OIDC (`mcp-publisher login github-oidc`), which grants the `io.github.MakeMyClip/*` namespace; `mcp-publisher` is downloaded and checked against its sigstore signature. + +If the registry step fails, npm and the GitHub release are already out, and re-running the job does nothing (semantic-release sees no new commits). List the version by hand from a checkout of the tag (its `package.json` still says `0.0.0-development`, so pass the version): `gh release download v --pattern makemyclip-editor.mcpb --dir release` (the registry entry must match the released file's SHA-256), `pnpm registry:prepare `, then `mcp-publisher login github --token ` (a token with `read:org`, from an owner of the MakeMyClip organization; the interactive login can't publish to an organization namespace) and `mcp-publisher publish`. + ## First-time setup These steps run **once**, before the first automatic release. diff --git a/SKILL.md b/SKILL.md index 41c7b90..db5111b 100644 --- a/SKILL.md +++ b/SKILL.md @@ -365,7 +365,7 @@ These are the recovery loop the council called must-haves. Snapshot before any u npx skills add MakeMyClip/editor ``` -That's it. +That's it. Or, as a Claude Code plugin: `/plugin marketplace add MakeMyClip/editor`, then `/plugin install makemyclip-editor@makemyclip`. If a command fails with a setup error (FFmpeg, its drawtext filter, whisper.cpp, a font, the workspace), or the user asks whether their setup works, run `npx -y @makemyclip/editor doctor`: it lists each check with the fix for anything missing. ## License diff --git a/biome.json b/biome.json index a05be5e..74e9184 100644 --- a/biome.json +++ b/biome.json @@ -10,6 +10,8 @@ "includes": [ "src/**/*", "tests/**/*", + "scripts/**/*", + "mcpb/**/*", "*.json", "*.md", "!dist", diff --git a/docs/backlog/apple-silicon-ffmpeg-build.md b/docs/backlog/apple-silicon-ffmpeg-build.md new file mode 100644 index 0000000..e3527ee --- /dev/null +++ b/docs/backlog/apple-silicon-ffmpeg-build.md @@ -0,0 +1,3 @@ +# Redistributable FFmpeg for Apple Silicon + +`ffmpeg-static`'s darwin-arm64 binary (FFmpeg 6.0 from osxexperts) is built with `--enable-nonfree`, and `ffmpeg -L` says it is not legally redistributable. npm installs on Apple Silicon download it, and the Claude Desktop extension ships no FFmpeg because of it. Find or build a GPL arm64 FFmpeg with drawtext and vidstab (a pinned CI build from source, with its corresponding source published) so Apple Silicon users get a redistributable build, and so the `.mcpb` could include FFmpeg for one-click setup. diff --git a/docs/backlog/plugin-mcp-server.md b/docs/backlog/plugin-mcp-server.md new file mode 100644 index 0000000..15e2f49 --- /dev/null +++ b/docs/backlog/plugin-mcp-server.md @@ -0,0 +1,3 @@ +# MCP server inside the Claude Code plugin + +The `makemyclip-editor` plugin ships only the skill, which drives the `clip` CLI. Declaring the MCP server in `plugin.json` (`mcpServers`) would give Claude Code the timeline tools with image results (frames, cut contact sheets) as well. The plugin directory blocks an unpinned `npx` server, so this needs `npx -y @makemyclip/editor@ mcp` kept in step with each release: the release job would have to commit the bump back (for example `@semantic-release/git`), which it doesn't do today. diff --git a/llms.txt b/llms.txt index 1792b18..5ac5b39 100644 --- a/llms.txt +++ b/llms.txt @@ -6,11 +6,11 @@ - **Name:** MakeMyClip Editor (npm `@makemyclip/editor`, current release v0.4.0) - **Type:** Agent-driven video editor (not generative AI) -- **Backend:** FFmpeg (bundled via `ffmpeg-static`); optional user-installed whisper.cpp for transcription +- **Backend:** FFmpeg (downloaded by `ffmpeg-static` at npm install; the Claude Desktop extension uses an installed FFmpeg); optional user-installed whisper.cpp for transcription - **Surfaces:** Claude Code skill · `clip` CLI · browser UI at `127.0.0.1:5573` · MCP server (`clip mcp`) - **Agent access:** Claude Code skill + MCP server (`clip mcp`); no API key — the editor embeds no language model, it runs on whichever Claude you already use - **Tests:** 741 passing -- **License:** MIT (code) + GPL (bundled FFmpeg binary, subprocess-isolated) +- **License:** MIT (code); FFmpeg runs as a subprocess under its own license (GPLv3 builds from `ffmpeg-static`, except a nonfree Apple Silicon build; the `.mcpb` ships no FFmpeg) ## What v0.4.0 does @@ -25,11 +25,13 @@ ## How to install -- Claude Code skill: `npx skills add MakeMyClip/editor` +- Claude Code skill: `npx skills add MakeMyClip/editor`, or as a plugin: `/plugin marketplace add MakeMyClip/editor` then `/plugin install makemyclip-editor@makemyclip` - CLI: `npm i -g @makemyclip/editor` then `clip --help` - Browser UI: `clip ui` (opens http://127.0.0.1:5573) - MCP server: `npx -y @makemyclip/editor mcp` (stdio), with `MAKEMYCLIP_WORKSPACE` set to the media folder +- Claude Desktop extension: install FFmpeg, then open https://github.com/MakeMyClip/editor/releases/latest/download/makemyclip-editor.mcpb - Transcription (optional): install whisper.cpp (macOS: `brew install whisper-cpp`), download a model such as `ggml-base.en.bin`, set `MAKEMYCLIP_WHISPER_MODEL` to its path +- Setup check: `npx -y @makemyclip/editor doctor` (or the MCP `doctor` tool) lists what's missing and how to fix it ## Tool catalog (19 tools) diff --git a/mcpb/manifest.json b/mcpb/manifest.json new file mode 100644 index 0000000..1295d2a --- /dev/null +++ b/mcpb/manifest.json @@ -0,0 +1,119 @@ +{ + "$schema": "https://raw.githubusercontent.com/modelcontextprotocol/mcpb/main/dist/mcpb-manifest-v0.3.schema.json", + "manifest_version": "0.3", + "name": "makemyclip-editor", + "display_name": "MakeMyClip Editor", + "version": "0.0.0-development", + "description": "Local video editor: remove silence, edit by transcript, add captions, export to Final Cut Pro, Resolve or Premiere.", + "long_description": "MakeMyClip Editor is an open-source (MIT) video editor that Claude drives through a timeline you can undo. Ask Claude to cut the pauses or repeated takes out of a recording, cut words using its transcript, add captions, and render an mp4 or export a project for Final Cut Pro, DaVinci Resolve or Premiere Pro. Everything runs on your computer, with no account and no API key.\n\n**Needs FFmpeg.** This extension doesn't include FFmpeg. Install it with `brew install ffmpeg` (macOS) or `winget install Gyan.FFmpeg` (Windows); the extension finds it on its own, or pick the program in the FFmpeg setting. Transcripts and captions also need a local whisper.cpp and a model file. Ask Claude \"is my setup OK?\" and it runs the editor's setup check, with the fix for anything missing.\n\nThe editor only reads and writes files inside the media folder you choose.", + "author": { + "name": "MakeMyClip", + "url": "https://makemyclip.com" + }, + "repository": { + "type": "git", + "url": "https://github.com/MakeMyClip/editor" + }, + "homepage": "https://github.com/MakeMyClip/editor", + "documentation": "https://github.com/MakeMyClip/editor#readme", + "support": "https://github.com/MakeMyClip/editor/issues", + "icon": "icon.png", + "server": { + "type": "node", + "entry_point": "dist/cli.js", + "mcp_config": { + "command": "node", + "args": ["${__dirname}/dist/cli.js", "mcp"], + "env": { + "MAKEMYCLIP_BUNDLE": "mcpb", + "MAKEMYCLIP_WORKSPACE": "${user_config.workspace}", + "MAKEMYCLIP_FFMPEG_PATH": "${user_config.ffmpeg_path}", + "MAKEMYCLIP_WHISPER_MODEL": "${user_config.whisper_model}", + "MAKEMYCLIP_WHISPER_PATH": "${user_config.whisper_path}" + } + } + }, + "tools": [ + { + "name": "timeline_show", + "description": "Read the timeline (tracks, clips, timings, whether it can render) and optionally the frame at a time." + }, + { + "name": "timeline_edit", + "description": "Apply editing verbs as one undoable change: add media, text or color, trim, move, split, remove, remove silence, cut words or retakes, caption speech, transition." + }, + { + "name": "timeline_check", + "description": "Review every hard cut for clipped words or clicks, with a contact sheet of the frames around each cut." + }, + { + "name": "timeline_transcript", + "description": "Transcribe a clip with the local whisper.cpp and return its words with timeline times." + }, + { + "name": "timeline_undo", + "description": "Undo the most recent timeline edit." + }, + { + "name": "timeline_redo", + "description": "Redo the most recently undone timeline edit." + }, + { + "name": "timeline_export", + "description": "Export the timeline as an mp4, an FCPXML project (Final Cut Pro, DaVinci Resolve) or FCP7 XML (Premiere Pro)." + }, + { + "name": "doctor", + "description": "Check the setup (FFmpeg and its filters, the media folder, a caption font, the optional whisper.cpp) and say how to fix what's missing." + } + ], + "keywords": [ + "video", + "video-editing", + "ffmpeg", + "captions", + "transcription", + "silence-removal", + "fcpxml", + "final-cut-pro", + "davinci-resolve", + "premiere-pro" + ], + "license": "MIT", + "compatibility": { + "platforms": ["darwin", "win32"], + "runtimes": { + "node": ">=24.0.0" + } + }, + "user_config": { + "workspace": { + "type": "directory", + "title": "Media folder", + "description": "The editor reads and writes files only inside this folder: put the videos you want to edit here, and exports land here too. Leave blank for a MakeMyClip folder in your Documents.", + "required": false, + "default": "" + }, + "ffmpeg_path": { + "type": "file", + "title": "FFmpeg", + "description": "Optional: the ffmpeg program to use. Leave blank to use an installed FFmpeg (Homebrew, MacPorts, winget, Scoop, Chocolatey, or on PATH).", + "required": false, + "default": "" + }, + "whisper_model": { + "type": "file", + "title": "Whisper model", + "description": "Optional, for transcripts, retake removal and captions: a whisper.cpp model file such as ggml-base.en.bin.", + "required": false, + "default": "" + }, + "whisper_path": { + "type": "file", + "title": "whisper-cli", + "description": "Optional: the whisper.cpp whisper-cli program. Leave blank to use an installed one (Homebrew, or on PATH).", + "required": false, + "default": "" + } + } +} diff --git a/package.json b/package.json index aaa83a1..b6a83c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "name": "@makemyclip/editor", "version": "0.0.0-development", + "mcpName": "io.github.MakeMyClip/editor", "description": "Open-source AI video editor for Claude Code and MCP clients: remove silence, edit by transcript with local whisper.cpp, add captions, and export to Final Cut Pro, DaVinci Resolve or Premiere. Runs locally on FFmpeg.", "keywords": [ "video-editing", @@ -58,6 +59,9 @@ "scripts": { "build": "tsup && pnpm build:ui", "build:ui": "vite build --config src/ui/web/vite.config.ts", + "build:mcpb": "tsx scripts/build-mcpb.ts", + "registry:prepare": "tsx scripts/mcp-registry.ts prepare", + "registry:publish": "tsx scripts/mcp-registry.ts publish", "dev": "tsx src/cli.ts", "dev:ui": "vite --config src/ui/web/vite.config.ts", "start": "node dist/cli.js", @@ -79,6 +83,7 @@ "zod": "^4.6.7" }, "devDependencies": { + "@anthropic-ai/mcpb": "2.1.2", "@biomejs/biome": "^2.6.4", "@semantic-release/changelog": "^7.0.0", "@semantic-release/commit-analyzer": "^13.0.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a63b252..c8ea798 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,6 +191,9 @@ importers: specifier: ^4.6.7 version: 4.6.7 devDependencies: + '@anthropic-ai/mcpb': + specifier: 2.1.2 + version: 2.1.2(supports-color@7.2.0) '@biomejs/biome': specifier: ^2.6.4 version: 2.6.4 @@ -266,6 +269,10 @@ packages: '@actions/io@3.0.2': resolution: {integrity: sha512-nRBchcMM+QK1pdjO7/idu86rbJI5YHUKCvKs0KxnSYbVe3F51UfGxuZX4Qy/fWlp6l7gWFwIkrOzN+oUK03kfw==} + '@anthropic-ai/mcpb@2.1.2': + resolution: {integrity: sha512-goRbBC8ySo7SWb7tRzr+tL6FxDc4JPTRCdgfD2omba7freofvjq5rom1lBnYHZHo6Mizs1jAHJeN53aZbDoy8A==} + hasBin: true + '@babel/code-frame@7.29.7': resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} engines: {node: '>=6.9.0'} @@ -513,6 +520,62 @@ packages: peerDependencies: hono: ^4 || ^5.0.0-0 + '@inquirer/checkbox@3.0.1': + resolution: {integrity: sha512-0hm2nrToWUdD6/UHnel/UKGdk1//ke5zGUpHIvk5ZWmaKezlGxZkOJXNSWsdxO/rEqTkbB3lNC2J6nBElV2aAQ==} + engines: {node: '>=18'} + + '@inquirer/confirm@4.0.1': + resolution: {integrity: sha512-46yL28o2NJ9doViqOy0VDcoTzng7rAb6yPQKU7VDLqkmbCaH4JqK4yk4XqlzNWy9PVC5pG1ZUXPBQv+VqnYs2w==} + engines: {node: '>=18'} + + '@inquirer/core@9.2.1': + resolution: {integrity: sha512-F2VBt7W/mwqEU4bL0RnHNZmC/OxzNx9cOYxHqnXX3MP6ruYvZUZAW9imgN9+h/uBT/oP8Gh888J2OZSbjSeWcg==} + engines: {node: '>=18'} + + '@inquirer/editor@3.0.1': + resolution: {integrity: sha512-VA96GPFaSOVudjKFraokEEmUQg/Lub6OXvbIEZU1SDCmBzRkHGhxoFAVaF30nyiB4m5cEbDgiI2QRacXZ2hw9Q==} + engines: {node: '>=18'} + + '@inquirer/expand@3.0.1': + resolution: {integrity: sha512-ToG8d6RIbnVpbdPdiN7BCxZGiHOTomOX94C2FaT5KOHupV40tKEDozp12res6cMIfRKrXLJyexAZhWVHgbALSQ==} + engines: {node: '>=18'} + + '@inquirer/figures@1.0.15': + resolution: {integrity: sha512-t2IEY+unGHOzAaVM5Xx6DEWKeXlDDcNPeDyUpsRc6CUhBfU3VQOEl+Vssh7VNp1dR8MdUJBWhuObjXCsVpjN5g==} + engines: {node: '>=18'} + + '@inquirer/input@3.0.1': + resolution: {integrity: sha512-BDuPBmpvi8eMCxqC5iacloWqv+5tQSJlUafYWUe31ow1BVXjW2a5qe3dh4X/Z25Wp22RwvcaLCc2siHobEOfzg==} + engines: {node: '>=18'} + + '@inquirer/number@2.0.1': + resolution: {integrity: sha512-QpR8jPhRjSmlr/mD2cw3IR8HRO7lSVOnqUvQa8scv1Lsr3xoAMMworcYW3J13z3ppjBFBD2ef1Ci6AE5Qn8goQ==} + engines: {node: '>=18'} + + '@inquirer/password@3.0.1': + resolution: {integrity: sha512-haoeEPUisD1NeE2IanLOiFr4wcTXGWrBOyAyPZi1FfLJuXOzNmxCJPgUrGYKVh+Y8hfGJenIfz5Wb/DkE9KkMQ==} + engines: {node: '>=18'} + + '@inquirer/prompts@6.0.1': + resolution: {integrity: sha512-yl43JD/86CIj3Mz5mvvLJqAOfIup7ncxfJ0Btnl0/v5TouVUyeEdcpknfgc+yMevS/48oH9WAkkw93m7otLb/A==} + engines: {node: '>=18'} + + '@inquirer/rawlist@3.0.1': + resolution: {integrity: sha512-VgRtFIwZInUzTiPLSfDXK5jLrnpkuSOh1ctfaoygKAdPqjcjKYmGh6sCY1pb0aGnCGsmhUxoqLDUAU0ud+lGXQ==} + engines: {node: '>=18'} + + '@inquirer/search@2.0.1': + resolution: {integrity: sha512-r5hBKZk3g5MkIzLVoSgE4evypGqtOannnB3PKTG9NRZxyFRKcfzrdxXXPcoJQsxJPzvdSU2Rn7pB7lw0GCmGAg==} + engines: {node: '>=18'} + + '@inquirer/select@3.0.1': + resolution: {integrity: sha512-lUDGUxPhdWMkN/fHy1Lk7pF3nK1fh/gqeyWXmctefhxLYxlDsc7vsPBEpxrfVGDsVdyYJsiJoD4bJ1b623cV1Q==} + engines: {node: '>=18'} + + '@inquirer/type@2.0.0': + resolution: {integrity: sha512-XvJRx+2KR3YXyYtPUUy+qd9i7p+GO9Ko6VIIpWlBrpWwXDv8WLFeHTxz35CfQFUiBMLXlGHhGzys7lqit9gWag==} + engines: {node: '>=18'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -913,9 +976,15 @@ packages: '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/mute-stream@0.0.4': + resolution: {integrity: sha512-CPM9nzrCPPJHQNA9keH9CVkVI+WR5kMa+7XEs5jcGQ0VoAGnLv242w8lIVgwAEfmE4oufJRaTc9PNLQl0ioAow==} + '@types/node@10.17.60': resolution: {integrity: sha512-F0KIgDJfy2nA3zMLmWGKxcH2ZVEtCZXHHdOQs2gSaQ27+lNeEfGxzkIw90aXswATX7AZ33tahPbzy6KAfUreVw==} + '@types/node@22.20.5': + resolution: {integrity: sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==} + '@types/node@24.19.2': resolution: {integrity: sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==} @@ -930,6 +999,9 @@ packages: '@types/react@19.3.0': resolution: {integrity: sha512-N0rFCuH9YoxG9/m61l9MfpJKfmLOVU0em7ipIz6TRgSSkvReLB9vL85GB+yr8Bs5leqpvg96JSwF4ZS1s4viQg==} + '@types/wrap-ansi@3.0.0': + resolution: {integrity: sha512-ltIpx+kM7g/MLRZfkbL7EsCEjfzCcScLpkg37eXEtx5kmrAKBkTJwd1GIAjDSL8wTpM6Hzn5YO4pSb91BEwu1g==} + '@vitejs/plugin-react@6.1.2': resolution: {integrity: sha512-fhJNGUdscQa4HkI0xFxwonWyhnVReCPDbXI7FH7jmzpnVCrFQI7PRrhWecO263610vOrs9tr2Y4xcf8uAdFxbw==} engines: {node: ^20.19.0 || >=22.12.0} @@ -1000,6 +1072,10 @@ packages: ajv@8.20.0: resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + ansi-escapes@4.3.2: + resolution: {integrity: sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==} + engines: {node: '>=8'} + ansi-escapes@7.3.0: resolution: {integrity: sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==} engines: {node: '>=18'} @@ -1110,6 +1186,9 @@ packages: resolution: {integrity: sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==} engines: {node: '>=10'} + chardet@0.7.0: + resolution: {integrity: sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==} + chokidar@5.0.0: resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} engines: {node: '>= 20.19.0'} @@ -1131,6 +1210,10 @@ packages: resolution: {integrity: sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==} engines: {node: 10.* || >= 12.*} + cli-width@4.1.0: + resolution: {integrity: sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==} + engines: {node: '>= 12'} + cliui@7.0.4: resolution: {integrity: sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==} @@ -1151,6 +1234,10 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + commander@13.1.0: + resolution: {integrity: sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==} + engines: {node: '>=18'} + commander@4.1.1: resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} engines: {node: '>= 6'} @@ -1399,6 +1486,10 @@ packages: resolution: {integrity: sha512-qCqy1xSoaugxOD4IVHb1ZWdkvCF2W3aE2okKouWj8VwJ+ztskSiXoBnCSJqfKT+cVPJhuJqe9YLbY425+Zh00g==} engines: {node: '>= 18'} + external-editor@3.1.0: + resolution: {integrity: sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==} + engines: {node: '>=4'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1414,6 +1505,9 @@ packages: picomatch: optional: true + fflate@0.8.3: + resolution: {integrity: sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==} + ffmpeg-static@5.3.0: resolution: {integrity: sha512-H+K6sW6TiIX6VGend0KQwthe+kaceeH/luE8dIZyOP35ik7ahYojDuqlTV1bOrtEwl01sy2HFNGQfi5IDJvotg==} engines: {node: '>=16'} @@ -1449,6 +1543,10 @@ packages: fix-dts-default-cjs-exports@1.0.1: resolution: {integrity: sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==} + flora-colossus@2.0.0: + resolution: {integrity: sha512-dz4HxH6pOvbUzZpZ/yXhafjbR2I8cenK5xL0KtBFb7U2ADsR+OwXifnxZjij/pZWF775uSCMzWVd+jDik2H2IA==} + engines: {node: '>= 12'} + forwarded@0.2.0: resolution: {integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==} engines: {node: '>= 0.6'} @@ -1457,6 +1555,10 @@ packages: resolution: {integrity: sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==} engines: {node: '>= 0.8'} + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + fs-extra@11.4.1: resolution: {integrity: sha512-KYAb4c9BJQI6QqGKthV68OHe0badztdXJWKo0WtBA9IuCFPTKvE5ZdUBglP833aMjhaSPNO4A5j/EkzZtGlKjA==} engines: {node: '>=14.14'} @@ -1473,6 +1575,10 @@ packages: resolution: {integrity: sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==} engines: {node: '>=18'} + galactus@1.0.0: + resolution: {integrity: sha512-R1fam6D4CyKQGNlvJne4dkNF+PvUUl7TAJInvTGa9fti9qAv95quQz29GXapA4d8Ec266mJJxFVh82M4GIIGDQ==} + engines: {node: '>= 12'} + get-caller-file@2.0.5: resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} engines: {node: 6.* || 8.* || >= 10.*} @@ -1585,10 +1691,18 @@ packages: resolution: {integrity: sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==} engines: {node: '>=18.18.0'} + iconv-lite@0.4.24: + resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.3: resolution: {integrity: sha512-IKXpvIzjnC9XTAUbVBcMfGS0EPaIXtW6v+zr+RRp+hqULEpo0owZax6wyRwPOJbWbzjYspQwusTsfVr0ifh4uQ==} engines: {node: '>=0.10.0'} + ignore@7.0.14: + resolution: {integrity: sha512-wCBiZ77u1tuWde6G/nfOtLaCTYcFDU5yCzARzK10y+rLhzYoL7a7yEemcopcc2Mzow7gZOIjnV2ym1NYmdiJOQ==} + engines: {node: '>= 4'} + import-fresh@3.3.1: resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} engines: {node: '>=6'} @@ -1912,6 +2026,10 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mute-stream@1.0.0: + resolution: {integrity: sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==} + engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} + mz@2.7.0: resolution: {integrity: sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==} @@ -1934,6 +2052,10 @@ packages: resolution: {integrity: sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==} engines: {node: '>=18'} + node-forge@1.4.3: + resolution: {integrity: sha512-P6wzfh6t70avmV8FrVEAd/36sOPJQ+hkaJVX1Laap9J/77m7M0eyozDvc8SwnDf4P7MP94gwbAAo0rg3YVXT+g==} + engines: {node: '>= 6.13.0'} + normalize-package-data@6.0.2: resolution: {integrity: sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==} engines: {node: ^16.14.0 || >=18.0.0} @@ -2052,6 +2174,10 @@ packages: resolution: {integrity: sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg==} engines: {node: '>=20'} + os-tmpdir@1.0.2: + resolution: {integrity: sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==} + engines: {node: '>=0.10.0'} + p-each-series@3.0.0: resolution: {integrity: sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==} engines: {node: '>=12'} @@ -2202,6 +2328,10 @@ packages: resolution: {integrity: sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A==} engines: {node: '>=20'} + pretty-bytes@5.6.0: + resolution: {integrity: sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==} + engines: {node: '>=6'} + pretty-ms@9.3.1: resolution: {integrity: sha512-HzMy3Geq23nVALD/M2LliU+F+M+gVNsvkQWWqeBZ8HDiCgzo6YPJ/Omrmtq24EFrIsk0a3EkQGEd7bDOo+IhGA==} engines: {node: '>=18'} @@ -2529,6 +2659,10 @@ packages: resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} engines: {node: '>=12.0.0'} + tmp@0.0.33: + resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} + engines: {node: '>=0.6.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -2576,6 +2710,10 @@ packages: resolution: {integrity: sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==} engines: {node: '>=0.6.11 <=0.7.0 || >=0.7.3'} + type-fest@0.21.3: + resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} + engines: {node: '>=10'} + type-fest@1.4.0: resolution: {integrity: sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==} engines: {node: '>=10'} @@ -2612,6 +2750,9 @@ packages: engines: {node: '>=0.8.0'} hasBin: true + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.24.6: resolution: {integrity: sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==} @@ -2773,6 +2914,10 @@ packages: wordwrap@1.0.0: resolution: {integrity: sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==} + wrap-ansi@6.2.0: + resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==} + engines: {node: '>=8'} + wrap-ansi@7.0.0: resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} engines: {node: '>=10'} @@ -2812,6 +2957,10 @@ packages: resolution: {integrity: sha512-9OpKOLeaoNFecEp7P6iYbzze/5CqWoH7N3SMs/6y1XF4nMvFMspEGZzJ6uFi9MmQwXhyzqYoSEKO3tvA3Z/o2w==} engines: {node: ^20.19.0 || ^22.12.0 || >=23} + yoctocolors-cjs@2.1.3: + resolution: {integrity: sha512-U/PBtDf35ff0D8X8D0jfdzHYEPFxAI7jJlxZXwCSez5M3190m+QobIfh+sWDWSHMCWWJN2AWamkegn6vr6YBTw==} + engines: {node: '>=18'} + yoctocolors@2.2.0: resolution: {integrity: sha512-xYqdZFUK/VYazNl/oCDYN+3WloWQwMfZxBoiNt6qNyk+xfOdi598muWE42rNZFp1kNOiqW936q5RhUdnpqElSg==} engines: {node: '>=18'} @@ -2821,6 +2970,9 @@ packages: peerDependencies: zod: ^3.25.28 || ^4 + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + zod@4.6.7: resolution: {integrity: sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==} @@ -2842,6 +2994,20 @@ snapshots: '@actions/io@3.0.2': {} + '@anthropic-ai/mcpb@2.1.2(supports-color@7.2.0)': + dependencies: + '@inquirer/prompts': 6.0.1 + commander: 13.1.0 + fflate: 0.8.3 + galactus: 1.0.0(supports-color@7.2.0) + ignore: 7.0.14 + node-forge: 1.4.3 + pretty-bytes: 5.6.0 + zod: 3.25.76 + zod-to-json-schema: 3.25.2(zod@3.25.76) + transitivePeerDependencies: + - supports-color + '@babel/code-frame@7.29.7': dependencies: '@babel/helper-validator-identifier': 7.29.7 @@ -2985,6 +3151,102 @@ snapshots: dependencies: hono: 4.13.14 + '@inquirer/checkbox@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + + '@inquirer/confirm@4.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + + '@inquirer/core@9.2.1': + dependencies: + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + '@types/mute-stream': 0.0.4 + '@types/node': 22.20.5 + '@types/wrap-ansi': 3.0.0 + ansi-escapes: 4.3.2 + cli-width: 4.1.0 + mute-stream: 1.0.0 + signal-exit: 4.1.0 + strip-ansi: 6.0.1 + wrap-ansi: 6.2.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/editor@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + external-editor: 3.1.0 + + '@inquirer/expand@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/figures@1.0.15': {} + + '@inquirer/input@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + + '@inquirer/number@2.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + + '@inquirer/password@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + ansi-escapes: 4.3.2 + + '@inquirer/prompts@6.0.1': + dependencies: + '@inquirer/checkbox': 3.0.1 + '@inquirer/confirm': 4.0.1 + '@inquirer/editor': 3.0.1 + '@inquirer/expand': 3.0.1 + '@inquirer/input': 3.0.1 + '@inquirer/number': 2.0.1 + '@inquirer/password': 3.0.1 + '@inquirer/rawlist': 3.0.1 + '@inquirer/search': 2.0.1 + '@inquirer/select': 3.0.1 + + '@inquirer/rawlist@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/type': 2.0.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/search@2.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + yoctocolors-cjs: 2.1.3 + + '@inquirer/select@3.0.1': + dependencies: + '@inquirer/core': 9.2.1 + '@inquirer/figures': 1.0.15 + '@inquirer/type': 2.0.0 + ansi-escapes: 4.3.2 + yoctocolors-cjs: 2.1.3 + + '@inquirer/type@2.0.0': + dependencies: + mute-stream: 1.0.0 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.6.0 @@ -3335,8 +3597,16 @@ snapshots: '@types/estree@1.0.9': {} + '@types/mute-stream@0.0.4': + dependencies: + '@types/node': 24.19.2 + '@types/node@10.17.60': {} + '@types/node@22.20.5': + dependencies: + undici-types: 6.21.0 + '@types/node@24.19.2': dependencies: undici-types: 7.24.6 @@ -3351,6 +3621,8 @@ snapshots: dependencies: csstype: 3.2.3 + '@types/wrap-ansi@3.0.0': {} + '@vitejs/plugin-react@6.1.2(vite@8.3.5(@types/node@24.19.2)(esbuild@0.28.2)(tsx@4.23.15))': dependencies: '@rolldown/pluginutils': 1.0.1 @@ -3405,6 +3677,10 @@ snapshots: json-schema-traverse: 1.0.0 require-from-string: 2.0.2 + ansi-escapes@4.3.2: + dependencies: + type-fest: 0.21.3 + ansi-escapes@7.3.0: dependencies: environment: 1.1.0 @@ -3501,6 +3777,8 @@ snapshots: char-regex@1.0.2: {} + chardet@0.7.0: {} + chokidar@5.0.0: dependencies: readdirp: 5.1.1 @@ -3526,6 +3804,8 @@ snapshots: optionalDependencies: '@colors/colors': 1.5.0 + cli-width@4.1.0: {} + cliui@7.0.4: dependencies: string-width: 4.2.3 @@ -3550,6 +3830,8 @@ snapshots: color-name@1.1.4: {} + commander@13.1.0: {} + commander@4.1.1: {} compare-func@2.0.0: @@ -3840,6 +4122,12 @@ snapshots: transitivePeerDependencies: - supports-color + external-editor@3.1.0: + dependencies: + chardet: 0.7.0 + iconv-lite: 0.4.24 + tmp: 0.0.33 + fast-deep-equal@3.1.3: {} fast-uri@3.1.9: {} @@ -3848,6 +4136,8 @@ snapshots: optionalDependencies: picomatch: 4.0.7 + fflate@0.8.3: {} + ffmpeg-static@5.3.0(supports-color@7.2.0): dependencies: '@derhuerst/http-basic': 8.2.4 @@ -3897,10 +4187,23 @@ snapshots: mlly: 1.8.2 rollup: 4.65.1 + flora-colossus@2.0.0(supports-color@7.2.0): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + fs-extra: 10.1.0 + transitivePeerDependencies: + - supports-color + forwarded@0.2.0: {} fresh@2.0.0: {} + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + fs-extra@11.4.1: dependencies: graceful-fs: 4.2.11 @@ -3914,6 +4217,14 @@ snapshots: function-timeout@1.0.2: {} + galactus@1.0.0(supports-color@7.2.0): + dependencies: + debug: 4.4.3(supports-color@7.2.0) + flora-colossus: 2.0.0(supports-color@7.2.0) + fs-extra: 10.1.0 + transitivePeerDependencies: + - supports-color + get-caller-file@2.0.5: {} get-east-asian-width@1.7.0: {} @@ -4036,10 +4347,16 @@ snapshots: human-signals@8.0.1: {} + iconv-lite@0.4.24: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.3: dependencies: safer-buffer: 2.1.2 + ignore@7.0.14: {} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 @@ -4284,6 +4601,8 @@ snapshots: ms@2.1.3: {} + mute-stream@1.0.0: {} + mz@2.7.0: dependencies: any-promise: 1.3.0 @@ -4307,6 +4626,8 @@ snapshots: emojilib: 2.4.0 skin-tone: 2.0.0 + node-forge@1.4.3: {} + normalize-package-data@6.0.2: dependencies: hosted-git-info: 7.0.2 @@ -4359,6 +4680,8 @@ snapshots: powershell-utils: 0.2.1 wsl-utils: 1.0.1 + os-tmpdir@1.0.2: {} + p-each-series@3.0.0: {} p-event@6.0.1: @@ -4471,6 +4794,8 @@ snapshots: powershell-utils@0.2.1: {} + pretty-bytes@5.6.0: {} + pretty-ms@9.3.1: dependencies: parse-ms: 4.0.0 @@ -4899,6 +5224,10 @@ snapshots: fdir: 6.5.0(picomatch@4.0.7) picomatch: 4.0.7 + tmp@0.0.33: + dependencies: + os-tmpdir: 1.0.2 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -4946,6 +5275,8 @@ snapshots: tunnel@0.0.6: {} + type-fest@0.21.3: {} + type-fest@1.4.0: {} type-fest@2.19.0: {} @@ -4971,6 +5302,8 @@ snapshots: uglify-js@3.19.3: optional: true + undici-types@6.21.0: {} + undici-types@7.24.6: {} undici@6.29.1: {} @@ -5052,6 +5385,12 @@ snapshots: wordwrap@1.0.0: {} + wrap-ansi@6.2.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi@7.0.0: dependencies: ansi-styles: 4.3.0 @@ -5098,10 +5437,18 @@ snapshots: y18n: 5.0.8 yargs-parser: 22.0.0 + yoctocolors-cjs@2.1.3: {} + yoctocolors@2.2.0: {} + zod-to-json-schema@3.25.2(zod@3.25.76): + dependencies: + zod: 3.25.76 + zod-to-json-schema@3.25.2(zod@4.6.7): dependencies: zod: 4.6.7 + zod@3.25.76: {} + zod@4.6.7: {} diff --git a/scripts/build-mcpb.ts b/scripts/build-mcpb.ts new file mode 100644 index 0000000..d33502f --- /dev/null +++ b/scripts/build-mcpb.ts @@ -0,0 +1,87 @@ +/** + * Build the Claude Desktop extension, `release/makemyclip-editor.mcpb`: + * the built CLI and its production dependencies, without FFmpeg. The Apple + * Silicon build ffmpeg-static downloads isn't redistributable, so the extension + * runs the FFmpeg the user installs (see the README's License section). + * Run `pnpm build` first. + */ +import { existsSync } from 'node:fs'; +import { cp, mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { execa } from 'execa'; + +const root = resolve(import.meta.dirname, '..'); +const stage = join(root, 'build', 'mcpb'); +const outDir = join(root, 'release'); + +async function main(): Promise { + if (!existsSync(join(root, 'dist', 'cli.js'))) { + throw new Error('dist/cli.js is missing: run `pnpm build` first.'); + } + const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as { + version: string; + }; + + await rm(stage, { recursive: true, force: true }); + await mkdir(stage, { recursive: true }); + for (const file of [ + 'package.json', + 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', + 'README.md', + 'LICENSE', + ]) { + await cp(join(root, file), join(stage, file)); + } + await cp(join(root, 'dist'), join(stage, 'dist'), { + recursive: true, + filter: (src) => !src.endsWith('.d.ts'), + }); + await cp(join(root, 'logo.png'), join(stage, 'icon.png')); + + // A flat production install, the layout npm makes: `mcpb pack` follows + // symlinks, so pnpm's default layout would pack its whole store. + // --ignore-scripts keeps ffmpeg-static from downloading FFmpeg. + await execa( + 'pnpm', + ['install', '--prod', '--frozen-lockfile', '--ignore-scripts', '--config.node-linker=hoisted'], + { cwd: stage, stdio: 'inherit' }, + ); + const ffmpegStaticDir = join(stage, 'node_modules', 'ffmpeg-static'); + const binaries = (await readdir(ffmpegStaticDir)).filter((f) => /^ffmpeg(\.exe)?$/.test(f)); + if (binaries.length > 0) { + throw new Error( + `FFmpeg must not ship in the extension, but ${ffmpegStaticDir} has ${binaries}.`, + ); + } + + const manifest = JSON.parse(await readFile(join(root, 'mcpb', 'manifest.json'), 'utf8')) as { + version: string; + }; + manifest.version = version; + await writeFile(join(stage, 'manifest.json'), `${JSON.stringify(manifest, null, 2)}\n`); + await writeFile( + join(stage, '.mcpbignore'), + [ + 'pnpm-lock.yaml', + 'pnpm-workspace.yaml', + 'node_modules/.pnpm', + 'node_modules/.modules.yaml', + '', + ].join('\n'), + ); + + await mkdir(outDir, { recursive: true }); + // No version in the name, so `releases/latest/download/makemyclip-editor.mcpb` + // is a stable link to the newest one. + const output = join(outDir, 'makemyclip-editor.mcpb'); + const mcpb = { cwd: root, preferLocal: true, stdio: 'inherit' } as const; + await execa('mcpb', ['validate', join(stage, 'manifest.json')], mcpb); + await execa('mcpb', ['pack', stage, output], mcpb); + process.stdout.write(`${output}\n`); +} + +main().catch((err: unknown) => { + process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); +}); diff --git a/scripts/mcp-registry.ts b/scripts/mcp-registry.ts new file mode 100644 index 0000000..15d4bcd --- /dev/null +++ b/scripts/mcp-registry.ts @@ -0,0 +1,92 @@ +/** + * List a release in the official MCP Registry (registry.modelcontextprotocol.io). + * + * prepare [version] + * Stamp server.json for `version`, by default the one in + * package.json (which semantic-release has just written), and + * add the Claude Desktop extension as an `mcpb` package: its + * release URL and the SHA-256 of release/makemyclip-editor.mcpb. + * publish `mcp-publisher login github-oidc`, then `mcp-publisher publish`. + * Retried, since the registry reads the new npm version, which + * can take a minute to appear. Needs `mcp-publisher` on PATH and + * a GitHub Actions OIDC token (`id-token: write`). + * + * server.json is changed in the release job's checkout only; nothing is + * committed back. + */ +import { createHash } from 'node:crypto'; +import { readFile, writeFile } from 'node:fs/promises'; +import { join, resolve } from 'node:path'; +import { setTimeout as sleep } from 'node:timers/promises'; +import { execa } from 'execa'; + +const root = resolve(import.meta.dirname, '..'); +const serverJsonPath = join(root, 'server.json'); +const bundlePath = join(root, 'release', 'makemyclip-editor.mcpb'); +const REPO = 'https://github.com/MakeMyClip/editor'; + +interface RegistryPackage { + registryType: string; + identifier: string; + version?: string; + fileSha256?: string; + transport: { type: string }; + [key: string]: unknown; +} + +interface ServerJson { + version: string; + packages: RegistryPackage[]; + [key: string]: unknown; +} + +async function prepare(versionArg?: string): Promise { + const version = + versionArg ?? + (JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as { version: string }).version; + const server = JSON.parse(await readFile(serverJsonPath, 'utf8')) as ServerJson; + const sha256 = createHash('sha256') + .update(await readFile(bundlePath)) + .digest('hex'); + + server.version = version; + const packages = server.packages.filter((p) => p.registryType !== 'mcpb'); + for (const p of packages) if (p.registryType === 'npm') p.version = version; + packages.push({ + registryType: 'mcpb', + identifier: `${REPO}/releases/download/v${version}/makemyclip-editor.mcpb`, + version, + fileSha256: sha256, + transport: { type: 'stdio' }, + }); + server.packages = packages; + await writeFile(serverJsonPath, `${JSON.stringify(server, null, 2)}\n`); + process.stdout.write(`server.json: ${version}, makemyclip-editor.mcpb sha256 ${sha256}\n`); +} + +async function publish(): Promise { + await execa('mcp-publisher', ['login', 'github-oidc'], { cwd: root, stdio: 'inherit' }); + const attempts = 6; + for (let attempt = 1; ; attempt++) { + try { + await execa('mcp-publisher', ['publish'], { cwd: root, stdio: 'inherit' }); + return; + } catch (err) { + if (attempt === attempts) throw err; + process.stderr.write(`mcp-publisher publish failed (attempt ${attempt}); retrying in 20 s\n`); + await sleep(20_000); + } + } +} + +const [command, version] = process.argv.slice(2); +const run = + command === 'prepare' ? () => prepare(version) : command === 'publish' ? publish : undefined; +if (!run) { + process.stderr.write('Usage: tsx scripts/mcp-registry.ts prepare [version] | publish\n'); + process.exit(1); +} +run().catch((err: unknown) => { + process.stderr.write(`${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); +}); diff --git a/server.json b/server.json new file mode 100644 index 0000000..179a873 --- /dev/null +++ b/server.json @@ -0,0 +1,61 @@ +{ + "$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json", + "name": "io.github.MakeMyClip/editor", + "title": "MakeMyClip Editor", + "description": "Local video editor: cut silence, edit by transcript, add captions, export to Final Cut or Premiere.", + "websiteUrl": "https://github.com/MakeMyClip/editor#readme", + "repository": { + "url": "https://github.com/MakeMyClip/editor", + "source": "github", + "id": "1250722349" + }, + "icons": [ + { + "src": "https://raw.githubusercontent.com/MakeMyClip/editor/main/logo.png", + "mimeType": "image/png", + "sizes": ["1024x1024"] + } + ], + "version": "0.0.0-development", + "packages": [ + { + "registryType": "npm", + "registryBaseUrl": "https://registry.npmjs.org", + "identifier": "@makemyclip/editor", + "version": "0.0.0-development", + "runtimeHint": "npx", + "packageArguments": [{ "type": "positional", "value": "mcp" }], + "transport": { "type": "stdio" }, + "environmentVariables": [ + { + "name": "MAKEMYCLIP_WORKSPACE", + "description": "Folder the editor may read and write; put your videos here. Defaults to a folder in the temp directory.", + "format": "filepath", + "isRequired": false, + "isSecret": false + }, + { + "name": "MAKEMYCLIP_WHISPER_MODEL", + "description": "Optional: a whisper.cpp model file (e.g. ggml-base.en.bin) for transcripts and captions.", + "format": "filepath", + "isRequired": false, + "isSecret": false + }, + { + "name": "MAKEMYCLIP_FFMPEG_PATH", + "description": "Optional: an FFmpeg to use instead of the one ffmpeg-static downloads.", + "format": "filepath", + "isRequired": false, + "isSecret": false + }, + { + "name": "MAKEMYCLIP_WHISPER_PATH", + "description": "Optional: the whisper-cli binary, when it isn't on PATH.", + "format": "filepath", + "isRequired": false, + "isSecret": false + } + ] + } + ] +} diff --git a/src/cli.ts b/src/cli.ts index a773069..b704b91 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -9,6 +9,7 @@ import { adjust } from './tools/adjust.js'; import { chromaKey } from './tools/chroma-key.js'; import { concat } from './tools/concat.js'; import { deleteOp } from './tools/delete-op.js'; +import { doctor, formatDoctorReport } from './tools/doctor.js'; import { highlightReel } from './tools/highlight-reel.js'; import { ingest } from './tools/ingest.js'; import { inspect } from './tools/inspect.js'; @@ -74,6 +75,11 @@ MCP (drive the editor from Claude Desktop / any MCP client — no API key): client's MCP config; it exposes the timeline tools (show / edit / undo / redo / export). +Setup: + clip doctor [--json] Check Node, FFmpeg and its filters, the workspace, the + caption font and the optional whisper.cpp; print a fix + for each problem. Exits 1 when the editor can't run. + Session safety (these tools do not log themselves): clip snapshot [