{
  "markdown": "# @sheetrender/mcp\n\nMCP server for [SheetRender](https://sheetrender.com). It lets your AI assistant\nrender PDFs from HTML templates and spreadsheet data.\n\n## Setup\n\nGet an API key from [sheetrender.com](https://sheetrender.com) under Settings →\nAPI keys, then add this to your MCP client config:\n\n```json\n{\n  \"mcpServers\": {\n    \"sheetrender\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@sheetrender/mcp\"],\n      \"env\": { \"SHEETRENDER_API_KEY\": \"sr_live_...\" }\n    }\n  }\n}\n```\n\n`SHEETRENDER_API_URL` is also read, and defaults to `https://sheetrender.com`.\nSet it only if you're pointing at a self-hosted or staging instance.\n\n## Hosted endpoint\n\nThe same server runs at **`https://mcp.sheetrender.com/mcp`** over Streamable\nHTTP, so clients that can't spawn a local process can use it too. Nothing is\ninstalled; each request carries your API key:\n\n```\nAuthorization: Bearer sr_live_...\n```\n\nRequests without that header get a 401. The key goes straight through to the\nSheetRender API for that one request and is never stored — the server keeps no\nsessions, so every request stands alone.\n\nWhere the key goes depends on the client:\n\n- **Claude Code**:\n  `claude mcp add --transport http sheetrender https://mcp.sheetrender.com/mcp --header \"Authorization: Bearer sr_live_...\"`\n- **Cursor, Windsurf, VS Code** and other clients with an `mcp.json`:\n\n  ```json\n  {\n    \"mcpServers\": {\n      \"sheetrender\": {\n        \"url\": \"https://mcp.sheetrender.com/mcp\",\n        \"headers\": { \"Authorization\": \"Bearer sr_live_...\" }\n      }\n    }\n  }\n  ```\n\n- **Claude API** (the Messages API's MCP connector): add\n  `{\"type\": \"url\", \"url\": \"https://mcp.sheetrender.com/mcp\", \"name\": \"sheetrender\", \"authorization_token\": \"sr_live_...\"}`\n  to `mcp_servers`.\n- **claude.ai, Claude Desktop and ChatGPT custom connectors** take a server URL\n  and an OAuth client, not a static header. Until the endpoint speaks OAuth,\n  use the stdio package above there — it's the same tools, with the key in\n  `env` — or bridge with `npx mcp-remote https://mcp.sheetrender.com/mcp --header \"Authorization: Bearer sr_live_...\"`\n  as the command.\n\nTwo differences from the stdio server follow from the process not running on\nyour machine. Rendered PDFs come back inline as a base64 resource (up to 8 MB;\nlarger ones are reported with their size and left for the web app) instead of\nas a temp-file path, and `upload_dataset` is not offered because there is no\nlocal file to read — send rows with `create_dataset` instead.\n\n`GET /healthz` answers 200 without credentials. Request bodies are capped at\n25 MB; anything larger is a 413.\n\n### Running it yourself\n\n`sheetrender-mcp-http` is a second bin in the package. It reads `PORT`\n(default 8080), `HOST` (default `0.0.0.0`), `SHEETRENDER_API_URL`,\n`MAX_BODY_BYTES` and `IDLE_TIMEOUT_MS` (default 60000), and logs one JSON\nline per request to stdout — method, path, status, duration, the JSON-RPC\nmethod and tool name, and a fingerprint of the key, never the key. The\n`Dockerfile` in this repo builds a non-root runtime image for it:\n\n```sh\ndocker build -t sheetrender-mcp .\ndocker run --rm -p 8080:8080 sheetrender-mcp\n```\n\n## Tools\n\n### `render_pdf`\n\nRenders one PDF from HTML you supply.\n\n| Argument | Type | |\n| --- | --- | --- |\n| `html` | string | Required. A full HTML document. CSS has to be inline in a `<style>` tag; external stylesheets, fonts and scripts are not fetched. |\n| `data` | object | Optional. Keys become Jinja variables, so `{\"total\": \"42.00\"}` makes `{{ total }}` available in the HTML. |\n| `page_settings` | object | Optional, see below. |\n\nReturns the path of the saved PDF and its size. Under 512 KB it's also attached\ninline as a base64 resource, so clients that display attachments show the\ndocument itself.\n\nTwo server limits apply. HTML over 2 MB is rejected, and that's measured both on\nwhat you send and on the document after `data` is substituted in, so a template\nthat expands a long dataset can cross the line even when the markup you wrote\ndoesn't. The other is the free plan, where every rendered PDF carries a \"Made\nwith SheetRender\" footer. That applies to `render_pdf` and `render_template`\nalike. Paid plans don't get it.\n\n### `list_templates`\n\nNo arguments. Returns each saved template's name, id and last-updated date. Call\nit to turn a template name the user mentioned into the id the other tools want.\n\n### `render_template`\n\nRenders one PDF from a template already saved in the account.\n\n| Argument | Type | |\n| --- | --- | --- |\n| `template_id` | string | Required, from `list_templates`. |\n| `data` | object | Required. One row's values, as Jinja variables. |\n| `page_settings` | object | Optional. Omit it to keep the template's own saved page setup; passing it overrides that for this render. |\n\nSame return as `render_pdf`.\n\n### `create_dataset`\n\nTurns JSON rows into a dataset a batch job can render. This is the usual way to\nstart a batch: assemble the rows, send them, get back a `dataset_id`.\n\n| Argument | Type | |\n| --- | --- | --- |\n| `template_id` | string | Required, from `list_templates`. The dataset lands in that template's project. |\n| `rows` | array of objects | Required. One flat object per document. |\n| `name` | string | Optional label, used as the stored filename. |\n\nThe header is the union of every row's keys in first-seen order, so rows don't\nhave to agree on their keys — a missing one is a blank cell rather than a\nshifted row. Values have to be scalars: strings, numbers, booleans or null.\nNested objects and arrays are rejected, and so are NaN, Infinity and whole\nnumbers past 2^53 (send those as strings to keep them exact). The caps are\n50,000 rows and 500,000 cells per call.\n\nReturns the dataset id, row count and, for each column, the **sanitized key**.\nThat key is what template placeholders, `filename_template` and `group_by`\naddress, and it's often not the header verbatim — `Invoice No` becomes\n`invoice_no`. Read it off this result instead of guessing.\n\nCreating a dataset is free; only rendering counts against the plan.\n\n### `upload_dataset`\n\nThe same thing from a file that already exists.\n\n| Argument | Type | |\n| --- | --- | --- |\n| `template_id` | string | Required, from `list_templates`. |\n| `file_path` | string | Required. A `.csv` or `.xlsx` on the machine running this server — the user's machine, not SheetRender's. `~` is expanded. |\n\nThe first row has to be the header. Files over 20 MB, the wrong extension and\nempty files are refused locally, before anything is uploaded. Same return as\n`create_dataset`.\n\n### `list_datasets`\n\nTakes `template_id` and lists every dataset in that template's project, newest\nfirst, with ids, row counts and column keys. Use it to find data the user\nalready loaded, or to read a dataset's column keys before writing a\n`filename_template` or picking `group_by`.\n\n### `create_batch_job`\n\nQueues a background job that renders one PDF per row of a dataset. The whole\nloop runs from here — `list_templates` → `create_dataset` or `upload_dataset` →\n`create_batch_job` → `get_job` → `get_document`.\n\n| Argument | Type | |\n| --- | --- | --- |\n| `template_id` | string | Required, from `list_templates`. |\n| `dataset_id` | string | Required, from `create_dataset`, `upload_dataset` or `list_datasets`. Must be in the same project as the template. |\n| `filename_template` | string | Optional. Output naming pattern, e.g. `invoice-{{ invoice_no }}`. |\n| `group_by` | string | Optional. Column key to group rows by, giving one multi-page PDF per distinct value. |\n\nReturns the job id to poll with `get_job`. Worth knowing: `filename_template`\nand `group_by` are persisted to the template and the project respectively, so\nthey change the defaults for later runs too.\n\nIf the server predates the public batch endpoint, the tool reports that batch\njobs are unavailable rather than failing obscurely. The three dataset tools do\nthe same for a server that predates the dataset endpoints.\n\n### `get_job`\n\nTakes `job_id`. Returns the status, rows done and failed, and the id and\nfilename of every rendered document. That document list stays empty while the\njob is `queued`, `retry_queued` or `running`, and fills in once the job reaches\n`succeeded`, `partial`, `failed` or `cancelled`. Those document ids are what\n`get_document` takes.\n\n### `get_document`\n\nTakes `document_id` and downloads that single rendered PDF. The ids come from\n`get_job` on a finished batch, and there's no other way to get one. Same return\nas `render_pdf`: path, size, and an inline blob under 512 KB.\n\nIf you want a whole batch, the merged PDF and ZIP in the web app beat fetching\neach document in turn.\n\n### `page_settings`\n\nShared by both render tools. Every field is optional:\n\n```json\n{\n  \"page_size\": \"a4\",\n  \"orientation\": \"portrait\",\n  \"margins\": { \"top\": 15, \"right\": 15, \"bottom\": 15, \"left\": 15 }\n}\n```\n\n`page_size` is lowercase: `a3`, `a4`, `a5`, `letter`, `legal` or `tabloid`.\nMargins are plain numbers in millimetres, not CSS lengths.\n\nRendered PDFs are written to the system temp directory. API errors like a bad\nkey, a missing template or a rate limit come back as tool errors carrying the\nserver's own message.\n\nThe public API allows 120 requests per minute per API key. Past that it returns\n429, and the tool reports that you're rate limited and should retry shortly.\nBatch job creation is metered separately and more tightly.\n\n## Development\n\nYou don't need node or npm on the host: `scripts/dev.sh install`, then\n`scripts/dev.sh deno task build` and `scripts/dev.sh deno task test`.\n`scripts/dev.sh node dist/http.js` runs the HTTP server on the host network.\n\nMIT licensed.\n",
  "bytes": 9607,
  "sha": "ee33f6985d31c51f21bba1d5d0b8b30e5a5b113f82d826c00d5717d28f4f9657",
  "repo_slug": "sheetrender/sheetrender-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_sheetrender_sheetrender_mcp_74208e0c/readme"
}