Skip to content

Audience routes

read as .md

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

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 like ggui_runtime_sync_context and ggui_runtime_submit_action.
  • A design-time client authoring blueprints needs static spec/discovery tools like ggui_protocol_describe_blueprint_format once, 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.

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.

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

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

The four audiences map onto two surfaces:

Surface Route Audiences mounted Typical caller Auth posture
Data plane /mcp (+ /apps/<appId> when configured) agentruntime LLM agent + view runtime Bearer token or session auth
Control plane /control protocolops 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 agentruntime surface also mounts at a per-app path (/apps/<appId> on hosted deployments); the audience model is identical — only the tenancy resolution differs.

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

When you add a new MCP handler, walk this tree before picking an audience:

  1. Does the LLM agent invoke this during a chat turn? Yes → audience: ['agent'].
  2. Is this a tool declared with _meta.ui.visibility: ['app'] that the rendered view invokes through the host’s MCP-Apps tools/call relay? Yes → audience: ['runtime'].
  3. Is this a static spec or discovery tool a client reads once while authoring against the protocol? Yes → audience: ['protocol'].
  4. 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.

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 (or runtime if called from the iframe).
  • No → static spec/discovery → protocol.

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.

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.

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.

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.

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_orgs should 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 to agent.
  • runtime-audience tools that the agent should also call. If an LLM agent needs the data, tag it agent (or ['agent', 'runtime'] if the iframe legitimately calls it too). Tagging it runtime-only hides it from the agent’s tools/list.
  • Untagged handlers. The default-to-agent behavior is a backward-compatibility convenience, not a recommendation. Always declare audience explicitly on new handlers.