Google 现在让一个 MCP Server 同时进 Search 和 Gemini,而审核是按入口分开的
💡 工具推荐: JSON 格式化、API 测试器、OpenAPI 规范校验
2026 年 10 月 28 日,Google 开放了 Connected Apps Platform 的预览。官宣由 Search 的 Group Product Manager Chips Mistry 和 Gemini App 的 Group Product Manager Mai Lowe 联合署名,措辞很克制:给开发者「一个地方」,去构建和管理同时服务 Search 与 Gemini App 的应用。同一篇公告里还带了一批新伙伴——Adobe、Experian、Localiza、Peloton 的集成已经在 Gemini 里上线,几周内会进 Search,Calm 则两边一起上。真正值得开发者停下来看的是这句话:截至目前,Search 和 Gemini 之间已经上线了 50 多个 Connected App,覆盖生产力、活动票务、健康健身。也就是说,这不是一次普通的 API 发布,而是一个已经跑起来的分发渠道,现在开了门。
接入一次,覆盖两个入口:Search 和 Gemini App。
一、这次放出来的到底是什么
官方描述是「build once, reach high-intent users across both Search and the Gemini app」——一次接入,同时覆盖两个入口,而这两个入口的用户意图浓度都很高:一个是在搜索框里明确要办事的人,一个是在对话里已经把需求说清楚的人。接入方式写得很具体:开发者对接一个云端托管的 MCP server,或者用 AppFunctions API 走 Android 端 MCP;然后在 Google Connected Console 里注册、测试、提交;在正式审核之前,选择这个应用只进 Search、只进 Gemini App,还是两个都进。上线之后可以在控制台里看流量、延迟和错误率,并保留品牌身份的掌控权,包括经过验证的 Logo 和已认证的账号。开放是分阶段的,未来几个月会增加更多开发者、更多市场、更多 Google 产品。文档在 developers.google.com/connected-apps。
# One MCP tool, written so a model can actually choose it.
# The description is not documentation. It is the routing signal.
{
"name": "find_available_appointment",
"description": "Find open appointment slots for a service at a named location. Use when the user asks about availability, openings, or times. Read-only. Returns up to 10 slots.",
"inputSchema": {
"type": "object",
"properties": {
"service": { "type": "string", "description": "Service slug, e.g. 'oil-change'" },
"location": { "type": "string", "description": "City or store name as the user said it" },
"after": { "type": "string", "format": "date", "description": "Earliest date, ISO-8601" },
"max_results": { "type": "integer", "minimum": 1, "maximum": 10, "default": 5 }
},
"required": ["service", "location"]
},
"annotations": { "readOnlyHint": true, "idempotentHint": true }
}
# Rules that survived review on our side:
# 1. Verb-first names, no internal codenames.
# 2. Say what it returns AND how many items.
# 3. One tool, one intent. 'do_everything' never gets picked.
# 4. Mark read-only tools read-only so the surface can skip confirmation.二、接入契约:两条路径,一套后端
两条接入路径的差别不只是 SDK 不同,而是运行时不同。云端 MCP server 是你自己部署、自己运维、自己扩缩容;Android MCP 走 AppFunctions API,能力跑在设备侧,受系统调度和权限模型约束。两条路都通向同一件事:把「能力」以工具的形式暴露给模型,模型根据工具的名字和描述来决定调不调用。这句话的技术后果常被低估——工具描述不是给人看的文档,它是路由信号。描述里写「查询可用预约时段,只读,最多返回 10 条」,模型就知道什么时候该用它;描述里写「核心业务能力封装」,模型就只能猜。所以第一步不是写代码,是把你打算暴露的三到五个工具,用「动词开头的名字 + 它会返回什么 + 返回多少条」重写一遍。
// Android side: the same capability exposed through AppFunctions.
// AppFunctions is how an on-device agent discovers and calls your app.
@AppFunction
suspend fun findAvailableAppointment(
@AppFunctionParam("service") service: String,
@AppFunctionParam("location") location: String,
@AppFunctionParam("after") after: String? = null,
): AppointmentResult {
// Keep this path free of UI. If the agent has to render a screen,
// the interaction has already failed.
val slots = repository.findSlots(
service = service,
location = location,
after = after?.let(LocalDate::parse),
limit = 5,
)
return AppointmentResult(slots = slots, truncated = slots.size == 5)
}
// Two constraints that bite later:
// - Bound every call: an agent may retry quickly and often.
// - Never assume a logged-in UI session; carry identity explicitly.三、审核是按入口分开的,文案也得分开写
公告里有一句容易被略过:在审核之前,开发者要选择这个应用会在 Search、Gemini App 还是两者同时上线。这句话隐含两层工作量。第一层是产品层面:同一个能力在两个入口的呈现方式不同。Search 侧的交互节奏更短、更不容忍等待,一个四秒的工具调用没人会替你解释;Gemini 侧的对话可以先说一句「正在查」,用户愿意多等一会儿。第二层是合规与品牌层面:控制台明确提到保留品牌身份的控制权,包括验证过的 Logo 和认证过的账号,说明审核确实会查「你是不是你说的那个服务」。对做工具站的团队来说,这两个入口意味着两套措辞、两套示例问法、两套失败兜底,而不是把同一段文案复制两遍。
控制台给你流量、延迟和错误率;每次工具调用的成本还得自己埋点。
# Console submission, trimmed to the fields that decide the outcome.
app:
name: "FleetCharge"
mcp_endpoint: "https://mcp.example.com/v1" # cloud-hosted MCP server
android_mcp: "appfunctions://fleetcharge/v1" # optional Android MCP path
auth:
type: oauth2
scopes: ["appointments.read", "appointments.write"]
tools:
- find_available_appointment # read-only
- book_appointment # confirmation required
- cancel_appointment # confirmation required
surfaces: # chosen BEFORE review
search: true
gemini_app: true
brand:
display_name: "FleetCharge"
verified_logo: true
domain_verified: true
review:
data_handling_url: "https://example.com/privacy"
support_contact: "[email protected]"
# Ship Search-only first if your latency budget is tight: the query box
# is less forgiving than a chat turn that can say 'still looking'.四、控制台给你的指标,和它不给你的
上线后能看到的是流量、延迟、错误率——以入口维度切分。这三个数字足够回答「有没有人在用」和「有没有坏掉」,但回答不了「哪个工具慢」「哪次调用贵」「用户在哪个参数上卡住」。这类问题只能在自己的 MCP server 上埋点:每个工具一个直方图,按 tool、surface、status 三个维度打标签;错误按原因分类计数,而不是只记一个 500。为什么要按 surface 分开看?因为两个入口对延迟的容忍度真的不一样:搜索框里没人会等,聊天里用户至少看到了「正在处理」。同一套 p95 阈值套到两个入口上,一定会误判其中一个。
// The console reports traffic, latency and error rate per surface.
// It does not tell you which tool is slow. Instrument that yourself.
import time
from prometheus_client import Histogram, Counter
TOOL_LATENCY = Histogram(
"connected_app_tool_latency_seconds",
"Per-tool latency as seen by the MCP server",
["tool", "surface", "status"],
buckets=(0.05, 0.1, 0.25, 0.5, 1, 2, 5),
)
TOOL_ERRORS = Counter(
"connected_app_tool_errors_total",
"Tool failures by reason",
["tool", "surface", "reason"],
)
def call_tool(tool: str, surface: str, fn, *args, **kwargs):
start = time.perf_counter()
status = "ok"
try:
return fn(*args, **kwargs)
except TimeoutError:
status = "timeout"
TOOL_ERRORS.labels(tool, surface, "timeout").inc()
raise
except Exception:
status = "error"
TOOL_ERRORS.labels(tool, surface, "unhandled").inc()
raise
finally:
TOOL_LATENCY.labels(tool, surface, status).observe(time.perf_counter() - start)
# What to watch in week one: p95 per surface. Search traffic punishes
# slow tools differently than a chat turn does: no 'thinking' message
# covers for a four-second call.五、先做哪五个工具,别做什么
如果你的服务已经有 REST API,最容易犯的错是把整个 API 目录搬过去。工具不是接口,工具是「用户会说的那句话」的对应物。按这个标准筛,先做五类:查询类(有没有、多少钱、什么时候有空)、列表类(给我几个选项)、状态类(我的单子到哪了)、比较类(A 和 B 哪个更合适)、以及需要用户确认的写操作(下单、改期、取消)。写操作一定要标成需要确认,并且可撤销——这是审核会看、用户会记恨的地方。反过来,不要做的包括:一次调用返回几十条数据(入口渲染不了)、需要多轮澄清才能凑齐参数的(模型会放弃)、以及名字里带内部代号或缩写的(没有人能理解)。分阶段开放意味着你现在提交,可能只拿到部分市场的访问权,这反而是好事:先用小流量把 p95 和错误率压下去,再谈扩大。
按入口分开审核,意味着文案要写两遍,而不是复制粘贴。
# One contract, two surfaces: keep the difference in copy, not in code.
import pytest
from app.contracts import find_available_appointment
SURFACES = ["search", "gemini_app"]
@pytest.mark.parametrize("surface", SURFACES)
def test_same_intent_same_shape(surface):
result = find_available_appointment(
service="oil-change", location="Seattle", surface=surface
)
assert result["slots"], "empty results waste a surface slot"
assert len(result["slots"]) <= 5, "cap payloads; the surface renders a list"
assert result["surface"] == surface
@pytest.mark.parametrize("surface", SURFACES)
def test_never_raises_on_bad_input(surface):
# Agents pass user language straight through. Swallow and explain.
out = find_available_appointment(service="", location="", surface=surface)
assert out["error"]["retryable"] is False
assert "service" in out["error"]["message"]
# Run this before every submission: the review gate checks behaviour on
# both surfaces, and a crash on one delays both.📌 常见问题 FAQ
Connected Apps Platform 现在能用吗?
10 月 28 日开放的是预览(preview),采取分阶段开放的方式,官方说明会在未来几个月增加更多开发者、市场和 Google 产品。入口在 developers.google.com/connected-apps,需要先申请预览。
必须做 Android 端才能接入吗?
不用。公告给了两条路径:云端托管的 MCP server,或者用 AppFunctions API 的 Android MCP。前者是你自己的服务,后者是设备侧能力。只做 Web 服务的团队走第一条就行。
AppFunctions 是什么?
公告里的表述是「Android MCP using the AppFunctions APIs」,也就是把应用能力以函数形式暴露出来,供设备侧 agent 发现和调用。对开发者的实际要求是:这条路径不能依赖 UI 交互,必须在没有界面渲染的情况下返回结构化结果。
审核之前要决定什么?
要选择这个 Connected App 上线到 Search、Gemini App,还是两者都上。这个选择在提交审核之前做,并且直接决定两边的呈现方式和延迟预算,所以应该当成产品决策,而不是发布开关。
控制台能看到哪些数据?
官方列出的三项是流量、延迟、错误率。它不提供按工具维度的性能拆分,也不提供单次调用成本,所以工具级监控要自己在 MCP server 上做。