Zum Inhalt springen
OpenClaw Akademie

Modul 03 · 14 min

OpenClaw Tools und MCP - Wie du externe Funktionen sicher anbindest

OpenClaw-Tools richtig schreiben, MCP-Server einbinden, Sicherheit, Schema-Validierung und häufige Fallen.

Ohne Tools ist ein Agent ein teures Chat-Interface. Erst wenn ein Sprachmodell echte Aktionen anstoßen kann, also Dateien lesen, Datenbanken abfragen, E-Mails verschicken oder eine API ansprechen, wird aus dem Chatfenster ein Arbeitssystem. OpenClaw stellt für diesen Schritt zwei Wege bereit: lokale Python-Funktionen, die du mit einem Decorator zu Tools machst, und MCP-Server, die deine Werkzeuge über einen offenen Standard wiederverwendbar machen. In diesem Modul lernst du, wie du Tools sauber definierst, ihre Sicherheit absicherst, wann sich ein eigener MCP-Server lohnt und welche Fallen du in der Praxis vermeiden solltest.

Tool-Anatomie

Ein Tool in OpenClaw ist im Kern eine ganz normale Python-Funktion. Was sie zu einem Tool macht, ist der Decorator @agent.tool und ein paar Konventionen rund um Type-Hints, Docstrings und Rückgabewerte. Der Decorator registriert die Funktion am Agent, generiert daraus ein JSON-Schema für das Modell und sorgt dafür, dass jeder Aufruf vor der Ausführung validiert wird. Du musst dich also weder um Schema-Generierung noch um Argument-Parsing selbst kümmern, solange du die Type-Hints sauber setzt.

Type-Hints sind dabei kein Stilmittel, sondern Vertrag. OpenClaw liest die Annotations und baut daraus das Schema, das das Sprachmodell sieht. Wenn du to: str schreibst, weiß das Modell nur, dass dort ein String kommt. Schreibst du dagegen to: Annotated[str, Field(pattern=r'^[^@]+@[^@]+$')], generiert OpenClaw eine Pattern-Regel, validiert eingehende Aufrufe automatisch und gibt dem Modell den Hinweis mit, dass dort eine E-Mail-Adresse erwartet wird. Je präziser deine Hints, desto seltener halluziniert der Agent.

Der Docstring ist die Beschreibung, die das Modell zu lesen bekommt. Hier entscheidest du, wann das Tool aus Sicht des Agents aufgerufen werden soll. Schreibe nicht „Sendet eine E-Mail“ und denkst, das Modell weiß schon Bescheid. Schreibe stattdessen, unter welchen Bedingungen der Aufruf erlaubt ist, welche Eingaben sinnvoll sind und welche Fehlerfälle der Aufrufer erwarten muss. Diese paar Zeilen entscheiden darüber, ob dein Tool wie geplant gebraucht wird oder ob das Modell es ständig zur falschen Zeit feuert.

Return-Types sind die zweite Seite des Vertrags. OpenClaw erwartet serialisierbare Python-Objekte, also Dicts, Listen, Strings, Zahlen oder Pydantic-Modelle. Das Ergebnis wird automatisch in den Chat-Verlauf eingespeist und steht dem Agent für die nächste Iteration zur Verfügung. Wenn du dort eine ganze Datenbank-Row zurückgibst, frisst das schnell Tokens. Halte die Antworten knapp und gib lieber nur das zurück, was der Agent für die nächste Entscheidung wirklich braucht.

Edge-Cases sind das, was Anfänger gerne vergessen. Was passiert, wenn die externe API down ist? Was, wenn der Empfänger nicht existiert? Was, wenn das Limit überschritten wird? Tools sollten klar zwischen erwarteten Fehlern, die du dem Modell als strukturierte Antwort zurückreichst, und unerwarteten Fehlern, die du als Exception bubblen lässt, unterscheiden. Erwartete Fehler kann das Modell selbst behandeln, etwa indem es eine andere Adresse versucht. Unerwartete Fehler sollten den Agent-Loop stoppen.

Hier ein vollständiges Beispiel, das die fünf Punkte zusammenführt:

from typing import Annotated
from pydantic import Field

@agent.tool
def send_email(
    to: Annotated[str, Field(pattern=r'^[^@]+@[^@]+$')],
    subject: Annotated[str, Field(max_length=120)],
    body: str,
) -> dict:
    """Versendet eine E-Mail. Nutze nur, wenn explizit gefragt."""
    ...

Mit drei Type-Hints, einem Pattern, einer Längenbegrenzung und einem klaren Docstring hast du ein Tool, das schwerer zu missbrauchen ist als die meisten klassischen API-Wrapper. Das ist der Grund, warum OpenClaw so stark auf Pydantic setzt: Du beschreibst die Welt einmal, und das Modell hält sich dran.

Tool-Scoping und Sicherheit

Argument-Validation ist der erste Schutzwall. Jeder Tool-Aufruf läuft durch das von OpenClaw generierte Pydantic-Modell, bevor dein Code überhaupt anläuft. Wenn das Modell ein Feld vergisst oder einen falschen Typ liefert, bekommt der Agent eine strukturierte Fehlermeldung zurück und kann den Aufruf wiederholen. Du selbst musst weder if not to: noch try: int(value) schreiben. Was du tun solltest, ist die Constraints so eng wie möglich zu halten. Ein Field(ge=1, le=100) für eine Seitenzahl ist besser als ein offenes int.

Allowlist-Patterns sind das zweite Mittel. Wenn ein Tool eine URL, einen Pfad oder eine Tabellen-ID entgegennimmt, gehört dort keine offene Eingabe hin. Beschränke Pfade auf ein definiertes Verzeichnis, URLs auf eine Liste erlaubter Hosts, Datenbanktabellen auf einen Whitelist-Set. OpenClaw stellt dafür ScopedPath, ScopedURL und ScopedEnum als Annotated-Typen bereit, sodass die Allowlist Teil des Schemas wird und schon vor dem Tool-Aufruf greift.

Logging ist nicht nur Debugging, sondern auch Forensik. Aktiviere agent.logger.audit("tool_call", tool=..., args=..., result=...) für sicherheitskritische Tools. Du willst im Zweifelsfall nachweisen können, was der Agent wann mit welchen Argumenten aufgerufen hat, vor allem wenn es um Schreibzugriffe auf Produktivsysteme geht. Das Audit-Log läuft in einen separaten Stream und ist standardmäßig nicht Teil des Modell-Kontexts, sodass du es ohne Token-Kosten betreiben kannst.

Rate-Limits gehören in jedes Tool, das nach außen telefoniert. OpenClaw bietet einen Decorator @rate_limit(calls=10, per_seconds=60) an, der pro Tool und pro Agent-Session greift. Damit verhinderst du, dass ein außer Kontrolle geratener Loop deine Twilio-Rechnung sprengt oder eine externe API in den Block zwingt. Setze die Limits konservativ und beobachte erst einmal, wie häufig dein Agent ein Tool tatsächlich braucht, bevor du sie hochziehst.

Secrets-Handling ist der häufigste Anfängerfehler. Reiche Tokens, API-Keys oder Passwörter niemals als Argument durch. Erstens stehen sie dann im Modell-Kontext und können in Logs, Caches oder Replays landen. Zweitens lernt das Modell, dass Secrets etwas sind, was man als Argument übergibt, und versucht es im nächsten Schritt mit einem anderen Tool. Lies Secrets ausschließlich im Tool-Body aus Environment-Variablen oder einem Vault, und sorge dafür, dass dein Logger sie aus Stack-Traces ausfiltert.

Wann MCP, wann nicht?

Lokale Tools sind die richtige Wahl, wenn du eine Funktion brauchst, die nur dieser eine Agent kennt. Du schreibst eine Python-Funktion, dekorierst sie und bist fertig. Kein Subprozess, kein Netzwerk, kein zweiter Code-Pfad. Das ist schnell entwickelt, einfach zu testen und im Produktionsbetrieb leicht zu monitoren. Solange dein Werkzeugkasten überschaubar bleibt und in derselben Codebasis lebt wie der Agent, ist das immer der erste Griff.

MCP, das Model Context Protocol, kommt ins Spiel, sobald Wiederverwendbarkeit oder Sprachgrenzen ein Thema werden. Ein MCP-Server kapselt Tools hinter einer JSON-RPC-Schnittstelle, sodass jeder MCP-fähige Client, also nicht nur OpenClaw, sondern auch andere Agent-Frameworks, sie aufrufen kann. Das ist Gold wert, wenn dein Team bereits einen GitHub-Adapter, einen Slack-Adapter oder einen internen Datenbank-Wrapper als MCP-Server betreibt und du diesen einfach mitnutzen willst.

MCP lohnt sich auch, wenn das Tool aus Sicherheitsgründen in einer Sandbox laufen muss. Da der Server ein eigener Prozess ist, kannst du ihn in einem Container, mit reduzierten Rechten oder auf einer anderen Maschine starten. Damit bekommst du eine echte Privilege-Trennung zwischen Agent-Loop und Tool-Execution. Wenn dein Tool zum Beispiel beliebige SQL-Statements ausführt, willst du das nicht im selben Python-Prozess wie deinen Agent tun.

Der Overhead ist real. Ein MCP-Server bedeutet einen weiteren Prozess, ein weiteres Deployment, ein weiteres Logging-Ziel. Für ein simples get_weather, das nur einen HTTP-Call macht, ist das übertrieben. Die Faustregel: Solange das Tool kürzer ist als der Boilerplate-Code für einen MCP-Server, schreibst du es lokal.

KriteriumLokales ToolMCP-Server
Einfacher Use-CaseGeeignetOverhead
Sprache wechselnNeinGeeignet
Mehrere Agenten teilenAufwändigGeeignet
Sandboxing nötigNeinGeeignet

In der Praxis fährst du am besten mit einer Mischung. Die meisten Tools entstehen lokal, weil sie schnell geschrieben und direkt am Use-Case orientiert sind. Sobald ein Tool von mehreren Projekten gebraucht wird oder eine Sandbox sinnvoll ist, ziehst du es in einen MCP-Server um. Diesen Migrationspfad bietet OpenClaw bewusst an: Die Tool-Signatur bleibt gleich, du tauschst nur den Decorator gegen eine Server-Registrierung.

MCP-Server einbinden - Quickstart

Der schnellste Einstieg ist ein bereits existierender MCP-Server. Das Anthropic-Team und die Community pflegen eine wachsende Liste von Servern, die du per npx direkt starten kannst. Kein eigener Code, keine Installation, nur ein Kommando und ein Verzeichnis, in dem der Server arbeiten darf. Das ist ideal, um die Mechanik zu verstehen, bevor du selbst einen Server schreibst.

OpenClaw stellt dafür den MCPClient bereit. Du gibst ihm das Kommando, das den Server startet, und er kümmert sich um die JSON-RPC-Verbindung über stdio, das Auflisten der Tools und das Routing der Aufrufe. Sobald du den Client am Agent registrierst, sind die Server-Tools für das Modell genauso sichtbar wie lokal definierte Tools. Der Agent unterscheidet im Loop nicht zwischen beiden.

from openclaw.mcp import MCPClient

mcp = MCPClient.connect(
    command=["npx", "-y", "@modelcontextprotocol/server-filesystem", "/tmp/agent-fs"],
)
agent.register_mcp(mcp)

Lifecycle ist ein Punkt, den man leicht übersieht. Der MCPClient startet den Server-Prozess beim Connect und beendet ihn, wenn der Agent stoppt. Wenn dein Agent als langlaufender Service läuft, bleibt der Server die ganze Zeit aktiv. Achte darauf, dass der Server selbst keine Memory-Leaks hat oder Verbindungspools öffnet, die er nicht schließt. Bei Servern, die du nicht selbst geschrieben hast, hilft ein periodischer Restart über einen Healthcheck.

Auto-Discovery passiert beim Connect. Der Client fragt den Server nach seiner Tool-Liste, lädt die Schemas und registriert sie am Agent. Wenn der Server während der Laufzeit neue Tools registriert, kannst du mcp.refresh() aufrufen und der Agent sieht sie ab dem nächsten Loop-Schritt. Das ist nützlich für Server, die ihre Tool-Liste dynamisch aus einer Konfiguration ziehen, etwa für eine Tabellen-Liste, die sich erst nach dem Datenbank-Connect ergibt.

Tools auflisten kannst du jederzeit mit mcp.list_tools(). Das gibt dir Namen, Beschreibungen und Schemas zurück, ohne den Agent zu involvieren. Praktisch zum Debuggen, wenn du wissen willst, was der Server überhaupt anbietet, oder wenn du eine Whitelist erzwingen willst. OpenClaw erlaubt es, beim Registrieren ein allowed_tools=["read_file", "list_dir"] mitzugeben, sodass nur eine Teilmenge der Server-Tools für den Agent sichtbar wird.

Ein Detail, das in Produktivsystemen wichtig wird: MCP-Verbindungen können auch über Server-Sent-Events oder WebSockets laufen, nicht nur über stdio. Für lokale Setups ist stdio bequem, aber wenn dein Server auf einer anderen Maschine läuft oder mehrere Agenten gleichzeitig bedient, willst du eine Netzwerkverbindung. OpenClaw unterstützt beides über denselben MCPClient, du gibst statt command einfach eine url an.

Eigenen MCP-Server schreiben

Manchmal gibt es das, was du brauchst, einfach noch nicht. Eine interne API, ein eigenes Datenmodell, eine spezifische Geschäftslogik. Dann schreibst du selbst einen Server. Die gute Nachricht: Das offizielle MCP-SDK gibt es für Python und für TypeScript, und das Boilerplate ist überschaubar. Wenn du schon Tools für OpenClaw lokal geschrieben hast, kennst du den Großteil der Mechanik bereits.

Das Server-Skeleton besteht aus drei Bestandteilen: einer Server-Instanz, die das Protokoll spricht, einer Tool-Registrierung, die Funktionen mit Schemas verknüpft, und einem Transport-Layer, der die Verbindung zum Client abbildet. Der einfachste Transport ist stdio, weil der Client den Server als Subprozess starten kann und das Betriebssystem die Kanäle verwaltet. Für entfernte Setups wechselst du auf SSE oder WebSocket.

from mcp.server import Server
from mcp.types import Tool, TextContent

server = Server("acme-billing")

@server.list_tools()
async def list_tools() -> list[Tool]:
    return [Tool(
        name="get_invoice",
        description="Lädt eine Rechnung anhand ihrer ID.",
        inputSchema={
            "type": "object",
            "properties": {"invoice_id": {"type": "string"}},
            "required": ["invoice_id"],
        },
    )]

@server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
    if name == "get_invoice":
        invoice = load_invoice(arguments["invoice_id"])
        return [TextContent(type="text", text=invoice.to_json())]

Tool-Registrierung läuft über zwei Handler: list_tools deklariert, was der Server kann, und call_tool führt den eigentlichen Aufruf aus. Beide sind asynchron, weil das MCP-Protokoll asynchron funktioniert. Wenn dein Backend synchron ist, wickelst du es einfach in asyncio.to_thread. Wichtig ist, dass das Schema, das du in list_tools zurückgibst, zur Validierung passt, die du in call_tool durchführst. Inkonsistenzen führen zu Aufrufen, die das Modell technisch korrekt formuliert, dein Code aber ablehnt.

JSON-RPC ist das Drahtformat. Du musst es nicht von Hand schreiben, das SDK kümmert sich darum. Trotzdem hilft es, die Konzepte zu kennen: Es gibt Requests mit ID, Notifications ohne ID, Results und Errors. Wenn du das Server-Log mitliest, siehst du Methoden wie tools/list und tools/call an dir vorbeifliegen. Bei Fehlern bekommst du einen Code und eine Message, mit denen sich gezielt debuggen lässt, statt blind nach Indizien zu suchen.

Tests gehören vom ersten Tag an dazu. Da dein Server ein normaler Prozess ist, kannst du ihn mit pytest und einem In-Process-Client gegen das eigene Protokoll testen. Schreibe für jedes Tool mindestens drei Tests: einen Happy-Path, einen mit ungültigen Argumenten, und einen mit einem Backend-Fehler. Wenn dein Server in einem CI-Pipeline läuft, ist das die Versicherung, dass eine Refactoring-Runde nicht still die Agent-Integration zerlegt.

Ein praxisnahes Beispiel ist ein Server, der eine bestehende REST-API für deinen Agent zugänglich macht. Du startest mit den drei häufigsten Endpunkten, etwa list_customers, get_customer und create_ticket. Jeder davon bekommt ein eigenes Tool im Server, das die Argumente validiert, den HTTP-Call macht und die Antwort als JSON zurückgibt. Mit zwei bis drei Tagen Arbeit hast du einen Server, den dein gesamtes Team über jeden MCP-fähigen Client nutzen kann, von OpenClaw bis Claude Desktop.

Ein zweites Beispiel aus der Praxis ist ein Server für interne Logs. Du hast eine Kibana-Instanz oder einen Loki-Cluster und willst, dass der Agent gezielt Suchen ausführen kann. Statt Kibana-URLs oder rohe LogQL-Queries durchzureichen, baust du Tools wie search_errors_for_service, count_events_in_window und tail_recent_logs. Das schränkt die Query-Sprache auf das ein, was du in der Allowlist haben willst, und der Agent denkt in Domänenbegriffen statt in Filter-Syntax. Diese Abstraktion ist mehr wert als jede zusätzliche Schema-Regel.

Ein drittes Muster lohnt sich für Teams, die intern viele kleine Datenquellen haben. Statt für jede Quelle einen eigenen Server zu betreiben, schreibst du einen generischen Server, der seine Tool-Liste aus einer Konfigurationsdatei zieht. Jede Datenquelle wird zu einem Tool mit einem klar definierten Schema. Wenn ein neuer Endpoint dazukommt, ergänzt du eine YAML-Zeile und startest den Server neu. Das ist deutlich pflegeleichter als zehn separate Server, und der Agent bekommt mit mcp.refresh() mit, wenn sich etwas ändert.

Häufige Fallen

  • Zu großzügige Type-Hints: Wenn du dict schreibst, wo Address gemeint ist, kann das Modell beliebige Strukturen erfinden. Definiere Pydantic-Modelle für komplexe Argumente und überlasse die Validierung dem Framework.
  • Docstrings ohne Aufrufbedingungen: „Sendet eine E-Mail“ reicht nicht. Schreibe explizit, wann das Tool benutzt werden soll und wann nicht. Sonst feuert das Modell es bei jeder Gelegenheit.
  • Secrets in Argumenten: API-Keys gehören in Environment-Variablen, nicht in Tool-Args. Sobald sie im Modell-Kontext landen, sind sie in Logs und Replays unterwegs.
  • Riesige Rückgaben: Eine 200-KB-JSON-Antwort frisst dein Token-Budget. Filtere serverseitig und gib nur die Felder zurück, die der Agent für die nächste Entscheidung braucht.
  • Fehlende Rate-Limits: Ein Loop, der ein Tool 500-mal in Folge aufruft, ist kein Bug, sondern eine vorhersehbare Folge. Setze von Anfang an konservative Limits und logge Überschreitungen.
  • Langlaufende Tools ohne Async-Marker: Wenn ein Tool zehn Sekunden braucht, blockierst du den Agent-Loop. Markiere solche Tools mit async=True und gib einen Job-Handle zurück, den der Agent später einlöst.
  • MCP-Server ohne Healthcheck: Server-Prozesse können hängen, ohne abzustürzen. Ein periodischer list_tools-Ping deckt das auf, bevor der Agent in einer Tool-Liste-Timeout-Schleife landet.
  • Allowlists vergessen: Pfade, URLs und Tabellen gehören gescoped. Ein Filesystem-Tool, das auf / gerichtet ist, ist eine Backdoor, die du nicht haben willst.

Nächster Schritt

Modul 4 - Memory und Context

Häufige Fragen zu diesem Modul

Was ist MCP?

Model Context Protocol - ein Standard, mit dem LLM-Agenten externe Tools über einen einheitlichen Server-Client-Layer ansprechen, statt jedes Tool individuell zu wrappen.

Brauche ich für jedes Tool einen MCP-Server?

Nein. Einfache Funktionen schreibst du als @agent.tool direkt im Python-Code. MCP lohnt sich erst, wenn ein Tool wiederverwendbar oder sprachübergreifend sein soll.

Welche MCP-Server gibt es out-of-the-box?

Filesystem, GitHub, Slack, PostgreSQL, Brave-Search und einige Dutzend mehr. OpenClaw bringt einen Adapter mit, der jeden MCP-konformen Server akzeptiert.

Wie verhindere ich, dass ein Tool mehr darf, als es soll?

Durch Tool-Scoping: Jedes Tool deklariert eine erlaubte Argument-Range, und der Agent-Loop weist Calls außerhalb dieser Range automatisch ab.

Können Tools langlaufende Aufgaben starten?

Ja. Markiere das Tool mit async=True und liefere einen Job-Handle zurück, den der Agent später per wait_for(handle) einlöst.

Wie debugge ich Schema-Mismatches?

Die Validierung greift, bevor das Tool aufgerufen wird. Im Verbose-Log siehst du genau, welches Feld fehlte oder den falschen Typ hatte.

Wie sichere ich Secrets gegen Tool-Leaks ab?

Reiche Secrets nie direkt in Tool-Args - stelle sie als Environment-Variablen bereit und lese sie im Tool-Body.