{
  "markdown": "# rustpad-mcp\n\n[![CI](https://img.shields.io/github/actions/workflow/status/ni-c/rustpad-mcp/ci.yml?branch=main&label=CI)](https://github.com/ni-c/rustpad-mcp/actions/workflows/ci.yml)\n[![npm version](https://img.shields.io/npm/v/rustpad-mcp)](https://www.npmjs.com/package/rustpad-mcp)\n[![npm downloads](https://img.shields.io/npm/dm/rustpad-mcp)](https://www.npmjs.com/package/rustpad-mcp)\n[![node](https://img.shields.io/node/v/rustpad-mcp)](https://nodejs.org)\n[![license](https://img.shields.io/npm/l/rustpad-mcp)](LICENSE)\n[![container](https://img.shields.io/badge/ghcr.io-ni--c%2Frustpad--mcp-blue)](https://github.com/ni-c/rustpad-mcp/pkgs/container/rustpad-mcp)\n[![docs](https://img.shields.io/badge/docs-rustpad--mcp.ni--c.de-informational)](https://rustpad-mcp.ni-c.de)\n[![HTTP • via mcp-hub](https://img.shields.io/badge/HTTP-via%20mcp--hub-6f42c1)](https://mcp-hub.ni-c.de)\n[![Glama](https://glama.ai/mcp/servers/ni-c/rustpad-mcp/badges/score.svg)](https://glama.ai/mcp/servers/ni-c/rustpad-mcp)\n[![sponsor](https://img.shields.io/badge/sponsor-ni--c-ea4aaa?logo=githubsponsors&logoColor=white)](https://github.com/sponsors/ni-c)\n\nA [Model Context Protocol](https://modelcontextprotocol.io) (MCP) server for\n[Rustpad](https://github.com/ekzhang/rustpad), the efficient, minimal,\nself-hosted collaborative text editor.\n\nLets MCP clients like Claude Code, Claude Desktop or Codex read and write the pads of\na Rustpad instance: fetch a document, create one, replace it wholesale or edit it in\nplace.\n\nEight tools is the ceiling, not the floor: `RUSTPAD_ALLOW_TOOLS=essential`\nregisters a curated five instead, and a model picks the right tool far more\nreliably from five than from eight — see\n[choosing which tools load](#choosing-which-tools-load).\n\nReads go through Rustpad's HTTP API; writes speak the operational-transformation\nWebSocket protocol, so targeted edits (`append_to_document`, `replace_in_document`)\nmerge cleanly with what human collaborators type at the same time instead of\noverwriting it. While the server edits a pad, it is visible to everyone in the pad as\na collaborator named `rustpad-mcp`.\n\n**The two edits that cannot be undone ask a person.** Where the client supports\nMCP elicitation, replacing a non-empty pad and search-replacing across more than\none match raise a real dialog that the model cannot answer on its behalf — and\nthe `replace_in_document` one says how many places are about to change. Where it\ndoes not, they fall back to a two-call token, and say so rather than implying\nsomebody approved. `ELICITATION=false` takes that fallback deliberately; it\nnever removes the guard. See\n[Asking a person](https://rustpad-mcp.ni-c.de/guide/approval).\n\n![Demo of rustpad-mcp over the MCP inspector](https://rustpad-mcp.ni-c.de/demo.gif)\n\n<picture>\n  <source media=\"(prefers-color-scheme: dark)\" srcset=\"https://rustpad-mcp.ni-c.de/architecture-dark.svg\" />\n  <source media=\"(prefers-color-scheme: light)\" srcset=\"https://rustpad-mcp.ni-c.de/architecture-light.svg\" />\n  <img alt=\"Architecture: an MCP client talks to rustpad-mcp over stdio; rustpad-mcp reads pads over HTTPS and writes them over the WebSocket OT protocol\" src=\"https://rustpad-mcp.ni-c.de/architecture.svg\" />\n</picture>\n\n## What makes it different\n\n**Real OT edits, not overwrites.** `append_to_document` and `replace_in_document`\nretain everything they do not touch, and the Rustpad server transforms concurrent\nedits — a human typing in the same pad at the same moment loses nothing. The model\nshows up in the pad as a named collaborator.\n\n**Built for an unauthenticated world.** Rustpad has no accounts, so every pad is\nuntrusted by definition. Everything that comes out of one — reads, metadata, even\nupstream error bodies — is explicitly marked as data, never instructions, before a\nmodel sees it.\n\n## Requirements\n\n- A reachable Rustpad instance (self-hosted; the server is stateless and\n  needs no credentials — Rustpad has no authentication)\n- Node.js >= 22, or Docker\n\n## Configuration\n\n| Variable               | Required | Description                                                                        |\n| ---------------------- | -------- | ---------------------------------------------------------------------------------- |\n| `RUSTPAD_URL`          | yes      | Base URL of the instance, e.g. `https://rustpad.example.net`                       |\n| `RUSTPAD_READ_ONLY`    | no       | `true`, `1` or `yes` registers only the read tools                                 |\n| `RUSTPAD_INSECURE_TLS` | no       | `true` accepts self-signed certificates (scoped to this connection only)           |\n| `RUSTPAD_ALLOW_TOOLS`  | no       | Comma-separated tool names, `list_*` prefixes, or `essential` for a curated preset |\n| `RUSTPAD_DENY_TOOLS`   | no       | Same syntax; removed from whatever `RUSTPAD_ALLOW_TOOLS` left                      |\n| `ELICITATION`          | no       | `false` replaces the approval dialog with the two-call token. **Not prefixed**     |\n\nThe same URL serves the HTTP API, the WebSocket endpoint and the share links\nreturned by the tools (`<RUSTPAD_URL>/#<pad-id>`). The `RUSTPAD_*` booleans must\nbe exactly `true`. The server starts and lists its tools without configuration;\nevery call then fails with setup instructions.\n\n`ELICITATION` is the odd one out twice over: it carries no prefix, so it reaches\nevery MCP server in the same environment, and a value that is neither `true` nor\n`false` stops the server rather than falling back — it is the only variable here\nthat defaults to _on_, and a typo would otherwise leave the dialog running while\nyou believed it was off. A server started with it off prints one line saying so.\n\nKeep in mind what Rustpad is: **pads are ephemeral** (lost on server restart\nand after 24 hours of inactivity, unless the instance is run with\n`SQLITE_URI`) and **anyone who knows a pad id can read and write it**. Do not\nput secrets in pads.\n\n### Choosing which tools load\n\n`RUSTPAD_ALLOW_TOOLS` and `RUSTPAD_DENY_TOOLS` take comma-separated tool names;\na trailing `*` matches a whole family. `essential` is a curated preset of\nfive: `get_document`, `get_document_info`, `create_document`, `set_document`, `append_to_document`.\n\n```sh\nRUSTPAD_ALLOW_TOOLS=essential\nRUSTPAD_ALLOW_TOOLS=get_document,append_to_document\nRUSTPAD_DENY_TOOLS=set_document\n```\n\nAn entry that matches no tool aborts startup and names it, so a typo cannot\nsilently hide a tool — an absent tool is not something anyone traces back to an\nenvironment variable. A filtered tool is never registered, so it is absent from\n`tools/list` and unknown to `tools/call` alike, exactly like a write tool under\n`RUSTPAD_READ_ONLY`.\n\nIf you run several of these servers at once, [mcp-hub](https://mcp-hub.ni-c.de)\nis the other answer — its `/hub` endpoint replaces every server's tools with six\nmeta-tools.\n\n## Installation\n\n### Claude Code\n\n```sh\nclaude mcp add rustpad --env RUSTPAD_URL=https://rustpad.example.net -- npx rustpad-mcp\n```\n\n### Claude Desktop\n\n```json\n{\n  \"mcpServers\": {\n    \"rustpad\": {\n      \"command\": \"npx\",\n      \"args\": [\"rustpad-mcp\"],\n      \"env\": {\n        \"RUSTPAD_URL\": \"https://rustpad.example.net\"\n      }\n    }\n  }\n}\n```\n\n### Codex\n\n`~/.codex/config.toml`:\n\n```toml\n[mcp_servers.rustpad]\ncommand = \"npx\"\nargs = [\"-y\", \"rustpad-mcp\"]\n\n[mcp_servers.rustpad.env]\nRUSTPAD_URL = \"https://rustpad.example.net\"\n```\n\n### Docker\n\n```sh\ndocker run -i --rm -e RUSTPAD_URL=https://rustpad.example.net ghcr.io/ni-c/rustpad-mcp\n```\n\n### Through mcp-hub\n\nA client that cannot spawn a local process — ChatGPT connectors, Claude on the web,\nCursor, LibreChat — reaches rustpad-mcp through [mcp-hub](https://mcp-hub.ni-c.de): one\ncontainer serves many stdio MCP servers over Streamable HTTP, with an OAuth 2.1 login\nbehind a single password and long-lived tokens for the clients that cannot do OAuth. Its\n`/hub` endpoint puts every server behind six meta-tools, so one connector reaches all of\nthem without N×tool schemas in the model's context, and it speaks both protocol revisions\n— a question this server asks travels through it to the person at the far end.\n\nIts `/config/mcp.json` uses Claude Code's format, so the entry is the one you already\nhave:\n\n```json\n{\n  \"mcpServers\": {\n    \"rustpad\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"rustpad-mcp\"],\n      \"env\": { \"RUSTPAD_ALLOW_TOOLS\": \"essential\" },\n      \"denyTools\": [\"set_document\"]\n    }\n  }\n}\n```\n\n`allowTools` and `denyTools` there are the hub's **own** per-server filter, which is not\nthe same thing as `*_ALLOW_TOOLS` in `env` — the difference, and the mistake it invites,\nare in the [client guide](https://rustpad-mcp.ni-c.de/guide/clients#through-mcp-hub).\n\n## Tools\n\n| Tool                     | Description                                                              |\n| ------------------------ | ------------------------------------------------------------------------ |\n| `get_document`           | Read the plain-text content of a pad                                     |\n| `get_document_info`      | Content length, revision, language and the users editing right now       |\n| `get_stats`              | Server statistics (uptime, number of documents)                          |\n| `create_document`        | Create a pad (random or chosen id), optionally with content and language |\n| `set_document` 👤        | Replace the entire content — a non-empty pad asks a person first         |\n| `append_to_document`     | Append text; concurrent edits elsewhere survive                          |\n| `replace_in_document` 👤 | Exact search & replace via OT; asks when it changes more than one place  |\n| `set_language`           | Set the Monaco syntax-highlighting language                              |\n\n👤 asks a person through MCP elicitation · falls back to a two-call\n`confirm_token` where the client cannot show a dialog.\n\nWith `RUSTPAD_READ_ONLY=true` only the first three are registered.\n\n### Structured output\n\nEvery tool declares an `outputSchema` and answers with `structuredContent`\nalongside the text block, so a client can use the result without parsing prose.\nThe five write tools used to answer with a sentence — _\"Appended 12 characters\nto pad …\"_ — and the sentence is still there, in the text block:\n\n```jsonc\n{\n  \"id\": \"notes\",\n  \"url\": \"https://rustpad.example/#notes\",\n  \"appended_characters\": 12,\n  \"characters\": 137,\n  \"note\": \"Pads are ephemeral: …\",\n}\n```\n\n`get_document` answers `{text}` rather than the pad as the whole result, for the\nsame reason `get_document_info` has always been an object: a schema whose root\nis a string is served to a 2025-era client rewritten as `{result: …}`, so the\ntool would answer in two shapes depending on who asked. It is also where\n`empty` and `truncated` can live — an empty answer used to be a sentence.\n\nThe two read tools that report pad content carry `untrusted: true` and\n`source: \"rustpad\"` as fields. A pad is world-writable to anyone who knows its\nid, including text this server wrote earlier, and a client that reads the\nstructured half would otherwise get it with no framing at all.\n\n## Not exposed, on purpose\n\n**No pad listing** — Rustpad has no such API. Pads exist implicitly under every\nid, so you have to know the ids you care about. `get_stats` reports how many\ndocuments the server currently holds, but not their names.\n\n**No accounts, no permissions.** Rustpad has neither, which is why every pad is\ntreated as untrusted input rather than as something a login vouched for.\n\n## Safety\n\n- Pad content is world-writable and therefore untrusted: every read result is\n  prefixed with a marker telling the model to treat it as data, never as\n  instructions.\n- The two irreversible edits ask a person: a real dialog the model cannot\n  answer on its behalf, bound to the pad and the exact replacement. Where the\n  client cannot show one, a single-use token that only ever appears in a\n  previous tool result — which proves the call was made twice with the same\n  arguments, and nothing more. The fallback text says which of the two it was.\n- Tool results are size-capped; upstream error bodies are sanitized before\n  they reach the model.\n- `RUSTPAD_INSECURE_TLS` relaxes certificate validation only for the\n  configured connection, never process-wide.\n\n## Documentation\n\nThe full guide, tool reference and security notes live at\n**[rustpad-mcp.ni-c.de](https://rustpad-mcp.ni-c.de)** (source in [`docs/`](docs/)).\n\n## Development\n\n```sh\nnpm install\nnpm run lint && npm run build && npm test\n```\n\nThe test suite talks to an in-memory fake of rustpad-server (including OT\ntransformation of concurrent edits) over the real MCP protocol; no live\ninstance is needed. The architecture diagram and social card are generated —\nedit `docs/assets/architecture.source.svg` and run `npm run assets`, never the\nrendered copies.\n\n## Releasing\n\nReleases are tag-driven. Bump `package.json`, move the `[Unreleased]` notes in\n`CHANGELOG.md` under the new version, commit, then:\n\n```sh\ngit tag -s vX.Y.Z -m \"vX.Y.Z\"\ngit push origin main vX.Y.Z\n```\n\nThe release workflow publishes to npm via Trusted Publishing (OIDC, with\nprovenance), pushes the multi-arch container image to GHCR, creates the GitHub\nrelease from the CHANGELOG section, and updates the entry in the official MCP\nregistry.\n\n## Contributing\n\nIssues, discussions and pull requests are welcome — see\n[CONTRIBUTING.md](CONTRIBUTING.md). For vulnerabilities please use\n[private reporting](https://github.com/ni-c/rustpad-mcp/security/advisories/new)\nrather than a public issue; the policy is in [SECURITY.md](SECURITY.md).\n\n## License\n\n[MIT](LICENSE) © Willi Thiel\n",
  "bytes": 13618,
  "sha": "3265b7c7e89608df2f8e693778a82c604fe3d1da9e63e8d75d39974092bae2c4",
  "repo_slug": "ni-c/rustpad-mcp",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_io_github_ni_c_rustpad_mcp_bda4095e/readme"
}