Wat ik leerde van MCP-servers bouwen in Python

Met het Model Context Protocol (MCP) kan een AI-assistent zoals Claude tools aanroepen die jij schrijft. Ik heb een flink aantal MCP-servers gebouwd voor marketing-API's, zoals advertentieplatforms, analytics en productfeeds. Dit artikel laat een kleine server zien met de officiële Python-SDK, en de gewoontes die die servers veilig en makkelijk maakten voor een assistent.

Een kleine server

Installeer de SDK met pip install mcp httpx. Versie 2 van de SDK noemt de serverklasse MCPServer. Oudere handleidingen gebruiken FastMCP, de naam in versie 1.

import json
from pathlib import Path

import httpx
from mcp.server.mcpserver import MCPServer

mcp = MCPServer(
    "weather-notes",
    instructions=(
        "Look up the forecast with get_forecast. "
        "save_note only shows a preview by default. Show that preview to the user "
        "and call it again with dry_run=False only after they agree."
    ),
)

NOTES = Path("notes.json")


@mcp.tool()
async def get_forecast(latitude: float, longitude: float, days: int = 3) -> dict:
    """Daily minimum and maximum temperature in °C for a location, from Open-Meteo."""
    async with httpx.AsyncClient(timeout=10) as client:
        response = await client.get(
            "https://api.open-meteo.com/v1/forecast",
            params={
                "latitude": latitude,
                "longitude": longitude,
                "daily": "temperature_2m_min,temperature_2m_max",
                "forecast_days": days,
                "timezone": "auto",
            },
        )
    response.raise_for_status()
    daily = response.json()["daily"]
    return {
        day: {"min": low, "max": high}
        for day, low, high in zip(daily["time"], daily["temperature_2m_min"], daily["temperature_2m_max"])
    }


@mcp.tool()
def save_note(title: str, text: str, dry_run: bool = True) -> str:
    """Save a note. Returns a preview unless dry_run is False."""
    if dry_run:
        return f"Preview, nothing saved yet: '{title}' ({len(text)} characters). Call again with dry_run=False to save."
    notes = json.loads(NOTES.read_text()) if NOTES.exists() else {}
    notes[title] = text
    NOTES.write_text(json.dumps(notes, indent=2))
    return f"Saved '{title}'."


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

Elke functie met @mcp.tool() wordt een tool. De type hints worden het invoerschema en de docstring wordt de beschrijving die de assistent leest. mcp.run() praat via stdio, zo starten desktopclients zoals Claude Desktop een lokale server. Met mcp.run(transport="streamable-http") luistert dezelfde server op http://127.0.0.1:8000/mcp, zodat je hem voor een team kunt hosten.

Geef de assistent een werkwijze

De assistent weet alleen wat de instructies en de beschrijvingen van de tools hem vertellen. Mijn servers beginnen met een korte werkwijze in instructions, bijvoorbeeld welke tool eerst moet en wat te doen als een account ontbreekt. Zonder dat moet de assistent de volgorde van de aanroepen raden.

Zoek accounts op in plaats van ze in te stellen

Bij API's met veel accounts zet ik geen lijst met accounts in een configbestand. De server heeft een tool die de accounts toont waar de inloggegevens bij kunnen, en de instructies zeggen dat de assistent die eerst aanroept en de naam zoekt die de gebruiker noemt. Een nieuw account werkt dan zonder aanpassing aan de server.

Maak schrijfacties standaard een dry run

Data lezen kan geen kwaad, een budget wijzigen of een product verwijderen wel. In mijn servers hebben tools die iets veranderen dry_run: bool = True. De eerste aanroep geeft een voorbeeld, de assistent laat dat zien, en pas een tweede aanroep met dry_run=False voert de wijziging uit, nadat de gebruiker akkoord geeft. save_note hierboven werkt net zo.

Geef waarden terug die een mens kan lezen

Advertentie-API's geven bedragen vaak in micros, waarbij één euro 1.000.000 is. Ik geef het oorspronkelijke veld terug met een leesbaar bedrag ernaast, zodat de assistent niet hoeft om te rekenen en minder snel een fout maakt.

Test met een echte client

De SDK heeft ook een client. Een kort script dat de server via stdio start, de tools opvraagt en ze aanroept, vangt fouten op voordat een assistent de server ooit ziet.

import asyncio
import sys

from mcp import ClientSession
from mcp.client.stdio import StdioServerParameters, stdio_client


async def main():
    params = StdioServerParameters(command=sys.executable, args=["server.py"])
    async with stdio_client(params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            tools = await session.list_tools()
            print([tool.name for tool in tools.tools])
            result = await session.call_tool("save_note", {"title": "test", "text": "hello"})
            print(result.content[0].text)


asyncio.run(main())

De code in dit artikel is getest met versie 2.3 van het pakket mcp.