Crea un agente de investigación web con el Claude Agent SDK y un presupuesto estricto

Unas 60 líneas de Python: Claude Agent SDK más el servidor MCP de FastAF para búsqueda, lectura de páginas y Google Scholar. Una ejecución real costó 11 créditos.

Un agente de investigación necesita tres cosas: una forma de encontrar fuentes, una forma de leerlas y un límite a lo que puede gastar haciendo ambas. El Claude Agent SDK te da el bucle. El servidor MCP alojado de FastAF te da las tools. Este post los conecta en unas 60 líneas de Python, los ejecuta sobre una pregunta real y muestra lo que costó.

¿Qué necesitas antes de empezar?

Python 3.10 o superior, el SDK y dos claves:

python3 -m venv .venv && source .venv/bin/activate
pip install claude-agent-sdk
export ANTHROPIC_API_KEY=...   # según el quickstart del Agent SDK
export FASTAF_API_KEY=...      # desde tu dashboard de FastAF

El paquete es claude-agent-sdk (versión 0.2.168 en PyPI, publicada el 15 de octubre de 2026), e incluye el binario de Claude Code, así que no hay nada más que instalar. En una máquina de desarrollo también aprovecha un login de Claude Code ya existente, que es como se hizo la ejecución de abajo. La descripción general del Agent SDK de Anthropic, consultada el 19 de octubre de 2026, dice que los productos construidos sobre el SDK deben usar autenticación con API key y no el login de claude.ai, así que en producción usa ANTHROPIC_API_KEY.

La parte de FastAF es una sola URL: https://api.fastaf.co/mcp, Streamable HTTP, con tu clave como cabecera Bearer. Expone 15 tools; este agente usa tres. search_web y search_scholar cuestan 5 créditos cada una, scrape_page cuesta 1 crédito para una página estática. La documentación de MCP recoge el resto.

El agente

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=[],                                     # sin tools integradas (Bash, Read, ...)
    allowed_tools=[f"mcp__fastaf__{t}" for t in ("search_web", "scrape_page", "search_scholar")],
    permission_mode="dontAsk",                    # se deniega todo lo que no está permitido
    setting_sources=[],                           # ignora la configuración local de Claude Code
    env={"ENABLE_CLAUDEAI_MCP_SERVERS": "false"}, # sin conectores de 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]))

Las opciones que importan, según la referencia del SDK de Python y la guía de MCP, ambas consultadas el 19 de octubre de 2026:

  • Las tools MCP se llaman mcp__<server>__<tool>, así que la clave "fastaf" da mcp__fastaf__search_web.
  • allowed_tools solo aprueba automáticamente; no oculta las otras 12 tools de FastAF. Lo que deniega todo lo que no está en la lista es permission_mode="dontAsk".
  • tools=[] quita las tools integradas. En nuestra primera ejecución, el mensaje de inicio seguía listando conectores de claude.ai (Gmail entre ellos) procedentes del login local, y por eso ENABLE_CLAUDEAI_MCP_SERVERS está en false.
  • Cada resultado de tool de FastAF es un JSON con credits_used y credits_remaining, así que el script suma el gasto real en lugar de estimarlo.

¿Qué hizo en realidad?

Ejecutado el 19 de octubre de 2026 con la pregunta “What changed in the Model Context Protocol (MCP) specification in 2026, and what does recent academic research say about MCP security risks? Cite sources.” Salida recortada:

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

Tres llamadas a tools, 11 créditos de FastAF, unos 20 segundos y 0,0578 $ de coste de modelo según la estimación del SDK. Contrastamos lo que dice sobre la spec con la página descargada y las cifras de los papers con los snippets de Scholar: todo está ahí. El agente además avisó de que solo había visto los resúmenes, que es justo el comportamiento que quieres. Para preguntas con muchos papers, la página de la Google Scholar API describe los campos que devuelve search_scholar.

Una nota honesta: nuestra clave de pruebas comparte una cuenta con concurrencia 1 con otros jobs, y en los primeros intentos la mayoría de las llamadas recibieron RATE_LIMITED (las llamadas fallidas nunca se cobran). Para la ejecución de arriba añadimos un segundo hook de depuración que espera a que quede un hueco libre. Con tu propia clave no lo necesitarás, pero lee antes la siguiente sección.

¿Cómo limitas lo que gasta un agente de investigación?

Tres capas, de la más blanda a la más dura:

  1. max_turns acota el bucle. Con 10, el SDK se detiene con el subtipo error_max_turns planee lo que planee el modelo. max_budget_usd hace lo mismo con el coste del modelo, aunque la documentación advierte que la estimación puede pasarse.
  2. Un límite de llamadas en el prompt es una preferencia. Lo vimos de primera mano: el prompt decía “one at a time”, y en varias ejecuciones el modelo aun así envió dos llamadas en el mismo turno, una vez tres lecturas de página a la vez. En una cuenta Free (concurrencia 1) la llamada de más recibe RATE_LIMITED. El hook PreToolUse convierte el límite en una regla: con MAX_CALLS = 1 y el prompt todavía diciendo 6, el hook denegó tres scrapes, el agente respondió con su única búsqueda y la ejecución costó 5 créditos.
  3. El saldo de la cuenta es el tope duro. Cada llamada reserva su coste por adelantado, y un saldo vacío devuelve 402 insufficient_credits. El saldo pertenece a la cuenta y lo comparten todas sus claves, así que una clave aparte no te da un presupuesto aparte. Carga la cuenta con lo que estés dispuesto a perder en un bucle descontrolado.

El post sobre el límite de gasto profundiza en la parte del wrapper, y grounding con una SERP API cubre el mismo patrón de buscar y luego leer sin un bucle de agente.

En resumen

Apunta mcp_servers a https://api.fastaf.co/mcp con una cabecera Bearer, permite tres tools, fija permission_mode="dontAsk" y añade un hook PreToolUse que cuente las llamadas. Una respuesta con citas sobre un tema de 2026 costó 11 créditos, lo que da para unas 1.800 respuestas así con un pack Starter de 10 $. En precios tienes el coste de cada tool, y si te registras obtienes 500 créditos gratis para ejecutarlo tú mismo.