OpenAI Agents SDK
The OpenAI Agents SDK talks to streamable HTTP MCP servers directly, which is exactly what Galley serves. No adapter, no proxy, no local process.
https://mcp.galleyrender.com/mcpPython
Section titled “Python”pip install openai-agentsA complete script. It connects, runs one agent turn that produces a PDF, and prints the signed URL.
import asyncioimport os
from agents import Agent, Runnerfrom agents.mcp import MCPServerStreamableHttp
def galley_headers() -> dict[str, str]: """No key? Send nothing and land on the 50-render keyless trial.""" key = os.environ.get("GALLEY_API_KEY") if key: return {"X-Galley-Api-Key": key} # Optional, but recommended on the trial: a stable identity so you keep the # same 50 renders across restarts and IP changes. return {"X-Galley-Client-Id": os.environ.get("GALLEY_CLIENT_ID", "agents-sdk-demo-7f3c9a21")}
INSTRUCTIONS = """\You produce real document files with the Galley tools.
Before rendering a template you have not used, call get_template and read its JSON Schema — it isthe contract for `data`. Use validate_data to dry-run a payload; it is free. Always pin thetemplate version (invoice@1, not invoice) in anything you produce. Return the signed URL."""
async def main() -> None: async with MCPServerStreamableHttp( name="galley", params={ "url": "https://mcp.galleyrender.com/mcp", "headers": galley_headers(), "timeout": 60, }, cache_tools_list=True, max_retry_attempts=3, ) as galley: agent = Agent( name="Document agent", instructions=INSTRUCTIONS, mcp_servers=[galley], )
result = await Runner.run( agent, "Invoice Acme Robotics for one month of the Starter plan at $19 and 3.2 units of " "overage at $4, 7% tax, issued 2026-09-16 and due 2026-10-16. Render it as a PDF " "with the invoice starter template and give me the URL.", ) print(result.final_output)
if __name__ == "__main__": asyncio.run(main())export OPENAI_API_KEY=sk-…# Leave GALLEY_API_KEY unset for the keyless trial.python galley_agent.pyThe agent loop is Runner.run: it lists the Galley tools, calls get_template, assembles the
payload, calls render, and comes back with the URL in final_output.
Constructor options worth setting
Section titled “Constructor options worth setting”| Option | Why |
|---|---|
params["url"] | https://mcp.galleyrender.com/mcp. |
params["headers"] | Your key or client id. Omit entirely for the keyless trial. |
params["timeout"] | Seconds. 60 is comfortable: a cold Chromium PDF is the slowest thing here. |
cache_tools_list=True | The tool list is static. Caching it saves a round trip per run. |
max_retry_attempts=3 | Covers a transient network blip without your agent seeing it. |
tool_filter | Narrow the surface, e.g. to ["list_templates", "get_template", "validate_data", "render"] if the agent should not be able to publish templates. |
Long-running services
Section titled “Long-running services”async with opens and closes the connection around one block. In a service, construct the server
once at startup, await galley.connect(), keep it on the app state, and await galley.cleanup()
on shutdown. The Galley endpoint itself is stateless — there is no session to keep alive — so a
dropped connection costs nothing but a reconnect.
Streaming
Section titled “Streaming”Runner.run_streamed works the same way; Galley tools resolve in a single request/response, so
each one surfaces as one tool-call event rather than a stream of partials.
TypeScript
Section titled “TypeScript”npm install @openai/agents zodimport { Agent, run, MCPServerStreamableHttp } from '@openai/agents';
function galleyHeaders(): Record<string, string> { const key = process.env.GALLEY_API_KEY; if (key) return { 'X-Galley-Api-Key': key }; // Keyless trial: 50 renders. A stable client id keeps them across restarts. return { 'X-Galley-Client-Id': process.env.GALLEY_CLIENT_ID ?? 'agents-js-demo-7f3c9a21' };}
const instructions = `You produce real document files with the Galley tools.
Before rendering a template you have not used, call get_template and read its JSON Schema — it isthe contract for \`data\`. Use validate_data to dry-run a payload; it is free. Always pin thetemplate version (og-card@1, not og-card). Return the signed URL.`;
async function main() { const galley = new MCPServerStreamableHttp({ url: 'https://mcp.galleyrender.com/mcp', name: 'galley', cacheToolsList: true, requestInit: { headers: galleyHeaders() }, });
const agent = new Agent({ name: 'Document agent', instructions, mcpServers: [galley], });
try { await galley.connect(); const result = await run( agent, 'Make a 1200x630 OG image at 2x scale titled "Deterministic PDFs for agents" with the ' + 'subtitle "Same input, same file, every time", using the og-card starter template.', ); console.log(result.finalOutput); } finally { await galley.close(); }}
main().catch(console.error);export OPENAI_API_KEY=sk-…npx tsx galley-agent.tsHeaders go in requestInit, which is passed through to fetch — there is no top-level headers
option on the TypeScript class. Other options that map across:
| Python | TypeScript |
|---|---|
params["url"] | url |
params["headers"] | requestInit: { headers } |
params["timeout"] (seconds) | timeout (milliseconds) |
cache_tools_list | cacheToolsList |
tool_filter | toolFilter |
If you prefer automatic cleanup, the SDK also supports await using:
import { MCPServerStreamableHttp, connectMcpServers } from '@openai/agents';
const servers = [ new MCPServerStreamableHttp({ url: 'https://mcp.galleyrender.com/mcp', name: 'galley' }),];await using mcpServers = await connectMcpServers(servers);Keyless trial versus a real key
Section titled “Keyless trial versus a real key”| Keyless trial | API key | |
|---|---|---|
| Headers to send | none, or X-Galley-Client-Id: <8–200 chars> | X-Galley-Api-Key: glr_sk_… |
| Limit | 50 renders, lifetime | 200 renders a month free, then pay as you go |
| Identity | Your client id, or a salted hash of IP and User-Agent | The account the key belongs to |
| Survives a network change | Only with X-Galley-Client-Id | Yes |
| Starter library loaded | Yes | Yes |
Start keyless. When the agent hits quota_exceeded, it can call create_account itself:
result = await Runner.run( agent, "Call create_account with dev@example.com to lift the render limit, and tell me what to do next.",)The tool emails a verification link and returns pending_verification. After a human clicks it,
calling create_account again with the same email returns the API key — once. Put it in
GALLEY_API_KEY and the header switches over on the next run. The trial is upgraded in place, so
templates and renders made during it are kept.