Patrick Diaz

Vision · Part 07

MCP servers, read-only tools

Four servers of my own give the models read access to my docs, notes, files and homelab state, behind one gateway, with rights held in the credentials and test data in a scope of their own.

As of 2026-11-12

You are hereVision · map

Measured · aggregates only

What it does

The models reach my docs, notes, files and homelab state through MCP servers I wrote myself: knowledge for docs and notes, files for my PDFs, homelab for the status of machines and services, and one that can only add new notes. Together they offer fifteen read tools and one write tool.

Each server sits behind the gateway, and each layer holds only the rights it needs. Search over docs and notes comes from the knowledge core; which models may use tools at all is decided in agent core.

MODEL SMART · WORKER GATEWAY TOOLS PER KEY KNOWLEDGE DOCS · NOTES · READ FILES MY PDFS · READ HOMELAB STATUS · READ INBOX ADDS NEW NOTES ONLY 15 READ TOOLS · 1 WRITE TOOL results come back marked as untrusted data MODEL SMART · WORKER GATEWAY TOOLS PER KEY KNOWLEDGE DOCS · NOTES · READ FILES MY PDFS · READ HOMELAB STATUS · READ INBOX ADDS NEW NOTES ONLY 15 READ TOOLS · 1 WRITE TOOL dashed: results come back marked as untrusted data
Fig. 7.1 How a tool call reaches my data. Every call passes the gateway, where each key lists the tools it may use. Three servers can only read; the fourth can only add new notes. Each layer holds only the rights it needs, and every result comes back marked as untrusted data.

Why this way

Decision 01

My own servers first, all behind the gateway

None of the servers has a published port. Clients reach them only through the gateway that also carries every model call, so permissions and logs live in one place. Each client key lists the servers and tools it may use, and a tool added by an upgrade isn’t enabled by itself. Public servers may come later, under written rules: pinned versions, a source review before adoption and on every upgrade, and nothing hosted for anything private.

Rejected: each client connecting to each server directly

Every client would need its own credentials for every server, and no single place would show who called what.

Decision 02

Least privilege in the credential, not in the code

Each layer holds only the rights it needs, so a bug in the code above it can’t do more. The files server never sees a folder; it reads views of content that was indexed and isn’t private, so it has no path to get wrong. The keys of my home media server never leave that machine: a small service there answers one read-only question, from a fixed list of fields, and only counts and states come out. Status from my virtualization host comes through a token that can read status and nothing else, and container status through a filter that answers the list and nothing more. The one write tool can only add a new note to the agents’ inbox, and the server picks its name.

Rejected: code that promises to only read, holding a key that can write

Whatever the code promises, the key could still write.

Decision 03

Tool output is untrusted

Every result reaches the model inside a fixed block marked as untrusted data, and a run that has read untrusted content gets no write tools. A note written by a run that read one of my documents is marked as tainted, and that mark comes from the run’s record, never from the model. My notes can’t load remote images when I open them, so an injected instruction can’t use one to send data out.

Rejected: letting the model pass the taint mark as a tool argument

A model that has been steered by injected text would simply leave it off.

Decision 04

Every server has a scope for evals

Each server has a second, separate scope that serves test data only: repo docs, a seed vault of made-up notes, test PDFs, recorded homelab answers, and a throwaway folder for written notes. Evals run the real server code but can’t see or touch my real notes, files or machines.

Rejected: running evals against the production scopes

The agent eval must never see my own notes, and a test write must never land in them.

How it's tested

Nothing planted leaks, and a narrow key stays narrow

Tests plant a title, a path and a fake key in the homelab fixtures, and none of them ever reaches tool output. In a live check, a key narrowed to one tool was offered only that tool, and the gateway refused the others; the eval key sees only the eval scope. Every tool call is its own run record, without the query text, because in an eval the query is a held-out question. Homelab questions scored 1.0 with tools and 1.0 without them, with no invented numbers either way.

A refused tool never costs the reply

One of my messages arrived just before the chat’s key got its tool permission. The tool connection failed, but the answer still came back from smart without tools, and the run record showed the failed step. That case is now a test: a refused key or tool never costs me the reply.

What I learned

A server name took the gateway down

When I added an eval scope for the knowledge server, the gateway refused to start: it doesn’t accept a hyphen in a server name. The mistake had come in with an earlier item and went unnoticed because the gateway wasn’t restarted then. Every chat was down for a few minutes. The lesson went into how I deploy: a config change counts as tested only once the service has restarted on it.

Case 01

A contract field the source couldn’t give

What happened

The homelab contract promised one more detail per container than the filtered status can provide.

What I changed

Rather than widen the filter, I dropped the field and keep a list of the containers I expect on each host.

Result

A missing or stopped container is a finding only if it’s on that list.

Case 02

A missing permission shouldn’t break the chat

What happened

One of my messages came in a moment before the chat’s key was allowed tools, so the tool connection failed.

What I changed

Every client is built so that a refusal degrades to an answer without tools, and the failed step is recorded.

Result

The reply still came, from smart without tools. A permission that arrived too late cost a tool, not the conversation.