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):

bash
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:

bash
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. ToolError returns a clear message instead of crashing the server.
  • A limit. The limit parameter 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 the if __name__ == "__main__" guard keeps an import of the file from starting the server.
  • The server object lives in a global variable mcp: the mcp run command looks for an object named mcp, server or app.

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.

bash
uv run mcp dev server.py

Open 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:

bash
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:

bash
claude mcp add todo-scanner -- uv run --with "mcp[cli]" mcp run /absolute/path/to/server.py

The 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):

bash
export ANTHROPIC_BASE_URL=https://api.zukko.pro
export ANTHROPIC_AUTH_TOKEN=fetch_your_key
export ANTHROPIC_MODEL=claude-sonnet-5-5

The 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:

bash
uv run mcp run server.py --transport streamable-http

The 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

  1. Run the server command by hand. Silence and no stack trace mean all is well: a stdio server waits for the host.
  2. Check absolute paths (to server.py and to uv).
  3. Don't write to stdout: on stdio it is the protocol channel. Use the logging module for logs.
  4. Restart the agent session after changing configuration.
  5. Check what your object is called. If it is not mcp, server or app, 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.

Get API keyDocumentation

api.zukko.pro · fetch_*

How to write your own MCP server in Python: example · Zukko