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 (the tools) 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:

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

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.

pip freeze > requirements.txt

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.

```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"

```

```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"

```

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:

Don't use FastMCP when:

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