DEV Community

Ajitkumar Singh
Ajitkumar Singh

Posted on Originally published at engineertoai.com AI-assisted

Read an MCP server before you trust it

Four steps for a new MCP server. 1, pin a version: tool descriptions are instructions a model reads, and an unpinned install fetches whatever was published last. 2, list what it offers with the Inspector's tools/list before any model can; the reference server exposes fourteen tools. 3, read the hints and doubt them: get-env is marked readOnlyHint true and returned DEMO_API_KEY; gzip-file-as-resource is marked openWorldHint true and fetches any URL from your machine, even 169.254.169.254. 4, allow only what you decided: a denied tool is removed from the model's context, and an allow list is safer because tools a later version adds stay off. Read-only describes side effects, not confidentiality.

The quickest way to learn what an MCP server owes its users is to be one first. The reference server that the MCP maintainers publish to exercise every protocol feature is a good place to start: it is safe to run, and it shows exactly why you read a server before you trust it.

Pin the version

An MCP server's tool descriptions are instructions that a model reads. Start a server with npx some-server and no version, and every start fetches whatever was published last, so a later release can change what those instructions say without you noticing. Pin an exact version, and a change reaches you only when you choose to upgrade and read what changed.

List what it offers

The MCP Inspector, the protocol's own debugging client, has a command-line mode that connects, sends one request and prints the answer. Asked for tools/list, the reference server returns fourteen tools, each with a description, an input schema and four annotations: readOnlyHint, destructiveHint, idempotentHint and openWorldHint.

Two of them deserve a second look.

Tool Annotation What it actually does
get-env readOnlyHint: true Returns every environment variable, including the DEMO_API_KEY set for the test
gzip-file-as-resource openWorldHint: true Fetches whatever URL it is given, from the machine the server runs on

get-env really does change nothing, so its annotation is honest. Read-only describes side effects, not confidentiality. Annotations are also claims a server makes about itself, and the specification tells clients to treat them as untrusted unless the server itself is trusted.

Engineering Insight

gzip-file-as-resource is not marked destructive, yet a model, or text an attacker planted somewhere the model reads, can give it any URL: a router's admin page, a service on localhost, or a cloud metadata endpoint such as 169.254.169.254. The response lands in the model's context. That is server-side request forgery, and openWorldHint: true is the only clue in the annotations.

Allow only what you decided

In Claude Code, add the server and then deny the tools you do not want a model to have, in the project's .claude/settings.local.json:

{ "permissions": { "deny": ["mcp__everything__get-env", "mcp__everything__gzip-file-as-resource"] } }
Enter fullscreen mode Exit fullscreen mode

A denied MCP tool is removed from the model's context entirely, not merely refused when called. In Codex, list the only tools the server may expose with enabled_tools. Of the two, the allow list is safer when a server can grow, because a tool added in a later version stays off until you choose to enable it.

A stdio server runs as you, with your files, your network and your environment variables. Installing one is running a program, not adding a bookmark.


Originally published at engineertoai.com. The full lesson is free, with no sign-up: Use a server before you build one.

Top comments (4)

Collapse
 
cryptostrapon profile image
cryptostrapon •

The core point as I read it: a stdio MCP server is just a program running with your permissions, so you audit it for what it can reach, not only for what it claims to do. The annotations are self-reported, which is why the openWorldHint on a fetch tool matters more than the destructive flag staying false. One caveat on the deny list: it hides tools by name, so a server that renames a tool in a later release slips past it. Do you pin by lockfile hash as well as version, so a republished tag cannot change under you?

Collapse
 
heyvaldemar profile image
Vladimir Mikhalev •

A deny list by name has the rename problem; an allow list per caller, kept on the server, does not. In the MCP sandbox I run, policy.json grants each agent identity its tools, the identity comes from a ServiceAccount token checked by TokenReview, and anything not granted comes back as Unknown tool, the same answer as a tool that does not exist.

A renamed or newly added tool reaches no agent until someone writes it into the grant. The decision moves from every client's settings file to one reviewed file.

The threat model states the limit plainly: the server decides who may call a tool, not what a granted tool does. A granted fetch tool pointed at 169.254.169.254 still needs a wall in the network.

Collapse
 
cryptostrapon profile image
cryptostrapon •

Keeping the grant on the server and answering Unknown tool for anything not granted is the part I would copy, because it also stops an agent from probing which tools exist. Your last point is the one people skip: the allow list decides who calls fetch, and only an egress rule decides where fetch can go.

Collapse
 
launchgatecheck profile image
Launch Gate •

The get-env row is the sharpest teaching example here because it splits two senses of "safe" that the annotation vocabulary collapses. readOnlyHint certifies side effects on the world; it says nothing about information flow into the context. A tool can be side-effect-free and still be a disclosure channel - get-env is safe for the filesystem and unsafe for the DEMO_API_KEY. Reading a server before trusting it means auditing both dimensions: what it can change, and what it can disclose. The four hints only encode the first half.

The allow-list point pairs with that. A denied tool is removed from the model's context entirely, which is stronger than a runtime refusal - a tool the model cannot see cannot be talked into misuse by injected text. But deny lists age badly against a growing server: the tool you have not heard of yet is allowed by default. Pin the version, allow-list the surface, and the instruction set the model runs on changes only when you decide it does. That is the same reason the spec calls annotations untrusted claims: everything a server says about itself is content until verified, and your two-row table is exactly where content and capability diverge.