Skip to content
Public preview · the console is open

Control MCP plane

read as .md

The /control plane is one of GGUI’s two MCP surfaces. It carries everything called around a session rather than during one:

  • Design-time protocol tools — format references, example blueprints, validators. Answered anonymously, because an agent authoring a blueprint has no account yet. See MCP protocol for those tools.
  • Operator-class ops tools — the same actions the console UI exposes to a human (create an app, rename it, mint a connector key, redeem a coupon, …), exposed as MCP tools so an LLM acting as an operator agent can perform them on the user’s behalf. Auth required, and confirm-gated when state-changing.

This page is the wire reference for the 16 ops-audience handlers across four domains that ship in the open @ggui-ai/mcp-server-handlers package, plus the 24 hosted-only ops tools that live directly in the cloud pod’s own tool registry and answer only on mcp.ggui.ai (see Hosted-only ops below). The agent-loop surface (handshake / render / consume / …) lives on the /mcp data plane and is documented separately — the two surfaces are strictly disjoint.

The ops half is the destination for an operator agent — an LLM acting as the console’s hands. Typical caller: a Claude conversation that the user opens from console.ggui.ai and gives natural-language instructions like “create a new app called Inbox Triage and lock a connector key to it.” The agent calls ggui_ops_create_app followed by ggui_ops_issue_connector_key, never touching the AppSync GraphQL layer directly.

Every tool here mirrors a UI action the console exposes. The open apps handlers in @ggui-ai/mcp-server-handlers are pure over typed seams (AppsSource, UserDefaultAppSource) — the cloud pod binds AppSync-backed adapters; OSS deployments leave the seams unwired and the surface stays narrow. Orgs, connector keys and coupons are hosted-only tools (see Hosted-only ops).

POST http://127.0.0.1:6781/control

The route is always mounted — the control plane is part of what a GGUI server is, not an opt-in. A default self-hosted boot serves the 6 design-time protocol tools there. The ggui_ops_* families register when the operator seams are wired into createGguiServer({opsApps}) (the ops-blueprint family additionally hangs off the opsBlueprint dep bundle). Hosted GGUI serves the same route at https://mcp.ggui.ai/control, with every domain bound — the full hosted surface is 46 tools: 6 protocol + 40 ops (the 16 handler-package tools below, including provider-keys and credits, plus the 24 hosted-only tools in Hosted-only ops). See “OSS vs hosted” below for what a self-hosted deployment gets instead.

/control is anonymous-capable: a request with no bearer is admitted with a synthesized anonymous identity so the design-time protocol tools answer. Every ggui_ops_* handler then re-imposes auth for itself — an anonymous caller gets an auth error naming the tool, not a silent no-op and not a transport-level 401.

Authenticated callers present a bearer exactly as on /mcp, via the same upstream AuthAdapter:

Authorization: Bearer dev
Content-Type: application/json

Self-hosted: with ggui serve --dev-allow-all, any bearer (or none) authenticates as the builder identity; default serve requires a pairing-minted bearer. Hosted GGUI runs the OAuth 2.0 Dynamic Client Registration ceremony (see OAuth on mcp.ggui.ai). The bearer presented on /control is the same bearer presented on /mcp — there is no separate “ops token”.

Externally-federated end-user identities (source: 'oidc') are rejected at the route with 403, before MCP dispatch. The control plane is for the account holder and their operator agents, not for an app’s end users.

An ops tool that changes state answers the first call with a 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 the user’s behalf: it forces the agent to show the human what will happen and get explicit approval. Reads and lists are single-call.

Classification is default-deny — an ops tool not on the read-only list is treated as state-changing. Deployments that register their own read-only ops tools declare them via createGguiServer({control: {singleCallOps: [...]}}).

The single-call ops tools (SINGLE_CALL_OPS, 15 entries) are: ggui_ops_list_apps, ggui_ops_list_orgs, ggui_ops_list_connector_keys, ggui_ops_list_blueprints, ggui_ops_list_my_apps, ggui_ops_list_my_blueprints, ggui_ops_list_provider_keys, ggui_ops_list_credit_transactions, ggui_ops_list_recent_renders, ggui_ops_get_credit_balance, ggui_ops_get_org_balance, ggui_ops_get_my_blueprint_source, ggui_ops_get_render_source, ggui_ops_setup_byok, and ggui_ops_save_library_blueprint.

Fourteen of those are genuine reads. ggui_ops_save_library_blueprint is the one deliberate exception — a MUTATION added to the single-call set by owner ruling (GGUI#525, 2026-08-16). It earns the exemption on three properties: it’s idempotent (same name ⇒ one row, a repeat is a no-op), it writes only to the caller’s own library (no cross-tenant or account-wide effect), and its primary caller is the console helper agent auto-saving after a render, where the confirm round-trip was an agent self-confirm with no human in the loop — it bought no safety and cost an extra call on every helper render. A future single-call entry that lacks any of those three properties does not inherit this ruling on its own.

Every handler resolves the calling identity through a single helper:

function resolveOwnerSub(toolName: string, ctx: HandlerContext): string {
const sub = ctx.userId ?? ctx.appId;
if (!sub) throw new Error(`${toolName}: missing caller identity`);
return sub;
}
  • Hosted (multi-tenant): ctx.userId is the caller’s Cognito sub, populated by the upstream auth adapter.
  • OSS (single-tenant): ctx.userId is undefined; ctx.appId (resolved by the auth adapter via defaultAppIdFromIdentity — typically workspaceId ?? userId for kind=user identities) serves as the identity.
  • Neither set: the handler throws — that means an unauthenticated caller slipped past auth, surfaced as a 5xx rather than masked as an empty list.

Every read and write is scoped by the resolved identity at the seam layer (AppsSource.list(ownerSub) returns only the caller’s rows; AppsSource.get returns null for foreign rows). Cross-tenant probes never reveal whether a given id exists on another user’s account — the handlers translate “row exists but you don’t own it” to the same shape as “no such row”:

OperationCross-tenant probe
ggui_ops_list_appsReturns only caller’s rows; foreign rows invisible.
ggui_ops_update_app / ggui_ops_set_app_theme / ggui_ops_set_default_appThrows app_not_found — same as a genuinely missing id.
ggui_ops_delete_appReturns {deleted: true} without touching the foreign row. Uniform with “row didn’t exist.”
ggui_ops_rename_org / ggui_ops_remove_org_member / ggui_ops_get_org_balanceThrows org_not_found for orgs the caller isn’t a member of — same as a missing id.
ggui_ops_invite_to_org / ggui_ops_revoke_inviteThrows org_invite_access_denied for orgs the caller doesn’t administer.
ggui_ops_revoke_connector_keyThrows connector_key_access_denied for keys owned by other users.
ggui_ops_redeem_couponThrows coupon_access_denied for targetOrgId orgs the caller isn’t a member of.
ggui_ops_set_provider_key / ggui_ops_remove_provider_key with appIdThrows app_not_found for apps the caller doesn’t own — same as a missing id.

Four domains, 16 handlers total. Each domain is optional: apps hangs off CreateGguiServerOptions.opsApps; ops-blueprint hangs off the opsBlueprint dep bundle on defaultHandlers; provider-keys and credits are bound by the hosted cloud pod. Leaving a domain unwired removes its tools from tools/list at registration time.

Operator actions on GguiApp rows — the rows the universal MCP route resolves per-request to scope sessions. Each row carries appId (server-minted base62), displayName, optional systemPrompt override, optional rateLimitPerMinute, createdAt, updatedAt. Bound on the cloud pod via DDB adapters over the GguiApp table.

Enumerate every GguiApp row owned by the calling user. Returns metadata only — same data the console’s Apps section renders. Use to discover ids before calling the mutating tools.

Inputs: none.

Returns: { apps: AppRecord[] }

interface AppRecord {
readonly appId: string;
readonly displayName: string;
readonly systemPrompt?: string;
readonly createdAt: string;
readonly updatedAt: string;
}

Tenancy: scope is ownerSub from the bearer token. Cross-user listings are impossible by construction.

Provision a fresh GguiApp owned by the calling user. Wraps the cloud’s provisionGguiApp mutation — opaque base62 appId is minted server-side; argument-supplied appId is NEVER honored (tenant-takeover vector).

FieldTypeRequiredDescription
displayNamestring (1–120 chars)NoHuman-friendly label. Defaults to 'My GGUI app' when absent — matches the auto-create path in useGguiUser.

Returns: the full AppRecord shape as above.

Follow-up: call ggui_ops_set_default_app({appId}) to promote the new app to the user’s default.

Partially update an app the caller owns — displayName, systemPrompt, and rateLimitPerMinute in one call. At least one field is required; an empty update is rejected before any store work.

FieldTypeRequiredDescription
appIdstringYesTarget GguiApp.appId. Discover via ggui_ops_list_apps.
displayNamestring (1–120 chars)NoNew display name.
systemPromptstring (≤10,000 chars)NoReplacement text. Pass "" to clear the override — renders then fall back to the universal default.
rateLimitPerMinuteinteger (≥ 0)NoPer-API-key renders/minute. Pass 0 to clear the limit (unlimited). The stored column keeps ONE “unlimited” representation, server-side.

Returns: the updated AppRecord (with systemPrompt / rateLimitPerMinute omitted when cleared).

Errors:

CodeWhen
app_not_foundThe id doesn’t exist OR exists under another tenant (uniform shape — no existence leak).

Hard-delete an app owned by the calling user. Idempotent — a second delete of the same id resolves cleanly.

FieldTypeRequiredDescription
appIdstringYesTarget GguiApp.appId.

Returns: { deleted: true }

Tenancy: cross-tenant probes return the success shape without touching the foreign row. Uniform with “row didn’t exist.”

Errors:

CodeWhen
default_app_delete_blockedThe target is the caller’s default app. The universal route resolves defaultAppId on every request, so it must point at a live app — call ggui_ops_set_default_app first, then delete.

Set the calling user’s GguiUser.defaultAppId — the universal MCP route resolves this on every request to scope the session. The handler first verifies the caller owns the target appId before writing User.defaultAppId.

FieldTypeRequiredDescription
appIdstringYesTarget app — must be owned by the caller.

Returns: { defaultAppId: string }

Errors:

CodeWhen
app_not_foundTarget appId doesn’t exist OR is owned by another tenant.

Replace the theme on an app the caller owns. The payload is validated with the protocol’s canonical appThemeSchema — the same validator every GguiApp.theme write surface runs — so no surface can persist a theme another surface would reject.

FieldTypeRequiredDescription
appIdstringYesTarget GguiApp.appId.
themeAppThemeYes{mode: 'light'|'dark', cssVariables, name?}. Keys must match --ggui-*; values ≤256 chars with breakout characters rejected; ≤200 variables total.

Returns: { appId, theme, updatedAt } — the persisted theme echoed back.

Errors:

CodeWhen
app_not_foundTarget appId doesn’t exist OR is owned by another tenant.

Operator blueprint authorship — generate, register, list, update, delete cached blueprints for the calling app. Unlike the apps domain, this family registers on the OSS server via the opsBlueprint dep bundle on defaultHandlers (registry + blueprint store + search; generate additionally requires the resolveLlm + blueprints deps the render generation path reads).

Author a blueprint via the bound generator (LLM generation + validation).

FieldTypeRequiredDescription
appIdstringNoTarget app. Defaults to the caller’s bound app; a caller not bound to an app must pass it.
contractobjectYesThe DataContract to generate against.
generatorstringNoGenerator slug. Unknown slug fails with generator_not_found.
personastringNoVariance axis — normalized lowercase + trimmed.
aestheticstringNoVariance axis.
contextobjectNoVariance axis: a small structured signal.
seedPromptstringNoVariance axis.
intentstringNoWhat to generate, in plain words: the prompt the generator composes from, at most 2,000 characters. Not part of the cache identity, so calls that differ only in intent share a variant key. When omitted, the generator falls back to seedPrompt.
setAsOperatorDefaultbooleanNoMark the result as the operator default for its contract (orders listings; does not decide what serves).

Returns: { blueprintId, codeHash?, source } — source is the stamped provenance { kind: 'llm', generator, model } from the engine’s own metadata stamp.

Errors: generator_not_found; missing_credentials (BYOK fix: ggui_ops_set_provider_key); generation failure.

Register pre-built component code verbatim — no LLM, no validator. Operator entry point for fixture seeding and export/reimport round-trips.

FieldTypeRequiredDescription
contractobjectYesThe DataContract the code implements.
componentCodestringYesThe component code to register verbatim (min 1 char).

Plus the same optional generator / persona / aesthetic / context / seedPrompt / setAsOperatorDefault fields as ggui_ops_generate_blueprint, and four optional non-identity facts the semantic reuse pre-filter reads off the cache row (none is part of the cache identity; an omitted fact reads not evaluated, never a miss):

FieldTypeRequiredDescription
intentstringNoWhat the interface is for, in the request’s own words — the sentence the semantic reuse judge compares. Falls back to seedPrompt, then persona.
judgedCanvasesstring[]NoA non-empty list of the canvas classes the interface was judged on (e.g. xs-chat-card, md). A request on a canvas outside the list is not offered it for semantic reuse.
aestheticPreset{ id: string, version?: string }NoThe aesthetic preset (and resolved version, when known) it was generated under. A request under another preset, or a pinned version that differs, is not offered it.
directionDigeststring (sha256 hex)NoDigest of the direction text it was generated under, normalised (trimmed, whitespace collapsed, lowercased). A request whose direction digests differently is not offered it.
directionScope'app' | 'request'NoWhich direction directionDigest hashes: app — the direction the app’s profile carries; request — a direction given with the request itself. Only beside directionDigest (refused without it).
clonedFromstringNoWhen these bytes are a copy of a row this app already holds, that row’s blueprintId. The new row records it as provenance; nothing that serves reads it. Refused (cloned_from_refused, nothing persisted) when empty, the new row’s own id, not held, or another app’s row, with the same answer for a missing row as for another app’s.

Returns: { blueprintId, codeHash, source } — source is always { kind: 'user' }; hand-supplied bytes carry no engine claim, so none is recorded.

FieldTypeRequiredDescription
contractHashstringNoFilter by canonical contract hash.
generatorstringNoFilter by generator slug.
personastringNoDispatches semantic search.
intentKeywordsstring[]NoDispatches semantic search. Filters are AND-composed.

Returns: { blueprints: Blueprint[] }

A row minted by a generation carries build: the minting engine’s version, design mode and template digests. Registered, installed and older rows carry none, and a reader treats that as an unknown build.

FieldTypeRequiredDescription
blueprintIdstringYesTarget blueprint.
isOperatorDefaultliteral trueNoMark as the operator default (orders listings; does not decide what serves).
varianceobjectNoPartial-merge of variance axes; {persona: ""} clears the field.

Returns: { blueprintId, updatedAt }

FieldTypeRequiredDescription
blueprintIdstringYesTarget blueprint.

Returns: { deleted: true } — idempotent.


Provider keys (provider-keys, 3 handlers) — BYOK

Section titled “Provider keys (provider-keys, 3 handlers) — BYOK”

Operator actions on the caller’s BYOK LLM provider keys. Provider enum: 'anthropic' | 'openai' | 'google' | 'openrouter'. The handler factories ship in @ggui-ai/mcp-server-handlers; they are bound today by the hosted cloud pod, which validates keys against the provider and encrypts at rest.

FieldTypeRequiredDescription
providerenumYesOne of anthropic / openai / google / openrouter.
plaintextKeystringYesThe provider API key (min 1 char). Re-set replaces (rotation).
labelstringNoHuman label.
appIdstringNoScope the key to ONE app the caller owns instead of the whole account. App key beats account key for that app’s renders. Requires app-scoped key storage; deployments without it reject with app_scoped_keys_unavailable. Foreign/missing apps answer app_not_found.

Returns: { provider, label?, lastFour, createdAt?, lastUsedAt? } — never echoes the key.

Inputs: none.

Returns: { keys: [{ provider, label?, lastFour, createdAt?, lastUsedAt? }] }

FieldTypeRequiredDescription
providerenumYesProvider to remove.
appIdstringNoRemove the key scoped to one app the caller owns instead of the account-level key. Same availability + tenancy rules as on ggui_ops_set_provider_key.

Returns: { deleted, provider }


Read-only views over the caller’s prepaid credit wallet. Bound by the hosted cloud pod; self-hosted deployments have no credit plane.

Inputs: none.

Returns: { balanceCents, lifetimeGrantedCents, lifetimeSpentCents, updatedAt }

FieldTypeRequiredDescription
limitnumberNo1–100, default 20.
cursorstringNoPagination cursor.

Returns: { transactions: [{ transactionId, kind, deltaCents, balanceAfterCents, reason, createdAt, relatedSessionId? }], nextCursor? } — kind is an open vocabulary: the known kinds are free_credit, render_charge, topup and refund, and a deployment may add others. Accept a kind you do not know, keep it as named, and never read it as a render charge.


Twenty-four more ops-audience tools live directly in cloud/ggui-protocol-pod/src/tools/ — the cloud pod’s own tool registry, not the open @ggui-ai/mcp-server-handlers package. They answer only on https://mcp.ggui.ai/control; a self-hosted ggui serve never registers them, because there is no seam to wire them through — they read and write cloud-pod-native tables directly. Thirteen back the chat-native experience (a Claude Desktop or claude.ai conversation acting as the user’s own agent) rather than a specific console UI action, so they’re absent from the Console parity table below. The other eleven — orgs, connector keys and coupons — mirror console actions and are listed there; they shipped in the open handler package before v0.25.0.

Universal connector key only (a per-app key is scoped to one app and structurally can’t enumerate or switch siblings).

Enumerate the caller’s apps, most-recently-touched first.

Inputs: limit (integer, 1–500, optional, default 100).

Returns: { apps: [{ appId, displayName, isDefault, createdAt, updatedAt }] } — isDefault marks the app the universal mcp.ggui.ai endpoint currently resolves to.

Switch the caller’s default app. Writes User.defaultAppId; takes effect on the next connection — the calling Claude session keeps its already-bound appId for the life of the connection.

FieldTypeRequiredDescription
appIdstringYes8-char base62 target app id. Must be owned by the caller. Discover via ggui_ops_list_my_apps.

Returns: { appId, message } — message reiterates the reconnect-to-switch caveat.

The caller’s own saved-blueprint library (Claude composes something useful in a conversation, saves it, renders it again later) — a different GguiUserBlueprint world from the ops-blueprint family’s app-scoped variant curation above, scoped per-app via ctx.appId rather than per-owner.

FieldTypeRequiredDescription
limitintegerNo1–500, default 100.

Returns: { blueprints: [{ name, description?, tags?, usageCount, createdAt, updatedAt, lastUsedAt?, systemSourceId?, systemVersion? }] } — definition bodies are omitted to keep the list lean; fetch one with ggui_ops_get_my_blueprint_source.

FieldTypeRequiredDescription
namestringYesBlueprint slug. Discover via ggui_ops_list_my_blueprints.

Returns: { name, definition: { source, contract?, fixtureProps? }, description?, tags?, updatedAt }.

Errors: blueprint_not_found (missing or foreign name — uniform, no existence leak); blueprint_definition_invalid (a stored row lost its source text).

Upsert a blueprint into the caller’s library, keyed on (appId, name) — same name replaces the existing row. Re-runs the canonical D16 validator (compile → self-check → runtime probe) server-side as defense-in-depth. Single-call by owner ruling; see the confirmation-gate section above for why.

FieldTypeRequiredDescription
namestringYesSlug 2–64 chars, kebab-case [a-z0-9-], no underscores, no single-char names.
blueprintobjectYes{source, contract?, fixtureProps?} — pass the object as-is, e.g. straight from ggui_ops_get_render_source’s blueprint field.
descriptionstringNoSurfaces in the console’s miniapp library.
tagsstring[]NoOrganization tags.

Returns: { name, createdAt, updatedAt }.

Errors: the D16 validator’s tiered failure envelope (same shape as ggui_protocol_validate_blueprint) when validation fails.

Rename a library blueprint and/or replace its description / tags in one call — at least one of newName, description, tags is required. Rename applies first and refuses to overwrite an existing sibling name.

FieldTypeRequiredDescription
namestringYesCurrent slug. Discover via ggui_ops_list_my_blueprints.
newNamestringNoNew slug, same rule as name. Same-name is a no-op.
descriptionstringNoReplacement (≤4000 chars); empty string clears it.
tagsstring[]NoReplacement list, trimmed/lowercased/deduped, ≤32 tags of ≤64 chars; empty array clears.

Returns: { name, updatedAt } — name is the post-rename value.

Errors: blueprint_not_found; blueprint_name_taken (rename collision, names the taken slug).

Idempotent delete by name.

FieldTypeRequiredDescription
namestringYesBlueprint slug. Discover via ggui_ops_list_my_blueprints.

Returns: { name, deleted } — deleted: false means no such blueprint existed; still a success.

Universal connector key only — same posture as the app meta-tools, since both read across every app the caller owns.

FieldTypeRequiredDescription
limitintegerNo1–100, default 20.
appIdstringNoNarrow to one app. Omit to list across every app the caller owns.
erroredOnlybooleanNoReturn only renders whose generation failed. Covers at most the 500 most-recent rows per call.

Returns: { renders: [{ sessionId, appId, createdAt, status, errorCode? }] } — status is active or expired; an empty list is a valid “nothing yet” answer, not an error.

Caller-gated source read for a render session — the link between a render and ggui_ops_save_library_blueprint. Only component-variant renders have source; MCP-Apps and system-card renders (and a render that hasn’t finished its first commit) answer a typed no-source error instead of an empty string.

FieldTypeRequiredDescription
sessionIdstringYesFrom ggui_ops_list_recent_renders or a ggui_render result.

Returns: { sessionId, blueprint: { source, contract?, fixtureProps? } } — feed blueprint straight into ggui_ops_save_library_blueprint’s blueprint argument with no reshaping.

Errors: render_not_found (missing or foreign session — uniform, no existence leak).

Entry point for the BYOK provider-key MCP App card — the chat-native counterpart to ggui_ops_list_provider_keys above, shaped for inline card rendering rather than flat consumption. Declares _meta.ui.resourceUri so MCP-Apps-aware hosts pre-fetch the bundle and render the card on first call.

Inputs: none.

Returns: { kind: 'provider-key', configured: [{ provider, prefix }] } — prefix is a display string like sk-ant…abcd (canonical provider prefix + the stored last-four), never the plaintext key.

Platform-operator only — gated by requireScopeOpsOperator against the REGISTRY_OPS_OPERATOR_SUBS allowlist (checked against ctx.userId), on top of the normal auth + confirm gates. Scope owners publish through the ordinary publish flow and never call these; they exist for the registry operator to reassign or verify scope ownership (e.g. reclaiming a squatted @scope for its proven brand owner) and to record which signer identities may publish under a scope. All three mutate global registry state, are deliberately excluded from the single-call set, and emit a structured audit line (scope_transfer / scope_verification_set / scope_san_allowlist_set / scope_ops_denied) on every call.

Reassign an existing scope’s ownership row, or seed one for a reserved scope that was never first-publish-claimable. The written row always lands verification: 'unverified' — a transfer moves ownership, it does not assert proof.

FieldTypeRequiredDescription
scopestringYesTarget scope, leading @ included.
newOwnerSubjectstringYesAuth subject that will own publishes under the scope.
reasonstringYesWhy ownership moves — recorded verbatim in the audit log.

Returns: { scope, ownerSubject, claimedAt, verification, previousOwnerSubject? }.

Record or withdraw the verified-domain proof on an existing ownership row. verified requires verifiedDomain; unverified forbids it (withdrawing clears both proof fields).

FieldTypeRequiredDescription
scopestringYesMust already have an ownership row — seed with ggui_ops_scope_transfer first.
verification'unverified' | 'verified'YesNew label.
verifiedDomainstringConditionalRequired with verified; forbidden with unverified.

Returns: { scope, ownerSubject, claimedAt, verification, verifiedDomain?, verifiedAt? }.

Set or clear the exact sigstore certificate identities (emails or CI workflow URIs) permitted to publish under a scope — pass exactly one of sanAllowlist (non-empty, to set) or clear: true (to remove); an empty array is refused rather than guessed at. Scopes without a list fall back to the deployment’s default identity rule (verified-email match, where wired).

FieldTypeRequiredDescription
scopestringYesMust already have an ownership row — seed with ggui_ops_scope_transfer first.
sanAllowliststring[]NoReplacement allowlist, exact literal strings only (no regex/glob). Mutually exclusive with clear.
clearbooleanNoRemove the allowlist. Mutually exclusive with sanAllowlist.

Returns: { scope, ownerSubject, claimedAt, verification, verifiedDomain?, verifiedAt?, sanAllowlist? } — sanAllowlist absent after a clear.

Operator actions on GguiOrg + GguiOrgMember + GguiOrgInvite (+ the org credit-balance) rows. Orgs are the unit of multi-user collaboration; each row carries orgId (ULID), name, ownerUserId, plus per-membership role on the join rows. Bound on the cloud pod via DDB adapters over the same tables the console’s org mutations write.

Enumerate every org the calling user belongs to — owner + admin + member memberships in a single list, each row carrying the caller’s role.

Inputs: none.

Returns: { orgs: OrgMembershipRecord[] }

interface OrgMembershipRecord {
readonly orgId: string;
readonly name: string;
readonly ownerUserId: string;
readonly role: "owner" | "admin" | "member";
readonly joinedAt: string;
}

Mirrors the AppSync fetchMyOrgs custom resolver. Use to discover orgId before calling the invite tools.

Provision a fresh GguiOrg owned by the calling user. Wraps the cloud’s provisionGguiOrg mutation — ULID orgId minted server-side; an owner membership row and a zero-balance credit row are inserted atomically via TransactWrite.

FieldTypeRequiredDescription
namestring (1–120 chars)YesHuman-friendly display name. Required (no default — orgs are intentional creations).

Returns:

interface CreateOrgOutput {
readonly orgId: string;
readonly name: string;
readonly ownerUserId: string;
readonly createdAt: string;
readonly updatedAt: string;
}

Rename an org the caller owns or administers. Authorization is owner-OR-admin — an admin curates the org’s presentation just like the owner; member-role callers are rejected.

FieldTypeRequiredDescription
orgIdstringYesTarget org — caller must own or administer it. Discover via ggui_ops_list_orgs.
namestring (1–120 chars)YesNew display name. Trimmed; cap matches org provisioning.

Returns: { orgId, name, updatedAt }

Errors:

CodeWhen
org_not_foundThe id doesn’t exist OR the caller isn’t a member (uniform — no existence leak).
org_access_deniedThe caller is a member but not owner/admin.

Remove a member from an org, or leave an org by removing yourself. Permission matrix (caller’s role × target’s role):

target=ownertarget=admintarget=member
caller=ownernoyesyes
caller=adminnoonly-selfyes
caller=membernonoonly-self

The org owner can never be removed — ownership transfer is a separate flow. Matrix violations surface the specific rule that was hit; only non-member callers get the uniform not-found.

FieldTypeRequiredDescription
orgIdstringYesTarget org — the caller must be a member.
memberUserIdstringYesMember to remove. Pass your own user id to leave the org.

Returns: { orgId, memberUserId, alreadyAbsent } — alreadyAbsent: true means no membership row existed (idempotent; a parallel removal is not an error).

Errors:

CodeWhen
org_not_foundThe id doesn’t exist OR the caller isn’t a member (uniform — no existence leak).
org_member_removal_deniedThe role matrix denies the removal — the message names the specific rule.

Read the shared prepaid credit balance of an org the caller belongs to. Any membership role — every member can see how much shared credit there is to spend. Single-call (no confirmation gate).

FieldTypeRequiredDescription
orgIdstringYesTarget org — the caller must be a member.

Returns: { orgId, balanceCents, lifetimeGrantedCents, lifetimeSpentCents, updatedAt } — an org with no spend history reads as zeros (the balance value is the contract, not the row’s existence).

Errors:

CodeWhen
org_not_foundThe id doesn’t exist OR the caller isn’t a member (uniform — no existence leak).

Issue an admin- or member-role invite to a GguiOrg the caller can administer. The invite link in the recipient’s email points at the console: console.ggui.ai/invites/<inviteId>.

FieldTypeRequiredDescription
orgIdstringYesTarget org — caller must own or administer it. Discover via ggui_ops_list_orgs.
emailstring (RFC 5322)YesRecipient email — the invite link is sent here.
role'admin' | 'member'YesRole the recipient holds once they accept. Owner can’t be granted via invite — ownership transfer is a separate flow.

Returns:

interface InviteToOrgOutput {
readonly inviteId: string;
readonly orgId: string;
readonly email: string;
readonly role: "admin" | "member";
readonly inviterUserId: string;
readonly status: "pending" | "accepted" | "revoked" | "expired";
readonly expiresAt: string;
readonly createdAt: string;
readonly reused: boolean;
}

Anti-double-issue: an existing pending invite for the same (orgId, email) is reused — no new row, no second email. reused: true flags the dedup.

Errors:

CodeWhen
org_invite_access_deniedCaller is not owner/admin of the target org.

Invalidate a pending org invite — the bearer-secret link in the recipient’s email stops working immediately.

FieldTypeRequiredDescription
inviteIdstringYesTarget invite — must belong to an org the caller can administer.

Returns:

interface RevokeInviteOutput {
readonly inviteId: string;
readonly status: "pending" | "accepted" | "revoked" | "expired";
readonly alreadyRevoked: boolean;
}

Concurrency: the adapter flips status from pending → revoked via a CAS ConditionExpression. A racing accept surfaces a clear conflict instead of silently overwriting. Already-revoked invites return alreadyRevoked: true; already-accepted invites reject.

Errors:

CodeWhen
org_invite_access_deniedCaller is not owner/admin of the invite’s org.
org_invite_not_foundThe id doesn’t exist OR isn’t reachable by the caller.

Operator actions on GguiUserApiKey rows — the user-facing ggui_user_* API key strings that Claude Desktop (and other Connectors) present to call the MCP routes on the user’s behalf. Bound on the cloud pod via the issueGguiUserApiKey AppSync mutation + the apiKeysByUserId GSI + raw DDB UpdateItem for revoke.

Read the calling user’s ggui_user_* connector keys. Metadata only — NEVER plaintext.

Inputs: none.

Returns: { keys: ConnectorKeySummary[] }

interface ConnectorKeySummary {
readonly id: string; // stable id for revoke
readonly apiKeyPrefix: string; // first ~8 chars of the secret (human re-identification)
readonly name?: string; // user-supplied label
readonly appId?: string; // optional FK — when set the key locks to that app
readonly status: "active" | "revoked";
readonly createdAt: string;
readonly lastUsedAt?: string; // from the last successful auth lookup
readonly expiresAt?: string; // past timestamp ⇒ adapter rejects auth
}

The hash itself is never returned on any tool.

Mint a fresh ggui_user_* connector key.

FieldTypeRequiredDescription
namestring (1–120 chars)NoOptional label, e.g. 'MacBook Claude Desktop'. Surfaces on ggui_ops_list_connector_keys.
appIdstringNoLock the key to one app. When set, sessions opened with this key scope to the named app and meta-tools (ggui_ops_open_app, ggui_ops_list_apps) are NOT exposed. Absent ⇒ universal key (scopes to User.defaultAppId per request).
expiresAtstring (ISO 8601)NoOptional expiry. Past timestamps reject auth from the start.

Returns:

interface IssueConnectorKeyOutput {
// metadata — same shape as a list row
readonly id: string;
readonly apiKeyPrefix: string;
readonly name?: string;
readonly appId?: string;
readonly status: "active" | "revoked";
readonly createdAt: string;
readonly lastUsedAt?: string;
readonly expiresAt?: string;
// ONE-TIME REVEAL — never returned again
readonly plaintextKey: string;
}

Soft-revoke a GguiUserApiKey row. The adapter sets status='revoked'; the auth path rejects revoked keys regardless of hash match. Rows are kept for audit (age-based sweep handles cleanup).

FieldTypeRequiredDescription
keyIdstringYesStable id of the row (NOT the secret string). Discover via ggui_ops_list_connector_keys.

Returns:

interface RevokeConnectorKeyOutput {
readonly id: string;
readonly status: "active" | "revoked";
readonly alreadyRevoked: boolean;
}

Errors:

CodeWhen
connector_key_access_deniedThe key belongs to another user.
connector_key_not_foundNo such key reachable by the caller.

Idempotent — re-revoking returns alreadyRevoked: true.

Operator action on GguiCoupon rows — bearer-secret promo codes that credit user or org wallets. Bound on the cloud pod via the redeemCoupon AppSync mutation.

Redeem a cpn_* coupon code, crediting the caller’s wallet (default) or a target org’s wallet. The adapter runs an atomic three-leg TransactWrite:

  1. Flip GguiCoupon.status from issued → activated.
  2. Credit the wallet (user or org).
  3. Insert a ledger row.

Failure of any leg rolls all back — no half-credit, no double-spend.

FieldTypeRequiredDescription
couponCodestringYesThe bearer-secret code in format cpn_<8 chars>. One-time redemption.
targetOrgIdstringNoWhen set, credits the named org’s wallet instead of the caller’s personal wallet. Caller MUST be a member of the org.

Returns:

interface RedeemCouponOutput {
readonly couponCode: string;
readonly creditCents: number;
readonly redeemedByPrincipalType: "user" | "org";
readonly redeemedByPrincipalId: string;
readonly activatedAt: string;
}

Errors:

CodeWhen
coupon_not_foundThe code doesn’t exist.
coupon_already_redeemedThe code was previously activated (one-time semantics).
coupon_expiredThe code is past its expiry.
coupon_access_deniedtargetOrgId was provided but the caller is not a member of that org.

The apps domain is wired through an optional field on CreateGguiServerOptions (ops-blueprint hangs off the opsBlueprint dep bundle on defaultHandlers; provider-keys + credits are cloud-pod-bound):

interface CreateGguiServerOptions {
readonly opsApps?: {
readonly apps: AppsSource;
readonly userDefaultApp: UserDefaultAppSource;
};
}
  • Hosted (mcp.ggui.ai): the cloud pod binds the apps domain — AppSync-backed adapters wrap the corresponding mutations. Provider-keys and credits (above) are also bound, and the pod additionally registers its own 24 hosted-only tools, orgs, connector keys and coupons among them (see Hosted-only ops) — the full ops surface is already live on /control.
  • OSS (ggui serve): every field is undefined by default. The route still mounts but tools/list rejects with Method not found — no tools capability is advertised when zero handlers are registered. Operator tools only make sense alongside a data model to operate on; the ops-blueprint family is the one most self-hosters wire (via the opsBlueprint dep bundle).
  • Wiring apps: a self-hosted deployment with its own AppsSource and UserDefaultAppSource binds opsApps and gets the six ggui_ops_*_app tools.
  • Before v0.25.0: orgs, connector keys and coupons were open handler families wired through opsOrgs, opsConnectorKeys and opsCoupon. Organisations, wallets and user API keys are features of the hosted service, so those families now ship only there; a deployment that bound them can stay on v0.24.0, or copy the handlers into its own code and register them through the handlers option.

The seam interfaces (AppsSource, UserDefaultAppSource) are exported from @ggui-ai/mcp-server-handlers — implementing them against your own backend is the integration path for downstream forks.

The console UI mirrors these tools 1:1 — every tool corresponds to one UI action:

ToolConsole surface
ggui_ops_list_appsApps section — main list.
ggui_ops_create_appApps section — “New app” button.
ggui_ops_update_appApps section — inline rename + System Prompt editor + rate-limit setting.
ggui_ops_set_app_themeApps section → Theme editor.
ggui_ops_delete_appApps section — row menu → Delete. Both paths converge on the app-delete cascade worker (see the tool’s section above); the console removes the app row synchronously, the tool’s dependent-row sweep is eventually consistent (seconds).
ggui_ops_set_default_appApps section — “Set as default” toggle.
ggui_ops_list_orgsOrgs section — main list.
ggui_ops_create_orgOrgs section — “New org” button.
ggui_ops_rename_orgOrgs section — org header → Rename.
ggui_ops_remove_org_memberOrgs section → Members → row menu → Remove / “Leave org”.
ggui_ops_get_org_balanceOrgs section → Wallet balance readout.
ggui_ops_invite_to_orgOrgs section → Members → Invite.
ggui_ops_revoke_inviteOrgs section → Members → pending invite row → Revoke.
ggui_ops_list_connector_keysAccount → Connector Keys list.
ggui_ops_issue_connector_keyAccount → Connector Keys → “Issue new key”.
ggui_ops_revoke_connector_keyAccount → Connector Keys → row menu → Revoke.
ggui_ops_redeem_couponBilling → Redeem coupon.
ggui_ops_set_provider_key / ggui_ops_remove_provider_key with appIdApp settings → Provider Keys (per-app override).

The MCP surface and the UI surface are siblings over the same seam — they write the same rows — the console’s “New app” and ggui_ops_create_app both go through AppsSource.create, for example. There’s no privileged path on either side.


This walkthrough targets a self-hosted server with the ops seams wired (started with --dev-allow-all for the Bearer dev shortcut). On hosted GGUI, the same calls go to https://mcp.ggui.ai/control with an OAuth bearer.

Note the confirm argument on the state-changing steps — without it, each returns a preview instead of acting.

Terminal window
# 1. Initialize
curl -X POST http://127.0.0.1:6781/control \
-H "Authorization: Bearer dev" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25","clientInfo":{"name":"curl","version":"1.0"},"capabilities":{}}}'
# 2. Enumerate the caller's apps
curl -X POST http://127.0.0.1:6781/control \
-H "Authorization: Bearer dev" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"ggui_ops_list_apps","arguments":{}}}'
# 3. Create a fresh app
curl -X POST http://127.0.0.1:6781/control \
-H "Authorization: Bearer dev" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"ggui_ops_create_app","arguments":{"displayName":"Inbox Triage","confirm":true}}}'
# 4. Promote the new app to default (use the appId from step 3's response)
curl -X POST http://127.0.0.1:6781/control \
-H "Authorization: Bearer dev" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"ggui_ops_set_default_app","arguments":{"appId":"<appId>","confirm":true}}}'

On hosted GGUI, a fifth call — ggui_ops_issue_connector_key with {"name":"MacBook Claude Desktop","appId":"<appId>","confirm":true} — mints a connector key locked to the new app; its response carries plaintextKey, to surface to the user immediately (see Connector keys).

The same calls can be made through the @modelcontextprotocol/sdk client by pointing the transport at /control instead of /mcp — the tool registration shapes are standard.


  • Console — the human-facing surface for the same actions.
  • Audience Routes — the agent / runtime / protocol / ops tag model and how it projects onto the two surfaces.
  • MCP Protocol Reference — the sibling agent-loop surface on /mcp.