{
  "markdown": "# @spintax/mcp\n\n[![npm](https://img.shields.io/npm/v/@spintax/mcp.svg)](https://www.npmjs.com/package/@spintax/mcp)\n[![CI](https://github.com/investblog/spintax-js/actions/workflows/ci.yml/badge.svg)](https://github.com/investblog/spintax-js/actions/workflows/ci.yml)\n[![license](https://img.shields.io/npm/l/@spintax/mcp.svg)](https://github.com/investblog/spintax-js/blob/main/LICENSE)\n\nA **local MCP server** for [spintax](https://spintax.net) templates: an agent on your machine\ncan validate, render and analyze a template through [`@spintax/core`](https://www.npmjs.com/package/@spintax/core),\nthe reference engine — over stdio, with no network call and no size caps.\n\nThere is also a hosted door at `https://spintax.net/mcp` (in the official registry as\n`net.spintax/mcp`). Use this package instead when you want:\n\n- **no caps** — the hosted server stops at 8 KB of template and 20 variants, because it pays for\n  its own CPU; the templates people actually ship are bigger than that;\n- **no network** — local-first and air-gapped setups included;\n- **`#include`** — resolving partials from disk, which a hosted server must never do.\n\nThis package holds **the tool module and the dispatcher both doors are meant to run** — one source\nof tool definitions, so a renamed tool or a changed result shape cannot differ between them. The\ntool list here is asserted, byte for byte, against the list the hosted server currently serves; the\nhosted server's own switch to this module is the next change on that side.\n\n## Use it\n\n```jsonc\n// Claude Desktop, Claude Code, Cursor, … — an MCP client config entry\n{\n  \"mcpServers\": {\n    \"spintax\": { \"command\": \"npx\", \"args\": [\"-y\", \"@spintax/mcp\"] }\n  }\n}\n```\n\nWith partials on disk:\n\n```jsonc\n{\n  \"mcpServers\": {\n    \"spintax\": {\n      \"command\": \"npx\",\n      \"args\": [\"-y\", \"@spintax/mcp\", \"--include-root\", \"/abs/path/to/partials\"]\n    }\n  }\n}\n```\n\nOr install it and run the binary directly:\n\n```sh\nnpm install -g @spintax/mcp\nspintax-mcp --help\n```\n\n## Tools\n\n| Tool | What it answers |\n|------|-----------------|\n| `validate_spintax` | Diagnostics with severity, a stable code and 1-based line/column. No `error` ⇒ structurally sound; read the warnings too. |\n| `render_spintax` | N variants. With a seed it is deterministic — variant *i* uses seed `<seed>#<i>`. |\n| `analyze_spintax` | Which variables the template needs, which directives it defines, best-effort construct counts. |\n\nTwo things worth knowing, because they are the traps this server exists to make visible:\n\n- **Plural arity is locale-sensitive, and a locale is not optional in practice.** With no `locale`\n  the engine files no arity *verdict* — the template may well be right for the locale you will\n  render with, and failing it here would fail a good template. What it does file is a\n  `plural.locale-missing` **warning** whenever a block's form count is not the 2 that `render`\n  defaults to, because that block will otherwise reach your finished text as `｛plural …｝`. So\n  \"no error\" is not the whole answer: name the locale, or read the warnings.\n- **The engine is lenient.** Structural mistakes never throw; they surface in the output, with\n  fullwidth braces `｛…｝` marking markup the parser could not read. Run `validate_spintax` first.\n\n## Options\n\n| Flag | Default | Notes |\n|------|---------|-------|\n| `--include-root <dir>` | — | Resolve `#include` against `<dir>`. Without it, an `#include` line is **inert**: it stays in the output verbatim. |\n| `--max-variants <n>` | `50` | Cap for `render_spintax`'s `count`. |\n| `--max-depth <n>` | `20` | `#include` / nesting depth guard. |\n| `--max-include-bytes <n>` | `1048576` | Refuse an `#include` file larger than this. |\n| `--max-message-chars <n>` | `8388608` | Refuse a single JSON-RPC message longer than this. |\n\nEach has an `SPINTAX_MCP_*` environment fallback (`SPINTAX_MCP_INCLUDE_ROOT`, …), used only when\nthe flag is absent.\n\nThere is deliberately **no cap on the template itself** — removing it is the point. One limit can\nstill meet a very large one, and it is a flag rather than a secret: `--max-message-chars` bounds the\nwhole JSON-RPC line a template arrives in. It exists because a message past a few megabytes almost\nalways means a client has lost the newline framing, not that someone wrote an 8 MB template; if you\ndid, raise it.\n\n### `#include` from disk\n\nA ref is untrusted template data, not a path an operator typed, so `--include-root` is a jail and\nnot a hint. A ref must look like `partials/opener.txt` — `[A-Za-z0-9._-]` segments joined by `/` —\nand the resolved real path must still sit inside the root, which is re-checked **after** resolving\nsymlinks. That last step is what stops a link or junction inside the root from reading\n`~/.ssh/id_rsa`. Non-files are refused (a FIFO would hang a synchronous resolver forever), a size\ncap applies, and a leading BOM is stripped. Nothing raises: every refusal is a miss.\n\nBecause the engine drops a cyclic or too-deep `#include` to an empty string *before* the resolver\nis asked, `render_spintax` attaches an **include report** telling the two apart:\n\n```jsonc\n\"include\": {\n  \"root\": \"/abs/path/to/partials\",\n  \"maxDepth\": 20,\n  \"resolved\": [\"opener.txt\", \"cta.txt\"],\n  \"missing\": [{ \"ref\": \"signoff.txt\", \"reason\": \"not-found\" }],\n  \"suppressed\": [{ \"ref\": \"loop.txt\", \"reason\": \"cycle\" }],\n  \"truncated\": false\n}\n```\n\nIt is **best effort** by construction: an `#include` produced by a spin choice\n(`{#include \"a\"|plain text}`) is invisible to static analysis, so a suppressed one can go\nunreported. With a root configured, `validate_spintax` also gets the refs that really resolve as\nits allow-list, which turns a broken partial into a diagnostic with a line and column — as long as\nat least one sibling resolves (the engine files those verdicts only for a non-empty allow-list).\n\n## Embedding it\n\nThe package's main entry is the transport-free half — the dispatcher, the tool builder and the\nengine wrappers — and it imports no Node builtin, so it runs on Cloudflare Workers and in a browser\nunchanged. That is not a side effect; it is why the hosted server can share it:\n\n```ts\nimport { buildTools, createDispatcher } from '@spintax/mcp';\n\nconst tools = buildTools({ maxVariants: 20, maxTemplateChars: 8192 });\nconst mcp = createDispatcher({\n  serverInfo: { name: 'my-server', title: 'My server', version: '1.0.0' },\n  instructions: 'Spintax tools.',\n  tools,\n  limits: { maxVariants: 20, maxTemplateChars: 8192 },\n});\n\nconst outcome = await mcp.dispatch(await request.json(), {\n  get: name => request.headers.get(name),\n});\n// { kind: 'accepted' } → HTTP 202, no body\n// { kind: 'response', body, httpStatus } → JSON with that status\n```\n\nCaps are **parameters, never constants**: they are interpolated into tool descriptions and JSON\nSchemas, so a server that hardcoded them would publish schemas that lie. The `headerAdapter`\nargument is how header mirroring stays out of the shared code — stdio has no header layer at all,\nwhich the MCP spec is explicit about, so the transport simply passes nothing.\n\nThe wire protocol is hand-rolled (revision **2026-07-28**, plus `initialize` for the four earlier\nrevisions) and the only runtime dependency is `@spintax/core`. No SDK: that decision belongs to the\nhosted server's ADR 0005 and this package continues it.\n\n## License\n\nMIT — see [LICENSE](https://github.com/investblog/spintax-js/blob/main/LICENSE).",
  "bytes": 7402,
  "sha": "4cff5082d1d773c262769d0d82d07a161d9eb65f36a357a6a6cde375b39ccf09",
  "repo_slug": "",
  "fonte": "npm",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/mcp_net_spintax_mcp_0be7fdea/readme"
}