{"slug": "fastmcp-turn-a-python-function-into-an-mcp-tool-claude-and-cursor-can-call", "title": "FastMCP: turn a Python function into an MCP tool Claude and Cursor can call", "summary": "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.", "body_md": "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.\n\nIf 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.\n\nHere'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.\n\nFrom 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:\n\nMessages are JSON-RPC 2.0, carried over **stdio** (a local process) or **Streamable HTTP** (a remote server).\n\n**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+.\n\nWhen 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.\n\n```\nuv add fastmcp\n# or\npip install fastmcp\n\nfastmcp version\n```\n\nSay 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:\n\n``` python\n# server.py\nfrom typing import Annotated, Literal\n\nfrom pydantic import Field\nfrom fastmcp import FastMCP\nfrom fastmcp.exceptions import ToolError\nfrom mcp.types import ToolAnnotations\n\nmcp = FastMCP(\"Order Lookup\", mask_error_details=True)\n\n# Stand-in for your real DB / internal API\nORDERS = {\n    \"ORD-10421\": {\"status\": \"shipped\", \"carrier\": \"BlueDart\", \"eta\": \"2026-11-14\"},\n    \"ORD-10422\": {\"status\": \"processing\", \"carrier\": None, \"eta\": None},\n}\n\n@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True, openWorldHint=False))\ndef get_order_status(\n    order_id: Annotated[str, Field(pattern=r\"^ORD-\\d{5}$\")],\n    fields: list[Literal[\"status\", \"carrier\", \"eta\"]] = [\"status\"],\n) -> dict:\n    \"\"\"Look up one order by ID and return only the requested fields.\n\n    Args:\n        order_id: Order ID in the form ORD-12345.\n        fields: Which fields to return.\n    \"\"\"\n    order = ORDERS.get(order_id)\n    if order is None:\n        raise ToolError(f\"No order found with ID {order_id}.\")\n    return {\"order_id\": order_id, **{f: order[f] for f in fields}}\n\nif __name__ == \"__main__\":\n    mcp.run()\n```\n\nWhat each piece does:\n\n`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.\nHere's the input schema FastMCP generated from that signature (I ran it with 4.1.8):\n\n```\n{\n  \"type\": \"object\",\n  \"additionalProperties\": false,\n  \"properties\": {\n    \"order_id\": {\"type\": \"string\", \"pattern\": \"^ORD-\\\\d{5}$\",\n                 \"description\": \"Order ID in the form ORD-12345.\"},\n    \"fields\": {\"type\": \"array\", \"default\": [\"status\"],\n               \"items\": {\"type\": \"string\", \"enum\": [\"status\", \"carrier\", \"eta\"]},\n               \"description\": \"Which fields to return.\"}\n  },\n  \"required\": [\"order_id\"]\n}\n```\n\nNo hand-written schema. Change the function, the schema follows.\n\n```\n# stdio (default) - what local clients like Claude Code and Cursor launch\nfastmcp run server.py\n\n# Streamable HTTP - for a shared/remote server, served at /mcp\nfastmcp run server.py --transport http --port 8000\n```\n\n`fastmcp run` finds an object named `mcp`, `server` or `app` in the file, or you can point at one with `server.py:my_server`.\n\nThree ways, from fastest to most visual.\n\n**CLI:** list tools and call one directly.\n\n```\nfastmcp list server.py\nfastmcp call server.py get_order_status order_id=ORD-10421\n# {\"order_id\": \"ORD-10421\", \"status\": \"shipped\"}\n```\n\n**In-memory client:** pass the server object straight to `Client`. No subprocess, no network, so it drops straight into pytest.\n\n``` python\nimport asyncio\nfrom fastmcp import Client\nfrom server import mcp\n\nasync def main():\n    async with Client(mcp) as client:\n        print([t.name for t in await client.list_tools()])\n\n        ok = await client.call_tool(\n            \"get_order_status\", {\"order_id\": \"ORD-10421\", \"fields\": [\"status\", \"eta\"]}\n        )\n        print(ok.data)\n        # {'order_id': 'ORD-10421', 'status': 'shipped', 'eta': '2026-11-14'}\n\n        bad = await client.call_tool(\n            \"get_order_status\", {\"order_id\": \"DROP TABLE\"}, raise_on_error=False\n        )\n        print(bad.is_error)  # True - string_pattern_mismatch, function never ran\n\n        missing = await client.call_tool(\n            \"get_order_status\", {\"order_id\": \"ORD-99999\"}, raise_on_error=False\n        )\n        print(missing.content[0].text)  # No order found with ID ORD-99999.\n\nasyncio.run(main())\n```\n\nThose 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.\n\n**MCP Inspector:** the official visual debugger from the MCP project.\n\n```\nfastmcp dev inspector server.py\n```\n\nIt starts your server with the Inspector UI, reloads on file changes, and lets you click through tools and see raw requests and responses.\n\nFastMCP has installers for common clients:\n\n```\nfastmcp install claude-code server.py\nfastmcp install cursor server.py\nfastmcp install claude-desktop server.py\n```\n\nThere are also `gemini-cli` and `goose` installers. For anything else, generate the config:\n\n```\nfastmcp install mcp-json server.py\n{\n  \"Order Lookup\": {\n    \"command\": \"uv\",\n    \"args\": [\"run\", \"--with\", \"fastmcp\", \"fastmcp\", \"run\", \"/path/to/server.py\"]\n  }\n}\n```\n\nOr register it with Claude Code yourself:\n\n```\nclaude mcp add order-lookup -- uv run --with fastmcp fastmcp run server.py\n```\n\nThen 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.\n\nAn 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.\n\n`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.\nStart with one read-only tool your team asks about daily. Test it with `Client(mcp)`. Then connect it.\n\n*What's the first internal lookup you'd expose as an MCP tool, and what would you refuse to expose?*", "url": "https://wpnews.pro/news/fastmcp-turn-a-python-function-into-an-mcp-tool-claude-and-cursor-can-call", "canonical_source": "https://dev.to/m_t_ramkrushna/fastmcp-turn-a-python-function-into-an-mcp-tool-claude-and-cursor-can-call-nkm", "published_at": "2026-11-12 04:10:35+00:00", "updated_at": "2026-11-12 04:21:18.805281+00:00", "lang": "en", "topics": ["ai-agents", "agent-protocols", "developer-tools", "ai-tools", "ai-products"], "entities": ["FastMCP", "Prefect", "Model Context Protocol", "Claude Code", "Cursor", "Jeremiah Lowin", "Nate Nowack", "PyPI"], "also_reported_by": [], "alternates": {"html": "https://wpnews.pro/news/fastmcp-turn-a-python-function-into-an-mcp-tool-claude-and-cursor-can-call", "markdown": "https://wpnews.pro/news/fastmcp-turn-a-python-function-into-an-mcp-tool-claude-and-cursor-can-call.md", "text": "https://wpnews.pro/news/fastmcp-turn-a-python-function-into-an-mcp-tool-claude-and-cursor-can-call.txt", "jsonld": "https://wpnews.pro/news/fastmcp-turn-a-python-function-into-an-mcp-tool-claude-and-cursor-can-call.jsonld"}}