MCP reference
The MCP endpoint is the same code path as the public API: a document rendered by
Claude and one rendered by curl go through one function, so quota, scope and
published-version resolution cannot drift between them.
POST https://composepdf.com/mcpTransport is Streamable HTTP, stateless — no Mcp-Session-Id is issued and
none is expected. See the connection guide for client setup.
Credentials
Section titled “Credentials”| Header | keyId in logs |
Organization | |
|---|---|---|---|
| API key | x-api-key, or Authorization: Bearer rsp_… |
the key | the one the key names |
| OAuth | Authorization: Bearer <access token> |
null — a person authorised it |
your oldest membership, or organizationId |
A request with no credential is answered 401 with a WWW-Authenticate header
naming /.well-known/oauth-protected-resource. That header is the whole OAuth
discovery mechanism; a client that only takes a URL follows it to dynamic client
registration.
tools/list shows only what the credential can actually do. A render key
never sees put_dataset_rows; a data-write key never sees render_document.
A model that cannot see a tool will not spend a turn discovering it is
forbidden.
| Tool | Role | What it does |
|---|---|---|
list_documents |
render | Published templates, one id⇥title per line |
describe_document |
render | The fields a template takes, one per line |
open_form |
render | Puts a form for the person to fill in in the conversation |
render_document |
render | Renders to PDF, keeps it a few days, returns a link |
put_dataset_rows |
data-write | Replaces one top-level array in a dataset |
check_document |
OAuth | Checks a 帳票DSL source against the quality gate |
create_document |
OAuth | Compiles a checked source into a published template |
list_organizations |
OAuth | Only when you belong to more than one |
Templates are created by people, not by keys. check_document and
create_document are offered on the OAuth connection only — the one authorised
by a signed-in person. Neither API key role can reach them, so a key living in
someone else’s spreadsheet can render your templates but never write one.
list_documents
Section titled “list_documents”No arguments. Returns one line per template: the id, a tab, the title.
describe_document
Section titled “describe_document”| Argument | ||
|---|---|---|
document |
required | Template id or exact title |
organizationId |
optional | OAuth callers with more than one organization |
Returns one line per field: path⇥type⇥required⇥example. Array members are
written the way the engine reports them (items[].price), so a path in an error
message can be found here without translating.
Usually unnecessary — a wrong payload comes back with the same table. Read it
when you need the paths before filling something in, such as before open_form.
open_form
Section titled “open_form”| Argument | ||
|---|---|---|
document |
required | Template id or exact title |
data |
optional | Values already known, in render_document’s data shape. Partial is fine |
organizationId |
optional | OAuth callers with more than one organization |
On a client that renders MCP Apps, this puts a
form (ui://composepdf/document-form) in the conversation for the person to
fill in; submitting it calls render_document over the same connection, so no
extra key or token is involved. The fields are the ones the contract declares —
the same buildForm the embed and the mobile app use, so one template produces
one set of fields everywhere.
Call it at most once per template. MCP Apps attaches the UI to the tool,
so every call draws another form. That is also why looking up field paths is
describe_document’s job and not this one’s.
render_document
Section titled “render_document”| Argument | ||
|---|---|---|
document |
required | Template id or exact title |
data |
optional | The payload. Omit it to use the template’s own dataset |
dataList |
optional | One record per page-run, bound into a single PDF. Exclusive with data |
fileName |
optional | Defaults to the template title |
organizationId |
optional | OAuth callers with more than one organization |
Always renders the published version; pinning an older one belongs to the render API and the studio.
The reply is a link, not the bytes — a PDF is megabytes and a tool result is
context. Which link depends on the credential: an OAuth caller holds no API key,
so it gets a browser URL; a key caller may not have a studio session, so it gets
a curl line. Stored PDFs expire after the retention window.
check_document
Section titled “check_document”| Argument | ||
|---|---|---|
source |
required | The .rpt source itself, not a path |
data |
optional | Real data to lay out with; omitted, the declarations’ own examples are used |
profiles |
optional | Business profiles to lint against, e.g. ["invoice-jp"] |
organizationId |
optional | OAuth callers with more than one organization |
Runs the three-layer gate in one call — content that did not fit, geometry
(overlap, off-plate, uneven margins, too many type sizes), and, with a profile,
what a Japanese form is legally required to carry. Answers with one diagnostic
per line: line:col⇥severity⇥code⇥message, and the repair to try underneath.
This is how a template is fixed. Iterating on diagnostics converges; iterating on a picture of the page does not, because a page tells you something is wrong without telling you which line made it wrong.
The tool’s own description carries one complete working template and the four rules that trip writers up. That is deliberate: the examples below are also served as resources, but a client that cannot fetch them leaves the model guessing at a syntax — and a guessed syntax compiles into a page with nothing on it. A description always arrives.
create_document
Section titled “create_document”| Argument | ||
|---|---|---|
title |
required | What the template is called |
source |
required | The .rpt source itself |
data |
optional | Real data to check the layout with |
profiles |
optional | Business profiles to lint against |
organizationId |
optional | OAuth callers with more than one organization |
Compiles the source, publishes it, and returns the id render_document takes.
It refuses while any diagnostic is an error, and the refusal carries the
diagnostics, so the next attempt can be right. Warnings do not block: a template
is written in passes, and a gate that fails a half-finished draft is a gate
people switch off.
The template lands in a new workspace of its own, and the .rpt is kept beside
the published document as the original it was compiled from.
put_dataset_rows
Section titled “put_dataset_rows”| Argument | ||
|---|---|---|
datasetId |
required | |
path |
required | One segment — "items", never "a.b[0].c" |
rows |
required |
A whole-array replacement, not an append: send every row you want to keep. A dataset linked to an external source refuses the write rather than being silently overwritten on its next read.
Resources
Section titled “Resources”Four worked .rpt sources are served as resources — rpt://examples/invoice,
rpt://examples/invoice-qualified, rpt://examples/inspection-report,
rpt://examples/production-schedule. They are the repository’s own examples,
not copies, so they cannot go stale. Start from the closest one rather than from
an empty file; they are advertised on the same connection that can create.
Errors
Section titled “Errors”Errors come back as tool errors carrying the HTTP status and code, so a client can tell “we are out of renders” from “the payload is wrong”:
400 data_contract_violation |
The payload does not satisfy the contract. The message names the offending paths and repeats the field table, so the next attempt can be right |
402 quota_exceeded |
The month’s allowance is spent, or the plan has no API |
404 not_found |
No template matches, or the name is ambiguous — the message lists the candidates |
429 rate_limited |
Over the per-minute limit for the plan |
A template that declares no variables is not checked against a contract
(templates published before declarations existed still work), so it renders with
whatever it was given. For those, read describe_document first.
Ambiguity
Section titled “Ambiguity”document takes a title because that is what people say. An exact match wins; a
prefix that matches two templates is refused with the candidates listed
rather than guessed at. Rendering the wrong document is worse than asking.