MCP services
read as.md A ggui server is not a single MCP server. One Node process composes:
- The audience-filtered routes (
/mcp,/protocol,/ops) — ggui’s native control plane, optionally extended byMcpServerMounts that aggregate external tools onto the same surface. - Zero or more
McpServices — fully isolated MCP servers, each mounted at its own HTTP path with its own tool namespace.
This page covers the second half. For the audience model that drives the shared routes, see Audience routes.
What an McpService is
Section titled “What an McpService is”An McpService is a complete, self-contained MCP server reachable at a single HTTP path you pick (/docs, /playground/todos, /internal/billing, …). A client connecting to that path sees exactly the tools the service declares — no ggui-native tools, no other service’s tools, no audience filtering. The path is the namespace boundary.
Contrast with McpServerMount. A mount is a named bundle of SharedHandlers aggregated onto the audience-filtered routes. Mounted tools compose alongside ggui’s native tools (ggui_render, ggui_handshake, ggui_consume, …) so a single MCP session over /mcp enumerates the union of everything. Services do not compose; they isolate.
┌─────────────────────────────────┐ Agent (LLM-driven) ─────▶│ /mcp │ │ ggui-native tools │ │ + every mount's tools │ audience-filtered │ (audience=agent|runtime) │ └─────────────────────────────────┘ ┌─────────────────────────────────┐ Operator console ──────▶│ /ops │ audience-filtered │ ggui_ops_* │ (audience=ops) └─────────────────────────────────┘ ┌─────────────────────────────────┐ Docs client ──────▶│ /docs │ │ docs_search, docs_read, │ isolated service │ docs_list │ (own namespace) └─────────────────────────────────┘ ┌─────────────────────────────────┐ Playground user ──────▶│ /playground/todos │ │ todos_list, todos_add, │ isolated service │ todos_toggle, todos_delete │ (own namespace) └─────────────────────────────────┘When to use which
Section titled “When to use which”| You want | Reach for |
|---|---|
Tools that should appear alongside ggui_render / ggui_consume in a generative-UI session | McpServerMount |
| Tools the LLM-driven agent should call as part of the same MCP connection that drives ggui | McpServerMount |
A complete, standalone MCP server at its own URL (mcp.example.com/docs, …/billing, …) | McpService |
| First-touch public surface for unauthenticated clients (docs lookup, marketing demos) | McpService (+ anonymous: true) |
| Conceptually distinct tool surfaces that should NOT collide on names with each other | McpService (one per surface) |
Rubric:
- Composes with ggui’s core tools? Mount.
- Replaces ggui’s core tools at its own URL? Service.
If you find yourself disabling ggui-native tools on a mount because they don’t make sense for the caller, you wanted a service. If you find yourself proxying ggui’s tools/list output through a service to bolt on extra tools, you wanted a mount.
The McpService shape
Section titled “The McpService shape”import type { SharedHandler } from "@ggui-ai/mcp-server-handlers";import type { ZodRawShape } from "zod";
interface McpService { /** * Human-readable service identifier. Surfaced in validation * errors and telemetry. No uniqueness constraint across services * — only `path` must be unique. */ readonly name: string;
/** * HTTP path the service mounts at (e.g. `/docs`, * `/playground/todos`). Validated at compose time. */ readonly path: string;
/** * Tool handler bundle. Same `SharedHandler` shape ggui-native * handlers and mount handlers use. */ readonly handlers: ReadonlyArray<SharedHandler<ZodRawShape, ZodRawShape>>;
/** * Auth-optional: a valid bearer still resolves to the real * identity; missing/invalid credentials fall back to a * synthesized anonymous builder. Default `false` — same auth * posture as `/mcp`. */ readonly anonymous?: boolean;}Field-by-field:
name— diagnostic-only. Surfaces invalidateMcpServiceserror messages and composition telemetry so an operator with several services can tell which one is misconfigured. Does NOT appear on the wire; tool names stay whatever each handler declares.path— the HTTP path Express mounts the service handler at. BrandedServicePathafter passingvalidateServicePath. Must be unique across themcpServicesarray.handlers—SharedHandler[], the same canonical shape every ggui-native handler satisfies. The server registers each handler throughbuildMcpServer’s regular path; validation, logging, and output-schema parsing all happen uniformly.anonymous— makes auth optional. Defaultfalse. See Anonymous mode below.
Path reservations
Section titled “Path reservations”Ten paths are reserved at validation time. Declaring a service at any of them throws at server-construction:
| Path | Why reserved |
|---|---|
/ | Root — must not be swallowed by a service router. |
/mcp | The agent-facing audience-filtered MCP route. |
/protocol | The protocol-discovery audience-filtered route. |
/ops | The operator-facing audience-filtered route. |
/ws | The live-channel WebSocket upgrade path. |
/health | Reserved for health probing — the live endpoint is /ggui/health; /health is held back so a service can never shadow a future short alias. |
/.well-known | RFC-reserved discovery prefix (OAuth metadata, security.txt, …). |
/oauth | OAuth dance endpoints (authorize / token / register). |
/_ggui | Internal ggui control surfaces (admin console, pairing, debug routes). |
/ggui | Same — public-facing /ggui/* endpoints. |
A typo in a host config (path: '/mpc' instead of /mcp) can no longer silently shadow OAuth discovery, health, or per-app traffic.
Path validation rules
Section titled “Path validation rules”validateServicePath enforces:
- Regex
^/[a-zA-Z0-9_/-]+$— must start with/and contain only letters, digits,-,_, and/. No whitespace, no., no path traversal. - No trailing slash —
/docs/is rejected; use/docs. Prevents trailing-slash variant collisions where/docsand/docs/would resolve to different routes. - Non-empty after the leading slash — at least one character after
/. - Not a reserved path — see the table above.
Throwing happens at server construction, before any request is served — misconfiguration cannot become a runtime mystery.
Compose-time invariants
Section titled “Compose-time invariants”validateMcpServices(services) walks the whole array and enforces:
- Non-empty
nameon every entry. The name appears in every other error message; empty names defeat the diagnostic. pathpassesvalidateServicePath— all the rules above.- Service paths are unique across the
mcpServicesarray. Two services cannot mount at the same path. - Every handler declares a non-empty
outputSchema. An emptyZodRawShape({}) silently stripsstructuredContentat the MCP SDK boundary — the handler can return{ items: [...] }and the wire answer is{}. Operators hitting this see success-looking responses with missing data and no diagnostic. Rejected at compose time so the failure arrives with the service + tool names attached. - No
audiencetag on service handlers. Services bypass audience filtering entirely — the path IS the audience. An explicitaudience: ['ops']on a service handler is silently meaningless. Rejected loudly. - Tool names are unique within a service. Two handlers declaring the same
nameinside one service collide; one would shadow the other at registration. Rejected. - Cross-service tool-name collisions ARE allowed. Services are isolated namespaces. A client connects to one path and only ever sees that path’s tools, so
docs_searchon/docsanddocs_searchon/internal/docscoexist without ambiguity. This is by design — collapsing into a global tool namespace would defeat the isolation that makes services worth having.
Anonymous mode
Section titled “Anonymous mode”anonymous: true makes auth optional, not skipped. The server always attempts to resolve a presented bearer — a valid credential resolves to the real identity (letting one service mix public reads with authenticated capabilities); only a missing or invalid credential makes the binding layer fall back to the synthesized:
{ identity: { kind: 'builder' }, source: 'anonymous',}Two things to note about the shape:
identity.kinddoes NOT widen. TheIdentityunion still has its three variants ('builder'/'user'/'app'). Anonymous traffic collapses to'builder'so handlers that pattern-match onkindkeep working without an extra arm. Adding a'anonymous'kind would force every consumer in the codebase to add a defensive case — the synthesized'builder'is the lighter touch.- The signal lives on
source.AuthResult.sourcecarries a dedicated'anonymous'variant. Handlers that need to distinguish “this request was authenticated” from “this request was let through anonymously” readsourcedirectly — pattern-matching onidentity.kindalone cannot answer the question.
When to use it
Section titled “When to use it”- Read-only public surfaces. Documentation lookup, public catalog browse, marketing demos.
- First-touch onboarding flows where requiring a bearer token would block the use case (a brand-new visitor to
mcp.example.com/docshas nothing to present). - Server-to-server probes that verify protocol shape without claiming an identity.
When NOT to use it
Section titled “When NOT to use it”- Any write path. An anonymous caller has no accountability surface; you cannot audit who created the row.
- Anything tenant-scoped.
ctx.appIdfor an anonymous request collapses to the single builder. Reads will return data the caller has no business seeing, or writes will land in a tenant they don’t own. - Anything sensitive enough that you’d want rate limits per-caller. Anonymous mode pairs with per-IP / per-session rate limits (the
RateLimiterseam composes the same way for authenticated and anonymous paths), but it cannot give you per-user attribution.
Wiring it up
Section titled “Wiring it up”Pass the service array to createGguiServer:
import { createGguiServer } from "@ggui-ai/mcp-server";// monorepo-internal first-party services (not published):import { createDocsHandlers, loadDocsCorpus } from "@ggui-private/mcp-docs";import { createPlaygroundTodosHandlers, createInMemoryTodoStore,} from "@ggui-private/mcp-playground-todos";
const corpus = await loadDocsCorpus("./docs");const todoStore = createInMemoryTodoStore();
const server = await createGguiServer({ // ... ggui-native options (renderChannel, blueprintStore, …)
mcpServices: [ { name: "docs", path: "/docs", handlers: createDocsHandlers({ corpus }), anonymous: true, // read-only doc lookup; no token required }, { name: "playground-todos", path: "/playground/todos", handlers: createPlaygroundTodosHandlers({ store: todoStore }), // no `anonymous: true` — handlers throw when ctx.userId is missing }, ],});
await server.listen(6781);After listen():
POST /mcp— ggui-native + every mount’s tools (audience-filtered).POST /docs—docs_search,docs_read,docs_list. No auth required.POST /playground/todos—todos_list,todos_add,todos_toggle,todos_delete. Auth-gated (handlers reject anonymous callers).
The audience-filtered routes and the service routes coexist on the same Node process, the same Express app, the same WebSocket binding. There is no second server to run.
Examples in the wild
Section titled “Examples in the wild”Three first-party services are built on this primitive for the hosted deployment (coming soon):
/docs—@ggui-private/mcp-docs. Three read-only tools (docs_search,docs_read,docs_list) over the ggui documentation corpus. Anonymous-mode; the canonical “public surface” example. See MCP Docs Service./playground/todos—@ggui-private/mcp-playground-todos. Four tools (todos_list,todos_add,todos_toggle,todos_delete) for the landing-page playground. Auth-gated per-user state (handlers reject anonymous callers). See Playground · todos./playground/mdh—@ggui-private/mcp-playground-mdh. Million-Dollar Homepage playground service. See Playground · MDH.
The hosted deployment’s unified /dev endpoint (coming soon) is the fullest example: one anonymous service co-hosting public docs + protocol tools with ops tools that re-impose auth per-tool — possible precisely because anonymous is auth-optional, so a presented connector key still resolves to the real identity.
For the operator-facing audience-filtered route alongside these services, see Ops MCP. For running an analogous service stack under your own hostname, see ggui serve.
Where to next
Section titled “Where to next”- Audience routes — the audience model behind
/mcp//protocol//ops - Architecture overview — the three-channel topology services live inside
- Event System — live-channel traffic is shared by every service on the same host
- MCP Docs Service — the canonical anonymous-mode example
- Ops MCP — the operator-facing audience-filtered route
- Playground · todos and Playground · MDH — authenticated-service examples
ggui serve— self-host an analogous service stack