Construire un agent de recherche web avec le Claude Agent SDK et un budget strict

Environ 60 lignes de Python : le Claude Agent SDK plus le serveur MCP de FastAF pour la recherche, la lecture de pages et Google Scholar. Une vraie exécution a coûté 11 crédits.

Un agent de recherche a besoin de trois choses : un moyen de trouver des sources, un moyen de les lire, et une limite à ce qu’il peut dépenser pour faire les deux. Le Claude Agent SDK vous donne la boucle. Le serveur MCP hébergé de FastAF vous donne les outils. Cet article assemble les deux en une soixantaine de lignes de Python, les lance sur une vraie question et montre ce que cela a coûté.

De quoi avez-vous besoin avant de commencer ?

Python 3.10 ou plus récent, le SDK et deux clés :

python3 -m venv .venv && source .venv/bin/activate
pip install claude-agent-sdk
export ANTHROPIC_API_KEY=...   # selon le quickstart de l'Agent SDK
export FASTAF_API_KEY=...      # depuis votre tableau de bord FastAF

Le paquet s’appelle claude-agent-sdk (version 0.2.168 sur PyPI, publiée le 15 octobre 2026), et il embarque le binaire de Claude Code : il n’y a rien d’autre à installer. Sur une machine de développement, il reprend aussi une connexion Claude Code existante, et c’est ainsi qu’a été faite l’exécution ci-dessous. La présentation de l’Agent SDK d’Anthropic, consultée le 19 octobre 2026, indique que les produits construits sur le SDK doivent utiliser l’authentification par clé API plutôt que la connexion claude.ai : livrez donc avec ANTHROPIC_API_KEY.

Côté FastAF, c’est une seule URL : https://api.fastaf.co/mcp, en Streamable HTTP, avec votre clé dans un en-tête Bearer. Le serveur expose 15 outils ; cet agent en utilise trois. search_web et search_scholar coûtent 5 crédits chacun, scrape_page coûte 1 crédit pour une page statique. La documentation MCP liste les autres.

L’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=[],                                     # aucun outil intégré (Bash, Read, ...)
    allowed_tools=[f"mcp__fastaf__{t}" for t in ("search_web", "scrape_page", "search_scholar")],
    permission_mode="dontAsk",                    # tout ce qui n'est pas autorisé est refusé
    setting_sources=[],                           # ignorer les réglages locaux de Claude Code
    env={"ENABLE_CLAUDEAI_MCP_SERVERS": "false"}, # aucun connecteur claude.ai
    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]))

Les options qui comptent, d’après la référence du SDK Python et le guide MCP, tous deux consultés le 19 octobre 2026 :

  • Les outils MCP sont nommés mcp__<server>__<tool>, donc la clé "fastaf" donne mcp__fastaf__search_web.
  • allowed_tools ne fait qu’approuver automatiquement ; il ne masque pas les 12 autres outils de FastAF. C’est permission_mode="dontAsk" qui refuse tout ce qui ne figure pas sur la liste.
  • tools=[] retire les outils intégrés. Lors de notre première exécution, le message d’initialisation listait encore des connecteurs claude.ai (dont Gmail) issus de la connexion locale, d’où le réglage de ENABLE_CLAUDEAI_MCP_SERVERS à false.
  • Chaque résultat d’outil FastAF est un JSON qui contient credits_used et credits_remaining, si bien que le script additionne la dépense réelle au lieu de l’estimer.

Qu’a-t-il réellement fait ?

Exécution le 19 octobre 2026 avec la question « What changed in the Model Context Protocol (MCP) specification in 2026, and what does recent academic research say about MCP security risks? Cite sources. » Sortie abrégée :

-> 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

Trois appels d’outils, 11 crédits FastAF, une vingtaine de secondes et 0,0578 $ de coût de modèle selon l’estimation du SDK. Nous avons vérifié les affirmations sur la spec par rapport à la page scrapée, et les chiffres des articles par rapport aux snippets de Scholar : tout y est. L’agent a aussi signalé qu’il n’avait vu que les résumés, ce qui est exactement le comportement souhaité. Pour les questions qui reposent beaucoup sur des articles scientifiques, la page Google Scholar API décrit les champs que renvoie search_scholar.

Une précision honnête : notre clé de test partage un compte à concurrence 1 avec d’autres jobs, et les premières tentatives ont reçu RATE_LIMITED sur la plupart des appels (les appels échoués ne sont jamais facturés). Pour l’exécution ci-dessus, nous avons ajouté un second hook de débogage qui attend qu’un créneau se libère. Avec votre propre clé, vous n’en aurez pas besoin, mais lisez d’abord la section suivante.

Comment plafonner ce que dépense un agent de recherche ?

Trois niveaux, du plus souple au plus strict :

  1. max_turns borne la boucle. À 10, le SDK s’arrête avec le sous-type error_max_turns, quoi que prévoie le modèle. max_budget_usd fait de même pour le coût du modèle, même si la documentation prévient que l’estimation peut être dépassée.
  2. Un plafond d’appels d’outils dans le prompt est une préférence. Nous l’avons constaté directement : le prompt disait « one at a time », et dans plusieurs exécutions le modèle a quand même envoyé deux appels dans le même tour, une fois même trois lectures de pages d’un coup. Sur un compte Free (concurrence 1), l’appel en trop reçoit RATE_LIMITED. Le hook PreToolUse transforme le plafond en règle : avec MAX_CALLS = 1 et un prompt qui disait toujours 6, le hook a refusé trois scrapes, l’agent a répondu à partir de sa seule recherche, et l’exécution a coûté 5 crédits.
  3. Le solde du compte est le plafond strict. Chaque appel réserve son coût à l’avance, et un solde vide renvoie 402 insufficient_credits. Le solde appartient au compte et il est partagé par toutes ses clés : une clé séparée ne vous donne donc pas un budget séparé. Approvisionnez le compte avec ce que vous êtes prêt à perdre dans une boucle qui dérape.

L’article sur le plafond de dépense approfondit le côté wrapper, et le grounding avec une API SERP couvre le même schéma chercher-puis-lire sans boucle d’agent.

En bref

Pointez mcp_servers vers https://api.fastaf.co/mcp avec un en-tête Bearer, autorisez trois outils, réglez permission_mode="dontAsk" et ajoutez un hook PreToolUse qui compte les appels. Une réponse sourcée sur un sujet de 2026 a coûté 11 crédits, soit environ 1 800 réponses de ce type avec un pack prépayé Starter à 10 $. La page tarifs donne le coût de chaque outil, et l’inscription vous offre 500 crédits gratuits pour le lancer vous-même.