On your server
Node.js
Call MorphScreen from a Node.js server with nothing but fetch: carry a page's AG-UI runs, stream an answer's text, or render a finished answer as HTML.
Node 18 or later has everything this needs. Keep MORPHSCREEN_KEY in the server's environment; it never goes to the page.
A route for the page
The page's connectAgent sends its runs to your route, which adds the key and the model and streams the events back:
// Your server's one route for the page: each AG-UI run goes on to MorphScreen
// with your key, and the events stream straight back. The key never reaches
// the browser, and the server, not the page, picks the model.
import { createServer } from "node:http";
const API = process.env.MORPHSCREEN_API ?? "https://api.morphscreen.com/v1";
const MODEL = process.env.MODEL ?? "deepseek/deepseek-v4.1-flash";
createServer(async (req, res) => {
if (req.method !== "POST" || req.url !== "/api/agent") return res.writeHead(404).end();
let body = "";
for await (const chunk of req) body += chunk;
const run = JSON.parse(body);
run.forwardedProps = { ...run.forwardedProps, model: MODEL };
const upstream = await fetch(`${API}/ui`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-MorphScreen-Key": process.env.MORPHSCREEN_KEY },
body: JSON.stringify(run),
});
res.writeHead(upstream.status, { "Content-Type": upstream.headers.get("content-type") ?? "text/event-stream" });
for await (const chunk of upstream.body) res.write(chunk);
res.end();
}).listen(process.env.PORT ?? 3000);In Express, Fastify or Hono, the route body is the same: read the run, set forwardedProps.model, fetch MorphScreen's /v1/ui, and pipe the response body back with its status and content type.
The answer's text, or HTML
For work on the server (a mail, a saved report, a page drawn without JavaScript), stream the answer's text from /v1/chat/completions, or render a finished answer with /v1/render:
// An answer's text as it streams, and a finished answer as HTML. Run it on
// your server (Node 18 or later): the key stays there.
const API = process.env.MORPHSCREEN_API ?? "https://api.morphscreen.com/v1";
const HEADERS = { "Content-Type": "application/json", "X-MorphScreen-Key": process.env.MORPHSCREEN_KEY };
/** Yields the answer's text, piece by piece, as the model writes it. */
export async function* streamAnswer(messages, model = "deepseek/deepseek-v4.1-flash") {
const res = await fetch(`${API}/chat/completions`, { method: "POST", headers: HEADERS, body: JSON.stringify({ model, stream: true, messages }) });
if (!res.ok) throw new Error(`MorphScreen ${res.status}: ${await res.text()}`);
const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of res.body) {
buffer += decoder.decode(chunk, { stream: true });
let end;
while ((end = buffer.indexOf("\n")) >= 0) {
const line = buffer.slice(0, end).trim();
buffer = buffer.slice(end + 1);
if (!line.startsWith("data: ") || line === "data: [DONE]") continue;
for (const choice of JSON.parse(line.slice(6)).choices ?? []) if (choice.delta?.content) yield choice.delta.content;
}
}
}
/** A finished answer drawn by the core as HTML, with its verdict. */
export async function renderHtml(text) {
const res = await fetch(`${API}/render`, { method: "POST", headers: HEADERS, body: JSON.stringify({ text }) });
if (!res.ok) throw new Error(`MorphScreen ${res.status}: ${await res.text()}`);
const { html, morphscreen } = await res.json();
return { html, verdict: morphscreen };
}import { renderHtml, streamAnswer } from "./morphscreen.mjs";
let text = "";
for await (const piece of streamAnswer([{ role: "user", content: "Make me a sales dashboard" }])) text += piece;
const { html, verdict } = await renderHtml(text);Both are what our tests run against a recorded answer, in Node and in Python.