dailytutorfor.you
Web Development

Complete 2026 Guide: Building a Production-Ready MCP Server with Python (FastMCP + HTTP Transport)

Learn how to build an MCP (Model Context Protocol) server from scratch to production-ready using Python and FastMCP.

20 min read

Complete 2026 Guide: Building a Production-Ready MCP Server with Python (FastMCP + HTTP Transport)

1) Introduction — What and Why

In the last 12 months, AI agents and MCP (Model Context Protocol) have grown very quickly in the developer community. From GitHub trend monitoring, many repositories related to “agent harness”, “MCP tools”, and “automation” have entered popular lists. In communities like dev.to, articles tagged #ai, #mcp, #automation, and #opensource also show high engagement.

The question is: why is MCP important?

Simply put, MCP is like “USB-C for AI integrations”. Instead of every AI model and every app creating its own integration format, MCP provides a communication standard between clients (LLM apps) and servers (tools/resources/prompts).

Real-world context

Imagine you have an internal support team. You want an AI assistant to check order status, read the knowledge base, and create helpdesk tickets. Without a standard, integration quickly becomes spaghetti. With MCP, you expose capabilities as standard tools/resources: more modular, reusable, and maintainable.

In this tutorial, we build a Python MCP server that:

  1. Provides simple TODO tools,
  2. Has proper error handling,
  3. Is ready to run via HTTP transport,
  4. Is easy to observe and evolve into production.

2) Prerequisites

  • Python 3.11+
  • pip or uv
  • Basic understanding of HTTP/JSON
  • Familiarity with Python typing and Pydantic

Install dependencies:

python -m venv .venv source .venv/bin/activate # Windows: .venv\Scripts\activate pip install --upgrade pip pip install "mcp[cli]" fastapi uvicorn pydantic

3) Core Concepts

Think of an MCP server like a digital restaurant:

  • Tools = kitchen (actions)
  • Resources = data showcase (context)
  • Prompts = interaction templates

Important concepts:

  • Server: protocol orchestration center
  • Tool: function for actions
  • Resource: read-only/semi read-only data
  • Transport: stdio/SSE/HTTP
  • Schema Validation: safe and consistent input-output

4) Architecture/Diagram

+-------------------+ MCP over HTTP +----------------------+ | MCP Client / LLM | <----------------------------> | MCP Server (Python) | | (IDE, Chat, Agent)| | FastMCP | +-------------------+ +----------+-----------+ | v +----------------------+ | Service Layer | | (TodoService) | +----------+-----------+ | v +----------------------+ | JSON File / Database | +----------------------+

The service layer is important so business logic does not get stuck inside tool functions.

5) Step-by-Step Implementation

Project structure

mcp-todo-server/ ├─ server.py ├─ data/ │ └─ todos.json └─ requirements.txt

Full server.py code

from __future__ import annotations import json import logging from dataclasses import dataclass from pathlib import Path from typing import Any, Optional from pydantic import BaseModel, Field, ValidationError from mcp.server.fastmcp import FastMCP logging.basicConfig( level=logging.INFO, format="%(asctime)s | %(levelname)s | %(name)s | %(message)s", ) logger = logging.getLogger("mcp-todo-server") class TodoCreateInput(BaseModel): title: str = Field(min_length=3, max_length=140) description: Optional[str] = Field(default="", max_length=500) class TodoUpdateStatusInput(BaseModel): todo_id: int = Field(gt=0) done: bool class TodoItem(BaseModel): id: int title: str description: str = "" done: bool = False class ApiResponse(BaseModel): success: bool message: str data: dict[str, Any] = {} @dataclass class TodoService: storage_path: Path def __post_init__(self) -> None: self.storage_path.parent.mkdir(parents=True, exist_ok=True) if not self.storage_path.exists(): self._write_json([]) def _read_json(self) -> list[dict[str, Any]]: try: raw = self.storage_path.read_text(encoding="utf-8") data = json.loads(raw) if not isinstance(data, list): raise ValueError("Invalid storage format: root must be list") return data except FileNotFoundError: self._write_json([]) return [] except json.JSONDecodeError as e: logger.error("Corrupted JSON storage: %s", e) raise RuntimeError("Storage corrupted. Please repair data/todos.json") def _write_json(self, data: list[dict[str, Any]]) -> None: tmp_path = self.storage_path.with_suffix(".tmp") tmp_path.write_text(json.dumps(data, ensure_ascii=False, indent=2), encoding="utf-8") tmp_path.replace(self.storage_path) def list_todos(self) -> list[TodoItem]: return [TodoItem(**item) for item in self._read_json()] def create_todo(self, payload: TodoCreateInput) -> TodoItem: items = self._read_json() next_id = max((item.get("id", 0) for item in items), default=0) + 1 todo = TodoItem(id=next_id, title=payload.title, description=payload.description or "", done=False) items.append(todo.model_dump()) self._write_json(items) return todo def set_status(self, payload: TodoUpdateStatusInput) -> TodoItem: items = self._read_json() for idx, item in enumerate(items): if item.get("id") == payload.todo_id: item["done"] = payload.done items[idx] = item self._write_json(items) return TodoItem(**item) raise ValueError(f"Todo dengan id={payload.todo_id} tidak ditemukan") mcp = FastMCP("TodoMCP-ProductionReady") service = TodoService(Path("data/todos.json")) @mcp.tool() def health_check() -> ApiResponse: """Cek status server dan storage.""" try: _ = service.list_todos() return ApiResponse(success=True, message="OK", data={"storage": "ready"}) except Exception as e: logger.exception("health_check failed") return ApiResponse(success=False, message=f"ERROR: {e}") @mcp.tool() def list_todos() -> ApiResponse: """Ambil semua todo item.""" try: items = [t.model_dump() for t in service.list_todos()] return ApiResponse(success=True, message="Todo list berhasil diambil", data={"items": items, "count": len(items)}) except Exception as e: logger.exception("list_todos failed") return ApiResponse(success=False, message=f"Gagal ambil todos: {e}") @mcp.tool() def create_todo(title: str, description: str = "") -> ApiResponse: """Buat todo baru.""" try: payload = TodoCreateInput(title=title, description=description) created = service.create_todo(payload) return ApiResponse(success=True, message="Todo berhasil dibuat", data={"todo": created.model_dump()}) except ValidationError as e: return ApiResponse(success=False, message=f"Validasi gagal: {e}") except Exception as e: logger.exception("create_todo failed") return ApiResponse(success=False, message=f"Gagal buat todo: {e}") @mcp.tool() def set_todo_status(todo_id: int, done: bool) -> ApiResponse: """Update status todo (done/undone).""" try: payload = TodoUpdateStatusInput(todo_id=todo_id, done=done) updated = service.set_status(payload) return ApiResponse(success=True, message="Status todo berhasil diupdate", data={"todo": updated.model_dump()}) except ValidationError as e: return ApiResponse(success=False, message=f"Validasi gagal: {e}") except ValueError as e: return ApiResponse(success=False, message=str(e)) except Exception as e: logger.exception("set_todo_status failed") return ApiResponse(success=False, message=f"Gagal update status: {e}") if __name__ == "__main__": mcp.run(transport="streamable-http")

Run

python server.py

Test flow

  1. health_check
  2. create_todo(title="Belajar MCP")
  3. list_todos
  4. set_todo_status(todo_id=1, done=true)

6) Best Practices

  • Separate transport from business logic.
  • Apply schema-first validation.
  • Use structured logging.
  • Avoid secret leakage in output.
  • Version tools when introducing breaking changes.
  • Prepare a retry strategy in clients for transient errors.
  • Add monitoring (latency, error rate).

7) Common Mistakes

  1. Putting all logic inside tool functions → hard to maintain.
  2. Not validating input → dirty data/crashes.
  3. Using resources for side effects → confusing design.
  4. Ignoring concurrency → race conditions.
  5. Testing only happy paths → bugs slip into production.

8) Advanced Tips

  • Migrate storage to PostgreSQL for safer concurrency.
  • Add auth + rate limiting if the server is remote.
  • Create tool permissioning: read-only vs high-risk writes.
  • Add contract testing to maintain schema compatibility.
  • Define SLOs, e.g. P95 < 1 second for lightweight tools.

9) Summary and Next Steps

An MCP server is not just a trend; it is a foundation for cleaner, more scalable AI integrations. With a service-layer approach, strict validation, and early observability, you can move from prototype to production without major refactors.

Next steps:

  1. Add a production database.
  2. Add auth and an audit trail.
  3. Add automated unit + integration tests.
  4. Deploy to staging and measure metrics.
  5. Add business-domain tools specific to your team.

10) References

A well-designed MCP will save significant maintenance costs in the future.

Complete 2026 Guide: Building a Production-Ready MCP Server with Python (FastMCP + HTTP Transport) | dailytutorfor.you