When an agent can already write the file, what is an MCP server for?

Agents are very good at writing files. So when a tool’s input is a file, an MCP server in front of it starts out with a problem: the agent can already do the job without it. The server has to earn its place by doing something a file can’t. Most of the ones I’ve considered building didn’t.

This post is about the one that did, and about how much narrower its win turned out to be than the plan said.

The tool is Teleplot. It’s a view-only dashboard server for Elasticsearch, a single Rust binary that renders existing Kibana dashboard exports and its own YAML dashboards, and reloads a dashboard whenever its file changes. I built it for software-delivery analytics (DORA metrics, cycle time, throughput) and it never writes to Elasticsearch. The whole project so far is 26 commits over 2 and 3 October 2026. The part this post is about is four of them on the 3rd: a plan for an MCP endpoint, the endpoint itself (about 2,900 lines across 22 files), a round of fixes from running real agents against it, and an agent skill that teaches an agent to use Teleplot on a real project.

I’ve made this mistake before. In August, for tropism, my architecture ruleset checker that I wrote about here, an early product review made the MCP server the headline feature. The reasoning sounded right: the command line competes with entrenched tools, and the MCP server had no competitor. The review also set a gate. Before building, check that the best question you’d put to the server is actually answerable.

It wasn’t. The flagship tool asked “why is this package in my dependency tree?”, which needs a resolved dependency tree, and four of the ten languages tropism supports never have one in any lockfile. The other feature billed as better through MCP, reading the ruleset, turned out to be a 111-line TOML file with the reasons written in it. An agent doesn’t need a protocol to read that. It needs a file read. tropism’s MCP server shrank from product to a thin adapter, and the release pipeline moved ahead of it.

So when I wanted agents to author Teleplot dashboards, the question wasn’t how to design the MCP tools. It was whether there should be any. Teleplot dashboards are YAML files that hot-reload. An agent that can write YAML can already make a dashboard. I wrote down the gate before anything else: the endpoint is only worth building if it gives the agent something the file cannot. The plan names four things, and they’re the test I’d now apply to any MCP server.

Discovery. A dashboard file names an index and some fields. It doesn’t say which indexes exist, what fields they have, which are dates, which can be grouped by, or what values a team field actually holds. An agent writing a dashboard from the file format alone is guessing field names, and the skill I wrote afterwards says plainly that guessing field names is the commonest cause of empty panels. So two of Teleplot’s tools are about the data, not the file: one lists indexes with document counts and time fields, and the other describes an index, with its field types, top values for keyword fields and one sample document.

Validation against the real data. A date histogram on a keyword field is valid YAML. So is a field that doesn’t exist, or a formula that refers to a column nobody defined. A schema can’t catch any of these, because whether they’re wrong depends on the cluster. Teleplot’s validation parses the dashboard and then checks it against Elasticsearch’s live field capabilities, which is the API that says what each field is and what you can do with it. Every problem comes back with a path into the dashboard, in the form panels[3].content.layers[0].columns[2], and a message that says what to change.

Previewing numbers before saving. This is the one I’d defend hardest, because it’s the only check that asks whether the dashboard answers the question rather than whether it runs. The preview tool runs a draft, or one panel of it, through the same query engine the UI uses, and returns the first rows of each layer with the minimum, maximum and last value of each numeric column. “Change fail rate by team” can validate perfectly and still be counting the wrong documents. Seeing a team at 27% when you expected single figures is how you find out before anyone reads it.

A safe save. Writing a file is easy. Writing a file into a directory that people also edit, without clobbering their work, is not, and an agent writing files directly has no reason to be careful about it. The save had to come with rules.

The gate check in the plan was one concrete request: “build me a dashboard of change fail rate by team for last quarter, with the DORA benchmark bands.” It’s answerable by the four capabilities: describe the index to find the result and team fields, preview to see the per-team numbers, save to write the file, and it renders. That request became the acceptance test.

The save rules are where most of the design went, and they’re the part I’d copy into any tool that lets an agent write.

Agent dashboards go into their own folder, separate from the dashboards people wrote and the Kibana exports. The agent never supplies a path, only an id, and the id has to be a short lower-case slug, so there’s nothing to traverse. An id that belongs to a dashboard outside the agents’ folder is refused, with a suggestion to save a copy under a new id. Files Teleplot didn’t write, including every Kibana export, are read-only to the agent.

Replacing a dashboard the agent wrote earlier needs proof of what it read. Reading a dashboard returns a content hash, and saving over it requires that hash back. If the file changed in between, perhaps because I edited it by hand, the save is refused with a message to read it again and apply the change to the current version. It’s the same precondition as an HTTP If-Match header, and it exists so an agent can’t silently undo a change it never saw. The previous version moves into a history folder, keeping the last 20, so undo is copying a file back. The write itself goes to a temporary file and is then renamed into place, so the UI’s file watcher never loads half a dashboard.

Two rules are about what the agent can’t do at all. There’s no delete tool: a person removes files, and an agent can only add or replace them. And writes are enabled only while Teleplot is bound to a loopback address or running over stdio. On any other address, the endpoint is read-only. Teleplot has no login, so that’s the line between “an agent on my machine” and “anyone who can reach the port”.

One smaller decision turned out to matter for agents specifically. Problems an agent can fix, like a missing field or a stale hash, come back as ordinary tool results it can read and act on. Protocol errors are kept for requests that can’t be understood at all. An agent handles “this field doesn’t exist, did you mean team.keyword?” well. A protocol error tends to end the attempt.

The rest of the shape is deliberately small. There are seven tools, and the research behind the plan found community Metabase servers exposing more than a hundred. There are two resources: the dashboard format’s guide and its JSON Schema. The schema is generated from the same Rust types that parse dashboards, so the model, the agent and an editor’s YAML autocomplete all describe a dashboard the same way. I used the official Rust MCP SDK rather than writing the protocol layer myself, because the protocol is still moving and the SDK moves with it. It already serves both the stateless 2026-07-28 spec and the older session-based one.

The plan’s done-when didn’t stop at CI. It asked Claude Code, connected once over HTTP and once over stdio, to build and save the gate-check dashboard using only the MCP tools, and then for me to check the saved dashboard in the browser against the numbers the integration tests expect.

The first HTTP run used all seven tools and saved a correct dashboard. That sounds like a pass, and it wasn’t. Two of the tools, preview and describe-index, failed the client’s own check of their results against their declared output schemas. Fields the server omits when they’re empty had been declared required, so whenever one was empty the client rejected the whole result. The agent did what agents do: it fell back to the shell and finished the job another way. The same run also found that a table ordered by a computed formula column failed at query time.

None of the tests had caught the schema bug, and it’s worth being precise about why. The tests checked that each tool returned what the code meant to return. Nothing checked what the tool said it would return, which is the only thing the client sees. The server was internally consistent and publicly wrong. The fix was to make optional fields optional. The lasting change was that every MCP test now validates its result against the tool’s declared output schema, so the contract is tested from the client’s side.

After the fixes, the second HTTP run used only the MCP tools: list indexes, describe the index, read the example dashboard, validate, preview twice, save. It took 10 turns, and the per-team rates were right, from checkout at 3.1% to data at 27.3%. It also hit one more client error, reading a resource, because 2026-07-28 clients require a cache hint the server wasn’t sending. The stdio run asked for something harder: a director’s dashboard with weekly deployments by team, median lead time inside a 25th–75th percentile band, and a team table. It saved in 11 turns with no tool errors, and the agent’s description of what the dashboard showed matched the data.

The fixes from those runs came to about 420 lines across nine files. They’re small, and every one of them was invisible to a test suite written by the same person who wrote the server. That’s the result I’d generalise most confidently from this. An MCP server is a contract with clients that check it more strictly than you do, and only a real client will tell you whether you’ve kept it.

Here’s the complication I didn’t see when I wrote the plan.

When the tools failed in that first run, the agent fell back to the shell and still produced a correct dashboard. That’s not an accident of one run. The same day, the endpoint landed with two command-line siblings: teleplot lint, which runs the same validation code (with --online, against the live cluster’s field capabilities), and teleplot schema, which prints the JSON Schema. teleplot query already ran a dashboard’s queries and printed every panel as a table. The agent skill I wrote straight afterwards gives an agent a shell route for every step: read-only curl against Elasticsearch for discovery, lint --online for validation, query for preview, and then writing the YAML file.

So three of the four capabilities in the gate are about Teleplot having a live connection to the cluster, not about MCP. A file plus a linter that can see the cluster gets most of the way there. I think that’s the right design rather than a failure of the endpoint, and it’s what the lint module’s own description says it’s for: a person in CI and an agent get the same answers. But it does mean the argument “a file can’t tell you the field types” is really an argument for a cluster-aware linter, and only secondarily one for MCP.

What MCP adds on top is narrower. The preview and validation come back as structured results with declared schemas, not as text the agent parses from a table. The tools announce themselves in the agent’s tool list, where a CLI has to be discovered. The tropism note called that a distribution argument rather than a capability one, and I still think that’s fair. And the save rules hold. An agent with a shell can write any file it likes into the dashboards directory, and none of the hash checks or folder rules apply. The rules are enforced, but only on the MCP path. That makes the endpoint the right interface for an agent client that has the tools you give it and nothing else, and a politeness layer for an agent that also has a shell.

That isn’t the claim I started with. It’s the honest one.

The broader claim is this: most MCP servers should fail the file gate, and be a file format plus a linter instead. I’m arguing it from two worked examples, tropism and Teleplot, which went opposite ways. That’s enough to show the gate discriminates. It isn’t enough to say what fraction of servers would fail it, and I haven’t surveyed any.

The reasoning is what I’d rely on. If a tool’s input is a file, the agent already has a reliable, inspectable, diffable way to produce it, and the person reviewing the work already knows how to read it. Wrapping the write in a protocol adds a second interface to keep in step with the first, a schema contract that real clients will hold you to, and a surface that’s harder to see in a code review than a YAML diff. That cost is worth paying only for what a file can’t carry. Teleplot’s version of the gate comes down to four questions:

  • Does the agent need live state the file doesn’t contain, such as what data exists and what values it holds?
  • Is there a check only the running system can make, one where valid syntax can still be wrong?
  • Is there a result the agent should look at before committing to it?
  • Does the write need a precondition or a boundary that a plain file write can’t enforce?

If the honest answer to all four is no, the agent needs your file format documented and a linter it can run, and nothing else. If the answers are yes, look again at whether a CLI covers them, because in Teleplot’s case it covered three out of four. What’s left for MCP is typically the fourth: rules about writing that have to hold even when the agent would rather not follow them.

There’s a related idea I’ve written about before. In brake, the point was to put the answer to “who does this break?” in files in the working tree, so an agent could check it without the network. Files are the agent’s native interface. They’re also the reviewer’s. An MCP server is a decision to step outside that interface, and I think it should need a reason you can write down before you build it.

For Teleplot, the reason I wrote down was mostly right and partly wrong. Discovery, validation and preview needed the cluster, not the protocol. The save needed the protocol. The acceptance runs needed real agents, because the most important bug was in a contract no test of mine was reading. If I were doing it again, I’d still build the endpoint, but I’d build lint --online first and ask the gate question a second time once it existed.