Skip to main content
Glama
mtnrabi

Google Flights MCP

Google Flights MCP — 免费、由广告支持

The real-time one-way and round-trip flight search for any MCP client. 无需 API 密钥、无需账户、无需付费。 它的运营资金来自依附于每条结果上的一条披露过的赞助卡片,而不是向你收费。

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

这就是完整配置方式。没有需要黏贴的密钥,也没有任何需要配置的东西。


两个工具

工具

功能

search_oneway_flights

按日期范围和目的地列表进行单程搜索

search_roundtrip_flights

往返票价按成对航段计费,同时支持日期范围和 nights 值

两者都接受一个日期范围(departure_date_from / departure_date_to)和一个目的地列表,并在服务器端进行展开。一次用户意图就是一次工具调用 —— “十月中随时去瑞士卡是否有最便宜机票” 就是一次调用,而不是三十次。

search_oneway_flights(from_airport="TLV", to_airport="CMB",
                      departure_date_from="2026-10-01",
                      departure_date_to="2026-10-31")

  → 31 combinations requested, 15 searched, results merged and re-sorted

search_roundtrip_flights 另外还接受 nights —— 一个数字或像 [5, 6, 7] 这样的列表 —— 而不需要回定 return_date,所以 “五月任意日に罗马住 5–7晚” 也是一次调用。

返回什么

每条结果都带价格、总时长、航空公司、经停次数、经停机场和中转时长,以及可预订的 buy_link。它还带 Google 对该航线和该时段的历史价格区间 —— price_insights_low、price_insights_high,以及在 price_range_in_relation_to_other_periods 中对 low / typical / high 的判断。

这最后一项正是选择它而不是一条简单价格 scrape 的原因:它能让助手说 “这个航线 $209 属于正常水平,不急着买”,而不仅背出一个数字。

一个空的结果列表意味着“该航线、该日期没有航班” —— 这是一个有效答案,而不是错误。

票价是实时,且仅实时有效

Every time you call is a fresh search. 票价几分钟内就会过期,所以结果永远不应缓存、存储,或事后当作即时的重新引用。如果一个答复引用了某项 fare,它应说明这是几时抓取,且应 re-search而非复用。


Related MCP server: trvl

它如何免费

每个成功的 result 都带一条明确披露的赞助卡片。那句卡就是整个运营模型 —— 是它付了后端搜索调用的成本,从而你不需要拿 key 或银行卡。

把这说清楚一点,因为你得知道你的是:

  • 每条结果最多一张赞助卡,且总是标注为“赞助”。

  • 绝不挂在错误上,也绝不挂在零结果答案上。但现在有点烦人:广告旁边没有任何实际数据的文字更像是 prompt injection尝试 —— Lulu 自己的实测精 3 次里有 3 次模型明确标出了这一点。

  • 广告不可能导致搜索挂掉。 广告 SDK 没有 credentials 时完全不工作,反正在任何网络错误上都会“fail open”。

  • 广告在 Lulu 的传送。本服务器不把你的身份发给它 —— 它只是发一个 “sloot” 请求,然后渲染返回内容。

因为涉及广告,这个服务器没有列官方 MCP Registry,也不在 Anthropic 与 OpenAI 的目录中:这三者都禁止现代广告的服务器。这是免费模式的刻意且已知后果,不是疏漏。

一个值得先说明的限制

“赞助卡片只能在支持 MCP widget 的助手内渲染。Caller 会按来源 IP 分类;而 enforcement 模式下,无法渲染卡片的客户端可能只分到更小的 fan-out 上限,或被拒绝。默认部署模式是 monitor —— 分类并记日志,服务所有人 —— 不过如果你要把这个接入 headless script 而不是 assistant,下面付费服务更合适,并且不受这种限制。

当你超免费版时

还有还有一个付费、无广告的同类,拥有相同的两个工具和相同的后端:https://google-flights-mcp.flightpowers.com/mcp

这里的任何能力都紧锁在它背后的 —— 免费版本身功能完整。当广告、15 则搜索限制或网客端限制开始碍事时,才现,就挑付费的。

免费 —— 本服务器

付费 —— google-flights-mcp

广告

每 Result 附带一条披露的赞助卡

无

API 密钥

无需

自己的 RapidAPI key

每次展开的上限

15 次搜索

30 次搜索(硬上限 60 / 每调用调用 max_searches 覆写)

费用报告

不适用

every response 会返回 api_usage

客户端限制

按层级分类;不可渲染客户端肯会被置限或拒绝

无 —— 任何客户端、任何传输

MCP Registry 上列入

不允许(注册广告真时 ban)

是,以 com.flightpowers/google-flights-mcp

成本

免费

你的 RapidAPI 用量

这里 fair trade-off 非常清楚:付费的版本“无广告、有限制”是因为 你 用自己 RapidAPI 的 key 负担搜索,而不是赞助商付。

得到 key 只要一分钟。 在 RapidAPI 上开通 Google Flights Live API —— 有免费 tier —— 然后复制你的 x-rapidapi-key:

https://rapidapi.com/mtnrabi/api/google-flights-live-api

然后把客户机指向付费版服务器:

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

Key 也可以被我当作一个 ?rapidapi_key= 查询参数传入,或在客户端提供了 API-key 字段时传入。每一次付费响应里都包含一个 api_usage,并有 requests_used_by_this_call、plan_requests_remaining 和 plan_requests_limit,这样你下一次调用前就能看到你的余额,而不是等到月底。


截断总是会报告

当一次请求超过上限时,日期会被在日期范围内均匀抽样,而不是缩减成前 N —— 十月 15 个均匀分布的天可完成“十月最便宜”;但只取前 15 天做不到。每次响应会带 search_coverage:

{"requested_combinations": 31, "searched_combinations": 15, "truncated": true,
 "departure_dates_searched": ["2026-10-01", "2026-10-03", "..."],
 "note": "This request expanded to 31 searches, above the 15-search limit..."}

一次 silent 的截断看起来就像一次完整搜索,这也正是用户会相信一个“最便宜”的答案,而它其实完全没有看到十月的后半段。

sort_type 不出口

在后台端,sort_type 它选择 哪个搜索运行,并不是 post-sort;一个-way 时上游 API 会默默 drop 它,另外 max_price 也会 override 用 它(app.py:255)。因为这个服务器反正要合并 up to 15 个搜索结果并 merge / re-sort,它总是让后端默认 sort 方案生效,然后自己用 sort_by(best / price / duration)排序。这是确定的;而把 sort_type 传过来则不确定。


Non-affiliation

The service is independent, returning publicly available flight price data. 它与 Google没有从属关系,也未获 Google’s 支持或背书。“Google Flights” 只是用它来描述价格数据来源是。


运行你自己的 instance

下文都是用来你自己运营这台服务器。如果你只是要用它,顶部的安装一行已经就绪。

MCP client ──HTTP──▶ this server ──POST──▶ upstream travel API ──▶ Google Flights
                          │
                          └──POST /slot──▶ ads.getlulu.dev

它等到上游 da microprocessor 的 travel API,这让它刻意与按 caller 计费用的付费渠道分开。

快速开始

cd mcp_server
python3 -m venv .venv && .venv/bin/pip install -r requirements.txt
cp example.env .env          # fill it in
set -a && . .env && set +a
.venv/bin/python -m src      # serves on :8000/mcp

在 /health 可以查健康,在 /metrics 可以看实时计数。

配置

BASE_LAMBDA_URL 和 RAPID_AUTH 把这个服务器指向你要对接的上游 travel API 并完成鉴权。如果你是从一个引号包着的 env 文件中复制值,要注意:loader 只会剥掉一层包裹引号 —— 直接包含引号粘贴过去会产生 403,看起来像密钥错了,而不是引号错误。

全部变量都请参考 example.env。这 3 个变量最重要:

变量

为什么重要

MCP_PUBLIC_URL

必须与客户端连接引用的 URL 完全一致。 见下文。

MAX_BACKEND_CALLS_PER_TOOL_CALL

成本上限。默认 15。

ENFORCEMENT_MODE

off / monitor / enforce。默认 monitor。

MCP_PUBLIC_URL 是一种不会报错的失败模式

Lulu 会从这个值通过哈希生成 Claude 的 _meta.ui.domain。Claude 自己也会重算同一 hash,并且会拒绝 domain 不匹配的东西。一个错误的 URL 就是:赞助卡片永远无法展示,也不会有任何错误抛出,而且 CPM 会一直 B零。

这台 server 会在 startup 时把派生出来的 domain 记下来,让你可以检查这个值:

sponsored widget domain for https://.../mcp -> <hash>.claudemcpcontent.com

为什么展开服务被放到内部

backend take 每一次调用就是一个 (origin, destination, date) 元组,所以“十月份去瑞士兰卡最便宜的航班”就是 31 个后端调用。这在付费 API 上没关系 —— 每次调用都是收入。但在这里,每tool 调用才一份 render-ad 的收入,每个后端调用都是支出,所以让模型丢 31 个 tool call 就是 1 份收入、31 份成本。

它收到模型调用后就会在“cap”内自行展开。

成本控制

两套独立的护栏:

  1. 每次 tool call 级 —— 由 MAX_BACKEND_CALLS_PER_TOOL_CALL 控制,默认 15。

  2. 滚动 24 小时级 —— 使用 DAILY_BACKEND_CALL_BUDGET(0 表示禁用)。在 BUDGET_DEGRADE_AT 这一比例时它会触发。

再是说:“当预算用了到一个 BUDGET_DEGRADE_AT(比如 80%)时,每次调用层上限会降到原来的三分。

减少优于拒绝:如果在 80% 时就硬停,那全天用户都会觉得像“挂掉了”。

这个 24 小时窗口会跨重启生存 —— 崩溃循环不能给 process 一个新预算。如何 survive 取决于 store:与 Upstash 的 store 窗口 Concurrent 在 Redis,跨多个 instance 共享;与 in-process 存储则会在启动时从 LOG_PATH 文件重建窗口。而都没有 —— both + no Upstash and no writable log file —— 每次冷启动都会重新预算,而且无是可控的。 这在 versel 就是 default,于是你要在 versel 把 Upstash 配置好;/memetrics会报 budget.enforceable`,这样你永不必去猜。

Enforcement:什么是能做到和什么无法做到

目标是“只让赞助卡可渲染的 assistant 使用它”。最直接的那条路不 work,所以值得精准说明。

clientInfo.name 不可能 gate 任何东西。 MCP spec rev2025-07-28 say so normatively: these fields “are self-reported by the sender, and not verified by the protocol… SHOULD NOT depend on them for security decisions” A script把 "claude-ai" 写在十行和 Claude 一样容易。因此它只被记录,仅供报表。

Source IP 可以。 IP 在完成的 TLS handshake 中不能被伪造,而且 both host 代理远程 MCP 时都是在自己的 cloud 而不是用户机上:

层级

判定方式

是否渲染组件?

llm_host

Anthropic 160.79.104.0/21,或 OpenAI 发布的 chatgpt-connectors.json 前缀

是——唯一能触发信标的层级

local_client

客户端名称提示(Claude Code、Cursor 等)

否——仅 CLI 文本卡片,结构上为 $0 CPM

unknown

其他所有情况

否

在 enforce 模式下,llm_host 获得完整配额,其他层级获得三分之一,而 BLOCKED_TIERS 中的任何内容都会被拒绝。有两条刻意的安全规则:

  • monitor 是默认模式。第一周就拦截会毁掉 POC 存在所要收集的流量样本。

  • 在 OpenAI 的地址段源未加载时,unknown 永远不会被拦截,因为如果没有它, 每个真实的 ChatGPT 调用者看起来都像是 unknown。

此服务器无法衡量的内容

渲染曝光信标从 Lulu 组件框架内部直接发送到 ads.getlulu.dev。它从不经过此进程, 因此此服务器无法自行计算真实的渲染率。

它做的是按层级统计已投放的槽位。将其与 Lulu 报告的渲染曝光进行对账, 得到真实比率,然后将任何渲染为零的层级放入 BLOCKED_TIERS。机制已构建; 输入是由真实数据支撑的人工决策。

统计 Lambda 请求

每次工具调用恰好向 stdout 输出一行,前缀为 MCP_CALL 。 下面的字段名是真实的;数值仅为示意——这里没有任何已发布的延迟或流量数据:

MCP_CALL {"iso":"2026-08-06T09:39:37Z","tool":"search_roundtrip_flights",
 "tier":"unknown","requested_combinations":3,"backend_calls":3,
 "results_returned":4,"ad_eligible":true,"duration_ms":0}

backend_calls 是该调用的 Lambda 请求数。 将任意时间段内所有 MCP_CALL 行中的该值相加,就得到后端请求总数。stdout 是记录载体,因为它是唯一在所有 环境中都有效的输出端——Vercel 会自动捕获它,而那里的文件系统是只读的。

每次工具调用一行,而非每次后端调用一行:Vercel 将运行时日志限制为 每次请求 256 行和 1 MB。

LOG_PATH 为容器部署增加了第二个文件输出端。如果路径不可写,它会自行禁用, 而不是向虚空追加。

指标

GET /metrics(设置 METRICS_TOKEN 以要求 x-metrics-token 请求头)。 格式如下,数值仅为示意——并非实际用量数据:

{"totals": {"tool_calls": 2, "backend_calls": 4, "ad_eligible_calls": 2},
 "backend_calls_per_tool_call": 2.0,
 "by_tier": {"llm_host": {"tool_calls": 2, "backend_calls": 4}},
 "budget": {"used_24h": 4, "budget": 1000, "remaining": 996,
            "enforceable": false},
 "durable_counters": false,
 "notes": ["..."]}

GET /metrics/calls?hours=24 返回按 UTC 小时统计的调用次数。

backend_calls_per_tool_call 是决定此渠道能否在 POC 之后存续的数字。 每次后端调用都是成本;每次工具调用最多对应一个渲染广告。将其乘以真实的 单次调用 Lambda + 代理成本,再与广告网络实际报告的 CPM 进行比较。

durable_counters: false 和 budget.enforceable: false 意味着这些总计 仅覆盖单个进程。在容器上这没问题。在 Vercel 上则意味着真实数字高于显示值, 且支出防护并未真正启用——见下文。

计数器存储

计数器位于一个小接口之后,因为部署目标决定了哪种实现是正确的:

存储方式

适用场景

持久性

进程内字典

容器、本地

否

Upstash Redis (REST)

Vercel、任何 serverless

是

在 Vercel 上,Fluid compute 在并发调用之间共享一个实例,并可自由扩展实例, 因此进程内计数器会碎片化并重置。它们不会报错——只是返回一个小于真实值的数字, 而这正是支出防护最糟糕的失败方式。因此 DAILY_BACKEND_CALL_BUDGET 只有在 使用共享存储时才可强制执行,服务器在启动时和 /metrics 中都会如实说明, 而不是假装可以。

vercel install upstash   # injects UPSTASH_REDIS_REST_URL / _TOKEN

Upstash 通过其 REST API 访问,而非 Redis 线协议:短生命周期调用不适合 连接池客户端,这也使 redis 不必进入依赖列表。每次存储失败都会被记录并吞掉—— 计数器故障绝不能变成航班搜索的故障。

广告接线

以组件 + 中间件的方式分别接线,而非使用一行式的 enable_lulu_ads, 因为只有中间件路径接受 is_error_result——这正是让广告不出现 在空结果上的机制。

结果通过 Lulu 的 result 组件渲染,而非其赞助卡片。赞助卡片会绘制广告, 但不包含渲染曝光信标;信标只存在于 result 组件固定的 SPONSORED 条中。 仅投放赞助卡片能获得点击收入,但 CPM 恰好为 $0,且没有任何地方报告问题。 赞助卡片保留为 result 组件注册失败时的回退方案,且该回退会被大声记录在日志中。

信标是一个指向 ads.getlulu.dev 的 1×1 <img>。MCP Apps 主机应用默认的 CSP 为 img-src 'self' data:,因此该域名必须在组件资源上声明——通过 app= 用于 MCP Apps,通过 openai/widgetCSP 用于 ChatGPT。未声明时,从外部看 与正常工作的集成完全一样:卡片渲染、条带显示、无曝光、无错误。

注册一次,然后设置 LULU_ADS_PUBLISHER_ID / LULU_ADS_API_KEY:

curl -X POST https://ads.getlulu.dev/publishers \
  -H 'content-type: application/json' \
  -d '{"name":"...","contact_email":"...","server_url":"..."}'

API 密钥仅返回一次。

计费字段是 /slot 响应上的 imp_url(snake_case)。TypeScript 类型定义称其为 impUrl,但 Python SDK 对响应键做了白名单过滤, 只读取 imp_url(lulu_ads/client.py:210)。一旦有了真实凭据, 请确认真实的 /slot 响应确实携带该字段——没有它,组件就没有可触发的信标, 也就不存在可计费的曝光,无论其他一切运行得多好。

测试

.venv/bin/python -m pytest tests -q     # 141 tests

后端和广告服务器均为桩实现,因此测试套件不需要网络和凭据。 tests/test_ads.py 和 tests/test_stores.py 运行真实的桩服务器, 而非模拟 SDK,因此它们实际验证了真实的线上格式。

部署到 Vercel

api/index.py      entrypoint, exports `app`
vercel.json       maxDuration + no-store on /mcp
.python-version   3.13
requirements.txt

Vercel 自动发现 api/index.py,并将整个应用作为一个 单一 Fluid 函数 提供服务,接收所有路径,因此 /mcp、/health 和 /metrics 都落在 同一个进程中。无需重写规则。

vercel link          # new project
vercel install upstash

# `vercel env add` takes ONE variable per invocation --
# `vercel env add <NAME> [environment]`. Each prompts for the value.
vercel env add BASE_LAMBDA_URL production
vercel env add RAPID_AUTH production
vercel env add MCP_PUBLIC_URL production
vercel env add LULU_ADS_PUBLISHER_ID production
vercel env add LULU_ADS_API_KEY production

vercel deploy --prod

此代码已处理的四个 Vercel 特定事项,每一项如果搞错都会静默失败:

生命周期(Lifespan)。 FastMCP 在 ASGI lifespan 启动事件中初始化其 streamable-HTTP 会话管理器。没有它,每个 /mcp 请求都会以 Task group is not initialized 失败——这是 MCP 端点的完全中断。 Vercel 的 lifespan 支持(2025-12-09 发布)被宣布为适用于 "FastAPI apps"; 是否覆盖裸 Starlette 应用并未说明。因此 api/index.py 将 FastMCP 应用 包装在 FastAPI 中,并传递 lifespan=_mcp_app.lifespan,这也是 FastMCP 文档中记载的集成方式。不要把这个"简化"掉。

无状态(Stateless)。 stateless_http=True。调用是短生命周期的, 且不保证到达同一实例,因此没有地方保存会话。

文件系统。 除临时 /tmp 外为只读。保持 LOG_PATH 为空; stdout 的 MCP_CALL 行就是记录。

计数器。 配置 Upstash,否则支出防护只是装饰——见上文。

其他在此处重要的限制:maxDuration 在 Fluid 下的所有套餐中默认为 300 秒 (后端调用峰值约 90 秒,因此足够);响应采用流式而非缓冲,因为 json_response=True 会重新触发 4.5 MB 请求体上限,而 15 路扇出可能接近该上限; 共享的 1,024 文件描述符池正是 MAX_HTTP_CONNECTIONS 存在的原因。

日志保留期为 Hobby 套餐 1 小时,Pro 套餐 1 天。如果 MCP_CALL 行 是 POC 的计费证据,那就有理由选择 Pro 并配置日志转储,或者将计数器 发送到 Upstash 并读取 /metrics。

部署为容器

Dockerfile 构建一个常驻 HTTP 服务,与 backend/Dockerfile(部署到 Render) 形态相同。设置 LOG_PATH 并在 /app/logs 挂载卷,以便支出防护的 滚动窗口在部署后仍然存活。

无论哪种方式:部署后,将 MCP_PUBLIC_URL 设置为真实 URL——不带尾部斜杠—— 并在启动日志中确认组件域名。

Related MCP Connectors

Related MCP Servers