DEV Community

Phil IPS
Phil IPS

Posted on

Building Your First MCP Server: A Practical Guide

Building Your First MCP Server: A Practical Guide

The Model Context Protocol (MCP) lets you expose tools and data to any
compatible AI client through one small server. Instead of writing a bespoke
integration for every assistant, you write the server once and every MCP-aware
client can use it.

This guide builds a working MCP server in about 60 lines and connects it to a
client.

Why MCP instead of a custom tool API

Before MCP, every assistant had its own tool format. A tool written for one
product had to be rewritten for the next. MCP standardises three things:

  • Tools — functions the model can call
  • Resources — read-only data the model can pull in
  • Prompts — reusable prompt templates the server exposes

Because the wire format is fixed, the same server works with a desktop client,
an IDE plugin, or an autonomous agent.

Installing the SDK

pip install mcp
Enter fullscreen mode Exit fullscreen mode

The Python SDK ships a FastMCP helper that turns decorated functions into
tools automatically.

Writing the server

Create server.py:

from mcp.server.fastmcp import FastMCP

mcp = FastMCP("demo-server")


@mcp.tool()
def add(a: float, b: float) -> float:
    """Add two numbers and return the result."""
    return a + b


@mcp.tool()
def word_count(text: str) -> int:
    """Count the words in a piece of text."""
    return len(text.split())


@mcp.resource("config://app")
def get_config() -> str:
    """Expose static configuration as a resource."""
    return '{"theme": "dark", "language": "en"}'


if __name__ == "__main__":
    mcp.run()
Enter fullscreen mode Exit fullscreen mode

Two details matter here.

First, the docstring is the tool description. The model reads it to decide
when to call the function, so write it for the model, not for yourself.

Second, type hints define the schema. a: float, b: float -> float becomes
a JSON Schema with two required numbers and a number return. There is no
separate schema to keep in sync.

Running and inspecting it

python server.py
Enter fullscreen mode Exit fullscreen mode

The server speaks JSON-RPC over stdio by default. You can poke it manually to
confirm the tools are registered:

echo '{"jsonrpc":"2.0","id":1,"method":"tools/list"}' | python server.py
Enter fullscreen mode Exit fullscreen mode

You should see both tools with their generated schemas.

Connecting a client

In a client configuration file (the exact path depends on the host application):

{
  "mcpServers": {
    "demo": {
      "command": "python",
      "args": ["/absolute/path/to/server.py"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

Restart the client and both tools appear in its tool list. Ask it to add two
numbers and it will call add rather than guessing.

Exposing something useful

A toy adder proves the plumbing; a real server earns its keep. A practical
pattern is wrapping an existing API so the model stops hallucinating values:

import httpx

@mcp.tool()
async def get_package_version(package: str) -> str:
    """Look up the latest published version of a PyPI package."""
    async with httpx.AsyncClient() as client:
        r = await client.get(f"https://pypi.org/pypi/{package}/json", timeout=10)
        r.raise_for_status()
        return r.json()["info"]["version"]
Enter fullscreen mode Exit fullscreen mode

The model now answers version questions from live data instead of from
training-time memory.

Common pitfalls

Relative paths break. The client launches your server from an arbitrary
working directory. Always resolve paths against the server file's own location
or use absolute paths in the config.

Printing to stdout corrupts the protocol. stdio is the transport. Use
stderr for logs:

import sys
print("debug", file=sys.stderr)
Enter fullscreen mode Exit fullscreen mode

Slow tools block the client. Mark I/O-bound tools async so one slow call
does not stall everything else.

Vague docstrings cause wrong calls. "Process data" tells the model nothing.
"Convert a CSV string into a list of row dictionaries" tells it exactly when to
reach for the tool.

Where to go next

  • Add a resource that streams a file so the model can read local documents
  • Wrap your own service's API and expose only the read-only endpoints
  • Combine several small servers; the client merges their tool lists

MCP turns "write an integration per assistant" into "write one server, reuse it
everywhere" — and the server above is small enough to finish in an afternoon.

Top comments (0)