Frontend Labs
⋯

Chapter 58 — Hands-on: build an MCP server and connect it

What you'll build

An MCP server exposing two tools over stdio — one read-only, one that writes — plus a resource. Then you'll connect it to a host and to your own client.

Step 1 — Install the SDK

pip install "mcp[cli]"          # Python SDK
# or:  npm install @modelcontextprotocol/sdk

Step 2 — Write the server

# order_server.py
from mcp.server.fastmcp import FastMCP
import re, json, pathlib

mcp = FastMCP("orders")

ORDERS = {
    "ORD-1001": {"customer": "C-42", "status": "shipped",
                 "total_eur": 89.90, "items": ["Blue mug", "Notebook"]},
    "ORD-1002": {"customer": "C-42", "status": "processing",
                 "total_eur": 15.00, "items": ["Pen set"]},
    "ORD-2001": {"customer": "C-99", "status": "delivered",
                 "total_eur": 240.00, "items": ["Desk lamp"]},
}
REFUNDS = pathlib.Path("/tmp/refunds.jsonl")


@mcp.tool()
def get_order(order_id: str) -> dict:
    """Look up an order by its ID. Returns status, items and total.
    Use this whenever a customer asks about an order."""
    if not re.fullmatch(r"ORD-\d{4}", order_id):          # VALIDATE
        return {"error": "Invalid order ID. Expected format ORD-1234."}
    order = ORDERS.get(order_id)
    if not order:
        return {"error": f"No order found with ID {order_id}."}
    return order


@mcp.tool()
def list_customer_orders(customer_id: str) -> dict:
    """List all order IDs belonging to a customer."""
    ids = [oid for oid, o in ORDERS.items() if o["customer"] == customer_id]
    return {"customer_id": customer_id, "order_ids": ids, "count": len(ids)}


@mcp.tool()
def request_refund(order_id: str, reason: str) -> dict:
    """REQUEST a refund for an order. This does NOT issue the refund --
    it creates a request that a human must approve. Use only when the
    customer explicitly asks for a refund."""
    order = ORDERS.get(order_id)
    if not order:
        return {"error": "Unknown order."}
    if order["total_eur"] > 100:                          # POLICY IN CODE
        return {"status": "escalated",
                "message": "Refunds over 100 EUR require a manager."}
    with REFUNDS.open("a") as f:
        f.write(json.dumps({"order_id": order_id, "reason": reason,
                            "state": "pending_approval"}) + "\n")
    return {"status": "pending_approval",
            "message": f"Refund requested for {order_id}. Awaiting approval."}


@mcp.resource("policy://refunds")
def refund_policy() -> str:
    """The company refund policy, for the assistant to read."""
    return (
        "Refund policy:\n"
        "- Refunds accepted within 30 days of delivery.\n"
        "- Orders over 100 EUR require manager approval.\n"
        "- Digital goods are non-refundable.\n"
    )


if __name__ == "__main__":
    mcp.run()          # stdio transport by default

Three things to notice — they're the lessons of Part 12 and 14 in code:

  1. get_order validates its input before touching data (Chapter 52).
  2. request_refund does not issue a refund. It creates a pending request. The irreversible action needs a human (Chapter 60).
  3. The refund policy is enforced in Python, not asked of the model. Rules that must hold belong in code (Chapter 6's lesson, all the way back in Part 2).

Step 3 — Test it by hand

python order_server.py

Paste in:

{"jsonrpc":"2.0","id":1,"method":"tools/list","params":{"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"manual-test","version":"0.1.0"}}}}

Then:

{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"get_order","arguments":{"order_id":"ORD-1001"},"_meta":{"io.modelcontextprotocol/protocolVersion":"2026-07-28","io.modelcontextprotocol/clientInfo":{"name":"manual-test","version":"0.1.0"}}}}

You should see your tool's output come back as JSON-RPC. You have written a working MCP server.

The SDK also ships an inspector, which is far more pleasant:

mcp dev order_server.py        # opens a UI to browse and call your tools

Step 4 — Connect it to a host

Most MCP hosts use a JSON config listing servers. The shape is consistent:

{
  "mcpServers": {
    "orders": {
      "command": "python",
      "args": ["/absolute/path/to/order_server.py"]
    }
  }
}

Restart the host, and your three tools appear. Ask it "What's the status of order ORD-1001?" and watch it call your server.

Check your specific host's documentation for the config file location — it differs between applications and changes between versions.

Step 5 — Write your own client

This is what makes MCP click: connect a server to your agent from Chapter 51.

# my_client.py
import asyncio, json
from mcp import ClientSession, StdioServerParameters
from mcp.client.stdio import stdio_client

async def main():
    params = StdioServerParameters(command="python", args=["order_server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            # Some SDK versions expose initialize() for compatibility with
            # older MCP revisions. For 2026-07-28, discovery/metadata are
            # request-scoped; follow your installed SDK's current tutorial.
            if hasattr(session, "initialize"):
                await session.initialize()

            # DISCOVER what the server can do
            tools = await session.list_tools()
            for t in tools.tools:
                print(f"tool: {t.name} -- {t.description.splitlines()[0]}")

            # CALL a tool
            result = await session.call_tool("get_order", {"order_id": "ORD-1001"})
            print("result:", result.content[0].text)

            # READ a resource
            res = await session.read_resource("policy://refunds")
            print("policy:", res.contents[0].text[:80], "...")

asyncio.run(main())

To wire it into your agent: convert the MCP tool list into your provider's tool-schema format, and route tool calls through session.call_tool. That's roughly twenty lines, and then your Chapter 51 agent can use any MCP server anyone has written.

Practice (60 min)

  1. Run the server and client above. Confirm both work.
  2. Add a tool — search_orders(status) returning matching IDs. Notice how little code it takes once the server exists.
  3. Connect a third-party server — try the official filesystem, fetch, or git reference servers. Point one at a scratch directory.
  4. Wire MCP into your Chapter 51 agent. Have it answer "How many orders does customer C-42 have, and what's in the most recent one?" — that needs two tool calls in sequence. Watch the loop do it.
  5. Break it deliberately: make get_order slow (time.sleep(30)). What happens? Now make it raise an exception. Does your agent recover?

Common confusions

  • "Do I need MCP to use tools?" No (Chapter 56). Use it for reuse, isolation, or third-party servers.
  • "Can an MCP server call the LLM?" Not in the core flow — the host owns the model. Keep the boundary clean.
  • "Is stdio secure?" The server runs as a subprocess with your permissions. That's convenient and dangerous — see Chapter 61.
  • "How do I do auth for remote servers?" The specification uses OAuth 2.1 (PKCE required, no implicit grant), and servers must validate the token audience. Chapter 61.

Check yourself

  1. What are the two transports, and when would you use each?
  2. In the example, why doesn't request_refund actually issue a refund?
  3. Why is the 100 EUR policy in Python instead of the tool description?
  4. What does a client do first after connecting?

Further reading

  • Docs: modelcontextprotocol.io — "Build an MCP server" and "Build an MCP client" tutorials. Follow both.
  • Code: github.com/modelcontextprotocol/servers — read two or three reference servers.
  • Registry: the official MCP registry and community directories — browse what already exists before writing your own.

End of Part 13 — Milestone check

  • I can explain the N×M problem MCP solves
  • I can name the three roles and say which talks to the LLM
  • I know the three server primitives and who controls each
  • I have run an MCP server and called it by hand with JSON-RPC
  • I have written my own MCP server
  • I have connected a server to a host or my own client

Generated from real-life-ai-example-v1.md · 8 Oct 2026 16:36