Skip to content

Ops MCP route

read as .md

The /ops route surfaces operator-class MCP tools — the same actions the console UI (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.

This page is the wire reference for the 24 ops-audience handlers across seven domains. The agent-loop surface (handshake / render / consume / …) lives on /mcp and is documented separately — /ops is a strictly disjoint route with no overlap.

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

POST http://127.0.0.1:6781/ops

Self-hosted servers register tools on /ops when the operator seams are wired into createGguiServer({opsApps, opsOrgs, opsConnectorKeys, opsCoupon}) (the ops-blueprint family additionally hangs off the opsBlueprint dep bundle on defaultHandlers). With nothing wired, the route still mounts but tools/list rejects with JSON-RPC Method not found — no tools capability is advertised when zero handlers are registered. Hosted ggui (coming soon) will serve the same route at https://mcp.ggui.ai/ops.

Identical to /mcp — bearer-token auth via the same upstream AuthAdapter:

Authorization: Bearer dev
Content-Type: application/json

Self-hosted: with ggui serve --dev-allow-all, any bearer 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). The bearer presented on /ops is the same bearer presented on /mcp — there is no separate “ops token”.

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_rename_app / ggui_ops_set_default_app / ggui_ops_update_app_system_promptThrows 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_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.

Seven domains, 24 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.

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, createdAt, updatedAt. Bound on the cloud pod via the AppSync provisionGguiApp mutation + the GguiApp model.

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.

Update an existing app’s displayName. The target app MUST be owned by the calling user.

FieldTypeRequiredDescription
appIdstringYesTarget GguiApp.appId. Discover via ggui_ops_list_apps.
displayNamestring (1–120 chars)YesNew display name. Cap matches the cloud provisioning Lambda.

Returns: the updated AppRecord.

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. The cloud adapter additionally cascades per-app keys / blueprints / sessions (orchestrated below the seam).

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

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.

Set or clear the per-app system-prompt override. Empty-string input clears the field — the pod’s per-app system-prompt resolution then falls back to the universal default.

FieldTypeRequiredDescription
appIdstringYesTarget GguiApp.appId.
systemPromptstring (≤10,000 chars)YesReplacement text. Pass "" to clear the override. Cap bounds the response payload and matches a reasonable agent-authored prompt length.

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

Errors:

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

Operator actions on GguiOrg + GguiOrgMember + GguiOrgInvite 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 the provisionGguiOrg / fetchMyOrgs / issueOrgInvite / revokeOrgInvite AppSync mutations.

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

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

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

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

Section titled “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.

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

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

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

FieldTypeRequiredDescription
contractobjectYesThe DataContract to generate against.
generatorstringNoGenerator slug. Unknown slug fails with generator_not_found.
personastringNoVariance axis — normalized lowercase + trimmed.
aestheticstringNoVariance axis.
contextstringNoVariance axis.
seedPromptstringNoVariance axis.
setAsOperatorDefaultbooleanNoPromote 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.

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.

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

FieldTypeRequiredDescription
blueprintIdstringYesTarget blueprint.
isOperatorDefaultliteral trueNoPromote to operator default.
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 (coming soon), 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.

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

Inputs: none.

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

FieldTypeRequiredDescription
providerenumYesProvider to remove.

Returns: { deleted, provider }


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.

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 one of free_credit / render_charge / topup / refund.


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

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

The console UI (coming soon) will mirror 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_rename_appApps section — inline rename.
ggui_ops_delete_appApps section — row menu → Delete.
ggui_ops_set_default_appApps section — “Set as default” toggle.
ggui_ops_update_app_system_promptApps section → System Prompt editor.
ggui_ops_list_orgsOrgs section — main list.
ggui_ops_create_orgOrgs section — “New org” button.
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.

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.


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/ops with an OAuth bearer.

Terminal window
# 1. Initialize
curl -X POST http://127.0.0.1:6781/ops \
-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/ops \
-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/ops \
-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"}}}'
# 4. Promote the new app to default (use the appId from step 3's response)
curl -X POST http://127.0.0.1:6781/ops \
-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>"}}}'
# 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/ops \
-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>"}}}'

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


  • Console — the human-facing surface for the same actions (coming soon).
  • Audience Routes — the agent / runtime / protocol / ops tag model and how it projects to routes.
  • MCP Protocol Reference — the sibling agent-loop surface on /mcp.