katto-mcp 0.5.9 → 0.6.0

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 (4) hide show
  1. package/README.md +24 -5
  2. package/SKILL.md +34 -10
  3. package/index.mjs +346 -20
  4. package/package.json +1 -1
package/README.md CHANGED
@@ -7,7 +7,7 @@
7
7
 
8
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
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).
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: the Creator plan's 25-video monthly quota covers videos up to 90 minutes each. API and MCP access is included on **every plan, including the free one** (2 videos/month), not gated behind a paid or enterprise tier.
11
11
 
12
12
  ## What it does
13
13
 
@@ -65,29 +65,48 @@ claude mcp add --transport http katto https://mcp.katto.tech/mcp
65
65
 
66
66
  ## Tools
67
67
 
68
+ **29 tools**, in five groups: nine for clipping, eight for editing, four for publishing, one for feedback, seven for account and reference.
69
+
68
70
  Clipping:
69
- - **`katto_create_clip_job(url, config?)`** clip a long video (YouTube, Twitch, Vimeo, Rumble, Zoom, Dailymotion). Returns a job id.
71
+ - **`katto_create_clip_job(url | upload_id, config?)`** clip a long video from a public link (YouTube, Twitch, Kick, Vimeo, Zoom, Google Drive, Dropbox, Loom, LinkedIn, Facebook, Rumble, Dailymotion, Medal.tv, or a direct https link to a video file) or a file uploaded with `katto_upload_video`. Returns a job id.
72
+ - **`katto_upload_video(filename, content_type, size)`** clip a local file: returns a presigned upload url to PUT the file to, then `katto_create_clip_job` with `upload_id`.
70
73
  - **`katto_get_job(id)`** poll until `status` is `completed`; `clips` holds the finished MP4 + caption (SRT) urls.
71
74
  - **`katto_get_clips(id)`** just the finished clips of a job (MP4 + SRT + title + virality score).
72
75
  - **`katto_get_transcript(id)`** the job's timestamped transcript segments.
73
76
  - **`katto_list_jobs(limit?, cursor?, status?)`** your recent jobs, newest first (keyset pagination).
74
77
  - **`katto_cancel_job(id)`** cancel a running job and refund the monthly video slot.
78
+ - **`katto_create_clip(id, start_seconds, end_seconds, aspect?, auto_reframe?)`** a new clip from any time range of the source (3-180 s, up to 10 per video, within 7 days). No quota.
79
+ - **`katto_more_clips(id, count?)`** render more clips from the moments Katto scored but did not render. No quota.
75
80
 
76
81
  Editing (no quota):
77
82
  - **`katto_rerender_clip(id, clip_index, layout_mode?, caption_style?)`** re-render one clip with a new reframe layout or caption style.
78
83
  - **`katto_dub_clip(id, clip_index, languages)`** re-render a clip dubbed into one or more of 19 languages.
79
84
  - **`katto_get_rerender(id, rerender_id)`** poll a re-render for the new versioned clip url.
85
+ - **`katto_translate_captions(id, clip_index, language)`** re-render a clip with its captions translated into one of 19 languages (Creator).
86
+ - **`katto_retitle_clip(id, clip_index, style? | title?)`** AI-regenerate a clip's title, or save your own.
87
+ - **`katto_adjust_clip(id, clip_index, start_offset_seconds?, end_offset_seconds?)`** trim or extend a clip (relative to its original bounds, 3-180 s; extending works within 7 days of the job) and re-render it.
88
+ - **`katto_add_hook(id, clip_index, text?, position?, duration?)`** burn a hook over the first seconds; without text, Katto's AI writes one.
89
+ - **`katto_generate_post_text(id, clip_index, platform?)`** AI-written title / caption / hashtags per platform, ready for `katto_publish_clip`. Nothing is posted.
90
+
91
+ Publishing:
92
+ - **`katto_list_connected_accounts()`** which of the 7 platforms are connected in Katto, and whether one needs a reconnect. Call it before publishing.
93
+ - **`katto_publish_clip(id, clip_index, platform, caption?, schedule_at?, ...)`** post a clip now or later to a social account connected in the Katto dashboard (YouTube, TikTok, LinkedIn, Instagram, Bluesky, Mastodon, Threads). Posts publicly: clients should confirm with the user first (annotated destructive). Paid plans; scheduling on Creator.
94
+ - **`katto_list_scheduled_posts(status?, limit?, cursor?)`** the scheduled posts, newest first, with their status and error.
95
+ - **`katto_cancel_scheduled_post(scheduled_post_id)`** cancel a post that has not gone out yet (annotated destructive).
96
+
97
+ Feedback:
98
+ - **`katto_rate_clip(id, clip_index, rating, reason?)`** record the user's good/bad verdict on a clip, like the thumbs on the results page.
80
99
 
81
100
  Account and reference:
82
101
  - **`katto_get_usage()`** your plan and remaining monthly video quota.
83
102
  - **`katto_get_account()`** the connected account, this key's scopes, and quota.
84
103
  - **`katto_get_brand_kit()`** your saved brand kits (colors, caption font, layout, watermark).
85
104
  - **`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.
105
+ - **`katto_list_sources()`** the link sources Katto can clip from, with an example url each.
87
106
  - **`katto_list_clip_lengths()`** the valid target clip-length buckets.
88
107
  - **`katto_list_caption_styles()`** the caption-style presets you can pass as `caption_style` when creating a job or re-rendering a clip.
89
108
 
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).
109
+ Jobs draw from your Katto plan's monthly video quota (25 on Creator, 10 on Solo, 2 on Free), with source videos up to 90, 60 and 30 minutes respectively. 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
110
 
92
111
  ## Env
93
112
 
@@ -108,7 +127,7 @@ Full docs: **[katto.tech/docs/api](https://katto.tech/docs/api)**
108
127
 
109
128
  - npm: [katto-mcp](https://www.npmjs.com/package/katto-mcp)
110
129
  - 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)
130
+ - [mcp.so](https://mcp.so/servers/katto) · [Smithery](https://smithery.ai/servers/dev-4lue/katto-mcp) · [Glama](https://glama.ai/mcp/servers/@miracleweasel/katto-mcp)
112
131
  - Official MCP Registry: `io.github.miracleweasel/katto-mcp`
113
132
 
114
133
  ## Public Discovery
package/SKILL.md CHANGED
@@ -1,13 +1,15 @@
1
1
  ---
2
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.
3
+ description: Turn a long video (a YouTube / Twitch / Kick / Vimeo / Zoom / Google Drive / Dropbox / Loom / LinkedIn / Facebook / Rumble / Dailymotion / Medal.tv link, a direct video-file link, or a local file) 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, to dub / re-render / retitle / translate an existing clip, or to publish it to a connected social account.
4
4
  ---
5
5
 
6
6
  # Katto clipping skill
7
7
 
8
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.
9
+ cut → captions → reframe → optional dub. You trigger it; nothing renders on the local
10
+ machine. `katto_publish_clip` posts to the social accounts the user connected in the Katto
11
+ app: **always get the user's explicit go-ahead first** (clip, platform, caption).
12
+ Local files: `katto_upload_video` → PUT the file → `katto_create_clip_job({ upload_id })`.
11
13
 
12
14
  ## Connect
13
15
  - **Hosted MCP (recommended, zero-install):** point the client at `https://mcp.katto.tech/mcp`
@@ -17,13 +19,33 @@ the local machine.
17
19
 
18
20
  ## Core flow
19
21
  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
+ monthly quota. Pass `idempotency_key` (e.g. a UUID) when you might retry, so a retry never
23
+ double-spends; pass `webhook_url` to be notified instead of polling.
24
+ 2. Poll `katto_get_job({ id })` every 30–60 s until `status` is `"completed"`.
22
25
  3. `katto_get_clips({ id })` → finished clips (MP4 url + SRT + title + score 0–100).
23
26
 
27
+ ## More clips from the same video — never counts against quota
28
+ - `katto_more_clips({ id, count? })` → renders the next best moments Katto scored but skipped.
29
+ - `katto_create_clip({ id, start_seconds, end_seconds })` → a clip from any range (source seconds,
30
+ 3–180 s, up to 10 per video, within 7 days of the job). Poll `katto_get_clips`.
31
+
24
32
  ## Editing — never counts against quota
25
33
  - `katto_rerender_clip({ id, clip_index, layout_mode?, caption_style? })`, then `katto_get_rerender(...)`.
26
34
  - `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.
35
+ - `katto_translate_captions({ id, clip_index, language })` → same 19 languages, captions only (Creator).
36
+ - `katto_retitle_clip({ id, clip_index, style? })` → new AI title (monthly allowance), or `{ title }` to set one.
37
+ - `katto_adjust_clip({ id, clip_index, start_offset_seconds?, end_offset_seconds? })` → trim (negative) or extend
38
+ (positive) relative to the clip's original bounds, 3–180 s; extending only within 7 days of the job. Poll `katto_get_rerender`.
39
+ - `katto_add_hook({ id, clip_index, text?, position?, duration? })` → hook text over the first seconds (AI-written if no text).
40
+ - `katto_rate_clip({ id, clip_index, rating: "good"|"bad", reason? })` → only when the user gave a verdict.
41
+
42
+ ## Publishing (paid plans; scheduling on Creator)
43
+ - First `katto_list_connected_accounts` → which platforms are connected (and `reconnect_required`).
44
+ - `katto_generate_post_text({ id, clip_index, platform? })` → AI caption / title / hashtags to show the user.
45
+ - `katto_publish_clip({ id, clip_index, platform, caption?, schedule_at? })` — YouTube, TikTok, LinkedIn,
46
+ Instagram, Bluesky, Mastodon, Threads. Not connected → error with a `connect_url`: ask the user to
47
+ connect it in the dashboard. TikTok needs `privacy_level` chosen by the user.
48
+ - `katto_list_scheduled_posts({ status? })` and `katto_cancel_scheduled_post({ scheduled_post_id })` (not yet published only).
27
49
 
28
50
  ## Reference / account
29
51
  - `katto_get_usage`, `katto_get_account` — remaining quota, plan, this key's scopes.
@@ -31,15 +53,17 @@ the local machine.
31
53
  - `katto_get_transcript` (timestamped), `katto_get_brand_kit`, `katto_get_webhook_secret` (treat as a credential).
32
54
 
33
55
  ## 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**.
56
+ - **One quota**, shared with the app and the API: Free = 2 videos/month (≤30 min each),
57
+ Solo = 10 (≤60 min), Creator = 25 (≤90 min). Re-renders are free; dubbing is Creator only.
58
+ - If a call fails on quota, tell the user and give them the upgrade link included in the error.
59
+ - Captions: **99 languages** (word-timed in 41, sentence-level in the rest). Dubbing: **19**.
37
60
  - **Don't invent timings.** The only measured figure is ~5 minutes for 8 clips on a 20-minute video;
38
61
  longer sources take longer. Say that, not a number you'd like.
39
62
 
40
63
  ## 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`
64
+ User: *"Clip this podcast, then dub clip 2 in Spanish."*
65
+ → `katto_create_clip_job({ url, idempotency_key })` → poll `katto_get_job` → `katto_get_clips`
66
+ (Katto decides how many clips the video holds: there is no clip-count parameter.)
43
67
  → `katto_dub_clip(id, 2, ["es"])` → `katto_get_rerender`.
44
68
 
45
69
  Full reference: https://katto.tech/docs/api · OpenAPI: https://katto.tech/openapi.json
package/index.mjs CHANGED
@@ -51,24 +51,27 @@ const TOOLS = [
51
51
  {
52
52
  name: "katto_create_clip_job",
53
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'.",
54
+ "Submit a long video to Katto by public link (YouTube, Twitch, Kick, Vimeo, Zoom, Google Drive, Dropbox, Loom, LinkedIn, Facebook, Rumble, Dailymotion, Medal.tv, or a direct https link to a video file) " +
55
+ "or as a file uploaded with katto_upload_video (pass upload_id). Returns a job id; the clips finish asynchronously. " +
56
+ "Poll katto_get_job with the id until status is 'completed'.",
56
57
  inputSchema: {
57
58
  type: "object",
58
59
  properties: {
59
- url: { type: "string", description: "Public video URL to clip." },
60
+ url: { type: "string", description: "Public video URL to clip (YouTube, Twitch, Kick, Vimeo, Zoom, Google Drive, Dropbox, Loom, LinkedIn, Facebook, Rumble, Dailymotion, Medal.tv, or a direct https link to a video file). Pass url OR upload_id." },
61
+ upload_id: { type: "string", description: "Instead of url: the upload_id returned by katto_upload_video, once the file has been PUT to its upload_url. Starts clipping that file." },
62
+ webhook_url: { type: "string", description: "Optional https URL that receives a signed POST when the job finishes, so you do not have to poll. Verify the signature with the secret from katto_get_webhook_secret. Private and local addresses are refused." },
63
+ idempotency_key: { type: "string", description: "Optional unique string (max 255 chars). Retrying with the same key returns the original job instead of creating a second one and spending another video. Use one per intended job, e.g. a UUID." },
60
64
  config: {
61
65
  type: "object",
62
66
  description: "Optional pre-clip settings.",
63
67
  properties: {
64
68
  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" },
69
+ clipLength: { type: "string", enum: ["lt30", "30_60", "60_90", "90_180"], description: "Target length band for the clips, in seconds. Guides candidate selection by setting a minimum and maximum duration; it is a preference, not a hard cut. Omit to let the model decide." },
70
+ customPrompt: { type: "string", description: "Free-text hint about what matters in this video. Its keywords nudge selection toward matching moments. It does not filter: a clip that matches nothing can still be selected if it scores well." },
71
+ topics: { type: "string", description: "Comma-separated topics to favour. Merged with customPrompt into a single keyword hint, so both bias the ranking rather than restrict it." },
68
72
  },
69
73
  },
70
74
  },
71
- required: ["url"],
72
75
  },
73
76
  },
74
77
  {
@@ -175,6 +178,7 @@ const TOOLS = [
175
178
  clip_index: { type: "number", description: "0-based index of the clip to re-render." },
176
179
  layout_mode: {
177
180
  type: "string",
181
+ description: "Force a reframing layout instead of letting detection choose one. Omit for automatic, which is what produces the best result on most clips.",
178
182
  enum: ["face_tracking", "wide", "split_screen", "stacked", "passthrough", "grid_3", "grid_4"],
179
183
  },
180
184
  caption_style: {
@@ -199,6 +203,7 @@ const TOOLS = [
199
203
  clip_index: { type: "number", description: "0-based clip index." },
200
204
  languages: {
201
205
  type: "array",
206
+ description: "Target languages for the dub, as ISO 639-1 codes. Nineteen are available; the clip keeps its original audio for any language not listed here.",
202
207
  items: { type: "string", enum: ["en", "es", "fr", "it", "pt", "hi", "ja", "zh", "de", "ko", "nl", "pl", "ru", "tr", "id", "vi", "th", "uk", "ar"] },
203
208
  },
204
209
  },
@@ -231,6 +236,205 @@ const TOOLS = [
231
236
  "(HMAC-SHA256 of {timestamp}.{body}). Pass webhook_url on a job to receive them.",
232
237
  inputSchema: { type: "object", properties: {} },
233
238
  },
239
+ {
240
+ name: "katto_create_clip",
241
+ description:
242
+ "Create a NEW clip from a time range of a completed job's source video (start/end in source seconds). " +
243
+ "Does not use video quota. Returns expected_clip_index; poll katto_get_clips until it appears.",
244
+ inputSchema: {
245
+ type: "object",
246
+ properties: {
247
+ id: { type: "string", description: "Job id of a completed job (from katto_create_clip_job)." },
248
+ start_seconds: { type: "number", description: "Start of the clip, in seconds of the SOURCE video (read timestamps from katto_get_transcript)." },
249
+ end_seconds: { type: "number", description: "End of the clip, in seconds of the source video. The clip must last 3 to 180 seconds." },
250
+ aspect: { type: "string", enum: ["9:16", "1:1", "16:9"], description: "Output aspect ratio. Omit to keep the job's setting (9:16 by default)." },
251
+ auto_reframe: { type: "boolean", description: "false = plain center crop instead of following the speaker. Omit for automatic reframing." },
252
+ },
253
+ required: ["id", "start_seconds", "end_seconds"],
254
+ },
255
+ },
256
+ {
257
+ name: "katto_more_clips",
258
+ description:
259
+ "Render more clips from a job's reserve (moments that were scored but not rendered). Does not use video quota. " +
260
+ "New clips are appended to the job; poll katto_get_clips.",
261
+ inputSchema: {
262
+ type: "object",
263
+ properties: {
264
+ id: { type: "string", description: "Job id from katto_create_clip_job." },
265
+ count: { type: "number", description: "How many extra clips to render (1-10, default 5). Capped by what is left in the reserve." },
266
+ },
267
+ required: ["id"],
268
+ },
269
+ },
270
+ {
271
+ name: "katto_upload_video",
272
+ description:
273
+ "Start clipping a LOCAL video file: returns a presigned upload_url to PUT the file to, then call " +
274
+ "katto_create_clip_job with upload_id. Use this when the video is a file, not a public link.",
275
+ inputSchema: {
276
+ type: "object",
277
+ properties: {
278
+ filename: { type: "string", description: "The file's name, e.g. episode-12.mp4 (display only)." },
279
+ content_type: { type: "string", enum: ["video/mp4", "video/quicktime", "video/webm", "video/x-matroska"], description: "MIME type of the file: mp4, mov (quicktime), webm or mkv (x-matroska)." },
280
+ size: { type: "number", description: "Exact file size in bytes. Checked against your plan's upload limit before anything is reserved." },
281
+ },
282
+ required: ["filename", "content_type", "size"],
283
+ },
284
+ },
285
+ {
286
+ name: "katto_translate_captions",
287
+ description:
288
+ "Translate a finished clip's burned-in captions into another language and re-render it (the audio is unchanged; " +
289
+ "use katto_dub_clip for voice). Returns a rerender_id; poll katto_get_rerender.",
290
+ inputSchema: {
291
+ type: "object",
292
+ properties: {
293
+ id: { type: "string", description: "Job id." },
294
+ clip_index: { type: "number", description: "0-based clip index." },
295
+ language: {
296
+ type: "string",
297
+ description: "Target caption language, ISO 639-1.",
298
+ enum: ["en", "es", "fr", "it", "pt", "hi", "ja", "zh", "de", "ko", "nl", "pl", "ru", "tr", "id", "vi", "th", "uk", "ar"],
299
+ },
300
+ },
301
+ required: ["id", "clip_index", "language"],
302
+ },
303
+ },
304
+ {
305
+ name: "katto_retitle_clip",
306
+ description:
307
+ "Give a clip a new title: AI-regenerate one (optionally in a style), or save a title you pass. Returns { title }.",
308
+ inputSchema: {
309
+ type: "object",
310
+ properties: {
311
+ id: { type: "string", description: "Job id." },
312
+ clip_index: { type: "number", description: "0-based clip index." },
313
+ style: { type: "string", enum: ["energetic", "descriptive", "fun", "shocking"], description: "Tone of the AI-generated title. Ignored when title is given." },
314
+ title: { type: "string", description: "Save this exact title instead of generating one (max 200 characters). Costs nothing." },
315
+ },
316
+ required: ["id", "clip_index"],
317
+ },
318
+ },
319
+ {
320
+ name: "katto_publish_clip",
321
+ description:
322
+ "Publish (or schedule) a finished clip to one of the user's connected social accounts. Public and irreversible: " +
323
+ "get the user's explicit confirmation of the clip, platform and caption first. Call katto_list_connected_accounts first.",
324
+ inputSchema: {
325
+ type: "object",
326
+ properties: {
327
+ id: { type: "string", description: "Job id." },
328
+ clip_index: { type: "number", description: "0-based clip index." },
329
+ platform: { type: "string", enum: ["youtube", "tiktok", "linkedin", "instagram", "bluesky", "mastodon", "threads"], description: "Where to post. The account must already be connected in the Katto dashboard." },
330
+ caption: { type: "string", description: "Post text. On YouTube it is used as the title and description unless title/description are given." },
331
+ title: { type: "string", description: "YouTube video title (max 100 characters). Defaults to the caption, then the clip's title." },
332
+ description: { type: "string", description: "YouTube description. Defaults to the caption." },
333
+ tags: { type: "array", items: { type: "string" }, description: "YouTube tags." },
334
+ privacy_status: { type: "string", enum: ["public", "unlisted", "private"], description: "YouTube visibility (defaults to public)." },
335
+ schedule_at: { type: "string", description: "Optional ISO 8601 date-time to publish later instead of now (future, at most 7 days ahead). Scheduling is a Creator feature." },
336
+ privacy_level: { type: "string", enum: ["PUBLIC_TO_EVERYONE", "MUTUAL_FOLLOW_FRIENDS", "FOLLOWER_OF_CREATOR", "SELF_ONLY"], description: "TikTok only, REQUIRED for TikTok: who can see the post. Must be chosen by the user, never assumed." },
337
+ disable_comment: { type: "boolean", description: "TikTok only: turn comments off." },
338
+ disable_duet: { type: "boolean", description: "TikTok only: turn duets off." },
339
+ disable_stitch: { type: "boolean", description: "TikTok only: turn stitches off." },
340
+ brand_content: { type: "boolean", description: "TikTok only: the post is a paid partnership (labelled 'Paid partnership'; cannot be SELF_ONLY)." },
341
+ brand_organic: { type: "boolean", description: "TikTok only: the post promotes the user's own business (labelled 'Promotional content')." },
342
+ },
343
+ required: ["id", "clip_index", "platform"],
344
+ },
345
+ },
346
+ {
347
+ name: "katto_list_connected_accounts",
348
+ description:
349
+ "List which social platforms (YouTube, TikTok, LinkedIn, Instagram, Bluesky, Mastodon, Threads) the user connected in Katto, " +
350
+ "with the account name and whether a reconnect is needed. Call before katto_publish_clip.",
351
+ inputSchema: { type: "object", properties: {} },
352
+ },
353
+ {
354
+ name: "katto_list_scheduled_posts",
355
+ description:
356
+ "List the user's scheduled social posts, newest first: { scheduled_posts: [{ id, job_id, clip_index, platform, scheduled_at, status, error }], next_cursor }.",
357
+ inputSchema: {
358
+ type: "object",
359
+ properties: {
360
+ status: { type: "string", enum: ["pending", "posting", "posted", "failed"], description: "Optional filter: pending (waiting for its time), posting, posted, failed." },
361
+ limit: { type: "number", description: "How many posts to return (1-100, default 20)." },
362
+ cursor: { type: "string", description: "Pass the previous response's next_cursor for the next page." },
363
+ },
364
+ },
365
+ },
366
+ {
367
+ name: "katto_cancel_scheduled_post",
368
+ description:
369
+ "Cancel a scheduled post that has not been published yet (pending or failed). Get its id from katto_list_scheduled_posts.",
370
+ inputSchema: {
371
+ type: "object",
372
+ properties: {
373
+ scheduled_post_id: { type: "string", description: "The scheduled post id (scheduled_post_id from katto_publish_clip, or id from katto_list_scheduled_posts)." },
374
+ },
375
+ required: ["scheduled_post_id"],
376
+ },
377
+ },
378
+ {
379
+ name: "katto_generate_post_text",
380
+ description:
381
+ "Write platform-ready post text for a clip (title, description/caption, hashtags) with AI, per platform. Returns text only; nothing is posted.",
382
+ inputSchema: {
383
+ type: "object",
384
+ properties: {
385
+ id: { type: "string", description: "Job id." },
386
+ clip_index: { type: "number", description: "0-based clip index." },
387
+ platform: { type: "string", enum: ["youtube", "tiktok", "linkedin", "instagram", "bluesky", "mastodon", "threads"], description: "Platform to write for. Omit to get text for all seven." },
388
+ },
389
+ required: ["id", "clip_index"],
390
+ },
391
+ },
392
+ {
393
+ name: "katto_adjust_clip",
394
+ description:
395
+ "Make a clip shorter or longer by moving its start and/or end, then re-render it. Returns a rerender_id; poll katto_get_rerender.",
396
+ inputSchema: {
397
+ type: "object",
398
+ properties: {
399
+ id: { type: "string", description: "Job id." },
400
+ clip_index: { type: "number", description: "0-based clip index." },
401
+ start_offset_seconds: { type: "number", description: "Seconds to move the START: positive starts earlier (extends), negative starts later (trims). Relative to the clip's original start." },
402
+ end_offset_seconds: { type: "number", description: "Seconds to move the END: positive ends later (extends), negative ends earlier (trims). Relative to the clip's original end." },
403
+ },
404
+ required: ["id", "clip_index"],
405
+ },
406
+ },
407
+ {
408
+ name: "katto_add_hook",
409
+ description:
410
+ "Burn a hook (big text over the first seconds) into a clip and re-render it. Without text, Katto's AI writes one. Returns a rerender_id; poll katto_get_rerender.",
411
+ inputSchema: {
412
+ type: "object",
413
+ properties: {
414
+ id: { type: "string", description: "Job id." },
415
+ clip_index: { type: "number", description: "0-based clip index." },
416
+ text: { type: "string", description: "The hook to display (max 80 characters; 3-7 words reads best). Omit to let Katto write one from the clip." },
417
+ position: { type: "string", enum: ["top", "center"], description: "Where the hook sits on the frame (default top)." },
418
+ duration: { type: "number", description: "How many seconds the hook stays on screen, 1-10 (default 4)." },
419
+ },
420
+ required: ["id", "clip_index"],
421
+ },
422
+ },
423
+ {
424
+ name: "katto_rate_clip",
425
+ description:
426
+ "Record the user's verdict on a clip (good or bad, optional reason), like the thumbs up/down on the results page. Helps Katto pick better clips.",
427
+ inputSchema: {
428
+ type: "object",
429
+ properties: {
430
+ id: { type: "string", description: "Job id." },
431
+ clip_index: { type: "number", description: "0-based clip index." },
432
+ rating: { type: "string", enum: ["good", "bad"], description: "The user's verdict on the clip." },
433
+ reason: { type: "string", description: "Optional: why, in the user's words (max 2000 characters)." },
434
+ },
435
+ required: ["id", "clip_index", "rating"],
436
+ },
437
+ },
234
438
  ];
235
439
 
236
440
  // Tool behaviour hints (MCP annotations). Required by the Claude Connectors
@@ -240,7 +444,11 @@ const TOOLS = [
240
444
  // Read tools: read-only, non-destructive, idempotent, closed-world. Writes flip
241
445
  // readOnlyHint. create_clip_job fetches a user URL (openWorld) + spends 1 quota
242
446
  // slot (not idempotent). rerender/dub each spawn a NEW render (not idempotent —
243
- // unsafe to blindly retry). cancel_job is the only destructive tool.
447
+ // unsafe to blindly retry), as are create_clip / more_clips / translate / retitle /
448
+ // upload. publish_clip posts publicly on the user's account (destructive, open-world,
449
+ // never idempotent) and cancel_job refunds a running job. cancel_scheduled_post removes a
450
+ // pending post (destructive, idempotent). generate_post_text / adjust_clip / add_hook each
451
+ // spend an AI call or a render (not idempotent); rate_clip records a verdict (idempotent).
244
452
  const ANNOTATIONS = {
245
453
  katto_create_clip_job: { title: "Create clip job", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
246
454
  katto_get_job: { title: "Get job status & clips", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
@@ -258,12 +466,25 @@ const ANNOTATIONS = {
258
466
  katto_get_rerender: { title: "Poll a re-render", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
259
467
  katto_get_brand_kit: { title: "Get brand kits", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
260
468
  katto_get_webhook_secret: { title: "Get webhook signing secret", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
469
+ katto_create_clip: { title: "Create a clip from a time range", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
470
+ katto_more_clips: { title: "Render more clips from the reserve", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
471
+ katto_upload_video: { title: "Upload a local video", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
472
+ katto_translate_captions: { title: "Translate a clip's captions", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
473
+ katto_retitle_clip: { title: "Retitle a clip", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
474
+ katto_publish_clip: { title: "Publish a clip to social media", readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
475
+ katto_list_connected_accounts: { title: "List connected social accounts", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
476
+ katto_list_scheduled_posts: { title: "List scheduled posts", readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false },
477
+ katto_cancel_scheduled_post: { title: "Cancel a scheduled post", readOnlyHint: false, destructiveHint: true, idempotentHint: true, openWorldHint: false },
478
+ katto_generate_post_text: { title: "Write post text for a clip", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
479
+ katto_adjust_clip: { title: "Trim or extend a clip", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
480
+ katto_add_hook: { title: "Add a hook to a clip", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
481
+ katto_rate_clip: { title: "Rate a clip", readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
261
482
  };
262
483
 
263
484
  // Agent-facing descriptions (when to call, returns, errors, quota cost). Served
264
485
  // on tools/list — identical across all sources, enriching the terser inline text.
265
486
  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.",
487
+ katto_create_clip_job: "Submit a long video for clipping, either by public link in url (YouTube, Twitch, Kick, Vimeo, Zoom, Google Drive, Dropbox, Loom, LinkedIn, Facebook, Rumble, Dailymotion, Medal.tv, or a direct https link to a video file; Google Drive and Dropbox links must be shared publicly) or, for a local file, by upload_id after katto_upload_video and the PUT of the file. Pass exactly one of url or upload_id. 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 every 30-60 s until status is 'completed', or pass webhook_url to be notified (url jobs only). Pass idempotency_key when you might retry a url job, so a retry never spends a second video (an upload can only be started once anyway). Fails if the URL is unsupported, the uploaded file is missing, or the video is longer than your plan allows.",
267
488
  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
489
  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
490
  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.",
@@ -271,7 +492,7 @@ const DESCRIPTIONS = {
271
492
  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
493
  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
494
  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.",
495
+ katto_list_sources: "Read-only. The link sources Katto can clip from (YouTube, Twitch, Kick, Vimeo, Zoom, Google Drive, Dropbox, Loom, LinkedIn, Facebook, Rumble, Dailymotion, Medal.tv, or a direct https link to a video file), each with an example URL. Use it to confirm a URL is supported before calling katto_create_clip_job; for a local file use katto_upload_video instead.",
275
496
  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
497
  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
498
  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.",
@@ -279,6 +500,19 @@ const DESCRIPTIONS = {
279
500
  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
501
  katto_get_brand_kit: "Read-only. Your saved brand kits (colors, caption font and position, default layout, watermark url).",
281
502
  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.",
503
+ katto_create_clip: "Create a NEW clip from any time range of a completed job's source video, when the user wants a moment Katto did not pick. start_seconds/end_seconds are SOURCE-video seconds (take them from katto_get_transcript segments); the clip must last 3-180 s. Does NOT use video quota. Limits: at most 10 created clips per video, one clip in preparation at a time (409 'Another clip is being prepared' = wait and retry), and only within 7 days of the job (410 after that: the source is no longer kept). Captions come from the words Katto already timed; has_captions=false means that range had none and the clip renders without captions. Not idempotent: each call queues another clip. Returns expected_clip_index; poll katto_get_clips every 15-30 s until a clip with that clip_index appears.",
504
+ katto_more_clips: "Render more clips from a job's reserve: moments Katto scored but did not render, best first. Use when the user wants more clips from the same video. Does NOT use video quota. count is 1-10 (default 5), capped by what is left. Returns { status: 'queued'|'pending'|'empty', requested, remaining }: 'pending' means a request is already running (do not call again), 'empty' means the reserve is used up. New clips are appended to the job; poll katto_get_clips.",
505
+ katto_upload_video: "Step 1 of clipping a LOCAL file (when the user has a file, not a public link). Pass the exact filename, MIME type and size in bytes; the size is checked against the plan's upload limit and the monthly quota is checked (not yet spent). Returns { upload_id, upload_url, headers, expires_in, next_steps }. Then: (2) HTTP PUT the raw file bytes to upload_url with exactly the returned Content-Type header (e.g. curl -X PUT -H 'Content-Type: video/mp4' --upload-file ./file.mp4 '<upload_url>'), before expires_in seconds; (3) call katto_create_clip_job with upload_id: that verifies the file arrived, spends 1 video of quota and starts clipping; (4) poll katto_get_job with id = upload_id. You need a tool that can make an HTTP PUT with a file body (shell, code execution); if you have none, give the user the upload_url and the curl command.",
506
+ katto_translate_captions: "Translate one finished clip's burned-in captions into one of 19 languages and re-render it with them; word timings are kept and the original clip is preserved. Only the captions change: for a translated VOICE use katto_dub_clip. Creator feature (requires an active Creator subscription; otherwise an error with code upgrade_required and a pricing link to give the user). Does NOT use video quota. Not idempotent: each call starts a new render. Returns a rerender_id; poll katto_get_rerender until status is 'completed' for the new clip_url.",
507
+ katto_retitle_clip: "Change a clip's title. Without 'title', Katto's AI writes a new scroll-stopping title in the clip's language (optional style: energetic, descriptive, fun, shocking); each AI regeneration uses one unit of the plan's monthly title-regeneration allowance (an error with 'limit' when it is used up). With 'title', that exact text is saved and nothing is spent. Synchronous: returns { title, clip_index, regenerated }. Does NOT use video quota.",
508
+ katto_publish_clip: "Publish a finished clip to one of the user's connected social accounts (YouTube, TikTok, LinkedIn, Instagram, Bluesky, Mastodon, Threads), now or at schedule_at (future, at most 7 days ahead). Call katto_list_connected_accounts first to see which platforms are connected (and not in need of a reconnect); katto_generate_post_text can draft the caption. This posts PUBLICLY on the user's behalf and cannot be undone from Katto: before calling, show the user the clip, the platform, the caption (and TikTok privacy level) and get their explicit confirmation; never publish on your own initiative or retry a publish blindly (each call is a new post). Requires a paid plan; scheduling requires Creator. If the platform is not connected you get an error with code platform_not_connected and a connect_url: ask the user to connect it in the Katto dashboard, then retry. TikTok requires privacy_level chosen by the user (no default) and disclosure of commercial content (brand_content / brand_organic); until Katto's TikTok app is audited, TikTok only accepts posts to a private account. Returns { status: 'published', result } or { status: 'scheduled', scheduled_post_id, scheduled_at }.",
509
+ katto_list_connected_accounts: "Read-only. Which of the 7 publishable platforms (youtube, tiktok, linkedin, instagram, bluesky, mastodon, threads) the user has connected in the Katto dashboard: { connections: [{ platform, connected, display_name, reconnect_required, connected_at }], connect_url }. Call it BEFORE katto_publish_clip to pick a platform that will work: if the one the user wants shows connected=false or reconnect_required=true, ask them to (re)connect it at connect_url instead of attempting the publish. Never returns tokens.",
510
+ katto_list_scheduled_posts: "Read-only. The user's scheduled social posts (from katto_publish_clip with schedule_at, or the dashboard calendar), newest first: { scheduled_posts: [{ id, job_id, clip_index, platform, scheduled_at, status, title, error, posted_at, created_at }], next_cursor }. status: pending = waiting for its time, posting = being published, posted = done, failed = see error. Paginate with cursor = the previous next_cursor; optional status filter.",
511
+ katto_cancel_scheduled_post: "Cancel a scheduled post before it goes out: it is removed from the calendar and will not be published (same as deleting it in the dashboard calendar). Only pending or failed posts can be cancelled; one already posting or posted returns an error (a published post cannot be taken down from Katto). Confirm with the user which post (platform, clip, time) before cancelling. Safe to retry: a second call returns 'not found'. Returns { id, status: 'cancelled', previous_status }.",
512
+ katto_generate_post_text: "Write publish-ready post text for one clip with AI, tailored to each platform's tone and character limits, in the clip's language: { clip_index, posts: { [platform]: { title, description, hashtags } } }. title is only filled for YouTube (for the other platforms the caption is description, hashtags included). Omit platform to get all seven at once. Nothing is posted or saved and no video quota is used: show the text to the user, then pass it to katto_publish_clip (caption, or title/description for YouTube). Each call is a new AI generation (identical requests within an hour return the cached text).",
513
+ katto_adjust_clip: "Trim or extend one finished clip by moving its start and/or end, then re-render it. Offsets are in seconds, relative to the clip's ORIGINAL bounds (two calls do not add up): positive extends (start earlier / end later), negative trims. The result must last 3-180 s and stay inside the source video. Extending re-cuts from the source, which Katto keeps 7 days: after that only trimming works (410). Captions for an extended part come from words Katto already timed or the transcript sentence. Does NOT use video quota; the original clip is kept. Not idempotent: each call starts a new render. Returns { rerender_id, status_url, start_seconds, end_seconds, duration_seconds, extended }; poll katto_get_rerender for the new clip_url.",
514
+ katto_add_hook: "Add a hook, big bold text over the first seconds that stops the scroll, to one finished clip and re-render it. Pass text to use your own (max 80 characters, 3-7 words reads best) or omit it and Katto's AI writes one from the clip in its language. position: top (default) or center; duration: 1-10 s (default 4). Styled like the editor's AI Hook (white, boxed, pop-in). Does NOT use video quota; the original clip is kept. Not idempotent: each call starts a new render, and a re-render starts from the original clip, so calling it again replaces the hook rather than stacking two. Returns { rerender_id, hook_text, generated, status_url }; poll katto_get_rerender for the new clip_url.",
515
+ katto_rate_clip: "Record the user's verdict on a clip: rating good or bad, with an optional reason in their words. Same signal as the thumbs up/down on the Katto results page; it is used to improve which moments Katto picks. Only rate when the user expressed an opinion, never on your own. Rating again records the new verdict (the latest one counts). Returns { ok, clip_index, rating }.",
282
516
  };
283
517
 
284
518
  // TOOLS enriched with agent descriptions + annotations — served on tools/list.
@@ -290,12 +524,20 @@ const TOOLS_LISTED = TOOLS.map((t) => ({
290
524
 
291
525
  // Static reference data — returned by the list_* tools without an API call.
292
526
  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' },
527
+ { id: "youtube", example: "https://www.youtube.com/watch?v=dQw4w9WgXcQ" },
528
+ { id: "twitch", example: "https://www.twitch.tv/videos/123456789" },
529
+ { id: "kick", example: "https://kick.com/xqc/videos/0b5c3a7e-1234-4cde-9f00-123456789abc" },
530
+ { id: "vimeo", example: "https://vimeo.com/123456789" },
531
+ { id: "zoom", example: "https://zoom.us/rec/share/abc123", note: "Public share link without a passcode." },
532
+ { id: "gdrive", example: "https://drive.google.com/file/d/1AbCdEfGhIjKlMnOpQrStUvWxYz012345/view", note: "Shared as 'Anyone with the link'." },
533
+ { id: "dropbox", example: "https://www.dropbox.com/scl/fi/abc123def456/video.mp4?rlkey=xyz789&dl=0" },
534
+ { id: "loom", example: "https://www.loom.com/share/0123456789abcdef0123456789abcdef" },
535
+ { id: "linkedin", example: "https://www.linkedin.com/posts/janedoe_activity-7123456789012345678-AbCd" },
536
+ { id: "facebook", example: "https://www.facebook.com/reel/1234567890123456" },
537
+ { id: "rumble", example: "https://rumble.com/v1a2b3c-title.html" },
538
+ { id: "dailymotion", example: "https://www.dailymotion.com/video/x8abcde" },
539
+ { id: "medal", example: "https://medal.tv/games/valorant/clips/abc123XYZ" },
540
+ { id: "url", example: "https://example.com/media/episode-12.mp4", note: "Direct https link to a .mp4, .mov, .m4v, .webm or .mkv file on a public host." },
299
541
  ];
300
542
  const CLIP_LENGTHS = [
301
543
  { value: 'lt30', label: 'Under 30 seconds' },
@@ -331,7 +573,49 @@ const CAPTION_STYLES = [
331
573
  { value: 'multicolor', label: 'Multicolor' },
332
574
  ];
333
575
 
334
- const server = new Server({ name: "katto", version: "0.5.8" }, { capabilities: { tools: {} } });
576
+ // Lues par le client au demarrage, avant tout appel : le deroule type, la
577
+ // cadence de sondage et quoi faire d'un quota epuise. Meme texte dans les 3 sources.
578
+ const SERVER_INSTRUCTIONS = "Katto turns a long video into short vertical clips. Typical flow: katto_get_usage (each new job spends 1 video from the monthly quota) -> katto_create_clip_job with the public URL -> poll katto_get_job every 30-60 seconds until status is 'completed' (a 20-minute video took about 5 minutes for 8 clips; longer sources take longer) -> katto_get_clips, and show each clip's title and score rather than only ids. Do not download, open or transcribe the video yourself: Katto fetches it. If a call fails with a quota error, tell the user and give them the upgrade link included in the message. Re-renders and dubbing are separate jobs: start one, then poll katto_get_rerender; do not start the same re-render twice in a loop. For a local file: katto_upload_video, PUT the file to the returned upload_url, then katto_create_clip_job with upload_id. Never call katto_publish_clip without the user's explicit confirmation of the clip, platform and caption: it posts publicly on their account.";
579
+
580
+ // katto_upload_video: the REST response plus the exact next steps, because an
581
+ // agent that only sees "upload_url" tends to stop there. upload_id = job_id.
582
+ function uploadNextSteps(up) {
583
+ const ct = (up.headers && up.headers["Content-Type"]) || "video/mp4";
584
+ return {
585
+ ...up,
586
+ upload_id: up.job_id,
587
+ next_steps: [
588
+ `1. HTTP PUT the raw file bytes to upload_url with the header Content-Type: ${ct}, within ${up.expires_in} seconds. Example: curl -X PUT -H "Content-Type: ${ct}" --upload-file ./your-file "<upload_url>"`,
589
+ `2. Call katto_create_clip_job with upload_id "${up.job_id}" (optional config). It checks the file arrived, spends 1 video of quota and starts clipping. Calling it before the PUT finishes returns an error you can retry.`,
590
+ `3. Poll katto_get_job with id "${up.job_id}" every 30-60 s until status is 'completed'.`,
591
+ ],
592
+ };
593
+ }
594
+
595
+ // katto_publish_clip: forward only the fields the REST endpoint knows.
596
+ const PUBLISH_FIELDS = ["platform", "caption", "title", "description", "tags", "privacy_status", "schedule_at", "privacy_level", "disable_comment", "disable_duet", "disable_stitch", "brand_content", "brand_organic"];
597
+ function publishBody(args) {
598
+ const b = {};
599
+ for (const k of PUBLISH_FIELDS) if (args[k] !== undefined && args[k] !== null && args[k] !== "") b[k] = args[k];
600
+ return b;
601
+ }
602
+
603
+ // katto_adjust_clip / katto_add_hook: forward only the fields that were given.
604
+ function adjustBody(args) {
605
+ const b = {};
606
+ if (typeof args.start_offset_seconds === "number") b.start_offset_seconds = args.start_offset_seconds;
607
+ if (typeof args.end_offset_seconds === "number") b.end_offset_seconds = args.end_offset_seconds;
608
+ return b;
609
+ }
610
+ function hookBody(args) {
611
+ const b = {};
612
+ if (args.text) b.text = args.text;
613
+ if (args.position) b.position = args.position;
614
+ if (typeof args.duration === "number") b.duration = args.duration;
615
+ return b;
616
+ }
617
+
618
+ const server = new Server({ name: "katto", version: "0.6.0" }, { capabilities: { tools: {} }, instructions: SERVER_INSTRUCTIONS });
335
619
 
336
620
  server.setRequestHandler(ListToolsRequestSchema, async () => ({ tools: TOOLS_LISTED }));
337
621
 
@@ -339,10 +623,15 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
339
623
  const { name, arguments: args = {} } = req.params;
340
624
  try {
341
625
  let data;
342
- if (name === "katto_create_clip_job") {
626
+ if (name === "katto_create_clip_job" && args.upload_id) {
627
+ // An uploaded file: the complete step verifies the bytes, charges the quota and enqueues.
628
+ data = await api(`/api/v1/uploads/${encodeURIComponent(args.upload_id)}/complete`, { method: "POST", body: JSON.stringify(args.config ? { config: args.config } : {}) });
629
+ } else if (name === "katto_create_clip_job") {
630
+ if (!args.url) throw new Error("Pass url (a public video link) or upload_id (from katto_upload_video).");
343
631
  data = await api("/api/v1/jobs", {
344
632
  method: "POST",
345
- body: JSON.stringify({ url: args.url, config: args.config }),
633
+ body: JSON.stringify({ url: args.url, config: args.config, ...(args.webhook_url ? { webhook_url: args.webhook_url } : {}) }),
634
+ ...(args.idempotency_key ? { headers: { "Idempotency-Key": String(args.idempotency_key) } } : {}),
346
635
  });
347
636
  } else if (name === "katto_get_job") {
348
637
  data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}`);
@@ -365,7 +654,7 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
365
654
  } else if (name === "katto_get_account") {
366
655
  data = await api("/api/v1/me");
367
656
  } 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)." };
657
+ data = { sources: SOURCES, note: "To clip a local file instead of a link, use katto_upload_video, then katto_create_clip_job with upload_id." };
369
658
  } else if (name === "katto_list_clip_lengths") {
370
659
  data = { clip_lengths: CLIP_LENGTHS };
371
660
  } else if (name === "katto_list_caption_styles") {
@@ -383,6 +672,43 @@ server.setRequestHandler(CallToolRequestSchema, async (req) => {
383
672
  data = await api("/api/v1/brand-kit");
384
673
  } else if (name === "katto_get_webhook_secret") {
385
674
  data = await api("/api/v1/webhook");
675
+ } else if (name === "katto_create_clip") {
676
+ const b = { start_seconds: args.start_seconds, end_seconds: args.end_seconds };
677
+ if (args.aspect) b.aspect = args.aspect;
678
+ if (typeof args.auto_reframe === "boolean") b.auto_reframe = args.auto_reframe;
679
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips`, { method: "POST", body: JSON.stringify(b) });
680
+ } else if (name === "katto_more_clips") {
681
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/more`, { method: "POST", body: JSON.stringify(args.count != null ? { count: args.count } : {}) });
682
+ } else if (name === "katto_upload_video") {
683
+ data = uploadNextSteps(await api("/api/v1/uploads", { method: "POST", body: JSON.stringify({ filename: args.filename, content_type: args.content_type, size: args.size }) }));
684
+ } else if (name === "katto_translate_captions") {
685
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/translate-captions`, { method: "POST", body: JSON.stringify({ language: args.language }) });
686
+ } else if (name === "katto_retitle_clip") {
687
+ const b = {};
688
+ if (args.title) b.title = args.title;
689
+ else if (args.style) b.style = args.style;
690
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/retitle`, { method: "POST", body: JSON.stringify(b) });
691
+ } else if (name === "katto_publish_clip") {
692
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/publish`, { method: "POST", body: JSON.stringify(publishBody(args)) });
693
+ } else if (name === "katto_list_connected_accounts") {
694
+ data = await api("/api/v1/connections");
695
+ } else if (name === "katto_list_scheduled_posts") {
696
+ const qs = new URLSearchParams();
697
+ if (args.status) qs.set("status", String(args.status));
698
+ if (args.limit != null) qs.set("limit", String(args.limit));
699
+ if (args.cursor) qs.set("cursor", String(args.cursor));
700
+ const q = qs.toString();
701
+ data = await api(`/api/v1/scheduled-posts${q ? `?${q}` : ""}`);
702
+ } else if (name === "katto_cancel_scheduled_post") {
703
+ data = await api(`/api/v1/scheduled-posts/${encodeURIComponent(args.scheduled_post_id)}`, { method: "DELETE" });
704
+ } else if (name === "katto_generate_post_text") {
705
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/post-text`, { method: "POST", body: JSON.stringify(args.platform ? { platform: args.platform } : {}) });
706
+ } else if (name === "katto_adjust_clip") {
707
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/adjust`, { method: "POST", body: JSON.stringify(adjustBody(args)) });
708
+ } else if (name === "katto_add_hook") {
709
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/hook`, { method: "POST", body: JSON.stringify(hookBody(args)) });
710
+ } else if (name === "katto_rate_clip") {
711
+ data = await api(`/api/v1/jobs/${encodeURIComponent(args.id)}/clips/${encodeURIComponent(args.clip_index)}/feedback`, { method: "POST", body: JSON.stringify(args.reason ? { rating: args.rating, reason: args.reason } : { rating: args.rating }) });
386
712
  } else {
387
713
  throw new Error(`Unknown tool: ${name}`);
388
714
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "katto-mcp",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
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",