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
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()
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
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
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"]
}
}
}
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"]
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)
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)