Skip to main content

    [ GUIDE ]

    How to build an MCP server, end to end.

    By Alex Cinovoj, Founder & CTO, TechTide AI · 13 years in US enterprise IT.

    This is the guide we wish existed when we started shipping Model Context Protocol servers. It walks the full build: choosing a transport, designing typed tools, wiring OAuth 2.1, writing evals, and standing up production hosting. It's grounded in the current Anthropic MCP spec (2025-06-18), the official TypeScript and Python SDKs, and the patterns TechTide AI runs in production. For the productized offer, see MCP server development.

    What an MCP server actually is.

    An MCP server is a small, typed program that exposes tools, resources, and prompts to a Model Context Protocol client (Claude Desktop, Claude Code, Cursor, ChatGPT connectors, LangChain agents, custom orchestrators). The client speaks JSON-RPC over a defined transport; the server answers. That's the whole shape.

    The value is the tool boundary. Instead of pasting your API docs into a system prompt and hoping the model calls them right, you publish a typed schema the model can introspect and the runtime can validate. Wrong arguments fail closed. Auth lives at the edge. Audit logs are trivial.

    Step 1: Pick a transport.

    The 2025-06-18 spec supports two transports: stdio and Streamable HTTP. SSE is deprecated in favor of Streamable HTTP.

    • stdio for local-only servers, developer tools, and anything running on the user's machine as a subprocess of the client. Zero network surface. Easiest to ship first.
    • Streamable HTTP for remote, multi-tenant, or organizationally owned servers. Single endpoint that handles both request/response and server-initiated streaming. Required for anything published to a connector catalog.

    Rule of thumb: prototype in stdio, ship to production over Streamable HTTP. All Streamable HTTP POST requests must send Accept: application/json, text/event-stream, or a spec-compliant server returns HTTP 406.

    Step 2: Design the tool surface before writing code.

    The single biggest predictor of whether an MCP server survives production contact is whether the tool surface was designed on paper first. Rules that hold up:

    1. One verb per tool. create_ticket, list_tickets, close_ticket. Not manage_ticket(action, ...).
    2. Narrow inputs. Enums over free strings. Required fields required. Optional fields with defaults documented in the schema, not the description.
    3. Structured outputs. Return structuredContent alongside human-readable content. Agents parse the structured form; humans read the text.
    4. Behavior hints. Fill annotations.readOnlyHint, destructiveHint, idempotentHint, openWorldHint. Clients use these to gate confirmation UI.
    5. Descriptions written for the model. First sentence names the tool, second sentence names when to use it, third names when NOT to. Models pick tools by these strings; treat them as production copy.

    Step 3: Implement with the official SDK.

    Use the official SDKs: @modelcontextprotocol/sdk for TypeScript or mcp for Python. Both handle JSON-RPC framing, transport negotiation, capability advertisement, and cancellation. Do not hand-roll the wire protocol.

    A minimal typed tool in TypeScript:

    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { z } from "zod";
    
    const server = new McpServer({ name: "billing", version: "1.0.0" });
    
    server.tool(
      "get_invoice",
      "Fetch a single invoice by ID. Read-only.",
      { invoice_id: z.string().min(1) },
      { readOnlyHint: true, idempotentHint: true },
      async ({ invoice_id }, ctx) => {
        const row = await db.invoices.byId(invoice_id, ctx.userId);
        return {
          content: [{ type: "text", text: JSON.stringify(row) }],
          structuredContent: row,
        };
      },
    );

    Every handler must validate at the boundary (Zod / Pydantic), enforce authorization inside the handler (not in the client), and return within the client's timeout (tens of seconds). Long jobs belong behind a create_job + get_job_status pair, not a single blocking tool.

    Step 4: Wire OAuth 2.1 the way the spec expects.

    Remote MCP servers use OAuth 2.1 with PKCE and dynamic client registration (RFC 7591). That means:

    • The MCP server is the resource server. It validates bearer tokens, does not issue them.
    • An external authorization server (Supabase Auth, Auth0, Keycloak, Descope, WorkOS) handles login, consent, DCR, and token issuance.
    • The server publishes /.well-known/oauth-protected-resource pointing at the authorization server so any client can discover it.
    • Every tool call carries the user's token; the handler derives the user from the verified token (never from tool input) and forwards the token to downstream services so row-level security runs as that user.

    Do not paste a long-lived API key as the auth mechanism, do not accept the user's identity from tool arguments, and do not run tools against a database using a service-role key behind an unauthenticated endpoint. Each is a straight path to a data-exfiltration bug.

    Step 5: Write an eval suite before you deploy.

    An MCP server without evals is a demo. The minimum useful eval suite has three layers:

    1. Schema evals. For each tool, a golden set of valid and invalid inputs. Runs on every commit. Catches accidental schema changes.
    2. Behavior evals. For each tool, a set of prompts a client model would issue, scored against expected tool calls and expected results. Anthropic's own docs recommend this pattern; LangChain's evaluation harness is one option, Braintrust and Langfuse are others.
    3. End-to-end task evals. A handful of realistic multi-tool tasks, run against the real model (Claude, GPT, Gemini) with the real server. These catch tool-boundary mistakes evals-in-isolation miss.

    Run all three on every merge to main. Regressions in tool descriptions are silent otherwise.

    Step 6: Host it like a real service.

    • Hosting. Cloudflare Workers, Fly.io, Railway, or a Supabase Edge Function are all fine. Pick based on where the data already lives.
    • TLS everywhere. Streamable HTTP over plain HTTP is a spec violation and most clients refuse it.
    • Structured logs. Log tool name, caller user id, argument hash (never raw arguments, they may contain PII), latency, and outcome. Ship to a real log store.
    • Rate limits per user. Not per IP. Agents share IPs.
    • Health check. A simple GET /health that verifies downstream connectivity. Wire it to your uptime monitor.
    • Versioning. Bump the server version on every tool change. Clients cache tool lists.

    Step 7: Publish and connect.

    Once the server is live, connecting it is straightforward across clients:

    • Claude Desktop and Claude Code: MCP settings, add the server URL, complete the OAuth flow.
    • ChatGPT: Custom connectors accept Streamable HTTP MCP endpoints.
    • Cursor: ~/.cursor/mcp.json for local; remote support ships behind a flag.
    • LangChain / LangGraph: langchain-mcp-adapters loads any MCP server's tools into an agent's tool list.
    • Hermes and other OSS orchestrators: any spec-compliant Streamable HTTP MCP server works without adapter code.

    The mistakes that keep MCP servers out of production.

    1. Overbroad tools. A single run_sql(query) is a resume-generating event, not a tool.
    2. Trusting caller-supplied identity. user_id comes from the verified token, always.
    3. No timeouts on downstream calls. A slow database hangs the client and the client shows the call as "interrupted."
    4. Descriptions written for humans, not models. Models select tools by the description string. Vague descriptions cause wrong-tool selection.
    5. No evals. The tool worked in the demo and silently regresses six weeks later.
    6. Skipping OAuth to "ship faster." Every serious client will require it anyway. Build it first.

    Frequently asked

    • A single-tool stdio prototype is a weekend. A production Streamable HTTP server with OAuth 2.1, a handful of typed tools, evals, hosting, and observability is typically four to eight weeks depending on how messy the underlying systems are.

    Want us to build the MCP server for you?