Volver a la lista

Serie de servidores MCP personalizados: implemente cinco servicios web de bioinformática como una solución para el despliegue en equipo mediante un protocolo estándar.

Episodio 14 ampliado. Se implementan cinco servicios web de bioinformática (PubMed, UniProt, PDB, ChEMBL y BLAST) como servidores MCP completos, y se abordan temas de infraestructura avanzada como la contenedorización con Docker, el transporte SSE, la autenticación, el límite de velocidad, la observabilidad, el registro de habilidades de Anthropic y el despliegue en equipo. Infraestructura estándar para la orquestación autónoma de agentes de IA.

Avanzado
|
50min
|
Verificado (2026-07)
Progreso0/15 (0%)

Serie de servidores MCP personalizados: encapsular cinco servicios web de bio para el despliegue en equipo mediante un protocolo estándar

En la parte 14, vimos las especificaciones básicas del Protocolo de contexto del modelo Anthropic (MCP) y la canalización mediante la cual un agente Bio-LLM orquesta de forma autónoma varios servidores MCP. Esta parte es una extensión de lo anterior. Se trata de una parte práctica de la infraestructura que implementa cinco servicios web de bio necesarios para la investigación y el desarrollo reales (PubMed, UniProt, PDB, ChEMBL y BLAST) como servidores MCP completos, y que abarca la contenedorización de Docker, el transporte SSE (Eventos enviados por el servidor), la autenticación, el límite de velocidad, el manejo de errores, el registro de observabilidad, el registro de habilidades de Anthropic y el despliegue en equipo. Esta parte es la última de la ampliación de la serie y el punto álgido de la infraestructura.

📚 Recomendación de partes previas (muy recomendable)

Esta parte es el punto álgido de la infraestructura de la serie avanzada de IA y bio. Le recomendamos encarecidamente que vea y escuche primero las siguientes partes de DryBench antes de empezar.

Si empieza sin haber visto las partes previas, le resultará difícil seguir el ritmo, ya que esta parte avanza directamente al código práctico sin volver a explicar la orquestación de herramientas de agentes, las estrategias de ahorro de contexto y la implementación práctica de Claude Code CLI.


Ya aprendimos esto en DryBench

En DryBench ai-native #9, aprendimos que un agente puede procesar de forma autónoma tareas complejas mediante un bucle de llamadas a herramientas, y en #10, aprendimos sobre el problema de que los resultados de las llamadas a herramientas consuman el contexto y las estrategias de resumen y descarga. En #14, vimos que Claude Code es una herramienta que implementa estos principios en la práctica a través de la CLI.

MCP es la estandarización de la infraestructura de estos principios. En lugar de definir herramientas de nuevo en cada proyecto individual, se crea un servidor MCP común para que los equipos, las organizaciones y la comunidad de código abierto lo reutilicen. Esta parte es una guía práctica para crear esta infraestructura y desplegarla a nivel de producción.

Definición del problema práctico

Requisitos prácticos de la serie de servidores MCP

Cuando varios equipos de una organización de investigación biológica llevan a cabo diferentes proyectos, tener un conjunto común de servidores MCP puede:

  • Reutilización: Un equipo crea un servidor UniProt MCP y otros equipos lo utilizan inmediatamente.
  • Consistencia: Varios proyectos comparten el mismo esquema de herramientas, manejo de errores y políticas de registro.
  • Despliegue aislado: El servidor MCP es un proceso independiente, lo que aísla los fallos del cliente.
  • Escalabilidad: Se pueden utilizar inmediatamente servidores MCP de código abierto (oficiales de Anthropic y de la comunidad).
  • Seguridad: Integración de autenticación, limitación de velocidad y registro de auditoría.
  • Observabilidad: Paneles de control de latencia, tasa de éxito y uso.

Objetivos de este proyecto

  • Implementación completa de 5 servidores MCP: PubMed, UniProt, PDB, ChEMBL y BLAST.
  • Exposición de 4 a 6 herramientas por servidor.
  • Clase base común: Reutilización de la limitación de velocidad, reintentos, registro y manejo de errores.
  • Contenerización con Docker: Cada servidor tiene su propia imagen y se ejecuta de forma integrada con docker-compose.
  • Opción de transporte SSE: stdio como opción predeterminada y soporte para despliegue remoto con SSE.
  • Registro en Anthropic Skills: Activación inmediata en Claude Desktop y Claude Code CLI.
  • Automatización de pruebas: Pruebas unitarias y de integración para cada herramienta.
  • Infraestructura de observabilidad: Métricas de Prometheus, registro estructurado y paneles de control.
  • Documentación: README, referencia de la API y ejemplos de uso.

Pila de herramientas y requisitos de infraestructura

HerramientaFunciónLicencia
mcp Python SDKMarco de trabajo del servidor MCPMIT
Anthropic Claude APIIntegración de Skills y AgenteComercial
Docker y docker-composeContenerización y orquestaciónApache 2.0
FastAPI + uvicorn (para el transporte SSE)Despliegue remotoMIT y BSD
BiopythonUtilidades NCBI E y análisis PDBLicencia Biopython
requests y aiohttpClientes de cada API RESTApache 2.0 y MIT
pytest y pytest-asyncioMarco de pruebasMIT
structlog y richRegistro y salida en la CLIApache 2.0 y MIT
Cliente de Prometheus y GrafanaObservabilidadApache 2.0 y AGPL

Requisitos de infraestructura:

  • No se requiere GPU. Cada servidor se centra en la CPU y la red.
  • Implementación local: Docker Desktop o Docker Engine.
  • Implementación remota (opcional): VPS pequeño o Cloudflare Workers, AWS Lambda o Kubernetes.

Coste estimado para que el alumno pueda replicar el proyecto: Implementación local gratuita. VPS remoto, 5 a 10 USD al mes. Cada API pública (PubMed, UniProt, PDB, ChEMBL, BLAST) es gratuita.

Implementación práctica de la canalización

Arquitectura general:

mermaid

Paso 1. Clase base común (marco reutilizable)

Limitación de velocidad, reintentos, registro, manejo de errores y observabilidad, elementos que todos los servidores MCP comparten.

python
"""bio_mcp_base.py — clase base común de todos los servidores MCP de biología."""
import asyncio
import json
import time
from abc import ABC, abstractmethod
from dataclasses import dataclass
from functools import wraps
from typing import Any, Callable
import requests
import structlog
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from prometheus_client import Counter, Histogram, start_http_server
log = structlog.get_logger()
@dataclass
class RateLimitConfig:
"""Política de límite de solicitudes."""
max_requests_per_second: float = 3.0
max_requests_per_hour: int | None = None
burst: int = 5
class RateLimiter:
"""Token bucket rate limiter (async)."""
def __init__(self, config: RateLimitConfig):
self.config = config
self.tokens = float(config.burst)
self.last_refill = time.time()
self.lock = asyncio.Lock()
async def acquire(self) -> None:
async with self.lock:
now = time.time()
elapsed = now - self.last_refill
self.tokens = min(
self.config.burst,
self.tokens + elapsed * self.config.max_requests_per_second,
)
self.last_refill = now
if self.tokens < 1.0:
wait_time = (1.0 - self.tokens) / self.config.max_requests_per_second
await asyncio.sleep(wait_time)
self.tokens = 0.0
else:
self.tokens -= 1.0
# Métricas de Prometheus (compartidas por todos los servidores)
TOOL_CALLS = Counter(
"mcp_tool_calls_total",
"Número de llamadas a herramientas MCP",
["server", "tool", "status"],
)
TOOL_LATENCY = Histogram(
"mcp_tool_latency_seconds",
"Latencia de herramientas MCP (segundos)",
["server", "tool"],
buckets=[0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, 30.0, 60.0],
)
def instrument(server_name: str, tool_name: str):
"""Decorador que registra la latencia y la tasa de éxito de las llamadas a herramientas."""
def decorator(func: Callable):
@wraps(func)
async def wrapper(*args, **kwargs):
logger = log.bind(server=server_name, tool=tool_name)
start = time.perf_counter()
try:
result = await func(*args, **kwargs)
elapsed = time.perf_counter() - start
TOOL_CALLS.labels(server=server_name, tool=tool_name, status="success").inc()
TOOL_LATENCY.labels(server=server_name, tool=tool_name).observe(elapsed)
logger.info("tool_success", latency_ms=elapsed * 1000)
return result
except Exception as e:
elapsed = time.perf_counter() - start
TOOL_CALLS.labels(server=server_name, tool=tool_name, status="failure").inc()
TOOL_LATENCY.labels(server=server_name, tool=tool_name).observe(elapsed)
logger.error("tool_failure", latency_ms=elapsed * 1000, error=str(e))
raise
return wrapper
return decorator
class BioMCPServerBase(ABC):
"""Clase base de todos los servidores MCP de biología."""
def __init__(
self,
server_name: str,
rate_limit_config: RateLimitConfig,
max_retries: int = 3,
prometheus_port: int | None = 9090,
):
self.server = Server(server_name)
self.server_name = server_name
self.rate_limiter = RateLimiter(rate_limit_config)
self.max_retries = max_retries
if prometheus_port:
try:
start_http_server(prometheus_port)
except OSError:
pass # Omite si el puerto ya está en uso
self._register_handlers()
def _register_handlers(self):
"""Registra los handlers estándar de MCP."""
@self.server.list_tools()
async def _list() -> list[Tool]:
return self.tools
@self.server.call_tool()
async def _call(name: str, arguments: dict) -> list[TextContent]:
try:
result = await self.dispatch_tool(name, arguments)
return [TextContent(type="text", text=json.dumps(result, ensure_ascii=False, indent=2))]
except requests.RequestException as e:
log.error("api_request_failed", tool=name, error=str(e))
return [TextContent(type="text", text=json.dumps({"error": f"Falló la solicitud a la API: {e}"}))]
except Exception as e:
log.error("tool_execution_error", tool=name, error=str(e), exc_info=True)
return [TextContent(type="text", text=json.dumps({"error": str(e)}))]
@property
@abstractmethod
def tools(self) -> list[Tool]:
"""Lista de herramientas que expone este servidor."""
...
@abstractmethod
async def dispatch_tool(self, name: str, arguments: dict) -> Any:
"""Despacha por nombre de herramienta."""
...
async def http_get_with_retry(
self,
url: str,
params: dict | None = None,
timeout: int = 30,
) -> requests.Response:
"""Solicitud GET con límite de solicitudes y reintentos."""
for attempt in range(self.max_retries):
await self.rate_limiter.acquire()
try:
resp = requests.get(url, params=params, timeout=timeout)
if resp.status_code == 429: # Rate limit hit
wait_time = 2 ** attempt
log.warning("rate_limited", wait=wait_time, attempt=attempt, url=url)
await asyncio.sleep(wait_time)
continue
resp.raise_for_status()
return resp
except requests.Timeout:
if attempt == self.max_retries - 1:
raise
await asyncio.sleep(2 ** attempt)
raise RuntimeError(f"Se superó el máximo de reintentos: {url}")
async def run_stdio(self):
"""stdio transport."""
async with stdio_server() as (read, write):
await self.server.run(read, write, self.server.create_initialization_options())

Paso 2. Servidor PubMed MCP (expansión de la versión 14)

Profundice en los conceptos de la versión 14. Incluye la búsqueda de citas, artículos relacionados y autores.

python
"""pubmed_mcp.py — servidor MCP de PubMed E-utilities preparado para producción."""
import asyncio
import os
from xml.etree import ElementTree as ET
from typing import Any
from mcp.types import Tool
from bio_mcp_base import BioMCPServerBase, RateLimitConfig, instrument
EUTILS_BASE = "https://eutils.ncbi.nlm.nih.gov/entrez/eutils"
class PubMedMCPServer(BioMCPServerBase):
def __init__(self, api_key: str | None = None):
# Con una clave de API de NCBI admite 10 solicitudes/s; sin ella, 3 solicitudes/s.
rate = 10.0 if api_key else 3.0
super().__init__(
server_name="pubmed-mcp",
rate_limit_config=RateLimitConfig(max_requests_per_second=rate, burst=int(rate)),
)
self.api_key = api_key
@property
def tools(self) -> list[Tool]:
return [
Tool(
name="search_pubmed",
description="Devuelve una lista de PMID a partir de una consulta de PubMed; permite indicar intervalo de fechas y orden.",
inputSchema={
"type": "object",
"properties": {
"query": {"type": "string"},
"max_results": {"type": "integer", "default": 20},
"date_range_years": {"type": "integer", "default": 5},
"sort": {"type": "string", "enum": ["relevance", "pub_date"], "default": "relevance"},
},
"required": ["query"],
},
),
Tool(
name="fetch_abstracts",
description="Consulta resúmenes, autores, revistas y DOI a partir de una lista de PMID.",
inputSchema={
"type": "object",
"properties": {"pmids": {"type": "array", "items": {"type": "string"}}},
"required": ["pmids"],
},
),
Tool(
name="find_related",
description="Consulta artículos relacionados con un PMID mediante PubMed Similar Articles.",
inputSchema={
"type": "object",
"properties": {"pmid": {"type": "string"}, "top_k": {"type": "integer", "default": 10}},
"required": ["pmid"],
},
),
Tool(
name="get_citations",
description="Lista artículos que citan un PMID, basada en PubMed Central.",
inputSchema={
"type": "object",
"properties": {"pmid": {"type": "string"}},
"required": ["pmid"],
},
),
Tool(
name="search_by_author",
description="Busca publicaciones recientes de un autor concreto.",
inputSchema={
"type": "object",
"properties": {
"author_name": {"type": "string", "description": "Por ejemplo: 'Doudna JA'"},
"max_results": {"type": "integer", "default": 20},
},
"required": ["author_name"],
},
),
]
async def dispatch_tool(self, name: str, arguments: dict) -> Any:
dispatch_map = {
"search_pubmed": self._search,
"fetch_abstracts": self._fetch,
"find_related": self._find_related,
"get_citations": self._get_citations,
"search_by_author": self._search_by_author,
}
if name not in dispatch_map:
raise ValueError(f"Unknown tool: {name}")
instrumented = instrument("pubmed-mcp", name)(dispatch_map[name])
return await instrumented(**arguments)
async def _search(
self, query: str, max_results: int = 20,
date_range_years: int = 5, sort: str = "relevance",
) -> dict:
params = {
"db": "pubmed",
"term": query,
"retmax": max_results,
"reldate": date_range_years * 365,
"datetype": "pdat",
"retmode": "json",
"sort": sort,
}
if self.api_key:
params["api_key"] = self.api_key
resp = await self.http_get_with_retry(f"{EUTILS_BASE}/esearch.fcgi", params)
pmids = resp.json().get("esearchresult", {}).get("idlist", [])
return {"query": query, "count": len(pmids), "pmids": pmids}
async def _fetch(self, pmids: list[str]) -> list[dict]:
if not pmids:
return []
params = {
"db": "pubmed",
"id": ",".join(pmids),
"rettype": "abstract",
"retmode": "xml",
}
if self.api_key:
params["api_key"] = self.api_key
resp = await self.http_get_with_retry(f"{EUTILS_BASE}/efetch.fcgi", params, timeout=60)
root = ET.fromstring(resp.content)
articles = []
for art in root.findall(".//PubmedArticle"):
articles.append({
"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")
][:10],
"journal": art.findtext(".//Journal/Title") or "",
"year": art.findtext(".//PubDate/Year") or "",
"doi": next((
id_.text for id_ in art.findall(".//ArticleId")
if id_.get("IdType") == "doi"
), None),
"mesh_terms": [m.findtext(".//DescriptorName") for m in art.findall(".//MeshHeading")][:10],
})
return articles
async def _find_related(self, pmid: str, top_k: int = 10) -> dict:
params = {
"dbfrom": "pubmed", "db": "pubmed", "id": pmid,
"linkname": "pubmed_pubmed", "retmode": "json",
}
if self.api_key:
params["api_key"] = self.api_key
resp = await self.http_get_with_retry(f"{EUTILS_BASE}/elink.fcgi", params)
links = resp.json().get("linksets", [{}])[0].get("linksetdbs", [])
related = []
for link in links:
if link.get("linkname") == "pubmed_pubmed":
related = [l["id"] for l in link.get("links", [])][:top_k]
break
return {"source_pmid": pmid, "related_pmids": related}
async def _get_citations(self, pmid: str) -> dict:
params = {
"dbfrom": "pubmed", "db": "pmc", "id": pmid,
"linkname": "pubmed_pmc_refs", "retmode": "json",
}
if self.api_key:
params["api_key"] = self.api_key
resp = await self.http_get_with_retry(f"{EUTILS_BASE}/elink.fcgi", params)
return {"source_pmid": pmid, "cited_by": resp.json()}
async def _search_by_author(self, author_name: str, max_results: int = 20) -> dict:
query = f"{author_name}[Author]"
return await self._search(query, max_results=max_results, sort="pub_date")
if __name__ == "__main__":
server = PubMedMCPServer(api_key=os.environ.get("NCBI_API_KEY"))
asyncio.run(server.run_stdio())

Paso 3 a 6. Los otros 4 servidores MCP (resumen)

Implementado con el mismo patrón de clase base. Cada servidor tiene un archivo y una imagen de Docker separados.

python
"""uniprot_mcp.py — servidor MCP para la API REST de UniProt."""
class UniProtMCPServer(BioMCPServerBase):
"""tools: search_protein · get_protein_details · get_sequence · get_features · get_orthologs · get_domains.
UniProt REST endpoint: https://rest.uniprot.org/uniprotkb/*.
Límite de solicitudes: ninguno; se recomiendan como máximo 20 por segundo.
"""
# Amplía el ejemplo de UniProt de la parte 14 con features, ortólogos y dominios
# Hereda de la clase base y define el despacho
pass
"""pdb_mcp.py — servidor MCP para la API de RCSB PDB."""
class PDBMCPServer(BioMCPServerBase):
"""tools: search_structure · get_structure_details · fetch_pdb_file · get_ligands ·
search_by_sequence · get_experimental_method · get_related_structures.
RCSB PDB REST API: https://data.rcsb.org/.
Límite de solicitudes: ninguno; se recomiendan como máximo 10 por segundo.
"""
# Patrón similar al de la parte 14: descarga de archivos CIF y PDB, etc.
pass
"""chembl_mcp.py — servidor MCP para la API REST de ChEMBL."""
class ChEMBLMCPServer(BioMCPServerBase):
"""tools: search_compound · get_compound_details · get_target_activity ·
smiles_to_chembl_id · get_bioactivities · get_similar_compounds.
ChEMBL REST: https://www.ebi.ac.uk/chembl/api/data/.
ChEMBL es una fuente abierta estándar de datos sobre fármacos, actividad y dianas.
"""
pass
"""blast_mcp.py — servidor MCP para NCBI BLAST QBlast."""
class BLASTMCPServer(BioMCPServerBase):
"""Herramientas: submit_blast, poll_blast, get_hits y quick_blast (integra envío, sondeo y obtención).
QBlast requiere sondeo asíncrono. Como consume muchos recursos, el límite es estricto: una solicitud por segundo.
"""
pass

Si se gestionan los servidores individuales como repositorios separados, se facilita la colaboración en equipo y la gestión de las versiones.

Paso 7. Contenerización con Docker

Cada servidor MCP como una imagen Docker independiente.

dockerfile
# Dockerfile.pubmed_mcp
FROM python:3.11-slim

WORKDIR /app

# Dependencias del sistema
RUN apt-get update && apt-get install -y --no-install-recommends \
    curl \
    && rm -rf /var/lib/apt/lists/*

# Python dependency
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Código fuente
COPY bio_mcp_base.py pubmed_mcp.py .

# Expone el puerto de métricas de Prometheus
EXPOSE 9090

# Comunicación MCP mediante stdin/stdout
CMD ["python", "pubmed_mcp.py"]

# Comprobación de estado (opcional)
HEALTHCHECK --interval=30s --timeout=10s \
  CMD curl -f http://localhost:9090/metrics || exit 1
yaml
# docker-compose.yml
version: "3.9"

services:
  pubmed-mcp:
    build:
      context: .
      dockerfile: Dockerfile.pubmed_mcp
    container_name: pubmed-mcp
    environment:
      - NCBI_API_KEY=${NCBI_API_KEY}
      - LOG_LEVEL=INFO
    ports:
      - "9091:9090"  # Métricas de Prometheus
    stdin_open: true
    tty: true
    restart: unless-stopped
    logging:
      driver: json-file
      options:
        max-size: "10m"
        max-file: "3"

  uniprot-mcp:
    build: {context: ., dockerfile: Dockerfile.uniprot_mcp}
    container_name: uniprot-mcp
    ports: ["9092:9090"]
    stdin_open: true
    tty: true
    restart: unless-stopped

  pdb-mcp:
    build: {context: ., dockerfile: Dockerfile.pdb_mcp}
    container_name: pdb-mcp
    ports: ["9093:9090"]
    stdin_open: true
    tty: true
    restart: unless-stopped

  chembl-mcp:
    build: {context: ., dockerfile: Dockerfile.chembl_mcp}
    container_name: chembl-mcp
    ports: ["9094:9090"]
    stdin_open: true
    tty: true
    restart: unless-stopped

  blast-mcp:
    build: {context: ., dockerfile: Dockerfile.blast_mcp}
    container_name: blast-mcp
    ports: ["9095:9090"]
    stdin_open: true
    tty: true
    restart: unless-stopped

  # Stack de observabilidad
  prometheus:
    image: prom/prometheus:latest
    container_name: prometheus
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    ports: ["9099:9090"]
    restart: unless-stopped

  grafana:
    image: grafana/grafana:latest
    container_name: grafana
    ports: ["3001:3000"]
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    volumes:
      - grafana-storage:/var/lib/grafana
    restart: unless-stopped

volumes:
  grafana-storage:

Advertencia: Dado que MCP utiliza la comunicación stdio por defecto, para su uso en Docker se requiere docker run -i o un wrapper stdio-over-network (por ejemplo, mcp-proxy). El SDK de MCP más reciente también admite el transporte SSE (Server-Sent Events), lo que facilita el despliegue remoto (véase el paso 8).

Paso 8. Transporte SSE (para despliegue remoto)

stdio solo funciona para subprocesos locales. Para servidores remotos, se requiere SSE.

python
"""sse_transport_wrapper.py — expone remotamente un servidor MCP mediante FastAPI y SSE."""
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
from mcp.server.sse import SseServerTransport
async def sse_endpoint_factory(mcp_server_instance):
"""Expone el servidor MCP en un endpoint SSE."""
app = FastAPI()
transport = SseServerTransport("/messages")
@app.get("/sse")
async def sse_endpoint(request: Request):
async with transport.connect_sse(request.scope, request.receive, request._send) as (read, write):
await mcp_server_instance.server.run(
read, write, mcp_server_instance.server.create_initialization_options(),
)
return StreamingResponse(iter([]), media_type="text/event-stream")
@app.post("/messages")
async def messages_endpoint(request: Request):
return await transport.handle_post_message(request.scope, request.receive, request._send)
return app
# Ejemplo de ejecución (despliegue remoto)
# from pubmed_mcp import PubMedMCPServer
# import uvicorn
# server = PubMedMCPServer(api_key=os.environ.get("NCBI_API_KEY"))
# app = await sse_endpoint_factory(server)
# uvicorn.run(app, host="0.0.0.0", port=8000)

Paso 9. Registrar las habilidades de Anthropic

Empaquete este conjunto de servidores como una habilidad reutilizable en Claude Skills [1].

yaml
# skills/bio-research/skill.yaml
name: bio-research
version: 1.0.0
description: |
  Agente autónomo de investigación bioinformática.
  Integra cinco servicios: PubMed, UniProt, PDB, ChEMBL y BLAST.
  Claude orquesta las herramientas de forma autónoma.
authors: ["Your Lab"]
license: MIT

mcp_servers:
  - name: pubmed
    command: docker
    args: ["run", "-i", "--rm", "-e", "NCBI_API_KEY", "bio-mcp/pubmed:latest"]
    env: ["NCBI_API_KEY"]
  - name: uniprot
    command: docker
    args: ["run", "-i", "--rm", "bio-mcp/uniprot:latest"]
  - name: pdb
    command: docker
    args: ["run", "-i", "--rm", "bio-mcp/pdb:latest"]
  - name: chembl
    command: docker
    args: ["run", "-i", "--rm", "bio-mcp/chembl:latest"]
  - name: blast
    command: docker
    args: ["run", "-i", "--rm", "bio-mcp/blast:latest"]

system_prompt: |
  Eres un asistente de investigación bioinformática.
  Llama de forma autónoma a las herramientas MCP necesarias y responde basándote en evidencia.

  Principios:
  1. No inventes hechos. Toda afirmación debe estar respaldada por resultados de herramientas.
  2. Incluye las fuentes: PMID, accession de UniProt, ID de PDB o ID de ChEMBL.
  3. Si falla una herramienta, intenta una ruta alternativa e informa del fallo.
  4. Ahorra contexto y minimiza las llamadas innecesarias.
  5. Ante un límite de solicitudes, aplica backoff automáticamente e informa al usuario de la espera.

capabilities:
  - biological literature search
  - protein sequence and structure retrieval
  - chemical compound and drug activity lookup
  - sequence similarity search (BLAST)
  - cross-reference between databases

examples:
  - "Resume las publicaciones de los últimos cinco años sobre BRCA1 humano, incluidas estructuras 3D y ligandos relacionados"
  - "Enumera proteínas diana candidatas a las que podría unirse un SMILES concreto"
  - "Busca vías, enfermedades y ensayos clínicos recientes relacionados con este gen"

Paso 10. Automatización de pruebas

Pruebas de integración pytest para cada herramienta del servidor MCP.

python
"""tests/test_pubmed_mcp.py — pruebas de integración del MCP de PubMed."""
import pytest
import time
from pubmed_mcp import PubMedMCPServer
@pytest.fixture
def server():
return PubMedMCPServer()
@pytest.mark.asyncio
async def test_search_returns_pmids(server):
result = await server._search("BRCA1", max_results=5)
assert result["count"] > 0
assert all(p.isdigit() for p in result["pmids"])
@pytest.mark.asyncio
async def test_fetch_abstracts_valid_pmid(server):
results = await server._fetch(["33746851"]) # PMID válido conocido
assert len(results) == 1
assert results[0]["title"]
assert results[0]["abstract"]
@pytest.mark.asyncio
async def test_fetch_abstracts_empty_list(server):
results = await server._fetch([])
assert results == []
@pytest.mark.asyncio
async def test_find_related(server):
result = await server._find_related("33746851", top_k=5)
assert "related_pmids" in result
assert isinstance(result["related_pmids"], list)
@pytest.mark.asyncio
async def test_search_by_author(server):
result = await server._search_by_author("Doudna JA", max_results=5)
assert result["count"] > 0
@pytest.mark.asyncio
async def test_rate_limit_respected(server):
"""El límite de solicitudes fuerza una espera ante llamadas rápidas consecutivas."""
start = time.time()
for _ in range(5):
await server._search("test", max_results=1)
elapsed = time.time() - start
# Política de NCBI: 3 solicitudes/s; cinco requieren al menos ~1,3 s sin clave de API
assert elapsed >= 1.0, f"Límite de solicitudes incumplido: {elapsed}"
@pytest.mark.asyncio
async def test_dispatch_tool_unknown(server):
with pytest.raises(ValueError, match="Unknown tool"):
await server.dispatch_tool("nonexistent_tool", {})
# Integración continua
# pytest tests/ -v --asyncio-mode=auto

Paso 11. Panel de control de supervisión (Grafana)

Visualice las métricas de Prometheus en Grafana.

yaml
# prometheus.yml
global:
  scrape_interval: 15s

scrape_configs:
  - job_name: 'mcp-servers'
    static_configs:
      - targets:
          - 'pubmed-mcp:9090'
          - 'uniprot-mcp:9090'
          - 'pdb-mcp:9090'
          - 'chembl-mcp:9090'
          - 'blast-mcp:9090'

Ejemplos de métricas para el panel de Grafana:

  • Latencia p50, p95 y p99 por herramienta y servidor
  • Tasa de éxito (éxitos / total)
  • Volumen de llamadas por hora (tasa)
  • Distribución de tipos de error (límite de tasa 429, 5xx, error de análisis)

Rendimiento, costo y casos de error conocidos

Referencia de rendimiento (citando fuentes públicas)

Rendimiento y limitaciones estándar de cada API:

APILímite de tasaTiempo de respuesta promedioAutenticación
PubMed E-utilities3 solicitudes/s (sin clave), 10 solicitudes/s (con clave) [2]200–800 msClave de API gratuita
UniProt RESTNinguno (se recomienda menos de 20 por segundo)100–500 msNinguno
RCSB PDBNinguno (se recomienda menos de 10 por segundo)200–1000 msNinguno
ChEMBL RESTNinguno (se recomienda menos de 10 por segundo)300–1000 msNinguno
NCBI BLAST QBlast1–3 por segundo [3]30 segundos–5 minutos (asíncrono)Ninguno

Costo estimado para la reproducción por parte del alumno

  • Ejecución local: gratuita.
  • Implementación remota: VPS pequeño, 5–20 USD al mes.
  • API de Claude: 5–20 USD por sesión activa de Skill.

Cinco casos de error conocidos (recopilados de la comunidad y de artículos)

  1. Error de comunicación MCP de Docker stdio (especialmente en Windows) Síntoma: En Windows Docker Desktop, el stdio del contenedor del servidor MCP se almacena en búfer parcialmente, lo que provoca un error de comunicación. Causa: Particularidad del manejo de stdio de Docker para Windows (capas como Winpty y MSYS). Solución: (a) Cambiar al transporte SSE (Paso 8), (b) Ejecutar localmente con un subproceso Python nativo en lugar de Docker Compose, (c) Ejecutar dentro de WSL2, (d) Usar docker attach en lugar de docker exec -it. Fuente: Problemas de GitHub de MCP, "Almacenamiento en búfer de stdio de Docker en Windows" [4].

  2. Conflicto de nombres de herramientas entre varios servidores MCP Síntoma: Conflicto de nombres entre search de PubMed MCP y search de UniProt MCP, lo que provoca que el cliente no pueda realizar el envío. Causa: La especificación de MCP solo garantiza la singularidad de los nombres de las herramientas dentro del servidor. Solución: Igual que en la sección 14. Forzar el prefijo {server_name}__{tool_name} en el cliente o fijar el nombre del servidor como prefijo de la herramienta (código de esta sección: servidor pubmed-mcp). Fuente: MCP Discussions [5].

  3. Gestión de claves API (variables de entorno vs. bóveda) Síntoma: Clave API codificada en la imagen de Docker, lo que provoca su filtración al implementar la imagen. Causa: Patrón anti-patrón que consiste en colocar secretos en la directiva ENV del archivo Docker. Solución: (a) Utilizar la variable de entorno ${NCBI_API_KEY} en docker-compose (como en este ejemplo), (b) Docker Swarm secrets o Kubernetes Secrets, (c) Integrar la gestión de secretos con HashiCorp Vault o AWS Secrets Manager, (d) El archivo .env es .gitignore obligatorio. Fuente: Docker best practices [6].

  4. Política de tiempo de espera y reintento de BLAST QBlast Síntoma: NCBI BLAST QBlast puede tardar más de 5 minutos, lo que provoca un tiempo de espera excesivo de la herramienta MCP. Causa: BLAST es una tarea que consume muchos recursos. El tiempo de respuesta puede variar según la carga del servidor. Solución: (a) Aumentar el tiempo de espera de la herramienta MCP (10 minutos o más), (b) Utilizar un patrón de sondeo asíncrono (enviar, recibir RID, sondear), (c) Considerar la posibilidad de ejecutar BLAST localmente (blastn/blastp CLI + índice de genoma local), (d) Para búsquedas grandes, considerar la posibilidad de utilizar la búsqueda de UniProt. Fuente: NCBI BLAST usage guidelines [3].

  5. Dificultad para diagnosticar problemas debido a la falta de métricas de observación Síntoma: En producción, la tasa de fallos de una herramienta específica aumenta repentinamente, pero faltan registros para determinar la causa. Causa: Falta de Prometheus, registros estructurados y alertas en la implementación inicial. Solución: (a) Utilizar un decorador @instrument obligatorio, como en la clase base de este ejemplo (Paso 1), (b) Crear previamente un panel de control de Grafana (Paso 11), (c) Utilizar Alertmanager para enviar notificaciones automáticas cuando la tasa de fallos supere un umbral, (d) Guardar el historial de llamadas a la herramienta en un registro de auditoría (para cumplir con las normativas). Fuente: Prometheus best practices, metodología SRE [7].

Ideas de ampliación

  • Ampliación del servidor MCP personalizado: GEO/SRA (transcriptómica, secuenciación), Ensembl (variante, gen, genómica comparada), STRING (red de interacción proteína-proteína), Reactome (vía), KEGG (metabolismo).
  • Colaboración multiagente: Separar el agente de investigación, el agente de visualización y el agente de generación de informes en sesiones independientes y compartir los resultados a través de MCP.
  • Despliegue remoto del servidor MCP: Ejecución continua del servidor MCP en Cloudflare Workers, AWS Lambda, Google Cloud Run o Kubernetes para que lo compartan varios usuarios.
  • Integración de laboratorio húmedo: Exposición de las API de los equipos de laboratorio (por ejemplo, robots de manipulación de líquidos, lectores de placas, citómetros de flujo) como herramientas MCP para la ejecución automatizada de planes de experimentos.
  • Gestión de autenticación y autorización: Control del acceso a cada herramienta mediante OAuth2, JWT y control de acceso basado en roles (RBAC).
  • Capa de caché: Almacenamiento en caché de los resultados consultados con frecuencia (por ejemplo, búsquedas en PubMed) con Redis o Memcached para reducir la latencia y los costos.

Próximo episodio

  • Episodio 14 bio-mcp-agent: Implementación de un agente que coordina el conjunto de servidores de este episodio (se volverá a visitar el episodio 14).
  • Episodio 09 llm-vendor-benchmark: Comparación de las capacidades de uso de las herramientas MCP por proveedor.

Referencias

  1. Documentación de Anthropic Skills: https://docs.anthropic.com/en/docs/build-with-claude/skills
  2. Directrices de uso de NCBI E-utilities: https://www.ncbi.nlm.nih.gov/books/NBK25497/
  3. NCBI BLAST QBlast: https://ncbi.github.io/blast-cloud/dev/api.html
  4. Problemas de MCP en GitHub (Windows Docker): https://github.com/modelcontextprotocol/specification/issues
  5. Debates de MCP en GitHub: https://github.com/modelcontextprotocol/specification/discussions
  6. Mejores prácticas de Docker para secretos: https://docs.docker.com/engine/swarm/secrets/
  7. Mejores prácticas de Prometheus: https://prometheus.io/docs/practices/
  8. Descripción general del protocolo de contexto del modelo de Anthropic: https://modelcontextprotocol.io/
  9. SDK de Python de MCP: https://github.com/modelcontextprotocol/python-sdk
  10. SDK de TypeScript de MCP: https://github.com/modelcontextprotocol/typescript-sdk
  11. Registro del servidor oficial de MCP (comunidad): https://github.com/modelcontextprotocol/servers
  12. API REST de UniProt: https://www.uniprot.org/help/api
  13. API de RCSB PDB: https://data.rcsb.org/
  14. API REST de ChEMBL: https://chembl.gitbook.io/chembl-interface-documentation/web-services
  15. Artículo de MCPmed: https://academic.oup.com/bib/article/27/1/bbag076/8495038
  16. structlog: https://www.structlog.org/
  17. prometheus_client Python: https://github.com/prometheus/client_python
  18. Grafana: https://grafana.com/
  19. FastAPI: https://fastapi.tiangolo.com/
  20. uvicorn: https://www.uvicorn.org/

💬 Preguntas y comentarios

0 comentarios

Puedes publicar sin iniciar sesión. Los comentarios de invitados no pueden editarse ni eliminarse después.

0/2000

Cargando...