Modal MCP

Um servidor MCP para gerenciar aplicativos, containers, volumes e segredos do Modal. Também ajuda a implantar e executar aplicativos Modal diretamente do Claude Code e de outros clientes MCP.

Documentação

Servidor MCP Modal

mcp-modal MCP server

PyPI

Um servidor MCP para gerenciar Modal — apps, contêineres, volumes e segredos — e para implantar e executar apps Modal diretamente do Claude Code e de outros clientes MCP.

Cada ferramenta executa comandos na sua CLI modal local, então opera com o perfil e as credenciais Modal configurados na sua máquina. Não há tokens extras para gerenciar.

Instalação

O servidor é publicado no PyPI como mcp-modal. Não é necessária instalação manual — a forma recomendada de executá-lo é com uvx, que o busca e inicia sob demanda. Basta apontar seu cliente MCP para o comando abaixo (veja Configuração).

Cada versão também é marcada e publicada na página de Releases, com notas de versão e os mesmos .whl / .tar.gz que o PyPI serve anexados — útil para fixar versões, instalações isoladas ou para ler o que mudou entre duas versões.

Fazendo login no Modal

Este servidor usa suas credenciais locais do Modal. Se você ainda não se autenticou, execute:

modal setup

Isso abre um navegador para login e armazena um token em ~/.modal.toml. Já está logado em outro lugar? Verifique com modal profile current.

Configuração

Adicione o servidor ao Claude Code com a CLI claude mcp:

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

Ou adicione-o a um arquivo .mcp.json na raiz do seu projeto, que é a melhor opção para um time — todos que abrirem o repositório recebem a mesma configuração:

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

Por que @latest, e quando fixar versão

uvx armazena em cache o ambiente que ele cria na primeira execução e não verifica o PyPI novamente:

"uvx usará a versão mais recente disponível da ferramenta solicitada na primeira invocação. Depois disso, uvx usará a versão em cache da ferramenta, a menos que uma versão diferente seja solicitada, o cache seja limpo ou o cache seja atualizado." — documentação do uv

Então um uvx mcp-modal simples significa mais recente no momento da instalação, congelado para sempre depois — reiniciar o cliente ou o sistema não muda nada, porque o cache fica no disco. Pessoas diferentes acabam em versões diferentes dependendo de quando executaram pela primeira vez, sem aviso.

  • mcp-modal@latest re-resolve a cada inicialização, então uma reinicialização pega novos lançamentos. Custa uma ida à rede na inicialização. Use enquanto a superfície da ferramenta ainda está mudando.
  • mcp-modal@0.4.0 (uma versão explícita) é reproduzível e atualizações se tornam uma mudança deliberada de uma linha. Use quando quiser estabilidade, ou para um público maior.

Para mover uma máquina que já está presa em um build antigo em cache, mudar para qualquer uma das formas acima é suficiente — solicitar uma versão invalida o cache. Caso contrário, uv cache clean mcp-modal força uma atualização.

Requisitos

  • Python 3.11 ou superior
  • uv (fornece uvx)
  • CLI Modal 1.5 ou mais recente, configurada com credenciais válidas (modal setup) — 1.5 é onde modal billing summary/rates chegaram e onde o relatório de cobrança mudou para colunas snake_case; a ferramenta de custo lê ambas as grafias, mas precisa da 1.5 para essas duas visualizações
  • Para suporte a deploy e run do Modal:
    • O projeto sendo implantado/executado deve usar uv para gerenciamento de dependências
    • modal deve estar instalado no ambiente virtual desse projeto

Segurança

Este servidor executa comandos na sua CLI modal local usando as credenciais que estão em ~/.modal.toml. Algumas ferramentas são poderosas por design — se o cliente MCP que dirige o servidor for vítima de injeção de prompt (por exemplo, por texto malicioso dentro de logs que ele buscou), estes são os caminhos de escalada e devem permanecer atrás dos prompts de aprovação de ferramentas do seu cliente em vez de serem aprovados automaticamente:

  • deploy_modal_app / run_modal_app — executam Python local arbitrário no host (modal deploy importa o arquivo do app; uv run resolve e instala as dependências do projeto alvo).
  • modal_volume_files com action="put" — pode ler qualquer arquivo local (ex.: ~/.ssh/id_rsa, ~/.modal.toml) e enviá-lo para um volume na nuvem (uma primitiva de exfiltração de dados).
  • modal_volume_files com action="get" e force=True — pode sobrescrever qualquer caminho local (ex.: ~/.zshrc ou um perfil de shell, uma primitiva de persistência).
  • manage_modal_container com action="exec" — executa comandos arbitrários dentro de um contêiner, por design.

Cada ferramenta declara anotações de ferramentas MCP, para que um cliente possa distinguir as quatro ferramentas somente leitura (list_modal_resources, get_modal_logs, search_modal_logs, analyze_modal_costs — todas readOnlyHint: true) das oito que alteram estado remoto ou iniciam computação. Seis dessas oito são destructiveHint: true; as exceções são run_modal_app e inspect_modal_secret, que iniciam computação sem remover ou sobrescrever nada. Aprovação automática para as leituras; mantenha o resto atrás de um prompt.

Lista de permissões opcional de caminhos locais

Para conter as duas ferramentas de volume que tocam no sistema de arquivos, defina a variável de ambiente MCP_MODAL_ALLOWED_LOCAL_PATHS para uma lista de diretórios separada por os.pathsep (: no macOS/Linux). Quando definida, modal_volume_files é recusada para qualquer caminho local — local_path em action="put", o destino em action="get" — a menos que o caminho resolvido, após expandir ~ e colapsar ../symlinks, caia dentro de uma dessas raízes. O destino de download "-" (retornar conteúdo em vez de escrever um arquivo) é isento porque nada é gravado no disco.

Quando a variável está não definida (o padrão) não há restrição, então configurações existentes não são afetadas. Configure-a no seu cliente MCP, ex.:

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

Todas as ferramentas também passam nomes/caminhos fornecidos pelo usuário após um separador de fim de opções --, então um valor começando com - é sempre tratado como dado, nunca como uma flag da CLI modal. Valores de segredos passados para manage_modal_secret são ocultados do comando ecoado, logs e qualquer saída de erro.

Ferramentas Suportadas

12 ferramentas. Operações relacionadas são agrupadas atrás de um argumento action/resource em vez de divididas uma-por-subcomando-CLI: cada esquema de ferramenta é carregado no contexto do modelo para a sessão inteira, então uma superfície menor deixa mais espaço para o seu trabalho real (e dá ao modelo menos ferramentas quase idênticas para escolher).

Ferramentas que falam com recursos com escopo de ambiente aceitam um argumento opcional env para mirar um ambiente Modal específico; se omitido, elas usam o padrão do perfil (ou MODAL_ENVIRONMENT). A exceção é manage_modal_container e logs de contêiner — um ID de contêiner é globalmente único e a CLI não aceita ambiente ali.

Somente leitura

  1. Listar Recursos Modal (list_modal_resources) — uma consulta para a conta inteira.

    • Parâmetros: resource (obrigatório), name, path (padrão /), env
    • Valores de resource:
      valorretornaname significa
      appsapps implantados/em execução/parados recentemente—
      app_historyversões de implantação de um app (para rollback)nome/ID do app
      containerscontêineres em execução (ta-...)ID do app para filtrar
      volumesvolumes nomeados—
      volume_filesarquivos dentro de um volume (com path)nome do volume
      secretsnomes de segredos (valores nunca são expostos)—
      environmentsvalores válidos de env para este workspace—
      profileperfil ativo + todos os perfis—
    • volume_files define empty: true com uma mensagem quando uma listagem genuinamente retorna nada, para que um diretório vazio seja distinguível de um caminho errado.
    • Listagens acima de 200 entradas são limitadas, com omitted_items dando o número descartado.
  2. Obter Logs Modal (get_modal_logs) — busca ou transmite logs de um app ou um contêiner.

    • Parâmetros: identifier (obrigatório), target (auto/app/container, padrão auto — qualquer coisa começando com ta- é um contêiner), timeout_seconds (padrão 30), env, since, until, tail, source (stdout/stderr/system), timestamps, follow
    • since sem tail busca todas as entradas no intervalo; passe until também (intervalo máximo 35 dias, tail máximo 20.000) para manter a saída de um app movimentado limitada.
    • Com follow=True, os logs são transmitidos até o app/contêiner parar ou timeout_seconds ser alcançado, retornando um instantâneo com truncated: true.
    • Cobre apenas os fluxos stdout/stderr/sistema; algumas falhas (ex.: uma falha relatada como "... saiu com ...") são eventos do painel Modal, não linhas de log, e não aparecerão aqui.
  3. Pesquisar Logs Modal (search_modal_logs) — busca em logs e obtém cada ocorrência com as linhas ao redor, feito para depuração de "onde deu errado?". Os logs são buscados uma vez e pesquisados localmente, então você obtém contexto, regex, controle de maiúsculas/minúsculas e contagens exatas de correspondências.

    • Parâmetros: identifier (obrigatório), pattern (obrigatório), target (padrão auto), regex, case_sensitive, context_lines (padrão 3), max_matches (padrão 50), since, until, tail (padrão para as últimas 1000 entradas), source, exclude (remove linhas de ruído antes de pesquisar, ex.: "queue put failed"), prefilter, timestamps (padrão true), timeout_seconds, env
    • Limite a janela em um app movimentado. since sozinho busca tudo desde então até agora — centenas de KB por hora em um app tagarela, que a busca de 30s corta (logs_truncated: true) e o orçamento de saída reduz. since e until em torno do minuto que você quer é a correção, e geralmente são kilobytes.
    • prefilter=True empurra pattern para o Modal como um filtro de substring no lado do servidor (modal app logs --search), então linhas sem correspondência nunca são buscadas — a alavanca para logs grandes demais para drenar. Requer regex=False, e linhas de contexto mostram então apenas outras correspondências, então use para localizar a janela e re-consultar com prefilter=False.
    • Retorna match_count e matches: blocos de contexto com timestamp e número de linha onde linhas correspondentes são prefixadas com >, ex.: > 8: 2026-06-04T... ValueError: bad input. Todo o log buscado é sempre pesquisado, então match_count permanece exato mesmo quando menos blocos são retornados. returned é quantas correspondências vieram (correspondências adjacentes se fundem em um bloco, contadas por returned_blocks). Relata excluded_lines quando exclude é usado.
    • Uma janela que a CLI rejeita (intervalo invertido, acima de 35 dias, tail acima de 20.000) retorna como success: false com a mensagem do próprio Modal, não um código de saída simples.
    • Mesma ressalva de stdout/stderr/sistema apenas que get_modal_logs.

Implantar e executar

  1. Implantar App Modal (deploy_modal_app)

    • Implanta um app Modal (modal deploy). Endpoints web implantados persistem, então qualquer link na saída está ativo e compartilhável (retornado em urls).
    • Parâmetros: absolute_path_to_app (obrigatório), env, name, tag, strategy (rolling/recreate), stream_logs
    • O diretório do app deve usar uv com modal instalado em seu virtualenv.
  2. Executar App Modal (run_modal_app)

    • Executa uma função ou entrypoint local uma vez e coleta sua saída (modal run).
    • Parâmetros: absolute_path_to_app (obrigatório), function_name, env, detach, timeout_seconds (padrão 120)
    • Retorna um snapshot com truncated: true se a execução ainda estiver em andamento no timeout. Passe detach=True para manter jobs longos ativos no Modal além do timeout.

Por que não há ferramenta modal serve? modal serve só mantém seus endpoints ativos enquanto o processo de bloqueio está em execução — uma ferramenta MCP que retorna os derrubaria imediatamente, devolvendo uma URL morta. Use deploy_modal_app para um endpoint persistente e compartilhável.

Mudanças de estado

  1. Gerenciar App Modal (manage_modal_app) — action é stop (desligar o app e encerrar seus contêineres) ou rollback (reimplantar uma versão anterior).

    • Parâmetros: action (obrigatório), app_identifier (obrigatório), version (somente rollback — padrão é a versão imediatamente anterior), env
  2. Gerenciar Contêiner Modal (manage_modal_container) — action é exec (executar um comando dentro de um contêiner em execução, modal container exec --no-pty) ou stop (encerrá-lo).

    • Parâmetros: action (obrigatório), container_id (obrigatório), command (somente exec — uma lista de argumentos, ex.: ["python", "-c", "print('hi')"]), timeout_seconds (padrão 60)
  3. Gerenciar Volume Modal (manage_modal_volume) — action é create, delete (o volume e todos os seus dados, irreversível), ou rename.

    • Parâmetros: action (obrigatório), volume_name (obrigatório), new_name (somente renomear), env
  4. Arquivos de Volume Modal (modal_volume_files) — operações de escrita nos arquivos de um volume: action é put (upload), get (download), cp (copiar dentro do volume), ou rm.

    • Parâmetros: action (obrigatório), volume_name (obrigatório), local_path, remote_path, paths (para cp: origens e depois destino), recursive, force, env
    • action="get" com local_path="-" retorna o conteúdo do arquivo em vez de gravar um arquivo.
    • Para listar o conteúdo de um volume, use list_modal_resources(resource="volume_files").
  5. Gerenciar Segredo Modal (manage_modal_secret) — action é create ou delete.

    • Parâmetros: action (obrigatório), secret_name (obrigatório), key_values (dict), from_dotenv (caminho), from_json (caminho), force, env. A criação exige pelo menos um de key_values, from_dotenv ou from_json.
    • Os valores dos segredos são ocultados de todos os campos retornados, incluindo saída de erro.
    • Para listar nomes de segredos, use list_modal_resources(resource="secrets").

Custos

  1. Analisar Custos Modal (analyze_modal_costs) — somente leitura. Busca modal billing uma vez e agrega localmente, para que você obtenha totais classificados e mudanças período a período em vez de centenas de linhas brutas.
    • Parâmetros: view (padrão by_app), period, start, end, resolution (d/h), timezone, app, environment, top_n (padrão 10), tag_names
    • Valores de view:
      valorresponde
      by_app"qual é o meu app mais caro?" — apps classificados por gasto, com participação %
      timeline"por que segunda-feira foi cara?" — custo por intervalo, além de um explanation que compara o intervalo de pico com o anterior e classifica quais apps cresceram
      by_environmentpara qual ambiente o dinheiro vai
      by_resourceCPU vs classe GPU vs memória vs armazenamento
      summarycusto faturado vs medido para um ciclo mensal, com ajustes de créditos/plano
      ratespreços unitários atuais
    • total_cost sempre cobre todas as linhas no intervalo, mesmo quando groups é reduzido para top_n — cite-o em vez de somar as linhas visíveis.
    • O faturamento é por workspace (a CLI não aceita -e), então isso reporta em todos os ambientes; environment filtra as linhas depois.
    • O Modal reporta apenas intervalos completos, então um dia parcialmente decorrido aparece menor.

Segredos — inspeção

  1. Inspecionar Segredo Modal (inspect_modal_secret) — lista os nomes das chaves dentro de um segredo, nunca os valores.
    • Parâmetros: secret_name (obrigatório), env, image, timeout_seconds (padrão 300)
    • O Modal não expõe nenhuma API para isso por design: nem a CLI, nem o SDK, nem a camada gRPC. A única maneira de ver quais chaves um segredo define é montá-lo em um contêiner e listar o ambiente. Então esta ferramenta executa modal shell --secret <name> com compgen -e (um builtin do bash que imprime apenas os nomes das variáveis exportadas — nenhum valor é jamais impresso, mesmo dentro do contêiner) e depois subtrai as variáveis que a imagem e o runtime do Modal definem de qualquer forma — 23 nomes conhecidos mais qualquer coisa sob seis prefixos (MODAL_, PYTHON, PIP_, NVIDIA_, CUDA_, LD_LIBRARY_PATH), o que também cobre as credenciais MODAL_TOKEN_* que existem em todo contêiner.
    • Esta única chamada inicia computação remota, então custa alguns centavos e leva dezenas de segundos (mais quando a imagem precisa ser construída). Todas as outras leituras neste servidor são gratuitas; use list_modal_resources(resource="secrets") para ver quais segredos existem e recorra a esta apenas quando precisar saber o que está dentro de um.
    • Retorna keys, além do all_env_names sem filtro, para que uma chave que pareça uma variável de runtime ainda fique visível em vez de ser descartada silenciosamente.
    • Omita image para usar o padrão do Modal (criado para corresponder ao Python do servidor — a escolha mais confiável). Passe um, ex.: python:3.12-slim, se o construtor de imagens do seu workspace rejeitar essa versão do Python.

Prompts

O servidor também inclui quatro prompts MCP — fluxos de trabalho em várias etapas que seu cliente pode invocar diretamente (no Claude Code eles aparecem como /mcp__mcp-modal__<name>). Os prompts são buscados sob demanda, então, ao contrário das ferramentas, não custam nada no contexto por sessão:

  • debug_modal_app (app_name, symptom opcional) — uma rotina de triagem ordenada: verificar se o app está no ar, buscar logs por tracebacks com contexto, estreitar a janela em vez de alargá-la quando uma busca de log retorna truncada, recorrer ao final do log, verificar se apps irmãos foram afetados na mesma janela, inspecionar contêineres e depois comparar com o histórico de implantação e considerar um rollback.
  • deploy_and_verify (absolute_path_to_app, env opcional) — confirmar o workspace de destino, implantar, reportar as URLs ao vivo e depois verificar se o app está saudável em vez de presumir que está.
  • review_modal_account (env opcional) — um inventário somente leitura que sinaliza apps ociosos, contêineres em execução inexplicáveis e volumes/segredos órfãos, nomeando a chamada exata que limparia cada um sem executá-la.
  • investigate_modal_costs (period opcional, app) — rastreia um aumento de gasto desde a linha do tempo diária até a hora de pico, a classe de recurso e a implantação ou contêiner ainda em execução por trás dele.

Limites de saída

A saída de logs, execuções e exec é limitada antes de ser retornada, para que um app muito verboso não inunde sua janela de contexto. O orçamento padrão é de 40.000 caracteres por campo de texto (aproximadamente 10 mil tokens); quando um campo é truncado, o resultado define output_capped: true e o texto carrega um marcador informando quanto foi descartado. Um campo limitado mantém seu início e seu fim, então um banner de inicialização e o traceback no final sobrevivem.

A busca nunca é limitada antes do fato: search_modal_logs percorre todo o log buscado e só limita quantos blocos de contexto retornam, então match_count é sempre exato.

Aumentar timeout_seconds ou o orçamento raramente é a resposta certa para uma busca de log truncada — buscar menos é. Limite a janela com since e until, filtre com source/exclude ou defina prefilter=True para descartar linhas sem correspondência dentro do Modal.

Defina MCP_MODAL_MAX_OUTPUT_CHARS para aumentar ou diminuir o orçamento, ou para 0 para desativar a limitação completamente:

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

Formato de resposta

Todas as ferramentas retornam respostas em um formato padronizado, com pequenas variações dependendo do tipo de operação:

# 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
}

Licença

Este projeto é licenciado sob a Licença MIT — consulte o arquivo LICENSE para detalhes.