FastMCP: Build Your Own MCP Server in Python in Minutes

# FastMCP: Membangun MCP Server Sendiri dengan Python dalam Hitungan Menit Halo temen-temen, kali ini kita bahas cara membangun MCP server sendiri pakai FastMCP. Kalau kalian sudah pernah memakai MCP...

By Ruby Abdullah · · tutorial
fastmcpmcpmodel-context-protocolpythonai-tools

FastMCP: Build Your Own MCP Server in Python in Minutes

Hey everyone, today we build our own MCP server using FastMCP. If you have already used someone else's MCP server in Claude Desktop or your editor and now wonder how to build one so AI can reach your internal database, your company API, or your team's custom tooling, this tutorial is for you.

In short, MCP (Model Context Protocol) is a standard protocol that lets AI applications connect to external data and tools. People often describe it as USB-C for AI: one standard connector, many devices. Before MCP, every integration had to be built specifically for each application. With MCP you write the server once and it works with Claude Desktop, Claude Code, and any other application supporting the protocol.

FastMCP is a Python framework that makes building MCP servers very simple. If you have used FastAPI, this will feel immediately familiar, because the style is the same: decorators on ordinary Python functions.

Introduction

The Three MCP Primitives

Before coding, understand the three things an MCP server can provide.

Tools are functions the model can call to do something. This is the most used primitive. Examples: search a database, send an email, create a ticket, compute something. Think of them as POST endpoints in a REST API, since they can have side effects.

Resources are data the model can read. Examples: file contents, prepared query results, configuration. Think of them as GET endpoints, ideally side effect free. Resources are usually selected by the user or application rather than chosen by the model.

Prompts are conversation templates a user can invoke. Examples: a code review template or a report analysis template. These surface as ready-made commands in the client application.

For most practical use cases you will mostly write Tools. So this tutorial focuses there, though I will show the other two as well.

Transport: STDIO or HTTP

There are two ways an MCP server talks to a client.

STDIO means the client launches your server as a child process and communicates over standard input and output. This is what Claude Desktop uses for local servers. Simple, safe, no network.

HTTP means your server runs as a web service and clients connect over the network. That is what you use when the server is deployed centrally and shared.

Rule of thumb: use STDIO for personal tools or tools touching local files, use HTTP for tools shared by a team.

Installation

python -m venv venv

source venv/bin/activate # Linux / Mac

venv\Scripts\activate # Windows

pip install fastmcp

Check the version:

fastmcp version

Your First Server

Let us start with the simplest thing that works.

# server.py

from fastmcp import FastMCP

mcp = FastMCP("Team Utilities")

@mcp.tool

def add(a: float, b: float) -> float:

"""Add two numbers together."""

return a + b

@mcp.tool

def countbusinessdays(startdate: str, enddate: str) -> int:

"""Count business days (Monday through Friday) between two dates.

Args:

startdate: format YYYY-MM-DD

enddate: format YYYY-MM-DD

"""

from datetime import date, timedelta

start = date.fromisoformat(startdate)

end = date.fromisoformat(enddate)

count = 0

cursor = start

while cursor <= end:

if cursor.weekday() < 5:

count += 1

cursor += timedelta(days=1)

return count

if name == "main":

mcp.run()

Done. That is a fully functional MCP server.

Two details in that code deserve close attention, because they determine whether your tools get used correctly.

First, type hints. FastMCP uses Python type annotations to generate the JSON schema sent to the model. Writing a: float tells the model that parameter must be a number. Without a type, the model has to guess, and it often guesses wrong.

Second, docstrings. The docstring is the description the model reads to decide when to call your tool. Write it clearly and specifically. "Add two numbers together" is far more useful than "add function". And state expected formats as shown above, because the model cannot guess which date format you want.

Running and Testing

To test before connecting to any application:

fastmcp dev server.py

That opens the MCP Inspector in your browser, where you can list tools, call them manually, and inspect responses. It is the fastest debugging loop, and I recommend always testing there before wiring into Claude.

To connect to Claude Desktop:

fastmcp install claude-desktop server.py

Or add it manually to the Claude Desktop config file:

{

"mcpServers": {

"team-utilities": {

"command": "python",

"args": ["/full/path/to/server.py"]

}

}

}

Restart Claude Desktop and your tools appear.

A More Realistic Tool: Database Access

The addition example is too simple. Here is something closer to real work.

import sqlite3

from typing import Literal

from fastmcp import FastMCP

mcp = FastMCP("Sales Database")

DB = "sales.db"

def connect():

conn = sqlite3.connect(DB)

conn.rowfactory = sqlite3.Row

return conn

@mcp.tool

def searchcustomers(keyword: str, limit: int = 10) -> list[dict]:

"""Search customers by name or email.

Args:

keyword: text searched in the name and email columns

limit: maximum number of results, default 10

"""

with connect() as conn:

rows = conn.execute(

"""

SELECT id, name, email, city, lifetimevalue

FROM customers

WHERE name LIKE ? OR email LIKE ?

ORDER BY lifetimevalue DESC

LIMIT ?

""",

(f"%{keyword}%", f"%{keyword}%", limit),

).fetchall()

return [dict(r) for r in rows]

@mcp.tool

def salessummary(

period: Literal["daily", "weekly", "monthly"] = "monthly",

periods: int = 6,

) -> list[dict]:

"""Get aggregated sales totals per period.

Args:

period: aggregation granularity

periods: how many recent periods to return

"""

timeformat = {

"daily": "%Y-%m-%d",

"weekly": "%Y-%W",

"monthly": "%Y-%m",

}[period]

with connect() as conn:

rows = conn.execute(

f"""

SELECT strftime('{timeformat}', date) AS period,

COUNT() AS transactioncount,

SUM(total) AS totalsales

FROM transactions

GROUP BY period

ORDER BY period DESC

LIMIT ?

""",

(periods,),

).fetchall()

return [dict(r) for r in rows]

if name == "main":

mcp.run()

Note the Literal type for period. It tells the model there are exactly three valid values, so it will not invent others. For parameters with a fixed set of options, always use Literal or an Enum.

Also note that I use parameterized queries rather than f-strings for values. Values coming from the model must be treated like user input: never interpolate them straight into SQL. This matters a lot, because a model can produce any parameter it likes.

A Tool That Calls an External API

import os

import httpx

from fastmcp import FastMCP

mcp = FastMCP("CRM Integration")

BASEURL = "https://api.internal-crm.com/v1"

APIKEY = os.environ["CRMAPIKEY"]

@mcp.tool

async def createticket(title: str, description: str, priority: str = "normal") -> dict:

"""Create a new support ticket in the CRM.

Args:

title: short ticket title

description: full description of the issue

priority: low, normal, high, or critical

"""

async with httpx.AsyncClient(timeout=30) as client:

resp = await client.post(

f"{BASEURL}/tickets",

headers={"Authorization": f"Bearer {APIKEY}"},

json={"title": title, "description": description, "priority": priority},

)

resp.raiseforstatus()

data = resp.json()

return {

"ticketid": data["id"],

"url": data["weburl"],

"status": data["status"],

}

FastMCP supports async functions natively, and for tools doing network calls async is the right choice so your server does not block.

Note that I return a trimmed dictionary rather than the whole raw API response. That is good practice. API responses often carry dozens of irrelevant fields, and all of them consume tokens in the model's context. Return only what is useful.

Resources: Readable Data

from fastmcp import FastMCP

mcp = FastMCP("Internal Documentation")

@mcp.resource("docs://leave-policy")

def leavepolicy() -> str:

"""The currently effective employee leave policy."""

with open("documents/leavepolicy.md", encoding="utf-8") as f:

return f.read()

@mcp.resource("data://customers/{customerid}")

def customerprofile(customerid: str) -> dict:

"""Full profile of a customer by ID."""

with connect() as conn:

row = conn.execute(

"SELECT FROM customers WHERE id = ?", (customerid,)

).fetchone()

if row is None:

raise ValueError(f"customer {customerid} not found")

return dict(row)

The second resource uses a URI template with a parameter, letting clients fetch specific records through a consistent addressing scheme.

Prompts: Ready-Made Templates

@mcp.prompt

def complaintanalysis(complainttext: str) -> str:

"""Template for analyzing a customer complaint in a structured way."""

return f"""Analyze the following customer complaint and provide:

  • Problem category
  • Urgency level with justification
  • Department that should handle it
  • Suggested first response to the customer
  • Complaint:

    {complainttext}

    """

    This appears in the client application as a command the user can select, typically through a slash command menu.

    Handling Errors Properly

    This part is often neglected despite mattering a lot for interaction quality.

    from fastmcp import FastMCP
    

    from fastmcp.exceptions import ToolError

    mcp = FastMCP("Safe Server")

    @mcp.tool

    def getreport(reportid: str) -> dict:

    """Fetch a report by ID."""

    if not reportid.startswith("RPT-"):

    raise ToolError("Report IDs must start with 'RPT-', for example RPT-2026-001")

    report = database.find(reportid)

    if report is None:

    raise ToolError(f"Report {reportid} not found. "

    f"Use the searchreports tool to list available reports.")

    return report

    ToolError is a message you intentionally show the model. Notice it does not just say "error", it tells the model what went wrong and what to do next. The model reads that and can fix its own call. This is the difference between an agent that gets stuck and one that recovers.

    By contrast, an uncaught ordinary exception produces a generic error message that helps nobody.

    Running as an HTTP Server

    For centralized deployment:

    if name == "main":
    

    mcp.run(transport="http", host="0.0.0.0", port=8000)

    Clients then connect to http://your-server:8000/mcp.

    If you run this on a network many people can reach, think about authentication. FastMCP supports auth middleware, and at minimum you should place it behind an authenticating reverse proxy. Never expose a data-mutating MCP server to the open internet unprotected.

    Testing Your Server

    Testing matters because your tools will be called by a model with parameters you did not necessarily imagine.

    import pytest
    

    from fastmcp import Client

    from server import mcp

    @pytest.mark.asyncio

    async def testcountbusinessdays():

    async with Client(mcp) as client:

    result = await client.calltool(

    "countbusinessdays",

    {"startdate": "2026-09-01", "enddate": "2026-09-30"},

    )

    assert result.data == 22

    @pytest.mark.asyncio

    async def testtoollisting():

    async with Client(mcp) as client:

    tools = await client.listtools()

    names = [t.name for t in tools]

    assert "countbusiness_days" in names

    FastMCP's built-in client can connect directly to the in-memory server object, so tests run fast with no separate process.

    Tips and Best Practices

    Write docstrings as though explaining to a new colleague. The model reads them to decide when to use your tool. A poor docstring means a tool that is rarely used or used wrongly.

    Always use specific type hints. Literal["daily", "weekly"] is far better than str.

    Return compact, cleaned data. Every unnecessary field is a wasted token and a source of confusion.

    Do not cram too many tools into one server. With 40 tools the model struggles to choose. Group them into several domain-specific servers, or merge similar tools behind a mode parameter.

    Treat every parameter from the model as untrusted input. Validate, bound, and use parameterized queries. A model can produce any value at all.

    Bound tools that could return large result sets. Always offer a limit parameter with a sane default.

    For tools that mutate data or have serious side effects, consider a confirmation pattern: a separate preview tool showing what would change, before the tool that actually executes it.

    Log every tool invocation with its parameters. When something looks strange, those logs will save you.

    Conclusion

    FastMCP turns MCP server development into a task of minutes rather than days. The summary:

    MCP standardizes how AI applications connect to tools and data, so you write a server once and many applications can use it.

    Tools for actions, Resources for readable data, Prompts for templates. Most practical needs are met with Tools.

    Type hints and docstrings are not optional documentation, they are the interface the model reads to use your tool correctly.

    ToolError with an informative message lets the model correct its own mistakes.

    Use STDIO for local tools and HTTP for shared ones, and do not forget authentication when the server is network reachable.

    Test with fastmcp dev before connecting to any application, and write automated tests with the built-in client.

    Start with one repetitive task on your team, maybe looking up order status or pulling a weekly metrics summary. Wrap it as a single MCP tool, connect it to Claude, and feel the difference when AI can reach your real company data instead of guessing. Happy building.

    Related Articles

    MCP (Model Context Protocol): Building Tool Integrations for AI

    MCP (Model Context Protocol): Membangun Integrasi Tools untuk AI Model Context Protocol (MCP) adalah protokol standar te...

    MergeKit Tutorial: Merging LLM Models to Create Custom AI Models

    Tutorial MergeKit: Menggabungkan Model LLM untuk Membuat Model AI Kustom Model bahasa besar (LLM) open-source terus berk...

    LlamaParse Tutorial: Parsing Complex Documents for RAG Pipelines

    Tutorial LlamaParse: Parsing Dokumen Kompleks untuk RAG Pipeline Membangun aplikasi Retrieval-Augmented Generation (RAG)...

    Semantic Router Tutorial: Smart Routing for LLM Applications

    Tutorial Semantic Router: Routing Cerdas untuk Aplikasi LLM Semantic Router adalah library Python yang memungkinkan Anda...