When tools became the interface: from a chat bot to 455 MCP tools
Lesson from:
How a Saturday-night agent experiment turned my home lab's scripts into typed tools, and how those tools learned to stop lying
The first commit of my MCP server is stamped Saturday, 21 March 2026, 19:51. Its message says: “initial commit — LSN MCP Server v18”.
Version eighteen. Seventeen versions came before the first one in Git. By 23:40 that night there were fifteen commits, and one of them, at 22:09, has the message that changed how I run my lab: “v4 agentic agent with tool use”.
Today the same server exposes 455 typed tools to AI agents, from Kubernetes and the router to the honeypot, the library and the blog you are reading. This post is not about how the tools are labelled for risk; I wrote that up in Typed risk annotations for 455 MCP tools. It is about the moment before that: when I stopped building interfaces for myself and started building them for machines, and what it took to make them honest.
Contents · 10 sections
In one minute #
- MCP, the Model Context Protocol, is a standard way for an AI model to discover and call tools: functions with a name, a description and typed parameters.
- Before MCP, I ran my lab through scripts and a chat bot with hand-written commands. Every new capability meant a new command, a new parser and new help text.
- In March 2026 I started wrapping those capabilities as typed tools. By 2 April there were 192 tools on 6 servers; by October, 455.
- The surprise was not how many tools a model can use. It was how many ways a tool can lie: work in the list but fail when called, accept a parameter and ignore it, return half an answer that looks whole.
- The turning point was treating tools as an interface with a contract, tested the way a model will call it.
The before: commands, scripts and a bot #
Before the MCP server, my lab had two front doors.
The first was my own shell: scripts for every recurring job, from checking disk usage to rotating logs. They worked as long as I remembered they existed and what their flags meant.
The second was a chat bot on Telegram and Discord, which I still run. On 2 April the record describes it as both a bot and a small REST API, with 15 command modules loaded from a folder: Kubernetes, Docker, the router, the security engine, the NAS, the honeypot, auto-remediation, system services. It ran as root, polled the messaging API, and every command was a function that parsed a line of text.
It was useful: it put the lab in a chat window. But each command was a little island. The help text lived in one place, the argument parsing in another, the actual logic in a third. Arguments were words in a line of text, so their order and spelling mattered. And nothing but me could use it, because nothing but me knew the grammar.
The full story of that bot, including the morning it went silent for six hours because of my own firewall, is in A chat-ops bot is a doorway, not a convenience.
The trigger: one evening of agents #
The evening of 21 March in the Git log is a small history of the idea arriving.
At 20:12 an “autonomous monitoring agent script v2” appears. At 20:41, version 3 “with auto-remediation and ask-first buttons”. Then four commits in a row that fix the same kind of thing: false positives, duplicate cleanup text, duplicate autofix text. An agent that reads alerts and writes messages produces a lot of words, and some of them twice.
At 22:09: “v4 agentic agent with tool use”. At 23:12: “v5 prometheus-powered agent with loki+promql tools”. At 23:17: “systemd service tools via infra-bot API”.
That last one is the hinge. The new agent did not get its own way to restart services. It called the bot’s API, through a tool. The bot’s commands were becoming functions that a model could call, with arguments it could not mistype.
At 23:40, the last commit of the night gives the model a tool to drop the kernel’s page cache when memory runs low. Giving an AI a tool to free memory on your server, before midnight on a Saturday, is either the beginning of something or a warning sign.
The build: from 192 to 455 #
Growth came in waves, and each wave followed something I needed.
2 April: one image, six servers. Version 30 of the server was deployed as six instances from the same image: a main one with every tool, and specialised ones for security, Kubernetes, the workstation, the network and monitoring, 192 tools in all. A client can connect only the slice it needs, which keeps the menu short for a small model.
24 May: the honeypot becomes tools. The day after I rebuilt my security lab from scratch, every one of its 47 honeypot APIs was exposed as an MCP tool, and my local agent could use them from the command line, the web and a chat interface.
6 June: the security toolkit moves in. 45 security and intelligence tools that used to live inside the old lab were migrated into the MCP server as one module. A scan that ran for minutes blocked every other client on the same server, so the same day a separate scanner instance took over the slow work.
16 June onwards: dossiers, library, web threats. Tools to search and build threat-intelligence investigations, to read my offline library, to query the web-threat classifier. By 5 August a rebuild reported 30 of 30 modules and 439 tools.
4 November: the blog. The newest module has ten tools for a blog-publishing service: list, read, scan for leaks, preview, propose. There is deliberately no tool to publish. This post reached my review queue through one of them.
The technical heart: what a typed tool actually is #
For a non-engineer, the difference between a bot command and a tool is the difference between a spoken request and a form.
A bot command is a line of text that a program has to guess the meaning of. A tool is a function whose signature is published as a schema. Here is the shape, in Python, with a generic example:
@mcp.tool()
def pod_status(namespace: str = "", only_problems: bool = True) -> str:
"""Pod health for one namespace, or all if empty.
Start here for 'is anything broken?'. Do not loop over namespaces."""
...
The MCP framework turns that into something the model receives when it connects:
{
"name": "pod_status",
"description": "Pod health for one namespace, or all if empty. Start here ...",
"inputSchema": {
"type": "object",
"properties": {
"namespace": {"type": "string", "default": ""},
"only_problems": {"type": "boolean", "default": true}
}
}
}
Three things follow from that, and each of them changed how I write code.
The description is the user interface. The model never sees my code. It sees the name, the docstring and the types. “Start here” and “do not loop over namespaces” are instructions to a reader who takes them literally.
The types are the validation. A model cannot pass a namespace where a boolean belongs, because the arguments are checked against the schema before the function runs. On the bot, I could.
The output is the next prompt. Whatever a tool returns becomes input to the model’s next step. A tool that returns a tidy-looking wrong answer does not fail. It misleads, at machine speed.
The wrong turns: tools that lie politely #
That third point is where most of my lessons came from. A typed interface makes it easy to build tools. It does not make them honest. The record has a long list.
Listed is not working. On 4 August, after a new module “verified 9/9 loaded”, only calling the tools end to end showed whether their SQL and formatting were right. The observation is one line: “A tool appearing in tools/list proves only that it imported and registered — not that it works.”
Parameters that do nothing. In July an adversarial check found “decorative” pagination parameters: declared in the tool’s signature, so the model happily used them, but never actually sent to the backend. The model asked for page two and got page one again.
Half an answer that looks whole. Database query tools cut results at 200 rows without saying so. A list that has been silently truncated reads as a complete one, and the model reasons from it. The rule since August: every list says how many items it dropped.
Tools that had never worked. On 13 August I found that three of the four database query tools had never worked. One used a database role that did not exist; two had placeholder credentials that were never valid. Agents had quietly worked around them for months, going through a container shell instead.
Phantom problems. On 2 October I asked my local agent a simple Kubernetes question and it reported problem pods that did not exist, after 49 to 60 separate calls. The tool filtered text that had been cut at 4,000 characters; the cut line, and the “truncated” marker itself, did not contain the word “Running”, so they were counted as broken pods. After the fix, the record says, the same question was “answered in 2 tool calls with correct pods, warnings and restarts”.
A dependency that moved under me. On 5 August a one-line change and a rebuild crashed all six servers at once. The framework had released a new major version between two builds, every requirement in the file had only a lower bound, and the new version had removed the module the server imports. The old pods kept serving throughout, so nobody noticed but me. Every dependency now has an upper bound taken from the last image that worked.
A door that should not have been there. Until 10 July, the server was reachable from the internet behind nothing but the single-sign-on login, through a route that an earlier AI agent session had created. It was found and removed that day, and the public address now leads nowhere. A server that can restart your cluster belongs on the inside.
None of these was dramatic on its own. Together they taught me that a tool for a model needs stricter testing than a command for a human, because a human notices when an answer looks odd. A model builds on it.
The after: what changed in how I work #
The obvious change is who does the typing. Many entries in the record since the summer start with a question to an agent and end with the tools it called. The bot still exists, and since October free text in a private chat can go straight to my local agent, with approvals arriving as buttons.
The deeper change is how I think about features. A capability is not finished when it works for me. It is finished when a model that has never seen my code can discover it, call it correctly from its description alone, and get an answer that is either right or clearly marked as partial.
That raised the next question, the one I answered in October with risk annotations: if a model can call 455 tools, which of them can it call without asking? My agent now separates read, write and critical tools, and critical ones need a second factor. The design of those tiers is in Read, write, critical: permission tiers for a local AI agent.
What it cost #
It cost a kind of innocence about my own tools. On 18 October a smoke test called every one of the 192 tools that take no arguments. 171 worked. The other 21 had drifted: a GPU tool still asking for an NVIDIA card after I had moved to AMD, a directory tool defaulting to a folder it was not allowed to read, a client sending the wrong kind of key. Every one of them had looked fine in the tool list.
It cost focus, too. 455 tools are 455 contracts to keep. One person, one machine, and a server that is now more interface than code.
What changed #
- Before: shell scripts and a chat bot with text commands; one user, me.
- After: one MCP server image, six instances, 455 tools any model can discover and call by schema.
- Interface: descriptions written for a literal reader, types as validation, outputs treated as the next prompt.
- Testing: call every tool the way a model will, not just check that it registered.
- Safety: the server stays internal; every tool is labelled read, write or critical.
Questions people ask me #
What is an MCP server, in plain words? #
It is a program that offers a list of tools to an AI model in a standard format. Each tool has a name, a description and typed inputs. The model reads the list, decides which tool fits the question, and the server runs it and returns the result. It is how an assistant goes from talking about your systems to acting on them.
Is it better to have many small tools or a few big ones? #
In my experience, small tools with very clear descriptions, grouped into separate servers so a model only sees the slice it needs. A big generic tool, like “run any shell command”, is powerful and impossible to reason about safely. A specific tool, like “pod status, start here”, tells the model what to do and tells me what it did.
How do you test tools that a model will call? #
Call them the way the model will: with the arguments from their own schema, end to end, against the real backend, and read the output as if you knew nothing else. Check that every parameter changes the result, that lists say when they are truncated, and that errors are errors rather than tidy dashes.
Can I build something like this at home? #
Yes. Start with three tools you use every day, written as plain functions with honest docstrings. The official SDKs for MCP turn them into a server in a few lines. The hard part is not the protocol. It is keeping each tool’s promise as the system around it changes.
The closing #
On my first night with tools, I gave a model the power to free my server’s memory before I had given it a way to tell me honestly how many pods were running. That order was wrong, and most of the work since has been about reversing it.
A tool is a promise written in a schema. The model will believe it completely. If you build tools for machines, build them as if the reader will take every word literally and never ask a follow-up question, because that is exactly what it will do.