Google Now Lets One MCP Server Reach Both Search and Gemini — and the Review Gate Is Per Surface

·10 min read·Evergreen Tools Team

On October 28, 2026, Google opened the preview of its Connected Apps Platform. The announcement carries two names — Chips Mistry, Group Product Manager for Search, and Mai Lowe, Group Product Manager for the Gemini app — and unusually restrained language: a single place for developers to build and manage apps for both Search and Gemini. The same post ships new partners. Adobe, Experian, Localiza and Peloton integrations are already live in Gemini and arrive in Search over the coming weeks; Calm is coming to both. The sentence worth pausing on is this one: more than 50 Connected Apps are already live across Search and Gemini, spanning productivity, event ticketing, health and fitness. That makes this less an API launch than a distribution channel that has been running quietly and has now opened a door.

Lines of code on a laptop screen

Integrate once, appear on two surfaces: Search and the Gemini app.

1. What actually shipped

The pitch is build once, reach high-intent users across both Search and the Gemini app. Those are two different kinds of intent: someone typing a task into a search box, and someone who has already explained what they want in a conversation. The integration path is stated concretely. Developers integrate with a cloud-hosted MCP server, or with Android MCP using the AppFunctions APIs, then register, test and submit through the Google Connected Console. Before review, they choose whether the app goes live in Search, in the Gemini app, or both. Once live, the console shows traffic, latency and error rates, and developers keep control of brand identity, including verified logos and authenticated accounts. Access is phased — more developers, markets and Google products over the coming months — and the documentation lives at 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.

2. The integration contract: two runtimes, one backend

The two paths differ in more than SDK. A cloud-hosted MCP server is yours to deploy, operate and scale. The Android MCP path runs through AppFunctions on the device, which means the system's scheduling and permission model constrains you. Both converge on one idea: expose capabilities as tools and let the model decide, based on the tool's name and description, whether to call it. The consequence is easy to underestimate — the description is not documentation for humans, it is the routing signal. A description that says "find open appointment slots for a service at a named location; read-only; returns at most 10 slots" tells the model exactly when to reach for it. A description that says "core business capability wrapper" tells the model nothing. So the first task is not code. It is rewriting the three to five tools you plan to expose as verb-first names plus what they return plus how many items.

// 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.

3. Review happens per surface, so the copy has to be written twice

One line in the post is easy to skim past: before review, developers choose whether the app goes live in Search, in the Gemini app, or both. That choice carries two layers of work. The product layer: the same capability lands differently on each surface. Search is short-cycle and unforgiving — nobody is going to narrate a four-second tool call for you — while a chat turn can say "still looking" and buy patience. The compliance and brand layer: the console explicitly mentions keeping control of brand identity, including verified logos and authenticated accounts, which tells you review genuinely checks that you are the service you claim to be. For a team running a tool site, two surfaces means two sets of phrasing, two sets of example queries, and two sets of failure fallbacks. Copy-pasting one paragraph twice is how you fail review on one side and never learn why.

Dashboard showing charts and metrics

The console gives you traffic, latency and error rates. Per-tool cost is still yours to instrument.

# 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'.

4. The metrics you get, and the ones you have to build

What the console reports is traffic, latency and error rates, split by surface. Those three numbers answer "is anyone using it" and "is it broken". They do not answer which tool is slow, which invocation is expensive, or which parameter users get stuck on. That class of question only exists on your own MCP server: one histogram per tool labelled by tool, surface and status, and counters that record a reason instead of collapsing everything into a 500. Splitting by surface matters because tolerance genuinely differs. Nobody waits inside a search box; a chat user at least saw a spinner. Applying one p95 threshold to both surfaces guarantees you will misjudge one of them.

// 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.

5. Five tools to ship first, and what to leave out

If your service already has a REST API, the tempting mistake is to port the whole catalogue. Tools are not endpoints; a tool is the thing a user says out loud. Filter by that standard and five families survive: lookup (is it available, how much, when), listing (give me a few options), status (where is my order), comparison (which of these two fits), and a write action that requires confirmation (book, reschedule, cancel). Write actions must be confirmed and reversible — this is what review checks and what users will hold against you. Leave out anything that returns dozens of rows, anything that needs three clarifying turns to assemble its arguments, and anything named after an internal codename. Phased access means a submission today may only grant some markets, which is a feature: use the small traffic to pull p95 and error rates down before asking for more.

A team discussing a product in a meeting room

Per-surface review means writing two sets of copy, not copy-pasting one.

# 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.

📌 Frequently Asked Questions

Is the Connected Apps Platform generally available?

No. October 28, 2026 opened the preview, with access rolled out in phases and more developers, markets and Google products added over the coming months. Sign-up and docs are at developers.google.com/connected-apps.

Do I need an Android app to participate?

No. The post describes two paths: a cloud-hosted MCP server, or Android MCP using the AppFunctions APIs. A team with only a web service takes the first path.

What are the AppFunctions APIs?

The post's phrasing is "Android MCP using the AppFunctions APIs" — app capabilities exposed as functions that an on-device agent can discover and call. Practically, that path cannot depend on UI interaction; it has to return structured results with no screen rendered.

What has to be decided before review?

Whether the Connected App goes live in Search, in the Gemini app, or both. That choice is made before submission and drives both the presentation and the latency budget, so treat it as a product decision rather than a release toggle.

What can I measure in the console?

The three named metrics are traffic, latency and error rates. There is no per-tool performance breakdown and no per-invocation cost, so tool-level observability has to be instrumented on your MCP server.