Building Your Own MCP Server for AI

Parameters and Structured Results

Validate inputs, return JSON the model can use, and turn failures into clear errors instead of stack traces.

A tool that returns traceback text trains the model to apologize. A tool that returns {"ok": false, "error": "city required"} trains it to retry with a city.

Validate at the edge

Type hints catch many bad calls. Anything the schema cannot express, check yourself and return a structured error. Do not raise unless the host is supposed to treat it as a crash.

from typing import Any

from mcp.server.mcpserver import MCPServer

mcp = MCPServer("weather-lite")

KNOWN = {"berlin": 12, "austin": 28, "oslo": 3}


@mcp.tool()
def temperature_c(city: str) -> dict[str, Any]:
    '''Return a demo temperature in Celsius for a known city (berlin, austin, oslo).'''
    key = city.strip().lower()
    if not key:
        return {"ok": False, "error": "city is required"}
    if key not in KNOWN:
        return {
            "ok": False,
            "error": "unknown city",
            "hint": "Use berlin, austin, or oslo in this lesson.",
        }
    return {"ok": True, "city": key, "celsius": KNOWN[key]}

Prefer objects over prose

Strings are fine for greetings. For anything the model must compare or repeat, use a dict (JSON object). Include units in field names (celsius) so the model does not invent Fahrenheit.

HTTP and other failures

Catch httpx.TimeoutException and httpx.HTTPStatusError. Return ok: false plus status when you have one. Never return an API key in an error body.

try:
    response.raise_for_status()
except httpx.HTTPStatusError as exc:
    return {"ok": False, "status": exc.response.status_code, "error": "upstream rejected the request"}

Check it

  1. Call temperature_c with berlinok is true.
  2. Call it with an empty string and with paris — both ok false, no traceback.
  3. Add a new city to KNOWN and confirm the schema did not need to change.

Next: when data should be a resource instead of a tool.