7a2c9d5·Insights·8 min
Your MCP server is not an add-on: it is an interface like the others
When a system already has a web interface and APIs, adding an MCP server feels like the last lap: a few functions exposed to an assistant. That is a mistake of perspective. If an agent can do something the interface doesn’t allow, or the other way round, you have two systems with different rules.
Same operations, three doors
The principle is easy to state: everything that can be done from the interface must be doable over MCP, and vice versa, with the same permissions and the same history. Not as a discipline to remember, but as a consequence of the structure.
To get there, every operation is defined exactly once: an identifier, whether it reads or writes, the permission it needs, the schema of what it accepts and returns, the event to record. A single executor runs it, whether it comes from a page, an API or an assistant: it validates, checks permission and scope, opens the transaction, writes the history. MCP tools are generated from these definitions and none is written by hand: when an operation changes, its tool changes with it.
What a tool has to do besides work
- Separate reading from writing. Distinct tools, with a title and annotations (read-only, destructive, idempotent), so the client can treat them differently.
- Take its perimeter from the connection, not from arguments. The organisation is written into the token at consent time. The effective permission is the intersection of what the application asked for and what the user can do.
- Have the server confirm destructive actions. The first call doesn’t delete: it replies with a summary of what is about to happen and a code to pass back after the person says yes. It isn’t enough for the client to ask: the server verifies.
- Return data, not instructions. What a tool returns (notes, names, descriptions written by others) is marked as data. Tool descriptions are fixed text in the code, never read from the database, to close the risk of tampered descriptions at the source.
- Explain errors. A refusal arrives as a tool result, with the same sentence the user would read in the interface: the agent explains why it can’t, instead of blindly retrying.
- Leave a trace of every call. Reads and refusals too, with the interface (MCP) and the client used.
The case no filter covers: read, then write
An agent reads a record written by someone else, reasons about it and then writes. If that text held a hidden instruction, a prompt injection, the write is the moment the damage becomes real. There is no reliable filter that recognises instructions inside text. What a server can do is less ambitious and more honest: after returning rows written by others, it asks for confirmation on the writes that follow, for a limited time (ten minutes, in our case).
It should be said where this defence ends: it asks for confirmation even on perfectly legitimate writes, it can’t tell whether a write is really caused by what was read, and it doesn’t cover text injected into the user’s own data. The cost is one extra confirmation. Better to say so than to suggest the problem is solved.
A detail that taught us something
During tests with a real client, an assistant connected in read-only mode suggested the user write “from the admin panel or directly in the project’s code”. It was right technically and wrong about the design: MCP is a door like the others, and what its tools don’t allow isn’t done elsewhere. Today the server’s instructions say so explicitly, and explain how to reconnect the application with the right rights.
Why it pays to do it from the start
Adding MCP after the first version means revisiting permissions, history and the definition of operations already written. And opening it to external users too raises the bar for everything else: it is the risk of the incident described in this other piece. A declaration of interest: it is how we build our own systems, and the topic is that of AI agents and middleware.