Skip to main content
Add the Char agent to your web application using npm or a script tag.

Installation

Choose your preferred installation method:
Add the script to your HTML:
<script src="https://unpkg.com/@mcp-b/char/dist/web-component-standalone.iife.js" defer></script>

Basic Usage

Add the custom element to your page:
<char-agent></char-agent>
The agent renders a full chat interface.

Choosing Your Authentication Path

How you authenticate depends on your application architecture:
ArchitectureAuth MethodBest For
SPA (client-side auth)connect({ idToken, clientId })React, Vue, Angular with client-side IDP SDKs
SSR (server-side auth)connect({ ticketAuth })Next.js, Remix, Rails, Django, Laravel
Which should you use? If your users authenticate client-side (IDP SDK returns a JWT to the browser), use the SPA path. If your users authenticate server-side (session cookies, server-rendered pages), use the SSR path.

SPA Authentication (Client-Side)

For single-page applications where users authenticate client-side through your IDP’s SDK. Use the imperative connect() method to pass tokens securely. This keeps tokens out of the DOM, protecting them from session replay tools and error monitoring services.
Required: When using idToken, you must also pass clientId. This is your OIDC client ID (the aud claim in your ID token) and must be added to Allowed Client IDs in your Char dashboard.
import { useEffect, useRef } from "react";
import "@mcp-b/char/web-component";
import type { WebMCPAgentElement } from "@mcp-b/char/web-component";

// Your OIDC client ID (must be in your allowed_audiences list)
const CLIENT_ID = "your-oidc-client-id";

function App({ idToken }: { idToken: string }) {
  const agentRef = useRef<WebMCPAgentElement>(null);

  useEffect(() => {
    agentRef.current?.connect({ idToken, clientId: CLIENT_ID });
  }, [idToken]);

  return <char-agent ref={agentRef} />;
}
The connect() method stores tokens as JavaScript properties, not DOM attributes. This protects tokens from:
  • Session replay tools (FullStory, LogRocket)
  • Error monitoring (Sentry, DataDog)
  • DOM inspection and browser extensions
  • Page source visibility

SSR Authentication (Server-Side)

For server-side rendered applications where authentication happens on the server. Instead of passing your IDP’s JWT to the client, exchange it server-side for a short-lived ticket.

How It Works

Exchanging a Token for a Ticket

Your server exchanges the user’s IDP token for a short-lived ticket:
curl -X POST https://api.usechar.ai/api/auth/ticket \
  -H "Authorization: Bearer <IDP_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
    "org_id": "org_abc123",
    "client_id": "your-oidc-client-id"
  }'
ParameterRequiredDescription
org_idYes (SSR)Your Char organization ID (from the dashboard). Required for SSR requests since there’s no Origin header.
client_idYes (production)Your OIDC client ID. Must be in your allowed_audiences list.
Response:
{
  "ticket": "a1b2c3d4e5f6...",
  "userId": "user_123",
  "orgId": "org_456",
  "expiresAt": 1705315800000,
  "expiresIn": 60
}
Tickets are opaque hex strings, not JWTs. They’re single-use, short-lived (60 seconds), and validated server-side. The 60-second window is just for establishing the initial connection—once the WebSocket connects, the session persists indefinitely.

Framework Examples

// app/dashboard/page.tsx
import { getServerSession } from "next-auth";
import { redirect } from "next/navigation";
import { CharAgent } from "./char-agent";

// Your Char organization ID (from the dashboard)
const ORG_ID = "org_abc123";
// Your OIDC client ID (must be in your allowed_audiences list)
const CLIENT_ID = "your-oidc-client-id";

async function getTicketAuth(idToken: string) {
  const response = await fetch("https://api.usechar.ai/api/auth/ticket", {
    method: "POST",
    headers: {
      Authorization: `Bearer ${idToken}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      org_id: ORG_ID,      // Required for SSR (no Origin header)
      client_id: CLIENT_ID // Required for production
    }),
  });
  return response.json(); // { ticket, userId, orgId, expiresAt }
}

export default async function DashboardPage() {
  const session = await getServerSession();
  if (!session?.idToken) redirect("/login");

  const ticketAuth = await getTicketAuth(session.idToken);

  return (
    <main>
      <h1>Dashboard</h1>
      <CharAgent ticketAuth={ticketAuth} />
    </main>
  );
}
// app/dashboard/char-agent.tsx
"use client";
import { useEffect, useRef } from "react";
import "@mcp-b/char/web-component";
import type { WebMCPAgentElement, TicketAuth } from "@mcp-b/char/web-component";

export function CharAgent({ ticketAuth }: { ticketAuth: TicketAuth }) {
  const agentRef = useRef<WebMCPAgentElement>(null);

  useEffect(() => {
    agentRef.current?.connect({ ticketAuth });
  }, [ticketAuth]);

  return <char-agent ref={agentRef} />;
}

Why ticketAuth for SSR?

AspectidToken (SPA)ticketAuth (SSR)
IDP token exposed to browserYesNo
Token validationOn every connection (JWKS fetch)Once server-side
Connection windowToken’s exp claim (often hours)60 seconds to connect
Best forSPAs with client-side authSSR apps with server sessions
The ticket pattern keeps your IDP token server-side while providing secure, short-lived authentication for the embedded agent. Once the agent connects, the WebSocket session persists—the 60-second limit only applies to establishing the initial connection.
Without idToken or ticketAuth, the agent requires dev-mode with an API key for anonymous mode (localhost development only). See Deployment Tiers for details.

Controlling Open State

Open or close the agent programmatically:
const agent = document.querySelector("char-agent");

// Open the agent
agent.open = true;

// Close the agent
agent.open = false;
In React with state:
import { useState, useEffect, useRef } from "react";
import "@mcp-b/char/web-component";
import type { WebMCPAgentElement } from "@mcp-b/char/web-component";

// Your OIDC client ID (must be in your allowed_audiences list)
const CLIENT_ID = "your-oidc-client-id";

function App({ idToken }: { idToken: string }) {
  const [isOpen, setIsOpen] = useState(false);
  const agentRef = useRef<WebMCPAgentElement>(null);

  useEffect(() => {
    agentRef.current?.connect({ idToken, clientId: CLIENT_ID });
  }, [idToken]);

  return (
    <>
      <button onClick={() => setIsOpen(true)}>Open Agent</button>
      <char-agent ref={agentRef} open={isOpen} />
    </>
  );
}

Development Mode (Anonymous)

For local development without IDP setup, use your own API key via dev-mode:
<char-agent dev-mode='{"anthropicApiKey":"sk-ant-..."}'></char-agent>
This runs in anonymous mode with full Durable Object persistence—messages persist across page refreshes.
Anonymous mode only works from localhost origins. Never embed API keys in production code—they’re visible to users.

Performance Tips

Script Loading

The defer attribute ensures the script loads in parallel without blocking page rendering:
<!-- Recommended: defer loads async, executes after DOM ready -->
<script src="https://unpkg.com/@mcp-b/char/dist/web-component-standalone.iife.js" defer></script>

Preconnect Hint

Speed up loading by adding a preconnect hint in your <head>:
<link rel="preconnect" href="https://unpkg.com" crossorigin>

Bundle Sizes

BuildSizeGzipped
ESM (npm)~560 KB~115 KB
Standalone (script tag)~2.2 MB~400 KB
The standalone build includes React for zero-dependency embedding. The ESM build is smaller when your app already uses React.

Security Best Practices

Token Handling

DoDon’t
Use connect({ idToken, clientId }) for SPAsPut JWTs in HTML attributes
Use connect({ ticketAuth }) for SSR appsPass your IDP’s JWT from server to client
Configure allowed_audiences in dashboardSkip audience validation
Validate tokens on every requestStore tokens in localStorage (use memory or cookies with httpOnly)
Set appropriate token TTLsUse long-lived tickets (max 120s)

Minimizing Token Exposure

For SPAs: The connect() method stores tokens as JavaScript properties, preventing them from appearing in:
  • Page source (view-source)
  • Browser dev tools Elements panel
  • Server logs (if HTML is logged)
  • Session replay tools (FullStory, LogRocket)
For SSR: Tickets are preferable to JWTs because:
  • They’re opaque (not JWTs, just hex strings)
  • They’re scoped to Char only (not your entire IDP)
  • They have a narrow connection window (60s) and are single-use, limiting replay attacks
  • Your IDP token never leaves the server

Ticket Refresh (SSR)

Tickets must be used within 60 seconds, but once the WebSocket connects, the session persists indefinitely. You only need to refresh tickets when the WebSocket disconnects (network interruption, laptop sleep, browser throttling):

Next steps

See also