How to Build an MCP Server in Python: A Guide
In short
An MCP server is a small program that exposes ready-made tools, data and prompts to an AI assistant (Claude Code, Cursor, Claude Desktop) over a single protocol. In Python, a minimal working server is 10–15 lines: a FastMCP object, functions decorated with @mcp.tool(), and a mcp.run() call. From there the server connects to a client with one command, gets debugged in MCP Inspector, and needs careful handling of security. If you don't need a custom integration, it's faster to grab something ready-made from our roundup of MCP servers or the Arsenal section.
What an MCP server is and why you'd want one
MCP (Model Context Protocol) is an open protocol that standardizes how a language model reaches external tools and data. Instead of hand-rolled integrations for every client, you describe your capabilities once — and any MCP-compatible assistant sees them.
An MCP server is a process implementing the server side of the protocol: it declares its capabilities and executes the client's (host's) requests. Communication runs over JSON-RPC 2.0, so the implementation language doesn't matter — the contract does.
A server exposes three types of capabilities (primitives):
| Primitive | What it is | Example |
|---|---|---|
| Tool | A function the model calls with arguments | get_forecast(city) |
| Resource | Data the model reads by URI | config://app |
| Prompt | A template injected into the conversation | "review this diff" |
Writing your own server makes sense when you need access to an internal API, a private database or non-standard logic. For common tasks (GitHub, files, browser, databases) servers already exist — start with the roundup so you don't reinvent the wheel.
What you'll need: SDK and environment
The minimum set as of July 2026:
- Python 3.10+.
- The official
mcpSDK (version 1.28.x as of July 2026, MIT license). Install withpip install "mcp[cli]"oruv add "mcp[cli]". The[cli]extras give you Inspector for debugging. FastMCPships inside the official package — a high-level layer that hides JSON-RPC behind decorators.- An alternative is the standalone
fastmcppackage (the 3.x line as of July 2026). It moves faster than the official one and imports asfrom fastmcp import FastMCP. - An isolated environment: keep the server in its own
venvor underuv. The client launches it with its own interpreter, and stray packages in the system Python are a classic source of flaky "works on my machine" errors.
An honest caveat: the official SDK has a next-generation beta line. For production in July 2026, take the stable release and keep the beta for experiments.
What a minimal Python server looks like
A complete server with one tool:
from mcp.server.fastmcp import FastMCP
mcp = FastMCP("weather")
@mcp.tool()
def get_forecast(city: str) -> str:
"""Return a short weather forecast for the given city."""
# a real external API call goes here
return f"{city}: +18°C, clear"
if __name__ == "__main__":
mcp.run() # default transport is stdio
The key idea: type hints and the docstring aren't decoration, they're the tool schema the model sees. The function name, argument types and description text are exactly what the assistant uses to decide whether to call the tool and with which parameters. A vague docstring means wrong calls. Describe tools as carefully as you would brief a subagent.
A tool can also be asynchronous — declare it with async def and await network calls inside without blocking the server. The return value is serialized automatically: a string, number, dict or Pydantic model reaches the model as a structured result rather than faceless text — which is why a well-designed response type matters as much as the name and description.
A resource is declared just as simply — the @mcp.resource("config://app") decorator over a function that returns the data for that URI.
How to run the server and connect it to Claude Code and Cursor
Running locally in stdio mode:
python server.py
stdio is the transport where the client itself launches the server as a subprocess and exchanges messages over standard input/output. For local assistants it's the default. There are three transports in total:
| Transport | What it is | When to use it |
|---|---|---|
| stdio | Client launches the server as a subprocess | Local integrations, the default |
| Streamable HTTP | Server lives behind an HTTP endpoint | Production, remote access, many clients |
| SSE | Server-Sent Events over HTTP | Legacy; prefer Streamable HTTP for new code |
Connecting to Claude Code takes one CLI command:
claude mcp add weather -- python /absolute/path/server.py
Everything after -- is passed to the server as the launch command; flags like --transport, --env and --scope go before the name. In Cursor and Claude Desktop the server is registered by hand in a JSON config. Both scenarios are walked through step by step in a separate guide — how to connect MCP to Claude Code.
After adding it, make sure the client actually sees the server: in Claude Code the /mcp command shows connected servers and their status, in Cursor there's a toggle in the MCP settings. A "failed" status almost always means a path or environment problem — check those first, and only then the code itself.
How to debug an MCP server
The main tool is MCP Inspector, a web interface showing your declared tools and letting you call them by hand:
mcp dev server.py
Three common mistakes:
print()to stdout breaks stdio. In stdio mode, standard output is occupied by JSON-RPC frames. Anyprint()to stdout corrupts the stream — log only tostderror to a file.- The server never shows up in the client. Almost always a relative path or the wrong interpreter. Specify an absolute path and the same virtual environment where the SDK is installed.
- The tool exists but never gets called. The model doesn't understand what it's for. Sharpen the docstring and the argument names — that's the "interface" the model reads.
Security: what you can't skip
An MCP server is code that performs actions at a model's "request," and its input is untrusted text. The baseline checklist:
- Don't trust the arguments. Validate everything coming from the model as you would user input: paths, SQL, shell commands.
- Least privilege. Give the server exactly the access its tools need, and no more.
- Secrets in environment variables, not in the code and not in docstrings (the docstring goes to the model).
- Prompt injection through responses. If a tool returns external text (a web page, an email), the model may read it as an instruction. Mark such content as data, not commands.
- Confirmation for irreversible actions. Deletion, payments, sending messages — only with explicit human confirmation.
From your own server to a ready-made setup
Writing your own MCP server is the right call for a unique integration. But most everyday tasks are already covered by existing servers plus a well-tuned workflow. The Arsenal section collects proven servers with setup instructions, and the Quest vibe coding engine (as of July 2026 — 4900 ₽ one-time for the engine, no subscription; modules at 1900 ₽ each) gives you a ready-made setup: rules, prompts and a tool stack so Claude Code behaves predictably from the start. If your goal is to extend the assistant without writing a server at all, look at Claude Skills — it's often faster.
FAQ
How is an MCP server different from a Skill or a plugin?
An MCP server gives the model new tools and data over the protocol and works with any MCP client. A Skill is a bundle of instructions and files that extends Claude's behavior specifically, with no separate process. They're often combined: the server provides access, the Skill provides the method.
Does the server have to be written in Python?
No. It's JSON-RPC underneath, and SDKs exist for many languages. Python gets picked for fast prototyping and a rich ecosystem, but the contract is identical across implementations.
Do I need a separate server for each tool?
No. A single server can declare dozens of tools and resources. Group by domain: one server per external system or per related set of tasks.
stdio or HTTP — which should I choose?
For a local assistant on your own machine, stdio: it's the default and needs no network. If the server is shared, runs on a remote machine or serves several clients, use Streamable HTTP with TLS.
How does this work from Russia?
The SDK itself and running the server don't depend on your region — it's open source. The restrictions concern access to the assistant and the Anthropic API; legally that's handled through aggregators and foreign cards. Details in our guide to the Claude API from Russia.