AI Part 2: The Minimum Viable MCP Server
The Model Context Protocol gives you a lot of freedom. If you’re building it for one user — yourself — most of that freedom is rope. This post is a tour of what I actually have running, and what I deliberately left out.
The shape
A personal MCP server is a tiny HTTP service. Mine is a single Node file with three dependencies — a web framework, the MCP SDK, and a schema validator. It listens on localhost. A reverse proxy on the same box terminates TLS and forwards to it. That’s it for the network shape.
Inside the file there are three responsibilities, in order of appearance:
- An OAuth 2.1 endpoint set, because the MCP client expects them.
- A bearer-token gate on the MCP endpoint.
- The tools themselves — the actual useful surface.
Items 1 and 2 are about a hundred lines of code combined. Item 3 is the rest of the file, and it is where every interesting decision lives.
OAuth, with the multi-user parts removed
The MCP spec assumes you might be exposing the server to multiple clients, which means OAuth, which means discovery metadata, dynamic client registration, authorisation code flow with PKCE, and refresh tokens. You can write all of that for a single user in an evening, because most of it collapses:
- Dynamic client registration returns the same hard-coded credentials every time. There is only ever one client.
- The authorisation endpoint auto-approves. There is nobody to ask. It just issues a code and redirects.
- The token store is a JSON file. Tokens persist across restarts. Refresh tokens rotate. That is the entire identity layer.
What I do not have: a user database, a consent screen, scopes, audiences, multi-tenant token isolation, RBAC, JWKS, anything resembling a real identity provider. The spec accommodates them; my server does not.
Two things I’m glad I built that aren’t strictly required:
- Persistent token store on disk. Without it, every restart of the service breaks the connector mid-session. With it, restarts are invisible.
- Refresh token rotation. A used refresh token is invalidated before the new one is issued. If a token leaks somehow, it stops working the next time the legitimate client refreshes.
The tool surface
The tools are where the choices that matter live. I’ll go deep on tool design in the next post, but the inventory is short and worth listing now:
- A pair for content drafting (
write_draft,read_draft,list_drafts). - A publishing tool (
deploy_post) that flips a frontmatter flag and runs my deploy script. - A pair for site source (
read_site_file,write_site_file) so the model can edit pages and components, not just blog content. - A delete tool (
delete_site_file) for cleanup, with a strict no-directories rule. - A
rebuild_sitebutton for content-free rebuilds.
That’s nine tools. None of them are rich. None of them take complex objects. Each one does a small thing that I would otherwise type into a shell.
What I left out
The temptation when building tools for an agent is to give it everything you wish you’d had in a CMS. I deliberately didn’t:
- No
move_fileorrename_file. The model can read, write to a new path, and delete the old one. Three tools is more legible than one tool with six edge cases. - No git commit, push, or branch tools. My deploy script handles that, atomically, in a way I trust. The agent doesn’t need a git API; it needs a deploy button.
- No tool for editing the server itself. The server runs from a path the tools can’t write to. If the agent wants to add a tool, I have to deploy that change by hand. This is a feature.
- No tool that runs arbitrary shell. That would have made everything else pointless.
Why this stays small
There’s a cost to every tool you add. Each one is a thing the model has to read, choose between, and understand the boundaries of. A ten-tool server is not five times as useful as a two-tool server; past a certain point it gets less useful, because the model spends more attention on tool selection and less on the actual task. Mine is at nine and feels close to its ceiling for content work. If I were building one for a different domain, I’d start at three and add reluctantly.
The next post is about the part of this I find most underrated: writing a tool description that the model will actually use correctly.