Packages and templates share assemble, scheduler, then press. Bearer POST /v1/render or OAuth on /mcp. The press only compiles typst-package.
OAuth agents call https://pdf.railman.io/mcp with the same package and template shapes as POST /v1/render. A third client path, not a third press.
https://pdf.railman.io/mcp
Streamable HTTP MCP with OAuth. Agents discover the authorization server from the protected-resource challenge — not from an API key. Sign-in uses magic link (Cloudflare Email Sending). Same free-tier quota and wallet credits as /v1/render.
Discovery
https://pdf.railman.io/.well-known/oauth-authorization-server
OAuth authorization server metadata. Issuer is https://pdf.railman.io; basePath /api/auth.
Discovery
https://pdf.railman.io/.well-known/oauth-protected-resource
Protected resource metadata for this MCP endpoint.
Point the client at the URL. OAuth + PKCE handles consent (/consent). There is no static client admin UI — dynamic registration and CIMD are the doors. CIMD metadata is fetched through the isolated egress worker.
Cursor mcp.json
{
"mcpServers": {
"pdf-railman": {
"url": "https://pdf.railman.io/mcp"
}
}
}Claude Desktop / Claude Code
{
"mcpServers": {
"pdf-railman": {
"type": "http",
"url": "https://pdf.railman.io/mcp"
}
}
}Codex config.toml
[mcp_servers.pdf-railman] url = "https://pdf.railman.io/mcp" # Then: codex mcp login pdf-railman
OpenCode opencode.json
{
"mcp": {
"servers": {
"pdf-railman": {
"type": "remote",
"url": "https://pdf.railman.io/mcp"
}
}
}
}Antigravity mcp_config.json
{
"mcpServers": {
"pdf-railman": {
"serverUrl": "https://pdf.railman.io/mcp"
}
}
}MCP Inspector
npx @modelcontextprotocol/inspector https://pdf.railman.io/mcp
Narrowing is allowed; widening returns invalid_target. Tools without a listed scope are open to any authenticated MCP session.
pdf:render
Create PDF jobs (`render_pdf`, `render_template`). Same quota and wallet as API keys.
pdf:read
Read your jobs, catalogs, and usage (`get_job`, `list_jobs`, `list_templates`, `list_library`, `get_usage`).
Render tools mirror the HTTP envelope. Jobs from MCP stamp caller_kind=mcp. Successful renders may return pdfBase64 in-band — never storage paths. Dashboard Fill is a session path; this door runs the API transformer.
| Tool | Scope | Behavior |
|---|---|---|
render_pdf | pdf:render | Compile a bounded Typst package into a PDF job. Same body as POST /v1/render with kind typst-package. |
render_template | pdf:render | Compile a published template ref with JSON data. Assembled in web (transformer runs); press still compiles typst-package. |
list_templates | pdf:read | List your published templates (drafts never appear). |
list_library | pdf:read | List public library templates (e.g. railman/invoice). |
get_job | pdf:read | Read one of your PDF jobs (public shape only — never r2Key or input snapshots). |
list_jobs | pdf:read | List your recent PDF jobs. |
get_usage | pdf:read | Read remaining credits, UTC period, and debit totals for the authenticated caller. Same numbers as GET /v1/usage. |
get_fonts | — | List available font profiles and license notes. |
After OAuth, tools/call arguments match the public render shapes. wait defaults true (up to 25s), same as HTTP.
tools/call · render_template
{
"jsonrpc": "2.0",
"id": 1,
"method": "tools/call",
"params": {
"name": "render_template",
"arguments": {
"template": "railman/invoice@1",
"data": {
"client": {
"address": "88 Market Street, San Francisco, CA",
"email": "ap@acme.example",
"name": "Acme Studio"
},
"currency": "USD",
"dueDate": "2026-09-16",
"invoiceNumber": "INV-1042",
"issueDate": "2026-08-17",
"items": [
{
"description": "Typst press retainer",
"quantity": 1,
"unitPrice": 1200
},
{
"description": "Extra compile hours",
"quantity": 4,
"unitPrice": 85
}
],
"notes": "Net 30. Wire details on file.",
"seller": {
"address": "Railman, SNRK",
"email": "billing@railman.io",
"name": "PDF Railman"
},
"shareUrl": "https://pdf.railman.io/library/railman/invoice",
"taxRate": 0
},
"wait": true
}
}
}tools/call · render_pdf
{
"jsonrpc": "2.0",
"id": 2,
"method": "tools/call",
"params": {
"name": "render_pdf",
"arguments": {
"kind": "typst-package",
"package": {
"main": "main.typ",
"files": [
{
"path": "main.typ",
"content": "= Hello\n#let data = json(\"data.json\")\n#data.title\n"
},
{
"path": "data.json",
"content": "{\"title\":\"Q3\"}"
}
]
},
"wait": true
}
}
}tools/call · get_usage
{
"jsonrpc": "2.0",
"id": 3,
"method": "tools/call",
"params": {
"name": "get_usage",
"arguments": {}
}
}Bearer /v1/* still uses API keys — mint those on Keys.
Session Fill on the dashboard skips the transformer and downloads via a signed link. MCP/API run the transformer for U-JSON → S-JSON.
Credits debit in the scheduler only. Publish a template before render_template can resolve it.
Jobs history (any caller) lives on Jobs.