@kolbo/mcp 1.123.0 → 1.124.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.
package/README.md CHANGED
@@ -322,6 +322,8 @@ Requires DaVinci Resolve Studio with the Kolbo plugin open (Workspace → Workfl
322
322
  | `list_visual_dna_folders` / `create_visual_dna_folder` / `update_visual_dna_folder` / `delete_visual_dna_folder` / `move_visual_dna_to_folder` | Organize Visual DNA characters into user folders (create/rename/recolor/delete + move DNAs in/out) |
323
323
  | `create_project` / `update_project` / `archive_project` / `unarchive_project` | Project lifecycle (create/rename/describe/archive; deletion stays in-app) |
324
324
  | `publish_project` / `get_project_publication` / `unpublish_project` / `submit_project_to_catalog` / `remove_project_hero_video` | Publish a project as a public showcase link, with write-up and final video as hero |
325
+ | `get_project_page_insights` | Visitor analytics for a published project page: visitors, active time, scroll depth, story reach, clicks, copies, sources, countries, most-opened media |
326
+ | `get_project_story` / `update_project_story` / `draft_project_story` | Write the published page as a post: headings, text, media, workflow steps (real prompt + model), before/after. AI draft from the project's own media |
325
327
  | `transfer_visual_dna` / `list_visual_dna_transfers` / `respond_visual_dna_transfer` | Send Visual DNAs you own to another Kolbo account (they accept a copy with new ids; optionally move instead of copy) |
326
328
  | `transfer_project` / `list_project_transfers` / `respond_project_transfer` | Hand a project you own to another Kolbo account (they accept; same ids; cast assets copied; payer history unchanged) |
327
329
  | `list_skills` / `create_skill` / `update_skill` / `delete_skill` | Skills — reusable named personas for the chat tool; `description` is the system instruction |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kolbo/mcp",
3
- "version": "1.123.0",
3
+ "version": "1.124.0",
4
4
  "description": "Kolbo AI MCP Server - Generate images, videos, music, speech, and sound effects from Claude Code",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -1,6 +1,6 @@
1
1
  # AUTO-GENERATED — do not edit
2
2
 
3
- This tree is mirrored from kolbo-code@75f60e6, the single source of truth.
3
+ This tree is mirrored from kolbo-code@db5c5c6, the single source of truth.
4
4
  Canonical source: packages/opencode/skills/kolbo/
5
5
  Distribution: .github/workflows/sync-skill-to-plugin.yml
6
6
 
package/skill/SKILL.md CHANGED
@@ -141,7 +141,6 @@ Font tools (when exposed by the installed MCP): `list_fonts`, `get_font`, `uploa
141
141
  ### Discovery, Library, Visual DNA, Moodboards, Chat, Publishing
142
142
  | Tool | Purpose |
143
143
  |------|---------|
144
- | `publish_project` / `get_project_publication` / `unpublish_project` / `submit_project_to_catalog` / `remove_project_hero_video` | Public showcase link for a project you own (the app's Publish tab). `publish_project` returns the public `url` and, in the same call, sets `allow_fork`, `showcase_about` and the FINAL VIDEO as page hero: first put the video in the project (`upload_media` with `project_id`, or generate with `project_id`), then pass its id as `hero_video_media_id`. Confirm with the user before publishing — anyone with the link sees the project's media. |
145
144
  | `list_models` / `list_voices` / `check_credits` / `show_plans` / `get_generation_status` / `cancel_generation` / `get_session_usage` | Discovery + status. `list_models` with no args returns the recommended shortlist out of ~428 — pass `type` for a full category with per-model caps. `cancel_generation` stops an in-flight job and refunds what it can: use it when the user changes their mind mid-generation instead of letting it run. `show_plans` renders the balance + upgrade card for pricing/plan/upgrade questions. |
146
145
  | `download_media_from_url` / `get_download_status` / `cancel_download` | Download a public social/video page URL to Kolbo storage. Async job; use download status, not generation status. See `workflows/media-library.md`. |
147
146
  | `upload_media` / `create_upload_ticket` / `list_media` / `get_media` / `get_media_stats` / `favorite_media` / `unfavorite_media` / `dislike_media` / `undislike_media` / `rate_media` / `clear_media_rating` / `delete_media` / `restore_media` / `permanently_delete_media` / `move_media` / `bulk_*_media` / `*_media_folder` | Media library — see `workflows/media-library.md`. Getting a LOCAL file in depends on where the server runs: `upload_media` with a path only works on a local (stdio) install; over a remote connector use `create_upload_ticket` and POST the file yourself. |
@@ -153,6 +152,7 @@ Font tools (when exposed by the installed MCP): `list_fonts`, `get_font`, `uploa
153
152
  | `search_music_library` / `browse_music_library` / `get_music_library_facets` / `get_music_track_audio` / `get_music_track_lyrics` / `get_music_track_related` / `analyze_script_for_music` / `acquire_clean_music_track` / `import_music_track_to_library` | **SYNCI licensed music** — commercially licensed catalog, not free stock; previews are **watermarked**. `acquire_clean_music_track` / `import_music_track_to_library` **CHARGES CREDITS** for the clean master — confirm with the user first + pass a stable `requestId`. Details: `workflows/media-library.md` "SYNCI licensed music". |
154
153
  | `list_projects` / `get_project` / `move_session` | Projects: resolve a project NAME → the `project_id` you pass on generation/upload/doc calls (`get_project` returns the full description — list clips it); `move_session` relocates a whole session + its media. See "Projects — Where Work Lands" below. |
155
154
  | `create_project` / `update_project` / `archive_project` / `unarchive_project` / `list_sessions` / `rename_session` / `delete_session` / `restore_session` | Project lifecycle + session inventory. Edit name/description with `update_project` (read via `get_project` first); rename sessions with `rename_session` — never delete+recreate. `list_sessions` returns `project_id` + `types[]` on every row. |
155
+ | `publish_project` / `get_project_publication` / `unpublish_project` / `submit_project_to_catalog` / `remove_project_hero_video` | Public showcase link for a project you own (the app's Publish tab). `publish_project` returns the public `url` and, in the same call, sets `allow_fork`, `showcase_about` and the FINAL VIDEO as page hero: first put the video in the project (`upload_media` with `project_id`, or generate with `project_id`), then pass its id as `hero_video_media_id`. Confirm with the user before publishing — anyone with the link sees the project's media. |
156
156
  | `bulk_move_sessions` / `list_session_generations` / `move_generations_to_session` / `split_session` / `undo_session_organization` | Reorganize many sessions or generations. `list_session_generations` is an inventory (not a live generation card). |
157
157
  | `add_project_context` / `list_project_context` / `delete_project_context` / `get_project_profile` / `regenerate_project_profile` | Project knowledge base (RAG): feed scripts/URLs/notes; `get_project_profile` = the living brief — read it to ground work in the project |
158
158
  | `list_project_assets` / `link_project_asset` / `unlink_project_asset` / `update_project_asset` | Project CAST roster: the Visual DNAs and moodboards tagged onto a project (`@Name` / `#Name`). `update_project_asset` writes each tagged DNA's identity description and/or its project-scoped purpose note. Never unlink+relink to edit. |
@@ -436,11 +436,11 @@ You are NOT allowed to:
436
436
 
437
437
  Existing video → modify → **single `generate_video_from_video` call** with source video URL + edit prompt.
438
438
 
439
- **Use a TRUE video-to-video model.** Image-to-video models reject with `WRONG_MODEL_TYPE`. Valid: `wan/2-7-videoedit`, `happyhorse/video-edit`, `kling-video/o3-video-to-video`, or any model whose DB `type` includes `video_to_video` (use `list_models({ type: "video_to_video" })`).
439
+ For MiniMax H3 Max Recast, use its exact live catalog identifier: one 5–30 second source, each continuous shot at most 15 seconds, and 1–4 ordered character photos. Prompt is optional (maximum 2000 characters); references replace main people left to right unless instructed otherwise. Choose the cheapest supported resolution explicitly, omit duration/aspect ratio, and leave `enhance_realism` false for a cheap test. Optional `seed` is a nonnegative integer. Original audio is retained; this is not native audio generation. Shared prompt enhancement is disabled for this model. Read current pricing from the catalog; USD-wallet execution and Unlimited are unsupported. Recast also supports character `visual_dna_ids` with empty instructions. Each accessible DNA supplies one existing canonical reference; manual photos come first, then DNAs in ID order. Total combined references must fit four. Do not attach the DNA thumbnail or its still again as a manual photo. Missing, inaccessible, non-character or overflowing identities fail before charging. Typed names map to numbered references; avoid ambiguous same-name mentions.
440
440
 
441
- **Motion-control / animate-move models invert the inputs**: `reference_images[0]` = the CHARACTER IMAGE to animate, `source_video` = the driving/reference video whose motion is transferred. Omitting the character image returns a `MOTION_CONTROL_INPUTS` error.
441
+ **Use a TRUE video-to-video model.** Image-to-video models reject with `WRONG_MODEL_TYPE`. Valid: `wan/2-7-videoedit`, `happyhorse/video-edit`, `kling-video/o3-video-to-video`, or any model whose DB `type` includes `video_to_video` (use `list_models({ type: "video_to_video" })`).
442
442
 
443
- **Do NOT** decompose into frames. **Do NOT** re-fire if the first call returned URLs.
443
+ **Motion-control / animate-move models invert the inputs**: `reference_images[0]` = the CHARACTER IMAGE to animate, `source_video` = the driving/reference video whose motion is transferred. Omitting the character image returns a `MOTION_CONTROL_INPUTS` error. **Do NOT** decompose into frames. **Do NOT** re-fire if the first call returned URLs.
444
444
 
445
445
  ## ⚠️ Character-Driven Video — Frames First, Then Animate (CRITICAL)
446
446
 
@@ -121,6 +121,10 @@ After the user approves the actual result, log its `credits_used`, resolution, d
121
121
 
122
122
  For a reference audit, inspect the persisted status result's `visual_dna` and reference image fields. The widget's `visual_dnas` is display metadata, not the original submitted request. Missing widget metadata means unknown, not that no reference was used. Server-side @mention resolution can attach references beyond the caller’s explicit fields. Keep submitted inputs, persisted references, and observed visual fidelity distinct; attaching a DNA does not prove identity fidelity.
123
123
 
124
- ## Native reference-video exceptions
124
+ ## Native reference-video exceptions
125
+
126
+ For Recast, character Visual DNAs use one access-checked canonical primary reference per ID, ordered after manual photos. The combined cap is four, not four photos plus four DNAs. Picker/SDK ID selection works with empty instructions; no Locked Intro, stylization token or extra generated collage is needed. Reuse must retain the IDs and omit server-injected DNA stills from loose references.
127
+
128
+ MiniMax H3 Max Recast is a separate character replacement route: one 5–30 second source with every shot at most 15 seconds, 1–4 ordered photos, optional instructions up to 2000 characters, and 768p/1080p. Preserve source duration and use source aspect selection; do not invent a target crop or promise exact geometry. Source audio is retained. Use `enhance_realism: false` unless requested, an optional nonnegative integer `seed`, and no shared prompt enhancement. This route is credit-only; read the live catalog before quoting. The application bounds source inspection to 128 MiB, while multipart uploads follow the existing upload limit.
125
129
 
126
130
  Use the exact native Motion Transfer or Object Swap model identifier returned by `list_models`; they are separate execution IDs behind one public model family. Where the `generate_elements` or `generate_video_from_video` tool description explicitly marks that identifier as prompt-optional, omit the prompt rather than inventing a Locked Intro. Other Elements models still require a prompt. Both native operations accept one 4–30 second source video and 1–8 combined reference images/Visual DNAs at 480p or 720p. Omit output duration and aspect ratio. Each DNA consumes at least one reference slot; the server allocates its stills within the combined budget. Billing uses the ceiling of source seconds once, with the selected resolution rate, not combined input plus output seconds. The 4–30 second window is the product launch limit, not a claim about the provider API.
@@ -29,6 +29,10 @@ Then create a **Visual DNA** from the sheet (`references/workflows/visual-dna.md
29
29
  - A carried prop (bag, phone, briefcase) gets its own sheet of the character holding it as in the scene. State the side per panel ("front panel: his left shoulder is on the IMAGE'S RIGHT; back panel: on the IMAGE'S LEFT"). Naming a prop only in the video prompt loses it. A prop first created on screen (a photo found in a pocket in Part 3) becomes a reference too: grab a clean frame of it and pass it to every later part, or the next part redraws it (the strip became a square photo).
30
30
  - Fix from the original: when an edited image needs another fix, redo it from the ORIGINAL in one combined edit. Every stacked edit drifts the face.
31
31
 
32
+ - **Split or torn props** (a torn photo, half a ticket): build the second piece from a clean frame grab of the episode where the first piece appeared, in one edit, and state "exactly the same width and paper as the first piece". Check the two pieces side by side before any video.
33
+ - **Quote image costs first.** Sheet-quality edits are not cheap (high-quality GPT Image and 4K Nano Banana edits run 30-45 credits each; a Midjourney batch of 4 is a few credits). Read the rates from `list_models` and give the batch total before running it.
34
+ - **Match the look from frames, not from the bible text.** Before writing a new location or look for a running series, pull a contact sheet of the latest approved episode and match what is actually on screen.
35
+
32
36
  ## Locations
33
37
 
34
38
  One location = one image: `nano-banana-pro`, 16:9, high resolution, **no people**. It is passed to video as the location AND grade reference.
@@ -47,7 +47,9 @@ Read `references/workflows/video-editor.md` and `get_video_editor_schema` first.
47
47
  | Platform episode | `python burn.py film <tight.mp4> <words-tight.srt> <ep.mp4>` (plain subtitles, max 4 words per row, 2 rows) or ship the clean master |
48
48
  | Social cutdown | `python burn.py social <short.mp4> <words-short.srt> <short-subbed.mp4> --hook "Line one|Line two"` |
49
49
 
50
- Burn at the delivery resolution: if the cut is upscaled, burn captions and hook after the upscale on the clean cut, never upscale burned text.
50
+ **Finishing order (never upscale an edited cut):** finalize or upscale every raw clip the cut uses first, then rebuild the same cut (same `cut.json`) from the full-resolution clips, then burn captions and hook at that resolution. Review cuts can be built from the Drafts; the delivery cut is always re-cut from finished clips.
51
+
52
+ **Hook placement:** the hook box fits its text tightly (no wide empty margins). Before burning, look at frames 0-3 s: if the box would cover the key visual (a face, the hero prop, the action that opens the scene), move it (above or below the action, or to the free side of the frame) and re-check.
51
53
 
52
54
  Re-transcribe the tight cut (and each short) before burning; word timings change after cutting. If the user wants to keep editing in the app, also build the Video Editor session.
53
55
 
@@ -29,7 +29,8 @@ The shot list then describes behaviour that follows from it (fixes a sleeve that
29
29
 
30
30
  - Regular drama coverage: eye-level mediums, medium close-ups, over-the-shoulders, two-shots staged in depth (never two profiles across the narrow frame), one wide for movement or geography. 35-50 mm feel.
31
31
  - Vertical framing: tightest default is MCU chest-up; one close-up per part for the key beat. Face fully in frame on any speaking or reacting shot.
32
- - Camera: "shoulder-mounted handheld operated by a real person, gentle organic sway, small natural reframes; never shaky, never locked off".
32
+ - Camera: for standalone social reels and glamour romance, "camera on a dolly or tripod: steady, slow push-ins only, no shake" (the format's best performers never go handheld). For gritty drama or thriller: "shoulder-mounted handheld operated by a real person, gentle organic sway, small natural reframes; never shaky, never locked off".
33
+ - A prop that appears in an insert (a timer, a phone, a photo) gets its own reference image or an exact one-way description repeated in every part; otherwise the next part redraws it as a different object.
33
34
  - Lens and grade: vertical cannot be anamorphic. "Vintage spherical cinema lenses, round creamy bokeh, gentle halation around practicals, shallow depth of field" + the bible's grade word for word in every prompt.
34
35
  - 2-3 shots per 13 s of slow action; dialogue-driven parts can carry more. Cuts are motivated by dialogue (speaker or listener reaction), never to show a new action.
35
36
  - Whoever makes a sound (breath, laugh, gasp) is on screen when it happens. Every close shot has one visible change (eyes, breath, a swallow).
@@ -9,6 +9,34 @@ Story craft comes from `references/filmmaking/scene-engine.md` (Goal → Obstacl
9
9
  - **Works with the sound off.** A viewer must follow the episode from the picture: glances, props, geography, reactions. Dialogue is sparse and sharp. Proven serials carry an episode on 4-10 short lines.
10
10
  - **Pace:** Episode 1 may breathe (longer takes for the meet or the setup). From Episode 2 the cut settles near 2 s per shot, with emotional beats on face close-ups and shot/reverse.
11
11
 
12
+ ## Standalone reels (Instagram / TikTok format)
13
+
14
+ When the kickoff picks **standalone reels** instead of serial episodes, each post is one self-contained scene of 25-35 s. On social, one-scene stories that stand alone outperform numbered serial parts, which lose viewers part after part (measured on a leading 90s-glamour micro-drama account, Oct 2026). A viewer who never saw the previous post must still get it.
15
+
16
+ | Time | Beat | Picture |
17
+ |---|---|---|
18
+ | 0-1 s | First line or action already mid-conflict, no setup shot | Two-shot: who and where |
19
+ | 1-8 s | 2-4 short lines; one character is cold | Close-ups, 1.5-2.5 s each |
20
+ | 8-18 s | SILENT stretch (room tone + foley only): a prop or physical action turns the scene | One wide + an insert + reaction close-ups |
21
+ | 18-25 s | The flip: 2-3 lines, the cold one loses the upper hand | Tighter shot/reverse |
22
+ | Last 3-5 s | Payoff: a smile or near-kiss, held long | Tight two-shot or one close-up |
23
+
24
+ - About 15-17 shots per 30 s, close-ups for most of the runtime, one wide per reel.
25
+ - Ends on a **payoff, not a cliffhanger**. A light callback (a prop, a gesture) links the reels.
26
+ - The silent stretch is where the user's own song goes in the edit; keep it free of dialogue.
27
+ - Title each reel with a short noun ("THE DOOR", "THE COAT"); number it only in the post caption.
28
+
29
+ ## Clarity rules (every format)
30
+
31
+ - **One scene says the backstory out loud.** Somewhere in the series a character plainly states who they are, why they left, and what the key prop means ("I found the ring in your coat. I ran from a wedding once, back in Ohio. So I ran again."). Mystery by implication alone reads as confusing, not intriguing.
32
+ - **Fill every gap the cut implies.** If a scene ends on "ten minutes", the audience wants those ten minutes. Write the scene that answers the obvious question before inventing a new location.
33
+ - **Every joke or charged line needs its setup** on screen or in the hook. "Gave it to a woman who was cold" only lands if we know whose coat she is wearing.
34
+ - **No hand-touch or finger "callback" gestures as the payoff** (palms raised to each other, fingertips meeting). They read as cringe. End on a face, a line, or a held look.
35
+ - **Emotion builds; it never starts at the peak.** In a confession or speech, the first line is composed or defensive, the middle line cracks (voice drops, eyes away), and only the last line reaches tears or glassy eyes. Write the build into the prompt line by line; generate the speech as one continuous take so the performance can build.
36
+ - **Cut to the listener during long lines.** A speech longer than about 2 s gets at least one reaction cutaway of the other person, with the speaker's voice continuing underneath (L-cut). Generate a silent listening shot of the other character for every key speech.
37
+ - **Pleasing hook and pleasing ending.** The winning reel built tension from frame 1 (faces, a charged situation) and ended on a warm physical payoff held long. Open on faces in tension, never on an object; end on a physical action that resolves the scene.
38
+ - Before approval, read the season as a stranger: for each reel, can you say in one sentence what is happening and why? If not, rewrite.
39
+
12
40
  ## Episode beat structure
13
41
 
14
42
  | Beat | Job |
@@ -21,7 +21,7 @@ Craft depth lives in the filmmaking packs; reuse them, do not restate them: `ref
21
21
  Ask these before any paid step, with the suggested default first. Store the answers at the top of the bible.
22
22
 
23
23
  1. **Idea + genre**: the user's premise, and one of the genre templates in `references/workflows/micro-drama/writing.md` (romance/revenge, thriller/mystery, brand series, comedy) or "other".
24
- 2. **Season shape**: number of episodes (suggest 6), episode length (suggest 45-90 s), aspect (suggest 9:16).
24
+ 2. **Format and season shape**: serial episodes (suggest 6 episodes of 45-90 s, cliffhanger endings) or standalone social reels (one scene of 25-35 s each, payoff endings; see `references/workflows/micro-drama/writing.md`), number of posts, aspect (suggest 9:16). Ask whether the user adds their own music in the edit; if so, every prompt stays music-free and each reel keeps a silent stretch for the track.
25
25
  3. **Language**: English is the reliable default. Seedance 2.5 performs English dialogue natively. For any other language, tell the user before anything is generated: Seedance 2.5 may not speak it (it does not speak Hebrew at all), and the route is either a model that speaks it natively (check `list_models`) or recorded/generated speech with a lip-sync model, both with their own cost and quality trade-offs. Let the user choose.
26
26
  4. **Look route**: photoreal drama, or a stylized glamour / nostalgic look (Midjourney stills as the look source). See `references/workflows/micro-drama/cast-locations-voices.md`.
27
27
  5. **Finals**: episodes are always blocked in Seedance 2.5 Draft. Read `draft_resolutions[].final_resolutions` for `seedance-2-5` in `list_models` (as of 2026-10 a Draft finalizes to 1080p only). If there is a choice, ask which tier; if not, tell the user the final tier and its cost per 30 s, and ask whether to finalize at all or deliver the Draft cut.
@@ -26,6 +26,8 @@ const READ_ONLY = [
26
26
  'list_media', 'list_media_folders', 'get_media', 'get_media_stats',
27
27
  'list_presets', 'list_cinematic_presets',
28
28
  'list_projects', 'get_project', 'get_project_publication', 'list_sessions', 'list_project_context', 'get_project_profile',
29
+ // Returns a draft without saving it.
30
+ 'get_project_story', 'draft_project_story', 'get_project_page_insights',
29
31
  'list_project_assets',
30
32
  'list_project_transfers', 'list_visual_dna_transfers',
31
33
  'list_session_generations',
@@ -140,6 +142,8 @@ const OPEN_WORLD_WRITE = [
140
142
 
141
143
  const OPEN_WORLD_DESTRUCTIVE = [
142
144
  'share_doc', 'revoke_review_share_link', 'unpublish_project',
145
+ // Replaces the whole story; live on the public page when the project is published.
146
+ 'update_project_story',
143
147
  'blender_apply_operations', 'blender_import_media', 'blender_render',
144
148
  'blender_undo', 'blender_file_operation', 'blender_execute_python',
145
149
  // Can delete layers; approved once per batch in the Kolbo panel.
@@ -320,28 +320,30 @@ function registerProjectTools(server, client) {
320
320
 
321
321
  server.tool(
322
322
  'publish_project',
323
- 'PUBLISH one of YOUR OWN projects as a public showcase page with a shareable link (the same as the app\'s Publish tab) and return the public `url`. Safe to call again: it never changes the URL, it only updates what you pass. In one call you can also set `allow_fork` (let viewers copy the project into their account), the public write-up `showcase_about`, and the project\'s FINAL VIDEO as the page hero. HERO VIDEO: the video must already be in THIS project — a generated video made with `project_id`, or a file you put there with `upload_media` (pass `project_id`; a local file is uploaded from disk). Then pass its media id as `hero_video_media_id` (or its Kolbo URL as `hero_video_url`). Anyone with the link can see the project\'s media, so ALWAYS confirm with the user before publishing, and consider `duplicate_project` first to publish a clean copy. Undo with `unpublish_project`.',
323
+ 'PUBLISH one of YOUR OWN projects as a public showcase page with a shareable link (the same as the app\'s Publish tab) and return the public `url`. Safe to call again: it never changes the URL, it only updates what you pass. In one call you can also set `allow_fork` (let viewers copy the project into their account), `story_only` (publish a blog-style post without sharing the project: see below), the public write-up `showcase_about`, and the project\'s FINAL VIDEO as the page hero. HERO VIDEO: the video must already be in THIS project — a generated video made with `project_id`, or a file you put there with `upload_media` (pass `project_id`; a local file is uploaded from disk). Then pass its media id as `hero_video_media_id` (or its Kolbo URL as `hero_video_url`). Anyone with the link can see the project\'s media, so ALWAYS confirm with the user before publishing, and consider `duplicate_project` first to publish a clean copy. STORY ONLY: with `story_only: true` the page shows only the story (get_project_story / update_project_story) and the media inside it; the media library, sessions, chats/docs/flows, cast and credits are not served, and copying is refused whatever `allow_fork` says. Use it when the user wants a post about the work without sharing the project. Undo with `unpublish_project`.',
324
324
  {
325
325
  project_id: z.string().describe('ObjectId of a project you OWN (from list_projects).'),
326
- allow_fork: z.boolean().optional().describe('Let viewers copy the project into their own account. Default true on first publish; omit to keep the current setting.'),
326
+ allow_fork: z.boolean().optional().describe('Let viewers copy the project into their own account. Default true on first publish; omit to keep the current setting. Has no effect while story_only is true.'),
327
+ story_only: z.boolean().optional().describe('true = the page shows only its story and the media in it (no library, sessions, chats, cast, credits, no copying). Default false on first publish; omit to keep the current setting.'),
327
328
  showcase_about: z.string().optional().describe('Public "About" write-up shown on the page (max 10000 chars, plain text). Omit on first publish and Kolbo drafts one for you; pass "" to clear.'),
328
329
  hero_video_media_id: z.string().optional().describe('Media id (from upload_media / list_media) of a VIDEO already in this project, shown as the full-bleed hero of the public page.'),
329
330
  hero_video_url: z.string().optional().describe('Alternative to hero_video_media_id: the Kolbo CDN URL of a video already in this project.')
330
331
  },
331
- async ({ project_id, allow_fork, showcase_about, hero_video_media_id, hero_video_url }) => {
332
+ async ({ project_id, allow_fork, story_only, showcase_about, hero_video_media_id, hero_video_url }) => {
332
333
  const body = {};
333
334
  if (allow_fork !== undefined) body.allow_fork = allow_fork;
335
+ if (story_only !== undefined) body.story_only = story_only;
334
336
  if (showcase_about !== undefined) body.showcase_about = showcase_about;
335
337
  if (hero_video_media_id) body.hero_video_media_id = hero_video_media_id;
336
338
  if (hero_video_url) body.hero_video_url = hero_video_url;
337
339
  const result = await client.post(`/v1/projects/${encodeURIComponent(project_id)}/publish`, body);
338
- return pubText(result, 'Share `url` with the user — it is live now. To list it in the public catalog call `submit_project_to_catalog` (review is manual).');
340
+ return pubText(result, 'Share `url` with the user — it is live now. Kolbo drafts the page\'s story on first publish; read or rewrite it with get_project_story / update_project_story. To list it in the public catalog (and Google) call `submit_project_to_catalog` (review is manual).');
339
341
  }
340
342
  );
341
343
 
342
344
  server.tool(
343
345
  'get_project_publication',
344
- 'Check whether a project is published and read its public `url`, allow_fork, showcase_about, hero video, catalog listing status (none / pending / approved / rejected) and fork count. Returns `{ published: false }` when it is not published.',
346
+ 'Check whether a project is published and read its public `url`, allow_fork, story_only, showcase_about, hero video, catalog listing status (none / pending / approved / rejected) and fork count. Returns `{ published: false }` when it is not published.',
345
347
  { project_id: z.string().describe('ObjectId of a project you OWN.') },
346
348
  async ({ project_id }) => pubText(await client.get(`/v1/projects/${encodeURIComponent(project_id)}/publish`))
347
349
  );
@@ -355,7 +357,7 @@ function registerProjectTools(server, client) {
355
357
 
356
358
  server.tool(
357
359
  'submit_project_to_catalog',
358
- 'Submit a PUBLISHED project to Kolbo\'s public catalog (status becomes `pending`; a Kolbo admin approves it — you cannot approve your own). Pass `submit: false` to withdraw. Fails with NOT_PUBLISHED if `publish_project` has not been called.',
360
+ 'Submit a PUBLISHED project to Kolbo\'s public catalog (status becomes `pending`; a Kolbo admin approves it — you cannot approve your own). Only approved projects are listed and may appear in Google search; until then the page is reachable by link but not indexed. Pass `submit: false` to withdraw. Fails with NOT_PUBLISHED if `publish_project` has not been called.',
359
361
  {
360
362
  project_id: z.string().describe('ObjectId of a project you OWN.'),
361
363
  submit: z.boolean().optional().describe('true (default) = submit for review, false = withdraw.')
@@ -363,6 +365,62 @@ function registerProjectTools(server, client) {
363
365
  async ({ project_id, submit }) => pubText(await client.post(`/v1/projects/${encodeURIComponent(project_id)}/publish/listing`, { submit: submit !== false }))
364
366
  );
365
367
 
368
+ // ─── project story (the public page as a post) ─────────────
369
+ const storyBlock = z.object({
370
+ type: z.enum(['heading', 'text', 'media', 'step', 'before_after']).describe('heading = section title, text = paragraph, media = an image/video with a caption, step = a generated result shown WITH its real prompt and model (how it was made), before_after = two items side by side.'),
371
+ text: z.string().optional().describe('heading (max 160 chars) / text (max 5000 chars). Plain text; blank lines separate paragraphs.'),
372
+ media_id: z.string().optional().describe('media / step: a media_id from get_project_story `available_media`. A step must be a GENERATED result.'),
373
+ media_url: z.string().optional().describe('Alternative to media_id: the Kolbo URL of media already in this project.'),
374
+ before_media_id: z.string().optional().describe('before_after: the "before" item (often an upload, e.g. a sketch or source photo).'),
375
+ after_media_id: z.string().optional().describe('before_after: the "after" item (the result).'),
376
+ caption: z.string().optional().describe('media / before_after: one line on what it shows (max 300 chars).'),
377
+ note: z.string().optional().describe('step: one or two sentences on what this step did and why (max 1000 chars).')
378
+ });
379
+
380
+ server.tool(
381
+ 'get_project_story',
382
+ 'Read the STORY of a project you own — its public page written as an ordered post (headings, text, media, workflow steps with their real prompts, before/after pairs) — plus `available_media`: every item the story may use (this project\'s generated results and the owner\'s own uploads), with media_id, type, prompt and model. Call this FIRST before writing or editing a story: you need the `version` (pass it back as `base_version`) and the media ids. Also returns whether the project is published and its listing status.',
383
+ { project_id: z.string().describe('ObjectId of a project you OWN.') },
384
+ async ({ project_id }) => pubText(await client.get(`/v1/projects/${encodeURIComponent(project_id)}/publish/story`))
385
+ );
386
+
387
+ server.tool(
388
+ 'update_project_story',
389
+ 'REPLACE the story of a project you own (the public page\'s post) with the blocks you pass, in order. Read `get_project_story` first and pass its `version` as `base_version`: if the owner (or another agent) saved in between you get STORY_VERSION_CONFLICT — read again and re-apply. Blocks reference media by media_id from `available_media`; to use a NEW image or video, put it in the project first (`upload_media` with `project_id`, or generate with `project_id`). A `step` shows the generation\'s real prompt and model automatically — never retype them. Invalid blocks come back as `errors` with the block `index`; nothing is saved until all are valid. `warnings` are advice (missing captions, no steps). Good structure: hero media → short intro → "How it was made" heading → 3-8 steps in order → before/after where an upload became a result → one closing line. If the project is published the change is live at once, so confirm with the user before replacing a story they wrote.',
390
+ {
391
+ project_id: z.string().describe('ObjectId of a project you OWN.'),
392
+ blocks: z.array(storyBlock).describe('The whole story, in order (max 60 blocks). An empty array clears it.'),
393
+ base_version: z.number().optional().describe('The `version` from get_project_story. Strongly recommended — without it you overwrite whatever is there.')
394
+ },
395
+ async ({ project_id, blocks, base_version }) => pubText(await client.put(
396
+ `/v1/projects/${encodeURIComponent(project_id)}/publish/story`,
397
+ { blocks, ...(base_version !== undefined ? { base_version } : {}) }
398
+ ), 'Saved. The project page shows it now if the project is published (get_project_publication for the url).')
399
+ );
400
+
401
+ server.tool(
402
+ 'draft_project_story',
403
+ 'Ask Kolbo to DRAFT a story for a project you own, by analysing the project\'s own media (prompts, models, sessions, which generation used which upload). Takes up to about a minute. Returns the draft `blocks` WITHOUT saving them — review or edit them, then save with `update_project_story`. Fails with STORY_DRAFT_UNAVAILABLE when the project has no generated media yet.',
404
+ { project_id: z.string().describe('ObjectId of a project you OWN.') },
405
+ async ({ project_id }) => pubText(
406
+ await client.post(`/v1/projects/${encodeURIComponent(project_id)}/publish/story/draft`, {}),
407
+ 'Not saved yet. Edit if needed, then call update_project_story with these blocks and the version from get_project_story.'
408
+ )
409
+ );
410
+
411
+ server.tool(
412
+ 'get_project_page_insights',
413
+ 'Visitor analytics for the PUBLIC PAGE of a project you own (its "Page insights"): visitors, visits, average ACTIVE time (tab visible and in use), how many reached the story, scroll depth, clicks (media opened, recipes opened, project video played, "Use this project"), copies made, SIGN-UPS (new Kolbo accounts whose first visit was this page) with conversion rates, where visitors came from, countries, devices, a per-day series and the most-opened media. Anonymous first-party counts: no personal data, bots and the owner\'s own visits excluded, visitors deduped per day. Use it to tell the owner what works on their page (e.g. which step gets opened most) before editing the story.',
414
+ {
415
+ project_id: z.string().describe('ObjectId of a project you OWN.'),
416
+ days: z.union([z.literal(7), z.literal(30), z.literal(90)]).optional().describe('Window in days: 7, 30 (default) or 90.')
417
+ },
418
+ async ({ project_id, days }) => {
419
+ const qs = days ? `days=${days}` : '';
420
+ return pubText(await client.get(`/v1/projects/${encodeURIComponent(project_id)}/publish/insights${qs ? '?' + qs : ''}`));
421
+ }
422
+ );
423
+
366
424
  server.tool(
367
425
  'remove_project_hero_video',
368
426
  'Clear the hero video of a published project (the page falls back to a mosaic of the project\'s stills). The video file itself stays in the project\'s media.',