Modal MCP

Un servidor MCP para gestionar aplicaciones, contenedores, volúmenes y secretos de Modal. También ayuda a desplegar y ejecutar aplicaciones de Modal directamente desde Claude Code y otros clientes MCP.

Documentación

Servidor MCP Modal

mcp-modal MCP server

PyPI

Un servidor MCP para gestionar Modal — aplicaciones, contenedores, volúmenes y secretos — y para desplegar y ejecutar aplicaciones Modal directamente desde Claude Code y otros clientes MCP.

Cada herramienta invoca tu CLI local de modal, por lo que opera con el perfil y las credenciales de Modal configurados en tu máquina. No hay tokens adicionales que gestionar.

Instalación

El servidor está publicado en PyPI como mcp-modal. No se necesita instalación manual — la forma recomendada de ejecutarlo es con uvx, que lo descarga y lo lanza bajo demanda. Solo tienes que apuntar tu cliente MCP al comando siguiente (consulta Configuración).

Cada versión también está etiquetada y publicada en la página de Releases, con notas de la versión y los mismos .whl / .tar.gz que sirve PyPI adjuntos — útil para fijar versiones, instalaciones en entornos aislados o para leer qué cambió entre dos versiones.

Iniciar sesión en Modal

Este servidor usa tus credenciales locales de Modal. Si aún no te has autenticado, ejecuta:

modal setup

Esto abre un navegador para iniciar sesión y guarda un token en ~/.modal.toml. ¿Ya has iniciado sesión en otro lugar? Compruébalo con modal profile current.

Configuración

Añade el servidor a Claude Code con la CLI de claude mcp:

claude mcp add mcp-modal -- uvx mcp-modal@latest

O añádelo a un archivo .mcp.json en la raíz de tu proyecto, que es la mejor opción para un equipo — todos los que abran el repositorio obtienen la misma configuración:

{
  "mcpServers": {
    "mcp-modal": {
      "command": "uvx",
      "args": ["mcp-modal@latest"]
    }
  }
}

Por qué @latest, y cuándo fijar versión en su lugar

uvx almacena en caché el entorno que construye en la primera ejecución y no vuelve a consultar PyPI:

"uvx utilizará la versión más reciente disponible de la herramienta solicitada en la primera invocación. Después, uvx utilizará la versión en caché de la herramienta a menos que se solicite una versión diferente, se limpie la caché o se actualice la caché." — documentación de uv

Así que un uvx mcp-modal simple significa la más reciente en el momento de la instalación, congelada para siempre después — reiniciar el cliente o el sistema no cambia nada, porque la caché vive en el disco. Personas diferentes terminan en versiones diferentes según cuándo lo ejecutaron por primera vez, sin ninguna advertencia.

  • mcp-modal@latest vuelve a resolver en cada lanzamiento, por lo que un reinicio recoge nuevas versiones. Cuesta un viaje de ida y vuelta de red al inicio. Úsalo mientras la superficie de herramientas aún se está moviendo.
  • mcp-modal@0.4.0 (una versión explícita) es reproducible y las actualizaciones se convierten en un cambio deliberado de una línea. Úsalo cuando quieras estabilidad, o para una audiencia más amplia.

Para mover una máquina que ya está atascada en una compilación antigua en caché, cambiar a cualquiera de las dos formas anteriores es suficiente — solicitar una versión invalida la caché. De lo contrario, uv cache clean mcp-modal fuerza una actualización.

Requisitos

  • Python 3.11 o superior
  • uv (proporciona uvx)
  • CLI de Modal 1.5 o más reciente, configurada con credenciales válidas (modal setup) — 1.5 es donde aterrizaron modal billing summary/rates y donde el informe de facturación cambió a columnas snake_case; la herramienta de costos lee ambas grafías pero necesita 1.5 para esas dos vistas
  • Para soporte de deploy y run de Modal:
    • El proyecto que se despliega/ejecuta debe usar uv para la gestión de dependencias
    • modal debe estar instalado en el entorno virtual de ese proyecto

Seguridad

Este servidor invoca tu CLI local de modal usando las credenciales que haya en ~/.modal.toml. Algunas herramientas son potentes por diseño — si el cliente MCP que maneja el servidor sufre una inyección de prompt (por ejemplo, por texto malicioso dentro de los registros que obtiene), estas son las vías de escalada y deberían permanecer detrás de los avisos de aprobación de herramientas de tu cliente en lugar de estar aprobadas automáticamente:

  • deploy_modal_app / run_modal_app — ejecutan Python local arbitrario en el host (modal deploy importa el archivo de la aplicación; uv run resuelve e instala las dependencias del proyecto objetivo).
  • modal_volume_files con action="put" — puede leer cualquier archivo local (p. ej. ~/.ssh/id_rsa, ~/.modal.toml) y subirlo a un volumen en la nube (una primitiva de exfiltración de datos).
  • modal_volume_files con action="get" y force=True — puede sobrescribir cualquier ruta local (p. ej. ~/.zshrc o un perfil de shell, una primitiva de persistencia).
  • manage_modal_container con action="exec" — ejecuta comandos arbitrarios dentro de un contenedor, por diseño.

Cada herramienta declara anotaciones de herramientas MCP, para que un cliente pueda distinguir las cuatro herramientas de solo lectura (list_modal_resources, get_modal_logs, search_modal_logs, analyze_modal_costs — todas readOnlyHint: true) de las ocho que cambian el estado remoto o inician cómputo. Seis de esas ocho son destructiveHint: true; las excepciones son run_modal_app y inspect_modal_secret, que inician cómputo sin eliminar ni sobrescribir nada. Aprueba automáticamente las lecturas; mantén el resto detrás de un aviso.

Lista blanca de rutas locales opcional

Para contener las dos herramientas de volúmenes que tocan el sistema de archivos, establece la variable de entorno MCP_MODAL_ALLOWED_LOCAL_PATHS a una lista separada por os.pathsep de directorios (: en macOS/Linux). Cuando está establecida, modal_volume_files se rechaza para cualquier ruta local — local_path en action="put", el destino en action="get" — a menos que la ruta resuelta, después de expandir ~ y colapsar ../enlaces simbólicos, caiga dentro de una de esas raíces. El destino de descarga "-" (devolver el contenido en lugar de escribir un archivo) está exento porque no se escribe nada en el disco.

Cuando la variable está sin establecer (el valor predeterminado) no hay restricción, por lo que las configuraciones existentes no se ven afectadas. Configúrala en tu cliente MCP, p. ej.:

{
  "mcpServers": {
    "mcp-modal": {
      "command": "uvx",
      "args": ["mcp-modal"],
      "env": { "MCP_MODAL_ALLOWED_LOCAL_PATHS": "/Users/me/modal-workspace:/tmp/modal" }
    }
  }
}

Todas las herramientas también pasan nombres/rutas proporcionados por el usuario después de un separador de fin de opciones --, por lo que un valor que comienza con - siempre se trata como datos, nunca como una bandera de CLI de modal. Los valores secretos entregados a manage_modal_secret se redactan del comando mostrado, los registros y cualquier salida de error.

Herramientas compatibles

12 herramientas. Las operaciones relacionadas se agrupan detrás de un argumento action/resource en lugar de dividirse una por subcomando de CLI: cada esquema de herramienta se carga en el contexto del modelo para toda la sesión, por lo que una superficie más pequeña deja más espacio para tu trabajo real (y le da al modelo menos herramientas casi idénticas entre las que elegir).

Las herramientas que hablan con recursos de ámbito de entorno toman un argumento opcional env para apuntar a un entorno de Modal específico; si se omite, usan el predeterminado del perfil (o MODAL_ENVIRONMENT). La excepción es manage_modal_container y los registros de contenedores — un ID de contenedor es globalmente único y la CLI no acepta entorno allí.

Solo lectura

  1. Listar recursos de Modal (list_modal_resources) — una sola consulta para toda la cuenta.

    • Parámetros: resource (obligatorio), name, path (predeterminado /), env
    • Valores de resource:
      valordevuelvename significa
      appsaplicaciones desplegadas/ejecutándose/detenidas recientemente—
      app_historyversiones de despliegue de una aplicación (para rollback)nombre/ID de la aplicación
      containerscontenedores en ejecución (ta-...)ID de la aplicación para filtrar
      volumesvolúmenes con nombre—
      volume_filesarchivos dentro de un volumen (con path)nombre del volumen
      secretsnombres de secretos (los valores nunca se exponen)—
      environmentsvalores válidos de env para este espacio de trabajo—
      profileperfil activo + todos los perfiles—
    • volume_files establece empty: true con un mensaje cuando un listado genuinamente devuelve nada, para que un directorio vacío se distinga de una ruta incorrecta.
    • Los listados de más de 200 entradas se limitan, con omitted_items indicando el número descartado.
  2. Obtener registros de Modal (get_modal_logs) — obtener o transmitir registros de una aplicación o un contenedor.

    • Parámetros: identifier (obligatorio), target (auto/app/container, predeterminado auto — cualquier cosa que comience con ta- es un contenedor), timeout_seconds (predeterminado 30), env, since, until, tail, source (stdout/stderr/system), timestamps, follow
    • since sin tail obtiene todas las entradas en el rango; pasa until también (rango máximo 35 días, tail máximo 20,000) para mantener acotada la salida de una aplicación ocupada.
    • Con follow=True, los registros se transmiten hasta que la aplicación/contenedor se detiene o se alcanza timeout_seconds, devolviendo una instantánea con truncated: true.
    • Solo cubre los flujos stdout/stderr/sistema; algunos fallos (p. ej. un bloqueo reportado como "... salió con ...") son eventos del panel de Modal, no líneas de registro, y no aparecerán aquí.
  3. Buscar registros de Modal (search_modal_logs) — busca en los registros y obtén cada coincidencia con las líneas circundantes, diseñado para depurar "¿dónde salió mal?". Los registros se obtienen una vez y se buscan localmente, por lo que obtienes contexto, expresiones regulares, control de mayúsculas y recuentos exactos de coincidencias.

    • Parámetros: identifier (obligatorio), pattern (obligatorio), target (predeterminado auto), regex, case_sensitive, context_lines (predeterminado 3), max_matches (predeterminado 50), since, until, tail (predeterminado a las últimas 1000 entradas), source, exclude (eliminar líneas de ruido antes de buscar, p. ej. "queue put failed"), prefilter, timestamps (predeterminado true), timeout_seconds, env
    • Acota la ventana en una aplicación ocupada. since por sí solo obtiene todo desde entonces hasta ahora — cientos de KB por hora en una aplicación locuaz, que la búsqueda de 30s corta (logs_truncated: true) y el presupuesto de salida recorta. since y until alrededor del minuto que te importa es la solución, y suele ser kilobytes.
    • prefilter=True empuja pattern hacia Modal como un filtro de subcadena del lado del servidor (modal app logs --search), por lo que las líneas que no coinciden nunca se obtienen — la palanca para registros demasiado grandes para drenar. Requiere regex=False, y las líneas de contexto muestran entonces solo otras coincidencias, así que úsalo para localizar la ventana y vuelve a consultarla con prefilter=False.
    • Devuelve match_count y matches: bloques de contexto con marca de tiempo y número de línea donde las líneas coincidentes tienen el prefijo >, p. ej. > 8: 2026-06-04T... ValueError: bad input. Todo el registro obtenido siempre se busca, por lo que match_count sigue siendo exacto incluso cuando se devuelven menos bloques. returned es cuántas coincidencias llegaron (las coincidencias adyacentes se fusionan en un bloque, contadas por returned_blocks). Reporta excluded_lines cuando exclude se usa.
    • Una ventana que la CLI rechaza (rango invertido, más de 35 días, tail más de 20,000) vuelve como success: false con el mensaje propio de Modal, no un código de salida desnudo.
    • Misma advertencia de solo stdout/stderr/sistema que get_modal_logs.

Desplegar y ejecutar

  1. Desplegar aplicación Modal (deploy_modal_app)

    • Despliega una aplicación Modal (modal deploy). Los endpoints web desplegados persisten, por lo que cualquier enlace en la salida está activo y se puede compartir (devueltos en urls).
    • Parámetros: absolute_path_to_app (obligatorio), env, name, tag, strategy (rolling/recreate), stream_logs
    • El directorio de la aplicación debe usar uv con modal instalado en su virtualenv.
  2. Ejecutar App de Modal (run_modal_app)

    • Ejecuta una función o punto de entrada local una vez y recopila su salida (modal run).
    • Parámetros: absolute_path_to_app (obligatorio), function_name, env, detach, timeout_seconds (predeterminado 120)
    • Devuelve una instantánea con truncated: true si la ejecución aún continúa al agotarse el tiempo de espera. Pase detach=True para mantener trabajos largos activos en Modal más allá del tiempo de espera.

¿Por qué no hay una herramienta modal serve? modal serve solo mantiene sus endpoints activos mientras el proceso de bloqueo se ejecuta — una herramienta MCP que devuelve resultados los eliminaría inmediatamente, entregando una URL muerta. Use deploy_modal_app para un endpoint persistente y compartible.

Cambios de estado

  1. Gestionar App de Modal (manage_modal_app) — action es stop (apagar la app y terminar sus contenedores) o rollback (redesplegar una versión anterior).

    • Parámetros: action (obligatorio), app_identifier (obligatorio), version (solo reversión — predeterminado a la versión inmediatamente anterior), env
  2. Gestionar Contenedor de Modal (manage_modal_container) — action es exec (ejecutar un comando dentro de un contenedor en ejecución, modal container exec --no-pty) o stop (terminarlo).

    • Parámetros: action (obligatorio), container_id (obligatorio), command (solo exec — una lista de argumentos, p. ej. ["python", "-c", "print('hi')"]), timeout_seconds (predeterminado 60)
  3. Gestionar Volumen de Modal (manage_modal_volume) — action es create, delete (el volumen y todos sus datos, irreversible), o rename.

    • Parámetros: action (obligatorio), volume_name (obligatorio), new_name (solo renombrar), env
  4. Archivos de Volumen de Modal (modal_volume_files) — operaciones de escritura en los archivos de un volumen: action es put (subir), get (descargar), cp (copiar dentro del volumen), o rm.

    • Parámetros: action (obligatorio), volume_name (obligatorio), local_path, remote_path, paths (para cp: orígenes y luego destino), recursive, force, env
    • action="get" con local_path="-" devuelve el contenido del archivo en lugar de escribir un archivo.
    • Para listar el contenido de un volumen use list_modal_resources(resource="volume_files").
  5. Gestionar Secreto de Modal (manage_modal_secret) — action es create o delete.

    • Parámetros: action (obligatorio), secret_name (obligatorio), key_values (dict), from_dotenv (ruta), from_json (ruta), force, env. Crear requiere al menos uno de key_values, from_dotenv, o from_json.
    • Los valores de los secretos se redactan de cada campo devuelto, incluida la salida de errores.
    • Para listar nombres de secretos use list_modal_resources(resource="secrets").

Costos

  1. Analizar Costos de Modal (analyze_modal_costs) — solo lectura. Obtiene modal billing una vez y agrega localmente, para que obtenga totales clasificados y cambios de período a período en lugar de cientos de filas sin procesar.
    • Parámetros: view (predeterminado by_app), period, start, end, resolution (d/h), timezone, app, environment, top_n (predeterminado 10), tag_names
    • Valores de view:
      valorresponde
      by_app"¿cuál es mi app más costosa?" — apps clasificadas por gasto, con % de participación
      timeline"¿por qué el lunes fue caro?" — costo por intervalo, más un explanation que compara el intervalo pico con el anterior y clasifica qué apps crecieron
      by_environmenta qué entorno va el dinero
      by_resourceCPU vs clase GPU vs memoria vs almacenamiento
      summarycosto facturado vs medido para un ciclo mensual, con ajustes de créditos/plan
      ratesprecios unitarios actuales
    • total_cost siempre cubre cada fila en el rango, incluso cuando groups se reduce a top_n — cítelo en lugar de sumar las filas visibles.
    • La facturación es a nivel de espacio de trabajo (la CLI no acepta -e), por lo que esto informa en todos los entornos; environment filtra las filas después.
    • Modal informa solo intervalos completos, por lo que un día parcialmente transcurrido se lee bajo.

Secretos — inspección

  1. Inspeccionar Secreto de Modal (inspect_modal_secret) — lista los nombres de claves dentro de un secreto, nunca los valores.
    • Parámetros: secret_name (obligatorio), env, image, timeout_seconds (predeterminado 300)
    • Modal no expone ninguna API para esto por diseño: ni la CLI, ni el SDK, ni la capa gRPC. La única forma de ver qué claves define un secreto es montarlo en un contenedor y listar el entorno. Entonces esta herramienta ejecuta modal shell --secret <name> con compgen -e (un builtin de bash que imprime solo nombres de variables exportadas — ningún valor se imprime jamás, incluso dentro del contenedor), luego resta las variables que la imagen y el runtime de Modal establecen de todos modos — 23 nombres conocidos más cualquier cosa bajo seis prefijos (MODAL_, PYTHON, PIP_, NVIDIA_, CUDA_, LD_LIBRARY_PATH), que también cubre las credenciales de MODAL_TOKEN_* que viven en cada contenedor.
    • Esta única llamada inicia cómputo remoto, por lo que cuesta unos centavos y toma decenas de segundos (más cuando la imagen tiene que construirse). Cada otra lectura en este servidor es gratuita; use list_modal_resources(resource="secrets") para ver qué secretos existen y recurra a esto solo cuando necesite saber qué hay dentro de uno.
    • Devuelve keys, más el all_env_names sin filtrar para que una clave que parece una variable de runtime siga siendo visible en lugar de descartarse silenciosamente.
    • Omita image para usar el predeterminado de Modal (diseñado para coincidir con el Python del servidor — la opción más confiable). Pase uno, p. ej. python:3.12-slim, si el constructor de imágenes de su espacio de trabajo rechaza esa versión de Python.

Prompts

El servidor también incluye cuatro prompts de MCP — flujos de trabajo de múltiples pasos que su cliente puede invocar directamente (en Claude Code aparecen como /mcp__mcp-modal__<name>). Los prompts se obtienen bajo demanda, por lo que a diferencia de las herramientas no cuestan nada en contexto por sesión:

  • debug_modal_app (app_name, opcional symptom) — una rutina de triaje ordenada: verificar que la app esté activa, buscar tracebacks en los registros con contexto, reducir la ventana en lugar de ampliarla cuando una búsqueda de registros regresa truncada, recurrir a la cola de registros, verificar si las apps hermanas fueron afectadas en la misma ventana, inspeccionar contenedores, luego comparar contra el historial de despliegues y considerar una reversión.
  • deploy_and_verify (absolute_path_to_app, opcional env) — confirmar el espacio de trabajo objetivo, desplegar, informar las URLs en vivo, luego verificar que la app esté saludable en lugar de asumirlo.
  • review_modal_account (opcional env) — un inventario de solo lectura que señala apps inactivas, contenedores en ejecución inexplicables y volúmenes/secretos huérfanos, nombrando la llamada exacta que limpiaría cada uno sin ejecutarla.
  • investigate_modal_costs (opcional period, app) — rastrea un aumento de gasto desde la línea de tiempo diaria hasta la hora pico, la clase de recurso y el despliegue o contenedor aún en ejecución detrás de él.

Límites de salida

La salida de registros, ejecuciones y exec se limita antes de devolverse, para que una app habladora no inunde su ventana de contexto. El presupuesto predeterminado es de 40,000 caracteres por campo de texto (aproximadamente 10k tokens); cuando un campo se recorta, el resultado establece output_capped: true y el texto lleva un marcador que indica cuánto se descartó. Un campo limitado conserva su inicio y su final, por lo que un banner de inicio y el traceback al final ambos sobreviven.

La búsqueda nunca se limita antes del hecho: search_modal_logs examina todo el registro obtenido y solo limita cuántos bloques de contexto regresan, por lo que match_count siempre es exacto.

Elevar timeout_seconds o el presupuesto rara vez es la respuesta correcta a una búsqueda de registros truncada — obtener menos es. Limite la ventana con since y until, filtre con source/exclude, o establezca prefilter=True para descartar líneas que no coincidan dentro de Modal.

Establezca MCP_MODAL_MAX_OUTPUT_CHARS para subir o bajar el presupuesto, o a 0 para deshabilitar el límite por completo:

{
  "mcpServers": {
    "mcp-modal": {
      "command": "uvx",
      "args": ["mcp-modal"],
      "env": { "MCP_MODAL_MAX_OUTPUT_CHARS": "80000" }
    }
  }
}

Formato de respuesta

Todas las herramientas devuelven respuestas en un formato estandarizado, con ligeras variaciones según el tipo de operación:

# Lookups (list_modal_resources):
{
    "success": True,
    "apps": [...],          # or "containers", "volumes", "contents", "secrets", ...
    "omitted_items": 0      # present when the listing was capped at 200 entries
}

# Action operations (deploy, stop, rollback, create, delete, rename, cp, put, get, rm):
{
    "success": True,
    "message": "Operation successful message",
    "command": "executed command string",
    "stdout": "command output",  # if any
    "stderr": "error output"     # if any
}

# Log / run / exec operations (snapshot-based):
{
    "success": True,
    "logs": "...",          # or "output" for run/exec
    "truncated": False,     # True when cut off at timeout_seconds
    "output_capped": False, # True when text was trimmed to fit MCP_MODAL_MAX_OUTPUT_CHARS
    "command": "executed command string"
}

# Log search (search_modal_logs):
{
    "success": True,
    "match_count": 12,      # exact: the whole fetched log is searched
    "returned": 5,          # matches actually shown
    "returned_blocks": 2,   # adjacent matches merge into one context block
    "matches": ["> 8: ...", ...],
    "logs_truncated": False,  # True when the log fetch hit timeout_seconds
    "output_capped": False,
    "command": "executed command string"
}

# Error case (all operations):
{
    "success": False,
    "error": "Error message describing what went wrong",
    "command": "executed command string",
    "stdout": "command output",  # if available
    "stderr": "error output"     # if available
}

Licencia

Este proyecto está licenciado bajo la Licencia MIT — consulte el archivo LICENSE para más detalles.