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.
Drive a hosted project — createClient (recommended)
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_clip, upload_image, upload_audio, find_public_image, 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 Render button (no ffmpeg, no server):
const mp4 = await morpha.renderVideo("your-project-id");
createClient({ origin?, token?, fetch? }) returns a MorphaClient:
- `getProject(id)` —
GET /api/project/:id→ a validatedProject. - `listTools()` —
GET /api/tools→ToolFunction[](OpenAI tool shape:{ type, function: { name, description, parameters } }). - `callTool(id, name, args?)` —
POST /api/tool/:name. For a pure mutation tool →{ result, project, editorUrl }(the server loads → dispatches → writes back, exactly like MCP). For a server tool (workspace/upload/vision) →{ result }with noproject. - `renderFrame(id, frame?, opts?)` — one composited PNG
Bufferof that frame (real browser, no ffmpeg). - `renderVideo(id, opts?)` — the full composition as an MP4
Buffer(the same in-browser WebCodecs H.264 encode the editor's Render button runs — no ffmpeg, no server).
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`; a Standard or Pro subscription is required, since this endpoint is gated identically to MCP). 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:
- Workspace / lifecycle —
listProjects(),openProject(id),createProject({ fromProjectId?, name? })(a fresh blank project by default — the id is minted server-side and returned; passfromProjectIdonly to clone that project's JSON + assets + clips),duplicateProject(sourceId, { name? }),renameProject(id, name),reidProject(id, newId?),deleteProject(id). - Versions —
saveVersion(id, { name? }),listVersions(id),restoreVersion(id, versionId),renameVersion(id, versionId, name),deleteVersion(id, versionId). - Ingest —
uploadClip(id, { url, filename?, durationSeconds? }),uploadClipPresign(id, { filename }),uploadClipFinalize(id, { filename, durationSeconds? }),uploadImage(id, { url, filename? }),setCustomFont(id, { family, src, weight?, style? })(register a non-Google typeface;srcis a font URL or an uploaded asset filename),findPublicImage(id, query, { licenseType?, minDimension? }). - Vision / transcription —
detectTextRegions(id, { clip } | { image }),safeZones(id, { clip, … }),transcribeClip(id, clip). These read caches produced when a clip is opened in the editor, so they return{ status: "ready" | "not-ready", data, note? }—not-ready(never an exception) until the clip has been opened once.
const [{ id }] = await morpha.listProjects();
await morpha.uploadImage(id, { url: "https://example.com/logo.png" });
await morpha.callTool(id, "add_image_layer", { filename: "logo.png", x: 540, y: 600, width: 300, height: 300 });
await morpha.saveVersion(id, { name: "add logo" });
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?, origin?, token?, width?, height?, channel?, timeoutMs? }. 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.
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 Render button 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, origin?, token?, channel?, timeoutMs? }. 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; a 30 s 1080×1920 composition encodes in well under a minute. If the export fails (e.g. a clip can't load), renderVideo throws a descriptive error rather than returning a truncated file.
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, 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) 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.