cd /news/ai-agents/fastmcp-turn-a-python-function-into-… · home › topics › ai-agents › article
[ARTICLE · art-168132] src=dev.to ↗ pub= topic=ai-agents verified=true sentiment=↑ positive

FastMCP: turn a Python function into an MCP tool Claude and Cursor can call

FastMCP, a Python framework maintained by Prefect, lets developers expose ordinary Python functions as Model Context Protocol tools that clients such as Claude Code and Cursor can discover and call, generating input schemas from type hints and docstrings. The current PyPI release, 4.1.8, requires Python 3.10+, and the framework supports argument validation, JSON Schema enums via Literal types, and read-only tool annotations. A worked example shows an internal order-lookup function decorated with @mcp.tool and served over stdio or Streamable HTTP.

by read6 min views5 publishedNov 12, 2026

Thursday AI Tool/Workflow. This week's tool: FastMCP, the Python framework for building Model Context Protocol (MCP) servers and clients. You write a normal Python function, add one decorator, and Claude Code, Cursor, or any other MCP client can discover it and call it as a tool.

If you've tried to give an AI assistant access to your own systems, you know the usual mess: copy-pasting query results into chat, writing a one-off plugin for each client, or hand-writing JSON schemas that drift from the code. MCP fixes the protocol side: one server, many clients. FastMCP fixes the Python side: the schema comes from your type hints.

Here's exactly how to use it, with a real workflow: expose an internal order lookup as a read-only tool, test it, and connect it to a client.

From the official MCP docs: an MCP host (an AI app like Claude Code, Claude Desktop or VS Code) creates one MCP client per MCP server it connects to. Servers expose three core primitives:

Messages are JSON-RPC 2.0, carried over stdio (a local process) or Streamable HTTP (a remote server).

FastMCP is maintained by Prefect (authors Jeremiah Lowin and Nate Nowack), Apache-2.0 licensed. FastMCP 1.0 was incorporated into the official MCP Python SDK in 2024; the standalone fastmcp package is the actively maintained project. The current release on PyPI is 4.1.8 (published November 11, 2026 IST), and it requires Python 3.10+.

When you decorate a function with @mcp.tool, FastMCP uses the function name as the tool name, the docstring as the description (including per-argument descriptions from an Args: section), and the type hints as the input schema. It also validates arguments before your code runs.

uv add fastmcp
pip install fastmcp

fastmcp version

Say your support team constantly asks "where is order X?" and the answer lives in an internal DB. Here's a server with one narrow, read-only tool:

from typing import Annotated, Literal

from pydantic import Field
from fastmcp import FastMCP
from fastmcp.exceptions import ToolError
from mcp.types import ToolAnnotations

mcp = FastMCP("Order Lookup", mask_error_details=True)

ORDERS = {
    "ORD-10421": {"status": "shipped", "carrier": "BlueDart", "eta": "2026-11-14"},
    "ORD-10422": {"status": "processing", "carrier": None, "eta": None},
}

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False))
def get_order_status(
    order_id: Annotated[str, Field(pattern=r"^ORD-\d{5}$")],
    fields: list[Literal["status", "carrier", "eta"]] = ["status"],
) -> dict:
    """Look up one order by ID and return only the requested fields.

    Args:
        order_id: Order ID in the form ORD-12345.
        fields: Which fields to return.
    """
    order = ORDERS.get(order_id)
    if order is None:
        raise ToolError(f"No order found with ID {order_id}.")
    return {"order_id": order_id, **{f: order[f] for f in fields}}

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

What each piece does:

Field(pattern=...) puts a regex in the schema ORD- plus five digits is rejected before your function runs.Literal[...] becomes a JSON Schema enum, so the model can only ask for fields you allow. No "give me the customer's address".ToolAnnotations(readOnlyHint=True) tells clients this tool doesn't modify anything. The FastMCP docs note clients use these hints to decide on confirmation prompts, but they are ToolError messages always reach the client. With mask_error_details=True, any dict gives the client both a text block and structuredContent JSON. Here's the input schema FastMCP generated from that signature (I ran it with 4.1.8):

{
  "type": "object",
  "additionalProperties": false,
  "properties": {
    "order_id": {"type": "string", "pattern": "^ORD-\\d{5}$",
                 "description": "Order ID in the form ORD-12345."},
    "fields": {"type": "array", "default": ["status"],
               "items": {"type": "string", "enum": ["status", "carrier", "eta"]},
               "description": "Which fields to return."}
  },
  "required": ["order_id"]
}

No hand-written schema. Change the function, the schema follows.

fastmcp run server.py

fastmcp run server.py --transport http --port 8000

fastmcp run finds an object named mcp, server or app in the file, or you can point at one with server.py:my_server.

Three ways, from fastest to most visual.

CLI: list tools and call one directly.

fastmcp list server.py
fastmcp call server.py get_order_status order_id=ORD-10421

In-memory client: pass the server object straight to Client. No subprocess, no network, so it drops straight into pytest.

import asyncio
from fastmcp import Client
from server import mcp

async def main():
    async with Client(mcp) as client:
        print([t.name for t in await client.list_tools()])

        ok = await client.call_tool(
            "get_order_status", {"order_id": "ORD-10421", "fields": ["status", "eta"]}
        )
        print(ok.data)

        bad = await client.call_tool(
            "get_order_status", {"order_id": "DROP TABLE"}, raise_on_error=False
        )
        print(bad.is_error)  # True - string_pattern_mismatch, function never ran

        missing = await client.call_tool(
            "get_order_status", {"order_id": "ORD-99999"}, raise_on_error=False
        )
        print(missing.content[0].text)  # No order found with ID ORD-99999.

asyncio.run(main())

Those are the real outputs from my run. Write these as pytest cases (the FastMCP testing docs use pytest-asyncio with asyncio_mode = "auto") and you have a contract test for every tool.

MCP Inspector: the official visual debugger from the MCP project.

fastmcp dev inspector server.py

It starts your server with the Inspector UI, reloads on file changes, and lets you click through tools and see raw requests and responses.

FastMCP has installers for common clients:

fastmcp install claude-code server.py
fastmcp install cursor server.py
fastmcp install claude-desktop server.py

There are also gemini-cli and goose installers. For anything else, generate the config:

fastmcp install mcp-json server.py
{
  "Order Lookup": {
    "command": "uv",
    "args": ["run", "--with", "fastmcp", "fastmcp", "run", "/path/to/server.py"]
  }
}

Or register it with Claude Code yourself:

claude mcp add order-lookup -- uv run --with fastmcp fastmcp run server.py

Then ask: "What's the status and ETA for ORD-10421?" The client lists tools, sees get_order_status, calls it with typed arguments, and answers from your data.

An MCP tool is code the model decides to run. Treat every tool like a public API endpoint that a very fast, very literal intern can call.

get_order_status(order_id) beats run_sql(query). One job per tool, smallest possible blast radius. pattern, ge/ le, max_length and Literal are enforced before your code runs. Optionally set FastMCP(..., strict_input_validation=True) to stop type coercion such as "10" becoming 10. readOnlyHint=True, mark irreversible actions destructiveHint=True, but enforce permissions in your code. Hints are advisory. user_id, credentials or DB connections with Depends(); those parameters are excluded from the schema.@mcp.tool(timeout=30.0) returns an MCP error if a tool runs too long. Note the docs say a timeout does not undo side effects. Start with one read-only tool your team asks about daily. Test it with Client(mcp). Then connect it.

What's the first internal lookup you'd expose as an MCP tool, and what would you refuse to expose?

── more in #ai-agents 4 stories · sorted by recency
── more on @fastmcp 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
→ Live at https://your-agent.zahid.host ✓
Get free account → Pricing
from €0/mo · no card required
LIVE [news/fastmcp-turn-a-pytho…] indexed:0 read:6min 2026-11-12 · —