Protocol Proposal · 2026

WebMCP & AgentBridge

Frontend-orchestrated AI agents — one function call instead of many

Token savings
85.7%
vs. 3-call MCP baseline
HTTP round-trips
−66.7%
10 calls instead of 30
Wall-clock (MCP)
−67%
30 ms/booking vs 91 ms
Operations registered
47
across 6 domains, + 8 meta-ops
Agenda

10-minute walk-through

0:00
The problem — why current agent patterns are expensive
2:00
The protocol — WebMCP + AgentBridge architecture
4:00
Progressive tool disclosure — how agents discover operations
6:00
Benchmark results — token and latency savings
8:00
Security, RBAC, and audit model
9:30
Next steps & Q&A
The Problem

How agents work today — and what it costs

Every subsequent tool call re-reads all prior results. Cost compounds.

3-call pattern (status quo)
1. searchAvailability
↓ agent reads result #1
2. createReservation
↓ agent re-reads results #1 + #2
3. getReservation
3 HTTP calls · growing context
AgentBridge composite (this protocol)
1. book({ date, time, partySize, name })
↓ orchestration runs server-side
← structured result, zero cumulative context
1 HTTP call · no context bleed
The agent calls one named function. Domain logic lives in the page — not as tokens in the agent's context.
Approach Comparison

Three strategies for AI-agent integration

Approach Stability Token cost Agent burden HTTP calls / action
Browser automation (Playwright) Breaks on UI change Very high (DOM) Must map UI → intent Many
Raw MCP tool calls Stable High — 3+ / action Knows domain logic 3+
AgentBridge / WebMCP Stable — API contract Low — 1 call Calls one function 1
Architecture

One registry, two surfaces

All 47 operations are reachable over MCP Streamable HTTP (server) via progressive disclosure. The browser deliberately exposes a smaller surface — composite tools only (book, seatGuest) — so agent-facing UI stays a curated, high-leverage set rather than a mirror of the full registry.

Single source of truth
lib/operations/ — Operation registry
name · description · inputSchema (Zod) · roles · handler
dispatched to both adapters ↓
WebMCP (browser)
document.modelContext.registerTool()
Session cookie auth
MCP HTTP (server)
app/api/[transport]/route.ts
RFC 8707 Bearer token
8 Always-on MCP tools
explore — navigate module tree
search — glob over op paths
describe_tool — get input schema
invoke — call any op directly
load_tools — promote to native
unload_tools
getCapabilities — version hash
getContext — current session/role
Progressive Tool Disclosure

Agents discover operations without loading all 47

Path A — Load & call natively
1. explore("reservation.*") → subtree
2. load_tools(["book"]) → session state
3. Call book(…) as a native tool

Tool state persists per-token, 24h TTL. Agent sees only what it loaded.

Path B — Invoke directly (no state)
1. invoke("book", {…})
← result returned inline

One-off calls without session overhead. Reads dispatched concurrently; writes sequentially.

Token cost by strategy (7 ops)
Dump all ops
Progressive load (2 ops)
Progressive invoke
Dump all (7 ops)
~700 tok
Progressive load
~485 tok
Progressive invoke
~200 tok

For 50 ops: dump all = ~5 000 tokens; progressive invoke stays flat at ~200.

Benchmark Results

10 bookings, localhost, in-memory store

3-call MCP (baseline)
book() WebMCP
book-op MCP (server-side)
Total tokens
5,674
811 (−85.7%)
811 (−85.7%)
HTTP calls (10 bookings)
30
10 (−66.7%)
10 (−66.7%)
Avg latency / booking
91 ms
98 ms (browser sub-calls)
30 ms (−67%) ← in-process
Cumulative context tokens
3,251
0 (−100%)
0 (−100%)

Key finding: book-op MCP (server-side in-process dispatch) is 3× faster than WebMCP because internal sub-calls skip HTTP hops. WebMCP still saves 85.7% of tokens vs. the baseline.

Figures above from benchmark.mjs (Node-side simulation over /api/call). The 98 ms WebMCP figure was estimated, not measured in a browser — see the next slide for the real-transport numbers.

Benchmark Results · Real Transports

Four lanes, one flow — book(), measured live

scripts/bench.mjs replaces the estimate on the previous slide with numbers from the real MCP Streamable HTTP transport and a real headless browser — no simulation. Booking flow, 5 runs/lane, production build.

Lane Transport Calls Avg wall-clock Tool-schema tax Agent input Agent output
3-call MCP (load + native) Real MCP HTTP 3 47 ms 80 tok 62 tok 304 tok
3-call MCP (invoke, no load) Real MCP HTTP 3 33 ms 0 tok 58 tok 360 tok
book() composite Real MCP HTTP 1 14 ms 41 tok 26 tok 65 tok
book() composite Real browser (WebMCP) 1 26 ms — 19 tok 56 tok
Playwright UI automation Real browser (clicks) 7 steps 613 ms see next slide — DOM re-reads dwarf every other lane

"3-call MCP" isn't a single choice: raw MCP only ever exposes 8 always-on meta-tools, so an agent must either load_tools the 3 business ops first (schema tax, then 3 native calls) or call invoke three times with no load step. Both are measured above — neither is a strawman.

Benchmark Results · The DOM Tax

What an agent has to read before it can click

A function-call agent reads a JSON result. A browser-automation agent needs a fresh accessibility snapshot before every interaction — fill date, fill party size, click Search, click Book, fill name, click Confirm. That snapshot cost compounds across steps the same way the 3-call baseline's context did on slide 4.

book() via WebMCP — one call
Playwright UI — 7-step form flow
book() — agent tokens
91 tok
Playwright — cumulative aria-snapshot
5,710 tok

~63× more tokens to observe the page than to call the function — and that's before counting the 7 separate tool calls (click, fill, click, fill, click…) an automation agent also has to make, each with its own round-trip.

Progressive disclosure keeps the registry cheap to browse. Composite ops keep individual actions cheap to execute. Browser automation pays both costs on every single step.
Live Demo

Watch it, don't just read it

Watch
Playwright, headed
Fills the form, clicks through check-in
→
Compare
seatGuest(), MCP
One call, done before the browser blinks
→
Confirm
Activity Log
Live SSE feed, agent vs. UI calls
node scripts/bench.mjs --flow=checkin --headed --slowmo=250 --demo
Benchmark Results · The Receptionist Session

Two actions, one session — book and seat a guest

A receptionist doesn't do one thing at a time: take a phone booking, then seat the walk-in already waiting. This flow drives both in one continuous browser session — no reload between them. Not "seat the guest just booked": book() and seatGuest() run on two disjoint stores with no shared id (a booking has no table concept; front-desk seating only works on the 3 seeded reservations). Two real, independent actions.

Lane Transport Calls Avg wall-clock Tool-schema tax Agent input Agent output
6-call MCP (load + native) Real MCP HTTP 6 95 ms 132 tok 106 tok 684 tok
6-call MCP (invoke, no load) Real MCP HTTP 6 88 ms 0 tok 98 tok 800 tok
book() + seatGuest() — 2 composites Real MCP HTTP 2 29 ms 57 tok 41 tok 113 tok
book() + seatGuest() — 2 composites Real browser (WebMCP) 2 62 ms — 26 tok 100 tok
Playwright UI automation Real browser (clicks) 10 steps 863 ms 11,495 tok cumulative aria-snapshot — nearly 2× the single-flow booking form

The composite lanes scale linearly with the number of real actions (1 call → 2 calls). The Playwright lane's DOM-read cost scales with the number of UI steps, which is why it's the metric that compounds fastest as scope grows.

Benchmark Results · Seven Calls, One Op

The cumulative-context argument, at real scale

Slide 4 made the cumulative-context case with 3 calls. Here's the same shape at 7 calls across two domains — CRM and front desk — hosting a VIP guest's visit: read their preferences, read their loyalty tier, confirm a table, seat them, validate, award points, log the visit.

1. getGuestPreferences → 2. getLoyaltyStatus → 3. getOccupancy
↓ agent re-reads results #1–3
4. checkInGuest → 5. getCheckinStatus
↓ agent re-reads results #1–5
6. addLoyaltyPoints → 7. logCommunication
7 HTTP calls · context keeps growing
Lane Transport Calls Avg wall-clock Tool-schema tax Agent input Agent output
7-call MCP (load + native) Real MCP HTTP 7 108 ms 151 tok 131 tok 677 tok
7-call MCP (invoke, no load) Real MCP HTTP 7 102 ms 0 tok 124 tok 797 tok
hostVipGuest() composite Real MCP HTTP 1 20 ms 43 tok 33 tok 225 tok
hostVipGuest() composite Real browser (WebMCP) 1 51 ms — 24 tok 173 tok

No Playwright row here — the CRM domain has no screens in this app, so there's nothing to click. That's the honest limit of the demo, not a hidden number: only 10 of 47 business operations have any UI at all today; the rest, including every CRM op used above, are agent-only.

speedbase · Test-Automation Speed

Testing the architecture, not just the agent

A different audience, a different question: how long does it take to test the business logic itself? scripts/speedbase.mjs runs the same scenario two ways — a Playwright script driving the browser, and a script calling runOne() (lib/operations/dispatch.ts) directly, in-process, with zero HTTP and no MCP client at all. Same Zod validation, same RBAC, same handler code — no transport.

Function call — runOne() in-process
Playwright — fresh browser per test
book()
1.15 ms
416 ms
seatGuest()
0.43 ms
488 ms

Bars scaled per-pair (book vs. book, seatGuest vs. seatGuest) — the absolute gap is 2–3 orders of magnitude, which would make the function-call bar invisible on a shared linear scale.

Scenario Function call Playwright Speedup
book() 1.15 ms 416 ms 363×
seatGuest() 0.43 ms 488 ms 1127×
hostVipGuest() 0.27 ms no UI exists — function-call only, same honesty rule as the CRM slide
Not an agent-token argument — this is separate from every other slide in this deck. The same book()/seatGuest() operations your E2E suite drives through a browser can be tested directly, in milliseconds, through the exact validation and RBAC path the browser path goes through.
Operation Model

47 operations across 6 domains + 8 meta-ops

Every op carries a validated schema (Zod), RBAC roles, a module path, and a result envelope.

Domain breakdown
Reservation
8 ops
CRM
10 ops
Front Office
10 ops
Tasks
6 ops
Housekeeping
6 ops
Finance
7 ops
Operation descriptor (key fields)
name: "book"
inputSchema: z.object({…}) // Zod
permission: "write"
roles: ["customer","support","admin"]
module: "reservation.booking"
parallelSafe: false
handler: (input, ctx) => Result<T>
Result envelope
{ success: true, data: T }
{ success: false, error: { code, message } }
Security & Access Control

Layered controls at every call boundary

RBAC hierarchy
admin all ops
support customer + support ops
customer own-data read/write

Role is checked at register time and on every call. Destructive ops carry requiresConfirmation: true — UI shows a dialog; agent must pass confirm: true.

Security controls
ControlImplementation
Browser authSession cookie
MCP HTTP authRFC 8707 Bearer, 8h TTL
Input validationZod schema, every call
Audit logLast 100 calls, SSE-streamed to UI
Capability versioningDJB2 hash over op fingerprints
Next Steps

What we're proposing

  • Adopt the Operation registry pattern as the standard integration layer — single source of truth for both browser and server agent surfaces.
  • Prefer composite ops over raw tool chains — move domain orchestration into the page, not into agent prompts.
  • Use progressive disclosure (explore → load_tools or invoke) to keep context budgets flat regardless of registry size.
  • Enable server-side in-process dispatch (makeDispatch(ctx)) for the maximum latency win — 3× faster than HTTP sub-calls.
Token reduction
85.7%
single composite call vs. 3-call chain
Latency reduction (server)
67%
in-process dispatch, no HTTP hops
Cumulative context
0
single-call approaches never re-read prior results
The browser already knows your business logic. AgentBridge makes it the agent's interface — not a side-effect of the agent's reasoning.