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.