FlightPowers Google Flights MCP

MCP hospedado para tarifas ao vivo do Google Flights com o veredito próprio do Google de baixo/típico/alto, viagens de ida e volta combinadas e cobrança via RapidAPI. Sem anúncios. Traga sua própria chave RapidAPI.

Documentação

Google Flights MCP: tarifas em tempo real que seu agente pode pesquisar em um intervalo de datas inteiro, sem anúncios

Uma URL, de qualquer forma:

claude mcp add --transport http google-flights https://flights.flightpowers.com/mcp

Em um cliente que suporta autorização MCP, um botão Entrar aparece: você entra com o Google e cola sua chave RapidAPI uma vez na página /connect, e nada vai para a configuração do seu cliente. Em um cliente que não suporta, traga a chave você mesmo:

claude mcp add --transport http google-flights https://flights.flightpowers.com/mcp --header "x-rapidapi-key: YOUR_RAPIDAPI_KEY"

Mesma URL, nas duas vezes. Uma requisição com qualquer tipo de credencial é atendida; uma requisição sem nada é respondida 401 com os metadados OAuth, que é o que faz o botão Entrar aparecer. https://flights.flightpowers.com/mcp/oauth ainda está ativo e exige o login na primeira requisição, para clientes cujo modo de autenticação é fixo quando um servidor é adicionado.

Hospedado. Nada para clonar, nada para compilar. Listado no Registro Oficial MCP como com.flightpowers/google-flights-mcp. Verificação de saúde: /health.

Precisa de uma chave? Assine a API Google Flights Live no RapidAPI, com plano gratuito disponível, e copie sua x-rapidapi-key: https://rapidapi.com/mtnrabi/api/google-flights-live-api

Ainda não tem chave? Comece com o servidor gratuito, mesma pesquisa, sem cadastro:** claude mcp add --transport http google-flights-free https://google-flights-lulu.flightpowers.com/mcp (com suporte a anúncios: um cartão patrocinado divulgado por resultado, limite de fan-out em 15, e clientes que não conseguem renderizar o cartão patrocinado podem ter limite ainda menor.) Volte aqui quando os anúncios, o limite de 15 pesquisas ou essas restrições de cliente atrapalharem.


O que seu agente recebe

Duas ferramentas que respondem a uma pergunta de tarifa, não a uma consulta de data.

  • Faça perguntas abertas. "Passagem só de ida mais barata para o Sri Lanka em qualquer data de outubro", "5 a 7 noites em Roma em algum momento de maio, saindo de Tel Aviv ou Larnaca": cada uma é uma chamada de ferramenta. Ambas as ferramentas aceitam um intervalo de data de partida, uma lista de aeroportos de destino e (para ida e volta) um valor nights em vez de uma data fixa de retorno, e expandem isso internamente.
  • Diga se um preço é realmente bom. Cada resultado traz a faixa histórica do próprio Google para aquela rota e período: price_insights_low, price_insights_high e um veredito price_range_in_relation_to_other_periods de low / typical / high. É isso que permite que um agente responda "$209 é típico aqui, não se apresse" em vez de apenas citar um número.
  • Reserve, não apenas navegue. Cada resultado inclui um buy_link para o Google Flights.
  • Saiba o que gastou. Cada resposta traz api_usage: requisições usadas por esta chamada e o que resta no plano de quem chamou. Veja Relatório de gastos.
  • Saiba o que pesquisou. Cada resposta traz search_coverage, para que o modelo possa dizer honestamente em quais datas e destinos a resposta se baseia.

Os resultados são tarifas ao vivo. Elas ficam desatualizadas em minutos: nunca armazene em cache uma tarifa nem reutilize um resultado anterior; pesquise novamente e informe quando os dados foram obtidos.

Obtenha uma chave (plano gratuito disponível)

O servidor não guarda nenhuma credencial upstream própria. Cada pesquisa é cobrada na sua assinatura RapidAPI, e é por isso que a chave viaja junto com a requisição.

  1. Assine a API Google Flights Live: https://rapidapi.com/mtnrabi/api/google-flights-live-api
  2. Copie sua x-rapidapi-key.
  3. Passe-a para o servidor de qualquer uma das três formas abaixo.

Se uma chave estiver faltando, as ferramentas não falham silenciosamente e não gastam nada. Elas retornam needs_api_key: true com a URL de cadastro e estas instruções, escritas para o modelo ler de volta para você.

Três formas de passar sua chave

FormaComoQuando usar
Cabeçalho (preferido)--header "x-rapidapi-key: YOUR_RAPIDAPI_KEY"Qualquer coisa que permita definir cabeçalhos. As chaves ficam fora das URLs e, portanto, fora dos logs de proxy e acesso.
Parâmetro de consultahttps://google-flights-mcp.flightpowers.com/mcp?rapidapi_key=YOUR_RAPIDAPI_KEYHosts que só permitem colar uma URL: o diálogo de conector personalizado do claude.ai é o caso que importa.
Campo de chave de API do clienteCole a chave na própria caixa "chave de API" do clienteHosts que enviam authorization: Bearer <key> ou x-api-key. O formulário de configuração salva do Smithery (config.rapidApiKey=) também é aceito.

A primeira fonte não vazia vence, nessa ordem. A chave nunca é registrada, nunca é ecoada em uma mensagem de erro e nunca é retornada em uma resposta de ferramenta.

Uma quarta forma: entre uma vez em /connect

Esta é a página para onde a URL de login no topo deste README envia você. Um cliente que fala autorização MCP guia você por ela sozinho; os passos abaixo são a mesma coisa feita manualmente.

Onde uma implantação tiver isso habilitado (verifique connect_enabled em /health), há uma página em /connect que substitui tudo acima por um login:

  1. Abra https://google-flights-mcp.flightpowers.com/connect (hotéis: https://hotels.flightpowers.com/connect) e entre com o Google.
  2. Cole sua chave RapidAPI uma vez, em um formulário, via TLS.
  3. Pressione Revelar a URL e copie a URL de conexão, …/mcp?fp_token=fpk_…, e use-a como a URL do servidor no seu cliente MCP. Clientes que permitem definir cabeçalhos podem enviar o mesmo token como Authorization: Bearer fpk_… em vez disso. (Ela fica oculta até você pedir: essa URL é uma credencial de portador de 90 dias para o seu plano, e uma página que a imprime por padrão a coloca em cada captura de tela e compartilhamento de tela.)

Se o seu cliente fez o login por conta própria — Claude, Cursor, ChatGPT e qualquer outra coisa que fale autorização MCP — não há URL para copiar. /connect diz isso: mostra a chave que você conectou e diz para voltar ao seu assistente. Há um link nela para o caso de um segundo cliente não conseguir entrar e precisar de uma URL de conexão.

O que isso traz: sua chave RapidAPI não fica na configuração do cliente, nem em uma URL, nem em quaisquer logs pelos quais essa URL passe. O custo: o servidor armazena sua chave, criptografada, e sabe seu ID de conta do Google e endereço de e-mail. Disconnect na mesma página exclui o registro e invalida todos os tokens de conexão da sua conta, imediatamente. A descrição completa está na seção 2a da política de privacidade.

Alguns detalhes que vale a pena saber:

  • Salvar executa uma verificação. A chave é validada contra a listagem antes de ser armazenada, então um erro de digitação falha na página em vez de no seu cliente uma hora depois. Essa verificação custa no máximo uma requisição do seu próprio plano: no plano BASIC gratuito (10 por mês), uma de dez. Uma chave que o RapidAPI rejeita no gateway não custa nada.
  • Uma chave na requisição sempre vence. Se você enviar um cabeçalho x-rapidapi-key (ou qualquer um dos outros canais acima) e carregar um token de conexão, a chave da própria requisição é usada. Nada do que você já configurou muda de comportamento porque você entrou.
  • O token não é sua chave e não pode ser convertido de volta nela. Ele é válido por 90 dias e para de resolver no momento em que você desconecta. Uma chamada com um token cuja chave foi desconectada recebe uma resposta needs_api_key dizendo para reconectar. Nunca cai na assinatura de outra pessoa e nunca gasta nada.
  • BASIC é gratuito. API Google Flights Live · API Booking Live. Uma chave RapidAPI cobre qualquer uma das duas que você assinou; você a conecta uma vez.

Executando /connect na sua própria implantação

Desligado a menos que todos os quatro estejam definidos. Uma implantação meio configurada não registra nenhuma das rotas e atende chamadas com chave exatamente como antes; /health reporta connect_enabled para que isso seja visível em vez de adivinhado.

VariávelO que é
GOOGLE_OAUTH_CLIENT_IDGoogle Cloud Console → Credenciais → ID do cliente OAuth, tipo Aplicativo web. Termina em .apps.googleusercontent.com.
GOOGLE_OAUTH_CLIENT_SECRETO segredo do mesmo cliente (GOCSPX-…).
MCP_KEY_MASTER32 bytes, base64: openssl rand -base64 32. Criptografa chaves armazenadas (AES-256-GCM) e deriva as chaves de assinatura de cookie e token.
DATABASE_URLNeon Postgres, endpoint pooled (…-pooler…). Esquema: migrations/001_mcp_user_keys.sql.

Opcional: MCP_CONNECT_VALIDATE=0 armazena uma chave colada sem verificá-la antes.

URIs de redirecionamento autorizadas para registrar no cliente Google, uma por origem de produto, exatamente:

https://google-flights-mcp.flightpowers.com/connect/callback
https://hotels.flightpowers.com/connect/callback

flights.flightpowers.com não precisa de entrada. /connect e /connect/start redirecionam um alias para a origem canônica antes de o login começar, porque cookies são por host e o Google compara redirect_uri literalmente: um alias que iniciasse seu próprio login voltaria para um host sem cookie de estado e falharia com uma mensagem que parece uma configuração errada do Google.

Também na tela de consentimento OAuth: escopos openid e .../auth/userinfo.email, e nada mais.

Rotacionar MCP_KEY_MASTER desconecta todos e invalida todas as chaves armazenadas. Isso é deliberado: após uma rotação, nada fica segurando um token que resolva para uma chave que ninguém pode ler. Os usuários veem "conecte novamente", não uma pesquisa falha. key_version na tabela está lá para que uma rotação em etapas seja possível depois sem um dia de mudança brusca.

Verificando o fluxo de ponta a ponta

Migração primeiro, uma vez por banco de dados:

psql "$DATABASE_URL" -f migrations/001_mcp_user_keys.sql

Depois, após a implantação:

# 1. The feature is actually on.
curl -s https://google-flights-mcp.flightpowers.com/health | grep connect_enabled

# 2. The page renders for an anonymous visitor.
curl -sI https://google-flights-mcp.flightpowers.com/connect        # 200
curl -sI https://google-flights-mcp.flightpowers.com/connect/start  # 302 to accounts.google.com

# 3. Sign in in a browser, paste a key, press Reveal and copy the connect URL.

# 4. MCP Inspector against that URL -- list the tools, then run one real search.
npx @modelcontextprotocol/inspector
#   Transport: Streamable HTTP
#   URL: https://google-flights-mcp.flightpowers.com/mcp?fp_token=fpk_...

# 5. Claude Code, the same URL.
claude mcp add --transport http flightpowers \
  "https://google-flights-mcp.flightpowers.com/mcp?fp_token=fpk_..."
claude mcp list          # shows it connected
#   then, in a session: ask for a fare and check the result is real

# 6. Cursor: Settings -> MCP -> Add, same URL. Or in ~/.cursor/mcp.json:
#   { "mcpServers": { "flightpowers": {
#       "url": "https://google-flights-mcp.flightpowers.com/mcp?fp_token=fpk_..." } } }

# 7. Hotels, the other hostname, with the same token.
#   https://hotels.flightpowers.com/mcp?fp_token=fpk_...

# 8. Press Disconnect on /connect, then re-run step 4. The tool must answer
#    needs_api_key with a "connect again" message -- not a search, and not a
#    generic "get a key" reply.

Um tools/list que tenha sucesso não prova nada sobre isso: um token só é consultado quando uma ferramenta realmente executa. O passo 4 tem que ser uma pesquisa real.

Uma quinta forma: entre de dentro do seu cliente MCP

Um cliente MCP só inicia um login quando uma requisição volta 401 com um cabeçalho WWW-Authenticate: Bearer resource_metadata=…. Nada mais no fio diz a ele que um está disponível. Até 2026-09-09 /mcp nunca enviou esse cabeçalho, então um chamador sem chave recebia um 200 cujo corpo dizia needs_api_key: JSON correto, invisível para o mecanismo de autenticação de todos os clientes, e o usuário via "a ferramenta falhou" sem onde clicar.

Agora /mcp desafia, mas apenas um chamador que não trouxe nada, e apenas em uma chamada que gastaria algo:

URLComportamento
https://flights.flightpowers.com/mcpA que deve ser usada. Uma chave RapidAPI em um cabeçalho, na string de consulta ou em um blob de configuração do Smithery, um token de conexão fpk_ ou um token de acesso fpo_: todos atendidos exatamente como antes. Nada: initialize, tools/list e o resto do handshake somente leitura ainda são respondidos, e tools/call recebe 401 + o desafio, então seu cliente oferece um botão Entrar.
https://hotels.flightpowers.com/mcpO mesmo, para hotéis.
…/mcp/oauthO mesmo servidor com o login exigido na primeira requisição. Para clientes cujo modo de autenticação é fixo quando um servidor é adicionado, e para conectores salvos nessa URL antes da mudança.

Mesmas ferramentas, mesmo roteamento por produto e hostname, tudo o mais igual. O alias é o registro de ferramentas idêntico atrás de uma verificação de token, não uma segunda cópia do servidor.

A propriedade que protege integrações pagantes: uma requisição com credencial nunca é desafiada, e a ordem de credenciais não muda (cabeçalho, consulta, blob de configuração, depois identidade OAuth, depois token de conexão, depois o fallback de env RAPIDAPI_KEY). Uma chave errada também não é "nada": ela chega às ferramentas e volta como o erro exato que o RapidAPI deu, porque substituir isso por um prompt de login seria uma resposta pior. Uma implantação com RAPIDAPI_KEY definido atende chamadores sem chave do próprio plano de propósito e nunca é desafiada. MCP_REQUIRE_AUTH=off desliga o desafio completamente, sem implantação. A descoberta permanece aberta, e isso foi aprendido da maneira mais difícil. Por algumas horas em 2026-09-09, o desafio também cobriu initialize e tools/list. A Glama verifica cada conector a cada hora abrindo uma conexão MCP e listando suas ferramentas, sem credenciais; ambas as listagens pagas foram marcadas como não saudáveis e rebaixadas na mesma noite, e a verificação de lançamento da Smithery, mcpservers.org e M8ven sondam da mesma forma. Então a linha é traçada no gasto, não na conexão: initialize, notifications/initialized, ping, tools/list, prompts/list e resources/list são servidos para qualquer pessoa (src/discovery.py), e todo o resto precisa de uma chave ou de um login. Um lote com um tools/call nele, um corpo superdimensionado e um que não pode ser analisado são todos desafiados — a lista de permissões falha de forma fechada. /mcp/oauth ainda desafia tudo, que é a URL para dar a um diretório que quer um servidor sempre exigindo autenticação, e o público, não autenticado /.well-known/mcp/server-card.json ainda está lá para um scanner que lê um cartão em vez disso.

Como é usar. Cole a URL /mcp/oauth no seu cliente. Ele se registra, abre um navegador, você faz login com o Google e aprova esse cliente pelo nome em uma página que diz exatamente o que ele poderá fazer: executar buscas cobradas no seu próprio plano RapidAPI, nada mais. O cliente nunca vê sua chave RapidAPI. Se você ainda não conectou uma, a aprovação ainda funciona e a primeira busca retorna dizendo para você colar uma chave em /connect, com a URL.

O que você pode revogar, e como. Pressione Desconectar em /connect: a chave armazenada é excluída e todo token OAuth para essa conta do Google é descartado na mesma ação. Um cliente que estava conectado para de funcionar imediatamente. Individualmente, um cliente pode chamar /oauth/revoke (RFC 7009).

Configuração do cliente

# Claude Code
claude mcp add --transport http flightpowers \
  "https://google-flights-mcp.flightpowers.com/mcp/oauth"
claude mcp list            # shows "needs authentication" until you sign in
/mcp                       # in a session: pick the server, follow the sign-in

# Cursor -- Settings -> MCP -> Add, URL above. Or ~/.cursor/mcp.json:
#   { "mcpServers": { "flightpowers": {
#       "url": "https://google-flights-mcp.flightpowers.com/mcp/oauth" } } }
# Cursor discovers the 401, registers itself and opens the browser.

# ChatGPT -- Settings -> Connectors -> Create. It asks for:
#   MCP server URL:  https://google-flights-mcp.flightpowers.com/mcp/oauth
#   Authentication:  OAuth
# Leave client id and secret EMPTY: this server supports dynamic client
# registration, so ChatGPT registers itself. Nothing else has to be filled in.

# MCP Inspector -- the quickest way to watch the whole handshake.
npx @modelcontextprotocol/inspector
#   Transport: Streamable HTTP
#   URL: https://google-flights-mcp.flightpowers.com/mcp/oauth
#   Auth: OAuth 2.0  ->  "Guided OAuth Flow" walks metadata -> DCR ->
#   authorize -> token, and shows each response. Then run ONE real search.

A superfície do protocolo

RotaEspecificação
GET /.well-known/oauth-protected-resource e …/mcp/oauthRFC 9728
GET /.well-known/oauth-authorization-server e …/mcp/oauthRFC 8414
POST /oauth/registerRFC 7591, registro dinâmico de cliente, aberto
GET /connect/authorize · POST /connect/authorizeRFC 6749 §4.1, PKCE S256 obrigatório
POST /oauth/tokenauthorization_code e refresh_token
POST /oauth/revokeRFC 7009

O endpoint de autorização está sob /connect de propósito: o cookie de sessão de login tem escopo Path=/connect para que nunca possa ser anexado a uma solicitação /mcp, e colocar authorize em qualquer outro lugar significaria alargar esse cookie ou fazer o usuário fazer login duas vezes.

Os códigos duram 10 minutos e são de uso único (DELETE … RETURNING, então duas trocas concorrentes disputam uma linha e exatamente uma vence). Os tokens de acesso duram 1 hora, os tokens de atualização 30 dias com rotação. Tudo é opaco e armazenado como um hash SHA-256, então um despejo do banco de dados não contém nada que possa ser reproduzido. Os tokens não são JWTs, deliberadamente: um token assinado permanece válido até expirar, independentemente do que decidirmos depois, e Desconectar tem que significar desconectar.

Um token de acesso é verificado contra o recurso para o qual foi aprovado antes de ser aceito, não apenas quando é emitido. Ambos os produtos são a mesma implantação, o mesmo banco de dados e a mesma chave RapidAPI armazenada por usuário, então sem essa verificação, um token aprovado na página de consentimento de voos, que diz "buscar tarifas de voos ao vivo" e nada mais, seria aceito no hostname de hotéis e gastaria o plano de hotéis do usuário. A verificação é estrita quanto ao host e tolerante quanto ao caminho, porque clientes no mundo real enviam a origem, /mcp e /mcp/oauth para o mesmo servidor. tests/test_oauth.py::TestATokenIsBoundToTheResourceItWasApprovedFor fixa ambas as metades.

Executando na sua própria implantação

Nenhuma nova variável de ambiente. Ele vem ativado onde /connect está configurado, porque reutiliza esse login do Google e esse armazenamento de chaves. Ele precisa de mais uma tabela no mesmo banco de dados:

psql "$DATABASE_URL" -f migrations/002_mcp_oauth.sql

/health então relata oauth_enabled: true e oauth_mcp_endpoint; essa URL é o que vai em uma listagem de diretório. MCP_OAUTH=off o desativa enquanto deixa /connect em execução; essa é a reversão que não precisa de mudança de código.

Nada precisa mudar no cliente OAuth do Google. O URI de redirecionamento ainda é …/connect/callback, porque o fluxo OAuth do cliente MCP termina na nossa página de autorização, e somente essa página fala com o Google.

Verificando de ponta a ponta

BASE=https://google-flights-mcp.flightpowers.com

# 1. On, and advertising itself.
curl -s $BASE/health | python3 -m json.tool | grep oauth_

# 2. The challenge. This is the whole feature in one response.
curl -si $BASE/mcp/oauth | head -20
#   HTTP/2 401
#   www-authenticate: Bearer resource_metadata="…/.well-known/oauth-protected-resource/mcp/oauth"

# 3. Discovery, at both the scoped and the bare path.
curl -s $BASE/.well-known/oauth-protected-resource/mcp/oauth | python3 -m json.tool
curl -s $BASE/.well-known/oauth-authorization-server | python3 -m json.tool

# 4. Dynamic registration answers.
curl -s -X POST $BASE/oauth/register -H 'content-type: application/json' \
  -d '{"client_name":"probe","redirect_uris":["http://127.0.0.1:9999/cb"],
       "token_endpoint_auth_method":"none"}' | python3 -m json.tool

# 5. The real test: MCP Inspector, Guided OAuth Flow, then ONE real search.
#    A tools/list proves nothing -- the token is only consulted when a tool runs.

# 6. Hotels, the other hostname, same walk: https://hotels.flightpowers.com/mcp/oauth

# 7. /mcp is untouched. This must still work, with no 401 anywhere:
curl -s -X POST $BASE/mcp -H 'content-type: application/json' \
  -H 'accept: application/json, text/event-stream' \
  -H "x-rapidapi-key: $REAL_KEY" \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"probe","version":"1"}}}'

# 8. Disconnect on /connect, then re-run step 5's search: it must answer
#    needs_api_key, and the client must be logged out.

Mantendo a tabela de registro honesta

/oauth/register é aberto, porque a especificação MCP exige e porque um client_id por si só não autoriza nada: todo fluxo através de um ainda termina em uma página de consentimento na qual um humano conectado tem que pressionar um botão. Aberto não é o mesmo que ilimitado, então três coisas ficam por trás disso.

MecanismoOnde viveO que impede
Limite de taxaem memória, por instânciauma rajada: 10 registros por endereço a cada 10 minutos, 60 solicitações de token por minuto, 10 /connect/save por hora. Acima do limite é 429 com Retry-After.
Limites diáriosPostgres, então toda instância concordaum gotejamento lento: 30 registros por endereço por dia. O limite global é 5.000 por dia — uma salvaguarda contra linhas ilimitadas, não uma defesa: um número global definido perto do tráfego real é uma alavanca que um atacante puxa para recusar todo novo usuário do Claude ou Cursor por um dia. Cruzar 500 em um dia registra e não recusa nada. MCP_OAUTH_DCR_MAX_PER_IP_PER_DAY, MCP_OAUTH_DCR_MAX_PER_DAY e MCP_OAUTH_DCR_WARN_PER_DAY os movem sem uma implantação.
Varreduraem /oauth/register, no máximo uma vez a cada 15 minutos por instânciao lixo: códigos e tokens expirados, e registros que nunca se tornaram uma autorização dentro de 7 dias. Um cliente com um token ativo, um código pendente ou uma página de consentimento que foi renderizada para ele nunca é varrido.

O limite de taxa é por INSTÂNCIA. No Vercel, isso significa que N instâncias ativas permitem até N vezes esses números entre elas, e uma inicialização a frio começa os contadores em zero. É por isso que os limites duráveis também existem: eles são contados no banco de dados, onde o número é o mesmo em todos os lugares.

O endereço no qual cada um deles é baseado vem de x-real-ip primeiro — o Vercel o define para o par que aceitou — e, caso contrário, da ÚLTIMA entrada utilizável de x-forwarded-for, pulando saltos que só podem ser internos. Nunca a entrada 0: proxies anexam à direita, então a entrada mais à esquerda é o que o chamador escreveu, e lê-la faria cada limite aqui estar a um cabeçalho de distância de ser contornado. Uma solicitação sem nenhum dos cabeçalhos compartilha um bucket chamado unknown, que é limitado por taxa como um único chamador e não está sujeito ao limite durável por endereço (um cabeçalho ausente na borda não deve bloquear o servidor inteiro por um dia).

Um registro é carimbado como em uso quando sua página de consentimento é RENDERIZADA, não apenas quando o humano pressiona Aprovar. Clientes MCP comumente registram quando são instalados e autorizam dias depois, e a varredura não deve excluir uma linha enquanto sua página de consentimento está na tela — não há chave estrangeira de códigos ou tokens de volta ao cliente, então a troca que se seguiu falharia invalid_client sem nada nomeando a causa.

Requer uma migração:

psql "$DATABASE_URL" -f migrations/003_mcp_oauth_hygiene.sql

Tokens de atualização rotacionam, e uma reprodução revoga a família

Um token de atualização é de uso único: trocá-lo emite um novo par e aposenta o apresentado. A linha aposentada é mantida e carimbada, não excluída, porque uma linha excluída e um token que nunca foi emitido parecem idênticos — e distinguir entre eles é o ponto. Apresentar um token de atualização já rotacionado significa ou um cliente que perdeu a resposta ou uma cópia nas mãos de outra pessoa, e OAuth 2.1 §4.14.2 diz para assumir a segunda: a resposta é invalid_grant, e todo token descendente dessa autorização é excluído. O cliente honesto faz login novamente; o token de acesso do ladrão para de funcionar no mesmo momento.

Com uma exceção deliberada, para o caso que quase sempre é o inocente: a PRIMEIRA reprodução do token que acabamos de rotacionar, do mesmo cliente, dentro de 10 segundos, é respondida com o par que a rotação já emitiu. É uma nova tentativa idempotente — nada de novo é criado — e significa que um cliente cuja resposta foi perdida em uma conexão interrompida não é silenciosamente desconectado. Uma segunda reprodução, ou uma após a janela, é a coisa real e ainda mata a família. A janela é por instância e em memória, então uma falha simplesmente cai na resposta conservadora.

Revogar um token de atualização através de /oauth/revoke leva seus tokens de acesso junto, pela mesma razão (RFC 7009 §2.1) — e somente se o token foi emitido para o cliente que está pedindo, que é a outra metade dessa seção. Um cliente apresentando o token de outra pessoa ainda recebe 200 (§2.2) e nada é revogado.

Um client_id pode ser uma URL

client_id_metadata_document_supported: true é anunciado em /.well-known/oauth-authorization-server. Um cliente pode usar uma URL https como seu client_id; o documento nessa URL lista seus redirect_uris, e nós o buscamos e verificamos por fluxo em vez de escrever uma linha de registro. A Smithery pede isso antes de fazer proxy de um servidor OAuth remoto.

O que é verificado, toda vez: apenas https, um hostname público (sem literais de IP, sem localhost, sem credenciais na URL), o hostname resolvido e cada endereço com o qual ele responde obrigado a ser unicast público (um nome não é um controle: 127.0.0.1.nip.io tem um ponto e aponta para loopback), nenhum redirecionamento seguido, um timeout de 5 segundos, um limite de 64 KB aplicado enquanto o corpo é lido em vez de depois que é armazenado em buffer, um client_id dentro do documento que corresponde à URL se estiver presente, e — o que importa — o redirect_uri na solicitação deve estar listado no documento. A consulta é limitada por taxa por conta própria (60 por endereço a cada 10 minutos), porque é a única busca de saída neste servidor que um chamador pode acionar antes de fazer login. O registro dinâmico permanece inalterado e ainda é o padrão: um client_id que não é uma URL https é consultado na tabela exatamente como antes.

Ferramentas

FerramentaO que faz
search_oneway_flightsTarifas de ida em tempo real. Entrada: IATA de origem, IATA de destino ou uma lista, e uma data de partida ou um intervalo de datas. Retorna preço, companhia aérea, duração, paradas, buy_link e o intervalo de preços histórico do Google para você julgar a tarifa. Use para qualquer pergunta de ida, incluindo as de data aberta: uma chamada com um intervalo, nunca uma chamada por data.
search_roundtrip_flightsTarifas de ida e volta em tempo real precificadas como trechos pareados, não duas idas. Entrada: origem, destino(s), uma data de partida ou intervalo, e um return_date ou uma duração de viagem em nights (um número ou uma lista como [5,6,7]). Retorna preço total, companhia aérea/paradas/duração por trecho e um buy_link para a viagem.

A implantação de hotéis serve search_hotels, find_hotel_by_name e compare_hotel_rates em vez disso; veja Hotels: providers e compare_hotel_rates.

search_oneway_flights

search_oneway_flights(
    from_airport: str,                     # origin IATA, e.g. "TLV"
    to_airport: str | list[str],           # destination IATA, or a list to compare
    departure_date: str | None = None,     # "YYYY-MM-DD"
    departure_date_from: str | None = None,# first date of a range
    departure_date_to: str | None = None,  # last date of a range
    max_stops: int | None = None,          # 0 = non-stop only
    airline_codes: list[str] | None = None,
    exclude_airline_codes: list[str] | None = None,
    departure_time_min: int | None = None, # hour, 0-23
    departure_time_max: int | None = None,
    arrival_time_min: int | None = None,
    arrival_time_max: int | None = None,
    currency: str = "usd",
    max_price: int | None = None,
    seat_type: int | None = None,          # 1 economy, 2 premium economy, 3 business, 4 first
    passengers: list[int] | None = None,   # [adults, children, infants]
    sort_by: str = "best",                 # "best" | "price" | "duration"
    limit: int = 10,                       # results returned after merge + sort
    max_searches: int | None = None,       # cap the billed requests this call may make
    use_fallback: bool | None = None,      # leave unset: accepted upstream, currently inert
)

search_roundtrip_flights

search_roundtrip_flights(
    from_airport: str,
    to_airport: str | list[str],
    departure_date: str | None = None,
    departure_date_from: str | None = None,
    departure_date_to: str | None = None,
    return_date: str | None = None,        # use this OR nights, not both
    nights: int | list[int] | None = None, # e.g. 7, or [5, 6, 7]
    max_departure_stops: int | None = None,
    max_return_stops: int | None = None,
    departure_airline_codes: list[str] | None = None,
    return_airline_codes: list[str] | None = None,
    currency: str = "usd",
    max_price: int | None = None,
    seat_type: int | None = None,
    passengers: list[int] | None = None,
    sort_by: str = "best",
    limit: int = 10,
    max_searches: int | None = None,
    use_fallback: bool | None = None,
)

sort_by é aplicado por este servidor no conjunto de resultados mesclado de cada busca que executou, então é previsível independentemente de quantas combinações foram expandidas.

Um exemplo prático

Usuário: "Estou em Tel Aviv. Viagem de uma semana mais barata para Roma ou Atenas, saindo em qualquer dia na primeira quinzena de maio."

Uma chamada:

{
  "name": "search_roundtrip_flights",
  "arguments": {
    "from_airport": "TLV",
    "to_airport": ["FCO", "ATH"],
    "departure_date_from": "2026-05-01",
    "departure_date_to": "2026-05-15",
    "nights": 7,
    "sort_by": "price",
    "limit": 5
  }
}

Isso expande para 15 datas × 2 destinos = 30 combinações, que é exatamente o limite por chamada. O formato da resposta (os nomes dos campos são reais; os valores abaixo são ilustrativos, não uma citação, execute a chamada para obter tarifas ao vivo):

{
  "results": [
    {
      "from_airport": "Tel Aviv (TLV)",
      "to_airport": "Rome (FCO)",
      "departure_date": "2026-05-05",
      "return_date": "2026-05-12",
      "total_price": "$XXX",
      "total_price_as_number": 0,
      "total_duration_seconds": 0,
      "total_stops": 0,
      "price_range_in_relation_to_other_periods": "low",
      "price_insights_low": 0,
      "price_insights_high": 0,
      "departure_flight_airline": "...",
      "departure_flight_departure_description": "...",
      "departure_flight_arrival_description": "...",
      "departure_flight_duration": "...",
      "departure_flight_stops": 0,
      "departure_stops_info": [],
      "return_flight_airline": "...",
      "return_flight_departure_description": "...",
      "return_flight_arrival_description": "...",
      "return_flight_duration": "...",
      "return_flight_stops": 0,
      "return_stops_info": [],
      "buy_link": "https://www.google.com/travel/flights?tfs=..."
    }
  ],
  "result_count": 5,
  "search_coverage": {
    "requested_combinations": 30,
    "searched_combinations": 30,
    "truncated": false,
    "max_searches_per_request": 30,
    "departure_dates_searched": ["2026-05-01", "..."],
    "destinations_searched": ["ATH", "FCO"]
  },
  "api_usage": {
    "requests_used_by_this_call": 30,
    "plan_requests_remaining": 0,
    "plan_requests_limit": 0,
    "note": "This search used 30 of your RapidAPI plan's requests; ... remain in the current period. Each date and destination combination is one billed request."
  }
}

Outros formatos de resposta a esperar, todos normais:

  • Nenhum voo nessas datas. results: [] com um message: o Google Flights genuinamente não retorna nada para algumas combinações de rota/data. Não é um erro. Tente datas próximas ou um aeroporto próximo. use_fallback não mudará isso e fica não definido por padrão: o backend aceita o campo, mas a segunda fonte de dados de voo que ele seleciona está bloqueada atrás de USE_FALLBACK_FLI (fallback_available()), que não está ativado para esta API, então nenhum dos seus três valores tem qualquer efeito observável em uma busca hoje. As novas tentativas automáticas que o backend faz em uma página ilegível são incondicionais e não são afetadas por isso.
  • Algumas buscas falharam. Um campo partial informa quantas das buscas executadas falharam, e os resultados cobrem o restante.
  • Intervalo muito amplo. search_coverage.truncated: true mais um note. O intervalo é amostrado uniformemente em toda a janela (primeiro e último mantidos), não encurtado, então a amostra é representativa, não os primeiros N dias. Aumente max_searches ou reduza o intervalo para uma cobertura mais completa.
  • Sem chave / chave rejeitada. needs_api_key: true, zero gasto, com a correção. Uma chave RapidAPI válida que não está assinada para esta API é a causa mais comum.
  • Plano esgotado. quota_exhausted: true com api_usage, além de um lembrete de que reduzir o intervalo faz a cota restante render mais.

Hotéis: um intervalo de check-in

POST /search precifica exatamente uma estadia, então "três noites mais baratas em Roma em maio" costumava ser 31 chamadas de ferramenta — ou, na prática, uma chamada em uma data que o modelo escolhia e uma resposta apresentada como a mais barata. Ambas as ferramentas de busca de hotéis agora assumem o formato de voos:

search_hotels(
    destination: str,
    checkin_date: str | None = None,        # one stay: this plus checkout_date
    checkout_date: str | None = None,
    checkin_date_from: str | None = None,   # or a range: this, checkin_date_to and nights
    checkin_date_to: str | None = None,
    nights: int | list[int] | None = None,  # 3, or [2, 3, 7] to price several lengths
    max_searches: int | None = None,        # cap the billed requests this call may make
    ...
)

Mesma mecânica do fan-out de voos (src/fanout.py): uma chamada de backend por estadia, limitada a max_searches_per_tool_call (30, máximo rígido de 60), amostrada uniformemente no intervalo quando não cabe, e relatada em search_coverage. nights deriva cada data de check-out, então substitui checkout_date em vez de se juntar a ele. Um checkout_date fixo contra um intervalo de datas de check-in é permitido e significa "saída no dia 4, quando eu chegar"; os pares impossíveis são descartados.

A resposta é limitada de propósito. Cada propriedade de cada estadia é ~25 KB por estadia (medido: 25.892 bytes para 25 propriedades, 18.989 delas URLs), então cada estadia relata sua propriedade mais barata, sua tarifa por noite e sua mediana, e a lista completa de propriedades volta apenas para a estadia mais barata:

{
  "results": [ /* every property of the CHEAPEST stay, upstream rows untouched */ ],
  "result_count": 18,
  "results_for_stay": {"checkin_date": "2026-05-12", "checkout_date": "2026-05-15", "nights": 3},
  "stays": [
    {
      "checkin_date": "2026-05-01", "checkout_date": "2026-05-04", "nights": 3,
      "search_status": "ok", "reason": "ok",
      "property_count": 22, "priced_count": 19,
      "cheapest_total": 411.0, "price_per_night": 137.0, "median_total": 690.0,
      "currency": "USD",
      "cheapest": { /* the row, minus its image URL */ }
    },
    {"checkin_date": "2026-05-02", "search_status": "degraded", "reason": "search_failed",
     "property_count": null, "priced_count": null, "cheapest": null},
    {"checkin_date": "2026-05-03", "search_status": "not_searched", "reason": "not_searched",
     "property_count": null, "cheapest": null}
  ],
  "cheapest_overall": {"checkin_date": "2026-05-12", "total": 305.0, "price_per_night": 101.67,
                       "currency": "USD", "property": { /* ... */ }},
  "search_status": "partial",
  "search_coverage": {
    "requested_combinations": 31,
    "searched_combinations": 15,
    "truncated": true,
    "max_searches_per_request": 30,
    "stays_searched": [{"checkin_date": "2026-05-01", "checkout_date": "2026-05-04"}, "..."],
    "checkin_dates_searched": ["2026-05-01", "..."],
    "note": "This request expanded to 31 stays, above the ..."
  },
  "api_usage": {"requests_used_by_this_call": 15, "note": "... Each stay -- one check-in date paired with one length -- is one billed request."}
}

reason em uma estadia é um fato sobre nosso pipeline, nunca um palpite sobre o hotel:

reasonsearch_statusSignificado
okokprecificado
no_availabilityemptybuscado, respondido, nada voltou
no_priceemptypropriedades voltaram, nenhuma trouxe preço (available: false cai aqui)
search_faileddegradeda busca errou, então nada é conhecido — não "sem quartos"
not_searchednot_searchedo limite o amostrou para fora

As contagens são null em vez de 0 nos dois últimos: zero lê como "nada lá", e nenhum caso sabe disso. O search_status de nível superior é ok / partial / empty / degraded sobre as estadias; cada estadia falhando levanta erro em vez de responder com uma lista vazia.

Duas recusas deliberadas: um intervalo de check-in com providers nomeando mais de uma fonte (um fan-out vezes um fan-out por fonte, cobrado em duas assinaturas, que nem search_coverage nem api_usage podem descrever honestamente hoje), e max_searches em uma única estadia, que silenciosamente não faria nada.

Uma estadia única é byte-idêntica ao que era antes disso existir — mesmo corpo de requisição, mesmas chaves de resposta, sem stays, sem search_coverage, sem search_status. Verificado em tests/test_hotel_date_range.py::TestTheOldShapeIsUntouched contra uma expectativa congelada capturada do código anterior.

Hotéis: providers e compare_hotel_rates

A implantação de hotéis (hotels.flightpowers.com, o mesmo código selecionado pelo cabeçalho Host) serve três ferramentas. search_hotels e find_hotel_by_name também aceitam um intervalo de check-in (acima); search_hotels ganhou um argumento opcional para fontes e há uma nova ferramenta.

FerramentaO que faz
search_hotelsTarifas ao vivo para um destino e datas, ou para cada estadia que um intervalo de check-in expande. providers nomeia as fontes para precificar: ["booking"] (o padrão), ["airbnb"], ou ambas.
find_hotel_by_nameUma propriedade nomeada, somente Booking.com. A página do quarto da Airbnb não traz preço, então uma busca por nome lá resolveria para algo que não pode ser precificado.
compare_hotel_ratesA mesma estadia precificada em cada fonte para a qual você tem uma chave, uma linha por fonte: total mais barato, total mediano, quantos lugares foram precificados, moeda e quando as linhas foram lidas.

O padrão não mudou

search_hotels sem argumento providers envia a mesma requisição upstream que sempre enviou, para o mesmo host, e responde com as mesmas chaves. O mesmo faz providers: ["booking"]. Ambos são verificados em tests/test_providers.py::TestTheDefaultDidNotMove, corpo da requisição upstream incluído — um padrão só é um padrão se mantê-lo não custa nada.

Nomear uma segunda fonte muda a forma da resposta, e só então:

{
  "results": [ /* every source's rows, each carrying "provider" and "rating_scale" */ ],
  "result_count": 3,
  "providers":        [ /* one row per source that was CALLED */ ],
  "providers_skipped":[ /* one row per source that was NOT, with a subscribe_url */ ],
  "caveats":          [ /* what to read before calling one source cheaper */ ],
  "api_usage":        { "requests_used_by_this_call": 2 }
}

Onde cada fonte é chamada

  • booking vai direto para booking-live-api.p.rapidapi.com na chave do chamador, cobrado pela RapidAPI na própria assinatura deles. Inalterado.
  • airbnb passa pela nossa própria porta da frente, POST https://api.flightpowers.com/v1/hotels/search com provider: "airbnb" no corpo (flight_rabbi #472), porque o backend da Airbnb não está na borda da RapidAPI. A chamada carrega a chave do chamador como x-rapidapi-key e se identifica com X-FP-Client: mcp-hotels/<version> — a porta da frente sobrescreve a atribuição do corpo com sua própria conclusão, então o cabeçalho é a única coisa que diz quem chamou (regra 11). Substitua a origem com API_FRONT_BASE_URL para uma porta de visualização; ela não guarda credencial.

A listagem da Airbnb ainda não existe na RapidAPI, e a porta da frente é enviada com o provedor desligado, então hoje uma chamada real de providers: ["airbnb"] volta como uma linha de degraded dizendo isso. Essa é a resposta projetada, não um bug: uma fonte sem medição acessível por qualquer pessoa com qualquer chave válida é um gateway pelo qual pagamos.

Quatro regras que a implementação mantém

  1. Uma fonte para a qual você não tem chave nunca é chamada na nossa. Ela é nomeada em providers_skipped com reason (no_key, not_subscribed, key_rejected) e a URL onde você assina. Cada listagem é uma assinatura separada, então um 403 do Hub significa "não assinado para aquela listagem", não "chave ruim".
  2. Uma fonte degradada é uma linha nomeada, não um buraco. search_status: "degraded", count: null, sem preços — e as linhas da outra fonte ainda voltam. "Booking não respondeu" e "Booking não tinha nada" são respostas opostas e devem ser dizíveis como frases diferentes.
  3. cheapest_total e median_total cobrem apenas as linhas que carregam um preço, e count é esse número. Um price_string nunca é analisado em um número.
  4. Sem aritmética entre moedas. Cada fonte é solicitada na mesma moeda; se duas responderem em moedas diferentes, cada linha mantém a sua e caveats diz que os totais não são comparáveis. Nada é convertido.

Chaves, por fonte

Quase todo mundo tem uma chave RapidAPI assinada para várias listagens, e essa chave é usada para cada fonte sem configuração extra. Um chamador que realmente tem duas pode nomear uma por fonte, e o nome com escopo de fonte vence:

x-rapidapi-key-airbnb: <key>        # header
?rapidapi_key_airbnb=<key>          # query
?config=<base64 {"rapidApiKeyAirbnb": "<key>"}>   # Smithery blob

A ordem é a que src/credentials.py já documenta — nomes com escopo de fonte, depois os nomes específicos sem escopo, depois o blob de configuração, depois nomes genéricos por último e ignorados completamente quando um parâmetro config está presente, porque a chave própria de um gateway sob um nome genérico não é nossa.

filters e price_as_seen_from são somente Booking

Em uma busca mista, eles são enviados para Booking e não para a porta da frente. Em uma busca somente Airbnb, eles são recusados, não descartados: um filtro silenciosamente descartado retorna mais propriedades do que você pediu e nada diz isso.

Saída estruturada (outputSchema, structuredContent, isError)

Cada ferramenta declara um outputSchema, e cada resultado carrega o payload duas vezes: uma vez como structuredContent, uma vez como o JSON serializado em um bloco de conteúdo de texto. A especificação MCP pede a duplicata —

Para compatibilidade reversa, uma ferramenta que retorna conteúdo estruturado DEVE também retornar o JSON serializado em um bloco TextContent.

— e isso é essencial aqui, não cerimonial, porque clientes que antecedem a saída estruturada leem o bloco de texto e nada mais. (Verificado contra a revisão da especificação 2026-07-28; a saída estruturada chegou em 2025-06-18.)

Os esquemas são deliberadamente additionalProperties: true com apenas results obrigatório. A especificação coloca a obrigação no servidor — "Servidores DEVEM fornecer resultados estruturados que estejam em conformidade com este esquema" — e essas ferramentas têm várias saídas legítimas que carregam chaves diferentes — uma resposta de zero resultados, a resposta sem chave needs_api_key e a resposta quota_exhausted. Um esquema mais restrito pareceria melhor e tornaria o servidor não conforme em um caminho que ele envia de propósito.

search_status, e por que degraded é um erro

Os resultados de voos carregam search_status, espelhando o próprio vocabulário X-Search-Status do backend:

valorsignificado
oktoda combinação buscada retornou resultados
emptya busca foi concluída; o Google genuinamente não tem itinerários. Uma resposta real
partialalgumas combinações retornaram resultados, algumas falharam. A lista está incompleta
degradedtoda combinação falhou. A busca não aconteceu; uma lista vazia não significa nada

Um resultado degraded é também sinalizado como isError: true. É o único que é. A especificação classifica "falhas de API" como erros de execução de ferramenta e diz que clientes "DEVEM fornecer erros de execução de ferramenta aos modelos de linguagem para permitir autocorreção", enquanto nada na especificação obriga um host a mostrar structuredContent ao modelo. Uma falha carregada apenas por um campo dentro do payload é, portanto, uma falha que o modelo pode nunca ver, que era todo o problema que search_status foi adicionado para resolver.

O payload ainda acompanha o erro — structuredContent e o bloco de texto estão ambos presentes, então nada é perdido. api_usage em particular: uma busca degradada ainda gastou as requisições RapidAPI do próprio chamador, e esconder isso esconderia uma cobrança que eles têm que pagar. empty e partial não são erros: um é um negativo verdadeiro e o outro carrega resultados que um chamador pode usar.

Relatório de gastos (api_usage)

O dinheiro é seu, então o medidor é visível. Cada resposta bem-sucedida carrega:

CampoSignificado
requests_used_by_this_callRequisições upstream cobradas que esta única chamada de ferramenta consumiu.
plan_requests_remainingO que resta no seu plano RapidAPI neste período.
plan_requests_limitO limite do seu plano para o período.
noteA mesma coisa em uma frase, para o modelo poder transmitir a você antes de você perguntar.

plan_requests_remaining e plan_requests_limit vêm da resposta upstream e são omitidos quando o upstream não os relata; o note se adapta. A regra que o modelo deve declarar em voz alta: uma data × um destino = uma requisição cobrada.

Controles de custo, em ordem de contundência: max_searches por chamada (reduza para gastar menos em uma pergunta ampla), um intervalo de datas mais estreito, uma lista de destinos mais curta.

Uma chamada vs trinta

A API REST subjacente aceita exatamente uma tupla (origin, destination, date) por chamada. Em um encaminhamento direto de uma data por chamada, "mais barato para o Sri Lanka em qualquer dia de outubro" são 31 chamadas de ferramenta separadas: 31 idas e voltas pelo modelo, 31 chances de perder o fio da meada e uma conta que o usuário só descobre depois.

Aqui, é uma chamada de ferramenta. A distribuição acontece no lado do servidor, de forma concorrente, limitada, com amostragem uniforme, deduplicada em buy_link, mesclada, ordenada pelo seu sort_by e relatada com transparência em search_coverage e api_usage.

Este servidor (pago)Servidor gratuito
Distribuição por chamada30 (máximo rígido de 60; aumente ou diminua por chamada com max_searches)15
Anúnciosnenhumum card patrocinado divulgado por resultado
Chavesua própria chave RapidAPInenhuma necessária
Relatório de gastosapi_usage em cada respostan/d
Listável em diretóriosimnão

Este servidor não exibe nenhum anúncio, não por preferência, mas por restrição: a política do diretório de conectores da Anthropic e as diretrizes de aplicativos da OpenAI proíbem publicidade e conteúdo patrocinado em resultados de ferramentas, então um servidor com anúncios nunca pode ser listado lá, e este pode.

Desenvolvimento local

git clone <this repo> && cd mcp_server_paid
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp example.env .env          # fill it in; leave RAPIDAPI_KEY empty
set -a && . .env && set +a
.venv/bin/python -m src      # streamable HTTP on http://localhost:8000/mcp

Aponte um cliente para o processo local da mesma forma:

claude mcp add --transport http google-flights-local http://localhost:8000/mcp --header "x-rapidapi-key: YOUR_RAPIDAPI_KEY"

Testes (620 passando, verificados):

.venv/bin/python -m pytest -q

A configuração fica em example.env; cada variável está documentada lá. As que importam:

VariávelPadrãoPor que importa
MAX_SEARCHES_PER_TOOL_CALL30Limite de distribuição por chamada. Limitado a um máximo rígido de 60.
MAX_CONCURRENT_SEARCHES10Concorrência da distribuição.
MAX_HTTP_CONNECTIONS60Teto do pool de conexões; instâncias serverless compartilham um pool de descritores de arquivo.
REQUEST_TIMEOUT_SECONDS75O Timeout da função upstream (60) mais uma margem de 15s de retransmissão de borda, para que este lado nunca desista de uma resposta que ainda está chegando.
DEFAULT_RESULT_LIMIT10Resultados solicitados por busca upstream individual.
MCP_PRODUCTSbothQual produto esta implantação atende: flights, hotels ou both. Seleciona o conjunto de ferramentas, as instruções do servidor, o nome do serviço, as páginas de política e a listagem RapidAPI para a qual um chamador sem chave ou sem assinatura é enviado. Uma implantação de hotéis deixada no padrão se apresenta como um servidor de voos.
SIGNUP_URLlistagem correspondente a MCP_PRODUCTSCitado de volta para usuários que chegam sem chave. Em both, as ferramentas de hotéis citam a listagem Booking independentemente: uma URL não pode ser o botão de Assinar para duas APIs.
MCP_PRODUCTS_BY_HOSTdefaultQual produto cada hostname atende, para que uma implantação possa carregar ambos os domínios pagos e cada listagem ainda receba exatamente seu próprio conjunto de ferramentas. default é o mapa integrado de aliases de flightpowers.com; off desativa o roteamento por host completamente (o rollback sem código); ou um mapa host=product,… explícito. Um hostname não mapeado cai no padrão MCP_PRODUCTS.
MCP_PUBLIC_URLhttp://localhost:8000/mcpRelatado por /health e a origem de todo link de página de política. MCP_PUBLIC_URL_FLIGHTS / MCP_PUBLIC_URL_HOTELS o substituem por produto em uma implantação que atende ambos. Sem eles, o hostname de hotéis anunciaria o de voos. SIGNUP_URL_FLIGHTS / SIGNUP_URL_HOTELS funcionam da mesma forma.
RAPIDAPI_KEY(vazio)Deixe vazio em produção. Se definido, todo chamador sem chave é atendido e cobrado nessa assinatura. O servidor registra um aviso na inicialização e /health relata server_side_key_configured.
METRICS_TOKEN(vazio)Quando definido, /metrics exige um cabeçalho x-metrics-token.
LOG_PATH(vazio)Vazio desativa o coletor de arquivos; as linhas MCP_CALL do stdout permanecem como registro. Correto em serverless.

Rotas operacionais: GET /health (pública, sem autenticação: registros a consultam), GET /metrics, GET /metrics/calls?hours=24, GET /.well-known/mcp/server-card.json (veja abaixo).

O card estático do servidor

GET /.well-known/mcp/server-card.json retorna os metadados desta implantação como um documento JSON independente: serverInfo, description, transport, capabilities, authentication, instructions e as listas completas de tools e prompts exatamente como tools/list as serializa. Público, sem autenticação, Cache-Control: public, max-age=3600, CORS aberto.

Ele existe porque a URL que publicamos em diretórios é /mcp/oauth, que sempre responde 401, então um scanner automatizado apontado para ESSA url não consegue ler a lista de ferramentas diretamente da conexão. (Desde 2026-09-09, um scanner apontado para /mcp consegue: a descoberta somente leitura é servida sem credencial — veja src/discovery.py.) A página de publicação do Smithery nomeia este documento como o caminho de saída: "Se a varredura automática não puder ser concluída (barreira de autenticação, configuração necessária ou outros problemas), você pode fornecer metadados do servidor manualmente via um card estático do servidor em /.well-known/mcp/server-card.json". A lista de campos segue SEP-1649.

Duas coisas que valem a pena saber:

  • A lista de ferramentas é lida do registro AO VIVO na primeira solicitação, nunca escrita manualmente, então não pode divergir do que tools/list retorna. tests/test_server_card.py compara os dois em ambos os produtos.
  • É por hostname, como tudo aqui: hotels.flightpowers.com retorna o card de hotéis, ambos os hostnames de voos retornam o card de voos, e toda URL dentro está na origem do próprio produto. transport.endpoint é /mcp/oauth onde OAuth está configurado, com o endpoint /mcp com chave listado sob _meta como alternativa.
curl -s https://flights.flightpowers.com/.well-known/mcp/server-card.json | python3 -m json.tool
curl -s https://hotels.flightpowers.com/.well-known/mcp/server-card.json  | python3 -m json.tool

O alvo de implantação é Vercel via api/index.py (wrapper FastAPI entregando o ciclo de vida do FastMCP, stateless_http=True). O caminho MCP canônico é /mcp, sem barra final.

Nunca faça commit de uma chave real. example.env acompanha com placeholders; mantenha assim.

Execute em um contêiner

O servidor hospedado não precisa de nada instalado. Isso é para auto-hospedagem, e é o que permite que o Glama execute seu teste de build e faça um lançamento.

docker build -t flightpowers-mcp .
docker run --rm -p 8000:8000 flightpowers-mcp
curl http://localhost:8000/health

O contêiner serve HTTP transmissível em ${PORT}/mcp, o mesmo transporte da implantação hospedada, via python -m src. api/index.py é o wrapper Vercel e não é usado aqui.

Nenhum segredo é embutido na imagem. Cada busca é cobrada na assinatura RapidAPI do próprio chamador e a chave dele viaja com a solicitação como x-rapidapi-key. RAPIDAPI_KEY é opcional e é um fallback do lado do servidor: quando definido, um chamador que não envia chave própria é atendido e cobrado nessa assinatura. Deixe sem definir, a menos que seja isso que você queira; /health relata server_side_key_configured de qualquer forma.

# optional, local development only
docker run --rm -p 8000:8000 -e RAPIDAPI_KEY=your_key flightpowers-mcp

Toda variável na tabela acima funciona como -e NAME=value. HOST tem como padrão 0.0.0.0 e PORT tem como padrão 8000 dentro da imagem. O HEALTHCHECK consulta /health em $PORT, então substituir PORT continua funcionando.

Não afiliação

Esta é uma API independente que retorna preços de voos publicamente disponíveis. Ela não é afiliada à, endossada por ou patrocinada pelo Google. "Google Flights" é usado apenas para descrever a fonte de dados pública. As tarifas são fornecidas pelo provedor upstream, mudam constantemente e não são garantidas. Sempre confirme o preço no site da companhia aérea ou de reservas antes da compra.