Capability graph · agent trust layer

Build the product once.
Let their agents use it too.

Launchpad is a small project-management app. Its six operations are defined once, with one schema, one middleware chain and one run() — then projected to the browser, to progressive-enhancement forms, to in-page agents, to signed remote callers, and to MCP tools at /mcp. Same business rules, by construction.

browser

The product

<Form capability> posts to the capability endpoint — with a form-encoded fallback when JavaScript never arrives.

server

The loader

invokeCapability() from SSR runs the same validation and middleware chain, no HTTP round trip.

webmcp

The tab

An in-browser agent gets the capability as a page tool, acting as the signed-in user in their own session.

http

The network

A remote agent signs its request with RFC 9421 and gets a verified identity, policy checks, and an audit event.

mcp

The tool list

POST /mcp serves the same capabilities as MCP tools over stateless Streamable HTTP — minus the destructive one.

src/capabilities/projects-archive.ts
export default defineCapability({
  title: "Archive project",
  input: { type: "object", properties: { projectId: { type: "string" } } },
  effect: "destructive",   // → webmcp/mcp exposure is a build error
  expose: { http: true }, // → prepare/commit + a human approval
  async run({ input }) { … },
});

The security model is the product

Four things the framework refuses to let you get wrong

Private by default

No loader and no API route is ever inferred as a tool. A capability without an explicit expose is unreachable over the network.

Destructive is gated, not annotated

A destructive capability cannot be exposed to WebMCP or MCP at all, and its HTTP dispatch is refused until a person approves the proposal.

Identity is verified, not claimed

Web Bot Auth checks an Ed25519 signature over the request before context.agent exists. Unverifiable means null, never a partial identity.

Widening shows up in review

pracht plan marks a new exposure, a dropped required field or a raised bound with a ! — the diff answers 'did this let agents reach more?'

Still a web framework

Every route renders the way it should

ssg

This page

Pre-rendered at build. Zero server cost.

isg

Pricing

Wants hourly revalidation — see the note in routes.ts.

ssr

Dashboard

Per-request, personalised, always current.

spa

Settings

Client-only. Shell paints instantly, no SEO needed.

One manifest, one build, one deployment — and the same manifest is where the capability graph, the trust config and the machine-enforced constraints live.