Photo by Nick Fewings / Unsplash

Five Tools, Zero Write Access: Designing an MCP Server You Can Trust (4)

MCP Aug 17, 2026

The last two articles covered what an agent sees (a sound's full provenance record) and how it finds anything at all (hybrid search, running locally). Neither matters if the layer connecting an agent to those pieces can't be trusted. This article is about that layer: the actual Model Context Protocol server, and why handing an AI agent direct access to it doesn't have to be a leap of faith.

The entire surface, named

Here's the complete list of things an agent can ask this server to do: list_catalog_info, search_sounds, describe_sound, get_license, get_preview. That's it. Not a starting list, not the "core" tools with more coming later. Five reads, and nothing else exists to call. No upload tool. No delete tool. No way to edit a catalog entry, change a license, or touch a file on disk. If an agent wants to do something other than look at the catalog, there's simply no function for it to invoke.

Read-only, enforced by the shape of the code

It would be easy to describe that as a policy, a promise we're making about how the server behaves. It's stronger than that. The server opens its index strictly read-only at the storage layer and never runs a write or a migration against it, which means the guarantee doesn't depend on every tool handler individually remembering not to write anything. There's nowhere in the code path for a write to happen even if one were attempted. A container deployment can go one step further and mount the catalog volume read-only at the filesystem level too, so the guarantee holds even if there turned out to be a bug in the application layer nobody caught.

This wasn't an obvious default, either. Adding an early "add to cart" or download-tracking tool was on the table at one point, and it got cut on purpose: a mutating tool is a genuinely bigger trust surface, both from prompt injection steering an agent toward doing something it shouldn't, and from an agent just making an ordinary mistake on a user's behalf. Read-only first removes that whole category of risk before it exists, rather than trying to fence it in afterward. When mutations do eventually show up, the plan is that they require a human explicitly confirming the action, not something an agent can complete unattended.

Two ways to connect, one definition of what's available

The server runs over two different transports depending on how it's deployed, and both offer the exact same five tools because they're built from a single shared registration, not two separate implementations that happen to agree today.

Locally, Claude Desktop or a similar client spawns the server as a subprocess and talks to it over stdin and stdout. There's no network involved at all, so there's no auth layer either. Whatever spawned the process already had filesystem access; the process boundary itself is the trust boundary.

Hosted, over HTTP, the story is different because anyone who can reach the port can otherwise call it: every request needs a Bearer token, checked with a constant-time comparison so a wrong guess can't be timed against a correct one, and a token-bucket rate limiter keyed to the token and the caller's IP, which only starts counting after auth succeeds so a bad token can't be used to burn through a legitimate caller's allowance.

Building the HTTP transport surfaced one genuinely interesting constraint worth mentioning, because it's the kind of thing you only find by actually building the thing: the protocol SDK's stateless HTTP transport refuses to be reused across more than one request; a second call against the same instance throws outright. The fix was building a fresh server-and-transport pair inside every single request instead of reusing one across requests, which sounds wasteful until you remember each request is independent anyway. It's a small detail, but it's the sort of constraint that only shows up once you're actually running real requests through real code, not reading the protocol spec.

What the security model is actually defending against

A few specific, concrete things, rather than "security" as an abstract category:

An agent's conversation includes whatever the catalog says back to it, and catalog content, titles, descriptions, tags, is treated as untrusted input relative to that conversation. None of it is ever inserted into a tool's own description, and no tool executes or fetches anything derived from what's in the catalog. A malicious or just weird description field can't quietly redirect what the agent does next.

Preview links are signed and time-limited rather than being simple file paths. The signature proves the link was legitimately issued for a specific sound and hasn't expired; it says nothing about whether the underlying file path is safe, so the lookup that turns a sound id into an actual file happens through an internal index, never by touching anything resembling a client-supplied path. A client can't forge a link to a sound it was never given, and can't walk that endpoint to enumerate what's in the catalog either.

Errors don't leak internals. Whatever actually went wrong inside a request, a client only ever gets back a generic message and a correlation id; anything with real diagnostic value, a stack trace, a file path, an exception's raw text, stays server-side in the logs, tied to that same correlation id if anyone needs to go look.

Small enough to actually reason about

None of this rests on a lot of moving parts. Five tools. The HTTP transport is plain node:http, not a general-purpose web framework, specifically so there's less surface area and fewer dependencies standing between a request and the code that handles it. A smaller, simpler system isn't just easier to build. It's easier to actually convince yourself is safe, because there's less of it to have gotten wrong in the first place.

What's left

All of this is design on paper until someone actually runs it. The last article in this series is exactly that: cloning the repository, indexing the bundled demo catalog, connecting Claude Desktop, and, if you want it running for real, deploying it with Docker.

Tags