TechByteByByte

Building Your First Complete MCP Server

Bring Tools, Resources, Prompts, and real error handling together into one, complete, growing server — starting genuinely small, and adding exactly one real capability at a time.

#MCP#Server#FastMCP

Recall Modules 5 through 8 — every real piece is now on the table. This module’s entire job is assembly: building one, real, complete server, growing it deliberately, version by version, so you feel exactly why each real addition earns its place.

Version 1: the minimal, real server

Let’s start with the smallest, real thing that actually runs — no capabilities yet, just a genuine, working server.

from fastmcp import FastMCP

mcp = FastMCP("support-server")  # a real, named server, with nothing exposed yet

if __name__ == "__main__":
    mcp.run()  # genuinely starts the server, using stdio by default

Run this, and it works — a real, live MCP server, connectable by any real Client, exposing genuinely nothing useful yet.

Version 2: adding a real tool

Let’s add the first, real, useful capability.

We’ll add a genuine order-status lookup, the same real pattern from Module 5.

from fastmcp import FastMCP

mcp = FastMCP("support-server")

ORDERS = {"O1": "Shipped", "O2": "Processing"}

@mcp.tool()
def get_order_status(order_id: str) -> str:
    """Look up the real, current status of an order."""
    return ORDERS.get(order_id, f"No order found with ID {order_id}")

if __name__ == "__main__":
    mcp.run()

Version 3: adding a real resource

Let’s add real, read-only context alongside the tool — recall Module 6’s own precise distinction.

We’ll expose the company’s real return policy as genuine, readable context.

@mcp.resource("policy://returns")
def return_policy() -> str:
    """The company's real, current return policy."""
    return "Returns are accepted within 30 days of purchase, with a valid receipt."

Notice this real addition doesn’t touch anything from Version 2 at all — Tools and Resources genuinely coexist on the same server, each doing its own, distinct job.

Version 4: adding a real prompt

Let’s add the third, real capability type — recall Module 7’s own reusable, tuned templates.

We’ll expose a genuine, consistent prompt for drafting real customer responses.

@mcp.prompt()
def draft_response(order_id: str, status: str) -> str:
    """A real, reusable prompt for drafting a customer-facing status update."""
    return f"Write a brief, friendly update to a customer about order {order_id}, whose real status is: {status}."

The server now genuinely exposes all three real capability types at once — a tool that acts, a resource that informs, and a prompt that shapes how a real response gets written.

Version 5: real error handling and logging

Let’s make this genuinely production-shaped, rather than a fragile demo — recall your own resilience coursework’s real discipline.

We’ll add real, deliberate error handling and genuine, structured logging to the existing tool.

import logging
from fastmcp import FastMCP

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger("support-server")

mcp = FastMCP("support-server")
ORDERS = {"O1": "Shipped", "O2": "Processing"}

@mcp.tool()
def get_order_status(order_id: str) -> dict:
    """Look up the real, current status of an order."""
    logger.info(f"Looking up order: {order_id}")  # a real, genuine log entry
    if order_id not in ORDERS:
        logger.warning(f"Order not found: {order_id}")  # a real, honest warning, not a silent failure
        return {"error": f"No order found with ID {order_id}"}
    return {"order_id": order_id, "status": ORDERS[order_id]}

@mcp.resource("policy://returns")
def return_policy() -> str:
    """The company's real, current return policy."""
    return "Returns are accepted within 30 days of purchase, with a valid receipt."

@mcp.prompt()
def draft_response(order_id: str, status: str) -> str:
    """A real, reusable prompt for drafting a customer-facing status update."""
    return f"Write a brief, friendly update to a customer about order {order_id}, whose real status is: {status}."

if __name__ == "__main__":
    mcp.run()

Recall Module 5’s own real lesson about tool errors — this final, real version returns an honest, structured error rather than crashing, and logs both the real request and any genuine failure, exactly the observability discipline a real, deployed server actually needs.

The real, complete growth, seen at a glance

flowchart LR
    A[V1: Minimal server] --> B[V2: + Tool]
    B --> C[V3: + Resource]
    C --> D[V4: + Prompt]
    D --> E[V5: + Error handling + logging]

Common mistakes worth avoiding

Adding every capability at once, in one large, untested file. Recall this module’s own deliberate progression — building and verifying one, real capability at a time makes any real failure immediately traceable to what you just added.

Skipping logging until something genuinely breaks in production. Recall Version 5’s own real, direct addition — structured logging costs almost nothing to add early, and becomes genuinely invaluable the first time a real, remote user reports something going wrong.

Forgetting if __name__ == "__main__":. Without it, a real server module can be imported elsewhere without accidentally starting a live server — worth keeping this guard even in a small, real file.

What you should take away from this module

  • A real MCP server grows genuinely incrementally — start minimal, verify it runs, then add exactly one capability at a time.
  • Tools, Resources, and Prompts genuinely coexist on the same server, each handling its own, distinct real responsibility.
  • Real, structured logging and honest, returned errors — not crashes — are what separate a genuine, production-shaped server from a fragile demo.

Where this goes next

The next module covers the other half of every connection: Building an MCP Client — connecting, discovering capabilities, and actually calling the real server you just built.

Author
TechByteByByte Editorial Team
Reviewed by
TechByteByByte Admin
Published
Last reviewed