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这就是完整配置方式。没有需要黏贴的密钥,也没有任何需要配置的东西。
两个工具
工具 | 功能 |
| 按日期范围和目的地列表进行单程搜索 |
| 往返票价按成对航段计费,同时支持日期范围和 |
两者都接受一个日期范围(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-sortedsearch_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而非复用。
它如何免费
每个成功的 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 则搜索限制或网客端限制开始碍事时,才现,就挑付费的。
免费 —— 本服务器 | 付费 —— | |
广告 | 每 Result 附带一条披露的赞助卡 | 无 |
API 密钥 | 无需 | 自己的 RapidAPI key |
每次展开的上限 | 15 次搜索 | 30 次搜索(硬上限 60 / 每调用调用 |
费用报告 | 不适用 | every response 会返回 |
客户端限制 | 按层级分类;不可渲染客户端肯会被置限或拒绝 | 无 —— 任何客户端、任何传输 |
MCP Registry 上列入 | 不允许(注册广告真时 ban) | 是,以 |
成本 | 免费 | 你的 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 个变量最重要:
变量 | 为什么重要 |
| 必须与客户端连接引用的 URL 完全一致。 见下文。 |
| 成本上限。默认 15。 |
|
|
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”内自行展开。
成本控制
两套独立的护栏:
每次 tool call 级 —— 由
MAX_BACKEND_CALLS_PER_TOOL_CALL控制,默认 15。滚动 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 而不是用户机上:
层级 | 判定方式 | 是否渲染组件? |
| Anthropic | 是——唯一能触发信标的层级 |
| 客户端名称提示(Claude Code、Cursor 等) | 否——仅 CLI 文本卡片,结构上为 $0 CPM |
| 其他所有情况 | 否 |
在 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 / _TOKENUpstash 通过其 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.txtVercel 自动发现 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——不带尾部斜杠——
并在启动日志中确认组件域名。
This server cannot be installed
Maintenance
Resources
Unclaimed servers have limited discoverability.
Looking for Admin?
If you are the server author, to access and configure the admin panel.
Related MCP Connectors
AI marketplace — flights, tours, activities, transport & more via MCP. No auth required.
Free, no-login flight search with real-time pricing from multiple airlines.
Duffel MCP — live flight search + pricing via the Duffel Flights API (duffel.com)
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/mtnrabi/google-flights-mcp-free'
If you have feedback or need assistance with the MCP directory API, please join our Discord server