Einen Web-Research-Agenten mit dem Claude Agent SDK und hartem Budget bauen

Rund 60 Zeilen Python: Claude Agent SDK plus der FastAF-MCP-Server für Suche, Seitenabrufe und Google Scholar. Ein echter Lauf kostete 11 Credits.

Ein Research-Agent braucht drei Dinge: einen Weg, Quellen zu finden, einen Weg, sie zu lesen, und ein Limit dafür, wie viel er für beides ausgeben darf. Das Claude Agent SDK liefert die Schleife. Der gehostete MCP-Server von FastAF liefert die Tools. Dieser Beitrag verdrahtet beides in rund 60 Zeilen Python, lässt es auf eine echte Frage los und zeigt, was es gekostet hat.

Was brauchst du, bevor du loslegst?

Python 3.10 oder neuer, das SDK und zwei Keys:

python3 -m venv .venv && source .venv/bin/activate
pip install claude-agent-sdk
export ANTHROPIC_API_KEY=...   # laut Quickstart des Agent SDK
export FASTAF_API_KEY=...      # aus deinem FastAF-Dashboard

Das Paket heißt claude-agent-sdk (Version 0.2.168 auf PyPI, hochgeladen am 15. Oktober 2026) und bringt das Binary von Claude Code gleich mit, du musst also nichts weiter installieren. Auf einem Entwicklungsrechner übernimmt es auch einen bestehenden Claude-Code-Login, und so wurde der Lauf unten gemacht. Anthropics Übersicht zum Agent SDK, geprüft am 19. Oktober 2026, sagt, dass Produkte auf Basis des SDK die Authentifizierung per API-Key nutzen müssen statt eines claude.ai-Logins, also liefere mit ANTHROPIC_API_KEY aus.

Auf der FastAF-Seite ist es eine einzige URL: https://api.fastaf.co/mcp, Streamable HTTP, mit deinem Key als Bearer-Header. Der Server stellt 15 Tools bereit; dieser Agent nutzt drei. search_web und search_scholar kosten je 5 Credits, scrape_page kostet 1 Credit für eine statische Seite. Die MCP-Docs listen den Rest.

Der Agent

import asyncio, json, os, sys
from claude_agent_sdk import (
    query, ClaudeAgentOptions, HookMatcher, AssistantMessage, UserMessage,
    ResultMessage, ToolUseBlock, ToolResultBlock,
)

MAX_CALLS = 6
SYSTEM = f"""You are a research agent. Answer only from what the tools return.
Use at most {MAX_CALLS} tool calls, one at a time. If a tool returns RATE_LIMITED, retry it.
search_web and search_scholar find sources; scrape_page reads the 1-3 best ones.
Answer in under 250 words. Cite every claim as [n] and list the URLs at the end."""

calls = 0
async def cap_calls(input_data, tool_use_id, context):
    global calls
    calls += 1
    if calls <= MAX_CALLS:
        return {}
    return {"hookSpecificOutput": {
        "hookEventName": input_data["hook_event_name"],
        "permissionDecision": "deny",
        "permissionDecisionReason": "Tool budget used up. Answer with what you have.",
    }}

options = ClaudeAgentOptions(
    model="claude-sonnet-5-5",
    system_prompt=SYSTEM,
    mcp_servers={"fastaf": {
        "type": "http",
        "url": "https://api.fastaf.co/mcp",
        "headers": {"Authorization": f"Bearer {os.environ['FASTAF_API_KEY']}"},
    }},
    tools=[],                                     # keine eingebauten Tools (Bash, Read, ...)
    allowed_tools=[f"mcp__fastaf__{t}" for t in ("search_web", "scrape_page", "search_scholar")],
    permission_mode="dontAsk",                    # alles nicht Erlaubte wird abgelehnt
    setting_sources=[],                           # lokale Claude-Code-Einstellungen ignorieren
    env={"ENABLE_CLAUDEAI_MCP_SERVERS": "false"}, # keine claude.ai-Connectors
    hooks={"PreToolUse": [HookMatcher(matcher="mcp__fastaf__.*", hooks=[cap_calls])]},
    max_turns=10,
    max_budget_usd=0.50,
)

async def main(question):
    credits = 0
    async for msg in query(prompt=question, options=options):
        if isinstance(msg, AssistantMessage):
            for b in msg.content:
                if isinstance(b, ToolUseBlock):
                    print(f"-> {b.name.removeprefix('mcp__fastaf__')} {b.input}")
        elif isinstance(msg, UserMessage) and isinstance(msg.content, list):
            for b in msg.content:
                if isinstance(b, ToolResultBlock):
                    text = b.content if isinstance(b.content, str) else b.content[0]["text"]
                    if b.is_error:
                        print(f"   error: {text}")
                    else:
                        data = json.loads(text)
                        credits += data["credits_used"]
                        print(f"   {data['credits_used']} credits, {data['credits_remaining']} left")
        elif isinstance(msg, ResultMessage):
            print("\n" + (msg.result or f"stopped: {msg.subtype}"))
            print(f"\nturns={msg.num_turns} fastaf_credits={credits} model_usd={msg.total_cost_usd:.4f}")

asyncio.run(main(sys.argv[1]))

Die Optionen, auf die es ankommt, laut der Python-SDK-Referenz und dem MCP-Guide, beide geprüft am 19. Oktober 2026:

  • MCP-Tools heißen mcp__<server>__<tool>, der Key "fastaf" ergibt also mcp__fastaf__search_web.
  • allowed_tools genehmigt nur automatisch; die anderen 12 FastAF-Tools blendet es nicht aus. Erst permission_mode="dontAsk" lehnt alles ab, was nicht auf der Liste steht.
  • tools=[] entfernt die eingebauten Tools. In unserem ersten Lauf führte die Init-Nachricht trotzdem claude.ai-Connectors (darunter Gmail) aus dem lokalen Login auf, deshalb steht ENABLE_CLAUDEAI_MCP_SERVERS auf false.
  • Jedes Tool-Ergebnis von FastAF ist JSON mit credits_used und credits_remaining, das Skript summiert also die echten Ausgaben, statt sie zu schätzen.

Was hat er tatsächlich gemacht?

Ausgeführt am 19. Oktober 2026 mit der Frage „What changed in the Model Context Protocol (MCP) specification in 2026, and what does recent academic research say about MCP security risks? Cite sources.” Gekürzte Ausgabe:

-> search_web {'query': 'Model Context Protocol specification 2026 changelog new version', 'language': 'en'}
-> search_scholar {'query': 'Model Context Protocol security risks attacks', 'year_from': 2025, 'results': 8}
   5 credits, 890 left
   5 credits, 885 left
-> scrape_page {'url': 'https://blog.modelcontextprotocol.io/posts/2026-07-28/'}
   1 credits, 884 left

The 2026-07-28 revision is the main 2026 release. It followed the November 2025 release [1].
- Stateless core: The initialize/initialized handshake and the Mcp-Session-Id header are gone. [...] [1]
- Routing headers: Mcp-Method and Mcp-Name are now required on Streamable HTTP. [...] [1]
- Authorization hardening: Clients must validate the RFC 9207 iss parameter. [...] [1]
[...]
- A survey in ACM Transactions on Software Engineering and Methodology (TOSEM) identifies 16 threat scenarios. [2]
- An arXiv study ran 847 attack scenarios. [...] This is a preprint, and I only saw its abstract snippet. [3]
[...]
[1] https://blog.modelcontextprotocol.io/posts/2026-07-28/
[2] https://dl.acm.org/doi/abs/10.1145/3796519
[3] https://arxiv.org/abs/2601.17549

turns=4 fastaf_credits=11 model_usd=0.0578

Drei Tool-Aufrufe, 11 FastAF-Credits, etwa 20 Sekunden und 0,0578 $ Modellkosten laut Schätzung des SDK. Wir haben die Aussagen zur Spec mit der gescrapten Seite abgeglichen und die Zahlen aus den Papern mit den Scholar-Snippets: Alle stehen dort. Der Agent hat außerdem angemerkt, dass er nur Abstracts gesehen hat, und genau dieses Verhalten willst du. Für Fragen mit vielen Papern beschreibt die Seite zur Google Scholar API die Felder, die search_scholar zurückgibt.

Ein ehrlicher Hinweis: Unser Test-Key teilt sich einen Account mit Nebenläufigkeit 1 mit anderen Jobs, und bei den ersten Versuchen bekamen die meisten Aufrufe RATE_LIMITED (fehlgeschlagene Aufrufe werden nie abgerechnet). Für den Lauf oben haben wir einen zweiten Debug-Hook ergänzt, der auf einen freien Slot wartet. Mit deinem eigenen Key brauchst du ihn nicht, aber lies zuerst den nächsten Abschnitt.

Wie deckelst du, was ein Research-Agent ausgibt?

Drei Ebenen, von weich bis hart:

  1. max_turns begrenzt die Schleife. Bei 10 stoppt das SDK mit dem Subtype error_max_turns, egal was das Modell plant. max_budget_usd macht dasselbe für die Modellkosten, wobei die Docs warnen, dass die Schätzung darüber hinausschießen kann.
  2. Ein Limit für Tool-Aufrufe im Prompt ist eine Präferenz. Das haben wir direkt gesehen: Der Prompt sagte „one at a time”, und in mehreren Läufen schickte das Modell trotzdem zwei Aufrufe im selben Turn, einmal sogar drei Seitenabrufe auf einmal. Auf einem Free-Account (Nebenläufigkeit 1) bekommt der zusätzliche Aufruf RATE_LIMITED. Der PreToolUse-Hook macht aus dem Limit eine Regel: Mit MAX_CALLS = 1, während der Prompt weiter 6 sagte, lehnte der Hook drei Scrapes ab, der Agent antwortete aus seiner einen Suche, und der Lauf kostete 5 Credits.
  3. Das Guthaben des Accounts ist das harte Limit. Jeder Aufruf reserviert seine Kosten vorab, und ein leeres Guthaben liefert 402 insufficient_credits. Das Guthaben gehört zum Account und wird von allen seinen Keys geteilt, ein separater Key gibt dir also kein separates Budget. Lade den Account mit dem auf, was du bereit bist, an eine kaputte Schleife zu verlieren.

Der Beitrag zum Ausgabenlimit geht tiefer auf die Seite des Wrappers ein, und Grounding mit einer SERP-API behandelt dasselbe Muster aus Suchen und Lesen ohne Agent-Schleife.

Kurzversion

Richte mcp_servers mit einem Bearer-Header auf https://api.fastaf.co/mcp, erlaube drei Tools, setz permission_mode="dontAsk" und füg einen PreToolUse-Hook hinzu, der die Aufrufe zählt. Eine Antwort mit Quellen zu einem Thema aus 2026 kostete 11 Credits, das sind rund 1.800 solcher Antworten aus einem Starter-Paket für 10 $. Die Preise zeigen die Kosten jedes Tools, und über die Anmeldung bekommst du 500 kostenlose Credits, um es selbst auszuprobieren.