How to build an MCP server: 6 decisions every tutorial skips

Automation Engineering · By Caleb Sakala · May 23, 2026

Cartoon robot wearing a hard hat, assembling a glowing server rack from colorful blocks on a construction site

Your first MCP server takes about 15 minutes. You install the SDK, define a tool with a decorator, run mcp dev server.py, and watch the Inspector spit back a result. Then you try to connect it to a real workflow, and everything falls apart.

That gap between a working calculator demo and a server that handles production traffic is where most MCP projects stall. Every tutorial covers the same ground: import FastMCP, add @mcp.tool(), connect to Claude Desktop. None of them cover what happens next — choosing a transport, designing schemas that won't break clients, handling authentication for a team.

This guide covers the six decisions you'll face after the tutorial ends.

Stdio or streamable HTTP — pick before you write a line of code

The transport protocol isn't something you refactor later. It determines how your server communicates, who can access it, and how you deploy it.

Stdio pipes JSON-RPC messages through standard input and output. The client spawns your server as a child process, which means zero network configuration and near-instant startup. Claude Desktop and Cursor both default to this mode. If your server wraps a local tool — a linter or a database CLI — stdio is the right call.

Streamable HTTP (which replaced the older SSE transport in the March 2025 spec update) runs your server as a standalone HTTP service. Clients connect over the network, multiple users can share one instance, and you can deploy it behind a load balancer. But now you own TLS termination, CORS headers, session management, and health checks.

Here's the decision framework. If only one person uses the server on one machine, use stdio. If anyone else needs access — a teammate, a CI pipeline, a second machine — use streamable HTTP from the start. Migrating later means rewriting your entry point, adding session handling, and retesting every tool under a different concurrency model.

Schema design determines whether your tools get called

LLMs decide which tool to invoke based on its name, description, and input schema. Get any of these wrong and the model either ignores your tool or calls it with garbage parameters.

Names should be verb-noun pairs. search_logs works. logSearcher doesn't. Descriptions need to state what the tool does and when to use it — in that order. Anthropic's MCP documentation recommends keeping descriptions under 1,024 characters, but the real constraint is clarity. A 200-character description that eliminates ambiguity outperforms a 900-character wall of caveats.

Input schemas punish vagueness. If a parameter accepts a date, specify the format (YYYY-MM-DD) in the description. If a string parameter only accepts certain values, use an enum instead of hoping the model guesses correctly. Zod (TypeScript) and Pydantic (Python) both serialize to JSON Schema, which is what MCP uses under the hood — lean on their validation rather than writing manual checks in your handler.

One pattern the tutorials miss entirely: error responses. The MCP spec defines an isError flag on tool results. Use it. When a tool fails, return a structured error message explaining what went wrong and what the user can fix. Models that receive clean error messages retry intelligently. Models that receive a stack trace hallucinate a workaround.

Stop at seven tools

The MCP spec doesn't impose a tool count limit. Claude's context window does.

Every tool you register gets injected into the system prompt as a JSON Schema definition. A server with 30 tools burns through roughly 15,000 tokens just on tool descriptions before the user says anything. That's context window space that could hold conversation history, document content, or reasoning chains.

Anthropic's own tool-use documentation recommends keeping tool counts low and descriptions concise. Seven is not a magic number, but it's a useful ceiling. If you're building past seven, split your server into two. A search server and a write server will outperform a single server that tries to do both, because the model spends less time disambiguating between similar tools.

Teams that manage dozens of MCP connections across projects burn 3-5 hours per week just on configuration and debugging. Chase Agents eliminates that overhead by letting you connect MCP servers as workspace-scoped actions — one config, every team member inherits it, and routing happens automatically based on input type.

Authentication is your problem now

Forget everything about tool design for a moment.

Stdio servers inherit the permissions of the user who launched them. If your terminal can read a database, so can the MCP server. This works fine for solo developers and collapses immediately for teams.

The MCP spec added an authorization framework in the March 2025 revision, built on OAuth 2.1. Servers can require clients to obtain an access token before calling any tool. The spec is clear on the flow: the client discovers the server's authorization metadata, redirects the user to authenticate, receives a token, and includes it in subsequent requests.

What the spec doesn't solve is the practical question of where your secrets live. API keys for third-party services need to be accessible to the server process but never embedded in the server code. Environment variables work for local development. For remote servers, use your platform's secret manager — AWS Secrets Manager, Vault, or whatever your team already trusts. And yet the most common pattern remains a .env file committed to a shared repo.

Error handling separates demos from production

None of this matters if your server crashes on the first unexpected input.

A tool handler that catches exceptions and returns a formatted error message is table stakes. Production error handling goes further.

Set timeouts on every external call. An HTTP request to a third-party API that hangs for 90 seconds will freeze the entire conversation in a stdio server, because the client is waiting on stdout. The MCP SDK doesn't enforce timeouts — you set them in your HTTP client or database driver.

Implement progress notifications for long-running operations. The spec supports notifications/progress messages that let the client display incremental updates. A tool that processes 500 records should report progress every 50, not go silent for 30 seconds and then dump a result.

Log every tool invocation with its inputs, outputs, and duration. When a tool starts returning unexpected results at 2 AM, you need the logs to figure out whether the problem is your code, the upstream API, or the model sending malformed inputs. Structured logging (JSON format, with tool name and request ID as fields) makes this searchable.

Deploy like it's a microservice

Skip this section if you're running stdio locally. Everyone else: your MCP server is now an API your team depends on. Treat it accordingly.

Containerize early. Pin your SDK version in the Dockerfile — both the TypeScript and Python MCP SDKs iterate fast, and a minor version bump can change transport behavior. Write a /health endpoint that verifies upstream connections (database, external APIs) are alive. Kubernetes uses these to restart failed containers. Your team will use them to diagnose why tools stopped responding at 2 AM.

Teams running 10+ automations that depend on external tool calls lose roughly 4-6 hours per misconfiguration incident — diagnosing the failure, restarting services, re-running jobs. Chase Agents handles this by routing tool calls through managed infrastructure with automatic retry and backoff, so a downed MCP server triggers an alert instead of silent data loss.

What happens next

The MCP ecosystem will consolidate around remote servers within the next 12 months. Stdio served its purpose as a bootstrapping mechanism, but the spec's trajectory — OAuth support, streamable HTTP as the primary transport, the new Streamable HTTP replacing SSE — points toward a world where MCP servers run as persistent services, not child processes.

The open question nobody has a good answer for yet: how do you version an MCP server's tool schema without breaking every client that depends on it? REST APIs solved this with URL versioning and content negotiation over decades of iteration. MCP will need something similar, and whatever emerges will reshape how teams think about building and maintaining these servers.

Start with stdio. Ship something. But design your schemas and error handling as if you'll be running this server for the next two years — because you probably will.