Get started
Quick start
From a key to an answer drawn in your page, in about ten minutes. One route on your server, a few lines in your page.
You need a MorphScreen key, a server that can make an HTTP request (Node.js and Python are shown), and a page that can import an npm package.
Get a key
Sign up at app.morphscreen.com with your email or GitHub. Your first key is shown once on the welcome page. Copy it into your server's environment:
MORPHSCREEN_KEY=ms_live_...The free plan has 100 calls on our model, so you don't need a model key to try it, then up to 3,000 calls a month on your own OpenRouter key.
Try a call
The API speaks OpenAI's chat completions. This streams an answer from the model included in your plan:
curl https://api.morphscreen.com/v1/chat/completions \
-H "X-MorphScreen-Key: $MORPHSCREEN_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "deepseek/deepseek-v4.1-flash", "stream": true,
"messages": [{"role": "user", "content": "Plan a weekend in Lisbon"}]}'Each event's delta.content is the next piece of the answer, in our format: Markdown with a few tags.
Add a route to your server
Your page talks to your server, and your server talks to MorphScreen with the key. One route carries the page's runs on to /v1/ui 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(${</span><span class="t1">API</span><span class="t3">}</span><span class="t1">/ui</span><span class="t3">, {
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);
The same route in Python, with only the standard library:
# Your server's one route for the page, with nothing but the standard
# library: 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 json
import os
import urllib.error
import urllib.request
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServerAPI = os.environ.get("MORPHSCREEN_API", "https://api.morphscreen.com/v1")
MODEL = os.environ.get("MODEL", "deepseek/deepseek-v4.1-flash")
class Agent(BaseHTTPRequestHandler):
def do_POST(self):
if self.path != "/api/agent":
self.send_error(404)
return
run = json.loads(self.rfile.read(int(self.headers["Content-Length"])))
run["forwardedProps"] = {**run.get("forwardedProps", {}), "model": MODEL}
req = urllib.request.Request(
f"{API}/ui",
data=json.dumps(run).encode(),
headers={"Content-Type": "application/json", "X-MorphScreen-Key": os.environ.get("MORPHSCREEN_KEY", "")},
method="POST",
)
try:
upstream = urllib.request.urlopen(req)
except urllib.error.HTTPError as e:
upstream = e
with upstream:
self.send_response(upstream.status)
self.send_header("Content-Type", upstream.headers.get("Content-Type", "text/event-stream"))
self.end_headers()
while chunk := upstream.read1(4096):
self.wfile.write(chunk)
self.wfile.flush()
ThreadingHTTPServer(("127.0.0.1", int(os.environ.get("PORT", "3000"))), Agent).serve_forever()
Install the web SDK
npm install @morphscreen/webpnpm add @morphscreen/webyarn add @morphscreen/webbun add @morphscreen/webBefore the launch
The package goes to npm with the public launch. Until then, /v1/render draws answers as HTML on your server (HTML without JavaScript), and everything on this page works the same once it's published.
Draw the conversation
connectAgent holds the conversation: it sends what the reader types, draws each answer as it streams, and sends buttons, forms and quiz answers back as the next turn.
<div id="chat"></div>
<form>
<input name="message" placeholder="Ask anything" aria-label="Your message" required>
<button>Send</button>
</form>import { connectAgent, loadCore } from "@morphscreen/web";
import "@morphscreen/web/morphscreen.css";// morphscreen-agent.wasm from the package, served with your page
const core = await loadCore("/morphscreen-agent.wasm");
const chat = connectAgent(document.querySelector<HTMLElement>("#chat")!, { core, url: "/api/agent" });
const form = document.querySelector("form")!;
form.addEventListener("submit", async (e) => {
e.preventDefault();
const input = form.querySelector("input")!;
const text = input.value;
input.value = "";
await chat.send(text);
});
Serve morphscreen-agent.wasm from the package with your page. Each integration shows how for its build tool.
That's a working chat. Ask it for a trip, a dashboard or a quiz, and the answer arrives as cards, charts and questions, checked line by line.