Back to all articles
AI / KI Software Development

Build Your Own MCP Server: When Your Agent Gets Its Own Tools

My last post was about connecting someone else's MCP servers to an LLM – Git, Drive, Mail. The obvious follow-up question came fast: "And how do I build one myself?"

Short answer: surprisingly easy. I had my first server running in under an hour. The longest part wasn't the code – it was deciding what the server should actually do.

Here's the smallest useful example: a server that gives my agent access to my own blog. After that, Claude can answer questions like "What did I write about most recently?" – without me copying anything. And the best part: an extended version of this exact server runs live – at the end of this post I'll show you how to add it to your Claude.

What an MCP Server Really Is

The term can be intimidating. It shouldn't be.

At its core, an MCP server is just a program that offers a list of tools. A tool is a function with a name, a description, and an input schema. The LLM reads the descriptions, decides on its own when a tool fits, calls it with the right arguments, and gets a result back.

Communication runs over a transport. For local servers that's simply stdin/stdout – your server reads JSON from standard input and writes JSON back. No HTTP server, no port, no deployment. The model just starts your program as a subprocess. For servers that should run over the network, there's an HTTP transport as well – the principle stays the same.

That's basically it. Tools plus transport. MCP also knows resources and prompts, but for getting started, tools are all you need. The rest is description work.

The Smallest Useful Example

New project, install the official SDK:

npm init -y
npm install @modelcontextprotocol/sdk zod

And then the index.js. The heart of it is one tool that fetches a blog post:

import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js'
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
import { readFile } from 'node:fs/promises'
import { z } from 'zod'

const server = new McpServer({ name: 'blog', version: '1.0.0' })

server.tool(
  'get_post',
  'Returns the full Markdown content of a blog post.',
  { slug: z.string().describe('The slug, e.g. "vibe-coding"') },
  async ({ slug }) => {
    const md = await readFile(`./content/blog/${slug}.md`, 'utf8')
    return { content: [{ type: 'text', text: md }] }
  }
)

await server.connect(new StdioServerTransport())

This actually runs – no pseudocode. A second tool, list_posts, which lists all posts, looks exactly the same – just without parameters. And each tool's description isn't decoration, it's the real interface to the model: it decides whether the agent reaches for the tool at the right moment. I now write these descriptions as carefully as a function comment meant for a colleague.

How Your Agent Finds the Server

The server doesn't run by itself – a client has to start it. In Claude (Desktop or Code), you add it to the configuration:

{
  "mcpServers": {
    "blog": {
      "command": "node",
      "args": ["/path/to/blog-server/index.js"]
    }
  }
}

That's all. On the next start, Claude launches your server as a subprocess, asks "which tools do you have?", and surfaces list_posts and get_post. From now on the agent can call them whenever it sees fit.

For debugging there's the MCP Inspector (npx @modelcontextprotocol/inspector node index.js) – a small web UI where you call your tools by hand before wiring them up to the model. I'd never skip that step.

What I Learned Building It

A few things I already did differently on my second server:

  • Start small. My first instinct was to build ten tools right away. Wrong. One tool that does one thing well beats five half-baked ones. And every tool you hand the model is one more it can call incorrectly.
  • Read-only first. My blog tools only read. The moment a tool writes, deletes, or sends, everything changes – see below.
  • Descriptions are code. A vaguely described tool gets called at the wrong time. A precise description is the difference between "useful" and "annoying."
  • Return errors, don't crash. If a slug doesn't exist, the agent wants a usable error message, not a dead subprocess. Return errors as text content so the model can react.

And the point I can't repeat often enough: the moment your server writes, you're no longer building a toy – you're building a gateway into your systems. Everything I wrote in the Prompt Injection post then applies to your own code. A tool called delete_post sounds convenient – until a manipulated piece of text in the context talks the model into calling it.

Do You Even Need Your Own Server?

Honest question – because often the answer is no. If you want to move data from A to B once a day, a script will do. Cron job, done. An MCP server is the wrong answer to an automation problem.

It gets interesting when you don't know in advance when the data will be needed. That's exactly what my blog server is for: I ask Claude something – about an article, an idea, a topic I've covered before – and the agent decides on its own whether it needs to look into my posts for that. I can't pour that decision into a script ahead of time.

That's my rule of thumb: if you can plan the call yourself, write a script. If the agent should plan the call, build a tool.

Try It: My Server Runs Live

The example in this post isn't theory code. An extended version runs live at https://finkbeiner.me/v1/mcp – using the HTTP transport mentioned above, same principle otherwise. Besides the blog tools, it also serves my projects, skills, and contact details.

In Claude Code, one command is enough:

claude mcp add --transport http finkbeiner https://finkbeiner.me/v1/mcp

In Claude Desktop, go to Settings → Connectors → "Add custom connector" with the same URL.

Then just ask Claude: "What does Patrick write about?" Or, if you happen to be looking for a freelancer: "Is Patrick a fit for my Nuxt project?" – and watch which tools the agent reaches for.

All read-only, of course. I follow my own rules.

Conclusion: An Afternoon, Not Wizardry

Building your own MCP server isn't a big project. It's an afternoon. The SDK handles the protocol details; you basically just write functions and describe them well.

The appeal is real: for the first time you give your agent tools that you defined, tailored to your workflow. That feels different from wiring up yet another off-the-shelf server from a directory.

Which is exactly why the same rule applies as with connecting other people's servers: every tool is a decision. Start with the things that only read. Build the writing part only once you're sure you want the consequences. And then have it ask before something happens – not inform you after it did.

An MCP server gives your agent hands. It's up to you how far they reach.