---
title: Control MCP plane
description: MCP tools on the /control plane — design-time protocol tools (anonymous) plus operator actions (apps, orgs, connector keys, coupons, blueprints, provider keys, credits) for operator agents.
---

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](/api/mcp-protocol/) for those tools.
- **Operator-class `ops` tools** — the same actions the [console UI](/clients/console/) (coming soon) will expose 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 27 ops-audience handlers across seven domains. The agent-loop surface (handshake / render / consume / …) lives on the [`/mcp`](/api/mcp-protocol/) data plane and is documented separately — the two surfaces are strictly disjoint.

:::note[Audience tags]
Every handler in this package carries an `audience: ['ops']` tag, which determines mounting. See [Audience Routes](/architecture/audience-routes/) for the full model — four tags, two surfaces: the `/mcp` data plane (`agent` + `runtime`) and the `/control` plane (`protocol` + `ops`). The tag is the source of truth; the surface is the projection.
:::

## What's on /control

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` (coming soon) 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 (coming soon) will expose. The handler files in `@ggui-ai/mcp-server-handlers` are pure over typed seams (`AppsSource`, `OrgsSource`, `OrgInvitesSource`, `ConnectorKeysSource`, `CouponRedeemSource`) — the cloud pod binds AppSync-backed adapters; OSS deployments leave the seams unwired and the surface stays narrow.

## Endpoint

```
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, opsOrgs, opsConnectorKeys, opsCoupon})` (the ops-blueprint family additionally hangs off the `opsBlueprint` dep bundle). Hosted ggui (coming soon) will serve the same route at `https://mcp.ggui.ai/control`, where it carries 36 tools: 6 protocol + 30 ops (the 27 handler-package tools below plus the hosted deployment's own library-curation tools).

## Authentication

`/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`](/api/mcp-protocol/#authentication), 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 (coming soon) will run the OAuth 2.0 Dynamic Client Registration ceremony (see [OAuth on mcp.ggui.ai](/api/oauth/)). 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.

## Confirmation gate on state-changing tools

An `ops` tool that changes state answers the first call with a **preview** and mutates nothing. Only a second call carrying `confirm: true` commits:

```jsonc
// 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 reads 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_get_credit_balance`, `ggui_ops_get_org_balance`, `ggui_ops_get_my_blueprint_source`, and `ggui_ops_setup_byok`.

## Identity model

Every handler resolves the calling identity through a single helper:

```typescript
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.

### Tenancy posture

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":

| Operation                                                                         | Cross-tenant probe                                                                           |
| --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- |
| `ggui_ops_list_apps`                                                              | Returns only caller's rows; foreign rows invisible.                                          |
| `ggui_ops_update_app` / `ggui_ops_set_app_theme` / `ggui_ops_set_default_app`     | Throws `app_not_found` — same as a genuinely missing id.                                     |
| `ggui_ops_delete_app`                                                             | Returns `{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_balance` | Throws `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_invite`                               | Throws `org_invite_access_denied` for orgs the caller doesn't administer.                    |
| `ggui_ops_revoke_connector_key`                                                   | Throws `connector_key_access_denied` for keys owned by other users.                          |
| `ggui_ops_redeem_coupon`                                                          | Throws `coupon_access_denied` for `targetOrgId` orgs the caller isn't a member of.           |
| `ggui_ops_set_provider_key` / `ggui_ops_remove_provider_key` with `appId`         | Throws `app_not_found` for apps the caller doesn't own — same as a missing id.               |

### One-time secret reveal

:::caution[Plaintext keys appear exactly once]
`ggui_ops_issue_connector_key` returns the plaintext `ggui_user_*` secret on its result — this is the **only call** that ever surfaces it. The adapter persists `sha256(plaintext)` hex plus the first ~8 plaintext characters (`apiKeyPrefix`); the plaintext is not stored anywhere. Subsequent `ggui_ops_list_connector_keys` responses carry the prefix and the metadata but never the full secret.

The MCP caller (Claude Desktop conversation, console) is responsible for surfacing the plaintext to the user immediately. There is no recovery if it's lost — the user must revoke and reissue.
:::

---

## Tools by domain

Seven domains, 27 handlers total. Each domain is optional: the four console-style domains (apps / orgs / connector keys / coupons) hang off `CreateGguiServerOptions`; ops-blueprint hangs off the `opsBlueprint` dep bundle on `defaultHandlers`; provider-keys and credits are bound by the hosted cloud pod (coming soon). Leaving a domain unwired removes its tools from `tools/list` at registration time.

## Apps (`ops-apps`, 6 handlers)

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.

### `ggui_ops_list_apps`

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[] }`

```typescript
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.

### `ggui_ops_create_app`

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

| Field         | Type                   | Required | Description                                                                                                    |
| ------------- | ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| `displayName` | `string` (1–120 chars) | No       | Human-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.

### `ggui_ops_update_app`

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.

| Field                | Type                     | Required | Description                                                                                                                               |
| -------------------- | ------------------------ | -------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `appId`              | `string`                 | Yes      | Target `GguiApp.appId`. Discover via `ggui_ops_list_apps`.                                                                                |
| `displayName`        | `string` (1–120 chars)   | No       | New display name.                                                                                                                         |
| `systemPrompt`       | `string` (≤10,000 chars) | No       | Replacement text. Pass `""` to clear the override — renders then fall back to the universal default.                                      |
| `rateLimitPerMinute` | `integer` (≥ 0)          | No       | Per-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:**

| Code            | When                                                                                     |
| --------------- | ---------------------------------------------------------------------------------------- |
| `app_not_found` | The id doesn't exist OR exists under another tenant (uniform shape — no existence leak). |

### `ggui_ops_delete_app`

Hard-delete an app owned by the calling user. Idempotent — a second delete of the same id resolves cleanly. The cloud adapter additionally cascades per-app keys / blueprints / sessions (orchestrated below the seam).

| Field   | Type     | Required | Description             |
| ------- | -------- | -------- | ----------------------- |
| `appId` | `string` | Yes      | Target `GguiApp.appId`. |

**Returns:** `{ deleted: true }`

**Tenancy:** cross-tenant probes return the success shape without touching the foreign row. Uniform with "row didn't exist."

### `ggui_ops_set_default_app`

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

| Field   | Type     | Required | Description                               |
| ------- | -------- | -------- | ----------------------------------------- |
| `appId` | `string` | Yes      | Target app — must be owned by the caller. |

**Returns:** `{ defaultAppId: string }`

**Errors:**

| Code            | When                                                        |
| --------------- | ----------------------------------------------------------- |
| `app_not_found` | Target `appId` doesn't exist OR is owned by another tenant. |

### `ggui_ops_set_app_theme`

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.

| Field   | Type       | Required | Description                                                                                                                                            |
| ------- | ---------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `appId` | `string`   | Yes      | Target `GguiApp.appId`.                                                                                                                                |
| `theme` | `AppTheme` | Yes      | `{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:**

| Code            | When                                                        |
| --------------- | ----------------------------------------------------------- |
| `app_not_found` | Target `appId` doesn't exist OR is owned by another tenant. |

---

## Orgs (`ops-orgs`, 7 handlers)

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.

### `ggui_ops_list_orgs`

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[] }`

```typescript
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.

### `ggui_ops_create_org`

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.

| Field  | Type                   | Required | Description                                                                          |
| ------ | ---------------------- | -------- | ------------------------------------------------------------------------------------ |
| `name` | `string` (1–120 chars) | Yes      | Human-friendly display name. Required (no default — orgs are intentional creations). |

**Returns:**

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

### `ggui_ops_rename_org`

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.

| Field   | Type                   | Required | Description                                                                       |
| ------- | ---------------------- | -------- | --------------------------------------------------------------------------------- |
| `orgId` | `string`               | Yes      | Target org — caller must own or administer it. Discover via `ggui_ops_list_orgs`. |
| `name`  | `string` (1–120 chars) | Yes      | New display name. Trimmed; cap matches org provisioning.                          |

**Returns:** `{ orgId, name, updatedAt }`

**Errors:**

| Code                | When                                                                             |
| ------------------- | -------------------------------------------------------------------------------- |
| `org_not_found`     | The id doesn't exist OR the caller isn't a member (uniform — no existence leak). |
| `org_access_denied` | The caller is a member but not owner/admin.                                      |

### `ggui_ops_remove_org_member`

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

|                 | target=owner | target=admin | target=member |
| --------------- | ------------ | ------------ | ------------- |
| `caller=owner`  | no           | yes          | yes           |
| `caller=admin`  | no           | only-self    | yes           |
| `caller=member` | no           | no           | only-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.

| Field          | Type     | Required | Description                                               |
| -------------- | -------- | -------- | --------------------------------------------------------- |
| `orgId`        | `string` | Yes      | Target org — the caller must be a member.                 |
| `memberUserId` | `string` | Yes      | Member 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:**

| Code                        | When                                                                             |
| --------------------------- | -------------------------------------------------------------------------------- |
| `org_not_found`             | The id doesn't exist OR the caller isn't a member (uniform — no existence leak). |
| `org_member_removal_denied` | The role matrix denies the removal — the message names the specific rule.        |

### `ggui_ops_get_org_balance`

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

| Field   | Type     | Required | Description                               |
| ------- | -------- | -------- | ----------------------------------------- |
| `orgId` | `string` | Yes      | Target 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:**

| Code            | When                                                                             |
| --------------- | -------------------------------------------------------------------------------- |
| `org_not_found` | The id doesn't exist OR the caller isn't a member (uniform — no existence leak). |

### `ggui_ops_invite_to_org`

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 (coming soon): `console.ggui.ai/invites/<inviteId>`.

| Field   | Type                  | Required | Description                                                                                                           |
| ------- | --------------------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `orgId` | `string`              | Yes      | Target org — caller must own or administer it. Discover via `ggui_ops_list_orgs`.                                     |
| `email` | `string` (RFC 5322)   | Yes      | Recipient email — the invite link is sent here.                                                                       |
| `role`  | `'admin' \| 'member'` | Yes      | Role the recipient holds once they accept. Owner can't be granted via invite — ownership transfer is a separate flow. |

**Returns:**

```typescript
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:**

| Code                       | When                                         |
| -------------------------- | -------------------------------------------- |
| `org_invite_access_denied` | Caller is not owner/admin of the target org. |

### `ggui_ops_revoke_invite`

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

| Field      | Type     | Required | Description                                                      |
| ---------- | -------- | -------- | ---------------------------------------------------------------- |
| `inviteId` | `string` | Yes      | Target invite — must belong to an org the caller can administer. |

**Returns:**

```typescript
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:**

| Code                       | When                                                   |
| -------------------------- | ------------------------------------------------------ |
| `org_invite_access_denied` | Caller is not owner/admin of the invite's org.         |
| `org_invite_not_found`     | The id doesn't exist OR isn't reachable by the caller. |

---

## Connector keys (`ops-connector-keys`, 3 handlers)

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.

### `ggui_ops_list_connector_keys`

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

**Inputs:** none.

**Returns:** `{ keys: ConnectorKeySummary[] }`

```typescript
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.

### `ggui_ops_issue_connector_key`

Mint a fresh `ggui_user_*` connector key.

| Field       | Type                   | Required | Description                                                                                                                                                                                                                             |
| ----------- | ---------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `name`      | `string` (1–120 chars) | No       | Optional label, e.g. `'MacBook Claude Desktop'`. Surfaces on `ggui_ops_list_connector_keys`.                                                                                                                                            |
| `appId`     | `string`               | No       | Lock 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). |
| `expiresAt` | `string` (ISO 8601)    | No       | Optional expiry. Past timestamps reject auth from the start.                                                                                                                                                                            |

**Returns:**

```typescript
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;
}
```

:::caution[One-time reveal]
`plaintextKey` is the `ggui_user_<random>` secret. It appears on this response and never again — the adapter persists `sha256(plaintextKey)` hex plus `apiKeyPrefix`, and the plaintext is not stored. The caller MUST surface it to the user immediately.
:::

### `ggui_ops_revoke_connector_key`

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

| Field   | Type     | Required | Description                                                                                |
| ------- | -------- | -------- | ------------------------------------------------------------------------------------------ |
| `keyId` | `string` | Yes      | Stable id of the row (NOT the secret string). Discover via `ggui_ops_list_connector_keys`. |

**Returns:**

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

**Errors:**

| Code                          | When                                 |
| ----------------------------- | ------------------------------------ |
| `connector_key_access_denied` | The key belongs to another user.     |
| `connector_key_not_found`     | No such key reachable by the caller. |

Idempotent — re-revoking returns `alreadyRevoked: true`.

---

## Coupons (`ops-coupon`, 1 handler)

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.

### `ggui_ops_redeem_coupon`

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.

| Field         | Type     | Required | Description                                                                                                           |
| ------------- | -------- | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `couponCode`  | `string` | Yes      | The bearer-secret code in format `cpn_<8 chars>`. One-time redemption.                                                |
| `targetOrgId` | `string` | No       | When set, credits the named org's wallet instead of the caller's personal wallet. Caller MUST be a member of the org. |

**Returns:**

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

**Errors:**

| Code                      | When                                                                   |
| ------------------------- | ---------------------------------------------------------------------- |
| `coupon_not_found`        | The code doesn't exist.                                                |
| `coupon_already_redeemed` | The code was previously activated (one-time semantics).                |
| `coupon_expired`          | The code is past its expiry.                                           |
| `coupon_access_denied`    | `targetOrgId` was provided but the caller is not a member of that org. |

---

## Blueprints (`ops-blueprint`, 5 handlers)

Operator blueprint authorship — generate, register, list, update, delete cached blueprints for the calling app. Unlike the four console-style domains, 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).

### `ggui_ops_generate_blueprint`

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

| Field                  | Type      | Required | Description                                                    |
| ---------------------- | --------- | -------- | -------------------------------------------------------------- |
| `contract`             | `object`  | Yes      | The `DataContract` to generate against.                        |
| `generator`            | `string`  | No       | Generator slug. Unknown slug fails with `generator_not_found`. |
| `persona`              | `string`  | No       | Variance axis — normalized lowercase + trimmed.                |
| `aesthetic`            | `string`  | No       | Variance axis.                                                 |
| `context`              | `string`  | No       | Variance axis.                                                 |
| `seedPrompt`           | `string`  | No       | Variance axis.                                                 |
| `setAsOperatorDefault` | `boolean` | No       | Promote the result to the operator default for its contract.   |

**Returns:** `{ blueprintId, codeHash?, validatorScore?, source }` — `validatorScore` (0–1) only on the advanced generator path; `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.

### `ggui_ops_register_blueprint`

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

| Field           | Type     | Required | Description                                           |
| --------------- | -------- | -------- | ----------------------------------------------------- |
| `contract`      | `object` | Yes      | The `DataContract` the code implements.               |
| `componentCode` | `string` | Yes      | The component code to register verbatim (min 1 char). |

Plus the same optional `generator` / `persona` / `aesthetic` / `context` / `seedPrompt` / `setAsOperatorDefault` fields as `ggui_ops_generate_blueprint`.

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

### `ggui_ops_list_blueprints`

| Field            | Type       | Required | Description                                           |
| ---------------- | ---------- | -------- | ----------------------------------------------------- |
| `contractHash`   | `string`   | No       | Filter by canonical contract hash.                    |
| `generator`      | `string`   | No       | Filter by generator slug.                             |
| `persona`        | `string`   | No       | Dispatches semantic search.                           |
| `intentKeywords` | `string[]` | No       | Dispatches semantic search. Filters are AND-composed. |

**Returns:** `{ blueprints: Blueprint[] }`

### `ggui_ops_update_blueprint`

| Field               | Type           | Required | Description                                                       |
| ------------------- | -------------- | -------- | ----------------------------------------------------------------- |
| `blueprintId`       | `string`       | Yes      | Target blueprint.                                                 |
| `isOperatorDefault` | `literal true` | No       | Promote to operator default.                                      |
| `variance`          | `object`       | No       | Partial-merge of variance axes; `{persona: ""}` clears the field. |

**Returns:** `{ blueprintId, updatedAt }`

### `ggui_ops_delete_blueprint`

| Field         | Type     | Required | Description       |
| ------------- | -------- | -------- | ----------------- |
| `blueprintId` | `string` | Yes      | Target blueprint. |

**Returns:** `{ deleted: true }` — idempotent.

---

## 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 (coming soon), which validates keys against the provider and encrypts at rest.

### `ggui_ops_set_provider_key`

| Field          | Type     | Required | Description                                                                                                                                                                                                                                                              |
| -------------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `provider`     | `enum`   | Yes      | One of `anthropic` / `openai` / `google` / `openrouter`.                                                                                                                                                                                                                 |
| `plaintextKey` | `string` | Yes      | The provider API key (min 1 char). Re-set replaces (rotation).                                                                                                                                                                                                           |
| `label`        | `string` | No       | Human label.                                                                                                                                                                                                                                                             |
| `appId`        | `string` | No       | Scope 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.

### `ggui_ops_list_provider_keys`

**Inputs:** none.

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

### `ggui_ops_remove_provider_key`

| Field      | Type     | Required | Description                                                                                                                                             |
| ---------- | -------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `provider` | `enum`   | Yes      | Provider to remove.                                                                                                                                     |
| `appId`    | `string` | No       | Remove 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 }`

---

## Credits (`credits`, 2 handlers)

Read-only views over the caller's prepaid credit wallet. Bound by the hosted cloud pod (coming soon); self-hosted deployments have no credit plane.

### `ggui_ops_get_credit_balance`

**Inputs:** none.

**Returns:** `{ balanceCents, lifetimeGrantedCents, lifetimeSpentCents, updatedAt }`

### `ggui_ops_list_credit_transactions`

| Field    | Type     | Required | Description        |
| -------- | -------- | -------- | ------------------ |
| `limit`  | `number` | No       | 1–100, default 20. |
| `cursor` | `string` | No       | Pagination cursor. |

**Returns:** `{ transactions: [{ transactionId, kind, deltaCents, balanceAfterCents, reason, createdAt, relatedSessionId? }], nextCursor? }` — `kind` is one of `free_credit` / `render_charge` / `topup` / `refund`.

---

## OSS vs hosted

The four console-style domains are wired through optional fields on `CreateGguiServerOptions` (ops-blueprint hangs off the `opsBlueprint` dep bundle on `defaultHandlers`; provider-keys + credits are cloud-pod-bound):

```typescript
interface CreateGguiServerOptions {
  readonly opsApps?: {
    readonly apps: AppsSource;
    readonly userDefaultApp: UserDefaultAppSource;
  };
  readonly opsOrgs?: {
    readonly orgs: OrgsSource;
    readonly invites: OrgInvitesSource;
  };
  readonly opsConnectorKeys?: {
    readonly connectorKeys: ConnectorKeysSource;
  };
  readonly opsCoupon?: {
    readonly coupons: CouponRedeemSource;
  };
}
```

- **Hosted (`mcp.ggui.ai`, coming soon):** the cloud pod binds all four — AppSync-backed adapters wrap the corresponding mutations — plus the provider-keys and credits families. The full ops surface is registered 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).
- **Partial wiring:** omit individual fields to drop their tools. A self-hosted deployment with its own `AppsSource` can register `ggui_ops_*_app` only and leave orgs / connector keys / coupons unwired.

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

## Console parity

The [console UI](/clients/console/) (coming soon) will mirror these tools 1:1 — every tool corresponds to one UI action:

| Tool                                                                      | Console surface                                                           |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------- |
| `ggui_ops_list_apps`                                                      | Apps section — main list.                                                 |
| `ggui_ops_create_app`                                                     | Apps section — "New app" button.                                          |
| `ggui_ops_update_app`                                                     | Apps section — inline rename + System Prompt editor + rate-limit setting. |
| `ggui_ops_set_app_theme`                                                  | Apps section → Theme editor.                                              |
| `ggui_ops_delete_app`                                                     | Apps section — row menu → Delete.                                         |
| `ggui_ops_set_default_app`                                                | Apps section — "Set as default" toggle.                                   |
| `ggui_ops_list_orgs`                                                      | Orgs section — main list.                                                 |
| `ggui_ops_create_org`                                                     | Orgs section — "New org" button.                                          |
| `ggui_ops_rename_org`                                                     | Orgs section — org header → Rename.                                       |
| `ggui_ops_remove_org_member`                                              | Orgs section → Members → row menu → Remove / "Leave org".                 |
| `ggui_ops_get_org_balance`                                                | Orgs section → Wallet balance readout.                                    |
| `ggui_ops_invite_to_org`                                                  | Orgs section → Members → Invite.                                          |
| `ggui_ops_revoke_invite`                                                  | Orgs section → Members → pending invite row → Revoke.                     |
| `ggui_ops_list_connector_keys`                                            | Account → Connector Keys list.                                            |
| `ggui_ops_issue_connector_key`                                            | Account → Connector Keys → "Issue new key".                               |
| `ggui_ops_revoke_connector_key`                                           | Account → Connector Keys → row menu → Revoke.                             |
| `ggui_ops_redeem_coupon`                                                  | Billing → Redeem coupon.                                                  |
| `ggui_ops_set_provider_key` / `ggui_ops_remove_provider_key` with `appId` | App settings → Provider Keys (per-app override).                          |

The MCP surface and the UI surface are siblings over the same seam — they call the same `AppsSource.create`, the same `OrgInvitesSource.issue`, etc. There's no privileged path on either side.

---

## Example: curl

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 (coming soon), the same calls will 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.

```bash
# 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-06-18","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}}}'

# 5. Issue a connector key locked to the new app
#    The response carries `plaintextKey` — surface it to the user immediately.
curl -X POST http://127.0.0.1:6781/control \
  -H "Authorization: Bearer dev" \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"ggui_ops_issue_connector_key","arguments":{"name":"MacBook Claude Desktop","appId":"<appId>","confirm":true}}}'
```

The same calls can be made through the [`@modelcontextprotocol/sdk`](https://github.com/modelcontextprotocol/typescript-sdk) client by pointing the transport at `/control` instead of `/mcp` — the tool registration shapes are standard.

---

## See Also

- [Console](/clients/console/) — the human-facing surface for the same actions (coming soon).
- [Audience Routes](/architecture/audience-routes/) — the `agent` / `runtime` / `protocol` / `ops` tag model and how it projects onto the two surfaces.
- [MCP Protocol Reference](/api/mcp-protocol/) — the sibling agent-loop surface on `/mcp`.