The MCP server
Galley Render runs a hosted Model Context Protocol server in front of the API:
https://mcp.galleyrender.com/mcpTen tools, no authentication required to start. The first call with no API key mints a 50-render trial and returns its token in the response. There is no signup, no card, and no human in the loop — an agent that has never heard of Galley can go from cold start to a signed PDF URL in one tool call.
Transport
Section titled “Transport”Streamable HTTP, and stateless: the server generates no session id and holds nothing between requests. A fresh MCP server and transport are built per HTTP request, which is what lets any instance answer any request.
enableJsonResponse is on, so a tool call comes back as an ordinary JSON body rather than an SSE
stream — every tool here is one request and one response, with no progress notifications. Your
client still has to send accept: application/json, text/event-stream, because the protocol
requires it, but what comes back is JSON.
The practical consequence: there is no handshake to perform. A single tools/call POST works
on its own.
curl -sS https://mcp.galleyrender.com/mcp \ -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -d '{ "jsonrpc": "2.0", "id": 1, "method": "tools/call", "params": { "name": "render", "arguments": { "template": "og-card", "format": "png", "data": { "title": "Documents for agents", "subtitle": "JSON in, PDF out." } } } }'{ "jsonrpc": "2.0", "id": 1, "result": { "content": [ { "type": "text", "text": "{\n \"object\": \"render\",\n \"id\": \"rnd_7hq2m4x8k1bv\",\n \"status\": \"succeeded\",\n \"template\": \"og-card@1\",\n \"format\": \"png\",\n \"cached\": false,\n \"url\": \"https://<account>.r2.cloudflarestorage.com/galley-renders/renders/…?X-Amz-Expires=3600&X-Amz-Signature=…\",\n \"page_count\": 1,\n \"billable_units\": 1,\n \"trial\": {\n \"mode\": \"keyless_trial\",\n \"renders_limit\": 50,\n \"renders_remaining\": 49,\n \"trial_token\": \"glr_sk_…\"\n }\n}" } ] }}Tool results are JSON encoded as a text content block — that is how MCP carries structured
results. Parse result.content[0].text as JSON.
To see the tool list instead:
curl -sS https://mcp.galleyrender.com/mcp \ -H 'content-type: application/json' \ -H 'accept: application/json, text/event-stream' \ -d '{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{}}'Clients that do run initialize first are handled normally; there is just no session id to carry
forward afterwards.
The ten tools
Section titled “The ten tools”| Tool | What it does | Billed |
|---|---|---|
list_templates | Templates on the account with their latest version. A new account already has the 22-template starter library. | no |
get_template | One version in full: JSON Schema, default options, example payload, HTML source. include_source: false trims the source. | no |
create_template | Create a template at version 1 from HTML, Liquid and a JSON Schema. | no |
update_template | Publish a new immutable version. Earlier versions keep rendering. | no |
validate_data | Dry-run a payload against the schema. Same field errors a render would give. Renders nothing. | no |
render | Template plus data → a signed URL for a PDF, PNG or JPG. | yes — 1 unit per PNG/JPG, 1 per PDF page. Cache hits free. |
get_render | Status by id, plus a freshly signed URL. Never re-renders. | no |
list_renders | Recent renders, newest first, with a signed URL for each. | no |
usage | Period usage, billable units by format, cost, free-tier balance, spend cap, trial balance. | no |
create_account | Email → verification link → API key. Upgrades the trial in place. | no |
render is the only tool that costs anything, and a cache hit — identical template version, data
and options — returns the stored object with cached: true for free. Nine of the ten tools are
free to call as often as you like, so there is no reason for an agent to guess at a schema.
The shape of a job
Section titled “The shape of a job”list_templates— see what exists.get_template— read the JSON Schema for the one you picked. Do this before rendering an unfamiliar template; the schema is the contract fordata.validate_data— optional, free. Dry-run the payload and get field-level errors.render— get the file.
If nothing fits, create_template with your own HTML, then render it.
Errors
Section titled “Errors”Tool failures come back as isError: true with the API’s error body as the text content,
unrewritten. Every error carries a stable type, a docs_url and — for validation — the field
path, the expected type, what was received and a value that would be accepted. Where it helps, a
next_step is added alongside. Fix the payload from the error and retry rather than guessing.
How you are identified
Section titled “How you are identified”Three ways, checked in this order.
| Order | Header | Identity |
|---|---|---|
| 1 | Authorization: Bearer glr_sk_…, X-Galley-Api-Key: glr_sk_… or X-Api-Key: glr_sk_… | A real account, or a trial token you kept. |
| 2 | X-Galley-Client-Id: <opaque, 8–200 chars> | A stable trial identity you choose. Survives an IP change. |
| 3 | nothing | A trial identity derived from a salted SHA-256 of the client IP and User-Agent. |
Two details worth knowing:
- A
Authorization: Bearervalue that does not start withglr_sk_is ignored rather than forwarded, so an agent already carrying an OAuth token for something else still lands on the trial instead of getting a401. - In case 3 the IP is hashed with a server-side salt. No raw address is stored against an account or sent on.
The keyless trial
Section titled “The keyless trial”The first tool call without a key mints a trial account and returns its token alongside the result:
{ "trial": { "mode": "keyless_trial", "renders_limit": 50, "renders_remaining": 49, "trial_token": "glr_sk_…", "keep_this_token": "Send it back as the `X-Galley-Api-Key` header (or `Authorization: Bearer`) on later MCP requests, and it works as an ordinary API key against https://api.galleyrender.com too.", "to_remove_the_limit": "Call `create_account` with an email. The trial is upgraded in place, so these templates and renders are kept." }}The trial account is a real account. It holds templates, renders and usage like any other, and it is created with the starter library already loaded. The ceiling is 50 renders total — a lifetime count, not a monthly one.
Keeping the same trial
Section titled “Keeping the same trial”The derived identity in case 3 is a hash of IP plus User-Agent, which changes when your network does. To stay on one trial across restarts and networks, do either of these:
-H "X-Galley-Api-Key: glr_sk_…"-H "X-Galley-Client-Id: my-agent-7f3c9a21"Any opaque string from 8 to 200 characters works as a client id. Pick one per deployment, not one per process.
Lifting the limit
Section titled “Lifting the limit”// create_account{ "email": "dev@example.com" }A verification link goes to that address and the tool returns status: "pending_verification".
Once the link has been clicked, call create_account again with the same email and it returns the
API key — once. Store it as GALLEY_API_KEY.
The trial is upgraded in place: templates and renders made during it are kept. The free tier is 200 renders a month; see pricing beyond that. The trial keeps working while you wait for the click, so there is no need to stop mid-task.
When the 50 are gone, render returns quota_exceeded (402) and the fix is create_account.
MCP or REST?
Section titled “MCP or REST?”Both surfaces speak to the same API, the same accounts and the same renders. A trial token minted
over MCP is a working API key against https://api.galleyrender.com.
| Use MCP when | Use REST when |
|---|---|
| A model is deciding what to render. | Your code already knows what to render. |
| You want tool descriptions and JSON Schemas in the model’s context. | You want a typed client and no protocol overhead. |
| You want the zero-setup trial. | You have an API key and a deploy pipeline. |
| You are inside Claude Code, Cursor, or an agent framework. | You are inside a job queue, a cron, or a web request handler. |
Discovery matters — list_templates, get_template, validate_data. | Throughput matters — POST /v1/render/batch, webhooks. |
A common split: MCP while a human and a model design the template together, REST in the service that renders it ten thousand times a day, with the version pinned.
Connecting a client
Section titled “Connecting a client”| Client | Page |
|---|---|
| Claude Code | Claude Code |
| Cursor | Cursor |
| OpenAI Agents SDK | OpenAI Agents SDK |
| Anything else | Point it at https://mcp.galleyrender.com/mcp as a streamable HTTP server. |
If your agent framework does not speak MCP, hand the model the skill file instead — the whole product on one page, written to be read by a model.