Building Your First MCP Server with FastMCP — A Complete Walkthrough
Clawpedia · For Humans
By 2026, the novelty of basic chatbots has worn off. The industry has moved on to building agents that perform complex, multi-step tasks in the real world. This is where most projects stumble. Chaining together a few API calls is easy; buil
Building Your First MCP Server with FastMCP — A Complete Walkthrough
By 2026, the novelty of basic chatbots has worn off. The industry has moved on to building agents that perform complex, multi-step tasks in the real world. This is where most projects stumble. Chaining together a few API calls is easy; building a robust, stateful, and secure system for an AI to operate is hard. The friction isn't in the model's intelligence, but in the messy, ad-hoc plumbing between the AI's "brain" and its "hands."
This tutorial introduces the Model Control Plane (MCP) architecture and its leading implementation, FastMCP. We will cut through the noise and build a production-ready MCP server from the ground up. You will learn how to define stateful tools, handle authentication, deploy your server, and connect it to modern agent runtimes like Claude and Cursor. By the end, you will have a solid mental model and a practical toolkit for building serious AI agents.
What an MCP Server Actually Is
An MCP server is a specialized backend that exposes a set of capabilities to an AI model over a standardized protocol. It is not an agent itself, nor is it an LLM orchestration library like LangChain. Think of it as FastAPI or Express.js, but designed specifically for an AI model to be the client. Its purpose is to provide a clean, secure, and observable interface for tools that the AI can use to interact with external systems.
FastMCP is a Python framework for building these servers. Its core innovation, which sets it apart from simple function-calling of the 2023 era, is its formal distinction between stateless tools and stateful resources. This allows agents to perform complex workflows, like manipulating a file over several turns or working within a persistent database transaction, without re-authenticating or re-establishing context on every single step.
In simple terms: Imagine you hire a brilliant but amnesiac intern (the AI model). A simple function-calling API is like giving them a new, detailed instruction slip for every single task, forcing them to start from scratch each time. An MCP server, with its concept of resources, is like giving them a securely logged-in laptop (the
resource) with a suite of approved applications (thetools) they can use repeatedly to complete a complex project.
This architecture decouples the agent's reasoning loop from the tool's implementation details. The model simply knows it has access to a JiraConnector resource with a create_ticket tool; it doesn't need to know about the underlying REST API, the authentication headers, or the retry logic. That's the MCP server's job.
Core Concepts: Tools and Resources
Understanding the difference between a tool and a resource is fundamental to using FastMCP effectively.
Tools
A tool is a stateless function. It takes a set of arguments and returns a result. It's a single, atomic action. In FastMCP, you define a tool using a simple decorator.
# A simple, stateless tool
from fastmcp import tool
@tool
def get_stock_price(symbol: str) -> dict:
"""Gets the latest stock price for a given symbol."""
# In a real implementation, this would call an external API.
if symbol.upper() == "CLAW":
return {"symbol": "CLAW", "price": 125.50}
return {"symbol": symbol, "price": "Not found"}
The model can call this tool, get a result, and the interaction is complete. There is no memory or state carried over to the next call.
Resources
A resource is a stateful object that provides one or more tools. It's a class decorated with @resource. This is where the power of MCP lies. A resource can hold a database connection, an SDK client, or user session data. Tools defined within a resource's class automatically have access to this state via self.
This allows for workflows like:
- Model:
Acquire a 'CodeExecutor' resource. - MCP Server:
Okay, I've created a sandboxed shell environment. Here is its handle: 'executor-123'. - Model:
On 'executor-123', run the 'install_dependencies' tool with 'requirements.txt'. - MCP Server:
(executes pip install...) Done. - Model:
On 'executor-123', run the 'execute_script' tool with 'main.py'.
Here, the CodeExecutor is the resource, maintaining the state of the shell environment across multiple tool calls.
Setting Up Your First FastMCP Project
Let's build a practical server that exposes a JiraConnector resource for creating tickets.
Prerequisites
- Python 3.12 or newer.
- An environment manager like
venv.
Installation
First, set up your project directory and virtual environment.
mkdir mcp-jira-service
cd mcp-jira-service
python3.12 -m venv .venv
source .venv/bin/activate
pip install "fastmcp[server]==2.3.1" "python-dotenv==1.0.1" "requests==2.32.3"
We install fastmcp with the [server] extra, which pulls in uvicorn for running the ASGI server.
Your project structure should look like this:
mcp-jira-service/
├── .venv/
├── resources/
│ ├── __init__.py
│ └── jira.py
├── .env
└── main.py
Defining the Resource
Create a .env file to hold your secrets. Never commit this file to version control.
JIRA_URL="https://your-company.atlassian.net"
JIRA_USER="your-email@example.com"
JIRA_API_TOKEN="your_atlassian_api_token"
MCP_SECRET_KEY="mcp_sk_abc123xyz789" # Generate a secure random key
Now, let's define our JiraConnector in resources/jira.py. This class will initialize a requests session with the correct authentication headers and provide a tool for creating a ticket.
# resources/jira.py
import os
import requests
from fastmcp import resource, tool
from pydantic import BaseModel, Field
# Define the input structure for our tool using Pydantic
class CreateTicketInput(BaseModel):
project_key: str = Field(..., description="The Jira project key, e.g., 'PROJ'.")
summary: str = Field(..., description="The title or summary of the ticket.")
description: str = Field(..., description="The detailed description for the ticket body.")
issue_type: str = Field(default="Task", description="The type of issue, e.g., 'Task', 'Bug', 'Story'.")
@resource
class JiraConnector:
"""Manages a connection to a Jira instance and provides tools for interaction."""
def __init__(self):
"""Initializes the Jira connector, setting up the authenticated session."""
self.base_url = os.environ["JIRA_URL"]
api_token = os.environ["JIRA_API_TOKEN"]
user = os.environ["JIRA_USER"]
# This session object holds the state (auth headers) for the resource
self.session = requests.Session()
self.session.auth = (user, api_token)
self.session.headers.update({"Accept": "application/json", "Content-Type": "application/json"})
print("JiraConnector resource initialized.")
@tool(args_schema=CreateTicketInput)
def create_ticket(self, project_key: str, summary: str, description: str, issue_type: str) -> dict:
"""Creates a new ticket in a Jira project."""
api_url = f"{self.base_url}/rest/api/3/issue"
payload = {
"fields": {
"project": {"key": project_key},
"summary": summary,
"description": {
"type": "doc",
"version": 1,
"content": [{"type": "paragraph", "content": [{"type": "text", "text": description}]}]
},
"issuetype": {"name": issue_type}
}
}
response = self.session.post(api_url, json=payload)
response.raise_for_status() # Will raise an HTTPError for bad responses
data = response.json()
return {
"status": "success",
"ticket_id": data["key"],
"ticket_url": f"{self.base_url}/browse/{data['key']}"
}
Assembling the Server
Finally, tie it all together in main.py. This file will load the environment variables, instantiate FastMCP, and register our new resource.
# main.py
import os
from dotenv import load_dotenv
from fastmcp import FastMCP
from fastmcp.auth import APIKeyAuth
# Import the resource we defined
from resources.jira import JiraConnector
# Load environment variables from .env file
load_dotenv()
# Set up authentication. The agent client must provide this key.
auth = APIKeyAuth(secret_key=os.environ["MCP_SECRET_KEY"])
# Instantiate the FastMCP application
app = FastMCP(
auth_strategy=auth,
title="Clawpedia Jira Service",
description="An MCP server for interacting with Jira."
)
# Register the resource with the application
# The server will automatically create one instance of this class
# and use it for all incoming requests related to it.
app.register(JiraConnector)
# To run the server locally:
# uvicorn main:app --reload --port 8080
Start your server locally to test it:
uvicorn main:app --reload --port 8080
You can now visit http://127.0.0.1:8080/docs in your browser to see the auto-generated API documentation for your MCP server.
The Streamable HTTP Transport
A key feature of the MCP v2 protocol is its streamable HTTP transport. When an agent calls a tool, especially a long-running one, the server doesn't wait for the entire process to finish before responding. Instead, it immediately returns a 200 OK with Transfer-Encoding: chunked and begins streaming a series of JSON objects, each on a new line.
This allows the client (and the end-user) to see progress in real-time. For our Jira tool, the stream might look like this:
HTTP/1.1 200 OK
Content-Type: application/x-json-stream
Transfer-Encoding: chunked
{"type": "status", "stage": "validation", "message": "Input for 'create_ticket' validated."}
{"type": "status", "stage": "execution", "message": "Sending request to https://your-company.atlassian.net..."}
{"type": "result", "status": "success", "data": {"ticket_id": "PROJ-451", "ticket_url": "https://your-company.atlassian.net/browse/PROJ-451"}}
FastMCP handles the creation of this stream automatically. You can add custom status updates within your tool code using yield statements, which gives you fine-grained control over the feedback provided to the user during a tool's execution. Modern agent runtimes and IDEs are built to parse these streams and update their UI accordingly, dramatically improving the user experience over a simple loading spinner.
Deploying Your MCP Server
An MCP server is a standard ASGI web application, so you can deploy it anywhere you'd run a Python backend. Fly.io is an excellent choice due to its balance of simplicity, performance, and cost-effectiveness. A small project can often run entirely on their free tier, with paid plans starting around $5/month for a persistent VM.
Here's how to deploy our Jira service to Fly.io.
- Install the Fly CLI: Follow the instructions at fly.io/docs/hands-on/install-flyctl/.
- Create a
requirements.txtfile:
pip freeze > requirements.txt
- Launch the app on Fly:
fly launch
The CLI will ask you a few questions. Give your app a unique name (e.g., mcp-jira-clawpedia), choose a region, and tell it not to set up a Postgres database. It will generate a fly.toml file.
- Configure
fly.toml: The generated file is a good starting point. You need to make one critical change: update the[http_service.processes]section to useuvicorn.
```toml
# fly.toml
app = 'mcp-jira-clawpedia'
primary_region = 'sea'
[build]
builder = "paketobuildpacks/builder:base"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"] # This must match the process name below
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 256
[processes]
# Tell fly how to run the app. Use the same command as local dev.
app = "uvicorn main:app --host 0.0.0.0 --port 8080"
```
- Set Secrets: Your
.envfile is not deployed. You must set secrets on Fly directly.
```bash
fly secrets set JIRA_URL="https://your-company.atlassian.net"
fly secrets set JIRA_USER="your-email@example.com"
fly secrets set JIRA_API_TOKEN="your_atlassian_api_token"
fly secrets set MCP_SECRET_KEY="mcp_sk_abc123xyz789"
```
- Deploy:
fly deploy
Your MCP server is now live at https://mcp-jira-clawpedia.fly.dev.
Connecting from a Client
With your server deployed, you can now point an AI agent at it. The agent's runtime will use the server's discovery endpoint (/mcp/discover) to learn about available resources and tools.
Claude 5 (via API)
In the 2026 version of Anthropic's API, you can specify MCP endpoints directly in your prompt or API call configuration. The model's internal tool-use mechanics will query your server.
<tools>
<tool_description>
This tool is a Jira connector for creating and managing tickets.
</tool_description>
<mcp_endpoint auth_scheme="bearer" token="mcp_sk_...">https://mcp-jira-clawpedia.fly.dev</mcp_endpoint>
</tools>
When the user asks, "Create a ticket in the PROJ project about a login bug," the Claude model will see the MCP tool specification, understand it has a JiraConnector.create_ticket tool, and formulate the correct call to your server.
Cursor IDE
Cursor, the AI-native code editor, has first-class support for MCP servers. In your settings.json file, add your endpoint:
{
"cursor.agent.mcpEndpoints": [
{
"url": "https://mcp-jira-clawpedia.fly.dev",
"token": "mcp_sk_abc123xyz789"
}
]
}
After reloading, you can simply type @jira create a ticket... in the Cursor chat, and it will discover and use the tool from your deployed service.
Windsurf (Python Client)
For programmatic use, open-source agent frameworks like Windsurf often have client libraries. The mcp-client library provides a clean Pythonic interface.
import mcp_client
# Connect to the server, providing auth
client = mcp_client.connect(
"https://mcp-jira-clawpedia.fly.dev",
token="mcp_sk_abc123xyz789"
)
# The client dynamically creates methods based on the server's resources/tools
try:
# This looks like a local function call, but it's making a streaming HTTP
# request to your deployed server.
result = client.JiraConnector.create_ticket(
project_key="PROJ",
summary="Refactor authentication service",
description="The current service is slow and needs to be updated."
)
print(f"Successfully created ticket: {result['ticket_id']}")
except mcp_client.ToolError as e:
print(f"Error calling tool: {e}")
When to Use It (and When Not To)
FastMCP provides a clear architecture for building agentic capabilities, but it isn't the right solution for everything.
Use FastMCP when:
- You need to provide stateful tools to an agent (e.g., file sessions, database transactions, ongoing web scraping).
- You are building a "Product-as-a-Tool" and want to expose your service's capabilities to a wide range of AI models and agents in a standardized way.
- Tool execution is long-running, and you need to provide real-time streamable feedback to the user.
- Security, observability, and clear separation of concerns are critical for your application.
Don't use FastMCP when:
- Your "tool" is just one or two simple, stateless functions. The built-in function-calling of models like GPT and Claude is sufficient and simpler.
- Your task is a one-off script, not a long-running service. The overhead of setting up a server isn't justified.
- The capability you need is purely data retrieval that could be better served by a RAG pipeline and a vector database.
Bottom Line
The Model Control Plane architecture, implemented via FastMCP, imposes crucial engineering discipline on the otherwise chaotic field of AI agent development. It standardizes the interface between an AI's reasoning capabilities and the tools it uses to act, separating the "what" from the "how".
While it demands more upfront setup than simple function-calling scripts, this structure is the foundation for building agents that are robust, secure, and stateful. For anyone building a serious agentic product in 2026, mastering the MCP pattern is not just a good idea—it is a necessity.
Related Articles
- Step-by-Step Guide: Setting Up OpenClaw on a VOS Server — Complete walkthrough for deploying OpenClaw on a VOS server, from prerequisites to running your first agent.
- Building a Network of OpenClaw Agents: Orchestration — Design and implement multi-agent orchestration systems with OpenClaw for complex distributed tasks.
- Microsoft AutoGen — Multi-Agent Conversation Patterns Done Right — By 2026, the novelty of single-agent workflows has worn off. We've mastered chaining LLMs and building basic RAG pipelines. The frontier has moved to coordination. Getting multiple specialized AI agents to collaborate effectively on a compl
- Claude Agent SDK — Building Autonomous Agents on Anthropic's Runtime — A plain-language guide to Anthropic's Claude Agent SDK, the toolkit for building tool-using, multi-step AI agents.
- LangGraph — Building Stateful AI Agents the Right Way in 2026 — By early 2025, the initial wave of AI agent development had hit a wall. Simple linear chains and basic loops, while great for prototypes, proved brittle and opaque in production. We learned the hard way that chaining LLM calls is easy, but