Building MCP Servers: When It Makes Sense

You've looked at the MCP server ecosystem. Nothing covers your internal API, or the existing server for your service is broken, or your use case is specific enough that no generic server fits. So you're thinking about building your own. Good — custom MCP servers are sometimes the right call. But they're also sometimes a waste of time that produces something worse than calling the API directly. The difference comes down to whether you're solving a real integration problem or just building infrastructure because building infrastructure feels productive.

What It Actually Does

Building an MCP server means writing a program that speaks JSON-RPC 2.0 over stdio or HTTP, exposes tool definitions with JSON Schema, handles incoming calls, and returns structured results. The MCP SDKs — primarily TypeScript and Python — handle the protocol layer, so you don't need to implement JSON-RPC from scratch. What you actually write is: tool definitions (name, description, input schema), handler functions (receive arguments, call your API or data source, return results), and error handling (catch failures, return messages the LLM can act on).

The minimal viable MCP server is smaller than people expect. A server that wraps a single REST API endpoint — say, looking up a customer record — is roughly 50-100 lines of code with the SDK handling the transport. Define one tool, one schema, one handler that makes an HTTP call and returns the result. That's a functional MCP server. You can have it running in Claude Code or Cursor within an hour.

The SDK scaffolding handles capability negotiation (telling the client what the server can do), transport management (stdio pipes or HTTP connections), message serialization, and the protocol lifecycle (initialize, list tools, call tool, shutdown). The Python SDK uses decorators to register tool handlers. The TypeScript SDK uses a similar pattern. Neither requires you to understand the protocol internals unless something breaks — and then you'll want to have the spec bookmarked.

Where the work increases: authentication, state management, and multi-tool coordination. If your API needs OAuth, you're implementing token management. If your tools need to share state — say, a "search" tool followed by a "get details" tool that uses the search result — you're managing a session. If you're wrapping an API with rate limits, you need backoff logic. The protocol is simple. The integration work is the same work it's always been.

What The Demo Makes You Think

The tutorials show you building an MCP server in fifteen minutes. And you can — if the API you're wrapping is a public REST endpoint with API key auth and predictable responses. The tutorial server works. You feel accomplished. Then you try to use it for something real and discover the distance between "works in a demo" and "works when Claude calls it at 2 AM and the API returns a 429."

Here's what the tutorials don't cover well enough.

Tool descriptions are load-bearing. The LLM decides whether to call your tool based on its name and description. If your tool is called "query_data" with a description of "queries data," the model has no idea when to use it. If it's called "search_customer_records" with a description of "Search customer records by name, email, or account ID. Returns matching customer profiles with contact information and account status" — now the model knows when this tool is relevant and what to expect back. The description isn't documentation for humans. It's a prompt for the model. Write it like one.

Input schemas prevent hallucinated arguments. Define your tool inputs with JSON Schema that includes type constraints, enums for fixed option sets, descriptions for every field, and required vs. optional annotations. If you accept a "status" field, enumerate the valid values. If you accept a date, specify the format. The model will generate whatever arguments it thinks are right — your schema is the only thing standing between "status: active" and "status: sure, this customer is active." Validation isn't optional. It's the difference between a tool that works and a tool that works sometimes.

Error messages are instructions to the model. When your handler catches an error, the message you return is what the LLM reads to decide what to do next. "Error: 404" tells the model nothing. "Customer not found. The customer_id may be incorrect — verify the ID and try again, or use search_customer_records to find the correct ID" tells the model exactly what to try. Every error path in your server should produce a message that an LLM could act on. This is the single highest-leverage improvement you can make to a custom server.

Over-tooling is worse than under-tooling. The temptation is to expose every endpoint in your API as a separate tool. Don't. LLMs perform worse as tool count increases — they struggle to select the right tool when presented with twenty options that overlap in scope. Start with the three to five most useful operations. Add more only when the model demonstrably needs them. A server with five well-described tools outperforms one with twenty vaguely described tools in every practical test.

When Building Makes Sense

Build a custom MCP server when:

  • Your data source is proprietary. Internal APIs, company databases, custom systems — nobody's building an open-source MCP server for your company's CRM. If you want an LLM to query it, you're writing the server.
  • The existing server is broken or abandoned. You found a community server, tested it, and it doesn't handle your cases. Forking and fixing is sometimes viable. Building fresh is sometimes faster.
  • You need specific tool semantics. The generic database server lets the model run arbitrary SQL. You need a server that exposes three specific queries with guardrails — no arbitrary writes, no access to certain tables, scoped permissions. Custom servers let you control exactly what the model can do.
  • Multiple people or workflows will use it. The build cost amortizes over users. One developer wrapping one API for one workflow is probably better off with direct function calling. A team that needs the same integration across Claude Code, Cursor, and a custom agent — build the server once.

When It Doesn't

Don't build when:

  • A good server already exists. Check the official servers first, then vetted community options. If someone's already solved your problem and maintained the solution, use it. Your time is better spent elsewhere.
  • Direct API calls are simpler. If your AI tool supports function calling and the integration is one or two API calls, the MCP abstraction layer adds complexity without benefit. MCP shines when multiple clients need the same integration. For one client making one call, it's overhead.
  • You won't maintain it. An MCP server is a dependency. APIs change, auth tokens expire, the MCP spec evolves. If you're building it and walking away, you're building a landmine for your future self. Budget ongoing maintenance or don't build.
  • You're wrapping a wrapper. If the service already has a Zapier connector, a REST API, and a GraphQL endpoint, and you're building an MCP server that calls the REST API — ask whether MCP is actually adding value or just adding a layer.

Common Mistakes

Not testing without the LLM. Test your server with direct JSON-RPC calls before you point an LLM at it. The MCP Inspector tool exists for this. If you can't manually call every tool with valid and invalid arguments and get sensible responses, the model won't be able to either.

Returning raw API responses. Your upstream API returns a 47-field JSON object. The model doesn't need 47 fields. Transform the response into what's actually useful — the relevant fields, in a format the model can parse and relay to the user. Less data, better results.

No input validation beyond the schema. JSON Schema catches type errors. It doesn't catch business logic errors — an ID that's the wrong format, a date range where the start is after the end, a request for a resource the user doesn't have access to. Validate at the application level too.

Ignoring the cold start problem. Stdio servers get spawned fresh for each session. If your server needs to load configuration, establish database connections, or warm caches on startup, that delay hits the user every time they start a new conversation. Keep initialization fast — defer heavy setup until the first actual tool call.

The Verdict

Building a custom MCP server is a small project with a long tail. The initial build — a minimal server with a few well-defined tools — takes hours, not days. The SDK handles the protocol. You write the integration logic. It works.

The cost isn't in building. It's in maintaining. APIs change their response formats. Auth tokens need refreshing infrastructure. The MCP spec adds features your server should support. Your tool descriptions need refining as you learn what the model gets wrong. Plan for this. A custom MCP server isn't a weekend project you finish and forget. It's a small service you operate.

If your situation genuinely calls for a custom server — proprietary data, multiple consumers, specific security requirements — build it. Keep it minimal, test it directly, write error messages for the model not for yourself, and plan to maintain it. If your situation doesn't clearly call for one, the existing ecosystem or direct API integration is almost certainly less work for the same result.


This is part of CustomClanker's MCP & Plumbing series — reality checks on what actually connects.