Phase 13 - Lesson 12

Roots y Elicitación — Alcance y Entrada del Usuario a Mitad de la Ejecución

This lesson includes a graded coding exercise that runs in your browser, unlocked with lifetime access.

Las rutas definidas en código (hard-coded) se rompen en el momento en que un usuario abre un proyecto diferente. Los argumentos de herramientas precompletados se rompen cuando el usuario no especifica lo suficiente. Las roots limitan el alcance del servidor a un conjunto de URIs controlado por el usuario; la elicitación (elicitation) pausa a mitad de la llamada a la herramienta para pedir al usuario una entrada estructurada a través de un formulario o URL. Dos primitivas del cliente, dos soluciones para modos de fallo comunes de MCP. SEP-1036 (elicitación en modo URL, 2025-11-25) es experimental hasta el primer semestre de 2026 — verifique las versiones del SDK antes de depender de ella.

Tipo: Build Lenguajes: Python (stdlib, demo de roots + elicitación) Prerrequisitos: Fase 13 · 07 (servidor MCP) Tiempo: ~45 minutos

Objetivos de Aprendizaje

  • Declarar roots y responder a notifications/roots/list_changed.
  • Restringir las operaciones de archivos del servidor a las URIs dentro del conjunto de roots declarado.
  • Usar elicitation/create para solicitar al usuario una confirmación o entrada estructurada a mitad de la llamada a la herramienta.
  • Elegir entre la elicitación en modo formulario (form-mode) y en modo URL (este último es experimental; riesgo de desviación observado).

El Problema

Dos fallas concretas que experimenta un servidor MCP de notas en producción.

Suposición de ruta incorrecta. El servidor está escrito pensando en ~/notes. Un usuario en una máquina diferente con notas en ~/Documents/Notes obtiene una llamada a la herramienta que falla silenciosamente (no se encontró el archivo) o, peor aún, escribe en el lugar equivocado.

Argumento faltante que el usuario conocería. El usuario solicita "eliminar la nota antigua del informe TPS". El modelo llama a notes_delete(title: "TPS report") pero hay tres notas coincidentes de 2023, 2024 y 2025. La herramienta no puede adivinar. Fallar con "ambiguo" es molesto; ejecutar en las tres es catastrófico.

Las roots solucionan el primero: el cliente declara en initialize el conjunto de URIs que el servidor puede tocar. La elicitación soluciona el segundo: el servidor pausa la llamada a la herramienta y envía elicitation/create para pedirle al usuario que elija cuál.

El Concepto

Roots

El cliente declara una lista de roots en initialize:

{
  "capabilities": {"roots": {"listChanged": true}}
}

El servidor puede entonces llamar a roots/list:

{"roots": [{"uri": "file:///Users/alice/Documents/Notes", "name": "Notes"}]}

Los servidores DEBEN tratar las roots como el límite: cualquier lectura o escritura de archivos fuera del conjunto de roots es rechazada. Esto no es impuesto por el cliente (el servidor sigue siendo código en el que el usuario confió), pero los servidores que cumplen con la especificación lo respetan.

Cuando el usuario añade o elimina una raíz, el cliente envía notifications/roots/list_changed. El servidor vuelve a llamar a roots/list y actualiza su límite.

Por que las roots son una primitiva del cliente

Las roots son declaradas por el cliente porque representan el modelo de consentimiento del usuario. El usuario le dijo a Claude Desktop "dale a este servidor de notas acceso a estos dos directorios". El servidor no puede ampliar ese alcance.

Elicitación: el modo por defecto en formulario (form-mode)

elicitation/create recibe un esquema de formulario más un prompt en lenguaje natural:

{
  "method": "elicitation/create",
  "params": {
    "message": "Delete 'TPS report'? Multiple notes match; pick one.",
    "requestedSchema": {
      "type": "object",
      "properties": {
        "note_id": {
          "type": "string",
          "enum": ["note-3", "note-7", "note-14"]
        },
        "confirm": {"type": "boolean"}
      },
      "required": ["note_id", "confirm"]
    }
  }
}

El cliente renderiza un formulario, recopila la respuesta del usuario y devuelve:

{
  "action": "accept",
  "content": {"note_id": "note-14", "confirm": true}
}

Tres acciones posibles: accept (el usuario lo completó), decline (el usuario lo cerró), cancel (el usuario abortó toda la llamada a la herramienta).

Los esquemas de formulario son planos (flat) — los objetos anidados no son compatibles en la v1. Los SDKs normalmente rechazan cualquier cosa más compleja que una sola capa.

Elicitación: modo URL (SEP-1036, experimental)

Novedad en 2025-11-25. En lugar de un esquema, el servidor envía una URL:

{
  "method": "elicitation/create",
  "params": {
    "message": "Sign in to GitHub",
    "url": "https://github.com/login/oauth/authorize?client_id=..."
  }
}

El cliente abre la URL en un navegador, espera a que se complete y regresa cuando el usuario vuelve. Útil para flujos de OAuth, autorización de pagos y firma de documentos donde un formulario es insuficiente.

Nota sobre riesgo de desviación (drift-risk): la estructura de respuesta de SEP-1036 aún se está definiendo; algunos SDKs devuelven la URL de retorno (callback URL), otros devuelven un token de finalización. Lea las notas de lanzamiento de su SDK antes de usar el modo URL en producción.

Cuándo la elicitación es la herramienta adecuada

  • Confirmación del usuario antes de acciones destructivas (indicación destructiva + elicitación).
  • Desambiguación (elegir una de N coincidencias).
  • Configuración inicial en la primera ejecución (claves de API, directorios, preferencias).
  • Flujos estilo OAuth (modo URL).

Cuándo la elicitación es incorrecta

  • Completar los argumentos obligatorios de una herramienta que el modelo podría haber solicitado en prosa. Use una nueva solicitud normal en texto, no un diálogo de elicitación.
  • Llamadas de alta frecuencia. La elicitación interrumpe la conversación; no la inicie dentro de un bucle.
  • Cualquier cosa que el servidor pueda validar a posteriori. Valide, devuelva un error y deje que el modelo le pregunte al usuario en texto.

Puente para el "Human-in-the-loop"

La elicitación y el sampling juntos habilitan el modelo "human-in-the-loop" (humano en el bucle) de MCP. El bucle del agente de un servidor puede pausarse ya sea para recibir la entrada del usuario (elicitación) o para el razonamiento del modelo (sampling). La Fase 13 · 11 cubrió el sampling; esta lección cubre la elicitación. Junte ambos para obtener un control completo a mitad del bucle.

Uso

code/main.py extiende el servidor de notas con:

  • Respuesta roots/list que el servidor vuelve a consultar después de las notificaciones de cambio en la lista de roots.
  • Una herramienta notes_delete que utiliza elicitation/create para desambiguar cuando coinciden varias notas.
  • Una herramienta notes_setup que utiliza la elicitación en modo URL para abrir una página de configuración de primera ejecución (simulada).
  • Una comprobación de límites que rechaza operaciones en URIs fuera de las roots declaradas.

La demostración ejecuta tres escenarios: camino feliz (una coincidencia), desambiguación (tres coincidencias, se activa la elicitación) y escritura fuera de la raíz (rechazada).

Entrega

Esta lección produce outputs/skill-elicitation-form-designer.md. Dada una herramienta que podría necesitar confirmación o desambiguación del usuario, la habilidad (skill) diseña el esquema del formulario de elicitación y la plantilla de mensaje.

Ejercicios

  1. Ejecute code/main.py. Active la ruta de desambiguación; confirme que la respuesta simulada del usuario se dirija de vuelta a la herramienta.

  2. Agregue una nueva herramienta notes_archive que requiera confirmación de elicitación cada vez (indicación destructiva). Compruebe la experiencia de usuario (UX): ¿cómo se compara esto con que el modelo vuelva a preguntar en texto?

  3. Implemente la elicitación en modo URL para un flujo de OAuth de primera ejecución. Tenga en cuenta el riesgo de desviación y agregue una protección de versión del SDK.

  4. Extienda el manejo de roots/list: cuando llegue una notificación, el servidor debe releer y escanear de nuevo atómicamente los descriptores de archivos abiertos que ahora podrían estar fuera del alcance.

  5. Lea el hilo de discusión de la issue SEP-1036 en GitHub. Identifique una pregunta abierta que afecte el modo en que los servidores deben manejar los retornos de llamada (callbacks) en modo URL.

Términos Clave

Término Lo que la gente dice Lo que realmente significa
Root "Límite de consentimiento" URI que el cliente ha permitido que el servidor toque
roots/list "El servidor solicita el alcance" El cliente devuelve el conjunto de roots actual
notifications/roots/list_changed "El usuario cambió el alcance" El cliente indica que el conjunto de roots ha mutado
Elicitación "Preguntar al usuario a mitad de llamada" Solicitud iniciada por el servidor para obtener una entrada de usuario estructurada
elicitation/create "El método" Método JSON-RPC para solicitudes de elicitación
Modo Formulario "Formulario basado en esquema" JSON Schema plano (flat) renderizado como formulario en la interfaz de usuario del cliente
Modo URL "Redirecionamiento del navegador" SEP-1036 experimental; abre una URL y espera
accept / decline / cancel "Resultados de la respuesta del usuario" Tres ramas que el servidor maneja
Desambiguación "Elige uno" Caso de uso común de elicitación cuando una herramienta tiene N candidatos
Formulario plano (flat) "Solo propiedades de primer nivel" Los esquemas de elicitación no se pueden anidar

Lectura Adicional

0 lifetime access. Curriculum based on AI Engineering from Scratch by Rohit Ghumare (MIT, used under attribution).