Morpha SDK
`morpha-studio-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 morpha-studio-sdk
Drive a hosted project — createClient (recommended)
createClient is the programmatic equivalent of an MCP session. Every tool you can call over MCP, you call here.
import { createClient } from "morpha-studio-sdk";
const morpha = createClient({ token: process.env.MORPHA_TOKEN }); // origin defaults to https://morphastudio.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→{ result, project, editorUrl }. The server loads the project, dispatches the tool, and writes it back — the same load → dispatch → write round-trip as MCP. - `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`). For local dev, createClient({ origin: "http://localhost:8787" }) needs no token.
Render a frame to PNG — renderFrame (no ffmpeg)
The standalone form, if you don't need a full client:
import { renderFrame } from "morpha-studio-sdk";
import { writeFile } from "node:fs/promises";
const png = await renderFrame({ projectId: "demo", frame: 150, token: process.env.MORPHA_TOKEN });
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 "morpha-studio-sdk";
import { writeFile } from "node:fs/promises";
const mp4 = await renderVideo({ projectId: "demo", token: process.env.MORPHA_TOKEN });
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, dispatch, projectSchema } from "morpha-studio-sdk";
let project = blankProject({ projectId: "demo", canvasWidth: 1080, canvasHeight: 1920 });
project = dispatch.add_text_layer(project, { text: "HELLO", x: 540, y: 600 }).project;
project = dispatch.add_caption_track(project, {
lines: [{ text: "first line", startFrame: 0, endFrame: 30 }],
}).project;
projectSchema.parse(project); // a valid Morpha project
dispatch.<tool>(project, 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.
Captions
import { transcriptToCaptionLines, buildCaptionsForClip } from "morpha-studio-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, CaptionLine, TranscriptWordLike, BlankProjectOpts.
SDK vs MCP
Use the SDK for scripts and agents you write in JS/TS — it's typed, ergonomic, and renders frames. 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.