
Architflix Original
Hardened MCP Servers
Three read-only MCP servers (SQLite, filesystem, web fetch) that treat every tool argument as hostile: SSRF checks that survive DNS rebinding, read-only SQL enforced by SQLite itself, symlink-safe file access, ReDoS timeouts and gzip-bomb caps. 167 tests at 94% statement coverage.
Overview
Hardened MCP Servers is a set of three read-only Model Context Protocol servers by Archit Jain, for SQLite, a sandboxed filesystem and allowlisted web fetches, each built around guards tested against the specific attack they stop, on the assumption that every tool argument comes from a prompt-injected model.
Season 1 · The Build
5 episodesThree read-only MCP servers (SQLite, filesystem, web fetch) that treat every tool argument as hostile: SSRF checks that survive DNS rebinding, read-only SQL enforced by SQLite itself, symlink-safe file access, ReDoS timeouts and gzip-bomb caps. 167 tests at 94% statement coverage.
Built with TypeScript, Node.js, MCP, SQLite, Zod, and 1 more.
Each server gives an LLM client (Claude Desktop, Claude Code or any MCP client) access to a resource that is dangerous to expose naively, built on the official @modelcontextprotocol/sdk in strict TypeScript. The SQLite server accepts one statement at a time and enforces read-only access with SQLite's own sqlite3_stmt_readonly flag on the compiled statement rather than by matching SQL text, so tricks like WITH … DELETE, ATTACH or PRAGMA don't get through; it also sets query_only and trusted_schema=OFF, streams results with a row cap, and stops runaway queries by terminating the worker thread that runs them, because better-sqlite3 queries can't be interrupted. The filesystem server checks path containment both textually and after resolving symlinks, opens files with O_NOFOLLOW, never follows symlinks during search, caps binary and oversized files, and runs user-supplied regular expressions under a node:vm timeout so a catastrophic-backtracking pattern can't hang it. The fetch server enforces host, scheme and port allowlists and runs its private-address check inside the HTTP client's DNS lookup hook, so a domain can't pass the check and then rebind to an internal address; IPv4 addresses hidden inside IPv6 forms (mapped, NAT64, 6to4) are unwrapped and checked, every redirect hop is re-checked, decompressed bytes are capped against gzip bombs, and one deadline covers the whole request. A shared library adds zod-validated configuration, per-tool token-bucket rate limits, stderr-only JSON logs and uniform error codes. 167 tests, including integration tests over stdio, reach 94% statement and 89% branch coverage, with CI on Node 22 and 24. Built by Archit with Claude as the AI coding assistant.
- SQLite: one statement per call, read-only enforced on the compiled statement, query_only and trusted_schema=OFF, streamed results capped at maxRows
- Runaway queries stopped by terminating and respawning the worker thread, with requests queued meanwhile
- Filesystem: containment checked before and after symlink resolution, O_NOFOLLOW opens, no symlink traversal in search, caps on binary and huge files
- Fetch: host, scheme and port allowlists, every resolved IP and every redirect hop checked, a decompressed-size cap, one overall deadline
- ReDoS protection by running regex search under a node:vm timeout
- Shared hardening library: zod config, per-tool token-bucket rate limits, stderr-only structured logs, uniform error codes
Every project hits walls. Here's what nearly broke Hardened MCP Servers — and how it shipped anyway.
- The DNS-rebinding race between checking an address and connecting to it, closed by checking inside the DNS lookup hook
- better-sqlite3 queries can't be cancelled, so long queries are stopped by terminating the worker and replaying the queue
- SQL bypasses such as WITH … DELETE, ATTACH and PRAGMA, handled by asking SQLite whether the compiled statement is read-only
- Symlink-swap races between checking a path and opening it
- Catastrophic regex backtracking and gzip bombs from untrusted content
Every build leaves residue. The takeaways from Hardened MCP Servers that carry forward.
- With LLM tools, the attacker is whoever wrote the text the model read; every argument is untrusted input
- Ask the engine, not a regex: SQLite knows whether a statement writes better than any parser bolted on top
- Map each guard to the attack it stops and test that attack, or the guard is decoration
- Remaining gaps are documented: a theoretical race on intermediate directories (closing it needs openat2, which Node doesn't expose) and no HTTP proxy support