How to build an MCP server in Python (FastMCP)

7 minUpdated:
How to build an MCP server in Python (FastMCP)

Install the MCP Python SDK with uv add "mcp[cli]", create a FastMCP instance, decorate typed Python functions with @mcp.tool(), and call mcp.run(). Debug it with mcp dev server.py, then register it in your client using uv run with the absolute path to the file.

Why build an MCP server in Python?

Python is where much of the data, scraping, machine learning and automation code already lives. Wrapping that code in an MCP server lets AI clients such as Claude Code, Claude Desktop or Cursor call it as tools without you building a separate API or UI.

FastMCP makes this short. It turns ordinary type-hinted functions into MCP tools, generating the JSON schema from your type hints and the description from your docstring. A useful server can be a single file.

The trade-off compared with a hand-written web API is control over the interface. You do not design endpoints for humans; you design tool names, descriptions and argument types for a model, and the quality of those descriptions directly decides how well the model uses your code.

Python servers also make good glue. A single server can combine a database query, a pandas transformation and a call to an internal API behind one tool, hiding complexity the model does not need to see.

Because the SDK handles the protocol, most of your time goes into the tool functions themselves, which is exactly where domain knowledge lives.

FastMCP in the SDK or the standalone package?

There are two things called FastMCP. A FastMCP class ships inside the official MCP Python SDK (package name mcp). A separately maintained fastmcp package grew from the same origin and adds extras such as server composition, proxying and client utilities.

For most new projects, start with the FastMCP class in the official SDK. It covers tools, resources and prompts, tracks the specification closely and is what most documentation examples use.

Switch to the standalone package only when you need one of its specific features, such as mounting several servers into one or proxying a remote server. Both approaches produce servers that any MCP client can use.

Whichever you choose, pin the version in your lock file, because method names and defaults have changed between releases.

OptionInstallBest forTrade-off
Official SDK FastMCPuv add "mcp[cli]"Most servers; tracks the spec closelyFewer high-level extras
Standalone fastmcp packageuv add fastmcpComposition, proxies, richer toolingSeparate project and release cycle; check its licence file and docs
Low-level SDK Server classPart of the mcp packageFull control of protocol handlersMore boilerplate for simple tools

How to set up the project

  • Install uv if you do not have it (see the uv documentation for the installer command for your OS).
  • Create a project: uv init notes-mcp, then cd notes-mcp
  • Add the SDK with the CLI extras: uv add "mcp[cli]"
  • Add any libraries your tools need, for example uv add httpx
  • Create server.py; keep business logic in separate functions or modules so you can test it without MCP.

How to write tools, resources and prompts

  • Import and create the server: from mcp.server.fastmcp import FastMCP, then mcp = FastMCP("notes")
  • Define a tool: put @mcp.tool() above a function such as def search_notes(query: str, limit: int = 5) -> str, with a docstring that explains when to use it.
  • Use precise type hints; FastMCP turns them into the input schema the model sees, and default values become optional parameters.
  • Write async def tools when they call network services, so slow requests do not block the server.
  • Expose data as a resource: @mcp.resource("notes://{note_id}") on a function that returns the note text.
  • Offer a reusable prompt with @mcp.prompt() on a function that returns a message template.
  • End the file with if __name__ == "__main__": mcp.run() which defaults to the stdio transport.

How to test and debug the server

  • Launch the development inspector: uv run mcp dev server.py, then open the Inspector UI it starts in your browser.
  • List tools and check that the generated schemas match what you intended, especially optional fields.
  • Call each tool with good and bad input; confirm errors come back as readable messages.
  • Unit-test the plain functions behind each tool with pytest; the MCP layer should stay thin.
  • Never print() to stdout in a stdio server; use the logging module, which writes to stderr by default.

How to connect it to Claude Code, Claude Desktop and Cursor

Clients launch stdio servers as a subprocess, so the command must work from any directory. Using uv with an explicit project directory is the most reliable pattern.

  • Claude Code: claude mcp add notes -- uv run --directory /absolute/path/notes-mcp server.py
  • Claude Desktop: the SDK can write the config for you with uv run mcp install server.py, or add an entry to claude_desktop_config.json with command uv and the same args.
  • Cursor: add an entry under mcpServers in .cursor/mcp.json with command uv and args ["run", "--directory", "/absolute/path/notes-mcp", "server.py"].
  • Pass secrets via the env block of the client configuration and read them with os.environ in Python.

Python vs TypeScript for MCP servers

FactorPython (FastMCP)TypeScript SDK
Schema definitionType hints and docstringszod schemas
Distributionuvx or pip packagesnpx from npm
Ecosystem strengthsData, ML, scraping, scientific librariesWeb APIs, Node tooling, frontend teams
Boilerplate for a first toolVery lowLow

Common mistakes with Python MCP servers

Once a server works, publishing it as a package that runs with uvx makes installation simple for others. RepoLoot’s catalog groups Python MCP servers by what they connect to, which helps you spot gaps worth filling.

The best first servers wrap code you already trust. A data team might expose read-only SQL against a reporting replica; a research team might wrap a paper search or a scraping routine; an ops team might expose log search.

Keep tools read-only at first and return summaries rather than raw dumps. Paginate large results with a limit and an offset or cursor parameter, and state in the docstring how many items a call returns.

Write the docstring for the model, not for a colleague. Say when to use the tool, what each argument means, and what the output looks like, including units and date formats.

When the tool set grows past a handful, group related tools into separate servers. Users can then enable only the servers a project needs, which keeps the model’s tool list short and its choices accurate.

  • Relying on the system Python: the client may not see your virtual environment, so imports fail at launch.
  • Relative file paths that work in your terminal but not when the client starts the process elsewhere.
  • Missing or vague docstrings, which leave the model guessing when to call a tool.
  • Returning huge DataFrames or raw HTML; summarize or paginate instead.
  • Blocking calls inside async tools, such as requests or time.sleep, which stall every other request.
  • Letting tools run arbitrary shell commands or SQL from model input without an allowlist.

How do you run a FastMCP server over HTTP for a team?

A stdio server runs once per user machine. When a whole team needs the same tools, such as access to an internal knowledge base, a single hosted server over Streamable HTTP is easier to maintain.

Treat it as a normal web service from that point: it needs authentication, logging, rate limits and deployment like any other API.

  • Select the HTTP transport when starting the server, following the transport options in the SDK documentation for your version.
  • Run it behind a reverse proxy that terminates HTTPS, such as Caddy or Nginx.
  • Require authentication; the MCP specification describes an OAuth-based flow for remote servers, and simpler deployments often start with a token checked by the proxy.
  • Package it in a container with pinned dependencies from uv.lock so every deploy is reproducible.
  • Register it in clients by URL, for example with claude mcp add --transport http in Claude Code.

Frequently asked questions

Is FastMCP the same as the official MCP Python SDK?
Partly. The official SDK includes a FastMCP class at mcp.server.fastmcp, and that is enough for most servers. A separate fastmcp package is developed independently with extra features like composition and proxying. Both follow the protocol, so clients cannot tell the difference; choose based on the features you need.
Can I run a Python MCP server over HTTP instead of stdio?
Yes. FastMCP supports the Streamable HTTP transport, which you select when calling run, and it can also be mounted into an ASGI application. Use HTTP when the server must be shared by a team or hosted remotely, and add authentication and rate limits because it becomes a network service.
Why can the client not find my Python packages?
The client starts your server with its own environment, often without your activated virtual environment. Launch it through uv run with the --directory flag pointing at your project, or use the absolute path to the virtual environment’s Python binary in the client configuration.
How do I pass API keys to a Python MCP server?
Put them in the env section of the client configuration, or pass them with the -e flag in claude mcp add, and read them with os.environ inside Python. Do not hard-code keys in server.py or commit them in a project-level configuration file shared through git.
Free for builders

Get a hand-picked shortlist of repos for your project

Tell us what you are building. A person — not a bot — reviews it and replies within 48 hours with the catalog projects that fit, including licence and difficulty notes.

We use your email only for this request. Privacy policy

Related guides