Agente Bio-LLM MCP: Cómo orquestar autónomamente herramientas experimentales utilizando el Protocolo de Contexto del Modelo (MCP)
Este es el último capítulo del piloto de la Fase 4. En los capítulos anteriores (01 IE clínico, 02 incrustaciones de proteínas, 03 segmentación celular, 11 estructura y afinidad de Boltz-2), construimos las tuberías principales para cada dominio y flujo de trabajo. Sin embargo, la I+D real no termina con una sola tubería: se leen artículos, se buscan secuencias, se encuentran estructuras, se diseñan experimentos, se reproducen resultados y se diagnostican las causas del fracaso. En este capítulo, construiremos un flujo de trabajo práctico donde el agente de Claude orquesta autónomamente esta orquestación multi-herramienta utilizando un protocolo estandarizado (MCP).
📚 **Prerrequisitos esenciales (altamente recomendados)
Este capítulo es el último del track avanzado de IA×Biotecnología. Antes de comenzar, se recomienda ver primero los siguientes capítulos de DryBench.
- DryBench ai-native #9 Agentes y uso de herramientas
- DryBench ai-native #10 Gestión de la ventana de contexto
- DryBench ai-native #14 Claude Code y Cursor
Si se avanza sin los prerrequisitos, será difícil seguir el contenido, ya que se abordará código práctico directamente sin explicaciones adicionales sobre los principios del bucle de agentes, las estrategias de compresión/resumen del contexto o los patrones de uso de herramientas de Claude Code.
Lo aprendido en DryBench
En DryBench ai-native #9 aprendimos que un agente se define por cuatro elementos: LLM + herramientas + bucle + memoria, y que cada llamada a herramienta se gestiona a través de un esquema obligatorio similar a una firma de función. En el capítulo #10 vimos cómo se comprime y descarga el contexto durante sesiones largas, y en el #14 aprendimos que Claude Code es la herramienta que implementa estos principios de manera práctica desde la CLI.
Sin embargo, en la investigación y el desarrollo biotecnológico reales, definir y codificar manualmente cada herramienta, junto con la autenticación, los reintentos, los tiempos de espera y el análisis de respuestas para cada proyecto, es extremadamente ineficiente. El Protocolo de Contexto del Modelo (MCP), anunciado por Anthropic en noviembre de 2024, es un protocolo abierto diseñado para estandarizar esta repetición [1]. Un servidor MCP expone el mismo conjunto de herramientas a múltiples clientes (Claude Desktop, Claude Code, Cursor, etc.), y las especificaciones para que el servidor y el cliente se descubran, invoquen y procesen respuestas son consistentes. Este artículo aplica esta especificación al dominio biológico en un contexto práctico.
Definición del problema central
Escenario de I+D real
Un investigador ha obtenido un nuevo candidato a gen relacionado con una enfermedad específica. Para determinar si este gen es realmente interesante, debe realizar como mínimo las siguientes tareas:
- Enviar la secuencia del gen a NCBI BLAST para encontrar homólogos.
- Consultar la información de UniProt del resultado principal para verificar su función.
- Buscar en PubMed los artículos relacionados de los últimos 5 años.
- Leer la sección de métodos del artículo principal y evaluar si es reproducible.
- Si es reproducible, clonar el repositorio de GitHub → configurar el entorno → adaptar sus propios datos → validar los resultados.
El enfoque tradicional requería que una persona navegara entre el navegador, la terminal y el editor para gestionar cada paso. El objetivo de este artículo es que un Agente Claude ejecute autónomamente estas 5 etapas completas y devuelva un informe estructurado con los resultados. Durante este proceso, el humano solo interviene en momentos decisivos (por ejemplo: "¿Intentaré reproducir los métodos de este artículo?").
¿Por qué se necesita MCP?
Sin MCP, es posible implementar este flujo de trabajo utilizando únicamente el uso de herramientas. Sin embargo, con MCP se obtiene:
- Reutilización: Si se crea un servidor para NCBI BLAST una vez, todos los clientes MCP (Claude Desktop, Claude Code, Cursor, etc.) pueden usarlo inmediatamente.
- Especificaciones estándar de autenticación, errores y carga útil: Basado en JSON-RPC 2.0. No es necesario reinventar la rueda para cada dominio.
- Ecosistema: Combina servidores MCP creados por otros (por ejemplo, sistema de archivos, GitHub, Slack) para construir agentes compuestos de inmediato.
- Procesos aislados: Los servidores se comunican mediante stdin/stdout o SSE; un fallo del servidor no afecta al cliente.
Objetivos de este artículo
- Ejecución autónoma de las 5 etapas anteriores en una sola sesión de Claude: Con menos de 3 intervenciones humanas.
- Servidores MCP para NCBI BLAST, UniProt y PubMed: Utilizando implementaciones de la comunidad abierta o implementaciones mínimas personalizadas.
- Recuperación de errores: Reintentos automáticos con retroceso exponencial ante límites de tasa, fallos de red o errores de análisis.
- Reproducibilidad: Los registros de sesión, el historial de llamadas a herramientas y el informe final se guardan en formato JSON, lo que permite su verificación posterior.
Conjunto de herramientas e infraestructura requerida
| Herramienta | Función | Licencia |
|---|---|---|
mcp SDK de Python (pip install mcp) | Implementación del estándar MCP (servidor y cliente) | MIT (oficial de Anthropic) |
API de Anthropic Claude (anthropic) | Orquestador de LLM y uso de herramientas | Comercial (se cobra por uso) |
| Biopython | Interfaz para E-utilities de NCBI | Licencia Biopython |
requests | Cliente REST de UniProt | Apache 2.0 |
| Docker (opcional) | Implementación del servidor MCP mediante contenedores | Apache 2.0 |
| Anthropic Skills (opcional) | Registro de habilidades específicas del dominio | Servicios de Anthropic |
Requisitos de infraestructura:
- Se puede ejecutar sin GPU (el LLM se accede a través de una API, las herramientas son servicios web).
- CPU local de 2 núcleos y RAM superior a 4 GB.
- Red: llamadas a las API de NCBI, UniProt y PubMed (es obligatorio respetar los límites de frecuencia).
Costo estimado para la reproducción por parte del estudiante: Según el cálculo basado en la tabla de tarifas de Claude API [2], completar un escenario cuesta aproximadamente entre 5 y 20 USD (dependiendo del número de llamadas repetidas a las herramientas). No hay costos adicionales al cumplir con cada API pública (NCBI 3 solicitudes/segundo, UniProt sin límite específico, PubMed gratuito por defecto).
Implementación práctica del flujo de trabajo
Flujo completo:
Paso 1. Implementación mínima del servidor MCP: ejemplo con la API REST de UniProt
El servidor MCP es un proceso que se comunica a través de stdin/stdout utilizando JSON-RPC 2.0. El SDK de Python se encarga de la mayor parte del código repetitivo [3].
"""uniprot_mcp_server.py — Exponer la API REST de UniProt como herramienta MCP.
Un servidor MCP puede exponer varias herramientas. Aquí hay 3:- search_protein: Búsqueda de proteínas mediante consulta de texto- get_protein_details: Detalles de información por accession- get_sequence: obtener secuencia FASTA por accession"""import asyncioimport jsonfrom typing import Any
import requestsfrom mcp.server import Serverfrom mcp.server.stdio import stdio_serverfrom mcp.types import Tool, TextContent
UNIPROT_BASE = "https://rest.uniprot.org"server = Server("uniprot-mcp")
@server.list_tools()async def list_tools() -> list[Tool]: return [ Tool( name="search_protein", description="Buscar proteínas en UniProt mediante consulta de texto. Devuelve los K primeros resultados.", inputSchema={ "type": "object", "properties": { "query": {"type": "string", "description": "Término de búsqueda (por ejemplo, 'p53 human')"}, "limit": {"type": "integer", "default": 10}, }, "required": ["query"], }, ), Tool( name="get_protein_details", description="Obtener información detallada de la proteína por accession de UniProt.", inputSchema={ "type": "object", "properties": {"accession": {"type": "string"}}, "required": ["accession"], }, ), Tool( name="get_sequence", description="Devuelve la secuencia FASTA del accession de UniProt.", inputSchema={ "type": "object", "properties": {"accession": {"type": "string"}}, "required": ["accession"], }, ), ]
@server.call_tool()async def call_tool(name: str, arguments: dict[str, Any]) -> list[TextContent]: """Despacho por nombre de herramienta. En producción, se recomienda separar cada función.""" try: if name == "search_protein": result = _search_protein(arguments["query"], arguments.get("limit", 10)) elif name == "get_protein_details": result = _get_details(arguments["accession"]) elif name == "get_sequence": result = _get_sequence(arguments["accession"]) else: raise ValueError(f"Unknown tool: {name}") return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))] except requests.RequestException as e: return [TextContent(type="text", text=json.dumps({"error": f"Error en la API de UniProt: {e}"}))] except Exception as e: return [TextContent(type="text", text=json.dumps({"error": str(e)}))]
def _search_protein(query: str, limit: int) -> dict: resp = requests.get( f"{UNIPROT_BASE}/uniprotkb/search", params={ "query": query, "format": "json", "size": limit, "fields": "accession,id,protein_name,organism_name,length", }, timeout=30, ) resp.raise_for_status() results = resp.json().get("results", []) return {"count": len(results), "results": results}
def _get_details(accession: str) -> dict: resp = requests.get( f"{UNIPROT_BASE}/uniprotkb/{accession}.json", timeout=30, ) resp.raise_for_status() data = resp.json() return { "accession": data.get("primaryAccession"), "protein_name": data.get("proteinDescription", {}), "organism": data.get("organism", {}), "length": data.get("sequence", {}).get("length"), "function": [ c["texts"][0]["value"] for c in data.get("comments", []) if c.get("commentType") == "FUNCTION" ], }
def _get_sequence(accession: str) -> dict: resp = requests.get(f"{UNIPROT_BASE}/uniprotkb/{accession}.fasta", timeout=30) resp.raise_for_status() lines = resp.text.strip().split("\n") return { "accession": accession, "header": lines[0], "sequence": "".join(lines[1:]), }
async def main(): async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__": asyncio.run(main())Paso 2. Segundo servidor MCP: búsqueda en PubMed.
"""pubmed_mcp_server.py — PubMed E-utilities MCP tool."""import asyncioimport jsonfrom xml.etree import ElementTree as ET
import requestsfrom mcp.server import Serverfrom mcp.server.stdio import stdio_serverfrom mcp.types import Tool, TextContent
EUTILS_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"server = Server("pubmed-mcp")
@server.list_tools()async def list_tools() -> list[Tool]: return [ Tool( name="search_pubmed", description="Obtener lista de artículos (PMID) de PubMed mediante término de búsqueda.", inputSchema={ "type": "object", "properties": { "query": {"type": "string"}, "max_results": {"type": "integer", "default": 20}, "date_range_years": {"type": "integer", "default": 5}, }, "required": ["query"], }, ), Tool( name="fetch_abstracts", description="Obtener información detallada de resúmenes, autores y revistas a partir de una lista de PMIDs.", inputSchema={ "type": "object", "properties": { "pmids": {"type": "array", "items": {"type": "string"}}, }, "required": ["pmids"], }, ), ]
@server.call_tool()async def call_tool(name: str, arguments: dict) -> list[TextContent]: try: if name == "search_pubmed": result = _search(arguments["query"], arguments.get("max_results", 20), arguments.get("date_range_years", 5)) elif name == "fetch_abstracts": result = _fetch(arguments["pmids"]) else: raise ValueError(f"Unknown tool: {name}") return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))] except Exception as e: return [TextContent(type="text", text=json.dumps({"error": str(e)}))]
def _search(query: str, max_results: int, date_range_years: int) -> dict: esearch = requests.get( f"{EUTILS_BASE}/esearch.fcgi", params={ "db": "pubmed", "term": query, "retmax": max_results, "reldate": date_range_years * 365, "datetype": "pdat", "retmode": "json", }, timeout=30, ) esearch.raise_for_status() pmids = esearch.json().get("esearchresult", {}).get("idlist", []) return {"query": query, "count": len(pmids), "pmids": pmids}
def _fetch(pmids: list[str]) -> list[dict]: if not pmids: return [] efetch = requests.get( f"{EUTILS_BASE}/efetch.fcgi", params={"db": "pubmed", "id": ",".join(pmids), "rettype": "abstract", "retmode": "xml"}, timeout=60, ) efetch.raise_for_status() root = ET.fromstring(efetch.content) articles = [] for art in root.findall(".//PubmedArticle"): pmid = art.findtext(".//PMID") title = art.findtext(".//ArticleTitle") or "" abstract = " ".join(t.text or "" for t in art.findall(".//AbstractText")) authors = [ f"{a.findtext('LastName') or ''} {a.findtext('Initials') or ''}".strip() for a in art.findall(".//Author") ] journal = art.findtext(".//Journal/Title") or "" year = art.findtext(".//PubDate/Year") or "" articles.append({ "pmid": pmid, "title": title, "abstract": abstract, "authors": authors[:10], "journal": journal, "year": year, }) return articles
async def main(): async with stdio_server() as (read_stream, write_stream): await server.run(read_stream, write_stream, server.create_initialization_options())
if __name__ == "__main__": asyncio.run(main())Paso 3. Resumen del servidor MCP de NCBI BLAST
Dado que los tiempos de respuesta de BLAST son prolongados, es necesario implementar un patrón de sondeo asíncrono.
"""blast_mcp_server.py — NCBI BLAST QBlast (versión resumida, solo lógica esencial)."""import timeimport reimport requests
BLAST_URL = "https://blast.ncbi.nlm.nih.gov/Blast.cgi"
def submit_blast(sequence: str, program: str = "blastp", database: str = "nr") -> str: """Devuelve el RID de QBlast. Requiere polling posterior.""" resp = requests.post(BLAST_URL, data={ "CMD": "Put", "PROGRAM": program, "DATABASE": database, "QUERY": sequence, }, timeout=60) resp.raise_for_status() m = re.search(r"RID = (\S+)", resp.text) if not m: raise RuntimeError("Error al analizar el RID de QBlast") return m.group(1)
def poll_blast(rid: str, poll_interval: int = 30, max_wait: int = 600) -> str: """Esperar resultado de BLAST. Devuelve XML al completarse.""" start = time.time() while time.time() - start < max_wait: resp = requests.get(BLAST_URL, params={"CMD": "Get", "RID": rid, "FORMAT_OBJECT": "SearchInfo"}, timeout=30) if "Status=READY" in resp.text: xml_resp = requests.get(BLAST_URL, params={"CMD": "Get", "RID": rid, "FORMAT_TYPE": "XML"}, timeout=60) xml_resp.raise_for_status() return xml_resp.text elif "Status=FAILED" in resp.text: raise RuntimeError(f"Fallo de BLAST RID={rid}") time.sleep(poll_interval) raise TimeoutError(f"BLAST timeout RID={rid}")
# El wrapper de herramienta MCP sigue el mismo patrón que los servidores UniProt y PubMed anteriores. Omiso por espacio.Paso 4. Cliente de Claude Agent: Orquestación de MCP
Claude Agent se conecta simultáneamente a varios servidores MCP para invocar herramientas de forma autónoma.
"""bio_agent.py — Agente Claude + orquestación de múltiples servidores MCP."""import asynciofrom contextlib import AsyncExitStack
import anthropicfrom mcp import ClientSession, StdioServerParametersfrom mcp.client.stdio import stdio_client
class BioMCPAgent: """Agente Claude que coordina múltiples servidores MCP."""
def __init__(self, model: str = "claude-sonnet-4-5", max_turns: int = 20): self.anthropic = anthropic.Anthropic() self.model = model self.max_turns = max_turns self.sessions: dict[str, ClientSession] = {} self.all_tools: list[dict] = [] self._exit_stack: AsyncExitStack | None = None
async def connect_server(self, name: str, command: str, args: list[str]) -> None: """Conecta un servidor MCP. Inicia proceso y obtiene lista de herramientas.""" params = StdioServerParameters(command=command, args=args) stdio_transport = await self._exit_stack.enter_async_context(stdio_client(params)) session = await self._exit_stack.enter_async_context(ClientSession(*stdio_transport)) await session.initialize() tools_result = await session.list_tools() # Convertir al esquema de uso de herramienta de Claude for tool in tools_result.tools: self.all_tools.append({ "name": f"{name}__{tool.name}", # Prefijo del nombre del servidor para evitar conflictos "description": tool.description, "input_schema": tool.inputSchema, }) self.sessions[f"{name}__{tool.name}"] = session
async def __aenter__(self): self._exit_stack = AsyncExitStack() await self._exit_stack.__aenter__() return self
async def __aexit__(self, exc_type, exc_val, exc_tb): await self._exit_stack.__aexit__(exc_type, exc_val, exc_tb)
async def run(self, user_query: str, system_prompt: str) -> dict: """Bucle autónomo. Claude repite hasta que stop_reason='end_turn'.""" messages = [{"role": "user", "content": user_query}] tool_log = []
for turn in range(self.max_turns): response = self.anthropic.messages.create( model=self.model, max_tokens=4096, system=system_prompt, tools=self.all_tools, messages=messages, ) messages.append({"role": "assistant", "content": response.content})
if response.stop_reason == "end_turn": break if response.stop_reason != "tool_use": continue
# Procesar bloque tool_use tool_results = [] for block in response.content: if block.type != "tool_use": continue session = self.sessions.get(block.name) if session is None: tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": f"Unknown tool: {block.name}", "is_error": True, }) continue try: # Eliminar prefijo del nombre del servidor MCP y llamar con el nombre original de la herramienta original_name = block.name.split("__", 1)[1] result = await session.call_tool(original_name, block.input) content_text = "\n".join( c.text for c in result.content if hasattr(c, "text") ) tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": content_text[:10000], # Ahorro de contexto }) tool_log.append({ "tool": block.name, "input": block.input, "output_preview": content_text[:500], }) except Exception as e: tool_results.append({ "type": "tool_result", "tool_use_id": block.id, "content": f"Error al llamar a la herramienta: {e}", "is_error": True, }) messages.append({"role": "user", "content": tool_results})
# Extraer solo el texto final final_text = "" for block in response.content: if hasattr(block, "text"): final_text += block.text return {"final_answer": final_text, "tool_log": tool_log, "turns": turn + 1}
SYSTEM_PROMPT = """Eres un asistente de investigación en bioinformática.Comprende las solicitudes del usuario y llama autónomamente a las herramientas MCP necesarias (uniprot__*, pubmed__*, blast__*)para responder basándote en evidencia.
Principios:1. No fabriques hechos. Todas las afirmaciones deben estar respaldadas por los resultados de las llamadas a herramientas.2. Incluye siempre las fuentes (acceso de UniProt, PMID, etc.) junto con la respuesta.3. Si falla la llamada a la herramienta, intenta una ruta alternativa y también informa al usuario sobre el fallo.4. Minimiza las llamadas innecesarias a herramientas (ahorro de contexto y costos)."""
async def main(): async with BioMCPAgent() as agent: await agent.connect_server("uniprot", "python", ["uniprot_mcp_server.py"]) await agent.connect_server("pubmed", "python", ["pubmed_mcp_server.py"]) # await agent.connect_server("blast", "python", ["blast_mcp_server.py"])
result = await agent.run( user_query="Resume los principales hallazgos de los 3 artículos más destacados de los últimos 5 años sobre TP53 humano, " "y compáralo con las anotaciones funcionales de UniProt de esta proteína para indicar si hay nuevas perspectivas.", system_prompt=SYSTEM_PROMPT, ) print("=== Final Answer ===") print(result["final_answer"]) print(f"\n=== Turns: {result['turns']} ===") print(f"=== Tool calls: {len(result['tool_log'])} ===") for entry in result["tool_log"]: print(f" - {entry['tool']}: {entry['input']}")
if __name__ == "__main__": asyncio.run(main())Paso 5. Integración de la herramienta para reproducir artículos (sustitución de P-02)
Se ha decidido integrar P-02 (agente para la reproducción de artículos) en este capítulo, según se indica en la sección §7-1. La herramienta de reproducción se proporcionará a través de un servidor MCP independiente.
"""paper_repro_mcp_server.py — Conjunto de herramientas para reproducir artículos (resumen)."""
# herramienta 1: fetch_paper_pdf — Obtención de PDF mediante DOI (prioridad a acceso abierto)# herramienta 2: extract_method_section — Extracción del texto de la sección de métodos desde el PDF# herramienta 3: find_github_repo — Búsqueda de URL de GitHub en el cuerpo del artículo y material complementario# herramienta 4: clone_repo — git clone (en carpeta sandbox)# herramienta 5: setup_env — Detección automática de requirements.txt y entorno conda · Intento de instalación# herramienta 6: run_reproduction — Ejecución del punto de entrada estándar del repositorio (README, setup.py)
# Cada herramienta sigue el mismo patrón MCP que los servidores UniProt y PubMed anteriores.# Por seguridad, la clonación y ejecución deben realizarse dentro de un contenedor aislado (sandbox Docker).Al agregar esta herramienta de servidor a BioMCPAgent mediante connect_server("paper_repro", ...), se completa el ciclo de ejecución autónoma para la reproducción de artículos científicos.
Integración de Anthropic Skills (mejora opcional)
Anthropic Skills permite registrar conjuntos de herramientas específicos del dominio en la familia Claude Sonnet [4]. Al empaquetar la combinación anterior de servidores MCP como una habilidad de "Investigación biológica", se puede activar inmediatamente en Claude Desktop o en la CLI de Claude Code.
# skills/bio-research/skill.yaml
name: bio-research
version: 1.0.0
description: Agente autónomo de investigación en bioinformática. Integración de UniProt, PubMed, BLAST y reproducción de artículos.
mcp_servers:
- name: uniprot
command: python
args: [/opt/mcp/uniprot_mcp_server.py]
- name: pubmed
command: python
args: [/opt/mcp/pubmed_mcp_server.py]
- name: blast
command: python
args: [/opt/mcp/blast_mcp_server.py]
- name: paper_repro
command: python
args: [/opt/mcp/paper_repro_mcp_server.py]
system_prompt: |
Usted es un asistente de investigación en bioinformática. ...Rendimiento, costo y casos de fallo conocidos
Referencias de rendimiento (con citas de fuentes públicas)
| Enfoque | Escenario | Tiempo | Costo | Fuente |
|---|---|---|---|---|
| Manual humano (navegando en el navegador) | Investigación genética de 5 niveles | 1~3 horas | 0 | Línea base experimental |
| Claude Agent + uso de herramientas (sin MCP) | Igual | 5~10 minutos | 3~10 USD | Benchmark de uso de herramientas de Anthropic [5] |
| Claude Agent + MCP (este artículo) | Igual | 3~7 minutos | 3~10 USD | Evidencia del artículo sobre MCPmed [6] |
| GPT-4o + llamada de funciones | Igual | 5~10 minutos | 5~15 USD | Documentación de OpenAI [7] |
Costos estimados para la reproducción por parte del estudiante
- API de Claude: 5~20 USD por escenario (dependiendo del número de interacciones, repeticiones de herramientas y tamaño de los resultados).
- Infraestructura del servidor MCP: Gratuito para ejecución local. Para implementación remota, VPS pequeño por 5~10 USD mensuales.
- NCBI · UniProt · PubMed: Gratuitos (cumplir con los límites de frecuencia).
3 casos de fallo conocidos (recopilados de la comunidad y literatura)
-
Exceso del límite de frecuencia de NCBI BLAST Síntoma: Al llamar a BLAST QBlast varias veces en un corto período, se obtiene 429 o bloqueo temporal. Causa: La política de NCBI establece un límite superior de 3 solicitudes por segundo (10 req/s con registro de clave API). Solución: (a)
time.sleep(1)intervalo mínimo, (b) registrar clave API de NCBI + pasar parámetroapi_key, (c) se recomienda ejecutar búsquedas por lotes durante la noche y fines de semana. Fuente: Documentación de E-utilities de NCBI "directrices de uso" [8]. -
Colisión de nombres de herramientas MCP Síntoma: Si varios servidores MCP exponen el mismo nombre de herramienta, el cliente no sabe cuál llamar. Causa: Según la especificación MCP, la unicidad del nombre de la herramienta solo está garantizada dentro del servidor. Solución: Envolver nuevamente con prefijo
{server_name}__{tool_name}en el cliente, como se muestra en el código anterior. Fuente: Discusiones de GitHub de MCP — "colisión de nombres de herramientas" [9]. -
Bucle infinito de llamadas incorrectas a herramientas por Claude Agent Síntoma: En ciertas tareas, el Agent sigue llamando repetidamente a la misma herramienta sin alcanzar
stop_reason="end_turn". Causa: (a) El mensaje del sistema no especifica claramente cuándo debe detenerse, (b) las respuestas de las herramientas siempre son parciales y ambiguas, (c) no hay un límite superior paramax_turns. Solución: (a) Establecer un límite máximo fijo demax_turns(este código usa 20), (b) Indicar en el mensaje del sistema que se informe al usuario después de 3 intentos fallidos, (c) Resumir el historial de llamadas a las herramientas en el contexto para detectar repeticiones. Fuente: Anthropic Cookbook: patrones de bucles de agentes [10].
Ideas para ampliar
- Serie de servidores MCP personalizados: Obtener archivos de estructura PDB, buscar compuestos en ChEMBL, datos de transcriptómica GEO, variantes Ensembl, etc. Continuará en el episodio 15.
- Colaboración entre múltiples agentes: Separar el agente de investigación, el agente de visualización y el agente de generación de informes en sesiones distintas y compartir los resultados mediante MCP.
- Implementación de servidores MCP remotos: Mantener servidores MCP activos de forma continua en Cloudflare Workers, AWS Lambda o Docker Swarm para que varios usuarios puedan acceder a ellos.
- Integración con laboratorios: Exponer la API de equipos de laboratorio (por ejemplo, robots manipuladores de líquidos) como herramientas MCP para ejecutar automáticamente los planes de experimentación.
Próximo episodio
- Episodio 15
bio-mcp-server-suite: Ampliar estos 3 servidores para crear una serie de 5 a 7 servidores MCP personalizados con PDB, ChEMBL, GEO, etc. - Episodio 09
llm-vendor-benchmark: Aplicar el mismo conjunto de herramientas MCP a Claude, GPT-4o y Gemini, y comparar la precisión en el uso de las herramientas. - Episodio 10
med-llm-reproduction: Automatizar la reproducción de pruebas de referencia basadas en MCP. - Episodio 01
clinical-notes-ie-llm: Exponer el flujo de trabajo de extracción de información clínica del episodio 01 como una herramienta MCP, de modo que el agente de este episodio pueda invocarla de forma autónoma.
Referencias
- Anthropic. "Model Context Protocol". Documentación oficial:
https://modelcontextprotocol.io// Blog de presentación:https://www.anthropic.com/news/model-context-protocol - Precios de la API de Anthropic Claude:
https://www.anthropic.com/pricing - SDK de Python para MCP:
https://github.com/modelcontextprotocol/python-sdk - Documentación de Anthropic Skills:
https://docs.anthropic.com/en/docs/build-with-claude/skills - Anthropic Tool Use documentation:
https://docs.anthropic.com/en/docs/agents-and-tools/tool-use/overview - Wagner F, Bittrich S. "MCPmed: a call for Model Context Protocol-enabled bioinformatics web services for LLM-driven discovery." Briefings in Bioinformatics 2026.
https://academic.oup.com/bib/article/27/1/bbag076/8495038 - OpenAI function calling:
https://platform.openai.com/docs/guides/function-calling - NCBI E-utilities usage guidelines:
https://www.ncbi.nlm.nih.gov/books/NBK25497/ - MCP GitHub Discussions:
https://github.com/modelcontextprotocol/specification/discussions - Anthropic Cookbook (agent patterns):
https://github.com/anthropics/anthropic-cookbook - UniProt REST API:
https://www.uniprot.org/help/api - NCBI BLAST QBlast:
https://ncbi.github.io/blast-cloud/dev/api.html - Biopython E-utilities wrapper:
https://biopython.org/wiki/EUtils - MCP official server registry:
https://github.com/modelcontextprotocol/servers - Claude Code (Anthropic official):
https://claude.com/product/claude-code