How to write your own MCP server in Python and connect it to an agent
We write an MCP server in Python in about 30 lines, test it without an agent, connect it to Claude Code and watch the agent call the tool.
5 min · · development · агенты
To write your own MCP server in Python, the official SDK and an ordinary function with types and a docstring are enough: the library builds the tool description itself. The server is then connected to an agent such as Claude Code, and the model starts calling your function when the task needs it. Below we build a working example with one tool, test it without an agent, register it in Claude Code running through the Zukko gateway, and look at what to do if the server doesn't show up.
What we're building
A find_todos tool: it takes a project directory and returns lines with TODO and FIXME comments together with the file and line number. For an agent it's a convenient way to get a summary of code debt without searching the whole repository by hand. The tool only reads files and changes nothing.
According to the MCP documentation, a server offers clients three kinds of capabilities: tools (called by the model), resources (data the application supplies) and prompts (templates chosen by the user). We need the first.
Installation
The SDK README requires Python 3.11 or newer. Install with the mcp development command (the first line is for pip, the second for a uv project):
pip install "mcp[cli]"
uv add "mcp[cli]"The cli extra installs the mcp utility with the mcp dev, mcp run and mcp install subcommands.
How to write your own MCP server in Python: the code
The server.py file:
from pathlib import Path
from mcp.server import MCPServer
from mcp.server.mcpserver.exceptions import ToolError
mcp = MCPServer("todo-scanner")
SUFFIXES = {".py", ".js", ".ts", ".java"}
SKIP_DIRS = {".git", "node_modules", ".venv"}
@mcp.tool()
def find_todos(directory: str, limit: int = 50) -> list[str]:
"""Find TODO and FIXME comments in source files under a directory.What matters here:
- No schema to write. The tool name comes from the function name, the description from the docstring, and the argument schema from the type annotations. So write the docstring for the model: it decides when to call the tool from it.
- An error for the model.
ToolErrorreturns a clear message instead of crashing the server. - A limit. The
limitparameter protects the agent's context from a huge reply. More on that in the cost estimate. mcp.run()with no arguments starts the server over stdio, and theif __name__ == "__main__"guard keeps an import of the file from starting the server.- The server object lives in a global variable
mcp: themcp runcommand looks for an object namedmcp,serverorapp.
In SDK version 2 the class is MCPServer, imported from mcp.server. In the previous line (1.x) the same class was FastMCP, imported from mcp.server.fastmcp; the migration documentation says FastMCP was renamed to MCPServer. Tool decorators work the same way, but some constructor and run() parameters changed in version 2, so check the migration guide when porting old code. Check your version with pip show mcp.
Testing without an agent
The shortest path is to open the server in MCP Inspector.
uv run mcp dev server.pyOpen the address the command prints. The tools tab will have find_todos with a form of two fields; call it on a directory with code. If the form appears and an answer comes back, the server is healthy.
For automated tests the SDK offers connecting to the server directly in memory, with no process or port:
import anyio
from mcp import Client
from server import mcp
async def main() -> None:
async with Client(mcp) as client:
result = await client.call_tool("find_todos", {"directory": ".", "limit": 5})
print(result.structured_content)
anyio.run(main)Connecting to Claude Code
The host launches your file as a child process, so connecting comes down to one launch command. The SDK documentation gives this form for Claude Code, where everything after -- is the server command:
claude mcp add todo-scanner -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.pyThe path must be absolute: the host starts the server from its own working directory. If uv isn't on the host's PATH, give its full path (which uv). To check, the /mcp command inside a Claude Code session shows that the server is connected and which tools it offers. From the terminal, claude mcp list and claude mcp get todo-scanner show the same.
To make Claude Code itself work through Zukko, set the variables (Claude Code guide):
export ANTHROPIC_BASE_URL=https://api.zukko.pro
export ANTHROPIC_AUTH_TOKEN=fetch_your_key
export ANTHROPIC_MODEL=claude-sonnet-5-5The address has no /v1, and the variable is specifically ANTHROPIC_AUTH_TOKEN. Run claude in the project directory and write: "Find all TODOs in the current project and group them by file". In the output you will see a call to the find_todos tool; on first use Claude Code may ask for permission. More on project setup in the notes on connecting Claude Code to a project and MCP servers in Claude Code.
The HTTP variant
If the server is needed beyond your machine, the SDK can work over Streamable HTTP:
uv run mcp run server.py --transport streamable-httpThe server listens on a port (in the SDK examples, http://localhost:8000/mcp), and in Claude Code it is added with claude mcp add --transport http todo-scanner http://localhost:8000/mcp. For anything outside your machine, plan authorization in advance.
What it does to spending
A tool result lands in the context and is resent to the model in the following requests. Suppose a find_todos reply takes 1,250 tokens and the agent then makes 10 more requests. The calculation for claude-sonnet-5-5 (prices as of October 18, 2026, $ per 1M tokens: input 0.4, cache read 0.05):
- without cache: 12,500 × 0.4 / 1,000,000 = $0.005;
- with cache reads: 12,500 × 0.05 / 1,000,000 = $0.000625.
Claude Code uses the Anthropic format, where the gateway sees the cache and applies the discount separately. Even so, a large tool output costs money at every following step, so limit replies (limit) and return the essentials. Model list and prices: GET https://api.zukko.pro/v1/models.
If the server didn't show up
- Run the server command by hand. Silence and no stack trace mean all is well: a stdio server waits for the host.
- Check absolute paths (to
server.pyand touv). - Don't write to stdout: on stdio it is the protocol channel. Use the
loggingmodule for logs. - Restart the agent session after changing configuration.
- Check what your object is called. If it is not
mcp,serverorapp, give the name explicitly:mcp run server.py:name.
FAQ
How does an MCP server differ from function calling in the API?
Function calling describes functions inside the request itself, and your application executes the code. MCP moves tools into a separate process with a standard protocol, and any compatible host connects them without extra work. The comparison is covered in a separate note on function calling.
Do I need async?
No: functions can be plain. Async is for when you make network calls inside and don't want to block the server.
How do I secure the server?
Keep tools narrow and read-only where possible, validate arguments, and don't grant arbitrary access to the file system or commands. Don't put secrets in a tool description.
Does this work from Russia?
There are three ways to pay from Russia: SBP, MasterCard cards and cryptocurrency. The MCP server itself runs locally and doesn't depend on that.
Create a key in the dashboard, connect Claude Code and ask the agent to call your tool.
zukko
Pay 25×
less
for Claude Code & ChatGPT
One fetch_* key and base URL api.zukko.pro — for Cursor, Claude Code, Codex, and any SDK.
Related articles
- Prompt Injection: What It Is and How to Protect a Bot and an AgentHow prompt injection works through user input and third-party documents, and which measures limit the damage: data isolation, tool permissions, key limits.
- Claude Code YOLO: Permission Modes and Working Without PromptsClaude Code has no mode literally called YOLO, but it has several permission modes and flexible rules. How to stop clicking Yes without losing files.
- Claude Code: a checklist of best practices for daily workHabits that make working with Claude Code predictable: a short CLAUDE.md, a plan before edits, small tasks, checking results and keeping spend under control.
- An MCP server in Java: a minimal example with one toolWe write a minimal MCP server in Java with one tool, register it in Claude Code, and learn to find the cause when the agent doesn't see it.