Inicio / Artículos

Diecisiete herramientas MCP pequeñas en lugar de un agente gigante: un sistema de escritura con IA

Cómo un sistema de investigación y redacción divide la ingesta de video, la búsqueda, la redacción preliminar, la crítica y la validación de código en herramientas MCP delimitadas, con artefactos y seguimiento.

Editor: REACTAPP.TOP13 min de lectura2957 palabras

Fuentes: Why I Built MaxyAI as 17 Small MCP Tools Instead of One Giant Agent

La parte difícil de un agente de escritura basado en IA no es hacer que un modelo de lenguaje escriba. Se trata de decidir por qué cosas el modelo no debería ser responsable. Los primeros intentos de crear tales agentes tienden a reducirse a una sola arquitectura: entregar un tema a un único modelo, adjuntarle una herramienta de búsqueda y pedirle que investigue, razoné, escriba, valide y se recupere de errores en un único proceso prolongado. Los prompts se vuelven cada vez más extensos, los rastreos son difíciles de depurar y los fallos deterministas se entrelazan con decisiones semánticas. MaxyAI, un sistema de investigación y escritura basado en IA, partió de otra idea: permitir que cada capacidad que tenga entradas distintas, modos de fallo, reglas de reintentos o un resultado reutilizable se convierta en su propia herramienta delimitada detrás de un único servidor MCP.

Los datos de entrada rara vez están organizados de forma ordenada. Pueden ser una conferencia de YouTube, un repositorio de GitHub, una página de documentación y unos pocos enlaces de referencia. MaxyAI recoge todo esto, amplía la investigación mediante búsquedas en la web, almacena el corpus en expansión en el disco y lo estructura en un esquema. Luego redacta sección por sección, envía el borrador a un evaluador numérico, extrae cada bloque de Python, valida y ejecuta el código, corrige los errores de sintaxis y exporta un artículo HTML independiente.

No existe ningún comando complejo detrás de esto. El sistema es un servidor FastMCP con 17 herramientas registradas, 12 de ellas destinadas a la investigación. Las herramientas están diseñadas para ser pequeñas. Una transcribe videos, otra entiende repositorios, otra busca en la web, otra crea un esquema, otra evalúa el artículo y otra valida el código. El modelo decide qué capacidad utilizar, y el Python que está detrás de cada herramienta realiza el trabajo de forma determinista.

Esa separación es lo más útil del diseño. El modelo puede cambiar sin afectar la integración con YouTube. Se puede sustituir el rastreador sin perturbar al generador de contenido. La fase de verificación del código puede depurarse sin tener que volver a ejecutar la investigación. Y como cada paso importante deja un artefacto o una huella, se puede inspeccionar un funcionamiento autónomo prolongado posteriormente en lugar de considerarlo como una única transcripción de chat enorme.

El número 17 nunca fue un objetivo. Proviene de una regla sencilla: una capacidad recibe su propio herramienta o gestor específico cuando interactúa con un sistema externo diferente, genera un artefacto reutilizable, necesita un comportamiento de intentos separado o se beneficia de una validación determinista. MCP luego proporciona una interfaz compartida para todos esos especialistas.

¿Por qué no una sola herramienta grande? Porque el sistema resuelve varios problemas de ingeniería distintos en lugar de un único problema de razonamiento. Estas tareas (ingestión de video, análisis de repositorios, descubrimiento en la web, gestión de estado, redacción, crítica, ejecución de código, reparación, publicación) tienen cada una sus propios insumos, costos, modos de fallo y necesidades de reintentos. Una herramienta mega reduciría la complejidad inicial, pero haría que el sistema fuera más difícil de observar y ampliar.

Nada de esto implica que cada agente necesite 17 herramientas. Se trata de un estudio de caso sobre la descomposición: dónde corresponde el juicio semántico a un modelo, dónde el código ordinario debe seguir siendo el encargado, cómo los artefactos pueden sustituir al estado conversacional excesivamente complejo, y por qué el código generado debe pasar pruebas deterministas antes de que un lector lo vea.

Un proceso de principio a fin

Tomemos el tema de la llamada a funciones de LLM y las salidas estructuradas, con un conjunto inicial de pautas como una charla en YouTube, un repositorio de GitHub y algunas URLs de documentación.

Fase 1 no genera texto prosaico. Su tarea es recopilar evidencias. El servidor MCP selecciona URLs, envía el video a la API multimodal de Gemini, convierte los repositorios en resúmenes adecuados para el modelo mediante gitingest, rastrea páginas con Firecrawl, llena las lagunas buscando en Tavily y registra el inventario de fuentes en archivos de estado dentro de .nova/. El resultado es research.md, un extenso documento de investigación que cualquiera puede abrir y leer, en lugar de una memoria de conversación oculta.

Fase 2 toma ese artefacto y planifica la publicación. generate_article_outline asigna las secciones, los objetivos de cantidad de palabras y los objetivos de código, y draft_article_section escribe entonces una sola sección por llamada. A continuación, critic_evaluate_article califica el borrador completo en cuanto a profundidad técnica, calidad del código, estructura y fidelidad en las citaciones, y refine_and_save_article aplica las revisiones que exige antes de escribir article.md.

Fase 3 trata al Markdown como algo que debe sobrevivir a la ejecución y no solo parecer convincente. Los bloques de Python se extraen y se analizan con ast.parse(). Los fragmentos válidos se guardan en .nova/sandbox/ y se ejecutan con un tiempo límite de cinco segundos. Una falla de sintaxis envía el error y el fragmento a fix_code_snippets, y el resultado vuelve a pasar por la validación. Una vez que el código aprueba, export_publication_bundle genera el documento HTML independiente.

El seguimiento de Opik abarca toda la ejecución. Cada seguimiento de modelo y herramienta muestra la latencia, los metadatos del proveedor, la jerarquía de herramientas y los resultados intermedios. Vista como un diagrama, la arquitectura consta de tres fases (investigación, redacción y crítica, pruebas de calidad del código y publicación) que comparten mecanismos de recuperación ante fallos y capacidades de observabilidad.

El servidor MCP como eje central

La primera decisión de diseño se tomó a nivel de protocolo: exponer las capacidades a través del Model Context Protocol y no integrar cada función directamente en un único bucle de aplicación.

MCP proporciona al cliente un vocabulario estándar para descubrir y llamar a las herramientas. FastMCP lo hace sencillo en Python, ya que una función tipada común se convierte en una herramienta MCP mediante un decorador, y su firma se convierte en parte del contrato de la herramienta. La implementación puede seguir siendo pura Python incluso cuando el cliente es un agente.

En el proyecto, mcp_server/src/routers/tools.py es el punto de entrada a la capa de herramientas. Un registro simplificado se ve así:

import opik
from fastmcp import FastMCP

from ..tools import run_web_research_tool

def register_mcp_tools(mcp: FastMCP) -> None:
    @mcp.tool(name="run_web_research")
    @opik.track(type="tool", tags=["research", "tavily"])
    async def run_web_research(
        queries: list[str],
        research_directory: str,
    ) -> dict:
        return await run_web_research_tool(
            queries=queries,
            research_directory=research_directory,
        )

El decorador hace más que simplemente mejorar la sintaxis. El cliente ve una herramienta con nombre y argumentos tipados, mientras que la implementación permanece en la capa de herramientas y controladores. Esa separación es importante una vez que el servidor supera un número limitado de capacidades.

Se evitó deliberadamente una única herramienta do_everything(topic), ya que reintroduciría el acoplamiento que el diseño buscaba eliminar. Herramientas más pequeñas permiten que el modelo componga comportamientos: extraer fuentes, verificar qué falta, buscar nuevamente, esbozar, redactar, criticar, reparar. También hacen que los registros sean mucho más fáciles de leer, ya que cada segmento corresponde a una acción con sentido.

Por lo tanto, el servidor con 17 herramientas no representa 17 comandos distintos, sino un conjunto de capacidades definidas. La investigación ocupa la mayor parte, con 12 herramientas, ya que la adquisición de fuentes es donde el flujo de trabajo necesita mayor variedad.

Investigación: fuentes diferentes, herramientas diferentes

La investigación absorbió la mayor parte del esfuerzo de diseño, ya que “buscar en la web” oculta varias operaciones. Un video, un repositorio de GitHub y un sitio de documentación con mucho código JavaScript plantean tres problemas de datos diferentes, y procesarlos con un único scraper proporciona al autor un contexto deficiente desde el principio.

Gemini para YouTube

Resolver videos de larga duración de forma local es costoso: hay que descargar el audio, ejecutar la conversión de voz a texto, gestionar la memoria de la GPU y luego resumir la transcripción. El módulo youtube_handler.py sigue un enfoque más sencillo. Envía directamente la URL pública de YouTube a Gemini como entrada multimodal y solicita un Markdown estructurado con marcas de tiempo.

La razón para elegir Gemini no es porque sea el mejor modelo para todo el proyecto, sino porque esta capacidad se adapta a la tarea: la ingestión multimodal nativa permite que el servidor procese videos sin necesidad de una stack local de Whisper ni de una GPU en la máquina del cliente.

Los errores del servidor del proveedor son gestionados por el mismo módulo mediante intentos exponenciales, lo que mantiene las fallas temporales del modelo dentro de la herramienta, de modo que el agente externo no necesita conocer los detalles de red.

@retry(
    retry=retry_if_exception_type(errors.ServerError),
    wait=wait_exponential(multiplier=1, min=5, max=60),
    stop=stop_after_attempt(3),
)
async def transcribe_youtube(url: str, output_path: Path, timestamp: int = 30):
    client = track_genai_client(
        genai.Client(api_key=settings.google_api_key.get_secret_value())
    )

    parts = [
        types.Part(file_data=types.FileData(file_uri=url)),
        types.Part(text=PROMPT_YOUTUBE_TRANSCRIPTION.format(timestamp=timestamp)),
    ]

    response = await client.aio.models.generate_content(
        model=settings.youtube_transcription_model,
        contents=parts,
    )

    output_path.write_text(response.text, encoding="utf-8")

En una prueba reportada, una conferencia de 39 minutos se convirtió en notas estructuradas en 17,8 segundos sin utilizar memoria de GPU en el lado del cliente. Se trata de un caso concreto observado y no de una métrica universal, ya que la carga del proveedor y las condiciones de la red pueden modificarla. La traza grabada mostró el identificador del modelo gemini-3.7-flash y el artefacto de estado persistente .nova/guidelines_filenames.json.

gitingest para repositorios

Un repositorio no es una página web. El modelo necesita la estructura de archivos y el código fuente real, no la interfaz de navegación de GitHub; por eso el procesador de GitHub utiliza gitingest para generar un resumen adecuado para modelos de lenguaje grande del repositorio.

La validación del AST es un asunto separado. gitingest solo se encarga de ingresar datos, proporcionando al modelo de investigación un mapa de directorios y el contenido de los archivos, mientras que el analizador de AST de Python interviene mucho más tarde, cuando el pipeline de publicación verifica el código generado. Combinar los dos complicaría la arquitectura.

Mantenerlos separados también facilita las extensiones. Un sistema de ingestión de GitLab o de carpetas locales solo necesitará generar el mismo artefacto de investigación, ya que al autor no le importa de dónde proviene el código.

Tavily y Firecrawl juntos

Los dos abordan partes diferentes de la investigación en la web. Tavily ayuda en la fase de descubrimiento, convirtiendo la pregunta “¿Qué más me falta?” en un conjunto de consultas específicas. Firecrawl interviene una vez que el servidor ha seleccionado una página concreta y desea extraer su contenido de manera limpia.

El proyecto aprovecha esta diferencia para hacer que la investigación sea iterativa. Ingerir las referencias iniciales no es el final. El agente examina el corpus, elabora nuevas consultas para lo que aún falta, las ejecuta, agrega los resultados obtenidos y puede repetir el proceso hasta tres veces.

El diseño es deliberadamente asimétrico: el LLM decide qué falta, mientras que Tavily y Firecrawl se encargan de la recuperación de información. Este mismo patrón se repite en todo el proceso, utilizando el modelo cuando el juicio semántico es útil y código y servicios ordinarios cuando es mejor contar con un trabajo determinista. La actividad de Firecrawl durante una investigación muestra que rastrea fuentes técnicas heterogéneas antes de comenzar a redactar.

Archivos en .nova/ en lugar de una única conversación extensa

La investigación puede generar fácilmente decenas de miles de tokens, y llevar consigo cada página extraída y cada decisión intermedia en cada llamada a una herramienta posterior sería un desperdicio.

En su lugar, el sistema escribe pequeños archivos de control bajo .nova/, como guidelines_filenames.json y next_queries.json, y guarda el contenido de mayor tamaño en archivos como research.md. El modelo recibe únicamente la información necesaria para la operación actual.

Eso es menos mágico que la “memoria del agente”, pero mucho más fácil de inspeccionar. Cuando una ejecución se detiene a mitad de camino, el directorio de estado muestra qué referencias se recopilaron o qué ronda de búsqueda debía realizarse a continuación. En efecto, el sistema de archivos funciona como un acuerdo aproximado pero funcional entre las diferentes fases.

El generador es un pipeline

Una vez que existe research.md, la tentación obvia es enviar todo el archivo a un modelo grande y solicitar un artículo de 2,000 palabras. El proyecto evitó eso desde el principio, ya que los límites de salida y la atención al contexto largo dificultan controlar al mismo tiempo la estructura, la cobertura de código y las citas.

En cambio, la fase de redacción divide el trabajo en cuatro herramientas:

  • generate_article_outline convierte el corpus de investigación en una lista de secciones con presupuestos de palabras y objetivos de código.
  • draft_article_section escribe secciones limitadas en lugar de una sola versión completa.
  • critic_evaluate_article evalúa el borrador elaborado según una rúbrica estructurada del 0 al 100.
  • refine_and_save_article realiza las revisiones necesarias, crea el índice, verifica la estructura de las citas y guarda article.md.

Lo importante es que el crítico no se limite a decir “se ve bien”. Su resultado son comentarios estructurados en los que otra herramienta puede actuar:

{
 "overall_score": 85,
 "status": "APPROVED",
 "category_scores": {
 "technical_depth": 27,
 "code_quality": 26,
 "structure_clarity": 17,
 "citation_fidelity": 15
 },
 "specific_critiques": [
 "Section 3.2 mentions timeout handling but omits the asyncio.wait_for example.",
 "Citation [14] is cited twice with conflicting author names."
 ],
 "required_revisions": [
 "Add the timeout example.",
 "Deduplicate bibliography reference [14]."
 ]
}

Este patrón reduce gradualmente el papel del modelo en cada paso. El modelo de esquema decide la estructura, el redactor sintetiza una sección, el crítico busca defectos y la fase de refinamiento aplica una lista de verificación. Ningún modelo individual debe realizar las cuatro tareas en una sola ejecución.

La puntuación de 85 sobre 100 no es una métrica científica de calidad. Es una señal de control del flujo de trabajo, y lo valioso es la lista de críticas y revisiones legible por la máquina.

Una huella de una verdadera cadena de escritura muestra una instrucción estructurada, una llamada al modelo de chat y un analizador Pydantic; ese tipo de registro permite comprender posteriormente un pipeline de escritura autónomo.

Código como artefacto ejecutable

Un artículo técnico puede estar bien escrito y, aun así, fallarle a los lectores si su código no se ejecuta. Por eso existe una tercera fase en primer lugar.

El desarrollador abre article.md, extrae su código Python entre marcos, valida cada bloque con ast.parse(), guarda los fragmentos verificados en .nova/sandbox/, los ejecuta como un proceso hijo mientras captura stdout y stderr, y permite que los fallos pasen por otra vez por una herramienta de reparación basada en LLM.

El análisis y ejecución estáticos detectan problemas diferentes. ast.parse() es rápido y rechaza el Python inválido de inmediato. El proceso hijo captura los errores en tiempo de ejecución que la sintaxis por sí sola no puede detectar.

def validate_python_syntax(code: str):
 try:
 ast.parse(code)
 return True, None
 except SyntaxError as exc:
 return False, f"SyntaxError at line {exc.lineno}: {exc.msg}"
result = subprocess.run(
 [sys.executable, str(file_path)],
 capture_output=True,
 text=True,
 timeout=5,
 env={"PYTHONUNBUFFERED": "1"},
)

Un fragmento defectuoso se repara y se envía nuevamente para su validación antes de que el artículo se compile. En un caso, un artículo generado contenía pip install google-genai dentro de un bloque de Python. Aunque parecía válido en Markdown, Python lo rechazó de inmediato. fix_code_snippets recibió el rastro de errores y el bloque dañado, reescribió el fragmento y lo devolvió para su validación.

# Broken
pip install google-genai
from google import genai

# Repaired
# Requires: pip install google-genai
import os
from google import genai

client = genai.Client(
    api_key=os.environ.get("GEMINI_API_KEY", "mock-key")
)

El bucle de reparación se mantiene local intencionadamente. Al modelo no se le permite reescribir todo el artículo solo porque un fragmento falló. Recibe la unidad más pequeña y útil: el código y su error, y las verificaciones deterministas siguen actuando como filtro.

En términos sencillos: article.md se escanea en busca de código Python entre marcos, cada fragmento se analiza, se almacena en .nova/sandbox/ y se ejecuta en un proceso hijo delimitado. En caso de fallo, el código y el rastro de errores se empacan para fix_code_snippets, y la reparación propuesta vuelve a seguir el mismo proceso de validación. El modelo nunca aprueba su propia reparación. El LLM propone, y ast.parse() junto con la ejecución toman la decisión. Solo cuando el bucle finaliza, export_publication_bundle convierte el artefacto revisado en el archivo HTML independiente.

Hay un aspecto que merece especial atención. La capa de ejecución es un ejecutor de subprocesos protegido y no un entorno aislado de seguridad reforzado. Un tiempo de espera y un proceso hijo son útiles para la verificación de calidad del artículo, ya que permiten detectar errores de sintaxis, fallos en tiempo de ejecución y bloqueos. El código realmente no confiable requeriría un aislamiento más estricto, como contenedores, nombres de espacio, seccomp, microVMs o un servicio de entorno aislado dedicado, con límites en el sistema de archivos, red, llamadas a sistemas, CPU y memoria.

Dos capas compartidas: enrutamiento de modelos y trazado

La arquitectura visible se compone de tres fases, pero dos componentes compartidos son igualmente importantes en el uso diario.

Enrutamiento de modelos con control de cuotas

Una ejecución de investigación puede generar muchas llamadas a modelos, lo que fácilmente agota las cuotas de la versión gratuita. Para evitar la inicialización específica del proveedor en cada manejador, la creación de modelos se centraliza en llm_utils.py, donde la configuración del proveedor y de la clave API se almacena en los parámetros de configuración.

El proyecto se desarrolló a partir de una serie de modelos como Gemini Flash, Gemini Flash Lite y Llama alojado en Groq. Esos modelos en particular no son permanentes, pero eso no es relevante. Cada herramienta interactúa con una abstracción de modelo, sin que los detalles del proveedor estén integrados en el código de investigación y desarrollo.

También resultan más económicos los experimentos, ya que al transferir una tarea a un modelo más barato o disponible en ese momento, el contrato de la herramienta MCP permanece intacto.

def get_chat_model(model_id: str, schema=None):
    selected = settings.llm_configs[model_id]
    identifier = selected["identifier"]
    params = selected.get("params", {}).copy()

    # resolve provider-specific API key
    ...

    model = init_chat_model(identifier, **params)
    return track_langchain_model(model)

Opik hace que las ejecuciones largas sean inspeccionables

Los registros pierden su utilidad una vez que un servidor MCP llama a un modelo que a su vez llama a una herramienta que escribe un artefacto que activa otro modelo. Al instrumentar los clientes de modelos y las herramientas MCP con el seguimiento de Opik, se muestra esa jerarquía directamente, sin necesidad de reconstruirla a partir de instrucciones de impresión.

El valor es práctico: se puede comparar un paso lento de Firecrawl con una llamada a un modelo, identificar el modelo que realmente respondió, ver el contenido recibido por un analizador y reproducir el camino que generó un artefacto defectuoso. Las capturas de pantalla del seguimiento son la interfaz de desarrollo para comprender el flujo de trabajo, no una decoración.

Qué enseña el proyecto sobre el diseño de agentes

La capacidad de generar artículos no es lo que hace interesante a MaxyAI, ya que un modelo de vanguardia ya puede convertir una instrucción en uno decente. Lo que destaca es cómo se divide el trabajo. La ingestión de video, el análisis de un repositorio, la búsqueda, la elaboración de esquemas, la redacción, la crítica, la extracción de código, la validación, la reparación y la exportación son herramientas separadas. MCP les proporciona un marco común; los archivos en disco permiten que las distintas fases compartan grandes cantidades de datos sin tener que almacenarlos en la memoria del chat, y Opik mantiene visible toda la trayectoria del proceso.

Los puntos de extensión son fáciles de identificar. Un procesador de PDF ofrece a la Fase 1 otro tipo de fuente de información. Sustituir Tavily no afecta al escritor. Se puede añadir validación en JavaScript a la Fase 3 sin tocar el motor de investigación. Cambiar los proveedores de modelos deja la interfaz MCP sin modificaciones.

El fortalecimiento para entornos de producción comenzaría con una mayor isolación en entornos de pruebas, seguido de un conjunto fijo de herramientas de evaluación que abarque la investigación, las citaciones y la calidad del código, metadatos más claros sobre las capacidades del proveedor y puntos de control duraderos durante la ejecución. Incluso hoy en día, el proyecto destaca que un modelo puede elegir la próxima acción, pero la calidad del sistema depende de las herramientas, los límites, los artefactos y los ciclos de retroalimentación que lo rodean.

Tres lecciones de ingeniería son aplicables a otros sistemas de agentes:

  1. Los límites de las herramientas son más importantes que su cantidad. Asigne un límite propio a cada capacidad si esta accede a un sistema diferente, falla o vuelve a intentarlo de manera distinta, o genera un artefacto que alguien volverá a utilizar.
  2. Los modelos proponen, las verificaciones deterministas deciden, siempre que sea posible. Un modelo de lenguaje grande puede corregir un fragmento de texto, pero el análisis y la ejecución son los que toman la decisión final.
  • Es mejor indicar qué datos se pueden inspeccionar que referirse vagamente a la “memoria”. La investigación guardada, los archivos de control y las huellas permitieron hacer depurables las ejecuciones prolongadas y facilitaron la reejecución de fases individuales sin tener que reconstruir toda la historia de conversaciones.
  • Leer más