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