Relatório de revisão de código · nível high

Servidor MCP — Google Forms

10 achados relevantes · 8 de julho de 2026 · revisão automatizada multi-agente (Claude Code)

Nota: este relatório é o retrato da revisão em 08/07/2026, feita antes das correções — os itens abaixo foram tratados na v0.3.0. O status atualizado de cada um está em docs/revisao-de-codigo.md.

O código é bem construído e organizado, e nada do que foi encontrado impede o uso imediato. Ainda assim, três problemas graves merecem correção prioritária: um comando de apagar que pode acertar o item errado, a impossibilidade de receber respostas sem uma etapa manual, e uma autorização que pode "dar certo" pela metade sem avisar.

3Graves
5Moderados
2Leves

Graves

— podem causar perda de conteúdo ou impedir o objetivo principal
01

Apagar ou mover perguntas pode acertar o item errado

Grave Confirmado src/server.js:179

Em palavras simplesUm formulário do Google pode conter, além de perguntas, títulos de seção, imagens e quebras de página. As ferramentas de apagar e mover contam as posições considerando tudo isso — mas a instrução exibida diz "posição da pergunta". E o servidor não confere o que existe na posição antes de agir.

ConsequênciaVocê pede para apagar "a primeira pergunta" e o servidor pode apagar outra coisa — um cabeçalho, uma imagem — sem avisar e sem como desfazer. Posições inexistentes (números negativos ou grandes demais) também passam direto e voltam como erro técnico do Google.

Texto técnico

Resumodelete_question e move_question documentam o índice como "posição da pergunta", mas a API o trata como posição bruta de qualquer item (blocos de texto, imagens e quebras de página contam) e não há validação de limites — o item errado pode ser apagado/movido silenciosamente.

Cenário de falhaFormulário com um bloco de texto na posição 0 e a primeira pergunta na posição 1: delete_question com index=0 apaga o bloco de texto, não a pergunta — sem erro e sem confirmar o que havia na posição. Índices negativos ou fora do intervalo (schema z.number().int() sem .min/.max) vão direto à API e voltam como 400 cru.

02

Formulários criados não conseguem receber respostas sem etapa manual

Grave Confirmado src/server.js:85

Em palavras simplesDesde 30/06/2026, todo formulário criado por programa nasce como rascunho não publicado — ninguém consegue respondê-lo até que seja publicado. O servidor não tem nenhum comando para publicar.

ConsequênciaO fluxo completo que o servidor promete — criar o formulário, compartilhar o link e ler as respostas — nunca se completa sozinho. Você envia o link, as pessoas não conseguem responder, e a leitura de respostas retorna sempre "Nenhuma resposta ainda". A única saída hoje é publicar manualmente no site do Google Forms.

Texto técnico

ResumoNão existe ferramenta para publicar o formulário (forms.setPublishSettings), mas desde 30/06/2026 formulários criados via API nascem despublicados — o fluxo anunciado criar → compartilhar → ler respostas não fecha usando só o servidor.

Cenário de falhaUsuário roda create_form + add_question e compartilha o responderUri; respondentes não conseguem enviar (formulário despublicado) e list_responses retorna para sempre "Nenhuma resposta ainda". Verificado na documentação do Google que setPublishSettings é o método necessário.

03

A autorização pode "dar certo" faltando a peça principal

Grave Confirmado scripts/get-token.js:54

Em palavras simplesNo passo de autorização com o Google, às vezes o Google não devolve a "chave de renovação" (refresh token) — a peça que permite ao servidor trabalhar sem pedir login toda hora. O script grava o arquivo de configuração sem essa chave e, mesmo assim, mostra "Autorização concluída!".

ConsequênciaÉ como instalar uma fechadura nova, receber o aviso "instalação concluída" — e descobrir depois que a chave não veio na caixa. Tudo parece pronto, mas qualquer uso do servidor falha dali em diante, com uma mensagem que não explica que o problema nasceu na autorização.

Texto técnico

ResumoSe o Google não retornar refresh_token, o script grava config.json sem o campo (JSON.stringify descarta undefined) e ainda imprime "Autorização concluída" — sucesso silencioso que deixa o servidor inutilizável.

Cenário de falhaGoogle omite o refresh_token (raro mas documentado, mesmo com prompt:'consent' — p.ex. políticas de workspace ou corrida de reautorização); o usuário vê "Pronto. refreshToken salvo", mas toda chamada de ferramenta passa a falhar com "Falta o refreshToken em credentials/config.json".

Moderados

— atrapalham o uso, confundem ou deixam tudo mais lento
04

Pergunta valendo pontos é recusada se o "modo prova" não estiver ligado

Moderado Confirmado src/server.js:53

Em palavras simplesPara uma pergunta valer pontos, o formulário precisa antes estar em "modo quiz" (há um comando para isso: set_quiz). Só que a ferramenta de adicionar pergunta não avisa dessa exigência nem ativa o modo sozinha.

ConsequênciaO caminho mais natural — criar um formulário e já adicionar uma pergunta valendo 1 ponto — termina num erro técnico em inglês, sem dica de que bastava ligar o modo quiz primeiro.

Texto técnico

Resumoadd_question anexa grading sempre que points é numérico, sem verificar/ativar o modo quiz; a API rejeita grading em formulário não-quiz, e a descrição da ferramenta não avisa que set_quiz precisa vir antes.

Cenário de falhaFluxo natural create_formadd_question com points:1batchUpdate retorna 400 INVALID_ARGUMENT ("grading properties can only be added to questions in forms configured as quizzes"), exposto cru ao cliente MCP.

05

A janela de autorização fica sem proteção contra intrusos

Moderado Plausível scripts/get-token.js:45

Em palavras simplesDurante a autorização, o script abre uma "porta de retorno" no seu computador para receber a confirmação do Google. Essa porta aceita qualquer confirmação que chegue, sem conferir se ela veio mesmo do processo que você iniciou — uma checagem de segurança padrão (chamada state) que ficou de fora.

ConsequênciaNum cenário raro — uma página maliciosa aberta no navegador exatamente durante esses segundos — o servidor poderia acabar conectado à conta Google de outra pessoa, sem você perceber. A probabilidade é baixa, mas a proteção é simples e padrão.

Texto técnico

ResumoO callback OAuth em localhost:4571 aceita qualquer ?code= sem validar um parâmetro state (sem proteção contra CSRF/injeção de código de autorização).

Cenário de falhaEnquanto o servidor local escuta, uma página maliciosa pode requisitar http://localhost:4571/?code=CODIGO_DO_ATACANTE; o script troca o código e grava o refreshToken de uma conta controlada pelo atacante. Janela curta e exige código válido para o mesmo client_id — mas a mitigação (state aleatório verificado no callback) é padrão e barata.

06

Respostas com arquivos enviados aparecem em branco

Moderado Confirmado src/server.js:233

Em palavras simplesO leitor de respostas só sabe exibir respostas de texto. Se o formulário tiver uma pergunta de "envio de arquivo" (possível quando ele é criado ou editado no site do Forms), essas respostas aparecem simplesmente vazias.

ConsequênciaVocê olha o relatório de respostas e conclui que ninguém anexou nada — quando na verdade anexaram. A informação existe, mas fica invisível, sem qualquer aviso.

Texto técnico

Resumolist_responses só renderiza textAnswers; respostas de upload de arquivo (fileUploadAnswers) — possíveis em formulários criados/editados na UI — aparecem como valor vazio, ocultando conteúdo enviado.

Cenário de falhaFormulário criado na UI com pergunta de upload é lido via list_responses: cada resposta imprime "- <pergunta>:" vazio, sem indicar que existe um arquivo enviado que não está sendo exibido.

07

Quando algo falha, a mensagem é "tecniquês" sem instrução

Moderado Confirmado src/server.js:76

Em palavras simplesQuando algo dá errado — permissão expirada, formulário que não existe, limite de uso do Google, arquivo de configuração corrompido — o servidor repassa o erro cru, em inglês técnico, em vez de traduzir para uma instrução ("rode npm run token de novo", "confira o ID do formulário").

ConsequênciaVocê não consegue distinguir um problema que se resolve em um minuto (reautorizar) de um defeito real, nem sabe qual o próximo passo. Curiosamente, algumas mensagens do próprio código já são amigáveis e em português — o tratamento é desigual.

Texto técnico

ResumoNenhum dos 7 handlers tem try/catch nem existe camada de tradução de erros: falhas da API do Google (401/403/404/429) e config.json corrompido (JSON.parse da linha 20) chegam ao cliente MCP como mensagens cruas.

Cenário de falhaRefresh token revogado → toda ferramenta responde "invalid_grant" cru em vez de "reautorize com npm run token"; config truncado → "Unexpected end of JSON input". Confirmado no SDK (mcp.js:135-162) que a mensagem do throw vai verbatim ao cliente; um wrapper único cobriria os 7 handlers.

08

A cada comando, o servidor refaz o "login" do zero

Moderado Confirmado src/server.js:16

Em palavras simplesEm vez de guardar a conexão com o Google e reaproveitá-la, o servidor a reconstrói inteira a cada comando — relê o arquivo de credenciais do disco e pede um "crachá de acesso" novo ao Google toda vez.

ConsequênciaCada comando fica de 0,1 a 0,3 segundo mais lento do que precisaria, e sequências rápidas de comandos podem esbarrar no limite de pedidos do Google, travando todas as ferramentas temporariamente.

Texto técnico

ResumogetFormsClient() relê/parseia config.json (I/O síncrono) e cria um OAuth2 client novo a cada chamada, zerando o cache de access token — cada invocação paga uma troca refresh→access token extra.

Cenário de falhaTodo tool call faz um round-trip HTTPS extra a oauth2.googleapis.com que um cliente memoizado evitaria (comprovado em oauth2client.js:348: o token só é reutilizado se já estiver na instância); sequências rápidas podem esbarrar em rate limit do endpoint de token.

Leves

— incômodos pequenos ou situações raras
09

Se a porta 4571 estiver ocupada, a autorização quebra sem explicação

Leve Confirmado scripts/get-token.js:70

Em palavras simplesO passo de autorização usa um "canal" fixo do computador (a porta 4571). Se outro programa já estiver usando esse canal, o script morre com uma tela de erro técnica, em vez de avisar o que houve ou tentar outro canal.

ConsequênciaSituação rara e restrita ao passo de configuração (que roda uma vez) — mas, quando acontece, não há qualquer pista de como resolver. Um canal automático resolveria sem nenhuma configuração extra.

Texto técnico

ResumohttpServer.listen(4571) não tem listener de 'error': porta ocupada (EADDRINUSE) vira exceção não tratada com stack trace cru; porta fixa, sem override por env nem fallback.

Cenário de falhaOutro processo segurando a 4571 → o fluxo de autorização morre em crash bruto. Como clientes OAuth "Desktop app" aceitam redirect de loopback em qualquer porta, usar porta efêmera (listen(0)) resolveria sem reconfigurar nada no Google Cloud.

10

Para cada pergunta adicionada, o servidor baixa o formulário inteiro

Leve Confirmado src/server.js:128

Em palavras simplesAntes de adicionar uma pergunta, o servidor baixa o formulário completo só para contar quantos itens ele tem (e assim saber onde encaixar a nova pergunta: no final).

ConsequênciaCada pergunta adicionada custa o dobro do trabalho e do tempo. E há uma janela de risco: se alguém estiver editando o formulário no navegador no mesmo instante, a contagem fica defasada e a pergunta pode entrar na posição errada.

Texto técnico

Resumoadd_question baixa o formulário inteiro (forms.get) só para calcular items.length — 2 chamadas de API por pergunta, com janela TOCTOU em que edições concorrentes deslocam o índice calculado.

Cenário de falhaAdicionar N perguntas custa 2N chamadas (dobro de quota e latência); edição concorrente na UI entre o get e o batchUpdate posiciona a pergunta errado. Um parâmetro de índice opcional ou contagem em cache elimina o get; nota verificada: a API exige location.index, então simplesmente omitir não é alternativa.

Itens menores, abaixo do corte

— verificados e reais, mas de baixa prioridade

Suspeitas descartadas na verificação

— pareciam problemas, mas não eram
Próximos passos sugeridos
  1. Corrigir os 3 graves (itens 01–03): validação de posições, uma ferramenta de publicação e a checagem da chave de renovação na autorização.
  2. Atacar os moderados em bloco: um único "tradutor de erros" resolve o item 07 para as 7 ferramentas; adotar a biblioteca oficial do Google resolve 05 e 09 juntos; guardar a conexão resolve o 08.
  3. Leves e itens menores são opcionais — bons de incluir se o servidor for crescer ou ser compartilhado.