Skip to content

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/mcp
Terminal window
pip install openai-agents

A complete script. It connects, runs one agent turn that produces a PDF, and prints the signed URL.

galley_agent.py
import asyncio
import os
from agents import Agent, Runner
from 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 is
the contract for `data`. Use validate_data to dry-run a payload; it is free. Always pin the
template 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())
Terminal window
export OPENAI_API_KEY=sk-…
# Leave GALLEY_API_KEY unset for the keyless trial.
python galley_agent.py

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

OptionWhy
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=TrueThe tool list is static. Caching it saves a round trip per run.
max_retry_attempts=3Covers a transient network blip without your agent seeing it.
tool_filterNarrow the surface, e.g. to ["list_templates", "get_template", "validate_data", "render"] if the agent should not be able to publish templates.

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.

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.

Terminal window
npm install @openai/agents zod
galley-agent.ts
import { 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 is
the contract for \`data\`. Use validate_data to dry-run a payload; it is free. Always pin the
template 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);
Terminal window
export OPENAI_API_KEY=sk-…
npx tsx galley-agent.ts

Headers 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:

PythonTypeScript
params["url"]url
params["headers"]requestInit: { headers }
params["timeout"] (seconds)timeout (milliseconds)
cache_tools_listcacheToolsList
tool_filtertoolFilter

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 trialAPI key
Headers to sendnone, or X-Galley-Client-Id: <8–200 chars>X-Galley-Api-Key: glr_sk_…
Limit50 renders, lifetime200 renders a month free, then pay as you go
IdentityYour client id, or a salted hash of IP and User-AgentThe account the key belongs to
Survives a network changeOnly with X-Galley-Client-IdYes
Starter library loadedYesYes

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.