MCP Servers

Build an MCP Server in Python — Step by Step

Learn how to build an MCP server in Python using FastMCP. This tutorial covers installation, tool creation, resource handling, testing, and deployment with practical code examples.

Prerequisites

You need Python 3.10 or later and a basic understanding of async/await syntax. The FastMCP library handles all protocol details — JSON-RPC transport, capability negotiation, schema generation — so you can focus entirely on your tool logic. You should also have Claude Desktop or Claude Code installed to test your server.

# Install the MCP Python SDK pip install mcp # Or with uv (recommended for faster installs) uv pip install mcp

The mcp package includes the FastMCP high-level framework, the low-level protocol implementation, and all transport handlers. FastMCP is the recommended way to build servers — it provides a decorator-based API similar to Flask or FastAPI that makes server creation straightforward.

Your First MCP Server

Create a file called server.py. The minimal MCP server needs three things: a Server instance, at least one tool function, and a way to run the server. Here is a complete working example that provides a word-counting tool:

from mcp.server.fastmcp import FastMCP # Create the server mcp = FastMCP("word-counter") @mcp.tool() def count_words(text: str) -> str: """Count the number of words in a text string. Args: text: The text to count words in """ word_count = len(text.split()) return f"The text contains {word_count} words." @mcp.tool() def count_characters(text: str, include_spaces: bool = True) -> str: """Count characters in a text string. Args: text: The text to count characters in include_spaces: Whether to include spaces in the count """ if include_spaces: return f"The text contains {len(text)} characters (including spaces)." return f"The text contains {len(text.replace(' ', ''))} characters (excluding spaces)." if __name__ == "__main__": mcp.run()

FastMCP uses Python type hints and docstrings to automatically generate the JSON Schema that MCP clients need. The text: str parameter becomes a required string in the schema. The include_spaces: bool = True parameter becomes an optional boolean with a default value. The docstring becomes the tool description that the AI reads to understand when and how to use each tool.

Connecting to Claude Desktop

To test your server, add it to the Claude Desktop configuration file. The configuration tells Claude Desktop how to start your server process:

{ "mcpServers": { "word-counter": { "command": "python", "args": ["/absolute/path/to/server.py"] } } }

Restart Claude Desktop after saving the configuration. You should see the server name in the MCP tools panel. Now you can ask Claude to count words in any text and it will call your MCP server tool. If something goes wrong, check the Claude Desktop logs — on macOS they are in ~/Library/Logs/Claude/.

Adding Resources

Resources are read-only data sources that the AI can access. Unlike tools (which execute operations), resources provide context. For example, a configuration file, a database schema, or documentation that the AI should reference:

@mcp.resource("config://app-settings") def get_app_settings() -> str: """Return current application settings.""" return json.dumps({ "max_file_size": "10MB", "supported_formats": ["txt", "md", "csv", "json"], "rate_limit": 100 }, indent=2) @mcp.resource("schema://database") def get_db_schema() -> str: """Return the database schema for reference.""" return """ Table: users (id INT PK, email TEXT, created_at TIMESTAMP) Table: documents (id INT PK, user_id INT FK, title TEXT, content TEXT) Table: analysis_results (id INT PK, document_id INT FK, score FLOAT) """

Resources are listed when the client connects and can be read at any time during the conversation. They are ideal for providing context that helps the AI make better tool calls — for instance, showing the database schema so the AI writes correct SQL queries.

Building a Practical Server: File Analyzer

Here is a more realistic example — an MCP server that analyzes files for AI-generated content markers. This demonstrates async operations, error handling, and returning structured data:

import os import json from pathlib import Path from mcp.server.fastmcp import FastMCP mcp = FastMCP("file-analyzer") @mcp.tool() async def analyze_file(filepath: str) -> str: """Analyze a text file for basic content statistics. Args: filepath: Path to the text file to analyze """ path = Path(filepath) if not path.exists(): return f"Error: File not found at {filepath}" if not path.is_file(): return f"Error: {filepath} is not a file" text = path.read_text(encoding="utf-8") words = text.split() sentences = [s.strip() for s in text.replace("!", ".").replace("?", ".").split(".") if s.strip()] avg_word_length = sum(len(w) for w in words) / len(words) if words else 0 avg_sentence_length = len(words) / len(sentences) if sentences else 0 # Vocabulary richness (type-token ratio) unique_words = set(w.lower().strip(".,!?;:") for w in words) ttr = len(unique_words) / len(words) if words else 0 return json.dumps({ "file": str(path), "size_bytes": path.stat().st_size, "word_count": len(words), "sentence_count": len(sentences), "avg_word_length": round(avg_word_length, 2), "avg_sentence_length": round(avg_sentence_length, 2), "type_token_ratio": round(ttr, 4), "unique_words": len(unique_words) }, indent=2) @mcp.tool() async def list_text_files(directory: str) -> str: """List all text files in a directory. Args: directory: Path to the directory to scan """ path = Path(directory) if not path.is_dir(): return f"Error: {directory} is not a valid directory" files = [] for f in sorted(path.glob("*.txt")): stat = f.stat() files.append({"name": f.name, "size": stat.st_size, "modified": stat.st_mtime}) return json.dumps(files, indent=2, default=str) if __name__ == "__main__": mcp.run()

Error Handling Best Practices

MCP tools should never raise unhandled exceptions — the protocol does not define how clients should display raw tracebacks. Instead, return error information as structured text that the AI can interpret and communicate to the user. Validate inputs early, catch expected exceptions, and return clear error messages. If a tool depends on an external service, handle connection timeouts and authentication failures gracefully.

For tools that perform potentially dangerous operations (writing files, executing queries, sending messages), include confirmation mechanisms. You can require a confirm: bool parameter, return a preview before executing, or use the MCP prompts capability to guide the user through a multi-step workflow.

Testing Your Server

The MCP SDK includes a testing client you can use to verify your server without Claude Desktop:

# test_server.py import asyncio from mcp import ClientSession, StdioServerParameters from mcp.client.stdio import stdio_client async def test(): server_params = StdioServerParameters( command="python", args=["server.py"] ) async with stdio_client(server_params) as (read, write): async with ClientSession(read, write) as session: await session.initialize() # List available tools tools = await session.list_tools() print(f"Available tools: {[t.name for t in tools.tools]}") # Call a tool result = await session.call_tool("count_words", {"text": "Hello world test"}) print(f"Result: {result.content[0].text}") asyncio.run(test())

Run this with python test_server.py to verify your tools work correctly before connecting them to an AI client. This is especially useful for CI/CD pipelines where you want automated testing of your MCP server.

Deploying to Production

For local use, running the server as a stdio process through Claude Desktop is sufficient. For team or production use, deploy as an SSE or streamable HTTP server. The FastMCP framework supports this natively:

# Run as HTTP server instead of stdio if __name__ == "__main__": mcp.run(transport="sse", host="0.0.0.0", port=8080)

For production deployments, add authentication (API keys or OAuth), rate limiting, logging, and monitoring. The MCP specification includes an authentication framework for remote servers. Container-based deployment (Docker) works well since MCP servers are typically lightweight single-process applications. Many teams deploy MCP servers alongside their existing API infrastructure on platforms like Railway, Fly.io, or AWS ECS.

The complete source code for dozens of reference MCP servers is available on GitHub under the modelcontextprotocol organization. Study the official filesystem, Git, and SQLite servers for production-quality patterns. Once your server is built, you can connect it to Claude Code for terminal-based workflows, or to Claude Desktop for a visual interface. For inspiration, explore our guides to production MCP servers for Notion, Supabase, and Figma.

Last updated: 2026 • Browse all courses