{"server":"ai.morphareels/morpha","count":48,"changes":[{"tool":"restore_project","kind":"added","observed_at":"2026-10-13T15:44:53.058Z","fields":[],"description_before":null,"description_after":"Bring a project back from Recently deleted, with its versions, files, workspace and sharing as they were. Returns { projectId, name }. Scheduled social posts cancelled by the delete stay cancelled. Requires edit access, as delete_project does; take the id from list_trash.","description_similarity":null,"before_hash":null,"after_hash":"2054a4680ce413817254960c8d3cb8f4"},{"tool":"list_trash","kind":"added","observed_at":"2026-10-13T15:44:53.058Z","fields":[],"description_before":null,"description_after":"List the projects in Recently deleted as { id, name, trashedAt, purgeAt, deletedBy, canEdit } entries. Each can be restored with restore_project until purgeAt (epoch ms), 30 days after it was deleted. `deletedBy` is { kind: \"you\" | \"person\" | \"agent\", label }: the caller, a teammate (label is their email), or a connected assistant (label is its name). `canEdit` says whether the caller may restore it. By default lists the caller's own personal projects; pass `workspaceId` (from list_workspaces) for that workspace's.","description_similarity":null,"before_hash":null,"after_hash":"4585da2a6693de9dedc2fa8f0dea2518"},{"tool":"delete_project","kind":"changed","observed_at":"2026-10-13T15:44:53.058Z","fields":["description"],"description_before":"Ask to permanently delete a project. Returns { name, confirmUrl } and deletes nothing: the project, its saved versions, uploaded images and clips are deleted only when the signed-in account that called this opens confirmUrl and confirms there. The link lasts 30 minutes. An anonymous account, which nobody can sign into, is refused with its claim link instead. Requires edit access — the owner, a workspace editor/admin/owner, or a direct-share editor; anyone else gets the opaque not-found.","description_after":"Move a project to Recently deleted. It leaves every project list at once and can be restored for 30 days, with restore_project or from the person's projects page; after that it is deleted for good, with its saved versions, uploaded images and clips. Returns { name, purgeAt, restorable: true }, where purgeAt is when it will be deleted for good (epoch ms). Deleting for good before then is the person's own step on their projects page; no tool does it. Requires edit access — the owner, a workspace editor/admin/owner, or a direct-share editor; anyone else gets the opaque not-found. Deleting the last remaining project is allowed.","description_similarity":0.346,"before_hash":"0d1a394c62e4111cfb8efc85054b8766","after_hash":"e1544e3cbe61acade48271ba2f544da1"},{"tool":"create_upload_link","kind":"removed","observed_at":"2026-10-12T04:33:01.579Z","fields":[],"description_before":"Put a file from the caller's own disk into a project: an image (.png/.jpg/.jpeg/.gif/.webp/.svg), an audio track (.mp3/.m4a/.wav/.ogg/.aac) or a font (.woff2/.woff/.ttf/.otf). Returns { uploadUrl, filename, name, command, expiresAt, maxBytes }. Run `command` from a shell (a curl line that PUTs the file's raw bytes to uploadUrl), then pass the returned `filename` (the file's id, never shown to a person) to add_image_layer, add_audio_overlay or set_custom_font. The link takes one file, stored under the id it was minted with, for 15 minutes; a second upload in that window replaces the first. The upload is checked as an editor drag-drop is: size cap (16 MB for an image or font, 50 MB for audio), SVG safety, storage quota. A client with no shell passes a public URL to upload_image or upload_audio instead. A video clip is not taken here, because its preview proxy is built on the caller's machine: use the morphareels-sdk's client.addVideo. Reference: https://morphareels.ai/docs/tools#createuploadlinkname","description_after":null,"description_similarity":null,"before_hash":"7936baff8e1e227f100778622f4675e8","after_hash":null},{"tool":"set_video_clip","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Repoint an existing video layer at a different uploaded clip — keeps the layer's id, position, size, animations, styles, and trim window; only the source mp4 changes. The clip must already be uploaded (the editor's Add video, or the morphareels-sdk's client.addVideo); pass the `filename` the upload returned, and its `name` as the label. Use this to swap a video layer's source WITHOUT losing its keyframes.","description_after":"Repoint an existing video layer at a different uploaded clip — keeps the layer's id, position, size, animations, styles, and trim window; only the source mp4 changes. The clip must already be uploaded (the editor's Add video); pass the `filename` the upload returned, and its `name` as the label. Use this to swap a video layer's source WITHOUT losing its keyframes.","description_similarity":0.9,"before_hash":"746cd63897abbd153b626db70b3acf66","after_hash":"30f73f2d2839d5673835b6d7f3a0f28b"},{"tool":"set_image_filename","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Repoint an existing image layer at a different uploaded asset — keeps the layer's id, position, size, animations, and styles; only the bitmap changes. The asset must already exist at users/<userId>/assets/<projectId>/<filename> (the editor's drag-drop, upload_image, create_upload_link, or POST /api/upload-asset/<projectId> with the raw bytes and an X-Upload-Name header); pass the `filename` the upload returned, and its `name` as the label. Use this to swap a layer's image WITHOUT losing its keyframes — `remove_layer` + `add_image_layer` would mint a new id and drop the animations.","description_after":"Repoint an existing image layer at a different uploaded asset — keeps the layer's id, position, size, animations, and styles; only the bitmap changes. The image must already be uploaded (upload_image for a web URL or a file attached in the chat; the editor for a file from the person's disk); pass the `filename` the upload returned, and its `name` as the label. Use this to swap a layer's image WITHOUT losing its keyframes — `remove_layer` + `add_image_layer` would mint a new id and drop the animations.","description_similarity":0.644,"before_hash":"f741e07ddd7986933ab0fbed44d66bc4","after_hash":"90051b275eb082815806e9c0c1a246db"},{"tool":"rename_layer","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["annotations"],"description_before":null,"description_after":null,"description_similarity":null,"before_hash":"61e1fb7c4637bc3d740fb57159613df0","after_hash":"9a698779c399045e6f5e091243ea9f2b"},{"tool":"set_custom_font","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Register a typeface Morpha does NOT ship, so text layers can use it by family name via font_family (exactly like a built-in family). Families already in the built-in catalogs (anything list_fonts returns from google/bunny/fontshare/fontsource/velvetyne) are REJECTED — they need no registration; just set font_family to them directly. `src` is EITHER a full URL (https://…) OR a font file already uploaded to the project's asset bucket (create_upload_link, or POST /api/upload-asset/<projectId> with raw bytes + an X-Upload-Name header; .woff2/.woff/.ttf/.otf): pass the filename the upload returned. Like add_image_layer, this does NOT verify an uploaded filename exists. Dedupes by family+weight+style, replacing a matching face. After registering, set a text layer's font_family to this family (add_text_layer / set_layer_text). NOTE: a pasted URL only loads if that host sends permissive CORS headers — uploading the font (served same-origin) is the robust path. A URL is not fetched when this tool runs: Morpha's renderer and every browser that opens the project download it later. Reference: https://morphareels.ai/docs/tools#setcustomfontfamily-src-weight-style","description_after":"Register a typeface Morpha does NOT ship, so text layers can use it by family name via font_family (exactly like a built-in family). Families already in the built-in catalogs (anything list_fonts returns from google/bunny/fontshare/fontsource/velvetyne) are REJECTED — they need no registration; just set font_family to them directly. `src` is EITHER a full URL (https://…) OR a font file (.woff2/.woff/.ttf/.otf) already uploaded to the project in the editor: pass the filename the upload returned. Like add_image_layer, this does NOT verify an uploaded filename exists. Dedupes by family+weight+style, replacing a matching face. After registering, set a text layer's font_family to this family (add_text_layer / set_layer_text). NOTE: a pasted URL only loads if that host sends permissive CORS headers — uploading the font (served same-origin) is the robust path. A URL is not fetched when this tool runs: Morpha's renderer and every browser that opens the project download it later. Reference: https://morphareels.ai/docs/tools#setcustomfontfamily-src-weight-style","description_similarity":0.898,"before_hash":"d61152406a89a33b705f40eac08dd92a","after_hash":"217d6e15cd27ce972150faee746de2ce"},{"tool":"add_audio_overlay","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Add an audio overlay (mp3/m4a/wav/ogg) scheduled at a frame-aligned start. The asset must already be uploaded (upload_audio, create_upload_link, or the editor); pass the upload's returned `filename`, and its `name` as the track's label. 30 fps; convert seconds with frames = round(s * 30). Plays in the editor preview and is mixed into the MP4 export.","description_after":"Add an audio overlay (mp3/m4a/wav/ogg) scheduled at a frame-aligned start. The asset must already be uploaded (upload_audio, or the editor for a file from the person's disk); pass the upload's returned `filename`, and its `name` as the track's label. 30 fps; convert seconds with frames = round(s * 30). Plays in the editor preview and is mixed into the MP4 export.","description_similarity":0.873,"before_hash":"8ceed7693ed2788c80ea8b822d63fb14","after_hash":"1f743cd10f8d52ffdf98909566e5082e"},{"tool":"rename_group","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["annotations"],"description_before":null,"description_after":null,"description_similarity":null,"before_hash":"86b430689c313d185dd069c0e0a75a85","after_hash":"dfef53708a5a563860acd98b765de233"},{"tool":"group_layers","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Wrap sibling elements in a new group. USE THIS whenever several layers are one thing — a butterfly assembled from wings + body + antennae, an icon built from primitives, a card + its title + badge, a lower-third — and name the group what the thing is; a multi-shape object left as loose siblings is a defect the user has to clean up, and the group's name is their only handle on it. The group composes its x/y/scale/rotation/opacity onto its descendants (so one track flies/spins/fades the whole thing) and pivots rotate/scale at its (pivotX, pivotY), seeded to the centroid of its children at create time. The group's x/y track values are translation offsets applied around the pivot — groups have no static body of their own. All listed elementIds must currently share the same parent (root, or one existing group).","description_after":"Wrap sibling elements in a new group, which the Layers panel shows as one named row the person can select, move and animate as one thing (a butterfly assembled from wings + body + antennae, an icon built from primitives, a card + its title + badge, a lower-third). `name` is that row's label. The group composes its x/y/scale/rotation/opacity onto its descendants (so one track flies/spins/fades the whole thing) and pivots rotate/scale at its (pivotX, pivotY), seeded to the centroid of its children at create time. The group's x/y track values are translation offsets applied around the pivot — groups have no static body of their own. All listed elementIds must currently share the same parent (root, or one existing group).","description_similarity":0.719,"before_hash":"7c457be089a19ad14a4026ff47f302bc","after_hash":"d71f39d7a03aaf1810fb4c7d3b4fb979"},{"tool":"add_video_layer","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Add a video layer. The clip must already be uploaded (the morphareels-sdk's client.addVideo, or the editor). An upload returns { filename, name }: pass `filename` (the stored clip's id) as `clip`, and `name` as the layer's label. To duplicate an existing layer, reuse its clip filename — the editor auto-assigns a fresh id.","description_after":"Add a video layer. The clip must already be uploaded; clips are uploaded in the MorphaReels editor. An upload returns { filename, name }: pass `filename` (the stored clip's id) as `clip`, and `name` as the layer's label. To duplicate an existing layer, reuse its clip filename — the editor auto-assigns a fresh id.","description_similarity":0.821,"before_hash":"15b92b923f62338f1e118ce49ff740ee","after_hash":"967fd13dbe8e6d7fa7ed01a1b99dfb81"},{"tool":"add_image_layer","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Add an image layer. The asset must already be uploaded (the editor's drag-drop, upload_image, create_upload_link, or POST /api/upload-asset/<projectId> with the raw bytes and an X-Upload-Name header). Every upload returns { filename, name, width, height }: pass `filename` (the stored file's id) here, and `name` as the layer's label. SIZE THE BOX TO THE PICTURE: pass `width` alone (or `height` alone) and the other side is filled from the image's own aspect, so the picture is never stretched; a box with both sides that does not match the image's aspect stretches the picture to fill it. To duplicate an existing layer, reuse its filename — the editor auto-assigns a fresh id.","description_after":"Add an image layer. The image must already be uploaded: upload_image takes a web URL or a file attached in the chat, and a file from the person's disk is uploaded in the editor. Every upload returns { filename, name, width, height }: pass `filename` (the stored file's id) here, and `name` as the layer's label. SIZE THE BOX TO THE PICTURE: pass `width` alone (or `height` alone) and the other side is filled from the image's own aspect, so the picture is never stretched; a box with both sides that does not match the image's aspect stretches the picture to fill it. To duplicate an existing layer, reuse its filename — the editor auto-assigns a fresh id.","description_similarity":0.75,"before_hash":"149ba5d0df6c99fa2796d9f7212017f0","after_hash":"b1a057b393a20fb77e294933901438fa"},{"tool":"render_video","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Render the project to an MP4 on Morpha's servers, for an agent that has no browser to drive (a chat app, a Linux box). Returns { renderId, status: 'rendering', next } — call render_status with that renderId after about 30 seconds. It needs a subscription and draws on the account's render allowance, so a longer composition and a larger scale each use more of it. Two other routes need no allowance and are usually better — give the person the editorUrl every project result carries and they choose Share, then Download (Render MP4 on a phone) in the editor, free and with no watermark; or a script runs the morphareels-sdk npm client's client.renderVideo(projectId) on macOS or Windows. Renders at 2x the canvas by default, the same as the editor and the npm client; pass scale: 1 for the canvas's own size, which is faster and uses less allowance. The allowance is checked before the render starts, so a render that would exceed it is refused up front rather than failing partway. Refused for a composition longer than 30 minutes, when the remaining allowance will not cover it, or while 3 renders are already running for this account. The finished file stays downloadable for 7 days, then it is deleted; the project stays and can be rendered again. The render downloads the project's fonts from their hosts, including a custom font's own URL (set_custom_font). Reference: https://morphareels.ai/docs/tools#rendervideopage-scale","description_after":"Render the project to an MP4 on Morpha's servers, for an agent that has no browser to drive (a chat app, a Linux box). Returns { renderId, status: 'rendering', next } — call render_status with that renderId after about 30 seconds. It needs a subscription and draws on the account's render allowance, so a longer composition and a larger scale each use more of it. The editor renders the same MP4 with no allowance: every project result carries an editorUrl, and there the person can choose Share, then Download (Render MP4 on a phone), free and with no watermark. Renders at 2x the canvas by default, the same as the editor; pass scale: 1 for the canvas's own size, which is faster and uses less allowance. The allowance is checked before the render starts, so a render that would exceed it is refused up front rather than failing partway. Refused for a composition longer than 30 minutes, when the remaining allowance will not cover it, or while 3 renders are already running for this account. The finished file stays downloadable for 7 days, then it is deleted; the project stays and can be rendered again. The render downloads the project's fonts from their hosts, including a custom font's own URL (set_custom_font). Reference: https://morphareels.ai/docs/tools#rendervideopage-scale","description_similarity":0.874,"before_hash":"fb5d235570b9d69dd4e20572d5e4bd69","after_hash":"5a8998a4e6bb5dda54e3f6bc35efea24"},{"tool":"clip_processing_status","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Report whether a clip — or every video clip in the project, if `clip` is omitted — has been through the browser-side video-processing pipeline: proxy build, audio split, transcription, OCR (text regions). Processing never runs on the server. The preview proxy is mandatory: every processing run builds it (client.addVideo / client.processClip / client.processProject in the morphareels-sdk npm client, which drive local Chrome), and the Morpha editor builds any missing proxy for every page's clip when the project is opened. The other steps run through the same npm client or in the editor. Returns { ok: true, data: { clips: [{ clip, processed, steps: { proxy, audio_split, transcript, text_regions } }], allProcessed } }, where each step is 'ready' | 'pending' | 'running' | 'unavailable' | 'cancelled' | 'error'. 'cancelled' is a transcript the user stopped in the editor: it stays stopped across opens, and only the editor's Re-run starts it again. audio_split stays 'pending' until the user splits the clip's audio into its own track in the editor (Inspector → Audio → Split audio) or the SDK's audio_split step runs; it never gates `processed`, and an unsplit clip plays its own sound. Use it to know whether transcribe_clip / detect_text_regions will return data, and to surface an 'unprocessed' state to the user.","description_after":"Report whether a clip — or every video clip in the project, if `clip` is omitted — has been through the browser-side video-processing pipeline: proxy build, audio split, transcription, OCR (text regions). Processing never runs on the server. The preview proxy is mandatory: every processing run builds it, and the Morpha editor builds any missing proxy for every page's clip when the project is opened. The other steps run in the editor too. Returns { ok: true, data: { clips: [{ clip, processed, steps: { proxy, audio_split, transcript, text_regions } }], allProcessed } }, where each step is 'ready' | 'pending' | 'running' | 'unavailable' | 'cancelled' | 'error'. 'cancelled' is a transcript the user stopped in the editor: it stays stopped across opens, and only the editor's Re-run starts it again. audio_split stays 'pending' until the user splits the clip's audio into its own track in the editor (Inspector → Audio → Split audio); it never gates `processed`, and an unsplit clip plays its own sound. Use it to know whether transcribe_clip / detect_text_regions will return data, and to surface an 'unprocessed' state to the user.","description_similarity":0.882,"before_hash":"314a99e5267f8a3721569715125f9495","after_hash":"a79441acd9fa0f089b4bfade130005b7"},{"tool":"upload_audio","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Upload an audio track into a project from a public http(s) URL or a file attached in the chat, fetched server-side — the ONLY way to get an audio file's bytes into a project over MCP/HTTP (add_audio_overlay / update_audio_overlay only reference a file that is already uploaded). Pass `url` for a public http(s) link, or `file` for a file the person attached in the chat (a host that supports file inputs, such as ChatGPT, fills `file` with a temporary download link itself; do not ask the person for a URL to something they have attached). The target is the URL you pass and nothing else: Morpha's Worker fetches it with a plain HTTP GET, following redirects, and identifies itself as \"Morpha/1.0 (+https://morphareels.ai; hello@morphareels.ai)\". No third-party API is involved. The worker downloads the .mp3/.m4a/.wav/.ogg/.aac, stores it in the project's asset bucket under a new id, and returns { filename, name, sizeBytes, contentType }. Then pass the returned `filename` (the file's id, never shown to a person) to add_audio_overlay (add a second track) or update_audio_overlay { id, filename } (replace an existing track's file — find the id via describe_video's audio_overlays), with `name` as the track's name. A url must be a direct, publicly-fetchable http(s) link (not an auth-walled page). Buffered in Worker memory, so capped at 50 MB. For a file on the caller's own disk, use create_upload_link instead. Reference: https://morphareels.ai/docs/tools#uploadaudiourl-file-name","description_after":"Upload an audio track into a project from a public http(s) URL or a file attached in the chat, fetched server-side — the ONLY way to get an audio file's bytes into a project over MCP/HTTP (add_audio_overlay / update_audio_overlay only reference a file that is already uploaded). Pass `url` for a public http(s) link, or `file` for a file the person attached in the chat (a host that supports file inputs, such as ChatGPT, fills `file` with a temporary download link itself; do not ask the person for a URL to something they have attached). The target is the URL you pass and nothing else: Morpha's Worker fetches it with a plain HTTP GET, following redirects, and identifies itself as \"Morpha/1.0 (+https://morphareels.ai; hello@morphareels.ai)\". No third-party API is involved. The worker downloads the .mp3/.m4a/.wav/.ogg/.aac, stores it in the project's asset bucket under a new id, and returns { filename, name, sizeBytes, contentType }. Then pass the returned `filename` (the file's id, never shown to a person) to add_audio_overlay (add a second track) or update_audio_overlay { id, filename } (replace an existing track's file — find the id via describe_video's audio_overlays), with `name` as the track's name. A url must be a direct, publicly-fetchable http(s) link (not an auth-walled page). Buffered in Worker memory, so capped at 50 MB. A file on the person's own disk is uploaded in the editor. Reference: https://morphareels.ai/docs/tools#uploadaudiourl-file-name","description_similarity":0.963,"before_hash":"e20545f3e8e77022bf13e0eadd32d754","after_hash":"13205c18363aaa2fc108a9d7b00c30e7"},{"tool":"upload_image","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Upload an image into a project from a public http(s) URL or a file attached in the chat, fetched server-side. Pass `url` for a public http(s) link, or `file` for a file the person attached in the chat (a host that supports file inputs, such as ChatGPT, fills `file` with a temporary download link itself; do not ask the person for a URL to something they have attached). The target is the URL you pass and nothing else: Morpha's Worker fetches it with a plain HTTP GET, following redirects, and identifies itself as \"Morpha/1.0 (+https://morphareels.ai; hello@morphareels.ai)\". No third-party API is involved. The worker downloads the .png/.jpg/.jpeg/.gif/.webp/.svg, stores it in the project's asset bucket under a new id, and returns { filename, name, sizeBytes, contentType, width, height }: pass the returned `filename` (the file's id, never shown to a person) to add_image_layer, and `name` is what people see; `width` and `height` are the picture's own pixel size (null when its header did not say), so size the layer from them or pass add_image_layer one side and let it keep the aspect. This tool does not search for images: pass a direct image file URL, such as one the person has given you, and respect the licence its source states. A url must be a direct, publicly-fetchable http(s) link (not an auth-walled page). SVGs are rejected if they carry a <script>, an inline event handler, a <foreignObject>, a javascript: URL, or an external resource reference — supply a static, self-contained SVG. Buffered in Worker memory, so capped at 16 MB. For a file on the caller's own disk, use create_upload_link instead. Reference: https://morphareels.ai/docs/tools#uploadimageurl-file-name","description_after":"Upload an image into a project from a public http(s) URL or a file attached in the chat, fetched server-side. Pass `url` for a public http(s) link, or `file` for a file the person attached in the chat (a host that supports file inputs, such as ChatGPT, fills `file` with a temporary download link itself; do not ask the person for a URL to something they have attached). The target is the URL you pass and nothing else: Morpha's Worker fetches it with a plain HTTP GET, following redirects, and identifies itself as \"Morpha/1.0 (+https://morphareels.ai; hello@morphareels.ai)\". No third-party API is involved. The worker downloads the .png/.jpg/.jpeg/.gif/.webp/.svg, stores it in the project's asset bucket under a new id, and returns { filename, name, sizeBytes, contentType, width, height }: pass the returned `filename` (the file's id, never shown to a person) to add_image_layer, and `name` is what people see; `width` and `height` are the picture's own pixel size (null when its header did not say), so size the layer from them or pass add_image_layer one side and let it keep the aspect. This tool does not search for images: pass a direct image file URL, such as one the person has given you, and respect the licence its source states. A url must be a direct, publicly-fetchable http(s) link (not an auth-walled page). SVGs are rejected if they carry a <script>, an inline event handler, a <foreignObject>, a javascript: URL, or an external resource reference — supply a static, self-contained SVG. Buffered in Worker memory, so capped at 16 MB. A file on the person's own disk is uploaded in the editor. Reference: https://morphareels.ai/docs/tools#uploadimageurl-file-name","description_similarity":0.964,"before_hash":"1f28e8ee189278e3b2ca477147e7d24c","after_hash":"51eb711a8851657760b885a089d276f0"},{"tool":"rename_version","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["annotations"],"description_before":null,"description_after":null,"description_similarity":null,"before_hash":"ca603797d67300f907d2a61bd6066b47","after_hash":"82f8a7eea091773a0dbb0279bf9fba41"},{"tool":"save_version","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Save a version of the current project state. ALWAYS call this once after each meaningful set of changes you make on the user's behalf — the user flicks between versions to compare or roll back, so an unsaved edit is one they can't easily revisit. Use a short descriptive `name` (e.g. \"add slide-in animation\"); the user sees this verbatim in the picker. One version per logical change-set, not one per individual tool call.","description_after":"Save a version of the current project state. The person flicks between versions in the editor to compare or roll back, so one version per logical change-set (not one per tool call) is what they can revisit. `name` is shown verbatim in the version picker, so a short description reads best (e.g. \"add slide-in animation\").","description_similarity":0.574,"before_hash":"cdac3edee3ad74e53bd481ba2b153741","after_hash":"bc9b28d928773ea01eb6b7cfca667f9f"},{"tool":"delete_project","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Permanently delete a project: its JSON, shortlist, versions, uploaded assets, and uploaded clips. Requires edit access — the owner, a workspace editor/admin/owner, or a direct-share editor; anyone else gets the opaque not-found. Deleting the last remaining project is allowed. The user's editor session won't auto-refresh — if they were viewing the deleted project they'll need to reload.","description_after":"Ask to permanently delete a project. Returns { name, confirmUrl } and deletes nothing: the project, its saved versions, uploaded images and clips are deleted only when the signed-in account that called this opens confirmUrl and confirms there. The link lasts 30 minutes. An anonymous account, which nobody can sign into, is refused with its claim link instead. Requires edit access — the owner, a workspace editor/admin/owner, or a direct-share editor; anyone else gets the opaque not-found.","description_similarity":0.341,"before_hash":"e9a14b888c77ab9fccb63311a0a26800","after_hash":"0d1a394c62e4111cfb8efc85054b8766"},{"tool":"rename_project","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["annotations"],"description_before":null,"description_after":null,"description_similarity":null,"before_hash":"208fbcbb6f576ee98479f06c26af3a4c","after_hash":"e93058413118b5cac917b10c12c08953"},{"tool":"duplicate_project","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Duplicate an existing project into a brand-new one — a full copy of its layers, animations, styles, uploaded assets, and clips. Pass the SOURCE project's `projectId`; the copy gets a fresh id automatically (ids are opaque — never shown to the user). Defaults the copy's name to \"<source name> copy\" unless you pass `name`. WORKSPACE: the copy FOLLOWS THE SOURCE by default — a project in a workspace is copied into that SAME workspace, and a personal project stays personal. Pass `workspaceId` to override: a workspace id (from list_workspaces) to place the copy in a specific workspace (you need an edit-capable role there), or null to force the copy into your personal space. If the source lives in a workspace you can only VIEW, the call FAILS rather than silently making a personal copy — get an editor role there, or pass workspaceId:null. (Duplicating a project shared with you from a workspace you're NOT a member of lands in personal, since you can't be placed in that workspace.) The returned `workspaceId` tells you where it went. The user's editor won't auto-refresh — they reload to see the copy in the picker. Versions or options of one video (styles to pick from, hooks, languages, a sheet's rows) are pages of ONE project (add_page, duplicate_index), never separate projects: the preview turns pages and exports one video per page.","description_after":"Duplicate an existing project into a brand-new one — a full copy of its layers, animations, styles, uploaded assets, and clips. Pass the SOURCE project's `projectId`; the copy gets a fresh id automatically (ids are opaque; people see the name). Defaults the copy's name to \"<source name> copy\" unless you pass `name`. WORKSPACE: the copy FOLLOWS THE SOURCE by default — a project in a workspace is copied into that SAME workspace, and a personal project stays personal. Pass `workspaceId` to override: a workspace id (from list_workspaces) to place the copy in a specific workspace (you need an edit-capable role there), or null to force the copy into your personal space. If the source lives in a workspace you can only VIEW, the call FAILS rather than silently making a personal copy — get an editor role there, or pass workspaceId:null. (Duplicating a project shared with you from a workspace you're NOT a member of lands in personal, since you can't be placed in that workspace.) The returned `workspaceId` tells you where it went. The user's editor won't auto-refresh — they reload to see the copy in the picker. Versions or options of one video (styles to pick from, hooks, languages, a sheet's rows) are pages of ONE project (add_page, duplicate_index), never separate projects: the preview turns pages and exports one video per page.","description_similarity":0.984,"before_hash":"452dcf4ac4fc9e3d038d67d9c0556118","after_hash":"4000b72bb9f184910bf9460ca70bcb08"},{"tool":"create_project","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Create a new project and return its server-minted id. The id is an opaque v4 UUID assigned automatically — you never choose or name it; refer to the project by its `name`. By default this mints a fresh BLANK project (empty canvas, no layers) — creation is independent from cloning. Pass `fromProjectId` ONLY when you want to clone: the new project then copies that project's JSON, uploaded assets, and uploaded clips, and — like duplicate_project — FOLLOWS THE SOURCE'S workspace by default (cloning a team project stays in that team; a blank project is personal). Pass `workspaceId` (from list_workspaces) to place it in a specific workspace instead — requires an edit-capable role there — or null to force it into the caller's personal space. If the clone source lives in a workspace you can only view, the call FAILS rather than silently making a personal copy — pass workspaceId:null for a personal copy. The user's editor session won't auto-refresh — they'll need to reload to see the new project in the picker. Options or versions of one video (styles to pick from, hooks, languages) are pages of ONE project (add_page, duplicate_index), not new projects: the chat preview turns pages, and separate projects cannot be flicked between.","description_after":"Create a new project and return its server-minted id. The id is an opaque v4 UUID assigned automatically, not chosen by the caller; people see the project's `name`. By default this mints a fresh BLANK project (empty canvas, no layers) — creation is independent from cloning. Pass `fromProjectId` ONLY when you want to clone: the new project then copies that project's JSON, uploaded assets, and uploaded clips, and — like duplicate_project — FOLLOWS THE SOURCE'S workspace by default (cloning a team project stays in that team; a blank project is personal). Pass `workspaceId` (from list_workspaces) to place it in a specific workspace instead — requires an edit-capable role there — or null to force it into the caller's personal space. If the clone source lives in a workspace you can only view, the call FAILS rather than silently making a personal copy — pass workspaceId:null for a personal copy. The user's editor session won't auto-refresh — they'll need to reload to see the new project in the picker. Options or versions of one video (styles to pick from, hooks, languages) are pages of ONE project (add_page, duplicate_index), not new projects: the chat preview turns pages, and separate projects cannot be flicked between.","description_similarity":0.962,"before_hash":"ab561002d107f08b52db996d6e2d6e7d","after_hash":"fddf572de7e827f266d6439a2a29ddd8"},{"tool":"open_project","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Get a tappable link that opens a project directly in the editor (the editor loads exactly this project from the URL). Returns { name, editorUrl }. Use this whenever the user wants to view their work — after you make a change, or when they say \"show me\" / \"open it\" — then give them the editorUrl and invite them to tap it. Refer to the project by its name in your reply; the link is the tap target.","description_after":"Get a link that opens a project directly in the editor (the editor loads exactly this project from the URL), where the person can see the work, change it by hand and export it. Returns { name, editorUrl }; `name` is the project's label.","description_similarity":0.4,"before_hash":"2cc9d59d420ae5eb26927d6a0464eb37","after_hash":"f843c375570260be2d1a44729e43a952"},{"tool":"rename_workspace","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["annotations"],"description_before":null,"description_after":null,"description_similarity":null,"before_hash":"199aa99e33127d19275bb2c5783e0ac6","after_hash":"be1c8364df45a6c6f28ba0e5a9a17e80"},{"tool":"create_workspace","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"Create a new workspace (a shared team space) owned by the caller, and return it as { id, name, role, memberCount }. Use this when the user wants a new place to group projects — then move_project_to_workspace files projects into it, and create_project can place new ones there directly. The caller becomes its owner. Requires an account with an email (workspace membership is email-based), so a keyless identity cannot create one. Refuses with a conflict if the caller already owns a workspace with the same name, which is almost always a repeated request rather than a genuine second workspace. Refer to it by `name` when talking to the user, never the id.","description_after":"Create a new workspace (a shared team space) owned by the caller, and return it as { id, name, role, memberCount }. Use this when the user wants a new place to group projects — then move_project_to_workspace files projects into it, and create_project can place new ones there directly. The caller becomes its owner. Requires an account with an email (workspace membership is email-based), so a keyless identity cannot create one. Refuses with a conflict if the caller already owns a workspace with the same name, which is almost always a repeated request rather than a genuine second workspace.","description_similarity":0.958,"before_hash":"d5a18c77096979f25f81fe9c8078ef12","after_hash":"cdc00f88040143c59baeea98bd0e8a35"},{"tool":"list_workspaces","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"List the workspaces (shared team spaces) the account belongs to, as { id, name, role, memberCount } entries. `role` is the caller's role in that workspace (owner / admin / editor / viewer) — only owner/admin/editor can add projects. Use the `id` as the `workspaceId` argument to create_project, move_project_to_workspace, and list_projects; refer to the workspace by its `name` when talking to the user (never paste the id). Returns an empty list when the account has no email (workspace membership is email-based, so a keyless/dev identity sees none). Call this to discover a workspace before placing a project into it.","description_after":"List the workspaces (shared team spaces) the account belongs to, as { id, name, role, memberCount } entries. `role` is the caller's role in that workspace (owner / admin / editor / viewer) — only owner/admin/editor can add projects. The `id` is the `workspaceId` argument of create_project, move_project_to_workspace and list_projects; `name` is the label people see in the workspace switcher. Returns an empty list when the account has no email (workspace membership is email-based, so a keyless/dev identity sees none).","description_similarity":0.718,"before_hash":"ebd702d7c1cdec0c3f2b5265d3989a44","after_hash":"6b7c165575373c698ee1b99e3c19f6cb"},{"tool":"list_projects","kind":"changed","observed_at":"2026-10-12T04:33:01.579Z","fields":["description"],"description_before":"List projects as { id, name, editorUrl } entries. The id is what every other tool's `projectId` argument takes. `name` is the human-readable label shown in the editor's project picker (\"Untitled\" when no name has been set — ids are opaque and never shown to the user, so always refer to a project by its name). `editorUrl` is a tappable link that opens that project in the editor — offer it to the user (by name) when they want to look at one. By default lists the user's OWN personal projects (those not in any workspace). Pass `workspaceId` (from list_workspaces) to instead list the projects that live in that workspace — including teammates' — each with the owner's `ownerEmail`. Call this when the user asks \"what projects do I have?\" or hasn't named a project but you need to operate on one.","description_after":"List projects as { id, name, editorUrl } entries. The id is what every other tool's `projectId` argument takes. `name` is the human-readable label shown in the editor's project picker (\"Untitled\" when no name has been set; ids are opaque and the editor never shows them). `editorUrl` is a link that opens that project in the editor. By default lists the user's OWN personal projects (those not in any workspace). Pass `workspaceId` (from list_workspaces) to instead list the projects that live in that workspace — including teammates' — each with the owner's `ownerEmail`. Call this when the user asks \"what projects do I have?\" or hasn't named a project but you need to operate on one.","description_similarity":0.859,"before_hash":"f5ffdfe8b1d2b9da1a1972271e43ad39","after_hash":"3df725d8089cf3fba7457dfd83f4515e"},{"tool":"add_page","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description"],"description_before":"Append a page to the project — works on any project, turning a single-page video into a multi-page one. Without duplicate_index a blank page is appended, sized to the project's canvas. With duplicate_index the page at that position is deep-copied (a fresh id is minted). There is no limit on page count. The new page becomes the active page; its index is returned. For several versions of one video (hooks, languages, a sheet's rows), make pages of one project with add_page and duplicate_index. Exporting gives one video per page, and pages have no limit. To edit the copies, select_page each one and describe_video for its element ids; to export them, use the editor's Videos option or renderVideo with page.","description_after":"Append a page to the project — works on any project, turning a single-page video into a multi-page one. Without duplicate_index a blank page is appended, sized to the project's canvas. With duplicate_index the page at that position is deep-copied (a fresh id is minted). There is no limit on page count. The new page becomes the active page; its index is returned. Versions or options of one video (styles to pick from, hooks, languages, a sheet's rows) are pages of ONE project (add_page, duplicate_index), never separate projects: the preview turns pages and exports one video per page. To edit the copies, select_page each one and describe_video for its element ids; to export them, use the editor's Videos option or renderVideo with page.","description_similarity":0.81,"before_hash":"6390f27d7aaf5a009519d162a7d0b86e","after_hash":"7eb6ede33df748f1967a1dc41987d368"},{"tool":"add_text_layer","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description"],"description_before":"Create a new text layer — a first-class leaf that animates, groups, and z-orders exactly like an image or shape. The renderer draws live typeset text (multi-line, auto-fit to the box). Defaults: x/y = canvas centre, width 900, height 320, font_family \"Hanken Grotesk\", text_size derived from existing text layers (or ~10% of canvas height). Also accepts full type styling: font_weight (100-900), font_style (italic), text_transform, letter_spacing, line_height, text_align, text_autofit (\"hug\" default = box shrink-wraps the text at the fixed text_size, honouring literal newlines, so it can't re-wrap between preview and export — bake your own \"\\n\" line breaks / \"wrap\"=fixed size + word-wrap to the box / \"fit\"=auto-size to fill the box, grows and shrinks / \"shrink\"=legacy shrink-only), text_valign (top/middle/bottom), an outline (stroke_width + stroke_color), and text_shadow. Returns the new layer's id + element id (text.<id>).","description_after":"Create a new text layer — a first-class leaf that animates, groups, and z-orders exactly like an image or shape. The renderer draws live typeset text (multi-line, auto-fit to the box). Defaults: x/y = canvas centre, width 900, height 320, font_family \"Hanken Grotesk\", text_size derived from existing text layers (or ~10% of canvas height). Also accepts full type styling: font_weight (100-900), font_style (italic), text_transform, letter_spacing, line_height, text_align, text_autofit (\"hug\" default = box shrink-wraps the text at the fixed text_size, honouring literal newlines, so it can't re-wrap between preview and export — bake your own \"\\n\" line breaks / \"wrap\"=fixed size + word-wrap to the box / \"fit\"=auto-size to fill the box, grows and shrinks / \"shrink\"=legacy shrink-only), text_valign (top/middle/bottom), an outline (stroke_width + stroke_color), and text_shadow. Returns the new layer's id + element id (text.<id>), plus text_box (the painted box, measured from the font's glyphs), fits ({canvas, safe_area}, judged at the layer's static position) and, when the text runs past either, a warning naming the text_size that fits: add \\n line breaks or lower text_size.","description_similarity":0.855,"before_hash":"cd5edaa6529c67c624ff62f2a786b7e3","after_hash":"454467f00003a4624141827f006536fa"},{"tool":"set_layer_text","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description"],"description_before":"Edit an existing text layer (text.<id>). Patches its text content, font, size, colour, and full type styling — pass only the fields you want to change. Does NOT create layers and does NOT touch image layers; use add_text_layer to make a new one. `font_family` is a Google Fonts family name (e.g. \"Anton\", \"Bebas Neue\"). `text_size` is the font size in px (omit to keep the current size). `text_color` is #rrggbb. Styling: font_weight (100-900, e.g. 800 for a black/heavy logo look), font_style (italic), text_transform (uppercase/lowercase), letter_spacing (px, may be negative for tight tracking), line_height (multiplier), text_align, text_autofit (\"wrap\" default = fixed size + word-wrap, the size you set is what renders; \"fit\"=auto-size to fill the box, grows and shrinks; \"shrink\"=legacy shrink-only), text_valign (top/middle/bottom block alignment), an outline via stroke_width + stroke_color, and a text_shadow. To make text MASK another layer (video/image-filled letterforms) use set_matte_source with this layer's id as the matte source. Only `elementId` is required.","description_after":"Edit an existing text layer (text.<id>). Patches its text content, font, size, colour, and full type styling — pass only the fields you want to change. Does NOT create layers and does NOT touch image layers; use add_text_layer to make a new one. `font_family` is a Google Fonts family name (e.g. \"Anton\", \"Bebas Neue\"). `text_size` is the font size in px (omit to keep the current size). `text_color` is #rrggbb. Styling: font_weight (100-900, e.g. 800 for a black/heavy logo look), font_style (italic), text_transform (uppercase/lowercase), letter_spacing (px, may be negative for tight tracking), line_height (multiplier), text_align, text_autofit (\"wrap\" default = fixed size + word-wrap, the size you set is what renders; \"fit\"=auto-size to fill the box, grows and shrinks; \"shrink\"=legacy shrink-only), text_valign (top/middle/bottom block alignment), an outline via stroke_width + stroke_color, and a text_shadow. To make text MASK another layer (video/image-filled letterforms) use set_matte_source with this layer's id as the matte source. Only `elementId` is required. The result carries text_box, fits and a warning when the text runs past the canvas or the safe area (see add_text_layer).","description_similarity":0.908,"before_hash":"ef5544e7a4327b5ce67fea3572728e2e","after_hash":"7d084ba3a62e48bce1bb8dd702914a60"},{"tool":"set_text_background","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description"],"description_before":"Add or update the rounded background box behind a TEXT layer (text.<id>) in one call — sets the backdrop fill plus the box's padding, corner radius, and optional stroke. Pass only the fields you want to change. New text layers are already text_autofit \"hug\", so padding alone shrink-wraps the box to the text — ideal for caption / sticker chips; set_layer_text(text_autofit:\"hug\") is only needed when adding a box to an OLDER layer still on \"wrap\". THIS IS HOW YOU BUILD A BUTTON: a button / CTA / chip / tag / pill / labelled badge is ONE text layer with a native background, never a rounded-rect shape with a text layer parked on top — padding is what sizes the box around the label, so the two can't drift apart when the text or the scale changes and the user drags one layer instead of two. The one exception: the box is NOT painted on CURVED text (a straight box behind a bent line reads as broken), so an arc-shaped chip genuinely needs a shape behind it. Pass fill null to remove the box. Text layers only; for shapes/images/video use set_layer_fill.","description_after":"Add or update the rounded background box behind a TEXT layer (text.<id>) in one call — sets the backdrop fill plus the box's padding, corner radius, and optional stroke. Pass only the fields you want to change. New text layers are already text_autofit \"hug\", so padding alone shrink-wraps the box to the text — ideal for caption / sticker chips; set_layer_text(text_autofit:\"hug\") is only needed when adding a box to an OLDER layer still on \"wrap\". THIS IS HOW YOU BUILD A BUTTON: a button / CTA / chip / tag / pill / labelled badge is ONE text layer with a native background, never a rounded-rect shape with a text layer parked on top — padding is what sizes the box around the label, so the two can't drift apart when the text or the scale changes and the user drags one layer instead of two. The one exception: the box is NOT painted on CURVED text (a straight box behind a bent line reads as broken), so an arc-shaped chip genuinely needs a shape behind it. Pass fill null to remove the box. Text layers only; for shapes/images/video use set_layer_fill. Returns text_box and fits for the padded box, with a warning when it runs past the canvas or the safe area.","description_similarity":0.925,"before_hash":"923aeca2fa660345b7902bb070f11892","after_hash":"591be93228171d012024fe55b026174f"},{"tool":"add_image_layer","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description","inputSchema"],"description_before":"Add an image layer. The asset must already be uploaded (the editor's drag-drop, upload_image, create_upload_link, or POST /api/upload-asset/<projectId> with the raw bytes and an X-Upload-Name header). Every upload returns { filename, name }: pass `filename` (the stored file's id) here, and `name` as the layer's label. To duplicate an existing layer, reuse its filename — the editor auto-assigns a fresh id.","description_after":"Add an image layer. The asset must already be uploaded (the editor's drag-drop, upload_image, create_upload_link, or POST /api/upload-asset/<projectId> with the raw bytes and an X-Upload-Name header). Every upload returns { filename, name, width, height }: pass `filename` (the stored file's id) here, and `name` as the layer's label. SIZE THE BOX TO THE PICTURE: pass `width` alone (or `height` alone) and the other side is filled from the image's own aspect, so the picture is never stretched; a box with both sides that does not match the image's aspect stretches the picture to fill it. To duplicate an existing layer, reuse its filename — the editor auto-assigns a fresh id.","description_similarity":0.653,"before_hash":"40b8328993fa82d6c5bcaf27dfc5b096","after_hash":"149ba5d0df6c99fa2796d9f7212017f0"},{"tool":"describe_video","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description"],"description_before":"Structural OVERVIEW of the composition (the table of contents) — canvas size, duration, platform_safe_area (the part of the canvas TikTok / Reels UI and Instagram's feed crop leave visible: keep text, captions, logos and buttons inside it; null when nothing is covered or cropped), the backdrop summary, and a z-ordered tree (top of stack first) of every layer with its elementId, type, name, type label (filename/clip/text/kind), geometry (x/y/width/height), and which properties are animated. Does NOT include keyframe values or styles — those are unbounded. On a multi-page project the tree describes the ACTIVE page and the data carries a `pages` block ({ page_count, active_index, pages: [{ index, name, has_video }] }) — content tools target that active page; use select_page to switch which page they target, add_page / delete_page / reorder_pages to manage the pages. Start here, then call inspect_layers([elementId, …]) for full detail on the specific layers you'll change. Don't guess keyframe/style values from this overview.","description_after":"Structural OVERVIEW of the composition (the table of contents) — canvas size, duration, platform_safe_area (the part of the canvas TikTok / Reels UI and Instagram's feed crop leave visible: keep text, captions, logos and buttons inside it; null when nothing is covered or cropped), the backdrop summary, and a z-ordered tree (top of stack first) of every layer with its elementId, type, name, type label (filename/clip/text/kind), geometry (x/y/width/height; a hug text layer's is its painted box, measured from the font, with metrics saying so), fits for every text layer ({canvas, safe_area} at its static position), and which properties are animated. Does NOT include keyframe values or styles — those are unbounded. On a multi-page project the tree describes the ACTIVE page and the data carries a `pages` block ({ page_count, active_index, pages: [{ index, name, has_video }] }) — content tools target that active page; use select_page to switch which page they target, add_page / delete_page / reorder_pages to manage the pages. Start here, then call inspect_layers([elementId, …]) for full detail on the specific layers you'll change. Don't guess keyframe/style values from this overview.","description_similarity":0.91,"before_hash":"de7d1f688ebf461b694e20676e8c18f4","after_hash":"9030f66465e768c7c810de1d10a75832"},{"tool":"upload_audio","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description","inputSchema"],"description_before":"Upload an audio track into a project by fetching a public http(s) URL server-side — the ONLY way to get an audio file's bytes into a project over MCP/HTTP (add_audio_overlay / update_audio_overlay only reference a file that is already uploaded). The target is the URL you pass and nothing else: Morpha's Worker fetches it with a plain HTTP GET, following redirects, and identifies itself as \"Morpha/1.0 (+https://morphareels.ai; hello@morphareels.ai)\". No third-party API is involved. The worker downloads the .mp3/.m4a/.wav/.ogg/.aac, stores it in the project's asset bucket under a new id, and returns { filename, name, sizeBytes, contentType }. Then pass the returned `filename` (the file's id, never shown to a person) to add_audio_overlay (add a second track) or update_audio_overlay { id, filename } (replace an existing track's file — find the id via describe_video's audio_overlays), with `name` as the track's name. The url must be a direct, publicly-fetchable http(s) link (not an auth-walled page). Buffered in Worker memory, so capped at 50 MB. For a file on the caller's own disk, use create_upload_link instead. Reference: https://morphareels.ai/docs/tools#uploadaudiourl-name","description_after":"Upload an audio track into a project from a public http(s) URL or a file attached in the chat, fetched server-side — the ONLY way to get an audio file's bytes into a project over MCP/HTTP (add_audio_overlay / update_audio_overlay only reference a file that is already uploaded). Pass `url` for a public http(s) link, or `file` for a file the person attached in the chat (a host that supports file inputs, such as ChatGPT, fills `file` with a temporary download link itself; do not ask the person for a URL to something they have attached). The target is the URL you pass and nothing else: Morpha's Worker fetches it with a plain HTTP GET, following redirects, and identifies itself as \"Morpha/1.0 (+https://morphareels.ai; hello@morphareels.ai)\". No third-party API is involved. The worker downloads the .mp3/.m4a/.wav/.ogg/.aac, stores it in the project's asset bucket under a new id, and returns { filename, name, sizeBytes, contentType }. Then pass the returned `filename` (the file's id, never shown to a person) to add_audio_overlay (add a second track) or update_audio_overlay { id, filename } (replace an existing track's file — find the id via describe_video's audio_overlays), with `name` as the track's name. A url must be a direct, publicly-fetchable http(s) link (not an auth-walled page). Buffered in Worker memory, so capped at 50 MB. For a file on the caller's own disk, use create_upload_link instead. Reference: https://morphareels.ai/docs/tools#uploadaudiourl-file-name","description_similarity":0.861,"before_hash":"52cf81d5052ca5b2014e8cfa7a876a23","after_hash":"e20545f3e8e77022bf13e0eadd32d754"},{"tool":"upload_image","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description","inputSchema"],"description_before":"Upload an image into a project by fetching a public http(s) URL server-side. The target is the URL you pass and nothing else: Morpha's Worker fetches it with a plain HTTP GET, following redirects, and identifies itself as \"Morpha/1.0 (+https://morphareels.ai; hello@morphareels.ai)\". No third-party API is involved. The worker downloads the .png/.jpg/.jpeg/.gif/.webp/.svg, stores it in the project's asset bucket under a new id, and returns { filename, name, sizeBytes, contentType }: pass the returned `filename` (the file's id, never shown to a person) to add_image_layer, and `name` is what people see. This tool does not search for images: pass a direct image file URL, such as one the person has given you, and respect the licence its source states. The url must be a direct, publicly-fetchable http(s) link (not an auth-walled page). SVGs are rejected if they carry a <script>, an inline event handler, a <foreignObject>, a javascript: URL, or an external resource reference — supply a static, self-contained SVG. Buffered in Worker memory, so capped at 16 MB. For a file on the caller's own disk, use create_upload_link instead. Reference: https://morphareels.ai/docs/tools#uploadimageurl-name","description_after":"Upload an image into a project from a public http(s) URL or a file attached in the chat, fetched server-side. Pass `url` for a public http(s) link, or `file` for a file the person attached in the chat (a host that supports file inputs, such as ChatGPT, fills `file` with a temporary download link itself; do not ask the person for a URL to something they have attached). The target is the URL you pass and nothing else: Morpha's Worker fetches it with a plain HTTP GET, following redirects, and identifies itself as \"Morpha/1.0 (+https://morphareels.ai; hello@morphareels.ai)\". No third-party API is involved. The worker downloads the .png/.jpg/.jpeg/.gif/.webp/.svg, stores it in the project's asset bucket under a new id, and returns { filename, name, sizeBytes, contentType, width, height }: pass the returned `filename` (the file's id, never shown to a person) to add_image_layer, and `name` is what people see; `width` and `height` are the picture's own pixel size (null when its header did not say), so size the layer from them or pass add_image_layer one side and let it keep the aspect. This tool does not search for images: pass a direct image file URL, such as one the person has given you, and respect the licence its source states. A url must be a direct, publicly-fetchable http(s) link (not an auth-walled page). SVGs are rejected if they carry a <script>, an inline event handler, a <foreignObject>, a javascript: URL, or an external resource reference — supply a static, self-contained SVG. Buffered in Worker memory, so capped at 16 MB. For a file on the caller's own disk, use create_upload_link instead. Reference: https://morphareels.ai/docs/tools#uploadimageurl-file-name","description_similarity":0.808,"before_hash":"9b9ee22428c40c0869f6546a84c97c4a","after_hash":"1f28e8ee189278e3b2ca477147e7d24c"},{"tool":"duplicate_project","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description"],"description_before":"Duplicate an existing project into a brand-new one — a full copy of its layers, animations, styles, uploaded assets, and clips. Pass the SOURCE project's `projectId`; the copy gets a fresh id automatically (ids are opaque — never shown to the user). Defaults the copy's name to \"<source name> copy\" unless you pass `name`. WORKSPACE: the copy FOLLOWS THE SOURCE by default — a project in a workspace is copied into that SAME workspace, and a personal project stays personal. Pass `workspaceId` to override: a workspace id (from list_workspaces) to place the copy in a specific workspace (you need an edit-capable role there), or null to force the copy into your personal space. If the source lives in a workspace you can only VIEW, the call FAILS rather than silently making a personal copy — get an editor role there, or pass workspaceId:null. (Duplicating a project shared with you from a workspace you're NOT a member of lands in personal, since you can't be placed in that workspace.) The returned `workspaceId` tells you where it went. The user's editor won't auto-refresh — they reload to see the copy in the picker. For several versions of one video (hooks, languages, a sheet's rows), make pages of one project with add_page and duplicate_index. Exporting gives one video per page, and pages have no limit.","description_after":"Duplicate an existing project into a brand-new one — a full copy of its layers, animations, styles, uploaded assets, and clips. Pass the SOURCE project's `projectId`; the copy gets a fresh id automatically (ids are opaque — never shown to the user). Defaults the copy's name to \"<source name> copy\" unless you pass `name`. WORKSPACE: the copy FOLLOWS THE SOURCE by default — a project in a workspace is copied into that SAME workspace, and a personal project stays personal. Pass `workspaceId` to override: a workspace id (from list_workspaces) to place the copy in a specific workspace (you need an edit-capable role there), or null to force the copy into your personal space. If the source lives in a workspace you can only VIEW, the call FAILS rather than silently making a personal copy — get an editor role there, or pass workspaceId:null. (Duplicating a project shared with you from a workspace you're NOT a member of lands in personal, since you can't be placed in that workspace.) The returned `workspaceId` tells you where it went. The user's editor won't auto-refresh — they reload to see the copy in the picker. Versions or options of one video (styles to pick from, hooks, languages, a sheet's rows) are pages of ONE project (add_page, duplicate_index), never separate projects: the preview turns pages and exports one video per page.","description_similarity":0.885,"before_hash":"bf77b84302ebb98726dad5a5227da25f","after_hash":"452dcf4ac4fc9e3d038d67d9c0556118"},{"tool":"create_project","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description"],"description_before":"Create a new project and return its server-minted id. The id is an opaque v4 UUID assigned automatically — you never choose or name it; refer to the project by its `name`. By default this mints a fresh BLANK project (empty canvas, no layers) — creation is independent from cloning. Pass `fromProjectId` ONLY when you want to clone: the new project then copies that project's JSON, uploaded assets, and uploaded clips, and — like duplicate_project — FOLLOWS THE SOURCE'S workspace by default (cloning a team project stays in that team; a blank project is personal). Pass `workspaceId` (from list_workspaces) to place it in a specific workspace instead — requires an edit-capable role there — or null to force it into the caller's personal space. If the clone source lives in a workspace you can only view, the call FAILS rather than silently making a personal copy — pass workspaceId:null for a personal copy. The user's editor session won't auto-refresh — they'll need to reload to see the new project in the picker.","description_after":"Create a new project and return its server-minted id. The id is an opaque v4 UUID assigned automatically — you never choose or name it; refer to the project by its `name`. By default this mints a fresh BLANK project (empty canvas, no layers) — creation is independent from cloning. Pass `fromProjectId` ONLY when you want to clone: the new project then copies that project's JSON, uploaded assets, and uploaded clips, and — like duplicate_project — FOLLOWS THE SOURCE'S workspace by default (cloning a team project stays in that team; a blank project is personal). Pass `workspaceId` (from list_workspaces) to place it in a specific workspace instead — requires an edit-capable role there — or null to force it into the caller's personal space. If the clone source lives in a workspace you can only view, the call FAILS rather than silently making a personal copy — pass workspaceId:null for a personal copy. The user's editor session won't auto-refresh — they'll need to reload to see the new project in the picker. Options or versions of one video (styles to pick from, hooks, languages) are pages of ONE project (add_page, duplicate_index), not new projects: the chat preview turns pages, and separate projects cannot be flicked between.","description_similarity":0.813,"before_hash":"1489a68b6496902b8f5189b3feb65bf9","after_hash":"ab561002d107f08b52db996d6e2d6e7d"},{"tool":"show_project","kind":"changed","observed_at":"2026-10-11T22:56:01.862Z","fields":["description"],"description_before":"Show the project in the chat as a live preview that plays and follows every edit. Call it once per project, right after creating or opening it; later edits appear in the preview without calling again. Answers { name, pages, durationSeconds, editorUrl }. The preview lasts 30 minutes; call again if the person comes back later. `controls` picks the player's controls: 'full' (the default: play, a scrub bar, the time, sound and fullscreen) or 'play' (the play button alone, for a preview that should look like a plain video). In a client that cannot render a preview, give the person the editorUrl instead.","description_after":"Show the project in the chat as a live preview that plays and follows every edit; a multi-page project turns its pages in the preview. Call it once per project, right after creating or opening it; later edits appear in the preview without calling again. Answers { name, pages, placards, durationSeconds, editorUrl }. Its result also carries one picture per page, a Morpha placard shaped like that page (up to 12, in page order): use it wherever you show a page, and never make or fetch a picture of your own. The preview lasts 30 minutes; call again if the person comes back later. `controls` picks the player's controls: 'full' (the default: play, a scrub bar, the time, sound and fullscreen) or 'play' (the play button alone, for a preview that should look like a plain video). In a client that cannot render a preview, give the person the editorUrl instead.","description_similarity":0.72,"before_hash":"9d9992e5275e31b3af327b5c768caa8c","after_hash":"acc466c0dbe7119ebf07436bce39216c"},{"tool":"move_band","kind":"removed","observed_at":"2026-10-11T16:45:12.129Z","fields":[],"description_before":"Place an embedded morpha band on the host timeline: set its TIME ORIGIN (the frame it starts). The band's whole inner reel plays relative to this frame, so its intro animations fire when the band appears instead of at 0:00 (the fix for 'the embedded intro doesn't animate'). Keeps the band's current window length; if it had none, the band spans from start to the composition end. Pass the band group's id (from describe_video — a group with morpha:true).","description_after":null,"description_similarity":null,"before_hash":"9442858eeba56323bec3d543b03bab88","after_hash":null},{"tool":"add_morpha_layer","kind":"removed","observed_at":"2026-10-11T16:45:12.129Z","fields":[],"description_before":"Embed another of the user's projects (\"a morpha\") inside this one as a version-pinned band. The source's layers are inlined into the host as a collapsible group, re-keyed to fresh ids, pinned to one immutable version of the source. Pass the source project's id as source_morpha_id (and optionally a version label); the server resolves and inlines the pinned version. Editing the band's inner layers only affects THIS video — the change is local and never propagates back to the source. To pin the band to a different saved version, re-pin it from the editor's Inspector. describe_video marks an embedded band with morpha:true + source_morpha_id so you can tell it apart from a plain group.","description_after":null,"description_similarity":null,"before_hash":"4975ae2ba79b444d453b2bfad7f31009","after_hash":null},{"tool":"set_embed_start","kind":"added","observed_at":"2026-10-11T16:45:12.129Z","fields":[],"description_before":null,"description_after":"Place an embedded morpha band on the host timeline: set its TIME ORIGIN (the frame it starts). The band's whole inner reel plays relative to this frame, so its intro animations fire when the band appears instead of at 0:00 (the fix for 'the embedded intro doesn't animate'). Keeps the band's current window length; if it had none, the band spans from start to the composition end. Pass the band group's id (from describe_video — a group with morpha:true).","description_similarity":null,"before_hash":null,"after_hash":"457957cbcb7e6af7e40e880f2da0749b"},{"tool":"set_layer_block","kind":"changed","observed_at":"2026-10-11T16:45:12.129Z","fields":["description"],"description_before":"Set (or replace) a layer's timeline BLOCK — the [start, start+duration) window it exists for. The layer is drawn ONLY inside that window, and its animation keyframes are sampled RELATIVE to the block start, so moving or trimming the block re-anchors its intro instead of leaving it behind. This is how a layer 'starts' at a point like an iMovie clip rather than being present for the whole composition. Works on any leaf or group. Frames are in the layer's parent timeline (composition frames at root; band-local inside an embedded morpha band). To place a whole embedded reel, use move_band.","description_after":"Set (or replace) a layer's timeline BLOCK — the [start, start+duration) window it exists for. The layer is drawn ONLY inside that window, and its animation keyframes are sampled RELATIVE to the block start, so moving or trimming the block re-anchors its intro instead of leaving it behind. This is how a layer 'starts' at a point like an iMovie clip rather than being present for the whole composition. Works on any leaf or group. Frames are in the layer's parent timeline (composition frames at root; band-local inside an embedded morpha band). To place a whole embedded reel, use set_embed_start.","description_similarity":0.972,"before_hash":"946e21dbb1e8de1398bec27cf9bc0ec2","after_hash":"b9e2c63f80007e0e16c6535c68b50852"},{"tool":"embed_project","kind":"added","observed_at":"2026-10-11T16:45:12.129Z","fields":[],"description_before":null,"description_after":"Embed another of the user's projects (\"a morpha\") inside this one as a version-pinned band. The source's layers are inlined into the host as a collapsible group, re-keyed to fresh ids, pinned to one immutable version of the source. Pass the source project's id as source_morpha_id (and optionally a version label); the server resolves and inlines the pinned version. Editing the band's inner layers only affects THIS video — the change is local and never propagates back to the source. To pin the band to a different saved version, re-pin it from the editor's Inspector. describe_video marks an embedded band with morpha:true + source_morpha_id so you can tell it apart from a plain group.","description_similarity":null,"before_hash":null,"after_hash":"2ed395afbf86fe201b056a42918c2bfa"},{"tool":"add_shape","kind":"changed","observed_at":"2026-10-11T16:45:12.129Z","fields":["description"],"description_before":"Add a shape layer. `kind` selects the primitive: rect | ellipse | triangle | star | pentagon | hexagon | arrow | heart | rounded-rect | diamond | parallelogram | trapezoid | semicircle | ring | pill | cross | heptagon | octagon | star-4 | star-6 | sparkle | burst | arrow-left | double-arrow | chevron | block-arrow-up | curve | lightning | speech-bubble | location-pin | checkmark | x-mark | shield | cloud | crescent | teardrop | banner. All are native vector shapes; never substitute an image layer for one. Default-positioned in canvas centre if x/y/w/h omitted.","description_after":"Add a shape layer. `kind` selects the primitive: rect | ellipse | triangle | star | pentagon | hexagon | arrow | heart | rounded-rect | diamond | parallelogram | trapezoid | semicircle | ring | pill | cross | heptagon | octagon | star-4 | star-6 | sparkle | burst | arrow-left | double-arrow | chevron | block-arrow-up | curve | lightning | speech-bubble | location-pin | checkmark | x-mark | shield | cloud | crescent | teardrop | banner. All are native vector shapes; never substitute an image layer for one. Default-positioned in canvas centre if x/y/w/h omitted. A new shape stacks ON TOP of every existing layer: for a background colour or gradient use set_layer_fill on \"background.canvas\", not a canvas-sized shape, and to place a shape behind other layers follow with reorder_layer. A shape that covers the whole canvas returns a warning naming the layers it hides.","description_similarity":0.68,"before_hash":"2ee6fd8376e959f7e4206d28ea09d3dc","after_hash":"b82fba743aff997d52c36824ec47b4ec"},{"tool":"show_project","kind":"added","observed_at":"2026-10-11T16:45:12.129Z","fields":[],"description_before":null,"description_after":"Show the project in the chat as a live preview that plays and follows every edit. Call it once per project, right after creating or opening it; later edits appear in the preview without calling again. Answers { name, pages, durationSeconds, editorUrl }. The preview lasts 30 minutes; call again if the person comes back later. `controls` picks the player's controls: 'full' (the default: play, a scrub bar, the time, sound and fullscreen) or 'play' (the play button alone, for a preview that should look like a plain video). In a client that cannot render a preview, give the person the editorUrl instead.","description_similarity":null,"before_hash":null,"after_hash":"9d9992e5275e31b3af327b5c768caa8c"},{"tool":"render_status","kind":"changed","observed_at":"2026-09-30T23:53:18.789Z","fields":["description"],"description_before":"Check a server render started by render_video, and get its download link. Returns { ok: true, status: 'not-ready' } while it is still rendering (wait and call again), or { ok: true, status: 'ready', data: { downloadUrl, editorUrl, expiresAt, videoSeconds, scale, chargedCents } } once the MP4 is ready. `downloadUrl` is a plain https link needing no Morpha account — give it to the person; it stops working when the render is deleted at `expiresAt` (7 days). `chargedCents` is what the render cost the subscription. A render that failed answers ok:false with a plain reason and is never charged. Only the account that started a render can read it.","description_after":"Check a server render started by render_video, and get its download link. Returns { ok: true, status: 'not-ready' } while it is still rendering (wait and call again), or { ok: true, status: 'ready', data: { downloadUrl, editorUrl, expiresAt, videoSeconds, scale } } once the MP4 is ready. `downloadUrl` is a plain https link needing no Morpha account — give it to the person; it stops working when the render is deleted at `expiresAt` (7 days). A render that failed answers ok:false with a plain reason and uses no allowance. Only the account that started a render can read it.","description_similarity":0.884,"before_hash":"9e5c13678a8929cf699942ddc9effc83","after_hash":"7a76fcac5070c1d60d93932666bd45a0"},{"tool":"render_video","kind":"changed","observed_at":"2026-09-30T23:53:18.789Z","fields":["description","inputSchema"],"description_before":"Render the project to an MP4 on Morpha's servers, for an agent that has no browser to drive (a chat app, a Linux box). Returns { renderId, status: 'rendering', next } — call render_status with that renderId after about 30 seconds. THIS COSTS THE ACCOUNT MONEY and needs a subscription: the render is charged to the subscription's credits at Morpha's measured compute cost, so a longer composition and a larger scale both cost more. Two free routes exist and are usually better — give the person the editorUrl every project result carries and they choose Share, then Download (Render MP4 on a phone) in the editor, free and with no watermark; or a script runs the morphareels-sdk npm client's client.renderVideo(projectId) on macOS or Windows. Renders at 2x the canvas by default, the same as the editor and the npm client; pass scale: 1 for the canvas's own size, which is faster and cheaper. The cost is reserved against the subscription's credits before the render starts, so a render is refused up front when the credit to pay for it is not there rather than discovered afterwards; you are then billed the real measured cost, never more than the amount reserved. Refused for a composition longer than 30 minutes, when the remaining credit will not cover it, or while 3 renders are already running for this account. The finished file stays downloadable for 7 days, then it is deleted; the project stays and can be rendered again. The render downloads the project's fonts from their hosts, including a custom font's own URL (set_custom_font). Reference: https://morphareels.ai/docs/tools#rendervideopage-scale","description_after":"Render the project to an MP4 on Morpha's servers, for an agent that has no browser to drive (a chat app, a Linux box). Returns { renderId, status: 'rendering', next } — call render_status with that renderId after about 30 seconds. It needs a subscription and draws on the account's render allowance, so a longer composition and a larger scale each use more of it. Two other routes need no allowance and are usually better — give the person the editorUrl every project result carries and they choose Share, then Download (Render MP4 on a phone) in the editor, free and with no watermark; or a script runs the morphareels-sdk npm client's client.renderVideo(projectId) on macOS or Windows. Renders at 2x the canvas by default, the same as the editor and the npm client; pass scale: 1 for the canvas's own size, which is faster and uses less allowance. The allowance is checked before the render starts, so a render that would exceed it is refused up front rather than failing partway. Refused for a composition longer than 30 minutes, when the remaining allowance will not cover it, or while 3 renders are already running for this account. The finished file stays downloadable for 7 days, then it is deleted; the project stays and can be rendered again. The render downloads the project's fonts from their hosts, including a custom font's own URL (set_custom_font). Reference: https://morphareels.ai/docs/tools#rendervideopage-scale","description_similarity":0.791,"before_hash":"ddb1754b591aec1e6f1d5116db38dc74","after_hash":"fb5d235570b9d69dd4e20572d5e4bd69"}],"next_before":null,"next_actions":[]}