Skip to main content
Glama
mustafadeel

@auth0/agent-components

by mustafadeel

@auth0/agent-components — Auth0 Universal Components for Agents

Turn an Auth0 Form into an MCP App — an interactive UI a Model Context Protocol server serves to an MCP client, rendered in a sandboxed iframe. This is Auth0 Universal Components for Agents: any Auth0 Form becomes a drop-in, agent-invokable UI, no Auth0 Action required.

Status: proof of concept. Works end-to-end against the official MCP Apps capability (SEP-1865), the MCP Inspector, and CopilotKit (see examples/copilotkit-poc).

Why

Auth0 Forms normally render only inside an Auth0 Action during Universal Login. The dx-flows-sdk decoupled that: a form can be embedded via the browser bundle outside a login redirect. MCP Apps let a server hand a client interactive HTML to render in a sandboxed iframe. Put them together and an agent can surface a real, fully-functional Auth0 Form — sign-up, consent, profile, payment — as one of its tools.

Related MCP server: Homespun

How it works

MCP host (e.g. MCP Inspector, CopilotKit)
 └─ sandboxed iframe  ← our ui:// HTML resource (text/html;profile=mcp-app)
      ├─ <script src="https://<tenant>/forms/sdk/forms.js">
      ├─ Auth0Forms.embed(formId, "#root", { fields: { session_token } })
      │    └─ form submits to its OWN Auth0 backend (/forms/api/...)  ← we never see the data
      └─ bridge: on af-submitForm-success → app.updateModelContext({ status: "completed" })
                                          → app.requestTeardown()   (close the app view)
                  on af-redirect          → app.openLink(url)       (never navigates the iframe)

The form owns its data. The Auth0 Form submits natively to its Auth0 backend. The MCP layer never reads or transmits field values — it reports only a completion status (completed / cancelled / errored, plus an optional redirect target) so the agent knows the user finished.

Packages

Package

What it is

packages/agent-components

@auth0/agent-components — register Auth0 Forms as MCP Apps on any McpServer.

packages/mcp-apps-middleware-auth

@auth0/mcp-apps-middleware-auth — adds authentication to CopilotKit's @ag-ui/mcp-apps-middleware, which can't attach a credential to its MCP connections.

examples/poc-server

Runnable MCP server POC; drive it from the MCP Inspector. Includes scripts/discover-forms.mjs (tenant form discovery via the Auth0 CLI).

examples/copilotkit-poc

CopilotKit as the MCP client — real Auth0 login (@auth0/auth0-server-js) → the form renders and submits inline. End-to-end.

Usage

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { registerAuth0Forms } from "@auth0/agent-components";

const server = new McpServer({ name: "my-server", version: "1.0.0" });

await registerAuth0Forms(
  server,
  [
    {
      formId: "your-form-id",
      tenantOrigin: "https://your-tenant.us.auth0.com",
      // inputMode: "prefill",         // expose form fields as optional agent inputs
      // ui: { csp: { frameDomains: ["https://js.stripe.com"] } }, // for payment/social steps
      onComplete: (r) => console.log(r.formId, r.status),         // status only — no field data
    },
  ],
  { assumeUiSupport: true }, // register before connect; see "Registration timing"
);

// ...connect your transport (StreamableHTTP / stdio / SSE)

Each form registers:

  • a tool open_form_<slug> whose _meta.ui.resourceUri points at …

  • a ui://agent-components/<formId> resource returning the form-app HTML + a CSP block.

CSP

The host renders the resource under a strict default policy (default-src 'none'). @auth0/agent-components automatically allowlists your tenantOrigin in both:

  • csp.resourceDomains — so forms.js, styles, fonts, images load, and

  • csp.connectDomains — so the bundle's fetch to /forms/api/... works.

If a form step nests another origin's iframe (Stripe, a social provider, a captcha), declare it:

ui: { csp: { frameDomains: ["https://js.stripe.com", "https://hooks.stripe.com"] } }

Watch the browser console the first time you render a new form: any CSP violation tells you exactly which origin to add.

Session-backed forms (flow/router forms)

Forms with a FLOW/ROUTER node need an authenticated session — otherwise the step after the router fails with ERR_INVALID_FORM_SESSION. The mechanism (confirmed with the Forms team, verified end-to-end):

  1. Declare a hidden field on the form (e.g. session_token).

  2. The form's flow reads it — e.g. an Update User action with user_id: {{fields.session_token}}.

  3. The MCP server, acting as an OAuth resource server, verifies the caller's Auth0 token, resolves the user's sub, mints a short-lived trusted JWT for it (signed with a shared secret the Forms backend trusts), and injects that into the hidden field per request.

Mark the form session-aware:

{
  formId: "ap_...",
  session: { field: "session_token" },   // must match the form's hidden field
}

…and initialize the client with the server-scoped trust config + a resolver that reads the verified identity (verifier + subFromExtra come from the /auth subpath, backed by @auth0/auth0-api-js):

import { createAgentComponents } from "@auth0/agent-components";
import { subFromExtra } from "@auth0/agent-components/auth";

const agentComponents = createAgentComponents({
  tenantOrigin: "https://your-tenant.auth0.com",
  assumeUiSupport: true,
  sessionTrust: { secret: process.env.FORMS_TRUST_SECRET! }, // shared with the Forms backend
  resolveUserSub: subFromExtra,                    // returns the caller's `sub`, or undefined
});
await agentComponents.register(server, forms);

The POC server (examples/poc-server) wires the full flow: createAuth0Verifier (→ @auth0/auth0-api-js), a ProtectedResourceMetadataBuilder metadata endpoint, and requireBearerAuth — so a 401 + WWW-Authenticate challenge lets the client run the Auth0 OAuth flow. The token/sub/minted-JWT never enter MCP model context — the minted token rides embed().fields to the Auth0 Forms backend only.

Note: a completed Auth0 Forms journey is single-use today (re-submitting a spent journey returns ERR_INVALID_FORM_SESSION). Universal Portals EPIC 7 adds re-completable journeys. The MCP App completes once and reports status, so a fresh tool invocation gets a fresh journey — re-submission isn't part of the flow.

Registration timing

The MCP SDK forbids adding a capability after server.connect(transport). Two supported patterns:

  • Register before connect (simplest) with assumeUiSupport: true. Correct for UI-capable clients (MCP Inspector, Claude). This is what the POC does.

  • Dynamic per-client gating (assumeUiSupport: "auto", the default): call registerAuth0Forms from server.server.oninitialized so the client's capabilities are known — and construct the server with { capabilities: { tools: {}, resources: {} } }, or register one tool + one resource before connecting, so the post-connect registration doesn't try to add a new capability.

Develop

npm install
npm run build      # builds all packages (agent-components builds its bridge first)
npm test           # unit tests (form→tool, ui-template CSP, bridge status-only contract)

License

MIT

Related MCP Connectors

Related MCP Servers

  • A
    license
    A
    quality
    D
    maintenance
    Enables AI agents to render interactive user interfaces such as forms, dashboards, charts, tables, and wizards directly in MCP-compatible clients. Supports structured data collection and richer interactions beyond text responses.
    6
    MIT
  • F
    license
    Not graded
    quality
    C
    maintenance
    Enables authenticated MCP apps to expose and interact with Auth0 Forms, submitting form data directly to Auth0 while only completion status is returned to the agent.
    -