Skip to content

The MCP server

Galley Render runs a hosted Model Context Protocol server in front of the API:

https://mcp.galleyrender.com/mcp

Ten 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.

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.

One curl, no initialize, no API key
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." }
}
}
}'
Response
{
"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:

Terminal window
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.

ToolWhat it doesBilled
list_templatesTemplates on the account with their latest version. A new account already has the 22-template starter library.no
get_templateOne version in full: JSON Schema, default options, example payload, HTML source. include_source: false trims the source.no
create_templateCreate a template at version 1 from HTML, Liquid and a JSON Schema.no
update_templatePublish a new immutable version. Earlier versions keep rendering.no
validate_dataDry-run a payload against the schema. Same field errors a render would give. Renders nothing.no
renderTemplate 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_renderStatus by id, plus a freshly signed URL. Never re-renders.no
list_rendersRecent renders, newest first, with a signed URL for each.no
usagePeriod usage, billable units by format, cost, free-tier balance, spend cap, trial balance.no
create_accountEmail → 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.

  1. list_templates — see what exists.
  2. get_template — read the JSON Schema for the one you picked. Do this before rendering an unfamiliar template; the schema is the contract for data.
  3. validate_data — optional, free. Dry-run the payload and get field-level errors.
  4. render — get the file.

If nothing fits, create_template with your own HTML, then render it.

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.

Three ways, checked in this order.

OrderHeaderIdentity
1Authorization: Bearer glr_sk_…, X-Galley-Api-Key: glr_sk_… or X-Api-Key: glr_sk_…A real account, or a trial token you kept.
2X-Galley-Client-Id: <opaque, 8–200 chars>A stable trial identity you choose. Survives an IP change.
3nothingA trial identity derived from a salted SHA-256 of the client IP and User-Agent.

Two details worth knowing:

  • A Authorization: Bearer value that does not start with glr_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 a 401.
  • In case 3 the IP is hashed with a server-side salt. No raw address is stored against an account or sent on.

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.

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:

Carry the token you were given
-H "X-Galley-Api-Key: glr_sk_…"
Or declare a stable client id up front
-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.

// 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.

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 whenUse 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.

ClientPage
Claude CodeClaude Code
CursorCursor
OpenAI Agents SDKOpenAI Agents SDK
Anything elsePoint 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.