MCP Server
oJo ships an official Model Context Protocol server, so
an AI agent can design and render images directly instead of you writing HTTP calls on its
behalf. It is published to npm as
@ojodotso/mcp-server, runs locally
over stdio, and talks to the same public API documented on these pages — there is nothing
extra to host.
The server authenticates with your oJo API key. Create one in API settings .
The point of using it over the raw API is the design loop: an agent can iterate on a design at full resolution for free, and spend a credit only on the frame that is actually right.
Install
Requires Node.js 20 or newer. You do not need to install anything by hand — npx fetches
the package on first run.
Claude Code
claude mcp add ojo -s user -e OJO_API_KEY=<your API key> -- npx -y @ojodotso/mcp-server-s user makes the server available in all your projects; drop it to scope the server to
the current project only. To share it with your team through version control, use
-s project — that writes to .mcp.json, so reference the key as ${OJO_API_KEY} and
keep the real value in your environment.
Restart your client after adding the server. It should then list six oJo tools.
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
OJO_API_KEY | Yes | — | Your oJo API key. The server refuses to start without it. |
OJO_API_BASE_URL | No | https://api.ojo.so/v1 | Point the server at a different oJo API — see Self Hosting. |
OJO_WEB_BASE_URL | No | https://ojo.so | Base URL used to build preview links. Change it if you serve the oJo web app from your own domain. |
OJO_API_BASE_URL decides where your API key is sent. Only point it at a host you
control.
The design loop
Rendering an image costs a credit; previewing one does not. The tools are built around that asymmetry, and an agent using them well will spend exactly one credit per finished image no matter how many revisions it took.
1. Draft the HTML
The agent writes plain HTML with inline CSS, using {{variable}} placeholders wherever the
content should be swappable. Google Fonts and other https: resources work.
2. Preview it — free, unlimited
create_preview_link returns a URL that renders the draft live, at full resolution,
through the same engine that produces the final PNG:
{
"html": "<div style=\"...\"><h1>{{title}}</h1></div>",
"variables": { "title": "Preview links, zero credits" },
"viewportWidth": 1200,
"viewportHeight": 630
}https://ojo.so/preview#draft=H4sIAAAAAAAAA5VW227jNhD9FYJBuhtAkilZdmTK2Rb...The draft is compressed into the URL fragment, so it never reaches a server and nothing is stored. From here you have two options, and both are free:
- The agent checks its own work — open the link in a headless browser and screenshot
the
iframe[title="oJo draft preview"]element. Give web fonts a moment to load before capturing. - A human checks it — paste the link into a message. Whoever opens it sees exactly what the render will produce, with no oJo account needed.
Fix what’s wrong, call create_preview_link again, repeat. Still zero credits.
3. Render or save it
Once the design is right, spend the credit — or don’t spend one at all:
generate_image_from_htmlrenders the final PNG and returns its public URL. 1 credit.create_templatesaves the HTML as a reusable template instead. No credit. Later,generate_image_from_templaterenders it with per-render values, one credit each.
The generate tools accept inspect: true, which attaches a 512px-wide thumbnail of the
result. It is useful for a quick sanity check, but far too small to judge layout or
typography — that is what the preview link is for.
Tools
| Tool | What it does | Credits |
|---|---|---|
create_preview_link | Build a live, shareable preview URL for a draft | 0 |
generate_image_from_html | Render HTML into a PNG | 1 |
generate_image_from_template | Render a saved template into a PNG | 1 |
create_template | Save HTML as a reusable template | 0 |
list_templates | List the templates on your account | 0 |
get_template | Fetch one template’s HTML and variable defaults | 0 |
create_preview_link
| Parameter | Type | Description |
|---|---|---|
html | string | Required. HTML/Handlebars to preview. Not base64-encoded. |
variables | object | Values to fill the placeholders for this preview. |
viewportWidth | integer | Width in pixels (default 1280). |
viewportHeight | integer | Height in pixels (default 800). |
generate_image_from_html
| Parameter | Type | Description |
|---|---|---|
html | string | Required. HTML with inline CSS. Not base64-encoded. |
viewportWidth | integer | Width in pixels (default 1280). |
viewportHeight | integer | Height in pixels (default 800). |
transparentBackground | boolean | Render on transparency instead of white. |
inspect | boolean | Also return a 512px thumbnail of the result. |
Returns the image id and its public URL. Mirrors Create Image from HTML.
generate_image_from_template
| Parameter | Type | Description |
|---|---|---|
templateId | string | Required. Id of an existing template. |
modify | object | Per-render values, merged over the template’s variable defaults. |
viewportWidth | integer | Width in pixels (default 1280). |
viewportHeight | integer | Height in pixels (default 800). |
transparentBackground | boolean | Render on transparency instead of white. |
inspect | boolean | Also return a 512px thumbnail of the result. |
Mirrors Create Image from Template.
create_template
| Parameter | Type | Description |
|---|---|---|
html | string | Required. HTML/Handlebars with {{variable}} placeholders. |
variables | object | Default values for the placeholders — the template’s shape. |
Mirrors Create Template.
list_templates
| Parameter | Type | Description |
|---|---|---|
page | integer | 1-based page number (default 1). |
pageSize | integer | Templates per page (default 10). |
sort | enum | asc or desc by creation time (default desc). |
get_template
| Parameter | Type | Description |
|---|---|---|
templateId | string | Required. Id of the template to fetch. |
Returns the template’s decoded HTML — you can pass it straight to
create_preview_link to preview a set of modify values before spending a credit on them.
Variables
Two different things share the word “variables”, and the tools keep them apart:
- Template Variables — the defaults you pass to
create_template. They describe the template’s shape: which placeholders exist and what they hold when nothing overrides them. - Modify Payload — the per-render values you pass as
modifytogenerate_image_from_template(or asvariablestocreate_preview_link). They are merged over the defaults for that one render and change nothing on the template.
Templates are rendered with Handlebars, so conditionals, loops and helpers all work — see
Template Helpers for the full list. Two things catch people out:
{{value}} escapes HTML, so use {{{value}}} when a variable contains markup, and
formatDate renders in local time rather than UTC.
Caveats
Restart the client after upgrading. npx -y caches the package rather than re-resolving
it on every run, and a running MCP server keeps whatever version it started with. If a newly
released tool doesn’t appear, find the cached copy:
find ~/.npm/_npx -maxdepth 5 -type d -path '*@ojodotso/mcp-server'Delete the _npx/<hash> directory that contains it — not all of ~/.npm/_npx, which caches
every other tool you run through npx — then restart your MCP client so it spawns a fresh
server.
Errors are returned as readable text, not raised as protocol errors, so the agent can
act on them: a missing or invalid key reports Unauthorized, an exhausted balance reports
Out of credits, and hitting a rate limit reports
Rate limited by the oJo API.
Preview links are self-contained and unverified. Everything in a /preview#draft= link
lives in the URL fragment, which is why nothing is stored and no credit is charged — but it
also means the content is supplied entirely by whoever built the link, not by oJo. The draft
is rendered inside a sandboxed iframe that cannot reach your oJo session, so opening one is
safe in that sense. Still, treat a preview link you received from someone else the way you’d
treat any untrusted web page: it can run its own scripts, and the page it shows is theirs,
not ours.