June 20, 2025
Building Your First FastMCP Server: A Complete Guide
Creating AI integrations used to mean wrestling with complex protocols, managing boilerplate code, and dealing with transport layers. FastMCP changes all that. It’s designed to be high-level and Pythonic. In most cases, decorating a function is all you need. This guide walks you through building a production-ready MCP server that any AI system can connect to—whether it’s Claude, GPT-4, or any other MCP-compatible client.
FastMCP has become the standard framework for working with the Model Context Protocol. FastMCP 1.0 was incorporated into the official MCP Python SDK in 2024. This is FastMCP 2.0, the actively maintained version that provides a complete toolkit for working with the MCP ecosystem. In this article, we’ll build a customer service MCP server that demonstrates all the core concepts you need to master.
What is FastMCP?
The Model Context Protocol lets you build servers that expose data and functions to LLM applications in a secure, standardized way. It is often described as “the USB-C port for AI”, providing a uniform way to connect LLMs to resources they can use. Just as USB-C created a standard interface that works across diverse hardware, MCP establishes a standardized protocol for AI models to connect with external data sources and functionality.
This comparison to USB-C for AI works on several levels:
- Works with any AI model: Like USB-C connecting different devices, MCP integrates with GPT, Claude, and Gemini without custom adapters.
- Instant connection: Just as USB-C devices work immediately when plugged in, AI models can access MCP resources without complex setup.
- Two-way communication: Like USB-C sending/receiving data, MCP enables models to both use information and trigger actions.
- Simplified development: Developers can use MCP without needing to understand all technical details of the protocol.
While the protocol itself is powerful, implementing it correctly involves significant complexity.
FastMCP provides a high-level, Pythonic interface for building, managing, and interacting with these servers. The MCP protocol is powerful but implementing it involves a lot of boilerplate - server setup, protocol handlers, content types, error management. FastMCP abstracts away these complexities, letting you focus on what matters: your business logic.
Think of FastMCP as the FastAPI of the MCP world—it transforms complex protocol implementation into simple Python decorators. Just as FastAPI revolutionized web API development, FastMCP makes MCP server development accessible to any Python developer.
Getting Started with FastMCP
Before diving into code, let’s understand what we’re building. Our customer service MCP server will demonstrate the three core capabilities that MCP servers can provide:
- Resources: Data that can be loaded into the LLM’s context (like customer information)
- Tools: Functions the LLM can execute (like creating support tickets)
- Prompts: Reusable templates for consistent LLM interactions
For detailed tutorials and documentation, check out the official FastMCP resources:
- FastMCP Getting Started Guide
- FastMCP Tutorials
- Or our comprehensive MCP guide that covers how to build a FastMCP server and how to connect to our MCP Server using Open AI, LiteLLM, LangChain, DSPy and Anthropic.
Now, let’s build our server step by step.
Project Structure
Our MCP server lives in a single file for simplicity (main.py):
mcp_article1/
├── src/
│ ├── main.py # Our FastMCP server
│ └── ... # Client integrations
├── pyproject.toml # Project dependencies
└── server_config.json # Client configuration
The src/main.py
file contains our entire server implementation. In production, you might split this into multiple modules, but for learning purposes, keeping everything in one file helps you see the complete picture.
Building the FastMCP Server
Let’s walk through our FastMCP server implementation, breaking it down into digestible chunks.
Initial Setup and Imports
File: src/main.py
(lines 1-15)
import asyncio
import logging
from datetime import datetime
from typing import List, Optional
from fastmcp import FastMCP
from pydantic import BaseModel, field_validator
from pydantic_core import PydanticCustomError
# Configure logging for better debugging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
# Initialize FastMCP server
mcp = FastMCP("Customer Service Assistant")
This opening section sets up our development environment. We import the key libraries:
- asyncio: Enables asynchronous operations, crucial for handling multiple concurrent requests
- logging: Provides visibility into server operations during development and debugging
- datetime: Used for timestamps and time-based operations
- typing: Enables type hints for better code clarity and IDE support
- FastMCP: The main framework class that powers our server
- pydantic: Provides data validation and serialization, ensuring our data models are correct
The line mcp = FastMCP("Customer Service Assistant")
creates our server instance. The name you provide here identifies your server to clients, making it easy to distinguish when multiple MCP servers are available.
Understanding Pydantic Models
Before we continue, let’s understand why we use Pydantic. In MCP servers, data validation is critical—you’re accepting input from AI models that might send unexpected data. Pydantic ensures that:
- Data conforms to expected types and formats
- Invalid data is rejected with clear error messages
- Data is automatically serialized to JSON for transport
Think of Pydantic models as contracts between your server and its clients. They define exactly what data is expected and what will be returned.
Defining Data Models
File: src/main.py
(lines 18-36)
# Data models for type safety and validation
class Customer(BaseModel):
id: str
name: str
email: str
phone: Optional[str] = None
account_status: str = "active"
last_interaction: Optional[datetime] = None
@field_validator("email", mode="after") # noqa
@classmethod
def email_must_be_valid(cls, v: str) -> str:
if "@" not in v:
raise PydanticCustomError(
"invalid_email",
"Invalid email format: {email} must contain @",
{"email": v},
)
return v
This Customer
model represents our core business entity. Let’s break down its components:
- Required fields:
id
,name
, andemail
must always be provided - Optional fields:
phone
andlast_interaction
can beNone
- Default values:
account_status
defaults to “active” if not specified
The @field_validator
decorator introduces custom validation logic. Here, we ensure email addresses contain an “@” symbol. The mode="after"
parameter means this validation runs after Pydantic’s built-in type checking.
The validator pattern demonstrates defensive programming—we don’t assume the AI will always send valid data. By validating at the model level, we catch errors early and provide helpful feedback.
Creating the Ticket Request Model
File: src/main.py
(lines 39-54)
class TicketRequest(BaseModel):
customer_id: str
subject: str
description: str
priority: str = "normal"
@field_validator("priority", mode="after") # noqa
@classmethod
def priority_must_be_valid(cls, v: str) -> str:
valid_priorities = ["low", "normal", "high", "urgent"]
if v not in valid_priorities:
raise PydanticCustomError(
"invalid_priority",
"Priority must be one of: {valid_priorities}, got {priority}",
{"valid_priorities": ", ".join(valid_priorities), "priority": v},
)
return v
The TicketRequest
model shows how to handle enumerated values. Instead of using Python’s Enum
class (which can complicate JSON serialization), we use a string field with validation. This approach provides:
- Clear error messages when invalid values are provided
- Flexibility to add new priority levels without changing the model structure
- Simple JSON representation that any client can understand
The custom error includes both the valid options and the invalid value received, making debugging much easier for client developers.
Setting Up Mock Data
File: src/main.py
(lines 57-73)
# Simulated customer database
CUSTOMERS_DB = {
"12345": Customer(
id="12345",
name="Alice Johnson",
email="alice@example.com",
phone="+1-555-0123",
account_status="active",
last_interaction=datetime.now(),
),
"67890": Customer(
id="67890",
name="Bob Smith",
email="bob@example.com",
account_status="suspended",
),
}
In a production system, this would connect to a real database. For our example, we use an in-memory dictionary. This mock data serves several purposes:
- Provides realistic test cases for development
- Demonstrates different customer states (active vs. suspended)
- Shows optional field usage (Bob has no phone number)
- Enables immediate testing without database setup
Creating MCP Resources
Resources in MCP are like GET endpoints in a REST API—they provide data that LLMs can load into their context. Let’s create our first resource:
File: src/main.py get_customer_info
# MCP Resource: Customer Data Access
@mcp.resource("customer://{customer_id}")
async def get_customer_info(customer_id: str) -> Customer:
"""Retrieve customer information by ID."""
logger.info(f"Retrieving customer info for ID: {customer_id}")
if customer_id not in CUSTOMERS_DB:
raise ValueError(f"Customer {customer_id} not found")
# Simulate database delay
await asyncio.sleep(0.1)
return CUSTOMERS_DB[customer_id]
The @mcp.resource
decorator transforms a regular Python function into an MCP resource. Key points:
- URL Pattern: The
"customer://{customer_id}"
pattern creates parameterized URLs. When a client requestscustomer://12345
, the server extracts “12345” as thecustomer_id
parameter. - Async Function: Using
async def
allows the server to handle multiple requests concurrently. While one request waits for a database query, others can proceed. - Type Annotations: The return type
Customer
tells FastMCP exactly what data structure to expect, enabling automatic validation and serialization. - Error Handling: Raising a
ValueError
for missing customers creates a clear error response that clients can handle appropriately.
The asyncio.sleep(0.1)
simulates database latency, making our mock server behave more like a real system.
In the world of MCP, resources are nouns. They are entities that your agentic code can read to use for its response, but not modify. On the other hand, MCP Tools are verbs, they allow your agentic code to perform an action.
Building MCP Tools
Tools are the action-oriented counterpart to resources—they allow LLMs to perform operations that change state or trigger side effects.
Creating Support Tickets
File: src/main.py
(lines 104-127)
# MCP Tool: Create Support Ticket
@mcp.tool()
async def create_support_ticket(request: TicketRequest) -> dict:
"""Create a new customer support ticket."""
logger.info(f"Creating ticket for customer {request.customer_id}")
# Validate customer exists
if request.customer_id not in CUSTOMERS_DB:
raise ValueError(f"Customer {request.customer_id} not found")
# Simulate ticket creation
ticket_id = f"TICKET-{datetime.now().strftime('%Y%m%d%H%M%S')}"
ticket = {
"ticket_id": ticket_id,
"customer_id": request.customer_id,
"subject": request.subject,
"description": request.description,
"priority": request.priority,
"status": "open",
"created_at": datetime.now().isoformat(),
}
return ticket
This tool showcases how to handle complex input:
- Pydantic Model Input: Using
request: TicketRequest
automatically validates all fields before the function executes. Invalid requests never reach your business logic. - Business Logic Validation: We verify the customer exists before creating a ticket, maintaining referential integrity.
- ID Generation: The timestamp-based ID ensures uniqueness for our simple example. Production systems might use UUIDs or database sequences.
- ISO Format Dates: Using
.isoformat()
creates standardized date strings that any client can parse.
Implementing Business Analytics
File: src/main.py
calculate account value
# MCP Tool: Calculate Account Value
@mcp.tool()
async def calculate_account_value(
customer_id: str, purchase_history: List[float]
) -> dict:
"""Calculate total account value and average purchase."""
logger.info(f"Calculating account value for {customer_id}")
if not purchase_history:
return {
"customer_id": customer_id,
"total_value": 0.0,
"average_purchase": 0.0,
"purchase_count": 0,
}
total = sum(purchase_history)
average = total / len(purchase_history)
return {
"customer_id": customer_id,
"total_value": round(total, 2),
"average_purchase": round(average, 2),
"purchase_count": len(purchase_history),
}
This analytics tool demonstrates:
- Multiple Parameters: Tools can accept multiple inputs of different types. Here we take both a customer ID and a list of purchases.
- Type Safety:
List[float]
ensures the purchase history contains numeric values, preventing type errors during calculation. - Edge Case Handling: Empty purchase lists are handled gracefully, returning zeros instead of causing division errors.
- Precision Control:
round(total, 2)
ensures monetary values have exactly two decimal places, avoiding floating-point precision issues.
Creating Reusable Prompts
Prompts in MCP are templates that help ensure consistent, high-quality LLM responses:
File: src/main.py
prompt
# MCP Prompt: Customer Service Response Template
@mcp.prompt("customer_service_response")
async def generate_service_response_prompt(
customer_name: str, issue_type: str, resolution_steps: List[str]
) -> str:
"""Generate a professional customer service response."""
steps_text = "\n".join(
[f"{i+1}. {step}" for i, step in enumerate(resolution_steps)]
)
return f"""
You are a professional customer service representative.
Generate a helpful and empathetic response for the customer.
Customer: {customer_name}
Issue Type: {issue_type}
Resolution Steps:
{steps_text}
Guidelines:
- Be professional but warm
- Acknowledge the customer's concern
- Provide clear, actionable steps
- End with an offer for further assistance
- Keep the tone positive and solution-focused
Generate a complete customer service response
following these guidelines.
"""
Prompts serve as guardrails for AI behavior:
- Structured Input: The function parameters define what information is needed to generate a good response.
- Formatting Logic: Converting the list of steps into numbered items happens in your code, not in the prompt, ensuring consistency.
- Clear Guidelines: Specific instructions help the LLM generate appropriate responses for your brand voice.
- Reusability: Once defined, this prompt can be used across different client applications, ensuring consistent customer communications.
We mentioned that resources are nouns, and tools are verbs. You can think of prompts as instruction manuals that teach your agentic code how to use those verbs and nouns.
Server Entry Point
File: src/main.py
(lines 181-202)
def main():
"""Main entry point for the MCP server."""
print("🚀 Starting Customer Service MCP Server...")
print("📋 Available Resources:")
print(" - customer://{customer_id} - Get customer info")
print("🔧 Available Tools:")
print(" - get_recent_customers - Get recent customers")
print(" - create_support_ticket - Create support ticket")
print(" - calculate_account_value - Calculate account value")
print("📝 Available Prompts:")
print(" - customer_service_response - Generate responses")
print("\n✅ Server ready for connections!")
# Run the server
mcp.run()
if __name__ == "__main__":
main()
The entry point provides a user-friendly startup experience:
- Visual Feedback: Emojis and clear formatting make it easy to see what the server provides at a glance.
- Documentation: Listing available endpoints helps developers understand the server’s capabilities without reading code.
- Simple Execution: The
mcp.run()
call handles all protocol details, transport setup, and connection management.
Running Your FastMCP Server
To run your server, simply execute:
poetry run python src/main.py
The server will start and listen for connections via standard input/output (stdio), the default transport for local MCP servers.
Connecting Clients to Your Server
Now that you have a working MCP server, you can connect various AI clients to it. Our comprehensive MCP guide shows how to connect this server to:
- Claude Desktop (via configuration file)
- OpenAI (using both Agents SDK and native API)
- Anthropic’s Claude API
- LangChain workflows
- DSPy programs
- LiteLLM for multi-provider support
Each client integration in the guide demonstrates how the same MCP server works seamlessly across different AI platforms—truly achieving the “USB-C for AI” vision.
Advanced FastMCP Concepts
Error Handling
FastMCP automatically converts Python exceptions into appropriate MCP error responses:
@mcp.tool()
async def risky_operation(data: str) -> str:
if not data:
raise ValueError("Data cannot be empty")
try:
# Perform operation
result = process_data(data)
except ProcessingError as e:
# Custom exceptions become error messages
raise RuntimeError(f"Processing failed: {e}")
return result
Simply throw a standard Python exception, like we did above. FastMCP converts the exception into an MCP error response, which is sent back to the client.
FastMCP is the easy button for developing MCP servers.
Context Access
FastMCP provides a context object for advanced scenarios:
from fastmcp import Context
@mcp.tool()
async def advanced_tool(data: str, ctx: Context) -> str:
# Log using MCP's logging protocol
ctx.log(level="info", message=f"Processing: {data}")
# Report progress for long operations
ctx.report_progress(0.5, "Halfway complete")
# Access server capabilities
if "sampling" in ctx.server.capabilities:
# Use LLM sampling if available
response = await ctx.sample(
prompt="Analyze this data",
max_tokens=100
)
return "Processed"
Type Support
For tool input parameters, FastMCP basically supports most of the types which are also supported by pydantic. This includes, but is not limited, to all built-in types, dates, literals, enums, collections (dicts, lists), uuids, complex pydantic models and many more.
For outputs, the supported types include:
- Basic types:
str
,int
,float
,bool
- Collections:
list
,dict
- Pydantic models
None
for empty responses
Best Practices
1. Use Descriptive Names and Docstrings
Your tool and resource names become part of the API. Make them clear and self-documenting:
# Good
@mcp.tool()
async def search_customers_by_email(email: str) -> List[Customer]:
"""Search for customers using their email address.
Returns all customers whose email contains the search term.
Case-insensitive partial matching is used.
"""
# Avoid
@mcp.tool()
async def find(s: str) -> list:
"""Find stuff"""
Your agentic client of the MCP Server will use the method descriptions (which become part of the tool definition) to determine when and how to use the available tools. Both tool descriptions and prompts provide guidance to your agentic software on using these tools and resources effectively.
2. Validate Early and Clearly
Use Pydantic models and validators to catch errors before they reach your business logic:
class DateRange(BaseModel):
start_date: datetime
end_date: datetime
@model_validator(mode='after')
def end_after_start(self) -> 'DateRange':
if self.end_date <= self.start_date:
raise ValueError("End date must be after start date")
return self
3. Handle Async Properly
Always use async functions for I/O operations to maintain server responsiveness:
# Good - Non-blocking
@mcp.tool()
async def fetch_data(url: str) -> dict:
async with httpx.AsyncClient() as client:
response = await client.get(url)
return response.json()
# Avoid - Blocks the server
@mcp.tool()
def fetch_data_blocking(url: str) -> dict:
response = requests.get(url) # Blocks!
return response.json()
4. Design for AI Consumption
Remember that your tools will be used by AI models. Design interfaces that are:
- Self-explanatory: Names and descriptions should clearly indicate functionality
- Focused: Each tool should do one thing well
- Predictable: Consistent patterns across your API
- Forgiving: Handle edge cases gracefully
Deployment Considerations
When moving from development to production:
- Replace Mock Data: Connect to real databases and services
- Add Authentication: Implement proper security for sensitive operations
- Scale Horizontally: FastMCP servers can run multiple instances behind a load balancer
- Monitor Performance: Add metrics and monitoring for production visibility
- Version Your API: Plan for backwards compatibility as your server evolves
Conclusion
FastMCP transforms the complex Model Context Protocol into simple Python decorators, making it accessible to any Python developer. FastMCP lowers the barrier to entry for building powerful, context-aware LLM applications by simplifying the implementation of the Model Context Protocol.
By following this guide, you’ve learned:
- How to structure a FastMCP server
- Creating resources for data access
- Building tools for actions and computations
- Defining prompts for consistent AI behavior
- Best practices for production-ready servers
Your MCP server is now ready to connect to any MCP-compatible AI client. Whether you’re building internal tools, customer-facing applications, or experimental AI systems, FastMCP provides the foundation for secure, standardized AI integrations.
Next Steps
- Explore Client Integrations: Check our comprehensive guide to connect your server to various AI platforms
- Learn how to create enterprise security for your MCP Servers by reading Securing MCP: From Vulnerable to Fortified — Building Secure HTTP-based AI Integrations
- Dive Deeper: Visit the official FastMCP documentation for advanced features
- Join the Community: Engage with other MCP developers to share patterns and solutions
- Build Something Amazing: Use your new knowledge to create AI-powered applications that solve real problems
The Model Context Protocol, powered by FastMCP, represents the future of AI integration—standardized, secure, and surprisingly simple. Welcome to the ecosystem!
References
- GitHub Repository: MCP Article Examples - Complete source code
- Official FastMCP Documentation: gofastmcp.com - Comprehensive guides and API reference
- FastMCP Tutorials: MCP Tutorials - Step-by-step tutorials
- MCP Specification: Model Context Protocol - Protocol specification
- Comprehensive Client Guide: MCP: From Chaos to Harmony - Connecting various AI clients to MCP servers
About the Author
Rick Hightower brings extensive enterprise experience as a former executive and distinguished engineer at a Fortune 100 company, where he specialized in Machine Learning and AI solutions to deliver an intelligent customer experience. His expertise spans the theoretical foundations and practical applications of AI technologies.
As a TensorFlow-certified professional and graduate of Stanford University’s comprehensive Machine Learning Specialization, Rick combines academic rigor with real-world implementation experience. His training includes mastery of supervised learning techniques, neural networks, and advanced AI concepts, which he has successfully applied to enterprise-scale solutions.
With a deep understanding of AI implementation’s business and technical aspects, Rick bridges the gap between theoretical machine learning concepts and practical business applications, helping organizations use AI to create tangible value.
If you like this article, follow Rick on LinkedIn or Medium.
TweetApache Spark Training
Kafka Tutorial
Akka Consulting
Cassandra Training
AWS Cassandra Database Support
Kafka Support Pricing
Cassandra Database Support Pricing
Non-stop Cassandra
Watchdog
Advantages of using Cloudurable™
Cassandra Consulting
Cloudurable™| Guide to AWS Cassandra Deploy
Cloudurable™| AWS Cassandra Guidelines and Notes
Free guide to deploying Cassandra on AWS
Kafka Training
Kafka Consulting
DynamoDB Training
DynamoDB Consulting
Kinesis Training
Kinesis Consulting
Kafka Tutorial PDF
Kubernetes Security Training
Redis Consulting
Redis Training
ElasticSearch / ELK Consulting
ElasticSearch Training
InfluxDB/TICK Training TICK Consulting