Capability UI Protocol / Full Spec
API
Capability UI Protocol · CUP-001 · version 0.1

Permission-aware software with generated interfaces.

This specification defines a library for exposing data and tools as typed capabilities, resolving policy into authorized views, allowing agents to compose presentation, and enforcing the final action at the execution boundary.

Draft standard · reference implementation: TypeScript

1. Goals and non-goals

CUP is the authorization and presentation contract between applications and agents. It makes available actions explicit without prescribing one visual language.

Goals

  • Represent data and tools with stable, typed contracts.
  • Distinguish discovery, inspection, reading, mutation, execution, sharing, and delegation.
  • Generate a safe capability view for an agent or renderer.
  • Express scope, purpose, expiration, approval, and risk.
  • Enforce the exact proposed action immediately before side effects.
  • Produce receipts that explain decisions and results.
  • Support web, mobile, voice, native, and future renderers.

Non-goals

  • Replacing authentication, identity proofing, or session management.
  • Choosing an agent model, planner, prompt, or vendor.
  • Replacing a database or defining data ownership.
  • Defining a universal visual component library.
  • Assuming that UI visibility is authorization.
  • Allowing an old authorization result to survive a policy change.

2. Core concepts

Resource

Something that exists

A data object, collection, file, tool, workflow, view, model, agent, or identity.

Principal

Something that acts

A person, agent, service, group, or organization. A delegated agent remains distinct from its delegator.

Capability

A typed possible action

A named operation with inputs, outputs, side effects, risk, and required policy.

Policy

A rule about access

An allow or deny rule connecting a principal, operation, resource, scope, and conditions.

Projection

A safe authorized view

The subset of resources, fields, and actions that may be disclosed to a consumer.

Receipt

Evidence of a decision

An append-only record of authorization, confirmation, execution, and outcome.

Normative rule: a generated interface is a presentation of an authorized projection. It is never the source of authorization.

3. Data model

All identifiers are namespaced strings. Resource schemas use JSON Schema 2020-12 or a compatible typed schema. Unknown fields are rejected by default.

interface Subject { id: string; // user:john, agent:personal type: 'user' | 'agent' | 'service' | 'group'; authenticated: boolean; attributes: Record<string, unknown>; } interface Resource { id: string; // calendar:john type: 'data' | 'capability' | 'workflow' | 'view' | 'agent'; version: string; // changes when schema or semantics change schema: JsonSchema; sensitivity: 'public' | 'personal' | 'confidential' | 'restricted'; owner?: string; metadata: Record<string, unknown>; } interface Capability extends Resource { type: 'capability'; operation: Operation; inputSchema: JsonSchema; outputSchema: JsonSchema; sideEffects: SideEffect[]; risk: 'low' | 'medium' | 'high' | 'critical'; confirmation: ConfirmationMode; idempotency: 'none' | 'supported' | 'required'; reversibility: 'reversible' | 'partially_reversible' | 'irreversible'; } type Operation = | 'discover' | 'inspect' | 'read' | 'create' | 'update' | 'delete' | 'execute' | 'share' | 'delegate';

3.1 Resource and capability separation

A tool is a resource whose operation is usually execute. Its schema can be inspectable even when invocation is denied. A data resource can expose read capabilities and mutation capabilities separately.

PermissionDisclosesDoes not imply
discoverThat a resource existsSchema, contents, or use
inspectMetadata, schema, risk, side effectsContents or execution
readContents, subject to field filtersMutation or sharing
executeInvocation of a named capabilityAccess to implementation data
sharePermission to grant access onwardPermission to use the resource personally

4. Policy language

Policies are declarative. The reference evaluator is deterministic, side-effect free, and usable without an agent or network call.

interface Policy { id: string; effect: 'allow' | 'deny'; principal: SubjectSelector; operation: Operation | Operation[]; resource: ResourceSelector; scope?: ScopeExpression; conditions?: ConditionExpression[]; obligations?: Obligation[]; priority: number; validDuring?: TimeWindow; version: string; } interface AuthorizationRequest { requestId: string; subject: Subject; operation: Operation; resource: ResourceRef; proposedInput?: unknown; purpose?: string; context: RequestContext; } interface Decision { requestId: string; effect: 'allow' | 'deny'; reasonCode: string; matchedPolicies: string[]; obligations: Obligation[]; fieldFilter?: FieldFilter; expiresAt?: string; policyVersion: string; }

4.1 Conditions

Subject and scope

subjectIs, roleIs, ownerIsSubject, resourceInWorkspace, fieldsWithin

Context

purposeIs, approvalPresent, timeBetween, deviceTrustAtLeast, mfaRecent

Input constraints

recipientCountAtMost, amountAtMost, domainIs, queryContainsNoSecrets

Obligations

requireConfirmation, previewChanges, redactFields, writeReceipt, requireHumanReview

policy.allow({ id: 'john-send-own-mail', principal: subject('user:john'), operation: 'execute', resource: capability('mail.send'), scope: { mailbox: 'user:john' }, conditions: [ purposeIs('job_outreach'), recipientCountAtMost(20), approvalPresent('this_action') ], obligations: [writeReceipt(), requireConfirmation('explicit')] });

5. Policy evaluation algorithm

The evaluator must return the same result for the same policy set, request, and context. A deny result is the default.

Normalize request
Check identity
Match rules
Resolve conflict
Apply obligations
Return decision
  1. Validate the request schema and reject unknown operations or malformed resource references.
  2. Require an authenticated subject for every operation except explicitly public discovery.
  3. Resolve the resource and its current version.
  4. Collect policies matching subject, operation, resource, scope, and validity window.
  5. Evaluate conditions against the current request context and proposed input.
  6. Apply deny-overrides: a matching deny wins over an allow at the same effective priority.
  7. Apply priority ordering. A more specific rule outranks a broader rule only when the policy explicitly declares its priority.
  8. Return allow only when at least one applicable allow remains and no applicable deny overrides it.
  9. Attach obligations, field filters, expiration, policy version, and machine-readable reason code.
Fail-closed behavior: unavailable policy services, stale policy versions, invalid input, missing identity, and ambiguous rule conflicts produce denial for protected resources.

6. Reference API

The library is split into pure core contracts and adapters. A host application can use only the parts it needs.

const cup = createCapabilityUI({ registry, policy: policyEngine, receipts: receiptSink, clock: systemClock, nonce: nonceProvider }); // 1. Register typed resources and tools. cup.register(calendarResource); cup.register(createEventCapability); // 2. Ask for the safe view given the user's goal. const view = await cup.project({ subject: john, goal: 'plan a team meeting', context: requestContext }); // 3. Give only this view to the agent or renderer. const response = await agent.compose({ goal, authorizedView: view }); // 4. Re-authorize the exact action at the side-effect boundary. const receipt = await cup.execute({ subject: john, capability: 'calendar.create_event', input: response.action.input, confirmation: response.action.confirmation, context: requestContext });

6.1 Public interfaces

Registry

register(resource) · get(id,version) · listDiscoverable(request) · validate(id,input)

Policy engine

authorize(request) · explain(request) · policyVersion() · invalidate(cacheKey)

Projector

project(request) · redact(resource,decision) · buildActionSchema(capability)

Executor

prepare(request) · confirm(token) · execute(request) · reverse(receiptId)

7. Authorized projection

The projector converts policy decisions into an agent-safe document. It includes only what the consumer may see and do.

interface AuthorizedView { viewId: string; subjectId: string; purpose?: string; generatedAt: string; policyVersion: string; resources: AuthorizedResource[]; globalObligations: Obligation[]; } interface AuthorizedResource { ref: ResourceRef; visibility: 'listed' | 'inspectable' | 'readable' | 'usable'; schema?: JsonSchema; data?: unknown; fields?: FieldPermission[]; capabilities: AuthorizedCapability[]; } interface AuthorizedCapability { id: string; inputSchema: JsonSchema; outputSchema: JsonSchema; risk: Risk; sideEffects: SideEffect[]; obligations: Obligation[]; actionToken: string; // opaque, short-lived, audience-bound }

7.1 Field-level permissions

Field filters are applied after authorization and before projection. A caller may read a contact record while receiving only name and organization. Redaction must be structural, not a string replacement that can leak through derived fields.

Projection stateAgent receivesRenderer may show
HiddenNothingNothing
ListedStable ref and safe labelExistence and label
InspectableSchema, risk, side effectsForm structure and warnings
ReadableFiltered contentsAuthorized fields
UsableShort-lived action tokenAction control with obligations

8. Execution and confirmation

Execution is a separate protocol from projection. The executor trusts neither a UI event nor an agent-produced payload.

Prepare

Validate capability ID, input schema, subject, purpose, current resource version, and action-token audience.

Confirm

For an explicit obligation, show the normalized action and side effects. Bind confirmation to the exact input hash.

Execute

Re-authorize immediately before calling the adapter. Enforce scope and input constraints again.

Receipt

Record policy version, input hash, confirmation, result, actor, and reversal reference.

Retry

Use idempotency keys for capabilities that can create duplicate external effects.

Reverse

Expose a separate reversal capability. Reversibility is never inferred from the original operation.

interface ExecutionRequest { subject: Subject; capability: string; input: unknown; actionToken?: string; confirmation?: Confirmation; idempotencyKey?: string; context: RequestContext; } interface Receipt { id: string; status: 'succeeded' | 'failed' | 'denied' | 'pending'; actor: SubjectRef; capability: string; resourceRefs: ResourceRef[]; inputHash: string; decision: Decision; confirmation?: Confirmation; resultSummary?: unknown; reversibleBy?: string; createdAt: string; }

9. Security model

Threats addressed

  • Prompt injection attempting to reveal hidden resources.
  • Agent hallucination of unavailable tools or fields.
  • Forged UI events and modified client payloads.
  • Confused-deputy actions through delegated agents.
  • Stale permissions after revocation.
  • Over-broad approval reused for another purpose.
  • Data leakage through derived output or error messages.

Required controls

  • Backend enforcement for every protected operation.
  • Opaque, audience-bound, short-lived action tokens.
  • Default deny and deny-overrides conflict resolution.
  • Policy version and resource version checks.
  • Input and output schema validation.
  • Purpose binding, expiration, and approval nonce binding.
  • Structured receipts with tamper-evident storage.

9.1 Trust boundaries

Identity provider
Host app
CUP policy core
Agent / renderer
CUP executor
External adapter

Boundary rule: agents and renderers are untrusted consumers of projections. External adapters are trusted only to perform the operation that the executor has already authorized.

10. Adapter contracts

Adapters isolate vendor and framework choices from the protocol.

IdentityAdapter

Resolves authenticated subjects and attributes. It cannot grant resource permissions.

ResourceAdapter

Loads current resource metadata, versions, schemas, and data. It applies ownership rules supplied by the host.

PolicyAdapter

Evaluates requests locally or through OPA, Cedar, a database, or a custom evaluator.

CapabilityAdapter

Executes a named operation after the executor has authorized it. It receives normalized input only.

RendererAdapter

Converts authorized schemas into React, Web Components, native controls, voice prompts, or other presentation.

ReceiptSink

Writes receipts to an append-only store and supports lookup by request, actor, resource, and capability.

interface CapabilityAdapter { capabilityId: string; invoke(input: unknown, ctx: ExecutionContext): Promise<unknown>; } interface RendererAdapter<T> { render(view: AuthorizedView, target: T): RenderResult; } interface ReceiptSink { append(receipt: Receipt): Promise<void>; find(query: ReceiptQuery): Promise<Receipt[]>; }

11. Conformance and testing

An implementation is CUP-conformant only when it passes semantic tests. Matching method names is insufficient.

Core conformance

  • Unknown operations are rejected.
  • No matching allow produces deny.
  • Matching deny overrides allow at equal priority.
  • Expiration and revocation take effect at execution.
  • Discovery never leaks protected metadata.
  • Field filters remove unauthorized values structurally.
  • Projection and execution produce the same policy version check.

Execution conformance

  • Modified input invalidates confirmation.
  • Wrong audience invalidates action tokens.
  • Missing idempotency key is rejected when required.
  • Adapters cannot be called after a denied decision.
  • Every required receipt is written before success returns.
  • Reversal uses a distinct authorized capability.
  • Policy failures fail closed for protected resources.

11.1 Reference fixture

test('other user cannot send from John mailbox', async () => { const decision = await engine.authorize({ subject: otherUser, operation: 'execute', resource: ref('mail.send'), proposedInput: { mailbox: 'user:john', recipients: ['[email protected]'] }, purpose: 'job_outreach', context }); expect(decision.effect).toBe('deny'); expect(adapter.invoke).not.toHaveBeenCalled(); });

12. Release plan and open decisions

v0.1 · semantic core

TypeScript types, pure evaluator, JSON Schema validation, projections, action tokens, receipts, and conformance fixtures.

v0.2 · production adapters

PostgreSQL resource adapter, OPA or Cedar bridge, React renderer contract, Web Components renderer, and policy simulator.

v0.3 · delegation

Delegation chains, capability attenuation, organization policy, approval workflows, and cross-service receipts.

Decisions requiring field experience

QuestionCurrent proposalEvidence needed
Policy languageTyped builder API plus portable JSON representationInteroperability tests across evaluators
Action tokensOpaque, short-lived, audience-bound tokensOperational latency and revocation requirements
Policy conflictDeny-overrides with explicit priorityEnterprise policy authoring studies
UI contractAuthorizedView independent of visual frameworkReact, native, voice, and accessibility implementations
Receipt storageAppend-only sink owned by host applicationAudit, privacy, retention, and redaction requirements
Recommended first build: implement the pure evaluator and execution guard before building a broad renderer ecosystem. The protocol’s value depends on the enforcement contract, not the visual novelty of the generated interface.

13. MCP server profile

CUP is transport-neutral at its core. MCP becomes an official server profile: a mapping from CUP resources and capabilities to MCP resources, resource templates, tools, prompts, and notifications. The MCP adapter exposes only the authorized projection for the current session.

Design decision: MCP is an interoperability surface, not the permission model. CUP evaluates authorization before an MCP item is listed, read, subscribed to, or called.

13.1 Protocol mapping

CUP conceptMCP surfaceAdapter rule
Resource with discoverresources/listReturn only discoverable resources; omit hidden resources rather than returning an authorization error that leaks existence.
Inspectable resourceresources/templates/list or resource metadataExpose schema, labels, risk, and side effects only when inspection is allowed.
Readable data resourceresources/readResolve the URI, re-authorize the subject, and apply field-level redaction before returning contents.
Capability with executetools/list + tools/callList the tool only when inspectable. Call requires a fresh CUP decision for the exact arguments.
UI or workflow promptprompts/list + prompts/getPrompts are templates, not authority. Treat returned prompt text as untrusted input.
Resource changenotifications/resources/list_changedNotify about safe catalog changes without revealing hidden resource identifiers.

13.2 Server architecture

MCP client
MCP transport
CUP session adapter
Projection
Policy core
Host adapter

The server holds a session subject and context. Every request enters through the adapter, which converts MCP parameters into an AuthorizationRequest. The adapter never passes the MCP client directly to a host capability.

interface MCPServerProfile { name: string; protocolVersion: string; capabilities: { resources?: boolean; tools?: boolean; prompts?: boolean; subscriptions?: boolean; sampling?: boolean; }; session: MCPSession; listResources(): Promise<MCPResource[]>; readResource(uri: string): Promise<MCPContent[]>; listTools(): Promise<MCPTool[]>; callTool(name: string, args: unknown): Promise<MCPToolResult>; listPrompts(): Promise<MCPPrompt[]>; getPrompt(name: string, args: unknown): Promise<MCPPromptResult>; } interface MCPSession { id: string; subject: Subject; purpose?: string; transport: 'stdio' | 'streamable_http'; policyVersion: string; createdAt: string; expiresAt: string; }

13.3 Tool projection

A CUP capability maps to an MCP tool only when its input schema is representable as JSON Schema and its output can be converted into MCP content blocks. The adapter adds CUP metadata in an extension namespace so an MCP client can render safer controls when it understands the extension.

{ "name": "calendar_create_event", "description": "Create a calendar event", "inputSchema": { "type": "object", "required": ["title", "start", "end"] }, "_cup": { "capabilityId": "calendar.create_event", "risk": "medium", "sideEffects": ["external_commitment"], "confirmation": "explicit", "reversibility": "reversible", "policyVersion": "policy-42" } }

13.4 MCP call sequence

  1. Complete MCP initialization and bind the authenticated subject to the session.
  2. Build an authorized CUP projection for the session purpose.
  3. Answer tools/list from that projection. Do not expose the host registry wholesale.
  4. On tools/call, resolve the MCP name to one immutable capability ID and current version.
  5. Validate arguments against the capability schema and compute an input hash.
  6. Run CUP authorization against the current subject, scope, purpose, approval, and input.
  7. Return a structured denial if policy rejects the call. Do not invoke the host adapter.
  8. If an obligation requires confirmation, return a preview-oriented result or use a host confirmation channel; never treat the MCP call itself as consent.
  9. Invoke the host adapter only after enforcement passes, then write a receipt before returning success.

13.5 Resources, templates, and subscriptions

Resource URIs must be stable, opaque where appropriate, and safe to log. URI templates must not allow a client to widen scope by editing path parameters. For subscriptions, the server rechecks authorization on every update and sends a removal or invalidation notification when access is revoked.

Streamable HTTP

Use for remote servers. Bind the MCP session to authenticated identity, origin, audience, and expiration. Do not accept a bearer token supplied inside tool arguments.

stdio

Use for local servers. Pass a filtered environment, keep the host policy authority outside the child process, and treat child output as untrusted content.

Sampling

Server-requested model calls are separate capabilities. Apply rate, model, token, and tool-loop limits. A server cannot use sampling to bypass CUP policy.

Delegation

Represent the MCP server as a distinct principal. Delegated authority is attenuated to the intersection of the user grant, server grant, and capability scope.

13.6 MCP conformance tests

  • Hidden CUP resources never appear in resources/list.
  • Unauthorized fields never appear in resources/read.
  • A tool absent from the authorized projection cannot be called by name.
  • Changing tool arguments invalidates a prior confirmation.
  • Revoking access removes or invalidates active subscriptions.
  • Every MCP call performs a fresh CUP decision.
  • MCP error text contains no protected resource contents.
  • Prompt content cannot grant a capability or alter policy.
  • Sampling cannot invoke tools outside the server’s attenuation set.
  • Receipts link the MCP session, request ID, capability, and host result.
Result: the same CUP library can power a web renderer, a native renderer, a voice interface, or an MCP server. Each surface receives a projection and uses the same execution guard.

14. Usage examples

These examples show how an application, agent, renderer, and MCP client share one capability contract. Each example begins with a concrete product problem, identifies the library boundary involved, and then shows a representative implementation.

Choose an example below according to the product problem you are solving. Each card explains what the pattern is for and which part of the library it exercises.

14.1 Minimal application setup

What it is: The smallest host application that creates a registry, policy engine, and receipt sink.
Purpose: Give a developer a starting point for adopting CUP without changing the existing database or agent.
How CUP is applied: The host registers a resource with an identity, schema, owner, and version. It starts from deny-by-default, then adds one explicit policy allowing John to read that resource. Later projections can expose only this authorized resource, while receipts provide the audit boundary.
import { createCapabilityUI, memoryRegistry, denyByDefault } from '@capability-ui/core'; const cup = createCapabilityUI({ registry: memoryRegistry(), policy: denyByDefault(), receipts: postgresReceipts(process.env.DATABASE_URL), clock: systemClock() }); cup.register({ id: 'notes.john', type: 'data', version: '1.0', sensitivity: 'personal', owner: 'user:john', schema: noteSchema }); cup.policy.allow({ id: 'john-read-notes', principal: subject('user:john'), operation: 'read', resource: resource('notes.john'), scope: { fields: ['id', 'title', 'body'] }, priority: 100 });

14.2 Expose a database as filtered data

What it is: A CRM resource whose rows and fields are constrained by workspace and role.
Purpose: Show that permission applies to the returned data, not merely to a database endpoint.
How CUP is applied: The resource adapter loads data only for the subject’s workspace. The policy matches the sales role, binds the workspace to a subject attribute, and attaches a redaction obligation. CUP evaluates the request first, then removes unauthorized fields before the renderer or agent receives the result.
cup.register(dataResource({ id: 'crm.contacts', version: '3.2', sensitivity: 'confidential', schema: contactSchema, read: async ({ scope }) => db.contacts.findMany({ where: { workspaceId: scope.workspaceId }, select: { id: true, name: true, company: true, email: true } }) })); cup.policy.allow({ id: 'sales-read-team-contacts', principal: role('sales'), operation: 'read', resource: resource('crm.contacts'), scope: { workspaceId: fromSubject('workspaceId') }, obligations: [redact(['phone', 'personalEmail'])] }); const view = await cup.project({ subject: alice, goal: 'find contacts for the launch', context: { workspaceId: 'acme', purpose: 'sales' } });

14.3 Turn a capability into a generated form

What it is: A calendar capability rendered into a form from its input schema.
Purpose: Let a renderer create useful controls without a hand-built screen for every action.
How CUP is applied: CUP projects the capability only when the user may inspect or execute it. The renderer reads the authorized input schema, builds controls, displays the declared external commitment, and attaches the confirmation obligation. The renderer creates presentation; CUP still decides whether the submitted event may be created.
const capability = defineCapability({ id: 'calendar.create_event', operation: 'execute', inputSchema: createEventSchema, outputSchema: eventSchema, sideEffects: ['external_commitment'], risk: 'medium', confirmation: 'explicit', reversibility: 'reversible' }); cup.register(capability); const view = await cup.project({ subject: john, goal: 'schedule a team meeting' }); const action = view.resources .flatMap(r => r.capabilities) .find(c => c.id === 'calendar.create_event'); // A renderer chooses the visual form from action.inputSchema. return renderCapabilityForm(action, { showSideEffects: true, showConfirmation: action.obligations.includes('require_confirmation') });

14.4 Protect a side-effecting tool

What it is: An email sender with high risk, explicit confirmation, and idempotency.
Purpose: Demonstrate the safe path for actions that contact other people or create external commitments.
How CUP is applied: The capability declares its side effect, risk, confirmation mode, and need for an idempotency key. CUP checks the purpose, recipient limit, approval, schema, and exact input hash immediately before invoking the mail provider. If any condition changes, execution is denied and the adapter is never called.
cup.register(defineCapability({ id: 'mail.send', operation: 'execute', inputSchema: mailSchema, outputSchema: receiptSchema, sideEffects: ['external_message'], risk: 'high', confirmation: 'explicit', idempotency: 'required', handler: async (input, ctx) => mailProvider.send(input, ctx.idempotencyKey) })); cup.policy.allow({ id: 'john-send-job-outreach', principal: subject('user:john'), operation: 'execute', resource: capability('mail.send'), conditions: [purposeIs('job_outreach'), recipientCountAtMost(20)], obligations: [requireConfirmation('explicit'), writeReceipt()] }); // The executor, not the button, is the security boundary. const receipt = await cup.execute({ subject: john, capability: 'mail.send', input: { from: '[email protected]', to: recipients, body }, purpose: 'job_outreach', confirmation: userConfirmation, idempotencyKey: crypto.randomUUID(), context });

14.5 Build a read-only personal assistant

What it is: A personal assistant limited to an authorized view of notes and briefing data.
Purpose: Give an agent enough information to be useful while withholding the user’s full registry and write actions.
How CUP is applied: The user’s goal and purpose become projection inputs. CUP returns readable note fields and only the capabilities allowed for a personal briefing. The agent receives that bounded view instead of the host registry. Even a read operation passes through CUP so field filters and current policy remain effective.
const authorized = await cup.project({ subject: assistantFor(john), goal: 'prepare my morning briefing', context: { purpose: 'personal_briefing', channel: 'telegram' } }); // The agent sees schemas and permitted actions, not the whole registry. const answer = await agent.run({ instruction: 'Prepare the briefing from the authorized view.', tools: authorized.resources.flatMap(r => r.capabilities), data: authorized.resources.filter(r => r.visibility === 'readable') }); // Read actions still pass through the executor for field redaction. const notes = await cup.read({ subject: assistantFor(john), resource: 'notes.john', fields: ['title', 'body'], context: { purpose: 'personal_briefing' } });

14.6 Share a capability with attenuation

What it is: A user grants an agent a narrower version of a search capability.
Purpose: Demonstrate safe delegation without handing the agent the user’s full authority.
How CUP is applied: CUP records the delegator, recipient, capability, purpose, expiration, fields, and allowed operations. The effective policy is the intersection of the original grant and the attenuation. At execution, CUP verifies the delegation chain and refuses any input that exceeds those limits.
const grant = await cup.delegate({ from: john, to: subject('agent:research'), capability: 'crm.contacts.search', attenuation: { scope: { workspaceId: 'acme', fields: ['name', 'company'] }, purpose: 'market_research', expiresIn: '30m', operations: ['execute'] } }); // Effective authority is the intersection of all grants. // The delegated agent cannot widen fields, purpose, time, or operations. await cup.execute({ subject: subject('agent:research'), capability: 'crm.contacts.search', delegation: grant, input: { query: 'healthcare startups' }, context });

14.7 Publish CUP capabilities through MCP

What it is: A CUP host exposes its authorized resources and tools as an MCP server.
Purpose: Make the library interoperable with MCP clients while keeping CUP as the permission authority.
How CUP is applied: The server authenticates the MCP session, builds a subject-specific projection, and maps only that projection to MCP resources and tools. On tools/call, the adapter resolves the MCP name, validates arguments, invokes cup.execute(), and writes a receipt. MCP changes the wire format; it does not bypass CUP.
import { createMCPServer } from '@capability-ui/mcp'; const server = createMCPServer({ name: 'john-workspace', cup, authenticate: async request => oidc.verify(request), transports: ['streamable_http', 'stdio'] }); // resources/list, resources/read, tools/list, and tools/call // are generated from the session's authorized CUP projection. await server.listen({ http: { port: 8787, path: '/mcp' } });

The MCP server must not call registry.listAll() and pass the result to the client. Its handlers call cup.project() for listing and cup.execute() for invocation.

14.8 Consume an MCP server through the same library

What it is: A local CUP host mounts tools from another MCP server under a namespace.
Purpose: Combine local policy with remote capabilities without treating a remote tool as trusted by default.
How CUP is applied: The MCP client adapter imports remote schemas as namespaced capabilities. The local policy engine decides whether they are discoverable, inspectable, or executable for this subject and purpose. The local executor rechecks the request and constrains the remote call before forwarding it.
import { connectMCP } from '@capability-ui/mcp-client'; const remote = await connectMCP({ url: 'https://tools.example.com/mcp', subject: john, auth: oidcToken, purpose: 'team_planning' }); // Remote tools enter the local registry as namespaced capabilities. await cup.mount(remote, { namespace: 'company_tools' }); const view = await cup.project({ subject: john, goal: 'plan the launch' }); const result = await cup.execute({ subject: john, capability: 'company_tools.release.create_plan', input: { project: 'launch-2027' }, context });

14.9 Subscribe to authorized changes

What it is: A client subscribes to project updates and rechecks access for every event.
Purpose: Prevent a previously authorized subscription from becoming a data leak after revocation.
How CUP is applied: CUP authorizes the initial subscription, but does not treat that decision as permanent. Each event is checked against the current policy and resource version. Allowed updates reach the renderer; revoked access produces removal or invalidation instead of leaking the new data.
const subscription = await cup.subscribe({ subject: john, resource: 'projects.acme', events: ['updated'], context }); subscription.on('event', async event => { // Re-authorize every event. Revocation produces 'access_removed'. const decision = await cup.authorize({ subject: john, operation: 'read', resource: event.resource, context }); if (decision.effect === 'allow') renderer.update(event.data, decision); else renderer.remove(event.resource); });

14.10 Test the entire boundary

What it is: An integration test that attempts to execute a hidden, unauthorized mail capability.
Purpose: Turn the security promise into an executable contract.
How CUP is applied: The test checks both layers: projection does not disclose the capability, and execution independently evaluates the forged request. The expected denial is meaningful only when the mail adapter remains untouched, proving that the enforcement boundary is outside the generated UI.
it('cannot execute a hidden or unauthorized capability', async () => { const view = await cup.project({ subject: otherUser, goal: 'send mail' }); expect(view.resources.some(r => r.ref.id === 'mail.send')).toBe(false); await expect(cup.execute({ subject: otherUser, capability: 'mail.send', input: { to: ['[email protected]'], body: 'hello' }, context })).rejects.toMatchObject({ code: 'CUP_NOT_AUTHORIZED' }); expect(mailProvider.send).not.toHaveBeenCalled(); });

14.11 Read authorized data at runtime

What it is: A user asks an agent to search and read a bounded set of records.
Purpose: Show the runtime path after setup: discover what exists, inspect the schema, query within scope, and receive redacted data.
How CUP is applied: The agent never reads the database directly. CUP authorizes discovery and reading separately, constrains the query fields and workspace, applies pagination limits, and returns a receipt for the read operation.
const visible = await cup.discover({ subject: john, purpose: 'personal_search', context }); const contacts = visible.find(r => r.ref.id === 'crm.contacts'); const page = await cup.read({ subject: john, resource: 'crm.contacts', query: { search: 'Acme', cursor: null, limit: 25 }, fields: ['id', 'name', 'company'], scope: { workspaceId: 'acme' }, purpose: 'personal_search', context }); console.log(page.items); // filtered records only console.log(page.nextCursor); // opaque cursor, if more data exists console.log(page.receiptId); // audit reference

14.12 Call a capability through prepare, confirm, execute

What it is: A calendar action is selected from the authorized view and executed after the user reviews it.
Purpose: Show the complete call protocol instead of stopping at capability registration.
How CUP is applied: prepare() validates the schema and produces a normalized preview. The UI displays the exact side effect. Confirmation is bound to that normalized input. execute() rechecks policy and rejects a changed payload.
const prepared = await cup.prepare({ subject: john, capability: 'calendar.create_event', input: { title: 'Launch review', start, end, attendees }, purpose: 'team_planning', context }); // Renderer shows prepared.preview and prepared.obligations. const confirmation = await user.confirm({ title: prepared.preview.title, changes: prepared.preview.changes, risks: prepared.obligations }); const receipt = await cup.execute({ ...prepared.request, confirmation, // bound to input hash idempotencyKey: 'launch-review-v1' }); if (receipt.status === 'succeeded') showSuccess(receipt); else showActionableError(receipt.decision.reason);

14.13 A single agent with a bounded action loop

What it is: An agent plans a multi-step task, such as preparing a meeting brief, using only capabilities in its authorized projection.
Purpose: Show how CUP constrains an agent throughout a task rather than authorizing the entire task once at the beginning.
How CUP is applied: The host creates a projection for the goal. The agent proposes one step at a time. Each step is checked against the current policy, resource version, purpose, and budget. A denied step is returned to the agent as a typed result, not silently substituted with a broader capability.
const view = await cup.project({ subject: assistantFor(john), goal: 'prepare tomorrow meeting brief', context: { purpose: 'meeting_brief', budget: { reads: 20, writes: 0 } } }); let state = { goal, facts: [], receipts: [] }; for (let step = 0; step < 8; step++) { const proposal = await agent.planNext({ state, authorizedView: view }); if (proposal.kind === 'final') break; const result = await cup.execute({ subject: assistantFor(john), capability: proposal.capability, input: proposal.input, purpose: 'meeting_brief', context, budget: state.budget }); state = reduce(state, result); if (result.status === 'denied') { state = addConstraint(state, result.decision.reasonCode); } } return agent.summarize(state);

14.14 Multi-agent research team

What it is: A coordinator agent delegates narrow research tasks to specialist agents and combines their outputs.
Purpose: Demonstrate that agents can collaborate without sharing the user’s entire data set or unrestricted tools.
How CUP is applied: The coordinator receives a broad but bounded authority. It creates attenuated grants for search-only workers with separate purposes and expiration times. Each worker is a distinct principal. Their outputs are treated as untrusted data, and the coordinator cannot grant permissions it does not possess.
const coordinator = subject('agent:research-coordinator'); const workers = await Promise.all([ cup.delegate({ from: john, to: subject('agent:market-worker'), capability: 'web.search', attenuation: { purpose: 'market_scan', expiresIn: '15m', operations: ['execute'], scope: { domains: ['example.com', 'public-data.org'] } }}), cup.delegate({ from: john, to: subject('agent:notes-worker'), capability: 'notes.read', attenuation: { purpose: 'market_scan', expiresIn: '15m', operations: ['read'], scope: { fields: ['title', 'body'], notebook: 'research' } }}) ]); const findings = await Promise.all(workers.map(grant => worker.run({ grant, goal: 'find evidence for the market scan', call: (capability, input) => cup.execute({ subject: grant.to, delegation: grant, capability, input, purpose: 'market_scan', context }) }))); return coordinator.synthesize({ findings: sanitizeUntrusted(findings) });

14.15 Agent using a remote MCP server

What it is: An agent uses a remote MCP server to create a project plan while CUP remains the local authority for what the agent may invoke.
Purpose: Show how MCP fits into an agent loop without turning remote tool discovery into automatic trust.
How CUP is applied: Remote tools are mounted under a namespace, inspected, and projected like local capabilities. The agent sees only permitted tools. Every remote call passes local policy, input validation, confirmation rules, and receipt creation before the MCP adapter forwards it.
const remote = await connectMCP({ url: 'https://planning.example/mcp', auth: oidcToken, subject: assistantFor(john), purpose: 'project_planning' }); await cup.mount(remote, { namespace: 'planning_remote' }); const view = await cup.project({ subject: assistantFor(john), goal: 'create a launch plan', context: { purpose: 'project_planning' } }); const plan = await agent.plan({ goal, tools: view.capabilities }); for (const action of plan.actions) { const receipt = await cup.execute({ subject: assistantFor(john), capability: action.capability, input: action.input, purpose: 'project_planning', context }); if (receipt.status !== 'succeeded') return recoverFromDecision(receipt); }

14.16 Adaptive interface for different principals

What it is: The same product renders different controls for an owner, a read-only collaborator, and a public visitor.
Purpose: Show the practical GenUI result of CUP: one resource model, many interfaces, with differences driven by policy rather than hard-coded role screens.
How CUP is applied: Each principal receives a different projection. The renderer displays read controls, edit controls, or an existence-only summary according to the returned capabilities. If a user changes roles while the page is open, the next execution is checked against the new policy.
const view = await cup.project({ subject: currentSubject, goal: 'work with project acme', context }); return render(view, { resource: 'projects.acme', componentFor(resource) { if (resource.capabilities.some(c => c.id === 'projects.update')) return ProjectEditor; if (resource.visibility === 'readable') return ProjectReader; if (resource.visibility === 'listed') return ProjectSummary; return null; } });
Implementation rule: every example uses the same core sequence: register, authorize, project, compose, confirm when required, re-authorize, execute, and receipt. MCP changes the wire protocol, not that sequence.