If you want to build mcp server python applications that expose tools and context to LLM clients, the official Model Context Protocol SDK makes it straightforward. This tutorial walks through standing up a working server with the FastMCP helper, registering a tool, a resource, and a prompt, and verifying it with a local client.
Prerequisites
Before you start, make sure you have:
- Python 3.10 or newer (the SDK uses type hints and modern syntax)
pipand a virtual environment tool- Basic familiarity with Python decorators
Install the SDK with the CLI extras so you get the mcp command for inspection:
python -m venv .venv
source .venv/bin/activate
pip install "mcp[cli]"
Verify the install:
mcp version
Expected output looks like mcp 1.2.0 (version will vary).
MCP basics without the hype
Model Context Protocol is a JSON-RPC 2.0 protocol layered over a transport (stdio or HTTP). A server declares capabilities: tools, resources, prompts. A client initializes a session, lists what’s available, and invokes them. Tools are model-callable functions. Resources are read-only data addressed by URI. Prompts are reusable message templates. That’s the entire mental model you need to build mcp server python code that interoperates with any compliant client.
Project setup
Create a working directory and the virtual environment as shown above. Then write a single file server.py. The FastMCP class handles schema generation from type hints and docstrings, so you write almost no boilerplate.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("demo-server")
if __name__ == "__main__":
mcp.run()
Run it briefly to confirm it doesn’t crash:
python server.py
The process blocks waiting for stdin/stdout JSON-RPC messages. Kill it with Ctrl-C. That is expected for stdio transport.
Add a tool
Tools are callable functions the model can invoke. Decorate a function with @mcp.tool(). The name defaults to the function name; the docstring becomes the description; type hints become the input schema.
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two integers and return the sum."""
return a + b
Keep tool logic synchronous if it is pure CPU work. If you need network or file I/O, define it as async def:
import asyncio
@mcp.tool()
async def slow_add(a: int, b: int) -> int:
"""Add after a short delay to simulate I/O."""
await asyncio.sleep(0.1)
return a + b
The SDK serializes the return value as content blocks. Returning a primitive is fine; the framework wraps it.
Complex inputs with Pydantic
For structured inputs, use a Pydantic model. FastMCP converts it to a JSON schema automatically.
from pydantic import BaseModel
class Coords(BaseModel):
x: float
y: float
@mcp.tool()
def distance(a: Coords, b: Coords) -> float:
"""Euclidean distance between two points."""
return ((a.x - b.x)**2 + (a.y - b.y)**2) ** 0.5
Add a resource
Resources are read-only data addressed by URI. They are fetched by the client, not called by the model. Use @mcp.resource() with a URI template.
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Return a personalized greeting for the given name."""
return f"Hello, {name}!"
When a client requests greeting://Ada, the server matches the template, binds name="Ada", and returns the string. Resources should be cheap and deterministic; never hide side effects there.
Add a prompt
Prompts are reusable message templates. They are not executed by the server; they return structured messages for the client to inject.
@mcp.prompt()
def summarize(text: str) -> str:
"""Build a prompt that asks the model to summarize text."""
return f"Summarize the following concisely:\n\n{text}"
A prompt can also return a list of Message objects for multi-turn templates, but a single string covers most cases.
Full server file
from mcp.server.fastmcp import FastMCP
from pydantic import BaseModel
mcp = FastMCP("demo-server")
@mcp.tool()
def add(a: int, b: int) -> int:
"""Add two integers and return the sum."""
return a + b
class Coords(BaseModel):
x: float
y: float
@mcp.tool()
def distance(a: Coords, b: Coords) -> float:
"""Euclidean distance between two points."""
return ((a.x - b.x)**2 + (a.y - b.y)**2) ** 0.5
@mcp.resource("greeting://{name}")
def greeting(name: str) -> str:
"""Return a personalized greeting for the given name."""
return f"Hello, {name}!"
@mcp.prompt()
def summarize(text: str) -> str:
"""Build a prompt that asks the model to summarize text."""
return f"Summarize the following concisely:\n\n{text}"
if __name__ == "__main__":
mcp.run()
Run with the inspector
The fastest way to validate your server is the MCP Inspector shipped with the CLI:
mcp dev server.py
This starts your server and opens a web UI (typically http://localhost:5173). You will see demo-server with two tools, one resource template, and one prompt.
In the inspector, click Tools → add, enter {"a": 2, "b": 3}, and execute. Expected output:
{
"content": [
{ "type": "text", "text": "5" }
]
}
Switch to Resources, fetch greeting://Ada, and you will get Hello, Ada!. This confirms the server speaks the protocol correctly.
Connect from Claude Desktop
To use your server with Claude Desktop, edit claude_desktop_config.json:
{
"mcpServers": {
"demo": {
"command": "python",
"args": ["/absolute/path/to/server.py"]
}
}
}
Restart Claude Desktop. The add tool and greeting resource become available without any client code changes. That is the payoff when you build mcp server python projects against a standard protocol: any compliant client can consume them.
Programmatic client test
For a headless check (useful in CI), write a small async client:
import asyncio
from mcp.client.stdio import stdio_client
from mcp import ClientSession
async def main():
async with stdio_client(["python", "server.py"]) as (read, write):
async with ClientSession(read, write) as session:
await session.initialize()
tools = await session.list_tools()
print("Tools:", [t.name for t in tools.tools])
res = await session.call_tool("add", {"a": 7, "b": 8})
print("add(7,8) ->", res.content[0].text)
if __name__ == "__main__":
asyncio.run(main())
Run it:
python client.py
Expected output:
Tools: ['add', 'distance']
add(7,8) -> 15
Switch to HTTP transport
Stdio is ideal for local processes. For many clients, use streamable HTTP:
if __name__ == "__main__":
mcp.run(transport="streamable-http")
The SDK starts an ASGI app (default port 8000). Put it behind Nginx or a load balancer. HTTP transport requires clients to handle session IDs and CORS; the inspector supports it via a URL field.
Operational notes
- Logging: Print statements on stdio corrupt the JSON-RPC stream. Use
loggingto a file or the built-inmcplogger. - Errors: Raise
ValueErrorinside a tool; the SDK converts it to an error response. Never let exceptions escape uncaught. - Schema drift: Change a tool signature and the JSON schema updates automatically. Bump the server name string in
FastMCP("demo-server")to help clients cache bust. - Auth: Stdio has no network auth. For HTTP, terminate TLS and validate tokens at the proxy layer; the protocol is stateless per session.
Debugging common failures
- Server exits immediately: Ensure the file is saved and
mcpis installed in the active environment. - Tool not visible: Decorators must run before
mcp.run(). Defining them inside a function will not register them. - Inspector can’t connect: Confirm you installed
mcp[cli], not justmcp.
Where to go next
You now know how to build mcp server python projects that expose tools, resources, and prompts with minimal code. From here, wrap your internal APIs as tools, serve database rows as resources, and template agent instructions as prompts. The protocol handles the handshake; your job is to keep the functions side-effect-free, fast, and honestly documented in their docstrings.