Build a complete MCP server in Python using the FastMCP framework. Define tools with the @mcp.tool() decorator, resources with @mcp.resource(), and prompts with @mcp.prompt(). Use type hints and docstrings for parameter descriptions and validation. Run the server with mcp.run() on stdio or Streamable HTTP transport.
Building a Complete MCP Server in Python
The MCP Python SDK provides FastMCP, a high-level framework that makes building MCP servers feel natural in Python. Instead of calling registerTool() with schemas, you decorate async functions with @mcp.tool(), use type hints for parameter types, and write docstrings for descriptions. FastMCP handles JSON Schema generation, validation, and protocol communication automatically.
This tutorial builds a complete server with tools, resources, and prompts. It covers the Python-specific patterns that differ from the TypeScript SDK, including type annotation conventions, error handling with exceptions, and running the server in different modes.
Prerequisites
- Python 3.10+ installed (for union type syntax and modern type hints)
- MCP Python SDK installed: pip install mcp
- Basic familiarity with Python async/await and type hints
- Understanding of MCP concepts (tools, resources, prompts)
Step-by-step guide
Install the SDK and create a server instance
Install the SDK and create a server instance
Install the MCP Python SDK with pip. Then create a new Python file and instantiate FastMCP with a server name. The FastMCP class handles all protocol details — you just decorate functions to register them as tools, resources, or prompts.
1# Install2pip install mcp34# server.py5from mcp.server.fastmcp import FastMCP6import sys78# Create server instance9mcp = FastMCP("my-python-server")1011# All logging goes to stderr (stdout is reserved for MCP protocol)12print("Server initializing...", file=sys.stderr)Expected result: A FastMCP server instance ready to accept tool, resource, and prompt registrations.
Define tools with decorators and type hints
Define tools with decorators and type hints
Use the @mcp.tool() decorator to register a function as an MCP tool. The function name becomes the tool name (with underscores converted to hyphens). Type hints define parameter types, and the docstring provides the tool description and parameter descriptions in Google docstring format. Return a string for simple text results.
1# Python2import json3import httpx45@mcp.tool()6async def get_weather(city: str, units: str = "celsius") -> str:7 """Get current weather for a city.89 Args:10 city: City name, e.g. San Francisco11 units: Temperature units (celsius or fahrenheit)12 """13 async with httpx.AsyncClient() as client:14 response = await client.get(15 "https://api.weather.example/v1/current",16 params={"city": city, "units": units},17 )18 response.raise_for_status()19 data = response.json()20 return f"{city}: {data['temperature']}° {units}, {data['condition']}"2122@mcp.tool()23async def calculate(expression: str) -> str:24 """Evaluate a mathematical expression safely.2526 Args:27 expression: Math expression like '2 + 3 * 4'28 """29 # Safe eval for basic math30 allowed = set("0123456789+-*/.() ")31 if not all(c in allowed for c in expression):32 raise ValueError("Error: Only basic math operators allowed")33 result = eval(expression) # safe after character validation34 return str(result)Expected result: Two tools registered: get_weather and calculate, each with validated parameters from type hints.
Define resources with URI patterns
Define resources with URI patterns
Use @mcp.resource() to register data sources. Pass the URI as the decorator argument. For static resources, use a fixed URI. For templates, use curly-brace parameters that match function arguments. Return a string for text content or bytes for binary data.
1# Python2import os34@mcp.resource("config://app")5async def get_config() -> str:6 """Application configuration."""7 config = {8 "app_name": "My Python App",9 "version": "1.0.0",10 "debug": os.getenv("DEBUG", "false") == "true",11 }12 return json.dumps(config, indent=2)1314@mcp.resource("file://project/{path}")15async def read_project_file(path: str) -> str:16 """Read a project source file."""17 # Security: prevent directory traversal18 safe_path = os.path.normpath(path)19 if safe_path.startswith("..") or os.path.isabs(safe_path):20 raise ValueError("Access denied: invalid path")2122 full_path = os.path.join(os.getcwd(), safe_path)23 with open(full_path, "r") as f:24 return f.read()Expected result: A static config resource and a parameterized file resource are available to MCP clients.
Define prompts with argument decorators
Define prompts with argument decorators
Use @mcp.prompt() to register message templates. The function returns a string (for single-message prompts) or a list of message dictionaries (for multi-message sequences). Arguments become prompt parameters that users fill in through the client UI.
1# Python2@mcp.prompt()3async def review_code(code: str, language: str = "python") -> str:4 """Generate a structured code review.56 Args:7 code: The code to review8 language: Programming language of the code9 """10 return f"""Review this {language} code for bugs, security issues, and improvements:1112```{language}13{code}14```1516Provide feedback in this format:171. Critical issues (bugs, security)182. Performance improvements193. Style and readability suggestions"""2021@mcp.prompt()22async def debug_error(error: str, context: str | None = None) -> str:23 """Help debug an error with structured analysis.2425 Args:26 error: The error message or stack trace27 context: Additional context about when the error occurs28 """29 ctx = f"\nContext: {context}" if context else ""30 return f"""Debug this error:{ctx}3132{error}3334Analyze:351. Root cause362. Step-by-step fix373. Prevention strategy"""Expected result: Two prompts registered that clients can invoke with arguments to start structured AI interactions.
Add error handling and run the server
Add error handling and run the server
Handle errors by raising ValueError or other exceptions in your tool functions — FastMCP catches them and returns isError responses to the client. Then start the server with mcp.run() specifying the transport. Use 'stdio' for local development and CLI integration, or 'streamable-http' for remote access. For teams building production Python MCP servers, RapidDev can help with deployment, monitoring, and scaling strategies.
1# Python — error handling in tools2@mcp.tool()3async def query_database(sql: str) -> str:4 """Run a read-only SQL query.56 Args:7 sql: SELECT query to execute8 """9 if not sql.strip().upper().startswith("SELECT"):10 raise ValueError("Only SELECT queries are allowed")1112 try:13 # Using asyncpg for async PostgreSQL14 import asyncpg15 conn = await asyncpg.connect(os.getenv("DATABASE_URL"))16 try:17 rows = await conn.fetch(sql)18 return json.dumps([dict(r) for r in rows], indent=2, default=str)19 finally:20 await conn.close()21 except asyncpg.PostgresError as e:22 raise ValueError(f"Database error: {e}")2324# Run the server25if __name__ == "__main__":26 mcp.run(transport="stdio")2728# Or for HTTP:29# mcp.run(transport="streamable-http", host="0.0.0.0", port=3000)Expected result: The server starts, listens on the chosen transport, and handles tool calls with proper error responses.
Complete working example
1"""MCP server built with Python FastMCP."""2import json3import os4import sys5from mcp.server.fastmcp import FastMCP67mcp = FastMCP("python-demo-server")89# --- Tools ---1011@mcp.tool()12async def calculate(expression: str) -> str:13 """Evaluate a mathematical expression.1415 Args:16 expression: Math expression like '2 + 3 * 4'17 """18 allowed = set("0123456789+-*/.() ")19 if not all(c in allowed for c in expression):20 raise ValueError("Only basic math operators are allowed")21 return str(eval(expression))2223@mcp.tool()24async def read_file(path: str) -> str:25 """Read a file from the project directory.2627 Args:28 path: Relative file path within the project29 """30 safe = os.path.normpath(path)31 if safe.startswith("..") or os.path.isabs(safe):32 raise ValueError("Access denied: path outside project")33 full = os.path.join(os.getcwd(), safe)34 if not os.path.exists(full):35 raise ValueError(f"File not found: {path}")36 with open(full, "r") as f:37 return f.read()3839@mcp.tool()40async def list_files(directory: str = ".") -> str:41 """List files in a project directory.4243 Args:44 directory: Relative directory path (default: project root)45 """46 safe = os.path.normpath(directory)47 if safe.startswith("..") or os.path.isabs(safe):48 raise ValueError("Access denied")49 full = os.path.join(os.getcwd(), safe)50 if not os.path.isdir(full):51 raise ValueError(f"Not a directory: {directory}")52 entries = []53 for entry in sorted(os.listdir(full)):54 entry_path = os.path.join(full, entry)55 kind = "dir" if os.path.isdir(entry_path) else "file"56 entries.append({"name": entry, "type": kind})57 return json.dumps(entries, indent=2)5859# --- Resources ---6061@mcp.resource("config://app")62async def app_config() -> str:63 """Application configuration."""64 return json.dumps({65 "name": "Python Demo",66 "version": "1.0.0",67 "python": sys.version,68 }, indent=2)6970# --- Prompts ---7172@mcp.prompt()73async def review_code(code: str, language: str = "python") -> str:74 """Generate a code review.7576 Args:77 code: Code to review78 language: Programming language79 """80 return f"""Review this {language} code:\n\n```{language}\n{code}\n```\n\nCheck for bugs, security, and style."""8182if __name__ == "__main__":83 print("Starting Python MCP server...", file=sys.stderr)84 mcp.run(transport="stdio")Common mistakes when building an MCP server in Python
Why it's a problem: Printing to stdout instead of stderr
How to avoid: Use print(..., file=sys.stderr) or logging configured to stderr. stdout is reserved for the MCP JSON-RPC protocol.
Why it's a problem: Missing Args section in docstrings
How to avoid: FastMCP extracts parameter descriptions from the Google-style Args section. Without it, parameters have no descriptions and AI models generate worse arguments.
Why it's a problem: Using synchronous functions instead of async
How to avoid: FastMCP expects async functions for tools, resources, and prompts. Use 'async def' and 'await' for all handlers.
Why it's a problem: Not validating file paths
How to avoid: Always normalize paths with os.path.normpath() and check for directory traversal (.. or absolute paths) before file access.
Why it's a problem: Forgetting to handle tool errors
How to avoid: Raise ValueError with a descriptive message for expected errors. FastMCP converts it to isError: true. Unhandled exceptions return generic errors.
Best practices
- Use Python 3.10+ for modern type hint syntax (str | None instead of Optional[str])
- Write Google-style docstrings with an Args section for every tool and prompt parameter
- Use async/await throughout — FastMCP is built on asyncio
- Raise ValueError for expected tool errors so the AI gets a clear message
- Log to sys.stderr, never stdout, to avoid corrupting the protocol stream
- Use httpx instead of requests for async HTTP calls in tool handlers
- Validate file paths and user inputs even in Python tools
- Test with the MCP Inspector: npx @modelcontextprotocol/inspector python server.py
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I want to build an MCP server in Python using FastMCP. Show me a complete server with tools (using @mcp.tool decorator), resources (using @mcp.resource), and prompts (using @mcp.prompt). Include type hints, docstrings, error handling, and the mcp.run() startup.
Add a Python MCP tool to my FastMCP server that [describe action]. Use the @mcp.tool() decorator with type hints for parameters and a Google-style docstring for descriptions. Handle errors by raising ValueError.
Frequently asked questions
Can I use synchronous functions with FastMCP?
FastMCP expects async functions. If you need to call synchronous code, use asyncio.to_thread() to run it in a thread pool: result = await asyncio.to_thread(sync_function, args).
How do I add dependencies to my Python MCP server?
Create a requirements.txt or pyproject.toml with your dependencies. Install with pip install -r requirements.txt. Common dependencies include httpx for HTTP, asyncpg for PostgreSQL, and aiofiles for async file I/O.
How does FastMCP generate JSON Schema from type hints?
FastMCP uses Pydantic internally to convert Python type hints into JSON Schema. str becomes {type: 'string'}, int becomes {type: 'integer'}, list[str] becomes {type: 'array', items: {type: 'string'}}. Docstring Args descriptions become schema descriptions.
Can I use Pydantic models for complex tool inputs?
Yes. Define a Pydantic BaseModel and use it as a parameter type. FastMCP will generate the full nested JSON Schema from the model definition.
How do I test my Python MCP server?
Run: npx @modelcontextprotocol/inspector python server.py. The Inspector connects to your server over stdio and lets you invoke tools, read resources, and test prompts interactively.
What is the difference between FastMCP and the low-level Server class?
FastMCP is a high-level wrapper that uses decorators and type hints. The low-level Server class requires manual JSON Schema definitions and message handling. Use FastMCP for most projects — it is simpler and covers all common patterns. For complex Python MCP projects that need low-level control, the RapidDev team can help evaluate which approach fits best.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation