Morpha SDK

`morphareels-sdk` is the official npm client for Morpha — the friendliest way to drive the editor from code. It calls the same tool catalog as MCP, over the same Worker HTTP endpoints, and adds one-call PNG frame + MP4 video rendering with no ffmpeg (a real local browser does the encode). It's the recommended client; it doesn't replace the MCP server — MCP-native agents (Claude Desktop, Claude Code) still connect over MCP.

npm i morphareels-sdk

Requires Node ≥ 20. renderFrame / renderVideo additionally need Playwright + system Chrome (see below); everything else is pure HTTP.

Get a key with no human — registerAccount

If you have no mp_… key and nobody to ask for one, mint an account yourself. No browser, no email address, no sign-up form.

import { registerAccount, createClient } from "morphareels-sdk";

const { token, claimUrl } = await registerAccount({ name: "my-agent" });
const morpha = createClient({ token });
// … build the video, then give the person `claimUrl`

It is an anonymous account: it can build, edit and render, and has no AI credit envelope, so drive the tools with your own model, which is free and unmetered.

Nobody can sign into it, so don't leave a person's work there. It becomes theirs through its claim link: registerAccount() returns it as claimUrl, and so does every create_project, open_project and list_projects result for that account. The person opens it, signs in or signs up, and the project moves into their own account. An anonymous account nobody claims is deleted after 30 days.

createClient is the programmatic equivalent of an MCP session over the full catalog. Every tool you can call over MCP you can call here — the pure mutation tools and the workspace/lifecycle, version, upload, and vision tools (list_projects, create_project, save_version, upload_image, upload_audio, detect_text_regions, safe_zones, …). Most have a typed convenience method (below); the rest go through generic callTool.

import { createClient } from "morphareels-sdk";

const morpha = createClient({ token: process.env.MORPHA_API_KEY }); // origin defaults to https://morphareels.ai

const tools = await morpha.listTools();                  // the tool catalog (OpenAI tool shape)
const project = await morpha.getProject("your-project-id"); // the live project JSON

// One tool call = load -> dispatch -> write back, server-side (exactly like MCP):
const { result, project: next, editorUrl } = await morpha.callTool(
  "your-project-id",
  "add_text_layer",
  { text: "HELLO", x: 540, y: 600, font_family: "Anton" },
);

// A composited PNG of one frame (real browser, no ffmpeg):
const png = await morpha.renderFrame("your-project-id", 150);

// The full composition as an MP4 Buffer — same WebCodecs pipeline as the
// editor's Share → Download (no ffmpeg, no server):
const mp4 = await morpha.renderVideo("your-project-id");

// The same MP4 written straight to disk, for long videos:
await morpha.renderVideoToFile("your-project-id", "video.mp4");

createClient({ origin?, token?, fetch? }) returns a MorphaClient:

callTool throws only on transport/HTTP errors; a tool-level failure (e.g. a missing asset) comes back as result.ok === false — a normal outcome to inspect, not an exception. The token is the same mp_… API key MCP uses (mint it at `/app/settings`; any signed-in account can, free or paid, since this endpoint is gated identically to MCP). A key an MCP client obtained by signing in works here unchanged — it is the same kind of key, listed on the same page. For local dev, createClient({ origin: "http://localhost:8787" }) needs no token.

Typed methods

The catalog's non-mutation tools have typed convenience methods that unwrap result.data for you:

const [{ id }] = await morpha.listProjects();
const { filename, name } = await morpha.uploadImage(id, { url: "https://example.com/logo.png" });
await morpha.callTool(id, "add_image_layer", { filename, name, x: 540, y: 600, width: 300, height: 300 });
await morpha.saveVersion(id, { name: "add logo" });

Uploads return the file's id

Every upload (addVideo, uploadImage, uploadAudio and findPublicImage) returns { filename, name }. Morpha names each stored file itself with an opaque id, such as 3f2a9c1e-5b7d-4e8f-9a0b-1c2d3e4f5a6b.png, the same way it mints a project's id. Pass that filename to add_image_layer, set_image_filename, add_video_layer (as clip), add_audio_overlay or set_custom_font exactly as returned, and never show it to a person. name is what people see: the file's own name unless you pass { name }, and the label to give the layer or track. Two uploads under one name are two separate files, each with its own id, so uploading over a name never replaces a file. To swap a layer's picture, upload the new file and pass its filename to set_image_filename.

Breaking change in 0.11. Earlier versions took a filename option that chose the stored name, and uploading under a name that was already there replaced that file. The option is now name, and it only sets what people see; passing filename throws. An earlier SDK is refused, and nothing is written, when it sends a stored name: a clip or a file from disk gets HTTP 409 upload-client-outdated, and a URL upload that passes filename gets a tool error. Update with npm i morphareels-sdk@latest and use the filename each call returns.

Render a frame to PNG — renderFrame (no ffmpeg)

The standalone form, if you don't need a full client:

import { renderFrame } from "morphareels-sdk";
import { writeFile } from "node:fs/promises";

const png = await renderFrame({ projectId: "demo", frame: 150, token: process.env.MORPHA_API_KEY });
await writeFile("frame-150.png", png);

Rendering composites the whole project (video + captions + shapes + text) in a real browser, so it's pixel-identical to the editor — and it decodes HEVC / iPhone `.MOV` via the OS decoder. It needs Playwright + Google Chrome on the calling machine:

npm i playwright    # optional peer dependency; only needed for renderFrame()

Options: { projectId, frame?, page?, origin?, token?, width?, height?, channel?, timeoutMs? }. page is the 0-based page index for multi-page projects (default: the project's active page) — combined with frame it photographs any page at any moment of its timeline. channel defaults to "chrome" (system Chrome — decodes HEVC on macOS/Windows); pass "chromium" for H.264/VP9/AV1 only. If a video layer can't decode, renderFrame throws a descriptive error rather than returning a black frame.

Several frames at once — renderFrames

Sampling a strip of frames? renderFrame in a loop pays for a browser launch, a document load, a project fetch, a font load and a full clip download per frame. renderFrames does all of that once and re-seeks for the rest:

const [start, middle, end] = await morpha.renderFrames("demo", [0, 150, 300]);
// or standalone:
import { renderFrames } from "morphareels-sdk";
const pngs = await renderFrames({ projectId: "demo", frames: [0, 150, 300], token });

One PNG per frame, in the order given, and pixel-identical to the one-at-a-time path — the saving comes from not repeating work. It grows with the frame count, because what it removes is the fixed per-frame cost: measured against production, 5 frames 9.2s → 3.4s (2.7×), 10 frames 18.8s → 4.9s (3.9×). Worth reaching for when sampling a strip; not a reason to batch two.

Options are renderFrame's, with frames: number[] in place of frame.

Render the full video to MP4 — renderVideo (no ffmpeg, no server)

renderVideo exports the whole composition to an MP4 — the same in-browser WebCodecs H.264 pipeline the editor's Share → Download uses, driven by a real local browser. There's no ffmpeg and no server-side render: the encode happens in your machine's Chrome.

import { renderVideo } from "morphareels-sdk";
import { writeFile } from "node:fs/promises";

const mp4 = await renderVideo({ projectId: "demo", token: process.env.MORPHA_API_KEY });
await writeFile("demo.mp4", mp4);

Options: { projectId, page?, scale?, origin?, token?, channel?, timeoutMs? }. scale is the export quality: 2 (the default) renders at double the canvas size, 2160×3840 for a portrait canvas, the same default as the editor's 1×/2× cards; 1 renders at the canvas's own size. page is the 0-based page index for multi-page projects (default: the project's active page) — loop the pages to export one MP4 per page, the scripted equivalent of the editor's "Videos" export card:

import { createClient } from "morphareels-sdk";
import { writeFile } from "node:fs/promises";

const morpha = createClient({ token: process.env.MORPHA_API_KEY });
const project = await morpha.getProject("your-project-id");
for (let i = 0; i < project.pages.length; i++) {
  await writeFile(`page-${i + 1}.mp4`, await morpha.renderVideo(project.project_id, { page: i }));
}

Like renderFrame, it needs Playwright + system Chrome (channel defaults to "chrome"don't use "chromium", which ships without the H.264 encoder and will fail). timeoutMs defaults to 10 minutes. On an Apple M5 Pro, a 30-second composition of shapes rendered in 8 seconds at 2× and 5.6 seconds at 1×; footage, effects and long projects take longer. The browser writes the MP4 to disk as it encodes, and the file comes back from it in 8 MiB chunks, so there is no size ceiling on the handoff. If the export fails (e.g. a clip can't load), renderVideo throws a descriptive error rather than returning a truncated file.

renderVideo returns a Buffer, so the whole MP4 ends up in your process's memory. For a long video, write it to disk instead: renderVideoToFile takes the same options plus path, writes each chunk to the file as it arrives, and returns { path, bytes }. The file appears at path only once every byte has arrived, so a failed render leaves nothing there.

import { renderVideoToFile } from "morphareels-sdk";

const { bytes } = await renderVideoToFile({
  projectId: "demo",
  token: process.env.MORPHA_API_KEY,
  path: "demo.mp4",
});

It needs Chrome on macOS or Windows. Chrome on Linux has no AAC audio encoder, so the render page refuses to export there, before any frame renders, rather than hand back a silent MP4. From a Linux machine, give the person the project's editor link and let them choose Share, then Download on their Mac or PC.

Build a project offline — the pure core

The package also exports the pure tool catalog — the same functions with no network and no persistence. Use it to construct or transform a project in memory (you save it yourself).

import { blankProject, dispatchOnProject, projectSchema } from "morphareels-sdk";

let project = blankProject({ projectId: "demo", canvasWidth: 1080, canvasHeight: 1920 });
project = dispatchOnProject(project, "add_text_layer", { text: "HELLO", x: 540, y: 600 }).project;
project = dispatchOnProject(project, "add_caption_track", {
  lines: [{ text: "first line", startFrame: 0, endFrame: 30 }],
}).project;

projectSchema.parse(project); // a valid Morpha project

dispatchOnProject(project, name, args) returns { project, result } and changes nothing on the server — it's local-only. To edit a project the editor will see, use callTool (above) instead. TOOL_DEFINITIONS is the full catalog in OpenAI tool shape.

dispatchOnProject is the entry point for a whole project — the same router the hosted surfaces use. A Morpha project is an ordered list of pages (one for a plain video, several for a carousel); dispatchOnProject runs the page tools (add_page / delete_page / reorder_pages / select_page) and set_canvas_size on the project, and routes every content tool at the active page, folding the edit back into it. The lower-level dispatch[name] catalog is also exported, but it operates on a single page's composition (Composition), not a Project — use dispatchOnProject unless you're driving one page's composition directly.

Captions

import { transcriptToCaptionLines, buildCaptionsForClip } from "morphareels-sdk";

transcriptToCaptionLines(words) turns transcript word-timings into synced caption lines; buildCaptionsForClip / hasCaptionsForClip / removeCaptionsForClip / videoElementIdForClip build and manage a caption track on a clip.

Types

All TypeScript types are exported: Project, ImageLayer, VideoLayer, TextLayer, LayerStyle, Easing, ToolFunction, ToolResult, ToolDispatch, RenderFrameOptions, MorphaClient, MorphaClientOptions, ToolCallResult, ToolResultEnvelope, CacheReadResult, UploadedFile, AddVideoSource, UploadAssetSource, CaptionLine, TranscriptWordLike, BlankProjectOpts.

SDK vs MCP

Use the SDK for scripts and agents you write in JS/TS — it's typed, ergonomic, exposes the full tool catalog (every tool MCP has), and is the only route that also renders frames and full MP4s locally. Use [MCP](/docs/mcp) for MCP-native clients (Claude Desktop, Claude Code) that speak the protocol directly. Both hit the same catalog over the same backend; the SDK is a client of it, not a replacement.