Building Your First MCP Server: A Step-by-Step Tutorial

CallMissed
·5 min readGuide

The Model Context Protocol (MCP) has gone from an Anthropic side-project announced in late 2024 to the de-facto plumbing for tool-using agents in eighteen months. OpenAI, Google, and most major IDE vendors now speak it natively, and the official spec moved through several revisions in 2025, with a 2025-11-25 specification landing the streamable-HTTP transport. If you have not built one yet, the gap between "I read the docs" and "I have a server Claude Desktop is calling" is smaller than it looks.

This tutorial walks the path: protocol basics, the three primitives, a minimal Python and TypeScript skeleton, and how to debug the thing without an LLM in the loop.

What MCP actually is

At the wire level, MCP is JSON-RPC 2.0. Messages are request/response or notification frames over either standard input/output (for locally-spawned servers) or HTTP with Server-Sent Events / streamable HTTP (for remote servers). The June 2025 spec adopted OAuth 2.1 for remote authentication, and the November 2025 spec replaced legacy SSE with streamable HTTP as the canonical remote transport.

A server exposes three primitives:

  • Tools — functions the model can call (e.g., search_database, send_email)
  • Resources — read-only data the model can request by URI (e.g., file://, db://table/users)
  • Prompts — parameterized templates the user can invoke explicitly
  • Clients and servers run a capability-negotiation handshake on connect to discover what each side supports. Once established, the model can list tools, call them, and receive structured results.

    Pick a transport

  • stdio — for local desktop integrations (Claude Desktop, Cursor, Zed). Simplest to build, no auth concerns, runs as a child process.
  • streamable HTTP — for remote / multi-tenant servers. Requires OAuth 2.1, advertises authorization through .well-known endpoints. [Inference] Pick stdio first; you can always add a remote transport later.
  • Minimal Python server

    The official mcp Python SDK takes a few lines:

    python
    from mcp.server.fastmcp import FastMCP
    
    mcp = FastMCP("weather-server")
    
    @mcp.tool()
    def get_weather(city: str) -> str:
        """Return current weather for a city."""
        # call your real API here
        return f"Sunny, 72F in {city}"
    
    if __name__ == "__main__":
        mcp.run()  # defaults to stdio

    The FastMCP decorator harvests the function signature and docstring into the JSON Schema the protocol exposes. Type hints become the parameter schema; the docstring becomes the tool description the model sees.

    Minimal TypeScript server

    typescript
    import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
    import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
    import { z } from "zod";
    
    const server = new McpServer({ name: "weather-server", version: "1.0.0" });
    
    server.registerTool(
      "get_weather",
      {
        title: "Get weather",
        description: "Return current weather for a city.",
        inputSchema: { city: z.string() },
      },
      async ({ city }) => ({
        content: [{ type: "text", text: `Sunny, 72F in ${city}` }],
      }),
    );
    
    await server.connect(new StdioServerTransport());

    Both SDKs handle the JSON-RPC framing, capability negotiation, and error envelope for you.

    Testing without an LLM

    This is the step most tutorials skip. The official MCP Inspector is a UI that lists tools, lets you invoke them with arbitrary arguments, and dumps the raw JSON-RPC traffic. Run it against your server:

    bash
    npx @modelcontextprotocol/inspector python my_server.py

    You get a browser tab where you can see the tools/list response, hand-craft a tools/call, and inspect what came back. Debug your schema and error shapes here before plugging into a model — every minute spent in Inspector saves ten in Claude Desktop.

    Wire it up to Claude Desktop

    Edit claude_desktop_config.json (location varies by OS):

    json
    {
      "mcpServers": {
        "weather": {
          "command": "python",
          "args": ["/absolute/path/to/my_server.py"]
        }
      }
    }

    Restart Claude Desktop. The hammer icon should show your tool. Ask "what's the weather in Paris" and watch your stdout — you'll see the call land.

    What goes wrong

    A few sharp edges from real builds:

  • Schema validation failures — return a clear isError: true content block with the validation message, not a raw exception
  • Long-running tools — the protocol supports progress notifications; use them or the model thinks you've hung
  • Resource URIs — pick a stable scheme (db://, file://, notion://) and document it; clients cache resource lists
  • Auth on remote servers — read the 2026 MCP roadmap before rolling your own; OAuth 2.1 has specific rules for token audiences
  • When to build vs. install

    Before writing one, check the official servers repo and the third-party catalogs. There are already MCP servers for Postgres, GitHub, Slack, Notion, Filesystem, and dozens more. Build your own when you're exposing a proprietary system or a workflow no off-the-shelf server covers — not because nobody has done filesystem access yet.

    Frequently Asked Questions

    Do I need to use Anthropic models to build an MCP server?
    No. MCP is an open protocol and the leading clients (Claude Desktop, Cursor, OpenAI's Apps SDK, Zed, Continue) all support it. Your server doesn't know or care which model is on the other end.
    What's the difference between a tool and a resource in MCP?
    Tools are model-invoked actions, often with side effects (send an email, query a DB). Resources are read-only data the model requests by URI for context, like loading a file or table snapshot.
    Should I expose my whole API as MCP tools?
    No — granularity matters. Define tools at the level of meaningful agent actions ("search_orders by customer") rather than mapping every REST endpoint one-to-one. Coarser tools with structured returns generally outperform a wall of fine-grained ones.

    Related Posts