On your server
Python
Call MorphScreen from Python with only the standard library: carry a page's AG-UI runs, stream an answer's text, or render a finished answer as HTML.
Nothing to install: these use only Python's standard library. 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, 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, ThreadingHTTPServer
API = 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()In FastAPI, Flask or Django the route does the same: read the run, set forwardedProps["model"], POST it to MorphScreen's /v1/ui, and stream the response body back with its status and content type, without buffering it.
The answer's text, or HTML
# An answer's text as it streams, and a finished answer as HTML, with nothing
# but the standard library. Run it on your server: the key stays there.
import json
import os
import urllib.request
API = os.environ.get("MORPHSCREEN_API", "https://api.morphscreen.com/v1")
HEADERS = {"Content-Type": "application/json", "X-MorphScreen-Key": os.environ.get("MORPHSCREEN_KEY", "")}
def stream_answer(messages, model="deepseek/deepseek-v4.1-flash"):
"""Yields the answer's text, piece by piece, as the model writes it."""
body = json.dumps({"model": model, "stream": True, "messages": messages}).encode()
req = urllib.request.Request(f"{API}/chat/completions", data=body, headers=HEADERS, method="POST")
with urllib.request.urlopen(req) as res:
for line in res:
line = line.decode().strip()
if not line.startswith("data: ") or line == "data: [DONE]":
continue
for choice in json.loads(line[6:]).get("choices", []):
text = (choice.get("delta") or {}).get("content")
if text:
yield text
def render_html(text):
"""A finished answer drawn by the core as HTML, with its verdict."""
req = urllib.request.Request(f"{API}/render", data=json.dumps({"text": text}).encode(), headers=HEADERS, method="POST")
with urllib.request.urlopen(req) as res:
out = json.load(res)
return out["html"], out["morphscreen"]from morphscreen import render_html, stream_answer
text = "".join(stream_answer([{"role": "user", "content": "Make me a sales dashboard"}]))
html, verdict = render_html(text)Both are what our tests run against a recorded answer.
With OpenAI's SDK
On your own OpenRouter key, OpenAI's Python SDK works with a new base_url and the X-MorphScreen-Key header (OpenAI-compatible clients). For the model in your plan, use the functions above: any Authorization header is taken as an OpenRouter key.