31 tools· updated sep 2026

Writing your own MCP server: the code is the easy part, the tool descriptions are not

Updated Marek.

  • mcp
  • agent-tools
  • agent-security

I run two MCP servers I wrote myself. One reads a calendar feed and offers two tools. The other talks to an ad platform and offers fourteen, four of which can spend money. Both took an afternoon to get working and considerably longer to get right, and the gap between those two states is the whole point of this post.

  • When it is worth it. No official server exists, or the official one is generic and wide when your job is narrow.
  • What the integration is. A small local program, one SDK dependency, a list of tools, a handler per tool. Under 800 lines for the bigger of mine.
  • Where the time actually goes. Not the code. The tool names and descriptions, because those are what the model reads when it decides what to call.
  • The dangerous default. A server that can create things can create them live. Mine creates them paused, and turning one on is a separate human decision.
  • When to skip it. If the task is the same every week, a script on a schedule beats a server every time.

If you are still deciding what to connect at all rather than what to write, start with the MCP servers review.

Why I wrote my own MCP server instead of finding one

Two reasons, and neither is "for fun".

The first is that no server existed for the thing I needed. My calendar lives behind a published ICS feed. There is no vendor server for that, and there should not be, because it is a text file over HTTP with recurrence rules in it. Fifty lines of parsing and my agent can answer what is on today.

The second reason is scope. Official servers for big platforms tend to be wide, because they have to serve everyone. Wide means many tools, and many tools means many descriptions loaded into context before the agent does anything useful. When I only need campaign reporting and creative upload, a server with the full surface of a marketing API is paying context rent for tools I will never call.

That is the honest test for a custom server. Not "could I build this" but "is the thing I need a narrow slice of something wide, or a thing that does not exist yet". Both of mine are the first case, and I would not have written either if a tight official server had been sitting there.

What an MCP server integration actually involves

Less than people expect. Here is the whole shape of the smaller one.

PartWhat it is
DependencyThe MCP SDK, plus one library for recurrence rules
TransportStdio, so the agent starts it as a local process
Tool listTwo entries, each with a name, a description, and an input schema
HandlersOne function per tool, returning text content
ConfigOne environment variable holding the feed URL

That is it. The server declares what it offers when the client asks, then answers calls. The protocol handles the rest. My larger server is the same skeleton with fourteen entries in the list instead of two and a lot more error handling, which is where those 781 lines went.

If you have written a small CLI, you have already written most of an MCP server. The new part is not the plumbing. It is that your caller is a language model rather than a person who read your docs.

The tool description is the interface

This is the part I got wrong first, and the part every quickstart skips.

When an agent decides which tool to call, it is reading your tool name, your description, and your parameter descriptions. That text is the entire contract. There is no man page it can consult, no colleague to ask, no chance to experiment before committing. If your description says "get data" the model will call it hopefully and often wrongly.

What fixed it for me was writing descriptions the way you write an error message: assume the reader is competent, in a hurry, and has no context. Say what the tool returns, say what the parameter expects, and give the format when the format is not obvious. In my ad platform server the aggregation parameter description lists the accepted values inline, because the first version said "granularity" and the agent guessed.

Two habits that came out of it:

  1. Name tools by what they return, not by what they wrap. get_insights beats query_api.
  2. Put the shape of the answer in the description. An agent that knows the output is a list of rows will ask for the right thing the first time.

The wider version of this judgment, how to weigh someone else's server before you connect it, is in choosing an MCP server for AI agents. Writing your own is where you find out how much of that burden you were putting on other people's authors.

How many tools should one server expose?

Mine sit at two and fourteen, and I would not go much past fourteen.

Every tool is a permanent line item in the agent's context, paid on every call in that session, whether the tool gets used or not. Fourteen is already enough that I think about it. The generic servers advertising fifty or a hundred tools are quietly making that decision for you.

The useful framing is that a server is not a library, it is a menu. Nobody wants a menu with a hundred items. If your surface is genuinely that wide, split it into two servers and connect the one the job needs, which is the same reasoning behind keeping a short list of connected servers rather than everything you have ever installed.

A server that can spend money needs a different default

Four of my fourteen tools create things. Campaigns, ad sets, ads, and a generic update call. On a live ad account, those are not read operations with extra steps. They move budget.

The rule I settled on is that creation is allowed, activation is not. Anything the server creates comes into existence paused. Turning it on is a separate action that a person takes, outside the agent path entirely. The agent can do all the assembly, and the last irreversible inch stays manual.

Two self-written MCP servers, their tool counts, and a create path that stops at paused until a person actsserver for a calendar feed2 toolsboth read onlyserver for an ad platform10 read4 createwhat those four create tools actually docreatepausedyou turn it onoutside the agentevery tool description is context, paid on every call
Every tool a server offers is a description loaded into context on each call, so the count is a budget, not a feature list. The four tools that create things do not create them live: they create them paused, and switching one on happens outside the agent path.

This is worth stating plainly because the protocol enforces nothing here. The spec does let a tool carry a destructiveHint annotation, but a client must not trust it unless the server is trusted. The spec also says hosts must get the user's consent before any tool call, yet nothing in the protocol makes them do it. There is no dry-run flag. If your handler calls a create endpoint, it calls it. Whatever restraint exists, you wrote it into the handler yourself, or it does not exist.

So when you write a server that touches anything irreversible, decide the default before you write the handler:

  • Does it create in a safe state, or a live one?
  • Is there an action the agent structurally cannot take, or only one it was asked not to?
  • If it did the wrong thing at three in the morning, what is the undo?

That last question is the same one I applied to database writes in the MCP database notes, and the answer keeps coming out the same way. The gate belongs in code, not in a prompt.

Where the credential lives

Neither of my servers has a secret in it.

The calendar one reads its feed URL from an environment variable, so the URL is in my config rather than my code. The ad platform one takes its token at runtime through a dedicated tool, so the token lives in the session rather than on disk. Neither value has ever been in the repository, and neither is printed in a tool response.

The reason to care is that a custom server is code you will paste around. It goes in a config file, into a backup, maybe into a gist when you ask someone for help. Every one of those is a place a hardcoded token becomes a token you now have to rotate. Reading it from the environment costs one line.

The related habit: do not let a tool echo the credential back. A helpful error message that prints what it tried to authenticate with is a credential in your terminal scrollback and in whatever log that scrollback ends up in.

For a hosted endpoint, follow the remote MCP OAuth walkthrough to distinguish credentials held by the client from tokens forwarded to an upstream service.

MCP server use cases that do not deserve a server

Some things look like they want a server and do not.

A weekly report. Same question, same shape, every Monday. That is a scheduled script running fixed code. Handing it to an agent means an unpredictable path where a predictable one would do, and a model deciding each week what "last week" means.

A single API call you make once a month. The wrapper costs more than the task. Call it from a script.

Something with an official server that fits. Writing your own means you now maintain it against someone else's API changes, forever, alone.

Anything you would not let run unattended. If every call needs your approval anyway, you have built a slower version of doing it yourself.

The pattern behind those four: MCP earns its keep when the question is open-ended and the answer cannot be written in advance. Exploration, ad-hoc analysis, "what does this account look like today". The moment the question stabilises, the right answer is code you can read in a diff.

Should you write one?

Run these five in order. Two noes and the answer is no.

  1. Does an official server exist and fit? If yes, connect it and stop reading.
  2. Is your slice narrow? A custom server is worth it mainly when the alternative is a wide generic one that costs context you do not use.
  3. Is the question open-ended? Repeatable work belongs in a script on a schedule.
  4. What is the worst call it can make? Write that answer down before you write the handler, and make the safe state the default.
  5. Will you maintain it? An API change six months from now is your problem, and nobody is going to file the issue for you.

Mine passed all five, twice. That is why there are two of them and not nine. The next question, once you start connecting servers you did not write, is who is holding your token on the other side, and that one I answered separately in the notes on remote MCP servers.

The rest of what I keep connected, custom and otherwise, is on the tools page.