How to build an MCP server in Python with FastMCP
The Model Context Protocol (MCP) lets a language model call your code through a small, typed interface. In Python the fastest way to ship one is FastMCP, the high-level API that now lives inside the official mcp SDK. You write plain functions, decorate them, and the SDK handles the protocol, the JSON-RPC plumbing, and the schema generation. This walkthrough builds a working server, runs it, connects it to Claude and Cursor, and wires in the security basics that matter before anyone else points a client at it.
Install the SDK
You need Python 3.10 or newer. Use uv if you have it, since it is what most MCP tooling assumes, but plain pip works fine too. The cli extra pulls in the dev runner and inspector.
uv add "mcp[cli]" # or pip install "mcp[cli]"
Define a tool
A tool is just a typed function. FastMCP reads your type hints and docstring and turns them into the JSON schema the model sees, so annotate everything. The docstring is not decoration, it is the description the model reads to decide when to call the tool. Keep it specific.
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather-tools")
@mcp.tool()
def convert_temp(value: float, to: str) -> str:
"""Convert a temperature. 'to' must be 'c' or 'f'."""
to = to.lower()
if to == "c":
return f"{(value - 32) * 5 / 9:.1f} C"
if to == "f":
return f"{value * 9 / 5 + 32:.1f} F"
raise ValueError("to must be 'c' or 'f'")
if __name__ == "__main__":
mcp.run()
By default mcp.run() speaks over stdio, which is what desktop clients like Claude Desktop and Cursor launch and talk to directly. No port, no HTTP, just a subprocess.
Run and inspect it
Before touching any client, run the server against the MCP Inspector. It gives you a UI to list tools, see the generated schema, and call them by hand. This catches bad type hints and confusing descriptions early.
uv run mcp dev server.py
The inspector opens in your browser. Confirm convert_temp shows up with the right parameters, call it with a sample value, and check the result. If the schema looks wrong here, it will look wrong to the model too.
Connect it to Claude or Cursor
Both clients read a JSON config that tells them how to launch your server as a subprocess. For Claude Desktop, edit claude_desktop_config.json (Settings, Developer, Edit Config). Cursor uses ~/.cursor/mcp.json or a project-level .cursor/mcp.json. The shape is the same:
{
"mcpServers": {
"weather-tools": {
"command": "uv",
"args": ["run", "--directory", "/abs/path/to/project", "server.py"]
}
}
}
Use an absolute path, since the client does not run from your project directory. Restart the client, and the tool appears in the tool list. A few things to watch:
- If the server fails to start, the client usually swallows the error. Check the client logs, or run
mcp devagain to reproduce it in isolation. - Never put secrets directly in
args. Pass them throughenvin the same config block and read them withos.environ. - Keep the tool count small. Every tool you expose is loaded into the model's context on each request, so ten sharp tools beat thirty fuzzy ones.
Bake in the security basics
An MCP tool is a function the model can be steered into calling by anything it reads, including a web page or a file. So treat every argument as hostile from the start, not as a later hardening pass.
- Validate and bound every input. FastMCP generates schema from your hints, but you still enforce ranges and enums in code. Reject out-of-range values with a clear error instead of letting them reach your logic.
- Never fetch a user-supplied URL with a raw client. A tool that does
httpx.get(url)on a model-controlled string is a server-side request forgery (SSRF) hole that can reach your internal network and the cloud metadata endpoint. Require https, block private, loopback, and link-local ranges, re-resolve the host to defeat DNS rebinding, and do not follow redirects. - Make side effects explicit and reversible. If a tool writes, deletes, or sends, say so in the docstring and gate destructive actions behind an explicit confirmation argument.
- Authenticate and rate-limit at the server, not per tool. If you move beyond stdio to an HTTP transport (
mcp.run(transport="streamable-http")), it is exposed over the network and needs auth and limits at the edge.
When the server feels done, scan it before you publish. The free, open-source mcp-audit walks your server and flags the common failures: unbounded inputs, unguarded fetches, missing auth, and tools whose schemas leak more than they should. It is a fast sanity check that catches the mistakes this list is meant to prevent.
Next steps
From here you can add resources (read-only context the model can pull in) and prompts (reusable templates) with the same decorator style: @mcp.resource() and @mcp.prompt(). The pattern stays the same, typed functions in, protocol handled for you. Get one tool solid, audited, and connected first, then grow the surface deliberately.
Ship a production-ready Python MCP server faster
MCP Forge Kit gives you a FastMCP server with validated example tools, an SSRF-safe fetch utility, auth and rate limiting at the edge, tests, and CI already wired, so you build features instead of boilerplate.
Get MCP Forge Kit, €39Related: Write a secure tool · Claude and Cursor setup · SSRF protection