MCP Servers
6

Everything I Got Wrong About MCP Before I Built One

What is this MCP thing? Beginner-friendly guide

April 30, 2026

I wanted Claude to be able to write directly to my blog database. Not draft an article and paste it for me — actually insert the row, push the markdown into the right table, set the published flag, the whole thing. So I started reading about MCP servers, because that's apparently the thing now, and within about ten minutes I had a mental model that turned out to be almost entirely wrong.

This post is the wrong model and the corrected one. If you're a Node.js developer who's been hearing about MCP and hasn't actually sat down with it yet, you probably have the same wrong model, and it'll save you an hour to read this before you start.

What I assumed

I figured MCP was something like this: I write a Node.js program that exposes some functions, I run it, it opens a port, and somehow Claude — or whichever LLM — finds it. There's a registry somewhere, or a discovery mechanism, or the LLM "checks for MCP servers" the way a browser checks for installed extensions. I imagined a moment where Claude looks at the user's question, decides it needs a tool, scans for available MCP servers on the network, finds mine, reads the tool descriptions, and calls one.

I also assumed the SDK was doing some kind of magic to make my Node.js code "visible" to Claude. Like the SDK was registering my server with Anthropic's infrastructure, or generating some kind of manifest that LLMs are trained to look for.

None of that is what happens. None of it.

The actual model: Claude has no idea MCP exists

This was the unlock. The thing that made the rest of it click.

The LLM was never trained to know what MCP is. It doesn't "look for" anything. It doesn't have a discovery mechanism. The protocol is entirely between the host application and the MCP server — Claude is just on the receiving end of a regular tool-calling API request, and it has no way of knowing whether the tools in that request came from an MCP server or were hardcoded in the app.

Here's the actual flow:

  1. I write a Node.js MCP server that exposes some tools — say, insertArticle, listDrafts, publishDraft.
  2. I configure my host — the application I'm using, like Claude Desktop, Claude Code, Cursor, Zed — to know about that server. Each host has its own config file: claude_desktop_config.json, ~/.cursor/mcp.json, and so on.
  3. When the host starts, it connects to the MCP server and asks "what tools do you have?" The server responds with the list and their descriptions.
  4. The host takes those tool descriptions and injects them into the LLM API call as regular function-calling tools. Same JSON shape Anthropic's API has documented forever.
  5. Claude sees [insertArticle, listDrafts, publishDraft] in its tool list. It cannot distinguish these from tools the host invented and hardcoded. It has no concept of "MCP origin."
  6. Claude decides to call one. The API returns a tool_use block. The host intercepts that block, sees "ah, this tool came from my MCP server X," routes the call there, gets the result back, feeds it to Claude. Claude continues the conversation.

So MCP isn't a way for LLMs to discover tools. It's a way for hosts to standardize how they connect to tool providers, so you write your server once and any compliant host can use it. Before MCP, every host had to write custom integrations for every tool. With MCP, you write the server once, configure it in any host, done.

The host is the matchmaker. The LLM is downstream of all of it.

Why this matters

Once you internalize that the LLM doesn't know about MCP, a few other things start making sense.

It's why you can use the same MCP server from Claude Code, from Cursor, from Zed, from a custom app you write yourself — they're all hosts, and the protocol they speak to your server is identical. The model behind each of them could be different. The host is what reads your config, spawns your server, lists your tools, and dispatches the calls. The model is just the thing producing the tool_use blocks.

It's also why it doesn't really matter, from your MCP server's perspective, which model is being used. Your server is talking to the host, not the model. As long as the host knows MCP and the model knows function calling, your server's job is the same.

The second surprise: stdio servers don't open ports

This one tripped me up because I'd written the assumption into my mental model: server runs, port opens, host connects to port. That's how every web service I've ever built works.

MCP doesn't have to work that way. There are two transports:

Stdio transport is the most common one for local MCP servers, and there's no port involved at all. The host literally spawns your node server.js as a child process and talks to it over standard input and standard output. JSON-RPC messages go in via stdin, responses come back via stdout, anything you log goes to stderr. It's the same mechanism that's been around since Unix pipes — except instead of grep talking to wc, you've got Claude Desktop talking to your tool server.

This is why local MCP servers feel weirdly invisible. They're not network services. You don't curl them. You don't put them behind nginx. They're processes that the host owns, with a stdio pipe between them.

And here's the part that bit me about ten minutes into building one: stdout is reserved for the protocol. The first time I dropped a console.log("server starting") in for a sanity check, the host dropped the connection without explaining why. No error, no log, just gone. Because console.log writes to stdout, and stdout is the wire — every byte you write into it has to be valid JSON-RPC or you've corrupted the stream. The fix is one character of muscle memory: console.error instead. That goes to stderr, which the host either swallows or surfaces in a separate log, and the protocol stream stays clean. It's the kind of gotcha that's obvious once you've internalized "stdout is the wire," and confusing for an entire afternoon if you haven't.

Streamable HTTP transport (the newer remote one) does open a port. This is what you'd use if your MCP server lives on a VPS and you want Claude.ai web or some hosted client to connect over the internet. There's also the older HTTP+SSE transport, which is being deprecated.

The choice between them isn't a technical preference — it's a deployment-model choice. Local dev tools? Stdio. Remote service for many users? HTTP. Most MCP servers in the wild today are stdio, because most of them are tools devs run on their own machines.

The third realization: the tool descriptions are most of the work

I had assumed the hard part of building an MCP server would be the code — the actual logic of what each tool does. Database query, API call, file write, whatever. The plumbing.

It's not. The plumbing is mostly trivial — most of my tools are 5-15 lines of code that do something I'd write anyway. The hard part is the tool description.

Here's why. When Claude is deciding whether to call a tool, it's reading the tool's name, description, and input schema, and reasoning about whether it fits. If the description is vague — "Inserts an article" — Claude will hallucinate parameters, misunderstand when to call it, or call it incorrectly. If the description is sharp — "Insert a new blog article into the published_articles table. Use this only after the user has explicitly confirmed they want to publish. Required: title (max 200 chars, no markdown), body_markdown (full article body), tags (array of existing tag slugs from list_tags). Returns the inserted article's ID and URL." — Claude uses it correctly.

This shifted my mental model again. Building a good MCP server is mostly an exercise in writing for an LLM as the consumer of your API documentation. The function bodies are downstream of that. There's a whole post coming on this — it might be the most undersold skill in the entire MCP ecosystem.

What about the SDK?

There's a @modelcontextprotocol/sdk package, official, maintained by Anthropic and the MCP project. I assumed it was doing something special — registering my server somewhere, exposing it to LLMs, the magic part.

It's not. The SDK is a thin wrapper around JSON-RPC 2.0 (the message format MCP uses), the protocol's lifecycle handshake (initialize, capability negotiation, initialized notification), schema validation, and transport abstraction (so you can swap stdio for HTTP without rewriting your handlers). That's it.

You could implement an MCP server from scratch in Node.js without the SDK. People have done it in Rust and Go before official SDKs landed. It's maybe a day of spec-reading and careful JSON-RPC framing. The SDK saves you that day; it doesn't enable anything that wasn't there. There's a post coming on this too — building one without the SDK and looking at what the SDK adds — because doing that demystifies the whole thing.

What I'm building next

The fix for my "Claude can't write to my blog DB" problem is concrete now:

  1. A Node.js MCP server with three tools: listDrafts (lists files in blog-drafts/), readDraft (returns the markdown of one), insertArticle (writes to the production blog table).
  2. Stdio transport, because I'll only use it from Claude Code on my laptop.
  3. Bounded permissions: insertArticle only accepts a filename that listDrafts already returned — Claude can't make up an article from nothing and insert it, only publish ones I've already written. The schema constrains the shape of the input; the handler enforces the boundary by checking the filename against the real draft directory before doing anything. Together they're the security wall.
  4. A tool description that makes Claude ask me to confirm before publishing, every single time.

About 80 lines of code. Maybe two hours including testing. I'll write it up properly later in this series — the canonical "what is MCP and how do you build one in Node.js" pillar — with everything I just glossed over here filled in.

What this post isn't

It's not a tutorial. There's no npm install step, no full code example, no "now run this command." This is the conceptual layer — the model you need before any of the tutorials make sense. If you've read MCP tutorials and felt like you were copy-pasting without understanding, this is probably what was missing: the host-server-LLM separation, the stdio invisibility, the realization that the tool descriptions are doing more work than the tool code.

Once that lands, the rest of the series is a guided build.

Next post: if MCP servers don't open ports and don't sit on the network, they're stretching what we mean by "Node.js app." So what is a Node.js program when it isn't a server with app.listen() at the bottom? That's where this series goes next, before the pillar build.

Related Topics

mcp explainedwhat is an mcp servermcp nodejsmodel context protocol explained

Ready to build your app?

Flutter apps built on Clean Architecture — documented, tested, and yours to own. See which plan fits your project.