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.

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

-
license - not tested
Not graded
quality - not tested
B
maintenance

Maintenance

Maintainers
Response time
Release cycle
Releases (12mo)
Commit activity

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

  • An AI concierge that turns static forms into adaptive AI conversations. From any MCP client.

  • Create and wire up contact forms from your coding agent. Forms, snippets, and submissions.

  • Phone, SMS & email for AI agents — one remote MCP endpoint, OAuth login, zero install.

View all MCP Connectors

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/mustafadeel/universal-components-agents'

If you have feedback or need assistance with the MCP directory API, please join our Discord server