katto-mcp 0.5.7 → 0.5.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (5) hide show
  1. package/QUICKSTART.md +35 -35
  2. package/README.md +128 -128
  3. package/SKILL.md +45 -45
  4. package/index.mjs +397 -397
  5. package/package.json +8 -3
package/QUICKSTART.md CHANGED
@@ -1,35 +1,35 @@
1
- # Katto — 60-second quickstart
2
-
3
- Turn a long video into captioned 9:16 short clips from your assistant, your terminal, or
4
- your code. **One quota** (Creator: 25 videos/mo, ≤90 min each · Free: 2/mo), shared across
5
- all three. Create a key at https://katto.tech/dashboard/api-keys (keys can be read-only).
6
-
7
- ## MCP — in Claude, Cursor, ChatGPT
8
- Hosted, zero-install, OAuth 2.1:
9
- ```json
10
- { "mcpServers": { "katto": { "url": "https://mcp.katto.tech/mcp" } } }
11
- ```
12
- Then just ask: *"Clip the best 6 moments from <url> and caption them."*
13
- Prefer local? `npx -y katto-mcp` with `KATTO_API_KEY=sk_live_...`.
14
-
15
- ## CLI — in your terminal or a cron
16
- ```bash
17
- npm i -g katto-cli
18
- katto login # or set KATTO_API_KEY=sk_live_...
19
- katto clip <url> --clips 6 --wait --json | jq '.clips[].url'
20
- ```
21
-
22
- ## REST API — in your product
23
- ```bash
24
- curl -X POST https://katto.tech/api/v1/jobs \
25
- -H "Authorization: Bearer sk_live_..." \
26
- -H "Idempotency-Key: $(uuidgen)" \
27
- -H "Content-Type: application/json" \
28
- -d '{"url":"https://youtube.com/watch?v=..."}'
29
- # → { "job_id": "..." }
30
- # then poll GET /v1/jobs/{id}, or pass a "webhook_url" for a signed completion callback.
31
- ```
32
-
33
- What comes back: finished clips (MP4 + SRT captions + a title and a 0–100 score), 9:16,
34
- word-timed captions, reframed. Re-render the layout/caption style or dub into 8 languages
35
- for free (no quota). Full reference: https://katto.tech/docs/api
1
+ # Katto — 60-second quickstart
2
+
3
+ Turn a long video into captioned 9:16 short clips from your assistant, your terminal, or
4
+ your code. **One quota** (Creator: 25 videos/mo, ≤90 min each · Free: 2/mo), shared across
5
+ all three. Create a key at https://katto.tech/dashboard/api-keys (keys can be read-only).
6
+
7
+ ## MCP — in Claude, Cursor, ChatGPT
8
+ Hosted, zero-install, OAuth 2.1:
9
+ ```json
10
+ { "mcpServers": { "katto": { "url": "https://mcp.katto.tech/mcp" } } }
11
+ ```
12
+ Then just ask: *"Clip the best 6 moments from <url> and caption them."*
13
+ Prefer local? `npx -y katto-mcp` with `KATTO_API_KEY=sk_live_...`.
14
+
15
+ ## CLI — in your terminal or a cron
16
+ ```bash
17
+ npm i -g katto-cli
18
+ katto login # or set KATTO_API_KEY=sk_live_...
19
+ katto clip <url> --clips 6 --wait --json | jq '.clips[].url'
20
+ ```
21
+
22
+ ## REST API — in your product
23
+ ```bash
24
+ curl -X POST https://katto.tech/api/v1/jobs \
25
+ -H "Authorization: Bearer sk_live_..." \
26
+ -H "Idempotency-Key: $(uuidgen)" \
27
+ -H "Content-Type: application/json" \
28
+ -d '{"url":"https://youtube.com/watch?v=..."}'
29
+ # → { "job_id": "..." }
30
+ # then poll GET /v1/jobs/{id}, or pass a "webhook_url" for a signed completion callback.
31
+ ```
32
+
33
+ What comes back: finished clips (MP4 + SRT captions + a title and a 0–100 score), 9:16,
34
+ word-timed captions, reframed. Re-render the layout/caption style or dub into 19 languages
35
+ for free (no quota). Full reference: https://katto.tech/docs/api
package/README.md CHANGED
@@ -1,128 +1,128 @@
1
- # katto-mcp
2
-
3
- [![npm version](https://img.shields.io/npm/v/katto-mcp.svg)](https://www.npmjs.com/package/katto-mcp)
4
- [![npm downloads](https://img.shields.io/npm/dm/katto-mcp.svg)](https://www.npmjs.com/package/katto-mcp)
5
- [![license](https://img.shields.io/npm/l/katto-mcp.svg)](./LICENSE)
6
- [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.miracleweasel%2Fkatto--mcp-blue)](https://registry.modelcontextprotocol.io)
7
-
8
- **The AI video clipping MCP server.** Turn long videos into scored, captioned, vertical 9:16 short clips from any MCP client (Claude, Cursor, Claude Code, ChatGPT, VS Code, and other conformant clients). Drop in a YouTube link, a podcast, or a Twitch VOD and get publish-ready shorts back through natural conversation.
9
-
10
- Powered by [Katto](https://katto.tech), an AI video clipper that turns long-form video (podcasts, interviews, streams, webinars) into short-form clips for TikTok, Reels and YouTube Shorts. Katto is a flat-priced, no-credits alternative to tools like OpusClip: one 25-video quota covers videos up to 90 minutes each, with the API and MCP included on every paid plan (not gated behind an enterprise tier).
11
-
12
- ## What it does
13
-
14
- Ask your agent something like *"clip the best moments from this podcast and reframe them for TikTok"* and Katto will:
15
-
16
- - find the strongest 30 to 90 second moments in a long video and score each clip 0 to 100 on Hook, Flow, Value and Trend,
17
- - reframe to vertical 9:16 with face tracking, split-screen for two speakers, and stacked layouts for gaming,
18
- - burn animated captions (auto-captioned in 99 languages via Whisper large-v3; word-by-word timing in 41 of them, sentence-level in the rest) and optionally dub into 8 languages,
19
- - and hand back publish-ready MP4 files plus SRT caption urls.
20
-
21
- Typical uses: repurpose a YouTube video into shorts, turn a podcast episode into clips, cut highlights from a Twitch VOD, or convert a long interview into vertical social posts.
22
-
23
- ## Two ways to connect
24
-
25
- ### Hosted (zero-install, recommended)
26
-
27
- Point any OAuth-capable MCP client at the hosted endpoint and sign in with your Katto account. The API key never touches your disk, and you can revoke access anytime:
28
-
29
- ```
30
- https://mcp.katto.tech/mcp (Streamable HTTP, OAuth 2.1)
31
- ```
32
-
33
- ### Local (npx)
34
-
35
- For Cursor, CI and scripts. Create an API key at **[katto.tech/dashboard/api-keys](https://katto.tech/dashboard/api-keys)**, then add the server to your MCP client config:
36
-
37
- ```json
38
- {
39
- "mcpServers": {
40
- "katto": {
41
- "command": "npx",
42
- "args": ["-y", "katto-mcp"],
43
- "env": { "KATTO_API_KEY": "sk_live_..." }
44
- }
45
- }
46
- }
47
- ```
48
-
49
- The hosted endpoint above also accepts `Authorization: Bearer sk_live_...` directly for key-based clients.
50
-
51
- ## Install per client
52
-
53
- **Claude Code** (hosted, OAuth):
54
- ```bash
55
- claude mcp add --transport http katto https://mcp.katto.tech/mcp
56
- ```
57
-
58
- **Claude Desktop** — Settings → Connectors → *Add custom connector* → `https://mcp.katto.tech/mcp`, then sign in. (Or add the `npx` block above to `claude_desktop_config.json`.)
59
-
60
- **Cursor** — Settings → MCP → *Add* → paste the `npx` JSON block above (uses `KATTO_API_KEY`).
61
-
62
- **VS Code** (MCP extension) — add the same `npx` block to your MCP settings, or point it at the hosted URL if your client supports remote OAuth servers.
63
-
64
- **ChatGPT** (Developer mode / connectors) — add a custom connector with URL `https://mcp.katto.tech/mcp`.
65
-
66
- ## Tools
67
-
68
- Clipping:
69
- - **`katto_create_clip_job(url, config?)`** clip a long video (YouTube, Twitch, Vimeo, Rumble, Zoom, Dailymotion). Returns a job id.
70
- - **`katto_get_job(id)`** poll until `status` is `completed`; `clips` holds the finished MP4 + caption (SRT) urls.
71
- - **`katto_get_clips(id)`** just the finished clips of a job (MP4 + SRT + title + virality score).
72
- - **`katto_get_transcript(id)`** the job's timestamped transcript segments.
73
- - **`katto_list_jobs(limit?, cursor?, status?)`** your recent jobs, newest first (keyset pagination).
74
- - **`katto_cancel_job(id)`** cancel a running job and refund the monthly video slot.
75
-
76
- Editing (no quota):
77
- - **`katto_rerender_clip(id, clip_index, layout_mode?, caption_style?)`** re-render one clip with a new reframe layout or caption style.
78
- - **`katto_dub_clip(id, clip_index, languages)`** re-render a clip dubbed into one or more of 8 languages.
79
- - **`katto_get_rerender(id, rerender_id)`** poll a re-render for the new versioned clip url.
80
-
81
- Account and reference:
82
- - **`katto_get_usage()`** your plan and remaining monthly video quota.
83
- - **`katto_get_account()`** the connected account, this key's scopes, and quota.
84
- - **`katto_get_brand_kit()`** your saved brand kits (colors, caption font, layout, watermark).
85
- - **`katto_get_webhook_secret()`** your webhook signing secret and how to verify signed callbacks (returns a secret — treat it like a credential).
86
- - **`katto_list_sources()`** the video platforms Katto can clip from, with an example url each.
87
- - **`katto_list_clip_lengths()`** the valid target clip-length buckets.
88
- - **`katto_list_caption_styles()`** the caption-style presets you can pass as `caption_style` when creating a job or re-rendering a clip.
89
-
90
- Jobs draw from your Katto plan's monthly video quota (25 on Creator, 2 on Free), videos up to 90 minutes. The underlying REST API also supports `Idempotency-Key` safe retries, read-only scoped keys, and signed HMAC webhooks. See the [docs](https://katto.tech/docs/api).
91
-
92
- ## Env
93
-
94
- | var | required | default |
95
- | --- | --- | --- |
96
- | `KATTO_API_KEY` | yes | (none) |
97
- | `KATTO_API_URL` | no | `https://katto.tech` |
98
-
99
- Full docs: **[katto.tech/docs/api](https://katto.tech/docs/api)**
100
-
101
- ## Security & data
102
-
103
- - **Hosted auth is OAuth 2.1** — the API key is never written to your disk, and you can revoke a connected client anytime from your Katto account settings.
104
- - **Local auth uses a scoped `sk_live_` key** you create yourself; keys can be read-only, and the npx server only sends it to `KATTO_API_URL` (default `katto.tech`) over HTTPS.
105
- - `katto_get_webhook_secret` returns a signing secret — handle it like any credential; it is only needed to verify inbound webhook signatures.
106
-
107
- ## Also listed on
108
-
109
- - npm: [katto-mcp](https://www.npmjs.com/package/katto-mcp)
110
- - GitHub: [miracleweasel/katto-mcp](https://github.com/miracleweasel/katto-mcp)
111
- - [mcp.so](https://mcp.so/servers/katto) · [Smithery](https://smithery.ai/server/@dev-4lue/katto-mcp) · [Glama](https://glama.ai/mcp/servers/@miracleweasel/katto-mcp)
112
- - Official MCP Registry: `io.github.miracleweasel/katto-mcp`
113
-
114
- ## Public Discovery
115
-
116
- Katto – AI video clipping MCP server: turn long videos and podcasts into scored, captioned, reframed 9:16 clips, from any MCP client (Claude, Cursor, ChatGPT). Hosted, OAuth 2.1, no API key on disk.
117
-
118
- Intents this server covers: *AI video clipping MCP server*, *MCP server for short-form video*, *MCP to turn podcasts into clips*, *video clipping for AI agents (Claude / Cursor / ChatGPT)*.
119
-
120
- Example prompts:
121
-
122
- 1. "Clip the best moments from this podcast episode and reframe them for TikTok: `https://youtube.com/watch?v=…`"
123
- 2. "Check the status of my last clipping job and give me the download links for the finished clips."
124
- 3. "Take clip 2 from that job, dub it into Spanish and re-render it with the Bold caption style."
125
-
126
- ## License
127
-
128
- MIT — see [LICENSE](./LICENSE).
1
+ # katto-mcp
2
+
3
+ [![npm version](https://img.shields.io/npm/v/katto-mcp.svg)](https://www.npmjs.com/package/katto-mcp)
4
+ [![npm downloads](https://img.shields.io/npm/dm/katto-mcp.svg)](https://www.npmjs.com/package/katto-mcp)
5
+ [![license](https://img.shields.io/npm/l/katto-mcp.svg)](./LICENSE)
6
+ [![MCP Registry](https://img.shields.io/badge/MCP%20Registry-io.github.miracleweasel%2Fkatto--mcp-blue)](https://registry.modelcontextprotocol.io)
7
+
8
+ **The AI video clipping MCP server.** Turn long videos into scored, captioned, vertical 9:16 short clips from any MCP client (Claude, Cursor, Claude Code, ChatGPT, VS Code, and other conformant clients). Drop in a YouTube link, a podcast, or a Twitch VOD and get publish-ready shorts back through natural conversation.
9
+
10
+ Powered by [Katto](https://katto.tech), an AI video clipper that turns long-form video (podcasts, interviews, streams, webinars) into short-form clips for TikTok, Reels and YouTube Shorts. Katto is a flat-priced, no-credits alternative to tools like OpusClip: one 25-video quota covers videos up to 90 minutes each, with the API and MCP included on **every plan, including the free one** (not gated behind a paid or enterprise tier).
11
+
12
+ ## What it does
13
+
14
+ Ask your agent something like *"clip the best moments from this podcast and reframe them for TikTok"* and Katto will:
15
+
16
+ - find the strongest 30 to 90 second moments in a long video and score each clip 0 to 100 on Hook, Flow, Value and Trend,
17
+ - reframe to vertical 9:16 with face tracking, split-screen for two speakers, and stacked layouts for gaming,
18
+ - burn animated captions (auto-captioned in 99 languages via Whisper large-v3; word-by-word timing in 41 of them, sentence-level in the rest) and optionally dub into 19 languages,
19
+ - and hand back publish-ready MP4 files plus SRT caption urls.
20
+
21
+ Typical uses: repurpose a YouTube video into shorts, turn a podcast episode into clips, cut highlights from a Twitch VOD, or convert a long interview into vertical social posts.
22
+
23
+ ## Two ways to connect
24
+
25
+ ### Hosted (zero-install, recommended)
26
+
27
+ Point any OAuth-capable MCP client at the hosted endpoint and sign in with your Katto account. The API key never touches your disk, and you can revoke access anytime:
28
+
29
+ ```
30
+ https://mcp.katto.tech/mcp (Streamable HTTP, OAuth 2.1)
31
+ ```
32
+
33
+ ### Local (npx)
34
+
35
+ For Cursor, CI and scripts. Create an API key at **[katto.tech/dashboard/api-keys](https://katto.tech/dashboard/api-keys)**, then add the server to your MCP client config:
36
+
37
+ ```json
38
+ {
39
+ "mcpServers": {
40
+ "katto": {
41
+ "command": "npx",
42
+ "args": ["-y", "katto-mcp"],
43
+ "env": { "KATTO_API_KEY": "sk_live_..." }
44
+ }
45
+ }
46
+ }
47
+ ```
48
+
49
+ The hosted endpoint above also accepts `Authorization: Bearer sk_live_...` directly for key-based clients.
50
+
51
+ ## Install per client
52
+
53
+ **Claude Code** (hosted, OAuth):
54
+ ```bash
55
+ claude mcp add --transport http katto https://mcp.katto.tech/mcp
56
+ ```
57
+
58
+ **Claude Desktop** — Settings → Connectors → *Add custom connector* → `https://mcp.katto.tech/mcp`, then sign in. (Or add the `npx` block above to `claude_desktop_config.json`.)
59
+
60
+ **Cursor** — Settings → MCP → *Add* → paste the `npx` JSON block above (uses `KATTO_API_KEY`).
61
+
62
+ **VS Code** (MCP extension) — add the same `npx` block to your MCP settings, or point it at the hosted URL if your client supports remote OAuth servers.
63
+
64
+ **ChatGPT** (Developer mode / connectors) — add a custom connector with URL `https://mcp.katto.tech/mcp`.
65
+
66
+ ## Tools
67
+
68
+ Clipping:
69
+ - **`katto_create_clip_job(url, config?)`** clip a long video (YouTube, Twitch, Vimeo, Rumble, Zoom, Dailymotion). Returns a job id.
70
+ - **`katto_get_job(id)`** poll until `status` is `completed`; `clips` holds the finished MP4 + caption (SRT) urls.
71
+ - **`katto_get_clips(id)`** just the finished clips of a job (MP4 + SRT + title + virality score).
72
+ - **`katto_get_transcript(id)`** the job's timestamped transcript segments.
73
+ - **`katto_list_jobs(limit?, cursor?, status?)`** your recent jobs, newest first (keyset pagination).
74
+ - **`katto_cancel_job(id)`** cancel a running job and refund the monthly video slot.
75
+
76
+ Editing (no quota):
77
+ - **`katto_rerender_clip(id, clip_index, layout_mode?, caption_style?)`** re-render one clip with a new reframe layout or caption style.
78
+ - **`katto_dub_clip(id, clip_index, languages)`** re-render a clip dubbed into one or more of 19 languages.
79
+ - **`katto_get_rerender(id, rerender_id)`** poll a re-render for the new versioned clip url.
80
+
81
+ Account and reference:
82
+ - **`katto_get_usage()`** your plan and remaining monthly video quota.
83
+ - **`katto_get_account()`** the connected account, this key's scopes, and quota.
84
+ - **`katto_get_brand_kit()`** your saved brand kits (colors, caption font, layout, watermark).
85
+ - **`katto_get_webhook_secret()`** your webhook signing secret and how to verify signed callbacks (returns a secret — treat it like a credential).
86
+ - **`katto_list_sources()`** the video platforms Katto can clip from, with an example url each.
87
+ - **`katto_list_clip_lengths()`** the valid target clip-length buckets.
88
+ - **`katto_list_caption_styles()`** the caption-style presets you can pass as `caption_style` when creating a job or re-rendering a clip.
89
+
90
+ Jobs draw from your Katto plan's monthly video quota (25 on Creator, 2 on Free), videos up to 90 minutes. The underlying REST API also supports `Idempotency-Key` safe retries, read-only scoped keys, and signed HMAC webhooks. See the [docs](https://katto.tech/docs/api).
91
+
92
+ ## Env
93
+
94
+ | var | required | default |
95
+ | --- | --- | --- |
96
+ | `KATTO_API_KEY` | yes | (none) |
97
+ | `KATTO_API_URL` | no | `https://katto.tech` |
98
+
99
+ Full docs: **[katto.tech/docs/api](https://katto.tech/docs/api)**
100
+
101
+ ## Security & data
102
+
103
+ - **Hosted auth is OAuth 2.1** — the API key is never written to your disk, and you can revoke a connected client anytime from your Katto account settings.
104
+ - **Local auth uses a scoped `sk_live_` key** you create yourself; keys can be read-only, and the npx server only sends it to `KATTO_API_URL` (default `katto.tech`) over HTTPS.
105
+ - `katto_get_webhook_secret` returns a signing secret — handle it like any credential; it is only needed to verify inbound webhook signatures.
106
+
107
+ ## Also listed on
108
+
109
+ - npm: [katto-mcp](https://www.npmjs.com/package/katto-mcp)
110
+ - GitHub: [miracleweasel/katto-mcp](https://github.com/miracleweasel/katto-mcp)
111
+ - [mcp.so](https://mcp.so/servers/katto) · [Smithery](https://smithery.ai/server/@dev-4lue/katto-mcp) · [Glama](https://glama.ai/mcp/servers/@miracleweasel/katto-mcp)
112
+ - Official MCP Registry: `io.github.miracleweasel/katto-mcp`
113
+
114
+ ## Public Discovery
115
+
116
+ Katto – AI video clipping MCP server: turn long videos and podcasts into scored, captioned, reframed 9:16 clips, from any MCP client (Claude, Cursor, ChatGPT). Hosted, OAuth 2.1, no API key on disk.
117
+
118
+ Intents this server covers: *AI video clipping MCP server*, *MCP server for short-form video*, *MCP to turn podcasts into clips*, *video clipping for AI agents (Claude / Cursor / ChatGPT)*.
119
+
120
+ Example prompts:
121
+
122
+ 1. "Clip the best moments from this podcast episode and reframe them for TikTok: `https://youtube.com/watch?v=…`"
123
+ 2. "Check the status of my last clipping job and give me the download links for the finished clips."
124
+ 3. "Take clip 2 from that job, dub it into Spanish and re-render it with the Bold caption style."
125
+
126
+ ## License
127
+
128
+ MIT — see [LICENSE](./LICENSE).
package/SKILL.md CHANGED
@@ -1,45 +1,45 @@
1
- ---
2
- name: katto-clip
3
- description: Turn a long video (a YouTube / Twitch / Vimeo / Rumble / Zoom / Dailymotion link, or an upload) into scored, captioned, 9:16 short clips using Katto's cloud pipeline. Use when the user wants to clip, repurpose, or make shorts from long-form video, or to dub / re-render an existing clip.
4
- ---
5
-
6
- # Katto clipping skill
7
-
8
- Katto runs the whole pipeline in its cloud — download → transcript → moment scoring →
9
- cut → captions → reframe → optional dub → publish. You trigger it; nothing renders on
10
- the local machine.
11
-
12
- ## Connect
13
- - **Hosted MCP (recommended, zero-install):** point the client at `https://mcp.katto.tech/mcp`
14
- (OAuth 2.1 + Dynamic Client Registration; no key on disk).
15
- - **Local:** `npx -y katto-mcp` with `KATTO_API_KEY=sk_live_...` (Cursor, CI, cron).
16
- - Tools are prefixed `katto_`. `tools/list` works without a key; every tool **call** needs a key.
17
-
18
- ## Core flow
19
- 1. `katto_create_clip_job({ url, config? })` → returns a `job_id`. Spends **1 video** from the
20
- monthly quota. Pass an idempotency key so a retry never double-spends.
21
- 2. Poll `katto_get_job({ id })` until `status` is `"completed"`.
22
- 3. `katto_get_clips({ id })` → finished clips (MP4 url + SRT + title + score 0–100).
23
-
24
- ## Editing — never counts against quota
25
- - `katto_rerender_clip({ id, clip_index, layout_mode?, caption_style? })`, then `katto_get_rerender(...)`.
26
- - `katto_dub_clip({ id, clip_index, languages })` → dub into any of **8 languages**: en, es, fr, it, pt, hi, ja, zh.
27
-
28
- ## Reference / account
29
- - `katto_get_usage`, `katto_get_account` — remaining quota, plan, this key's scopes.
30
- - `katto_list_sources` (accepted platforms), `katto_list_clip_lengths` (valid length buckets).
31
- - `katto_get_transcript` (timestamped), `katto_get_brand_kit`, `katto_get_webhook_secret` (treat as a credential).
32
-
33
- ## Rules of the road
34
- - **One quota**, shared with the app / API / CLI: Creator = 25 videos/month, ≤90 min each; Free = 2/month.
35
- Re-renders and dubs are free.
36
- - Captions: **99 languages** (word-timed in 41, sentence-level in the rest). Dubbing: **8**. Publish: **7 platforms**.
37
- - **Don't invent timings.** The only measured figure is ~5 minutes for 8 clips on a 20-minute video;
38
- longer sources take longer. Say that, not a number you'd like.
39
-
40
- ## Example
41
- User: *"Clip the best 6 moments from this podcast, then dub clip 2 in Spanish."*
42
- → `katto_create_clip_job(url, { clips: 6 })` → poll `katto_get_job` → `katto_get_clips`
43
- → `katto_dub_clip(id, 2, ["es"])` → `katto_get_rerender`.
44
-
45
- Full reference: https://katto.tech/docs/api · OpenAPI: https://katto.tech/openapi.json
1
+ ---
2
+ name: katto-clip
3
+ description: Turn a long video (a YouTube / Twitch / Vimeo / Rumble / Zoom / Dailymotion link, or an upload) into scored, captioned, 9:16 short clips using Katto's cloud pipeline. Use when the user wants to clip, repurpose, or make shorts from long-form video, or to dub / re-render an existing clip.
4
+ ---
5
+
6
+ # Katto clipping skill
7
+
8
+ Katto runs the whole pipeline in its cloud — download → transcript → moment scoring →
9
+ cut → captions → reframe → optional dub → publish. You trigger it; nothing renders on
10
+ the local machine.
11
+
12
+ ## Connect
13
+ - **Hosted MCP (recommended, zero-install):** point the client at `https://mcp.katto.tech/mcp`
14
+ (OAuth 2.1 + Dynamic Client Registration; no key on disk).
15
+ - **Local:** `npx -y katto-mcp` with `KATTO_API_KEY=sk_live_...` (Cursor, CI, cron).
16
+ - Tools are prefixed `katto_`. `tools/list` works without a key; every tool **call** needs a key.
17
+
18
+ ## Core flow
19
+ 1. `katto_create_clip_job({ url, config? })` → returns a `job_id`. Spends **1 video** from the
20
+ monthly quota. Pass an idempotency key so a retry never double-spends.
21
+ 2. Poll `katto_get_job({ id })` until `status` is `"completed"`.
22
+ 3. `katto_get_clips({ id })` → finished clips (MP4 url + SRT + title + score 0–100).
23
+
24
+ ## Editing — never counts against quota
25
+ - `katto_rerender_clip({ id, clip_index, layout_mode?, caption_style? })`, then `katto_get_rerender(...)`.
26
+ - `katto_dub_clip({ id, clip_index, languages })` → dub into any of **19 languages**: en, es, fr, it, pt, hi, ja, zh, de, ko, nl, pl, ru, tr, id, vi, th, uk, ar.
27
+
28
+ ## Reference / account
29
+ - `katto_get_usage`, `katto_get_account` — remaining quota, plan, this key's scopes.
30
+ - `katto_list_sources` (accepted platforms), `katto_list_clip_lengths` (valid length buckets).
31
+ - `katto_get_transcript` (timestamped), `katto_get_brand_kit`, `katto_get_webhook_secret` (treat as a credential).
32
+
33
+ ## Rules of the road
34
+ - **One quota**, shared with the app / API / CLI: Creator = 25 videos/month, ≤90 min each; Free = 2/month.
35
+ Re-renders are free; dubbing is a Creator feature.
36
+ - Captions: **99 languages** (word-timed in 41, sentence-level in the rest). Dubbing: **19**. Publish: **7 platforms**.
37
+ - **Don't invent timings.** The only measured figure is ~5 minutes for 8 clips on a 20-minute video;
38
+ longer sources take longer. Say that, not a number you'd like.
39
+
40
+ ## Example
41
+ User: *"Clip the best 6 moments from this podcast, then dub clip 2 in Spanish."*
42
+ → `katto_create_clip_job(url, { clips: 6 })` → poll `katto_get_job` → `katto_get_clips`
43
+ → `katto_dub_clip(id, 2, ["es"])` → `katto_get_rerender`.
44
+
45
+ Full reference: https://katto.tech/docs/api · OpenAPI: https://katto.tech/openapi.json
package/index.mjs CHANGED
@@ -1,397 +1,397 @@
1
- #!/usr/bin/env node
2
- /**
3
- * Katto MCP server — exposes the Katto clipping API as MCP tools so agents
4
- * (Claude, Cursor, ...) can turn long videos into scored 9:16 clips. Runs
5
- * locally over stdio and calls https://katto.tech/api/v1 with your API key.
6
- *
7
- * Config:
8
- * KATTO_API_KEY (required) — create at https://katto.tech/dashboard/api-keys
9
- * KATTO_API_URL (optional) — defaults to https://katto.tech
10
- */
11
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
- import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
-
15
- const API_URL = (process.env.KATTO_API_URL || "https://katto.tech").replace(/\/$/, "");
16
- const API_KEY = process.env.KATTO_API_KEY;
17
-
18
- // NOTE: we intentionally do NOT exit when the key is missing. The server must be
19
- // able to start and answer tools/list WITHOUT a key so that MCP directory/aggregator
20
- // sandboxes (Glama, mcp.so, ...) can enumerate the tools — otherwise they show
21
- // "0 tools". Nothing runs unauthenticated: the key is enforced at call time in api()
22
- // below, so tools/list is public but every tool CALL still requires a valid key.
23
- if (!API_KEY) {
24
- console.error("[katto-mcp] No KATTO_API_KEY set — tools/list works, but calling any tool requires a key. Create one at https://katto.tech/dashboard/api-keys");
25
- }
26
-
27
- async function api(path, init = {}) {
28
- if (!API_KEY) {
29
- throw new Error("KATTO_API_KEY is required to call Katto tools. Create one at https://katto.tech/dashboard/api-keys and set it in your MCP client config.");
30
- }
31
- const res = await fetch(`${API_URL}${path}`, {
32
- ...init,
33
- headers: {
34
- Authorization: `Bearer ${API_KEY}`,
35
- "Content-Type": "application/json",
36
- ...(init.headers || {}),
37
- },
38
- });
39
- const text = await res.text();
40
- let data;
41
- try {
42
- data = JSON.parse(text);
43
- } catch {
44
- data = { raw: text };
45
- }
46
- if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
47
- return data;
48
- }
49
-
50
- const TOOLS = [
51
- {
52
- name: "katto_create_clip_job",
53
- description:
54
- "Submit a long video (YouTube, Twitch, Vimeo, Rumble, Zoom, Dailymotion) to Katto. Returns a job id; " +
55
- "the clips finish asynchronously. Poll katto_get_job with the id until status is 'completed'.",
56
- inputSchema: {
57
- type: "object",
58
- properties: {
59
- url: { type: "string", description: "Public video URL to clip." },
60
- config: {
61
- type: "object",
62
- description: "Optional pre-clip settings.",
63
- properties: {
64
- genre: { type: "string", description: "e.g. podcast, gaming, sports, interview" },
65
- clipLength: { type: "string", enum: ["lt30", "30_60", "60_90", "90_180"] },
66
- customPrompt: { type: "string" },
67
- topics: { type: "string" },
68
- },
69
- },
70
- },
71
- required: ["url"],
72
- },
73
- },
74
- {
75
- name: "katto_get_job",
76
- description:
77
- "Get the status and clips of a Katto job by id. When status is 'completed', 'clips' holds the finished " +
78
- "9:16 MP4 urls and caption (SRT) urls.",
79
- inputSchema: {
80
- type: "object",
81
- properties: { id: { type: "string", description: "Job id from katto_create_clip_job." } },
82
- required: ["id"],
83
- },
84
- },
85
- {
86
- name: "katto_list_jobs",
87
- description:
88
- "List your recent Katto jobs, newest first. Paginate with 'cursor' (pass the previous next_cursor) and " +
89
- "optionally filter by 'status'. Returns { jobs: [{id, status, source, created_at, completed_at}], next_cursor }.",
90
- inputSchema: {
91
- type: "object",
92
- properties: {
93
- limit: { type: "number", description: "How many jobs to return (1-100, default 20)." },
94
- cursor: { type: "string", description: "Pass the previous response's next_cursor for the next page." },
95
- status: { type: "string", description: "Optional filter, e.g. 'completed', 'queued', 'failed'." },
96
- },
97
- },
98
- },
99
- {
100
- name: "katto_get_clips",
101
- description:
102
- "Convenience: fetch just the finished clips of a job (9:16 MP4 urls + caption SRT urls). " +
103
- "Returns an empty list while the job is still processing.",
104
- inputSchema: {
105
- type: "object",
106
- properties: { id: { type: "string", description: "Job id from katto_create_clip_job." } },
107
- required: ["id"],
108
- },
109
- },
110
- {
111
- name: "katto_get_usage",
112
- description:
113
- "Get your current plan and monthly video quota: { plan, videos_used, videos_limit, videos_remaining }.",
114
- inputSchema: { type: "object", properties: {} },
115
- },
116
- {
117
- name: "katto_get_transcript",
118
- description:
119
- "Get the compact transcript of a completed job as timestamped segments " +
120
- "[{ start, end, text }]. Returns 404 while the job is still processing.",
121
- inputSchema: {
122
- type: "object",
123
- properties: { id: { type: "string", description: "Job id from katto_create_clip_job." } },
124
- required: ["id"],
125
- },
126
- },
127
- {
128
- name: "katto_cancel_job",
129
- description:
130
- "Cancel a still-running job (queued/processing) and refund the monthly video slot. " +
131
- "Returns an error if the job already finished, failed, or was cancelled.",
132
- inputSchema: {
133
- type: "object",
134
- properties: { id: { type: "string", description: "Job id from katto_create_clip_job." } },
135
- required: ["id"],
136
- },
137
- },
138
- {
139
- name: "katto_get_account",
140
- description:
141
- "Get the account behind this key: plan, this key's scopes, and monthly quota " +
142
- "{ videos_used, videos_limit, videos_remaining }.",
143
- inputSchema: { type: "object", properties: {} },
144
- },
145
- {
146
- name: "katto_list_sources",
147
- description:
148
- "List the video platforms Katto can clip from, with an example URL for each. Use this to confirm a " +
149
- "URL is supported before calling katto_create_clip_job.",
150
- inputSchema: { type: "object", properties: {} },
151
- },
152
- {
153
- name: "katto_list_clip_lengths",
154
- description:
155
- "List the valid values for the optional config.clipLength on katto_create_clip_job (target clip " +
156
- "duration buckets). Note: clipLength is fixed at job creation and cannot be changed by re-render.",
157
- inputSchema: { type: "object", properties: {} },
158
- },
159
- {
160
- name: "katto_list_caption_styles",
161
- description:
162
- "List the valid caption_style preset names for katto_rerender_clip (e.g. 'hormozi' for bold " +
163
- "word-by-word highlight). Call this before re-rendering so you pass a real preset, not a guess.",
164
- inputSchema: { type: "object", properties: {} },
165
- },
166
- {
167
- name: "katto_rerender_clip",
168
- description:
169
- "Re-render one finished clip with a new reframe layout and/or caption style. Does NOT use video quota. " +
170
- "Returns a rerender_id; poll katto_get_rerender for the new clip url.",
171
- inputSchema: {
172
- type: "object",
173
- properties: {
174
- id: { type: "string", description: "Job id from katto_create_clip_job." },
175
- clip_index: { type: "number", description: "0-based index of the clip to re-render." },
176
- layout_mode: {
177
- type: "string",
178
- enum: ["face_tracking", "wide", "split_screen", "stacked", "passthrough", "grid_3", "grid_4"],
179
- },
180
- caption_style: {
181
- type: "string",
182
- // Keep in sync with CAPTION_STYLES below (katto_list_caption_styles).
183
- enum: ["default", "hormozi", "yellowPop", "redAlert", "skyline", "bubblegum", "aqua", "violet", "headline", "centerPop", "topLime", "impactMax", "bebasGold", "robotoBold", "montserratClean", "poppinsSoft", "robotoDoc", "broadcast", "wideClean", "mono", "bebasWhite", "rainbow", "multicolor"],
184
- description: "A caption style preset name. See katto_list_caption_styles for the labels.",
185
- },
186
- },
187
- required: ["id", "clip_index"],
188
- },
189
- },
190
- {
191
- name: "katto_dub_clip",
192
- description:
193
- "Re-render one finished clip dubbed into one or more languages (en, es, fr, it, pt, hi, ja, zh). Does " +
194
- "NOT use video quota. Returns a rerender_id; poll katto_get_rerender for the result.",
195
- inputSchema: {
196
- type: "object",
197
- properties: {
198
- id: { type: "string", description: "Job id." },
199
- clip_index: { type: "number", description: "0-based clip index." },
200
- languages: {
201
- type: "array",
202
- items: { type: "string", enum: ["en", "es", "fr", "it", "pt", "hi", "ja", "zh"] },
203
- },
204
- },
205
- required: ["id", "clip_index", "languages"],
206
- },
207
- },
208
- {
209
- name: "katto_get_rerender",
210
- description:
211
- "Poll a re-render started by katto_rerender_clip or katto_dub_clip. Returns { status, clip_url, captions_url }.",
212
- inputSchema: {
213
- type: "object",
214
- properties: {
215
- id: { type: "string", description: "Job id." },
216
- rerender_id: { type: "string", description: "The rerender_id returned by rerender/dub." },
217
- },
218
- required: ["id", "rerender_id"],
219
- },
220
- },
221
- {
222
- name: "katto_get_brand_kit",
223
- description:
224
- "Get your saved brand kits (colors, caption font and position, default layout, watermark url).",
225
- inputSchema: { type: "object", properties: {} },
226
- },
227
- {
228
- name: "katto_get_webhook_secret",
229
- description:
230
- "Get your webhook signing secret and how to verify Katto's signed completion callbacks " +
231
- "(HMAC-SHA256 of {timestamp}.{body}). Pass webhook_url on a job to receive them.",
232
- inputSchema: { type: "object", properties: {} },
233
- },
234
- ];
235
-
236
- // Tool behaviour hints (MCP annotations). Required by the Claude Connectors
237
- // Directory review: every tool must declare read-only vs write, plus
238
- // destructive / idempotent / open-world semantics, so a client can gate calls.
239
- // All four hints are declared on every tool (explicit > omitted for reviewers).
240
- // Read tools: read-only, non-destructive, idempotent, closed-world. Writes flip
241
- // readOnlyHint. create_clip_job fetches a user URL (openWorld) + spends 1 quota
242
- // slot (not idempotent). rerender/dub each spawn a NEW render (not idempotent —
243
- // unsafe to blindly retry). cancel_job is the only destructive tool.
244
- const ANNOTATIONS = {
245
- katto_create_clip_job: { title: "Create clip job", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
246
- katto_get_job: { title: "Get job status & clips", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
247
- katto_list_jobs: { title: "List jobs", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
248
- katto_get_clips: { title: "Get finished clips", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
249
- katto_get_usage: { title: "Get plan & quota", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
250
- katto_get_transcript: { title: "Get transcript", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
251
- katto_cancel_job: { title: "Cancel job & refund slot", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
252
- katto_get_account: { title: "Get account", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
253
- katto_list_sources: { title: "List supported sources", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
254
- katto_list_clip_lengths: { title: "List clip-length options", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
255
- katto_list_caption_styles: { title: "List caption styles", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
256
- katto_rerender_clip: { title: "Re-render a clip", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
257
- katto_dub_clip: { title: "Dub a clip", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
258
- katto_get_rerender: { title: "Poll a re-render", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
259
- katto_get_brand_kit: { title: "Get brand kits", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
260
- katto_get_webhook_secret: { title: "Get webhook signing secret", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
261
- };
262
-
263
- // Agent-facing descriptions (when to call, returns, errors, quota cost). Served
264
- // on tools/list — identical across all sources, enriching the terser inline text.
265
- const DESCRIPTIONS = {
266
- katto_create_clip_job: "Submit a long video (YouTube, Twitch, Vimeo, Rumble, Zoom, Dailymotion) for clipping. Consumes 1 video from your monthly quota — check katto_get_usage first. Clips are produced asynchronously; returns a job id, then poll katto_get_job until status is 'completed'. Fails if the URL is unsupported or the video is over 90 minutes.",
267
- katto_get_job: "Read-only. Get the status, progress and clips of a job by id. Clips come ranked by virality score (0-100, highest first; ties broken by clip_index). Each clip is { url, captions_url, title, score, clip_index, hd }: when status is 'completed' every url is a finished, downloadable clip; hd is true once the 1080p high-quality render is ready. hd=false does NOT mean broken — the url already works, the 1080p is just still rendering in the background (and stays the standard render on the free plan). The job-level hd_ready flag turns true once every clip is hd, i.e. the URLs are final and won't be swapped again (status='completed' fires earlier). Returns 404 for an unknown id.",
268
- katto_list_jobs: "Read-only. List your recent jobs, newest first. Paginate with 'cursor' (pass the previous next_cursor); optional 'status' filter. Returns { jobs: [{id, status, source, created_at, completed_at}], next_cursor }.",
269
- katto_get_clips: "Read-only convenience: the clips of a job as { url, captions_url, title, score, clip_index, hd }, ranked by score (highest first). hd=true means the 1080p render is ready; hd=false means the url is the standard render (HD still finishing, or the free plan). Empty until the first clip is ready.",
270
- katto_get_usage: "Read-only. Your current plan and monthly video quota: { plan, videos_used, videos_limit, videos_remaining }. Call before katto_create_clip_job to confirm remaining quota.",
271
- katto_get_transcript: "Read-only. The transcript of a completed job as timestamped segments [{ start, end, text }]. Returns 404 while the job is still processing.",
272
- katto_cancel_job: "Cancel a still-running job (queued/processing) and refund the video back to your monthly quota. Safe to retry (idempotent). Returns an error if the job already finished, failed, or was cancelled.",
273
- katto_get_account: "Read-only. The account behind this key: plan, this key's scopes (read/write), and monthly quota { videos_used, videos_limit, videos_remaining }.",
274
- katto_list_sources: "Read-only. The video platforms Katto can clip from, each with an example URL. Use it to confirm a URL is supported before calling katto_create_clip_job.",
275
- katto_list_clip_lengths: "Read-only. The valid values for the optional config.clipLength on katto_create_clip_job (target clip-duration buckets). Note: clipLength is fixed at creation and cannot be changed by re-render.",
276
- katto_list_caption_styles: "Read-only. The valid caption_style preset names for katto_rerender_clip (id + label), e.g. 'hormozi' for bold word-by-word highlight. Call this before re-rendering so you pass a real preset instead of guessing.",
277
- katto_rerender_clip: "Re-render one already-finished clip with a new reframe layout and/or caption style (get valid caption_style values from katto_list_caption_styles). Free — does NOT use video quota. Each call starts a new render (not idempotent); the original clip is kept. Returns a rerender_id; poll katto_get_rerender for the new clip url.",
278
- katto_dub_clip: "Re-render one finished clip dubbed into one or more of 8 languages (en, es, fr, it, pt, hi, ja, zh). Free — does NOT use video quota. Each call starts a new render (not idempotent). Returns a rerender_id; poll katto_get_rerender for the result.",
279
- katto_get_rerender: "Read-only. Poll a re-render started by katto_rerender_clip or katto_dub_clip. Returns { status, clip_url, captions_url } — clip_url is null until status is 'completed'.",
280
- katto_get_brand_kit: "Read-only. Your saved brand kits (colors, caption font and position, default layout, watermark url).",
281
- katto_get_webhook_secret: "Read-only. Returns your webhook signing SECRET — treat it as a credential (do not display or log it) — plus how to verify Katto's signed completion callbacks (HMAC-SHA256 of {timestamp}.{body}). Pass webhook_url on a job to receive them.",
282
- };
283
-
284
- // TOOLS enriched with agent descriptions + annotations — served on tools/list.
285
- const TOOLS_LISTED = TOOLS.map((t) => ({
286
- ...t,
287
- description: DESCRIPTIONS[t.name] ?? t.description,
288
- ...(ANNOTATIONS[t.name] ? { annotations: ANNOTATIONS[t.name] } : {}),
289
- }));
290
-
291
- // Static reference data — returned by the list_* tools without an API call.
292
- const SOURCES = [
293
- { id: 'youtube', example: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' },
294
- { id: 'twitch', example: 'https://www.twitch.tv/videos/123456789' },
295
- { id: 'vimeo', example: 'https://vimeo.com/123456789' },
296
- { id: 'rumble', example: 'https://rumble.com/v1a2b3c-title.html' },
297
- { id: 'zoom', example: 'https://zoom.us/rec/share/abc123' },
298
- { id: 'dailymotion', example: 'https://www.dailymotion.com/video/x8abcde' },
299
- ];
300
- const CLIP_LENGTHS = [
301
- { value: 'lt30', label: 'Under 30 seconds' },
302
- { value: '30_60', label: '30 to 60 seconds' },
303
- { value: '60_90', label: '60 to 90 seconds' },
304
- { value: '90_180', label: '90 to 180 seconds' },
305
- ];
306
- // Valid values for caption_style on katto_rerender_clip. Mirrors the editor's
307
- // named presets. Keep in sync with mcp-worker/src/index.js CAPTION_STYLES.
308
- const CAPTION_STYLES = [
309
- { value: 'default', label: 'Default' },
310
- { value: 'hormozi', label: 'Hormozi (bold word-by-word highlight)' },
311
- { value: 'yellowPop', label: 'Yellow Pop' },
312
- { value: 'redAlert', label: 'Red Alert' },
313
- { value: 'skyline', label: 'Skyline' },
314
- { value: 'bubblegum', label: 'Bubblegum' },
315
- { value: 'aqua', label: 'Aqua' },
316
- { value: 'violet', label: 'Violet' },
317
- { value: 'headline', label: 'Headline' },
318
- { value: 'centerPop', label: 'Center Pop' },
319
- { value: 'topLime', label: 'Top Lime' },
320
- { value: 'impactMax', label: 'Impact Max' },
321
- { value: 'bebasGold', label: 'Bebas Gold' },
322
- { value: 'robotoBold', label: 'Roboto Bold' },
323
- { value: 'montserratClean', label: 'Montserrat' },
324
- { value: 'poppinsSoft', label: 'Poppins' },
325
- { value: 'robotoDoc', label: 'Roboto Doc' },
326
- { value: 'broadcast', label: 'Broadcast' },
327
- { value: 'wideClean', label: 'Wide Clean' },
328
- { value: 'mono', label: 'Mono' },
329
- { value: 'bebasWhite', label: 'Bebas' },
330
- { value: 'rainbow', label: 'Rainbow (per-word colour cycle)' },
331
- { value: 'multicolor', label: 'Multicolor' },
332
- ];
333
-
334
- const server = new Server({ name: "katto", version: "0.5.7" }, { capabilities: { tools: {} } });
335
-
336
- server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS_LISTED }));
337
-
338
- server.setRequestHandler(CallToolRequestSchema, async (req) => {
339
- const { name, arguments: args = {} } = req.params;
340
- try {
341
- let data;
342
- if (name === "katto_create_clip_job") {
343
- data = await api("/api/v1/jobs", {
344
- method: "POST",
345
- body: JSON.stringify({ url: args.url, config: args.config }),
346
- });
347
- } else if (name === "katto_get_job") {
348
- data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}`);
349
- } else if (name === "katto_list_jobs") {
350
- const qs = new URLSearchParams();
351
- if (args.limit != null) qs.set("limit", String(args.limit));
352
- if (args.cursor) qs.set("cursor", String(args.cursor));
353
- if (args.status) qs.set("status", String(args.status));
354
- const q = qs.toString();
355
- data = await api(`/api/v1/jobs${q ? `?${q}` : ""}`);
356
- } else if (name === "katto_get_clips") {
357
- const job = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}`);
358
- data = { id: job.id, status: job.status, clips: job.clips || [], dropped_clip_indices: job.dropped_clip_indices || [], dropped_clips: job.dropped_clips || [] };
359
- } else if (name === "katto_get_usage") {
360
- data = await api("/api/v1/usage");
361
- } else if (name === "katto_get_transcript") {
362
- data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/transcript`);
363
- } else if (name === "katto_cancel_job") {
364
- data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}`, { method: "DELETE" });
365
- } else if (name === "katto_get_account") {
366
- data = await api("/api/v1/me");
367
- } else if (name === "katto_list_sources") {
368
- data = { sources: SOURCES, note: "You can also clip a local file via the REST API (POST /v1/uploads)." };
369
- } else if (name === "katto_list_clip_lengths") {
370
- data = { clip_lengths: CLIP_LENGTHS };
371
- } else if (name === "katto_list_caption_styles") {
372
- data = { caption_styles: CAPTION_STYLES };
373
- } else if (name === "katto_rerender_clip") {
374
- const b = {};
375
- if (args.layout_mode) b.layout_mode = args.layout_mode;
376
- if (args.caption_style) b.caption_style = args.caption_style;
377
- data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/rerender`, { method: "POST", body: JSON.stringify(b) });
378
- } else if (name === "katto_dub_clip") {
379
- data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/rerender`, { method: "POST", body: JSON.stringify({ dub: args.languages || [] }) });
380
- } else if (name === "katto_get_rerender") {
381
- data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/rerenders/${encodeURIComponent(args.rerender_id)}`);
382
- } else if (name === "katto_get_brand_kit") {
383
- data = await api("/api/v1/brand-kit");
384
- } else if (name === "katto_get_webhook_secret") {
385
- data = await api("/api/v1/webhook");
386
- } else {
387
- throw new Error(`Unknown tool: ${name}`);
388
- }
389
- return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
390
- } catch (e) {
391
- return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
392
- }
393
- });
394
-
395
- const transport = new StdioServerTransport();
396
- await server.connect(transport);
397
- console.error("[katto-mcp] ready");
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Katto MCP server — exposes the Katto clipping API as MCP tools so agents
4
+ * (Claude, Cursor, ...) can turn long videos into scored 9:16 clips. Runs
5
+ * locally over stdio and calls https://katto.tech/api/v1 with your API key.
6
+ *
7
+ * Config:
8
+ * KATTO_API_KEY (required) — create at https://katto.tech/dashboard/api-keys
9
+ * KATTO_API_URL (optional) — defaults to https://katto.tech
10
+ */
11
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
12
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
13
+ import { ListToolsRequestSchema, CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js";
14
+
15
+ const API_URL = (process.env.KATTO_API_URL || "https://katto.tech").replace(/\/$/, "");
16
+ const API_KEY = process.env.KATTO_API_KEY;
17
+
18
+ // NOTE: we intentionally do NOT exit when the key is missing. The server must be
19
+ // able to start and answer tools/list WITHOUT a key so that MCP directory/aggregator
20
+ // sandboxes (Glama, mcp.so, ...) can enumerate the tools — otherwise they show
21
+ // "0 tools". Nothing runs unauthenticated: the key is enforced at call time in api()
22
+ // below, so tools/list is public but every tool CALL still requires a valid key.
23
+ if (!API_KEY) {
24
+ console.error("[katto-mcp] No KATTO_API_KEY set — tools/list works, but calling any tool requires a key. Create one at https://katto.tech/dashboard/api-keys");
25
+ }
26
+
27
+ async function api(path, init = {}) {
28
+ if (!API_KEY) {
29
+ throw new Error("KATTO_API_KEY is required to call Katto tools. Create one at https://katto.tech/dashboard/api-keys and set it in your MCP client config.");
30
+ }
31
+ const res = await fetch(`${API_URL}${path}`, {
32
+ ...init,
33
+ headers: {
34
+ Authorization: `Bearer ${API_KEY}`,
35
+ "Content-Type": "application/json",
36
+ ...(init.headers || {}),
37
+ },
38
+ });
39
+ const text = await res.text();
40
+ let data;
41
+ try {
42
+ data = JSON.parse(text);
43
+ } catch {
44
+ data = { raw: text };
45
+ }
46
+ if (!res.ok) throw new Error(data.error || `HTTP ${res.status}`);
47
+ return data;
48
+ }
49
+
50
+ const TOOLS = [
51
+ {
52
+ name: "katto_create_clip_job",
53
+ description:
54
+ "Submit a long video (YouTube, Twitch, Vimeo, Rumble, Zoom, Dailymotion) to Katto. Returns a job id; " +
55
+ "the clips finish asynchronously. Poll katto_get_job with the id until status is 'completed'.",
56
+ inputSchema: {
57
+ type: "object",
58
+ properties: {
59
+ url: { type: "string", description: "Public video URL to clip." },
60
+ config: {
61
+ type: "object",
62
+ description: "Optional pre-clip settings.",
63
+ properties: {
64
+ genre: { type: "string", description: "e.g. podcast, gaming, sports, interview" },
65
+ clipLength: { type: "string", enum: ["lt30", "30_60", "60_90", "90_180"] },
66
+ customPrompt: { type: "string" },
67
+ topics: { type: "string" },
68
+ },
69
+ },
70
+ },
71
+ required: ["url"],
72
+ },
73
+ },
74
+ {
75
+ name: "katto_get_job",
76
+ description:
77
+ "Get the status and clips of a Katto job by id. When status is 'completed', 'clips' holds the finished " +
78
+ "9:16 MP4 urls and caption (SRT) urls.",
79
+ inputSchema: {
80
+ type: "object",
81
+ properties: { id: { type: "string", description: "Job id from katto_create_clip_job." } },
82
+ required: ["id"],
83
+ },
84
+ },
85
+ {
86
+ name: "katto_list_jobs",
87
+ description:
88
+ "List your recent Katto jobs, newest first. Paginate with 'cursor' (pass the previous next_cursor) and " +
89
+ "optionally filter by 'status'. Returns { jobs: [{id, status, source, created_at, completed_at}], next_cursor }.",
90
+ inputSchema: {
91
+ type: "object",
92
+ properties: {
93
+ limit: { type: "number", description: "How many jobs to return (1-100, default 20)." },
94
+ cursor: { type: "string", description: "Pass the previous response's next_cursor for the next page." },
95
+ status: { type: "string", description: "Optional filter, e.g. 'completed', 'queued', 'failed'." },
96
+ },
97
+ },
98
+ },
99
+ {
100
+ name: "katto_get_clips",
101
+ description:
102
+ "Convenience: fetch just the finished clips of a job (9:16 MP4 urls + caption SRT urls). " +
103
+ "Returns an empty list while the job is still processing.",
104
+ inputSchema: {
105
+ type: "object",
106
+ properties: { id: { type: "string", description: "Job id from katto_create_clip_job." } },
107
+ required: ["id"],
108
+ },
109
+ },
110
+ {
111
+ name: "katto_get_usage",
112
+ description:
113
+ "Get your current plan and monthly video quota: { plan, videos_used, videos_limit, videos_remaining }.",
114
+ inputSchema: { type: "object", properties: {} },
115
+ },
116
+ {
117
+ name: "katto_get_transcript",
118
+ description:
119
+ "Get the compact transcript of a completed job as timestamped segments " +
120
+ "[{ start, end, text }]. Returns 404 while the job is still processing.",
121
+ inputSchema: {
122
+ type: "object",
123
+ properties: { id: { type: "string", description: "Job id from katto_create_clip_job." } },
124
+ required: ["id"],
125
+ },
126
+ },
127
+ {
128
+ name: "katto_cancel_job",
129
+ description:
130
+ "Cancel a still-running job (queued/processing) and refund the monthly video slot. " +
131
+ "Returns an error if the job already finished, failed, or was cancelled.",
132
+ inputSchema: {
133
+ type: "object",
134
+ properties: { id: { type: "string", description: "Job id from katto_create_clip_job." } },
135
+ required: ["id"],
136
+ },
137
+ },
138
+ {
139
+ name: "katto_get_account",
140
+ description:
141
+ "Get the account behind this key: plan, this key's scopes, and monthly quota " +
142
+ "{ videos_used, videos_limit, videos_remaining }.",
143
+ inputSchema: { type: "object", properties: {} },
144
+ },
145
+ {
146
+ name: "katto_list_sources",
147
+ description:
148
+ "List the video platforms Katto can clip from, with an example URL for each. Use this to confirm a " +
149
+ "URL is supported before calling katto_create_clip_job.",
150
+ inputSchema: { type: "object", properties: {} },
151
+ },
152
+ {
153
+ name: "katto_list_clip_lengths",
154
+ description:
155
+ "List the valid values for the optional config.clipLength on katto_create_clip_job (target clip " +
156
+ "duration buckets). Note: clipLength is fixed at job creation and cannot be changed by re-render.",
157
+ inputSchema: { type: "object", properties: {} },
158
+ },
159
+ {
160
+ name: "katto_list_caption_styles",
161
+ description:
162
+ "List the valid caption_style preset names for katto_rerender_clip (e.g. 'hormozi' for bold " +
163
+ "word-by-word highlight). Call this before re-rendering so you pass a real preset, not a guess.",
164
+ inputSchema: { type: "object", properties: {} },
165
+ },
166
+ {
167
+ name: "katto_rerender_clip",
168
+ description:
169
+ "Re-render one finished clip with a new reframe layout and/or caption style. Does NOT use video quota. " +
170
+ "Returns a rerender_id; poll katto_get_rerender for the new clip url.",
171
+ inputSchema: {
172
+ type: "object",
173
+ properties: {
174
+ id: { type: "string", description: "Job id from katto_create_clip_job." },
175
+ clip_index: { type: "number", description: "0-based index of the clip to re-render." },
176
+ layout_mode: {
177
+ type: "string",
178
+ enum: ["face_tracking", "wide", "split_screen", "stacked", "passthrough", "grid_3", "grid_4"],
179
+ },
180
+ caption_style: {
181
+ type: "string",
182
+ // Keep in sync with CAPTION_STYLES below (katto_list_caption_styles).
183
+ enum: ["default", "hormozi", "yellowPop", "redAlert", "skyline", "bubblegum", "aqua", "violet", "headline", "centerPop", "topLime", "impactMax", "bebasGold", "robotoBold", "montserratClean", "poppinsSoft", "robotoDoc", "broadcast", "wideClean", "mono", "bebasWhite", "rainbow", "multicolor"],
184
+ description: "A caption style preset name. See katto_list_caption_styles for the labels.",
185
+ },
186
+ },
187
+ required: ["id", "clip_index"],
188
+ },
189
+ },
190
+ {
191
+ name: "katto_dub_clip",
192
+ description:
193
+ "Re-render one finished clip dubbed into one or more of 19 languages (en, es, fr, it, pt, hi, ja, zh, de, ko, nl, pl, ru, tr, id, vi, th, uk, ar). Does " +
194
+ "NOT use video quota. Returns a rerender_id; poll katto_get_rerender for the result.",
195
+ inputSchema: {
196
+ type: "object",
197
+ properties: {
198
+ id: { type: "string", description: "Job id." },
199
+ clip_index: { type: "number", description: "0-based clip index." },
200
+ languages: {
201
+ type: "array",
202
+ items: { type: "string", enum: ["en", "es", "fr", "it", "pt", "hi", "ja", "zh", "de", "ko", "nl", "pl", "ru", "tr", "id", "vi", "th", "uk", "ar"] },
203
+ },
204
+ },
205
+ required: ["id", "clip_index", "languages"],
206
+ },
207
+ },
208
+ {
209
+ name: "katto_get_rerender",
210
+ description:
211
+ "Poll a re-render started by katto_rerender_clip or katto_dub_clip. Returns { status, clip_url, captions_url, dub_urls } — dub_urls is { lang: url } and is where a DUB lands (clip_url stays the undubbed re-render), null otherwise.",
212
+ inputSchema: {
213
+ type: "object",
214
+ properties: {
215
+ id: { type: "string", description: "Job id." },
216
+ rerender_id: { type: "string", description: "The rerender_id returned by rerender/dub." },
217
+ },
218
+ required: ["id", "rerender_id"],
219
+ },
220
+ },
221
+ {
222
+ name: "katto_get_brand_kit",
223
+ description:
224
+ "Get your saved brand kits (colors, caption font and position, default layout, watermark url).",
225
+ inputSchema: { type: "object", properties: {} },
226
+ },
227
+ {
228
+ name: "katto_get_webhook_secret",
229
+ description:
230
+ "Get your webhook signing secret and how to verify Katto's signed completion callbacks " +
231
+ "(HMAC-SHA256 of {timestamp}.{body}). Pass webhook_url on a job to receive them.",
232
+ inputSchema: { type: "object", properties: {} },
233
+ },
234
+ ];
235
+
236
+ // Tool behaviour hints (MCP annotations). Required by the Claude Connectors
237
+ // Directory review: every tool must declare read-only vs write, plus
238
+ // destructive / idempotent / open-world semantics, so a client can gate calls.
239
+ // All four hints are declared on every tool (explicit > omitted for reviewers).
240
+ // Read tools: read-only, non-destructive, idempotent, closed-world. Writes flip
241
+ // readOnlyHint. create_clip_job fetches a user URL (openWorld) + spends 1 quota
242
+ // slot (not idempotent). rerender/dub each spawn a NEW render (not idempotent —
243
+ // unsafe to blindly retry). cancel_job is the only destructive tool.
244
+ const ANNOTATIONS = {
245
+ katto_create_clip_job: { title: "Create clip job", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
246
+ katto_get_job: { title: "Get job status & clips", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
247
+ katto_list_jobs: { title: "List jobs", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
248
+ katto_get_clips: { title: "Get finished clips", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
249
+ katto_get_usage: { title: "Get plan & quota", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
250
+ katto_get_transcript: { title: "Get transcript", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
251
+ katto_cancel_job: { title: "Cancel job & refund slot", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
252
+ katto_get_account: { title: "Get account", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
253
+ katto_list_sources: { title: "List supported sources", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
254
+ katto_list_clip_lengths: { title: "List clip-length options", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
255
+ katto_list_caption_styles: { title: "List caption styles", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
256
+ katto_rerender_clip: { title: "Re-render a clip", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
257
+ katto_dub_clip: { title: "Dub a clip", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
258
+ katto_get_rerender: { title: "Poll a re-render", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
259
+ katto_get_brand_kit: { title: "Get brand kits", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
260
+ katto_get_webhook_secret: { title: "Get webhook signing secret", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
261
+ };
262
+
263
+ // Agent-facing descriptions (when to call, returns, errors, quota cost). Served
264
+ // on tools/list — identical across all sources, enriching the terser inline text.
265
+ const DESCRIPTIONS = {
266
+ katto_create_clip_job: "Submit a long video (YouTube, Twitch, Vimeo, Rumble, Zoom, Dailymotion) for clipping. Consumes 1 video from your monthly quota — check katto_get_usage first. Clips are produced asynchronously; returns a job id, then poll katto_get_job until status is 'completed'. Fails if the URL is unsupported or the video is over 90 minutes.",
267
+ katto_get_job: "Read-only. Get the status, progress and clips of a job by id. Clips come ranked by virality score (0-100, highest first; ties broken by clip_index). Each clip is { url, captions_url, title, score, clip_index, hd }: when status is 'completed' every url is a finished, downloadable clip; hd is true once the 1080p high-quality render is ready. hd=false does NOT mean broken — the url already works, the 1080p is just still rendering in the background (and stays the standard render on the free plan). The job-level hd_ready flag turns true once every clip is hd, i.e. the URLs are final and won't be swapped again (status='completed' fires earlier). Returns 404 for an unknown id.",
268
+ katto_list_jobs: "Read-only. List your recent jobs, newest first. Paginate with 'cursor' (pass the previous next_cursor); optional 'status' filter. Returns { jobs: [{id, status, source, created_at, completed_at}], next_cursor }.",
269
+ katto_get_clips: "Read-only convenience: the clips of a job as { url, captions_url, title, score, clip_index, hd }, ranked by score (highest first). hd=true means the 1080p render is ready; hd=false means the url is the standard render (HD still finishing, or the free plan). Empty until the first clip is ready.",
270
+ katto_get_usage: "Read-only. Your current plan and monthly video quota: { plan, videos_used, videos_limit, videos_remaining }. Call before katto_create_clip_job to confirm remaining quota.",
271
+ katto_get_transcript: "Read-only. The transcript of a completed job as timestamped segments [{ start, end, text }]. Returns 404 while the job is still processing.",
272
+ katto_cancel_job: "Cancel a still-running job (queued/processing) and refund the video back to your monthly quota. Safe to retry (idempotent). Returns an error if the job already finished, failed, or was cancelled.",
273
+ katto_get_account: "Read-only. The account behind this key: plan, this key's scopes (read/write), and monthly quota { videos_used, videos_limit, videos_remaining }.",
274
+ katto_list_sources: "Read-only. The video platforms Katto can clip from, each with an example URL. Use it to confirm a URL is supported before calling katto_create_clip_job.",
275
+ katto_list_clip_lengths: "Read-only. The valid values for the optional config.clipLength on katto_create_clip_job (target clip-duration buckets). Note: clipLength is fixed at creation and cannot be changed by re-render.",
276
+ katto_list_caption_styles: "Read-only. The valid caption_style preset names for katto_rerender_clip (id + label), e.g. 'hormozi' for bold word-by-word highlight. Call this before re-rendering so you pass a real preset instead of guessing.",
277
+ katto_rerender_clip: "Re-render one already-finished clip with a new reframe layout and/or caption style (get valid caption_style values from katto_list_caption_styles). Free — does NOT use video quota. Each call starts a new render (not idempotent); the original clip is kept. Returns a rerender_id; poll katto_get_rerender for the new clip url.",
278
+ katto_dub_clip: "Re-render one finished clip dubbed into one or more of 19 languages (en, es, fr, it, pt, hi, ja, zh, de, ko, nl, pl, ru, tr, id, vi, th, uk, ar). Creator feature (requires an active subscription); does NOT use your monthly video quota. Each call starts a new render (not idempotent). Returns a rerender_id; poll katto_get_rerender for the result.",
279
+ katto_get_rerender: "Read-only. Poll a re-render started by katto_rerender_clip or katto_dub_clip. Returns { status, clip_url, captions_url, dub_urls } — dub_urls is { lang: url } and is where a DUB lands (clip_url stays the undubbed re-render), null otherwise — clip_url is null until status is 'completed'.",
280
+ katto_get_brand_kit: "Read-only. Your saved brand kits (colors, caption font and position, default layout, watermark url).",
281
+ katto_get_webhook_secret: "Read-only. Returns your webhook signing SECRET — treat it as a credential (do not display or log it) — plus how to verify Katto's signed completion callbacks (HMAC-SHA256 of {timestamp}.{body}). Pass webhook_url on a job to receive them.",
282
+ };
283
+
284
+ // TOOLS enriched with agent descriptions + annotations — served on tools/list.
285
+ const TOOLS_LISTED = TOOLS.map((t) => ({
286
+ ...t,
287
+ description: DESCRIPTIONS[t.name] ?? t.description,
288
+ ...(ANNOTATIONS[t.name] ? { annotations: ANNOTATIONS[t.name] } : {}),
289
+ }));
290
+
291
+ // Static reference data — returned by the list_* tools without an API call.
292
+ const SOURCES = [
293
+ { id: 'youtube', example: 'https://www.youtube.com/watch?v=dQw4w9WgXcQ' },
294
+ { id: 'twitch', example: 'https://www.twitch.tv/videos/123456789' },
295
+ { id: 'vimeo', example: 'https://vimeo.com/123456789' },
296
+ { id: 'rumble', example: 'https://rumble.com/v1a2b3c-title.html' },
297
+ { id: 'zoom', example: 'https://zoom.us/rec/share/abc123' },
298
+ { id: 'dailymotion', example: 'https://www.dailymotion.com/video/x8abcde' },
299
+ ];
300
+ const CLIP_LENGTHS = [
301
+ { value: 'lt30', label: 'Under 30 seconds' },
302
+ { value: '30_60', label: '30 to 60 seconds' },
303
+ { value: '60_90', label: '60 to 90 seconds' },
304
+ { value: '90_180', label: '90 to 180 seconds' },
305
+ ];
306
+ // Valid values for caption_style on katto_rerender_clip. Mirrors the editor's
307
+ // named presets. Keep in sync with mcp-worker/src/index.js CAPTION_STYLES.
308
+ const CAPTION_STYLES = [
309
+ { value: 'default', label: 'Default' },
310
+ { value: 'hormozi', label: 'Hormozi (bold word-by-word highlight)' },
311
+ { value: 'yellowPop', label: 'Yellow Pop' },
312
+ { value: 'redAlert', label: 'Red Alert' },
313
+ { value: 'skyline', label: 'Skyline' },
314
+ { value: 'bubblegum', label: 'Bubblegum' },
315
+ { value: 'aqua', label: 'Aqua' },
316
+ { value: 'violet', label: 'Violet' },
317
+ { value: 'headline', label: 'Headline' },
318
+ { value: 'centerPop', label: 'Center Pop' },
319
+ { value: 'topLime', label: 'Top Lime' },
320
+ { value: 'impactMax', label: 'Impact Max' },
321
+ { value: 'bebasGold', label: 'Bebas Gold' },
322
+ { value: 'robotoBold', label: 'Roboto Bold' },
323
+ { value: 'montserratClean', label: 'Montserrat' },
324
+ { value: 'poppinsSoft', label: 'Poppins' },
325
+ { value: 'robotoDoc', label: 'Roboto Doc' },
326
+ { value: 'broadcast', label: 'Broadcast' },
327
+ { value: 'wideClean', label: 'Wide Clean' },
328
+ { value: 'mono', label: 'Mono' },
329
+ { value: 'bebasWhite', label: 'Bebas' },
330
+ { value: 'rainbow', label: 'Rainbow (per-word colour cycle)' },
331
+ { value: 'multicolor', label: 'Multicolor' },
332
+ ];
333
+
334
+ const server = new Server({ name: "katto", version: "0.5.8" }, { capabilities: { tools: {} } });
335
+
336
+ server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS_LISTED }));
337
+
338
+ server.setRequestHandler(CallToolRequestSchema, async (req) => {
339
+ const { name, arguments: args = {} } = req.params;
340
+ try {
341
+ let data;
342
+ if (name === "katto_create_clip_job") {
343
+ data = await api("/api/v1/jobs", {
344
+ method: "POST",
345
+ body: JSON.stringify({ url: args.url, config: args.config }),
346
+ });
347
+ } else if (name === "katto_get_job") {
348
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}`);
349
+ } else if (name === "katto_list_jobs") {
350
+ const qs = new URLSearchParams();
351
+ if (args.limit != null) qs.set("limit", String(args.limit));
352
+ if (args.cursor) qs.set("cursor", String(args.cursor));
353
+ if (args.status) qs.set("status", String(args.status));
354
+ const q = qs.toString();
355
+ data = await api(`/api/v1/jobs${q ? `?${q}` : ""}`);
356
+ } else if (name === "katto_get_clips") {
357
+ const job = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}`);
358
+ data = { id: job.id, status: job.status, clips: job.clips || [], dropped_clip_indices: job.dropped_clip_indices || [], dropped_clips: job.dropped_clips || [] };
359
+ } else if (name === "katto_get_usage") {
360
+ data = await api("/api/v1/usage");
361
+ } else if (name === "katto_get_transcript") {
362
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/transcript`);
363
+ } else if (name === "katto_cancel_job") {
364
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}`, { method: "DELETE" });
365
+ } else if (name === "katto_get_account") {
366
+ data = await api("/api/v1/me");
367
+ } else if (name === "katto_list_sources") {
368
+ data = { sources: SOURCES, note: "You can also clip a local file via the REST API (POST /v1/uploads)." };
369
+ } else if (name === "katto_list_clip_lengths") {
370
+ data = { clip_lengths: CLIP_LENGTHS };
371
+ } else if (name === "katto_list_caption_styles") {
372
+ data = { caption_styles: CAPTION_STYLES };
373
+ } else if (name === "katto_rerender_clip") {
374
+ const b = {};
375
+ if (args.layout_mode) b.layout_mode = args.layout_mode;
376
+ if (args.caption_style) b.caption_style = args.caption_style;
377
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/rerender`, { method: "POST", body: JSON.stringify(b) });
378
+ } else if (name === "katto_dub_clip") {
379
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/rerender`, { method: "POST", body: JSON.stringify({ dub: args.languages || [] }) });
380
+ } else if (name === "katto_get_rerender") {
381
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/rerenders/${encodeURIComponent(args.rerender_id)}`);
382
+ } else if (name === "katto_get_brand_kit") {
383
+ data = await api("/api/v1/brand-kit");
384
+ } else if (name === "katto_get_webhook_secret") {
385
+ data = await api("/api/v1/webhook");
386
+ } else {
387
+ throw new Error(`Unknown tool: ${name}`);
388
+ }
389
+ return { content: [{ type: "text", text: JSON.stringify(data, null, 2) }] };
390
+ } catch (e) {
391
+ return { content: [{ type: "text", text: `Error: ${e.message}` }], isError: true };
392
+ }
393
+ });
394
+
395
+ const transport = new StdioServerTransport();
396
+ await server.connect(transport);
397
+ console.error("[katto-mcp] ready");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "katto-mcp",
3
- "version": "0.5.7",
3
+ "version": "0.5.9",
4
4
  "mcpName": "io.github.miracleweasel/katto-mcp",
5
5
  "description": "AI video clipping MCP server: turn long videos, podcasts and Twitch VODs into scored, captioned 9:16 shorts from Claude, Cursor, ChatGPT and any MCP client.",
6
6
  "type": "module",
@@ -38,8 +38,13 @@
38
38
  "video-dubbing"
39
39
  ],
40
40
  "homepage": "https://katto.tech/docs/api",
41
- "repository": { "type": "git", "url": "git+https://github.com/miracleweasel/katto-mcp.git" },
42
- "bugs": { "url": "https://katto.tech/contact" },
41
+ "repository": {
42
+ "type": "git",
43
+ "url": "git+https://github.com/miracleweasel/katto-mcp.git"
44
+ },
45
+ "bugs": {
46
+ "url": "https://katto.tech/contact"
47
+ },
43
48
  "author": "Katto (https://katto.tech)",
44
49
  "license": "MIT",
45
50
  "dependencies": {