Skip to main content J Williams
Blog
9 min read
Blog

Building an MCP Server: A Practical Introduction to the Model Context Protocol

What MCP actually is under the hood, the JSON-RPC messages flowing across the wire, and a real server you can run in five minutes

9 min read

Every AI agent eventually needs to do something in the real world — query a database, call an API, read a file. MCP is the protocol that lets a model do that safely, without every tool author having to write a bespoke integration for every host application.

MCP Server

Before MCP, every AI application that wanted to call external tools had to invent its own way of describing them. A chatbot that could search the web, query a database, and read your calendar needed three separate, bespoke integrations — and if you wanted to use that same database tool from a different chatbot, you wrote it again from scratch. The Model Context Protocol, introduced by Anthropic in late 2024, exists to remove that duplication. It defines one standard way for a host application (an AI assistant, an IDE, a CLI) to discover and call tools exposed by a server, regardless of which model or which host is involved.

That sounds abstract until you look at the actual messages flowing across the wire. So that’s where this post starts.

The Three Roles in MCP

MCP defines three participants, and getting the terminology straight makes everything else easier to follow.

The host is the application the user interacts with — Claude Desktop, Claude Code, an IDE extension, or any other AI-powered tool. The host embeds an MCP client, which is the piece of code that actually speaks the protocol. The server is a separate process (or remote endpoint) that exposes tools, resources, and prompts. Critically, the server has no idea which model is on the other end — it just answers protocol messages.

Diagram of the MCP host, client, and server, connected by JSON-RPC 2.0 messages over stdio or HTTP plus SSE, with the server exposing tools, resources, and prompts. Host application Claude Desktop Claude Code IDE extension MCP Client request response JSON-RPC 2.0 stdio or HTTP + SSE MCP Server Tools Resources Prompts

The arrow in the middle is doing a lot of work in that diagram, and most of the conceptual difficulty of MCP lives there: a fixed vocabulary of JSON-RPC 2.0 method calls, transported either over stdio (the server is a local subprocess, common for desktop tools) or over HTTP with Server-Sent Events (the server is remote). The transport is interchangeable; the message shapes are not.

The Protocol, Message by Message

A host doesn’t just start calling tools. The session follows a fixed handshake. Here is exactly what gets sent, in order.

1. Initialize. The client tells the server what protocol version and capabilities it supports, and the server replies with its own.

{"jsonrpc": "2.0", "id": 1, "method": "initialize", "params": {"protocolVersion": "2025-06-18", "capabilities": {"roots": {"listChanged": true}}, "clientInfo": {"name": "demo-host", "version": "1.0.0"}}}

2. List tools. Once initialized, the client asks what tools are available. The server returns a name, description, and a JSON Schema for each tool’s arguments — this schema is what the model actually sees when deciding whether and how to call the tool.

{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}

3. Call a tool. The host (on the model’s behalf) sends arguments matching that schema, and the server executes the underlying function and returns a result.

{"jsonrpc": "2.0", "id": 3, "method": "tools/call", "params": {"name": "get_h3_cell", "arguments": {"lat": 51.505, "lng": -0.09, "resolution": 9}}}

That’s genuinely most of it. Resources and prompts follow the same list/get pattern. The protocol is deliberately boring — boring is what makes it interoperable.

Try the Handshake Yourself

The widget below sends real JSON-RPC messages, step by step, exactly as a host would. The “tool call” step does something slightly different from a canned demo: it loads the actual h3-js library and computes a genuine H3 cell index for the coordinates in the request, the same way a real MCP server would run real code. Nothing here is pre-recorded.

Step through it in order and watch the id field — the client assigns a unique ID to each request, and the server’s response echoes it back, which is how JSON-RPC matches responses to requests over a connection that might be carrying several calls at once. The H3 cell returned in step 3 is computed by the real h3-js library running in your browser right now, not a hardcoded string.

Building a Real Server

The protocol above is what the official SDKs hide behind a few decorators. Here is a complete, runnable MCP server in Python using the mcp package, exposing the same tool the demo just simulated:

from mcp.server.fastmcp import FastMCP
import h3

mcp = FastMCP("h3-geo-server")

@mcp.tool()
def get_h3_cell(lat: float, lng: float, resolution: int) -> str:
    """Convert a latitude/longitude pair into an H3 hexagonal cell index.

    Args:
        lat: Latitude in decimal degrees.
        lng: Longitude in decimal degrees.
        resolution: H3 resolution, 0 (coarsest) to 15 (finest).
    """
    return h3.latlng_to_cell(lat, lng, resolution)

@mcp.tool()
def cell_neighbours(cell: str, k: int = 1) -> list[str]:
    """Return all H3 cells within k steps of the given cell."""
    return list(h3.grid_disk(cell, k))

if __name__ == "__main__":
    mcp.run(transport="stdio")

That’s a working server. FastMCP inspects the function signature and docstring to build the inputSchema the client sees in tools/list automatically — you write a normal Python function, and the protocol bookkeeping happens behind the decorator. Install it with:

pip install "mcp[cli]" h3
python h3_server.py

Wiring It Into a Host

A server sitting in a terminal isn’t useful until a host launches it. For Claude Desktop or Claude Code, you register the server in a config file, telling the host what command to run to start it:

{
  "mcpServers": {
    "h3-geo": {
      "command": "python",
      "args": ["/absolute/path/to/h3_server.py"]
    }
  }
}

On startup, the host launches that command as a subprocess and immediately performs the initialize handshake from earlier over the process’s stdin/stdout. From that point on, when the model decides it needs to convert coordinates to an H3 cell, the host sends tools/call exactly like the demo above, and your Python function runs.

Tools, Resources, and Prompts Are Different Things

It’s worth being precise about MCP’s three primitives, because they get conflated in casual writing:

Tools are functions the model can choose to invoke — they have side effects or do computation, and the model decides when to call them based on the conversation.

Resources are data the host can read and attach to context — a file, a database row, a URL — without the model “calling” anything. They’re closer to attachments than actions.

Prompts are reusable, parameterised templates the server exposes, so a host can offer “use this server’s recommended workflow” rather than the model improvising one from scratch.

Most servers people build start with tools alone, and that covers the great majority of real use cases — resources and prompts matter most once you’re exposing a server to multiple different hosts with different needs.

The Trust Boundary You Cannot Skip

An MCP server runs arbitrary code with whatever permissions you grant it, and its output becomes part of the model’s context — which means a malicious or compromised server can attempt prompt injection through a tool result, not just through user input. Two practical rules follow directly from that:

Only install MCP servers from sources you trust, the same way you’d vet any dependency that executes code on your machine — a server is not sandboxed by the protocol itself.

Treat tool output, not just tool input, as untrusted text if the server reads from anywhere outside your control (a web page, a third-party API, a shared file). A tool that fetches a webpage and returns its content is handing the model text that page’s author wrote, and the model has no inherent way to distinguish “data to summarise” from “instructions to follow” unless the host and server are designed with that distinction in mind.

This is the same reasoning that applies to any plugin architecture that grants code execution — MCP didn’t invent the problem, but its growing adoption means more people are wiring untrusted tools into models for the first time, often without realising the trust boundary has moved.

Where This Goes Next

MCP’s real value shows up once you have more than one server. A single host — Claude Desktop, Claude Code, a custom agent — can run a filesystem server, a database server, and a domain-specific server like the H3 one above simultaneously, and the model composes calls across all of them using the identical protocol each time. That composability, more than any individual feature, is why MCP spread as fast as it did: the cost of adding a new capability to an AI application dropped from “write a custom integration” to “point the host at a server.”