From 211e32a80be7e19f248826b07215fddd1c3eaa07 Mon Sep 17 00:00:00 2001 From: Slava Yultyyev Date: Sat, 7 Nov 2026 13:25:34 -0800 Subject: [PATCH 1/4] feat(mcp): describe the server for the official MCP Registry server.json lists io.github.MakeMyClip/editor as the npm package run as `npx @makemyclip/editor mcp`, with the env it reads; package.json gets the matching mcpName, which the registry checks on npm. At release, `pnpm registry:prepare` stamps the version and adds the Claude Desktop extension as an mcpb package (release URL + SHA-256), and `pnpm registry:publish` logs in with GitHub OIDC and publishes, retrying while npm catches up. Co-Authored-By: Claude Opus 5.5 --- package.json | 3 ++ scripts/mcp-registry.ts | 90 +++++++++++++++++++++++++++++++++++++++ server.json | 61 ++++++++++++++++++++++++++ tests/server-json.test.ts | 43 +++++++++++++++++++ 4 files changed, 197 insertions(+) create mode 100644 scripts/mcp-registry.ts create mode 100644 server.json create mode 100644 tests/server-json.test.ts diff --git a/package.json b/package.json index d9f7f70..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", @@ -59,6 +60,8 @@ "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", diff --git a/scripts/mcp-registry.ts b/scripts/mcp-registry.ts new file mode 100644 index 0000000..823c733 --- /dev/null +++ b/scripts/mcp-registry.ts @@ -0,0 +1,90 @@ +/** + * List a release in the official MCP Registry (registry.modelcontextprotocol.io). + * + * prepare Stamp server.json for the version in package.json (which + * semantic-release has just written) and add the Claude Desktop + * extension as an `mcpb` package: its release URL and SHA-256. + * Run after `pnpm build: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(): Promise { + const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as { + version: string; + }; + 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 = process.argv[2]; +const run = command === 'prepare' ? prepare : command === 'publish' ? publish : undefined; +if (!run) { + process.stderr.write('Usage: tsx scripts/mcp-registry.ts prepare|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/tests/server-json.test.ts b/tests/server-json.test.ts new file mode 100644 index 0000000..716480d --- /dev/null +++ b/tests/server-json.test.ts @@ -0,0 +1,43 @@ +import { readFileSync } from 'node:fs'; +import { describe, expect, it } from 'vitest'; +import { FFMPEG_PATH_ENV } from '../src/ffmpeg/run.js'; +import { WHISPER_MODEL_ENV, WHISPER_PATH_ENV } from '../src/transcribe/whisper.js'; + +const read = (path: string): Record => + JSON.parse(readFileSync(new URL(path, import.meta.url), 'utf8')) as Record; + +const pkg = read('../package.json') as { name: string; mcpName: string; homepage: string }; +const server = read('../server.json') as { + name: string; + description: string; + repository: { url: string }; + packages: { + registryType: string; + identifier: string; + packageArguments: { value: string }[]; + environmentVariables: { name: string }[]; + }[]; +}; + +describe('MCP Registry server.json', () => { + it('matches the mcpName the registry checks on npm, case included', () => { + expect(server.name).toBe('io.github.MakeMyClip/editor'); + expect(pkg.mcpName).toBe(server.name); + }); + + it('fits the registry limits and points at this repo', () => { + expect(server.description.length).toBeLessThanOrEqual(100); + expect(server.repository.url).toBe(pkg.homepage); + }); + + it('runs `npx @makemyclip/editor mcp` with the env the server reads', () => { + const [npm, ...rest] = server.packages; + expect(rest).toEqual([]); + expect(npm?.registryType).toBe('npm'); + expect(npm?.identifier).toBe(pkg.name); + expect(npm?.packageArguments.map((a) => a.value)).toEqual(['mcp']); + expect(npm?.environmentVariables.map((e) => e.name).sort()).toEqual( + ['MAKEMYCLIP_WORKSPACE', FFMPEG_PATH_ENV, WHISPER_MODEL_ENV, WHISPER_PATH_ENV].sort(), + ); + }); +}); From 01d01a6da0f43cd49f2943676612b4de9216fd61 Mon Sep 17 00:00:00 2001 From: Slava Yultyyev Date: Sat, 7 Nov 2026 13:25:34 -0800 Subject: [PATCH 2/4] ci(ci): publish each release to the MCP Registry The release job installs mcp-publisher v1.8.8, checked against its sigstore signature, and semantic-release stamps server.json in prepare and publishes it in success, after npm and the GitHub release (with the .mcpb) are out. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/release.yml | 26 +++++++++++++++++++++++++- .releaserc.json | 5 +++-- 2 files changed, 28 insertions(+), 3 deletions(-) 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/.releaserc.json b/.releaserc.json index dc78703..553148a 100644 --- a/.releaserc.json +++ b/.releaserc.json @@ -17,8 +17,9 @@ [ "@semantic-release/exec", { - "prepareCmd": "pnpm build:mcpb", - "publishCmd": "npm publish --provenance --access public" + "prepareCmd": "pnpm build:mcpb && pnpm registry:prepare", + "publishCmd": "npm publish --provenance --access public", + "successCmd": "pnpm registry:publish" } ], [ From 81d361723e84e4dcb9f2cb07d69e1381406f6af7 Mon Sep 17 00:00:00 2001 From: Slava Yultyyev Date: Sat, 7 Nov 2026 13:25:34 -0800 Subject: [PATCH 3/4] docs(docs): document the MCP Registry listing and release step Co-Authored-By: Claude Opus 5.5 --- README.md | 2 +- RELEASING.md | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8f61cce..4ea4cb0 100644 --- a/README.md +++ b/README.md @@ -91,7 +91,7 @@ Put your videos in the extension's **Media folder** (by default `Documents/MakeM } ``` -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. +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): diff --git a/RELEASING.md b/RELEASING.md index 08750ee..9f18c29 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: `pnpm build && pnpm build:mcpb`, replace `release/makemyclip-editor.mcpb` with the asset from the GitHub release (the registry entry must match its 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. From b73956df8a153c241c1fe480044b8bf5d01c6352 Mon Sep 17 00:00:00 2001 From: Slava Yultyyev Date: Sat, 7 Nov 2026 13:28:15 -0800 Subject: [PATCH 4/4] fix(mcp): let registry:prepare take the version for a manual retry A checkout of a release tag still says 0.0.0-development, so the recovery steps pass the version and download the released .mcpb. Co-Authored-By: Claude Opus 5.5 --- RELEASING.md | 2 +- scripts/mcp-registry.ts | 24 +++++++++++++----------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 9f18c29..cbb72f1 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -29,7 +29,7 @@ The package shows a verified publisher badge on npmjs.com because of provenance. `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: `pnpm build && pnpm build:mcpb`, replace `release/makemyclip-editor.mcpb` with the asset from the GitHub release (the registry entry must match its 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`. +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 diff --git a/scripts/mcp-registry.ts b/scripts/mcp-registry.ts index 823c733..15d4bcd 100644 --- a/scripts/mcp-registry.ts +++ b/scripts/mcp-registry.ts @@ -1,10 +1,11 @@ /** * List a release in the official MCP Registry (registry.modelcontextprotocol.io). * - * prepare Stamp server.json for the version in package.json (which - * semantic-release has just written) and add the Claude Desktop - * extension as an `mcpb` package: its release URL and SHA-256. - * Run after `pnpm build:mcpb`. + * 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 @@ -39,10 +40,10 @@ interface ServerJson { [key: string]: unknown; } -async function prepare(): Promise { - const { version } = JSON.parse(await readFile(join(root, 'package.json'), 'utf8')) as { - version: string; - }; +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)) @@ -78,10 +79,11 @@ async function publish(): Promise { } } -const command = process.argv[2]; -const run = command === 'prepare' ? prepare : command === 'publish' ? publish : undefined; +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|publish\n'); + process.stderr.write('Usage: tsx scripts/mcp-registry.ts prepare [version] | publish\n'); process.exit(1); } run().catch((err: unknown) => {