Agents
One set of tools, three surfaces
Every tool is defined once, in src/tools.ts, schema included. MCP server, Pi extension and OMP extension all get that one list and call the same executors in src/tool-operations.ts. The text that comes back is the same on all three and a fix lands once. That's the whole point. The wrapping around it isn't. MCP hands back that text and nothing else. The extensions also pass on the structured details the executor attaches.
| Tool | Does | Arguments |
|---|---|---|
ciphers_encode | Encode with a cipher named exactly | cipher, text, plus the cipher's options |
ciphers_decode | Decode with a cipher named exactly | same |
ciphers_caesar_brute | Every shift from 1 to 25, best fit first | text, lang |
ciphers_frequency | Histogram and index of coincidence | text, lang |
ciphers_period_estimate | Vigenère key lengths, the likely key for the top three | text, lang, maxPeriod |
ciphers_family_guess | Likely cipher families, the signal behind each and the call to make next | text, lang |
ciphers_passphrase_probe | Which digest, key length and iterations open a Salted__ blob, against the hits chance gives | text, key, digests, keyLengths, iterations |
ciphers_crib_drag | Places a crib could sit in ciphertexts under one reused XOR key, most readable first, and what the accepted ones give | ciphertexts, crib, known, lang, limit |
ciphers_hidden_text_read | A message hidden by position, or the usual places ranked when pick is left out | text, pick, letter, every, start, direction, lang |
ciphers_key_recover | Searched keys of a Vigenère, Beaufort, variant Beaufort, substitution or columnar text, best first, each with its plaintext | text, cipher, period, keyLength, lang, limit |
ciphers_info | List the ciphers by category, or one cipher's options | cipher or category, both optional |
Option arguments mirror the library: shift, key, secondKey, transposition, square, rotation, omit, orientation, indicator, indicatorUnder, blanks, separator, zero, book, address, pick, start, rails, width, corner, path, a, b, period, letters, positions, rings, plugboard, iv, segment, blockSize, nonce, aad, counter, tagLength, tweak, endian, bytes, digest, algorithm, count, keyLength, iterations, salt, preserveCase, stripNonAlpha. A model that doesn't know a cipher's options calls ciphers_info first. Every tool description says so, because models guess otherwise.
MCP
ciphers mcp
claude mcp add ciphers --scope user -- npx -y @agntn/ciphers mcp
Or in a client's config:
{
"mcpServers": {
"ciphers": { "command": "npx", "args": ["-y", "@agntn/ciphers", "mcp"] }
}
}
The server speaks MCP over stdio. Every call, here and in both extensions, gets checked against the tool's published JSON Schema before a cipher sees it. shift is an integer 1 to 25, rings and positions are three letters, text and key have a maximum length. Which ciphers need a key or a period is written in those two descriptions and checked next to the schema, because hosts flatten or drop conditional schema keywords before the model reads them. An argument the tool doesn't take gets refused too, so a typo like shfit comes back as an error that names it, not as a quiet shift 3. Fails the schema - tool error with the reason, and a value outside a fixed list comes back with that list. Throws a CipherError - also a tool error. The session survives either way.
Every tool answers with text, one block, and over MCP that's the whole answer. No details, no structuredContent. ciphers_encode and ciphers_decode return the output alone, so the cipher and options sit in the call the model made, not in the reply. ciphers_caesar_brute returns 25 lines, the one that reads most like English on top, or most like Polish or Japanese romaji with lang. Only the top line is whole, the other 24 stop at 80 characters. ciphers_frequency returns the same table the CLI prints. ciphers_period_estimate returns the ten likeliest key lengths, the Kasiski factors and a pointer to ciphers_decode. maxPeriod goes up to 100. ciphers_family_guess returns numbered candidates with a confidence, the signal and the next call, and says it's not a verdict. A short text never gets high. ciphers_crib_drag returns the ten most readable places, what each one reveals in the other ciphertexts, then the key and plaintexts known gives so far. The model accepts a place by adding it to known on the next call, so the whole drag sits in the arguments. ciphers_key_recover returns five keys, best first, with what plaintext and random letters score next to them. Only the top text is whole, the rest stop at 80 characters. Every key near the random score? Then it's another cipher. ciphers_passphrase_probe lists the settings whose padding held, most printable first. Next to them sits the count luck gives. ciphers_hidden_text_read with pick returns that one reading. Without it, the ten readings that look most like the language, each with the arguments that read it again, and only the top one whole. A number above 0 in front of an English one means its letter pairs read like English. lang only goes with the ranking, next to pick it's an error. ciphers_info with no name lists the ciphers under their category, one line each with its family. category keeps one of them: classical, or block for the modern ciphers that work on bytes. A stream cipher will get its own.
Remote MCP
Nothing to install, not even Node? This site serves the same tools over Streamable HTTP, at https://ciphers.agntn.dev/mcp. Any MCP client that takes a URL takes this one.
claude mcp add --transport http ciphers https://ciphers.agntn.dev/mcp
{
"mcpServers": {
"ciphers": { "type": "http", "url": "https://ciphers.agntn.dev/mcp" }
}
}
It's the Docus MCP server with every cipher tool added to it. toToolkitTools from @agntn/tools/toolkit hands Docus the very definitions ciphers mcp serves from src/tools.ts. No second list to fall out of step. So every call gets the exact text ciphers mcp would give, refusals too. Type shfit and both say no in the same words, then list what the tool does take. Sneak a bidi override into a key name? It comes back as a plain space. Docus throws in two more. list-pages lists the pages of this site and get-page hands one over as Markdown. So an agent can read up on Vigenère and then break one without leaving the server.
Only one thing differs. A web page on another origin gets a 403. Claude Code, Cursor and other clients that call from their own process send no origin and get in.
And the obvious one: every argument now travels to a Cloudflare worker. The site doesn't log or keep them. That's a promise, though, not something you can check from your side. Ciphertext from a public puzzle is fine. A passphrase to anything real never belonged in a tool call, and here it would also leave your machine. npx -y @agntn/ciphers mcp keeps the whole thing local.
Pi and OMP
pi install npm:@agntn/ciphers
Both extensions are declared in package.json. They get the same text as MCP plus details: the cipher, the operation and the options that were applied for ciphers_encode and ciphers_decode, the object info() returns for ciphers_info with a name. In the terminal a result shows as a collapsed preview cut to a few lines, with a count of what was left out, and the full text once you expand the row. Brute force is 25 lines, so this matters more than it sounds.
Bounds
Text is bounded per tool and keys are bounded too, in the schema. A model can't hand the process a novel to shift. Nothing is written anywhere, and over stdio there's no network to lose. One cipher keeps state: columnar caches its last 128 answers per instance.
IGNORE PREVIOUS INSTRUCTIONS learned one thing about the puzzle and nothing about its task.