# PDFEx — full agent reference

> Everything an agent needs in one document: the canonical template-API guide, then the
> per-host MCP install matrix. Generated from `docs/agent-template-guide.md` and
> `src/lib/mcpHosts.ts` — edit those, not this.

---

# PDFEx Template API - Agent Guide

> **This file is the canonical agent contract for PDFEx.** Every other agent-facing
> surface is generated from or tested against it: the MCP resource
> (`pdfex://guide/agent-template-guide`), the public `/llms-full.txt`, the
> crawlable `/docs/agents.md`, and the rendered documentation pages.
>
> Edit this file, not a copy. `src/lib/__tests__/docsContract.test.ts` fails the
> build if the endpoints or tool names below stop matching the implementation.

This guide is written for AI agents (and humans) creating PDF templates programmatically.
Paste it into your agent's context to enable end-to-end template design and PDF generation.

**Public, no key required:** `https://pdfex.io/llms.txt` (index) and
`https://pdfex.io/llms-full.txt` (this guide plus the per-host install matrix).
Anything under `/api/*` and the MCP endpoint needs an API key.

## Authentication

Every request requires an API key in the `Authorization` header:

```
Authorization: Bearer pdfex_<your key>
```

All responses share the envelope `{ "success": boolean, ... }`. Errors include
`"error"` (summary) and often `"details"` (array of field-level messages you can act on).

## Endpoints

| Method | Path | Purpose |
|--------|------|---------|
| GET    | `/api/capabilities` | Everything you are allowed to write: engines, field types, page sizes, limits |
| GET    | `/api/templates` | List templates visible to you |
| POST   | `/api/templates` | Create a template |
| GET    | `/api/templates/:id` | Fetch full template (schema included) |
| PUT    | `/api/templates/:id` | Update a template you own (supports `expected_version`) |
| DELETE | `/api/templates/:id` | Delete a template you own |
| GET    | `/api/templates/:id/export` | Export a template as a portable JSON bundle |
| POST   | `/api/templates/import` | Import templates from a bundle |
| GET    | `/api/templates/samples` | List sample templates, or fetch one full example |
| POST   | `/api/templates/validate` | Validate a template without saving or rendering it |
| POST   | `/api/templates/preview` | Render a schema to PDF without saving |
| POST   | `/api/pdf/token` | Create a PDF generation request from a saved template |
| GET    | `/api/pdf/:token` | Fetch the generated PDF (expires after 15 minutes) |

## MCP server (preferred for agent hosts that speak MCP)

The same authoring surface is exposed over **MCP** (Model Context Protocol), so a host can call it
as tools instead of shelling out to `curl`. The difference that matters: `preview_template`
returns the rendered page as an **image block**, so the agent sees what it built inside its own
context rather than a PDF it cannot inspect.

| | |
|---|---|
| Endpoint | `POST {BASE_URL}/mcp` |
| Auth | The same API key: `Authorization: Bearer <your pdfex key>` — no second credential |
| Transport | Streamable HTTP, **stateless**. `GET`/`DELETE` return 405 by design (no SSE stream is offered) |

Register it with a client that can set headers on the connection:

```json
{
  "mcpServers": {
    "pdfex": {
      "type": "http",
      "url": "https://pdfex.io/mcp",
      "headers": { "Authorization": "Bearer ${PDFEX_API_KEY}" }
    }
  }
}
```

Tools (all nine, and what each is for):

| Tool | Kind | Notes |
|------|------|-------|
| `get_capabilities` | read | Call this first — engines, field types, units, page sizes, your limits |
| `get_samples` | read | No arguments lists the catalogue; `kind` + `engine` together returns one full sample |
| `list_templates` | read | Summaries only, never the schema |
| `get_template` | read | `detail: "summary"` (default) omits the schema; `full` includes it |
| `validate_template` | read | Cheap. Call before previewing or saving |
| `preview_template` | read | Returns page images. Defaults to page 1 — ask for more with `pages` (max 10) |
| `create_template` | write | Counts against your plan's template limit |
| `update_template` | write | Pass `expected_version` from `get_template`; a stale value fails instead of overwriting |
| `delete_template` | write | Destructive. Requires `confirm: true` |

Also exposed:

- **Resource** `pdfex://guide/agent-template-guide` — this document, verbatim. Read it instead of
  guessing at an unusual template.
- **Prompt** `design_template` — the recommended loop (capabilities → sample → draft → validate →
  preview → fix → create) with the conventions that are easy to get wrong.

MCP field names follow the REST contract (`pdfme_schema`, `sample_data` on create/update;
`template`, `sampleData` on validate/preview), so this guide describes both surfaces identically.

## Start here: `GET /api/capabilities`

Call this once before authoring anything. It is the contract for your account, so you never have
to guess a field shape, a unit, a page size, or your plan limit:

```bash
curl -H "Authorization: Bearer ***" {BASE_URL}/api/capabilities
```

```json
{
  "success": true,
  "engines": ["pdfme", "html"],
  "units": "millimetres, origin top-left of the page",
  "field_types": {
    "text":  { "properties": ["fontSize", "alignment", "..."], "required": ["name", "type", "position", "width", "height"] },
    "image": { "value_format": "data:image/...;base64,... or http(s) URL fetched server-side" },
    "table": { "notes": "value is an array of string arrays; head defines the column count; headWidthPercentages must have one entry per head column" },
    "svg": {}, "line": {}, "rectangle": {}, "ellipse": {}
  },
  "page_formats": { "A4": { "width_mm": 210, "height_mm": 297 }, "Letter": { "width_mm": 215.9, "height_mm": 279.4 } },
  "default_page": { "width_mm": 210, "height_mm": 297 },
  "fonts": ["Roboto"],
  "template_limit": { "tier": "pro", "max_templates": 15, "used": 3 },
  "limits": { "preview_max_body_bytes": 10485760, "preview_max_pages": 10, "preview_max_scale": 4 }
}
```

- `field_types` is generated from the validator itself, so it is always the exact list of types
  that will be accepted.
- `fonts` is the default fallback font. Per-template `fonts` arrays are accepted on create and
  preview; they are stored per template, not per account.
- `template_limit` is your tier and current usage — `used` at or above `max_templates` means
  create will be refused.

## Sample templates: `GET /api/templates/samples`

Agents design far better from a working example than from a schema description.

```bash
curl -H "Authorization: Bearer ***" {BASE_URL}/api/templates/samples
# -> { "success": true, "samples": [ { "kind": "invoice", "label": "Invoice", "engines": ["pdfme", "html"], ... } ], "kinds": [...], "engines": [...] }

curl -H "Authorization: Bearer ***" "{BASE_URL}/api/templates/samples?kind=invoice&engine=html"
# -> { "success": true, "kind": "invoice", "engine": "html", "sample": { "html_template": "...", "sample_data": {...} } }
```

`kind` must be one of `invoice`, `certificate`, `paycheck-stub`; `engine` must be `pdfme` or
`html`. `kind` and `engine` are only meaningful together — sending one without the other, or an
unknown value, is a **400** listing the valid options.

## Validate before you save: `POST /api/templates/validate`

```json
POST /api/templates/validate
{
  "engine": "pdfme",
  "template": { "schemas": [[{ "name": "title", "type": "text", "position": { "x": 10, "y": 10 }, "width": 100, "height": 10 }]] }
}
```

```json
{ "success": true, "valid": true, "engine": "pdfme", "variables": ["title"] }
```

**A validation failure is a `200`, not a `400`.** You asked "is this valid?" and `valid: false`
is the answer. `400` is reserved for a malformed request (non-JSON body, wrong field types).

```json
{ "success": true, "valid": false, "engine": "pdfme",
  "errors": ["schemas[0][2] (\"logo\") has unsupported type \"qrcode\". Supported types: text, image, svg, line, rectangle, ellipse, table"] }
```

This differs deliberately from `/preview`, which returns `400` because you asked it to *render*.
Malformed Handlebars is a real failure here: `{{#each items}}` with no `{{/each}}` comes back as
`valid: false` with the compile error, before it ever reaches a renderer.

## Concurrent edits: `version` and `expected_version`

Every template carries a `version` (starts at `1`), returned by `/api/templates`,
`/api/templates/:id` and every write. It increments on every successful update, including a
rename. Send it back as `expected_version` on `PUT` to protect yourself from clobbering an edit
that landed since you last read the template:

```bash
curl -X PUT {BASE_URL}/api/templates/<id> \
  -H "Authorization: Bearer ***" -H "Content-Type: application/json" \
  -d '{ "name": "Invoice v2", "expected_version": 3 }'
# 200 -> { "success": true, "template": { ..., "version": 4 } }
```

If the stored version is not `3`, nothing is written and you get **409** with the current version:

```json
{ "success": false, "error": "Template version conflict",
  "details": ["expected_version 3 does not match the current version 4; re-fetch the template and retry"] }
```

Re-fetch the template, re-apply your change to the new version, and retry.
`expected_version` is optional, so existing callers keep working unchanged.

## The design loop

1. Draft a template (pdfme JSON or HTML, see engines below).
2. `POST /api/templates/preview` with sample data and `"output": "png"`. You get page images you can
   look at, plus `warnings` and `missing_variables` (see below).
3. Look at the image. Fix validation errors reported in `details` and anything in `warnings`.
4. `POST /api/templates` to save. Returns the template `id`.
5. Generate real PDFs: `POST /api/pdf/token` with `{ "template_id", "data" }`, then `GET` the returned `url`.

## Previewing: seeing what you built

`POST /api/templates/preview` renders through the same pipeline as production (same fonts, same
page size, same pagination) and saves nothing.

| Field | Default | Meaning |
|-------|---------|---------|
| `output` | `"pdf"` | `"pdf"` → `pdf_base64`; `"png"` → `images`; `"both"` → both |
| `page` | `1` | First page to rasterize (1-based) |
| `pages` | `1` | How many pages to rasterize (max 10) |
| `scale` | `1.5` | Image scale (max 4; images are capped at 2400px on the longest side) |
| `fonts` | — | `[{ "name", "data" }]` (base64 TTF), same as when saving a template |
| `page_format` | inferred | `Letter`, `Legal`, `A4`, `A3`, `A5` (HTML engine) |

Response (`output: "png"`):

```json
{
  "success": true,
  "engine": "pdfme",
  "variables": ["title", "customer"],
  "missing_variables": ["customer"],
  "warnings": [
    { "code": "FIELD_OUT_OF_BOUNDS", "loc": "schemas[0][3]", "message": "Field \"logo\" is outside the page (mm): right edge 230 exceeds page width 210" }
  ],
  "page_count": 2,
  "page_size_mm": { "width": 210, "height": 297 },
  "images": [{ "page": 1, "png_base64": "iVBORw0...", "width": 893, "height": 1263 }]
}
```

Start with page 1. `page_count` tells you whether content spilled onto more pages; request them
with `page`/`pages` if so.

Warning codes (the preview still renders; these are problems you would not otherwise notice):
- `FIELD_OUT_OF_BOUNDS` - a pdfme field extends past the page edge and will be clipped or missing.
- `FONT_NOT_AVAILABLE` - a pdfme field names a font that is not loaded; the default font is used instead.
- `VARIABLE_MISSING_SAMPLE_DATA` - the template uses variables your `sampleData` does not provide.
- `IMAGE_SUBSTITUTED` - an image value was replaced by a transparent placeholder, so it will be absent from the output.
- `FONTS_IGNORED_BY_ENGINE` - fonts were supplied to the html engine, which cannot load them.

## Engine 1: `html` (recommended for agents)

HTML templates use [Handlebars](https://handlebarsjs.com/) placeholders and are rendered
by headless Chromium. Write ordinary HTML + CSS; use `{{variable}}` for data injection
and `{{#each items}}...{{/each}}` for repeating rows.

Constraints:
- JavaScript is disabled and external network requests are blocked.
- Images must be inline `data:` URIs.
- Default page size is US Letter; use CSS `@page` rules and `page-break-*` properties for pagination.

Create example:

```json
POST /api/templates
{
  "name": "Invoice",
  "engine": "html",
  "html_template": "<html><head><style>body{font-family:Helvetica,Arial,sans-serif;margin:40px} h1{color:#334} table{width:100%;border-collapse:collapse} td,th{border-bottom:1px solid #ddd;padding:8px;text-align:left}</style></head><body><h1>Invoice {{invoice_number}}</h1><p>Billed to: {{customer_name}}</p><table><tr><th>Item</th><th>Amount</th></tr>{{#each items}}<tr><td>{{description}}</td><td>{{amount}}</td></tr>{{/each}}</table><h2>Total: {{total}}</h2></body></html>",
  "sample_data": {
    "invoice_number": "INV-001",
    "customer_name": "Acme Corp",
    "items": [{ "description": "Widget", "amount": "$100.00" }],
    "total": "$100.00"
  }
}
```

Variables are auto-extracted from the placeholders; you do not need to list them.

Preview example:

```json
POST /api/templates/preview
{
  "engine": "html",
  "html_template": "<html><body><h1>Hello {{name}}</h1></body></html>",
  "sampleData": { "name": "World" }
}
```

## Engine 2: `pdfme` (coordinate-based)

pdfme templates place fields at exact positions on the page. **All units are millimeters.**

Template structure:

```json
{
  "basePdf": { "width": 215.9, "height": 279.4, "padding": [0, 0, 0, 0] },
  "schemas": [
    [
      { "name": "title", "type": "text", "position": { "x": 20, "y": 20 }, "width": 170, "height": 12, "fontSize": 24, "alignment": "center" },
      { "name": "body", "type": "text", "position": { "x": 20, "y": 40 }, "width": 170, "height": 60, "fontSize": 11 }
    ]
  ]
}
```

- `basePdf`: page dimensions in mm. Common sizes: A4 = 210x297, Letter = 215.9x279.4, Legal = 215.9x355.6. Omit it to default to A4 portrait.
- `schemas`: array of pages; each page is an array of field objects. Every field needs a unique `name`, a `type`, a `position` (mm from top-left), and `width`/`height` (mm).

Supported field types: `text`, `image`, `svg`, `line`, `rectangle`, `ellipse`, `table`.

Field type notes:
- `text`: supports `fontSize` (pt), `alignment` ("left" | "center" | "right"), `verticalAlignment` ("top" | "middle" | "bottom"), `fontColor` (hex), `backgroundColor` (hex).
- `image`: data value must be a `data:image/...;base64,` URI or an http(s) URL (fetched server-side).
- `table`: data value is an array of string arrays (rows). Configure `head` (column titles) and `headWidthPercentages` on the field.

Create example:

```json
POST /api/templates
{
  "name": "Certificate",
  "engine": "pdfme",
  "pdfme_schema": {
    "basePdf": { "width": 297, "height": 210, "padding": [0, 0, 0, 0] },
    "schemas": [
      [
        { "name": "recipient", "type": "text", "position": { "x": 48.5, "y": 80 }, "width": 200, "height": 15, "fontSize": 28, "alignment": "center" },
        { "name": "date", "type": "text", "position": { "x": 48.5, "y": 150 }, "width": 200, "height": 10, "fontSize": 12, "alignment": "center" }
      ]
    ]
  },
  "sample_data": { "recipient": "Jane Doe", "date": "June 10, 2026" }
}
```

## Images and fonts

Both differ by engine, and both used to fail silently. Read this before adding either.

### Images

An image lives in the field's **value**, not in the schema. For a pdfme `image` field:

| Value | Behaviour |
|-------|-----------|
| `data:image/png;base64,...` | Used as-is. Preferred: no network, no policy to satisfy, always works. |
| `http(s)://...` | Fetched **server-side** during generation, then embedded as a data URI. |

Anything else — a bare path such as `./logo.png`, a filename, a missing value — is replaced by a
transparent 1x1 PNG.

A URL is checked before it is requested. Loopback, link-local (including cloud metadata at
`169.254.169.254`), carrier-grade NAT, multicast and reserved addresses are blocked, as are
`localhost` and the known metadata hostnames. Private ranges (`10/8`, `172.16/12`, `192.168/16`)
and IPv6 ULA are **intentionally allowed**, so a self-hosted deployment can pull images from its
own network.

The HTML engine fetches nothing at all. Only `data:` and `about:` are permitted; every other
request is aborted, and a `<meta http-equiv="refresh">` or `<iframe src>` is refused too. Inline
your images as `data:` URIs.

### Fonts

`fonts: [{ "name": "...", "data": "..." }]` is accepted on create, update and preview. `data` is
**base64 font bytes** — bare base64, *not* a `data:` URI, which is the opposite of how an image
value is written.

**Fonts apply to pdfme only.** The html engine renders through headless Chromium with no font map,
so a `fonts` array on an html template is accepted, stored, returned by the API, and then ignored
when it renders. To use a custom font in HTML, embed it in the template itself as a self-contained
`@font-face` with a `data:` URI.

In pdfme a field selects a font with `fontName`. `Roboto` is always loaded as the fallback. A
`fontName` that is not loaded is rewritten to the default before rendering and reported as
`FONT_NOT_AVAILABLE`.

### The trap they used to share

A substituted image or font does not fail the request. A replaced image appeared as an empty box
with the explanation only in the server log, which a caller never sees. The preview now reports
both, so check `warnings` on every preview:

| Code | Meaning |
|------|---------|
| `IMAGE_SUBSTITUTED` | An image value was replaced by a transparent placeholder: the target was blocked, the fetch failed, or the value was neither a `data:` URI nor a URL. |
| `FONTS_IGNORED_BY_ENGINE` | Fonts were supplied to the html engine, which cannot use them. |

Preview and **look at the image** before saving. A warning tells you that something was
substituted; only the rendered page tells you the layout is right.

The preview request body is capped at 10 MB, which base64 payloads count against. An embedded font
is typically 100-300 KB, so a handful fit comfortably.

## Generating PDFs from a saved template

```bash
curl -X POST {BASE_URL}/api/pdf/token \
  -H "Authorization: Bearer $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "template_id": "<id>", "data": { "recipient": "Jane Doe", "date": "June 10, 2026" } }'
# -> { "success": true, "token": "...", "url": "/api/pdf/..." }

curl {BASE_URL}/api/pdf/<token> -o output.pdf   # within 15 minutes
```

## Moving templates between environments

Export templates as portable `.pdfex-templates.json` bundles to move designs from staging to production without copying the database.

**Dashboard:** `/secure/templates` — **Export** (per template or all), **Import** (upload a bundle). On import, **Update existing** replaces templates with the same name; **Create new** always adds copies.

**Export one template:**

```bash
curl -H "Authorization: Bearer $API_KEY" \
  {BASE_URL}/api/templates/<id>/export
# -> { "success": true, "bundle": { "pdfex_export_version": 1, "templates": [...] } }
```

**Import a bundle:**

```json
POST /api/templates/import
{
  "bundle": {
    "pdfex_export_version": 1,
    "templates": [
      {
        "name": "Invoice",
        "engine": "html",
        "html_template": "<html>...</html>",
        "sample_data": { "invoice_number": "INV-001" }
      }
    ]
  },
  "mode": "upsert"
}
```

Response: `{ "success": true, "result": { "created": [...], "updated": [...], "skipped": [...] } }`.

- `mode`: `"create"` (default) or `"upsert"` (match by template name for your API key's user).
- Bundles omit environment-specific fields (`_id`, `created_by`, timestamps). Template IDs will differ after import — update integrations accordingly.
- Custom fonts (`fonts` array with base64 data) and both engines (`pdfme`, `html`) are included in exports.

**CLI** (see `scripts/template-transfer.ts`):

```bash
pnpm dlx tsx scripts/template-transfer.ts export --url {BASE_URL} --api-key $KEY --out all.json
pnpm dlx tsx scripts/template-transfer.ts import --url {BASE_URL} --api-key $KEY --in all.json --mode upsert
```

## Common validation errors

- `schemas[0][2] ("logo") has unsupported type "qrcode"` - only the listed field types are enabled.
- `schemas[0][1] requires position {x: number, y: number} in mm` - every pdfme field needs numeric position/size.
- `html_template failed to compile: ...` - malformed Handlebars syntax (for example an unclosed `{{#each}}`).
- HTTP 401 - missing or revoked API key. HTTP 404 - template missing or not visible to your key.

---

## Connect an agent host

PDFEx speaks MCP over Streamable HTTP, stateless at `https://pdfex.io/mcp`. Auth is the same API key the REST API uses: `Authorization: Bearer <key>`.

**Verification honesty:** of 18 hosts, 7 have their command checked against the tool itself or the vendor's docs, and 11 are unverified. Anything marked `not yet verified` is best current knowledge — confirm it against the linked documentation before relying on it. No install line here claims to have been run end to end unless it says so.

### One command (no file editing)

These hosts can register the server with a single command. Run the command, then run the verify command — saving a config proves nothing about reachability.

#### Hermes Agent

_Nous Research's open-source agent framework (CLI, desktop, gateway)._

- **Config:** `~/.hermes/config.yaml`
- **Key:** `mcp_servers`
- **Verification:** command verified (2026-09-30)
- **Docs:** https://hermes-agent.nousresearch.com/docs/

```bash
hermes mcp add
```

```json
mcp_servers:
  pdfex:
    url: "https://pdfex.io/mcp"
    headers:
      Authorization: "Bearer pdfex_your_key_here"
```

**Verify:** hermes mcp list

> `hermes mcp add` is a discovery-first wizard; it writes the entry for you. Hermes stores connections as YAML under `mcp_servers`, not JSON. Do not hand-edit the file — a stray indent corrupts it and breaks the running gateway.

#### Pi

_Coding agent CLI by Mario Zechner (@earendil-works/pi)._

- **Config:** `~/.pi/agent/mcp.json`
- **Key:** `mcpServers`
- **Verification:** command verified (2026-09-30)
- **Docs:** https://github.com/earendil-works/pi/blob/main/packages/coding-agent/docs/mcp.md

```bash
pi mcp add pdfex --url https://pdfex.io/mcp --bearer-token-env-var PDFEX_API_KEY
```

**Verify:** pi mcp list

> `--bearer-token-env-var` is a documented Pi flag that sends `Authorization: Bearer <value of that env var>`, so the key never lands in the file. `pi mcp list` exits non-zero when a server fails — it is a real health check. Use `-l` to scope the server to the current project instead of globally.

#### Claude Code

_Anthropic's terminal coding agent._

- **Config:** `~/.claude.json (or a project .mcp.json)`
- **Key:** `mcpServers`
- **Verification:** command verified (2026-09-30)
- **Docs:** https://docs.claude.com/en/docs/claude-code/mcp

```bash
claude mcp add --transport http pdfex https://pdfex.io/mcp \
  --header "Authorization: Bearer $PDFEX_API_KEY"
```

**Verify:** claude mcp list

> `--transport http` is required — without it Claude Code assumes a local stdio command. Add `--scope project` to write `.mcp.json` into the repo so a whole team shares it.

#### Codex CLI

_OpenAI's terminal coding agent._

- **Config:** `~/.codex/config.toml`
- **Key:** `[mcp_servers.pdfex]`
- **Verification:** command verified (2026-09-30)
- **Docs:** https://developers.openai.com/codex/mcp

```bash
codex mcp add
```

**Verify:** codex mcp list

> `codex mcp add` exists but its flag spelling was not confirmed — run `codex mcp add --help` first. Codex reads TOML, so a JSON block will not work; the entry belongs under `[mcp_servers.pdfex]` in `~/.codex/config.toml`.

#### Gemini CLI

_Google's open-source terminal agent._

- **Config:** `~/.gemini/settings.json`
- **Key:** `mcpServers`
- **Verification:** command verified (2026-09-30)
- **Docs:** https://github.com/google-gemini/gemini-cli

```bash
gemini mcp add pdfex https://pdfex.io/mcp
```

**Verify:** gemini mcp list

> `gemini mcp add <name> <commandOrUrl>` is confirmed from `--help`. The header flag spelling is not — run `gemini mcp add --help` and add the Authorization header, otherwise the server will answer 401.

#### OpenCode

_Open-source terminal coding agent._

- **Config:** `~/.config/opencode/opencode.json`
- **Key:** `mcp`
- **Verification:** command verified (2026-09-30)
- **Docs:** https://opencode.ai/docs/mcp-servers/

```bash
opencode mcp add
```

**Verify:** opencode mcp list

> Note the key is `mcp`, not `mcpServers` — OpenCode uses its own schema. `opencode mcp add` is interactive and will prompt for the URL and header.

#### OpenClaw

_Self-hosted assistant gateway that connects chat apps to agents._

- **Key:** `mcp.servers.pdfex (transport: "streamable-http")`
- **Verification:** vendor-documented (2026-09-30)
- **Docs:** https://docs.openclaw.ai/tools/mcp

```bash
openclaw mcp add pdfex --url https://pdfex.io/mcp \
  --transport streamable-http
```

**Verify:** openclaw mcp doctor pdfex --probe

> OpenClaw's own docs make the point this matrix is built on: saving a definition proves nothing about reachability — `doctor --probe` does. Also keep the key out of the config literal; OpenClaw asks you to store sensitive headers through its secret mechanism. You can also add it from the Control UI under Settings → MCP.

#### GitHub Copilot CLI

_GitHub's Copilot agent for the terminal._

- **Verification:** not yet verified
- **Docs:** https://docs.github.com/en/copilot/how-tos/use-copilot-agents/use-copilot-cli

```bash
copilot mcp add pdfex
```

**Verify:** copilot mcp list

> A `copilot mcp` subcommand exists, but the add syntax was not confirmed. Run `copilot mcp --help` before trusting this line.

#### Crush

_Charm's terminal-first coding agent._

- **Config:** `~/.config/crush/crush.json`
- **Key:** `mcp`
- **Verification:** not yet verified
- **Docs:** https://github.com/charmbracelet/crush

```bash
crush
```

**Verify:** crush --help

> Crush reads `mcp` entries from its config file. Not confirmed on this machine — check the repo before publishing a command.

### A JSON `mcpServers` block

Every host below accepts a JSON config block. Watch the key name — most use `mcpServers`, but VS Code uses `servers`, and a block with the wrong key is silently ignored.

#### Claude Desktop

_Anthropic's desktop app._

- **Config:** `claude_desktop_config.json`
- **Key:** `mcpServers`
- **Verification:** not yet verified
- **Docs:** https://modelcontextprotocol.io/quickstart/user

```json
{
  "mcpServers": {
    "pdfex": {
      "type": "http",
      "url": "https://pdfex.io/mcp",
      "headers": { "Authorization": "Bearer pdfex_your_key_here" }
    }
  }
}
```

**Verify:** Restart the app; the pdfex tools appear in the tool picker.

> Reachable from Settings → Developer → Edit Config. Restart the app after editing — it reads this file only at startup.

#### Cursor

_AI code editor._

- **Config:** `~/.cursor/mcp.json`
- **Key:** `mcpServers`
- **Verification:** not yet verified
- **Docs:** https://docs.cursor.com/context/model-context-protocol

```json
{
  "mcpServers": {
    "pdfex": {
      "type": "http",
      "url": "https://pdfex.io/mcp",
      "headers": { "Authorization": "Bearer pdfex_your_key_here" }
    }
  }
}
```

**Verify:** Settings → MCP shows pdfex connected with its tools listed.

> A project-scoped `.cursor/mcp.json` is also honoured, which is the better choice for a shared repo.

#### Windsurf

_Codeium's AI code editor._

- **Config:** `~/.codeium/windsurf/mcp_config.json`
- **Key:** `mcpServers`
- **Verification:** not yet verified
- **Docs:** https://docs.windsurf.com/windsurf/cascade/mcp

```json
{
  "mcpServers": {
    "pdfex": {
      "type": "http",
      "url": "https://pdfex.io/mcp",
      "headers": { "Authorization": "Bearer pdfex_your_key_here" }
    }
  }
}
```

**Verify:** Cascade panel → MCP servers lists pdfex.

> Windsurf also has an in-app MCP settings UI that edits this same file.

#### Cline

_Autonomous coding agent as a VS Code extension._

- **Config:** `cline_mcp_settings.json`
- **Key:** `mcpServers`
- **Verification:** not yet verified
- **Docs:** https://docs.cline.bot/mcp/configuring-mcp-servers

```json
{
  "mcpServers": {
    "pdfex": {
      "type": "http",
      "url": "https://pdfex.io/mcp",
      "headers": { "Authorization": "Bearer pdfex_your_key_here" }
    }
  }
}
```

**Verify:** The MCP Servers panel shows pdfex as connected.

> Cline offers a remote-server form in its UI, which is less error-prone than editing the JSON.

#### Roo Code

_VS Code coding agent (Cline fork)._

- **Config:** `mcp_settings.json`
- **Key:** `mcpServers`
- **Verification:** not yet verified
- **Docs:** https://docs.roocode.com/features/mcp/using-mcp-in-roo

```json
{
  "mcpServers": {
    "pdfex": {
      "type": "http",
      "url": "https://pdfex.io/mcp",
      "headers": { "Authorization": "Bearer pdfex_your_key_here" }
    }
  }
}
```

**Verify:** MCP Servers tab in the Roo Code panel shows pdfex connected.

> Same shape as Cline, different file location. Prefer the UI's remote-server form.

#### VS Code (Copilot Chat)

_GitHub Copilot Chat inside VS Code._

- **Config:** `.vscode/mcp.json`
- **Key:** `servers`
- **Verification:** not yet verified
- **Docs:** https://code.visualstudio.com/docs/copilot/chat/mcp-servers

```json
{
  "servers": {
    "pdfex": {
      "type": "http",
      "url": "https://pdfex.io/mcp",
      "headers": { "Authorization": "Bearer pdfex_your_key_here" }
    }
  }
}
```

**Verify:** Command Palette → “MCP: List Servers” shows pdfex running.

> THE TRAP: VS Code uses the key `servers`, not `mcpServers`. A block copied from Cursor or Claude Desktop is silently ignored here. This is the single most common copy-paste failure in this matrix.

### The host's own config file

These hosts do not use the JSON `mcpServers` shape. Copy the block for the host you are configuring, not the canonical one.

#### Continue

_Open-source IDE assistant (VS Code / JetBrains)._

- **Config:** `~/.continue/config.yaml`
- **Key:** `mcpServers`
- **Verification:** not yet verified
- **Docs:** https://docs.continue.dev/customize/deep-dives/mcp

```json
mcpServers:
  - name: pdfex
    type: http
    url: https://pdfex.io/mcp
    headers:
      Authorization: "Bearer pdfex_your_key_here"
```

**Verify:** The Continue sidebar lists the pdfex tools.

> Continue's YAML takes a LIST of servers, not a map — note the leading `-`. Its JSON form (`config.json`) uses a map like everyone else.

#### Goose

_Block's open-source local agent._

- **Config:** `~/.config/goose/config.yaml`
- **Key:** `extensions`
- **Verification:** not yet verified
- **Docs:** https://block.github.io/goose/

```json
extensions:
  pdfex:
    type: streamable_http
    uri: https://pdfex.io/mcp
    headers:
      Authorization: "Bearer pdfex_your_key_here"
```

**Verify:** `goose` starts and lists pdfex among its extensions.

> Goose calls MCP servers “extensions” and spells the transport `streamable_http` with an underscore, and the URL field `uri` not `url`. Three differences from every other host — copy from here, not from the canonical block.

#### Zed

_Collaborative code editor._

- **Config:** `~/.config/zed/settings.json`
- **Key:** `context_servers`
- **Verification:** not yet verified
- **Docs:** https://zed.dev/docs/ai/mcp

```json
{
  "context_servers": {
    "pdfex": {
      "source": "custom",
      "command": https://pdfex.io/mcp,
      "args": []
    }
  }
}
```

**Verify:** Zed's agent panel lists the pdfex tools.

> Zed uses the key `context_servers`, not `mcpServers` — another silent-ignore trap. Newer Zed builds support remote HTTP servers directly; consult the docs, since this schema has been changing.
