All Guides

Developer Guide

Write a Python MCP Server for OpenClaw

Every integration guide on this site ends the same way: register an MCP server, probe it, use it. This one is about the other side — writing the server. It takes about fifteen lines of Python, and the interesting decisions are async versus sync, and how you describe the tool.

Install the SDK

pip install "mcp[cli]"
# or, with uv:
uv add "mcp[cli]"

The [cli] extra matters — without it you get the library but not the mcp dev and mcp run commands, and the first thing you want is to run the server on its own before an agent is involved.

FastMCP was renamed in mcp 2.x. Almost every tutorial you will find opens with from mcp.server.fastmcp import FastMCP. On a current install that raises ModuleNotFoundError — the SDK ships a stub at that path whose only job is to tell you so. The class is now MCPServer, imported from mcp.server.mcpserver. If you must run v1 code unchanged, pin mcp<2.

A server that does one thing

# server.py
from mcp.server.mcpserver import MCPServer

mcp = MCPServer("mytools")

@mcp.tool()
def add(a: int, b: int) -> int:
    """Add two numbers and return the result."""
    return a + b

if __name__ == "__main__":
    mcp.run()

Three things are doing real work here. The @mcp.tool() decorator registers the function. The type hints become the tool's input schema, so the model knows it must send two integers. And the docstring becomes the tool description — which is the single highest-leverage line in the file, because it is what the model reads when deciding whether this tool is relevant.

Sync or async

A plain def is fine and is what the minimal example uses. Reach for async def when your tool awaits something:

import httpx
from mcp.server.mcpserver import MCPServer

mcp = MCPServer("mytools")

@mcp.tool()
async def fetch_status(url: str) -> str:
    """Fetch a URL and return its HTTP status code."""
    async with httpx.AsyncClient(timeout=10) as client:
        response = await client.get(url)
        return f"{response.status_code}"
The rule is the opposite of what people assume. A blocking call inside a plain def tool is safe: the SDK runs sync tool functions in a worker thread (anyio.to_thread.run_sync), so they do not occupy the event loop. The hazard is blocking inside an async def tool — using requests or a synchronous database driver there stalls the loop and everything else the server is doing. Pick async def only when you actually have something to await, and use an async client when you do.

Always set a timeout on outbound calls. An agent that hangs waiting on your tool looks to the user like the agent is broken, and there is nothing in the transcript to suggest your server is the cause.

Run it before wiring it up

uv run mcp dev server.py                              # interactive dev mode
uv run mcp run server.py --transport streamable-http  # HTTP mode

The SDK supports stdio, streamable-http and sse. Local server on the same machine as the agent: stdio, no network surface at all. Server somewhere else: streamable-http, behind TLS and authentication before anything can reach it.

Register it with OpenClaw

# Local, stdio
openclaw mcp add mytools \
  --command python \
  --arg server.py \
  --connect-timeout 60

# Remote, over HTTP
openclaw mcp add mytools \
  --url https://tools.example.com/mcp \
  --transport streamable-http \
  --header authorization="Bearer YOUR_TOKEN"

The stdio argument flag is singular and repeatable: one --arg per argument, in order. This is worth stating because Hermes Agent uses a plural --args that consumes everything after it, and copying a Hermes command into OpenClaw fails on an unknown option.

Use an absolute path to server.py, or set --cwd. A relative path is resolved against whatever working directory the runtime happens to have, so it can pass the probe from your shell and then fail when the service starts from somewhere else — a failure that appears days later and looks like the server broke on its own.

Two flags are worth knowing about here. openclaw mcp add probes the server before saving it, so a broken command fails at registration rather than silently later — pass --no-probe only if you deliberately want to save without connecting. And --include / --exclude take comma-separated tool names or globs, which is how you narrow a large server down to the tools you actually want exposed.

Verify with a probe

openclaw mcp list
openclaw mcp show mytools
openclaw mcp probe mytools

A probe opens a real MCP session and lists capabilities. Do this before you conclude anything from chatting with the agent. If the tools appear in the probe but the agent never uses them, the problem is the description, not the transport — rewrite the docstring to say plainly when the tool should be used, not just what it does.

Design notes that save you a rewrite

  • Few tools, clearly separated. A model choosing between six well-named tools does better than one choosing between sixty near-identical ones. Tool count is a cost, not a feature.
  • Return text a model can use. A dense JSON blob is worse than a short sentence stating the answer. You are writing for a reader, and the reader is the model.
  • Fail loudly and specifically. “Rate limited by the upstream API, retry in 60s” lets the agent do something sensible. A bare exception makes it guess.
  • Validate inputs yourself. Type hints shape the schema; they do not stop a model sending a plausible but wrong value.

Where this fits

An MCP server is the right tool when you need code to execute — hitting an internal API, querying a database, touching a system only you can reach. If you only need to give the agent knowledge or a procedure, a skill is lighter: see ClawHub skills instead. The same server also works with Hermes Agent, since both runtimes speak MCP.

OpenClaw Launch gives you a managed instance to point your server at, so you can test the integration without standing up an agent host first.

Python MCP servers and OpenClaw FAQ

Do MCP tool functions have to be async?

No. The MCP Python SDK accepts a plain def, and the canonical minimal server uses one. Use async def when your tool awaits something — an HTTP call, a database query — and note that the SDK runs sync tools in a worker thread, so blocking inside a plain def is safe. What is not safe is blocking inside an async def.

How do I install the MCP Python SDK?

pip install "mcp[cli]", or uv add "mcp[cli]" with uv. The [cli] extra is what gives you the mcp dev and mcp run commands.

Should I use stdio or HTTP transport?

Use stdio when the server runs on the same machine as the agent — it is simpler and needs no network exposure. Use streamable-http when the server lives elsewhere, and put it behind TLS and authentication before it is reachable.

How do I register it with OpenClaw?

For a local server: openclaw mcp add mytools --command python --arg server.py. The flag is singular and repeatable — one --arg per argument. Hermes Agent uses a plural --args that behaves differently, so the two are not interchangeable.

The agent ignores my tool. What is wrong?

Usually the server never started, or its description is too vague for the model to know when to call it. Run openclaw mcp probe first: a probe opens a real MCP session and lists capabilities, which tells you whether the problem is transport or prompting.

Related developer guides

Deploy an OpenClaw agent in seconds

Get a managed OpenClaw instance to point your MCP server at — no VPS to provision first.

Deploy OpenClaw