Foliant

Agents and MCP

Foliant is a Model Context Protocol server. Any MCP client can call render_document and get a PDF back, write and reuse templates, and check its own Typst before spending a page.

Connect a client

Two transports, same tools.

Remote, sign in from the client (recommended)

Streamable HTTP at https://api.foliant.dev/mcp. Add the URL and nothing else. Clients that implement MCP authorization (Claude.ai and Claude Desktop connectors, Cursor, VS Code, ChatGPT, Kiro) get a 401 with an OAuth challenge, open a browser, you sign in with your email and approve. The client receives its own API key, labelled mcp: <client name>, which you can revoke in the dashboard like any other key. No token to paste, nothing stored in a config file.

// Cursor, VS Code, Kiro, Windsurf: mcp config
{ "mcpServers": { "foliant": { "type": "http", "url": "https://api.foliant.dev/mcp" } } }
HostWhere to add the URL
Claude.ai, Claude DesktopSettings, Connectors, Add custom connector. Available on all paid plans; sign-in happens on first use.
ChatGPTSettings, Apps and Connectors, turn on Developer mode, Create. Custom connectors need Plus, Pro, Team or Enterprise (Team and Enterprise admins enable them in workspace settings). Foliant exposes the search and fetch tools ChatGPT expects, so it also works as a knowledge source in deep research.
GrokSettings, Connectors, Add custom connector (SuperGrok). In the xAI API, pass the URL as a remote MCP tool.
Mistral Le Chat, Mistral StudioConnectors, Custom MCP. Also available in the Agents API as a custom MCP connector.
PerplexitySettings, Connectors, Add custom remote connector (Pro and Max).
Kiro, Cursor, VS Code, Windsurf, Codex CLI, Claude CodeThe JSON above in the client's MCP config; OAuth opens a browser tab.

Under the hood: OAuth 2.1 with PKCE, dynamic client registration, metadata at /.well-known/oauth-authorization-server. Access tokens do not expire; disconnecting is revoking the key. For US processing use https://us.api.foliant.dev/mcp; same tools, same account, documents rendered in N. Virginia (see Regions).

Remote, with a pasted key

For clients without OAuth support, or for scripts, send an API key as a bearer header. Same URL, same tools.

{
  "mcpServers": {
    "foliant": {
      "type": "http",
      "url": "https://api.foliant.dev/mcp",
      "headers": { "Authorization": "Bearer fl_live_…" }
    }
  }
}

Remote, demo without an account

https://api.foliant.dev/mcp/demo needs no auth and uses the demo quota (3 renders a day, 5 pages). Template and asset tools are unavailable there.

Because the remote server runs on our side it cannot read or write your files. Rendered documents come back as a download link valid for one hour (the agent or you fetch it with curl or a browser), or inline as base64 with delivery: "inline". files are accepted as base64, URLs or stored assets.

Local (npx)

Runs on your machine over stdio. It can read local files for files and upload_asset, and writes rendered output to disk at output_path. Prefer this when the agent works inside a project directory.

{
  "mcpServers": {
    "foliant": {
      "command": "npx",
      "args": ["-y", "@foliant/mcp"],
      "env": { "FOLIANT_API_KEY": "fl_live_…" }
    }
  }
}

Environment: FOLIANT_API_KEY (optional, demo mode without it), FOLIANT_API_URL to target another deployment.

Tools

ToolDoesCosts
render_documentRenders source or a stored template with inputs to PDF, PNG or SVG. Local: writes to output_path. Remote: returns base64. PNG also comes back as an image the model can look at.Pages
check_sourceCompiles and returns errors with line numbers, warnings, and the page count. No output.Nothing
accountTier, pages used, balance, limits.Nothing
list_templatesNames, sizes and dates of stored templates.Nothing
get_templateSource of one template plus the list of sys.inputs keys it reads, so the agent knows what to pass.Nothing
save_templateCreates or replaces a template. Compiles it first with sample_inputs and refuses to save broken source.Nothing
list_assetsStored files: logos, fonts, data.Nothing
upload_assetStores a file from base64, an https URL, or (local only) a path.Nothing
searchSearches the account's templates and assets and the Foliant documentation. Returns { results: [{ id, title, url }] }. This is the connector-standard shape ChatGPT, Claude and others use for knowledge sources.Nothing
fetchReturns one search result in full: a template's source and the inputs it reads, an asset's metadata, or a documentation page, as { id, title, text, url, metadata }.Nothing

The template and asset tools need an API key. In demo mode they return an error explaining that.

A good workflow

The server's instructions tell the model to do this already; it helps to know why.

  1. Check before render

    Typst is strict. A model's first draft has a typo one time in five. check_source is free and returns the line; render_document on a broken document is also free but, in demo mode, consumes one of the three daily renders. Agents that check first almost never waste a render.

  2. Render once

    When the check passes, render. Read x-foliant-pages (in the tool result text) to confirm the page count matches what you intended.

  3. Look at the result when it matters

    For layout-sensitive documents, render to png first. The image comes back in the tool result and a multimodal model can see whether the table overflowed or the logo is the wrong size. Then render the PDF.

  4. Store what will repeat

    If the same document will be produced again with other values, turn it into a template (next section) so the next run is one small tool call.

Templates from an agent

The template tools let an agent build its own library. A typical session, as tool calls:

list_templates()
→ "No templates yet."

save_template(
  name: "invoice.typ",
  source: "#let input(k, default: \"\") = sys.inputs.at(k, default: default)\n#set page(paper: \"a4\")\n= Invoice #input(\"no\")\n…",
  sample_inputs: { no: "0001", customer: "Test" })
→ "Saved invoice.typ (612 bytes). Render with render_document(template: \"invoice.typ\", inputs: {...})."

render_document(template: "invoice.typ", inputs: { no: "2026-0917", customer: "Meridian Studio GmbH", total: "8496.60" }, output_path: "out/2026-0917.pdf")
→ "Rendered 1 page to /work/out/2026-0917.pdf (24 831 bytes, application/pdf)."

Later, a different agent (or the same one in a new session) starts with get_template("invoice.typ") and learns which inputs exist without reading the whole source:

get_template(name: "invoice.typ")
→ "…source…

   Inputs referenced: no, customer, address, items, due, footer"

Design templates for agents the way you would for a junior colleague: every input has a default, the input names are plain words, and formatting lives in the template so the agent only supplies facts. See the templates guide for three complete examples.

Prompting for documents

Things that reliably improve what a model produces with these tools:

Your own agent

If you are building an agent rather than using a client, you do not need MCP at all. Call the HTTP API from your tool implementation:

// Tool definition (any framework)
{
  name: "render_pdf",
  description: "Render Typst source or a stored template to PDF. Call check first.",
  parameters: { source?: string, template?: string, inputs?: Record<string,string>, check?: boolean }
}

// Implementation
const res = await fetch("https://api.foliant.dev/v1/render", {
  method: "POST",
  headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
  body: JSON.stringify({ ...args, response: "json" }),
});
const body = await res.json();
if (!res.ok) return `Error ${body.error.code}: ${body.error.message}` +
  (body.error.diagnostics ? "\n" + body.error.diagnostics.map(d => `line ${d.line}: ${d.message}`).join("\n") : "");
if (args.check) return `Compiles, ${body.pages} pages.`;
await fs.writeFile("out.pdf", Buffer.from(body.data, "base64"));
return `Wrote out.pdf, ${body.pages} pages.`;

response: "json" puts pages, warnings and the file in one body, which is easier to hand back to a model than binary plus headers. Return the diagnostics verbatim: line numbers are what let the model fix its own mistake.

The MCP package's src/client.ts is a 150-line reference implementation of exactly this, in TypeScript with no dependencies.

Limits and errors

Same limits as the API. In demo mode: 3 renders a day, 5 pages, 64 KB source, no URL or asset files, 10 requests a minute. Errors come back as tool errors with the message the model needs:

compile_errorListed with line numbers and hints. The model should fix and re-check.
demo_exhaustedExplains how to get a key. In an interactive client, switch the URL from /mcp/demo to /mcp and sign in when prompted.
quota_exceeded, insufficient_creditIncludes the dashboard URL to top up. The key keeps working afterwards.
too_many_pagesThe document exceeds the tier limit; the model can tighten the layout or split.
rate_limitedIncludes retry_after seconds.