Audience routes
read as.mdA single ggui server exposes two distinct MCP surfaces, not one. Each tool the server registers carries an audience tag that decides which surface the tool appears on. The agent runtime sees one slice of the tool set; design-time clients and operators see the other. The audience tag is the structural mechanism that keeps those slices honest.
This page explains the four audiences, the two surfaces they map onto, and the placement rules for every new handler.
Why audiences exist
Section titled “Why audiences exist”Different callers connect to a ggui server for different reasons:
- An LLM agent in the middle of a chat turn needs
ggui_render,ggui_handshake, blueprint search, and not much else. - The view runtime — the iframe-runtime, relayed through the host’s
tools/call— and first-party backend libraries (e.g.@ggui-ai/agent-server) need callbacks likeggui_runtime_sync_contextandggui_runtime_submit_action. - A design-time client authoring blueprints needs static spec/discovery tools like
ggui_protocol_describe_blueprint_formatonce, then never again. - An operator managing apps, keys, and orgs needs administrative tools that should never appear in an LLM’s
tools/list.
Putting all of those tools on one /mcp surface burns agent context on tools the agent will never call, and exposes operator surfaces to runtimes that shouldn’t see them. The audience tag splits the surface so each caller’s tools/list is exactly the tools that caller cares about.
The four audiences
Section titled “The four audiences”| Audience | Surfaces on | Wire-name prefix | Who calls |
|---|---|---|---|
agent |
/mcp |
ggui_* |
The LLM agent itself during a chat turn (render, handshake, blueprint search) |
runtime |
/mcp |
ggui_runtime_* |
The view runtime — iframe-runtime (via the host’s tools/call relay) + first-party backend libraries |
protocol |
/control |
ggui_protocol_* |
Design-time spec/discovery clients (conformance suites, registry browsers) |
ops |
/control |
ggui_ops_* |
Operator agents — an LLM acting as a console operator, dashboards, CI |
Each tag answers exactly one question: who is calling this tool, and on what time-scale?
Surfaced on /mcp. The LLM agent calls these tools live, inside a chat turn. They typically mutate render state, render contracts, or look up blueprints by intent.
Wire-name prefix: bare ggui_* (e.g. ggui_render, ggui_handshake, ggui_update). The bare prefix is reserved for the canonical agent route — these are the tools an agent calls most often, and they don’t need a route disambiguator.
runtime
Section titled “runtime”Surfaced on /mcp alongside agent tools. Called by the view runtime — the iframe-runtime, relayed through the host’s MCP-Apps tools/call — and by first-party backend libraries (e.g. @ggui-ai/agent-server declaring the per-app tool catalog via ggui_runtime_declare_tool_catalog) — not by the LLM directly. They handle things like syncing renderer state back to the server or submitting user actions.
Wire-name prefix: ggui_runtime_* (e.g. ggui_runtime_sync_context, ggui_runtime_submit_action).
protocol
Section titled “protocol”Surfaced on /control, and answered anonymously — an agent authoring a blueprint has no account yet, so these tools must work with no bearer token. Static design-time tools that describe the protocol itself: example blueprints, format references, schema validators. A client calls these once while authoring against the protocol, not during runtime.
Wire-name prefix: ggui_protocol_* (e.g. ggui_protocol_describe_blueprint_format, ggui_protocol_validate_blueprint, ggui_protocol_get_example_blueprints).
The litmus test: would the result change if the same caller invoked the tool again five minutes later? If no — the tool returns a static format reference or immutable example set — it belongs on /control as a protocol tool. If yes, it’s a runtime lookup and belongs on /mcp.
Surfaced on /control alongside protocol tools, but auth-gated per handler, and confirm-gated when state-changing. Operator-facing tools an LLM operator (or dashboard, or CI script) uses to manage apps, register blueprints, issue connector keys, redeem coupons, list orgs. Never visible to the agent runtime, never invoked from inside a rendered UI.
Wire-name prefix: ggui_ops_* (e.g. ggui_ops_create_app, ggui_ops_list_orgs, ggui_ops_issue_connector_key).
Surfaces table
Section titled “Surfaces table”The four audiences map onto two surfaces:
| Surface | Route | Audiences mounted | Typical caller | Auth posture |
|---|---|---|---|---|
| Data plane | /mcp (+ /apps/<appId> when configured) |
agent ∪ runtime |
LLM agent + view runtime | Bearer token or session auth |
| Control plane | /control |
protocol ∪ ops |
Design-time clients + operator agents | Anonymous for protocol; bearer required per ops handler |
The projection reads each handler’s audience array and includes it on the surface whose audience set intersects the handler’s tags. A handler tagged audience: ['agent'] lands on the data plane. A handler tagged audience: ['agent', 'runtime'] also lands on the data plane (the union doesn’t change membership). A handler tagged audience: ['ops'] lands on the control plane.
When per-app routing is configured, the same agent ∪ runtime surface also mounts at a per-app path (/apps/<appId> on hosted deployments); the audience model is identical — only the tenancy resolution differs.
One route, two auth postures
Section titled “One route, two auth postures”/control is mounted anonymous-capable: a request with no bearer token is admitted with a synthesized anonymous identity, so the protocol half answers. Every ops handler then re-imposes auth for itself and refuses an anonymous caller with an auth error the client can act on.
State-changing ops tools add a second step. The first call returns a confirmation preview and mutates nothing; only a second call carrying confirm: true commits:
// 1st call → preview, nothing happens{ "name": "ggui_ops_delete_app", "arguments": { "appId": "aB3kP9xY" } }// → { "confirmationRequired": true, "confirmationPrompt": "…" }
// 2nd call → commits{ "name": "ggui_ops_delete_app", "arguments": { "appId": "aB3kP9xY", "confirm": true } }The gate exists so an agent cannot silently mint credentials, spend credits, or delete resources on a user’s behalf. It is default-deny: an ops tool not explicitly classified as read-only is treated as state-changing.
Placement decision tree
Section titled “Placement decision tree”When you add a new MCP handler, walk this tree before picking an audience:
- Does the LLM agent invoke this during a chat turn?
Yes →
audience: ['agent']. - Is this a tool declared with
_meta.ui.visibility: ['app']that the rendered view invokes through the host’s MCP-Appstools/callrelay? Yes →audience: ['runtime']. - Is this a static spec or discovery tool a client reads once while authoring against the protocol?
Yes →
audience: ['protocol']. - Is this an administrative operation a human, dashboard, CI, or operator agent performs out-of-band?
Yes →
audience: ['ops'].
The placement test is exclusive — if more than one branch fires, pick the most-frequent caller and tag that audience. Multi-audience tags are rare; see below.
The “is this a runtime lookup?” trap
Section titled “The “is this a runtime lookup?” trap”Tools like ggui_search_blueprints sound like spec/discovery — they discover blueprints. But their results change per-app and per-session: the agent calls them at chat time to decide what to build, not to learn the protocol’s format. They are runtime lookups, tagged agent, surfaced on /mcp.
The litmus test repeats: would the result change if the same caller invoked this tool again five minutes later?
- Yes → runtime lookup →
agent(orruntimeif called from the iframe). - No → static spec/discovery →
protocol.
Wire-name prefix discipline
Section titled “Wire-name prefix discipline”The prefix encodes the audience at the wire-name level so a tool reader can infer what a tool is for without consulting documentation. An LLM scanning tools/list on /control sees ggui_protocol_describe_blueprint_format and knows it is a free design-time read, while ggui_ops_delete_app announces itself as an operator mutation that will want confirmation. Since both halves share one tools/list, the prefix is what tells them apart.
| Prefix | Implied surface | Implied audience |
|---|---|---|
ggui_* (bare) |
/mcp |
agent |
ggui_runtime_* |
/mcp |
runtime |
ggui_protocol_* |
/control |
protocol |
ggui_ops_* |
/control |
ops |
Bare ggui_* is the exception, not the rule. It is reserved for agent runtime essentials — the canonical chat-turn tools that don’t need a route disambiguator. Every other audience requires the explicit prefix.
How a handler declares its audience
Section titled “How a handler declares its audience”The audience field lives on every SharedHandler. It is a ReadonlyArray<'agent' | 'runtime' | 'protocol' | 'ops'> — an array because multi-audience tagging is structurally permitted (see below).
import type { SharedHandler } from '@ggui-ai/mcp-server-handlers';
export function createListOrgsHandler(): SharedHandler<…> { return { name: 'ggui_ops_list_orgs', title: 'List organizations', audience: ['ops'], description: 'Enumerate orgs visible to the calling operator.', inputSchema: { /* … */ }, outputSchema: { /* … */ }, async handler(input, ctx) { /* … */ }, };}That’s the entire contract. Once a handler is registered through the normal channel (the handlers array passed to createGguiServer), the surface projection reads the audience field at compose time and decides which surface the handler appears on. There is no separate route-registration step.
One extra obligation for ops handlers: declare a non-empty outputSchema. The control plane validates this at construction time, because an empty schema silently strips structuredContent at the MCP SDK boundary.
Multi-audience handlers
Section titled “Multi-audience handlers”The audience field is an array, not a scalar, because a handler can legally surface on more than one caller class. In practice this is rare and intentional:
export function createSomeBoundaryToolHandler(): SharedHandler<…> { return { name: 'ggui_some_boundary_tool', audience: ['agent', 'runtime'], // … };}Such a handler appears in both the agent’s tools/list and the iframe runtime’s view. The canonical example is a tool that has to land on the agent’s wire AND accept calls from inside the rendered UI — the runtime essentials that genuinely span both callers.
Multi-audience is a tool that serves multiple caller classes simultaneously. If you find yourself reaching for it, double-check the placement test first — most “multi-audience” tools are actually two tools wearing a trench coat, and splitting them sharpens both surfaces.
['protocol', 'ops'] is the one combination that is always a bug: both project onto /control, so the tool would be registered twice and the server would refuse to start on the duplicate name.
Relation to MCP services
Section titled “Relation to MCP services”The audience model governs the two shared surfaces — /mcp and /control — where handlers from different sources are aggregated under audience filtering. A separate concept, MCP services, lets a server expose isolated, complete MCP servers at their own HTTP paths (e.g. https://your-server/docs, https://your-server/playground/todos).
| Concept | Routing mechanism | Tool isolation | When to use |
|---|---|---|---|
| Audience | Tag filters tool onto shared route | Tools share namespace | Tool belongs alongside ggui-native tools |
| Service | Path mounts an isolated MCP server | Per-path namespace | Tool set is conceptually its own MCP server |
A service handler must not set an audience tag — the path IS the audience. The compose-time validator rejects services with audience-tagged handlers loudly: services bypass audience filtering entirely, so a tag would be silently meaningless.
→ See MCP services for the full service model.
Placement anti-patterns
Section titled “Placement anti-patterns”Audience tagging makes it cheap to add new tools, which means the placement discipline matters more than ever. Some patterns to avoid:
ops-audience tools that mutate non-tenant data. The control plane is operator-bounded but still scoped to the calling operator’s tenant. A tool that lets one operator probe another tenant’s apps is a confused-deputy bug, not a feature.- Cross-tenant probing.
ggui_ops_list_orgsshould enumerate orgs the caller can see, not all orgs. If a tool needs admin-level visibility, gate it on an explicit role check at the handler boundary, not at the route boundary. protocol-audience tools that return runtime data. If a result depends on which session is calling, it is a runtime lookup. Move it toagent.runtime-audience tools that the agent should also call. If an LLM agent needs the data, tag itagent(or['agent', 'runtime']if the iframe legitimately calls it too). Tagging itruntime-only hides it from the agent’stools/list.- Untagged handlers. The default-to-
agentbehavior is a backward-compatibility convenience, not a recommendation. Always declareaudienceexplicitly on new handlers.
See also
Section titled “See also”- MCP services — isolated per-path MCP servers
- MCP protocol — the
/mcpdata plane in detail - Control MCP — the
/controlplane in detail - Architecture overview — three channels and the capability model