Returning images & artifacts¶
Console output is text-only. Artifacts are how sandboxed code hands typed
payloads — images, audio, CSVs, arbitrary binary — back to the MCP client.
Images are the headline use case: the MCP spec defines an ImageContent tool
result block (base64 data + mimeType), and image/* artifacts are returned
as exactly that, so a model connected through MCP can see the image rather
than read about it.
Store an artifact from JavaScript¶
Call the artifact(key, mime, bytes) global anywhere in your code:
const png = renderChart(); // Uint8Array of PNG bytes
artifact("chart", "image/png", png);
key— caller-chosen identifier (≤ 256 bytes). Writing the same key again overwrites it.mime— atype/subtypemime type, e.g.image/png,text/csv,application/octet-stream.bytes— aUint8Array, any TypedArray, anArrayBuffer, or a string (UTF-8 encoded). Max 16 MiB per artifact.
Invalid arguments throw a TypeError/Error you can catch in JS. Artifacts
are stored in the server's execution database, so they persist across
executions (and server restarts) and are shared across sessions — keys are a
single global namespace.
Fetch an artifact over MCP¶
get_artifact(key) returns two content blocks: a JSON metadata block
(key, mime_type, size_bytes, created_at, execution_id, encoding)
followed by the payload rendered by mime type:
| Stored mime | MCP content block |
|---|---|
image/* |
ImageContent (base64 data + mimeType — the model sees the image) |
audio/* |
AudioContent (base64 data + mimeType) |
| anything else, valid UTF-8 | TextContent with the raw text |
| anything else, binary | TextContent carrying base64 |
list_artifacts() returns metadata for everything stored.
Discover what an execution produced¶
In async (stateful) mode, a completed execution lists what it emitted in the
artifacts field of get_execution:
{ "tool": "get_execution", "arguments": { "execution_id": "01J9W…" } }
// Response: { "status": "completed", …,
// "artifacts": [ { "key": "chart", "mime_type": "image/png",
// "size_bytes": 48213, … } ] }
Then fetch the payload:
{ "tool": "get_artifact", "arguments": { "key": "chart" } }
In stateless mode, run_js attaches emitted artifacts directly to its own
tool result as content blocks (up to 8 MiB of payloads inline; larger
artifacts are listed in the result JSON with "inline": false and stay
retrievable via get_artifact).
Fetch raw bytes over REST¶
The REST API serves artifact payloads verbatim — no base64 — with the stored
mime type as Content-Type:
curl http://localhost:8080/api/artifacts # metadata list
curl http://localhost:8080/api/artifacts/chart -o chart.png
Upload a file for your code to use¶
Artifacts also carry input. A client uploads a file under a key, and code
running in run_js reads it back — useful when the model has a file (a PDF to
attach to a form, a CSV to process) and the sandbox has no other way to
receive it.
MCP has no streaming or file-upload mechanism, and tool arguments are JSON, so sending a file through a tool call would mean the model emitting every byte as base64. Uploads therefore go out-of-band: the tool hands out a URL and the bytes travel over plain HTTP.
Upload with a one-time URL¶
get_artifact_upload_url returns a URL the client can PUT the raw file to:
{ "tool": "get_artifact_upload_url",
"arguments": { "key": "form.pdf", "mime_type": "application/pdf" } }
// Response: { "key": "form.pdf", "method": "PUT",
// "url": "https://mcp.example.com/api/artifact-uploads/3f9c…",
// "path": "/api/artifact-uploads/3f9c…",
// "expires_at": "…", "max_bytes": 16777216, "single_use": true }
curl -fsS -T ./form.pdf 'https://mcp.example.com/api/artifact-uploads/3f9c…'
- The token in the URL is the only credential: the route is not behind bearer auth, so a sandbox with no access token can use it. Treat the URL like a password until it is used.
- A URL works once and expires (
expires_in_secs, default 600, max 3600). mime_typeis optional; without it the upload'sContent-Typeis stored (application/octet-streamif there is none).- The server only knows its own public address if you tell it: set
--public-url(MCP_V8_PUBLIC_URL), e.g.https://mcp.example.com. Without it the tool returns justpath, to be appended to whatever origin the client reaches the server at.
Upload with your own credentials¶
A client that already holds the server's credentials can skip the URL step and
PUT the raw bytes directly; the request's Content-Type becomes the mime
type:
curl -X PUT --data-binary @form.pdf -H 'Content-Type: application/pdf' \
http://localhost:8080/api/artifacts/form.pdf
Read the upload in JavaScript¶
const file = artifact.get("form.pdf"); // null if the key doesn't exist
console.log(file.mime_type, file.size_bytes);
const bytes = file.bytes; // Uint8Array
artifact.list(); // [{ key, mime_type, size_bytes, created_at }, …]
The same limits apply as for artifact(): 16 MiB per artifact, keys ≤ 256
bytes, and the same key overwrites. Uploads share the one global key
namespace with artifacts written from JavaScript.
Sizing images for models¶
Model providers cap image inputs (Claude, for example, rejects images over
~5 MB or 8000×8000 px, and tokens scale with pixel count), and base64 inflates
payloads by ~33%. Downscale or compress in JS before calling artifact() —
a chart rarely needs to be wider than ~1500 px.