Char - AI chat agent for React and web components. Drop-in widget with MCP tool support.
npm install @mcp-b/charAn Intercom-like AI chat widget with MCP tool support and voice mode. Drop it into your app and you're done.
Styles are isolated using Shadow DOM. Customize appearance by setting CSS variables on the host page.
This package provides the custom element in two formats:
| Export | Format | Use Case |
|--------|--------|----------|
| @mcp-b/char / @mcp-b/char/web-component | ESM web component | Bundlers, monorepo dev |
| @mcp-b/char/standalone | IIFE web component | tag embeds |
| Build | Size | Gzipped | Use Case |
|-------|------|---------|----------|
| ESM | ~560 KB | ~115 KB | Bundlers (React externalized) |
| Standalone IIFE | ~2.2 MB | ~400 KB | Script tag embeds |
The standalone IIFE includes React, so it's larger but works on any website without dependencies.
ESM Build (root export or /web-component)
- Web component entrypoint for bundlers
- React is externalized via peerDependencies (install react and react-dom)
- Smaller bundle; no duplicate React if the host already uses it
- Uses Shadow DOM for style isolation
- Use for: bundlers, monorepo consumers
Standalone IIFE Build (/standalone)
- Web component via tag
- React is bundled inside the package
- Uses Shadow DOM for complete JS/CSS isolation
- Separate React instances in separate DOM trees = no conflict
- Use for: tag embedding on any website (React or not)
When a React app loads the standalone bundle, you get duplicate React instances which causes the infamous "hooks can only be called inside a function component" error.
Shadow DOM solves this: Each React instance manages its own separate DOM tree inside the shadow boundary, so they never conflict.
`` The function App() { useEffect(() => { return const authToken = session?.idToken ?? ""; useEffect(() => { if (!agent) { // Set other attributes (these don't contain sensitive data) return Use Alternative CDN (jsdelivr): Pin to a specific version for production: Char requires a browser environment — it uses Shadow DOM, custom elements, and other browser APIs. In SSR frameworks, you must ensure Char only renders on the client. Use client:only="react" ` client:load Use dynamic imports with ssr: false ` const Char = dynamic(() => import('@mcp-b/char').then(m => ({ default: m.Char })), { export default function Page() { | Prop | Attribute | Type | Description | Customize appearance by setting CSS variables on the host page: ` / Layout / / Backgrounds / / Messages / / Composer / / Tools / / Code blocks / / Font sizes / / Dark mode / Use your own API keys during development (stateless): ` Development modes: Common combinations: Always use defer ` When to use which: Speed up loading by adding a preconnect hint in your - MCP Tool Support - Connect to any MCP server Removed props: Migration examples: Benefits:
Host Page (React 18)
├── ← Host's React
│ └── ... host app ...
│
└──
└── #shadow-root ← Isolation boundary
└── ← Bundled React 19
└── ... widget ...
``Installation
bash`
npm install @mcp-b/charUsage
renders as a full-viewport overlay.connect()$3
method is the recommended way to authenticate. It keeps tokens out of DOM attributes, preventing exposure to session replay tools, error reporters, and DOM inspectors.`tsx`
import { useRef, useEffect } from "react";
import "@mcp-b/char/web-component";
import type { CharAgentElement } from "@mcp-b/char/web-component";
const { session } = useOktaAuth(); // or Azure, Auth0, Google, etc.
const agentRef = useRef
if (agentRef.current && session?.idToken) {
agentRef.current.connect({ idToken: session.idToken });
}
}, [session?.idToken]);
}`$3
html``$3
tsx`
import { useRef, useEffect } from "react";
import "@mcp-b/char/web-component";
import type { CharAgentElement } from "@mcp-b/char/web-component";
const agentRef = useRef
const agent = agentRef.current ?? document.querySelector("char-agent");
const newAgent = document.createElement("char-agent");
document.body.appendChild(newAgent);
// Connect after element is in DOM
(newAgent as CharAgentElement).connect({ idToken: authToken });
} else if (authToken) {
(agent as CharAgentElement).connect({ idToken: authToken });
}
agent?.setAttribute("dev-mode", JSON.stringify({ useLocalApi: true }));
agent?.setAttribute("enable-debug-tools", String(import.meta.env.DEV));
}, [authToken]);defer$3
for best performance - it loads the script without blocking page rendering:`html``html``html``$3
html
enable-debug-tools="true"
>
`SSR Frameworks (Astro, Next.js, etc.)
$3
to skip server-side rendering entirely:astro
---
import { Char } from '@mcp-b/char'
---
devMode={{ anthropicApiKey: import.meta.env.PUBLIC_ANTHROPIC_API_KEY }}
open={true}
/>
` or client:visible will not work because Astro still attempts to render the component on the server first. client:only="react" skips SSR entirely and renders only in the browser.$3
:tsx
import dynamic from 'next/dynamic'
ssr: false,
})
return
}
`Props / Attributes
|------|-----------|------|-------------|
| - | - | method | connect({ idToken }) - Secure authentication (token not in DOM) |connect({ ticketAuth })
| - | - | method | - SSR-friendly authentication (pre-fetched ticket) |disconnect()
| - | - | method | - Clear authentication |open
| | open | boolean | Controlled open state (optional) |devMode
| | dev-mode | object/JSON | Development mode config (optional) |enableDebugTools
| | enable-debug-tools | boolean | Enable debug tools (default: false) |Customization
css
char-agent {
/ Brand colors /
--wm-color-primary: #8b5cf6;
--wm-color-primary-foreground: #ffffff;
--wm-radius: 12px;
--wm-font-sans: 'Inter', sans-serif;
--wm-color-background: #ffffff;
--wm-color-foreground: #1a1a1a;
--wm-color-muted: #f5f5f5;
--wm-user-bubble-bg: #8b5cf6;
--wm-user-bubble-text: #ffffff;
--wm-assistant-bubble-bg: #f5f5f5;
--wm-assistant-bubble-text: #1a1a1a;
--wm-composer-bg: #ffffff;
--wm-composer-border: #e5e5e5;
--wm-composer-button-bg: #8b5cf6;
--wm-tool-bg: #f9fafb;
--wm-tool-border: #e5e7eb;
--wm-tool-approve-bg: #10b981;
--wm-tool-deny-bg: #ef4444;
--wm-code-bg: #1e1e1e;
--wm-code-text: #d4d4d4;
--wm-font-size-xs: 0.75rem;
--wm-font-size-sm: 0.875rem;
--wm-font-size-base: 1rem;
--wm-font-size-lg: 1.125rem;
}
char-agent.dark {
--wm-color-background: #1a1a1a;
--wm-color-foreground: #ffffff;
--wm-color-muted: #2a2a2a;
/ ... other dark mode overrides /
}
`Development Mode
html`
>
- anthropicApiKey: Use your own Anthropic API key (falls back to Gemini if not provided)openaiApiKey
- : Enable voice mode with your OpenAI keyuseLocalApi
- : Point to localhost instead of production API
- { useLocalApi: true } - Internal monorepo development{ anthropicApiKey: "sk-ant-..." }
- - External dev with your own key{ anthropicApiKey: "...", openaiApiKey: "...", useLocalApi: true }
- - Full stack local devPerformance
$3
or async when embedding the standalone script to avoid blocking page rendering:html
`
- defer (recommended) - Script executes after HTML is parsed, preserves execution orderasync
- - Script executes as soon as it loads, good for independent widgets$3
:`html`apiKeyFeatures
- Voice Mode - Talk to your AI assistant
- Action-First UI - Shows what the AI is doing
- Shadow DOM Isolation - Styles don't leak in or out
- Stateful Sessions - Messages persist across page refreshes (when authToken is provided)
- CSS Variable Theming - Customize appearance without JavaScriptMigration Guide
$3
- / userId - Replaced by authToken (use your existing IDP token)appId
- - No longer needed (removed dead code)siteId
- - Org-level routing in SSO-first mode (no site scoping)theme
- - Use CSS variables instead (see Customization section)isolateStyles
- - Always enabled (Shadow DOM is always used)disableShadowDOM
- - Removed (Shadow DOM is always enabled)`html`
site-id="site_123"
api-key="sk_live_xxx"
theme='{"primaryColor":"#ff0000","mode":"dark"}'
isolate-styles="false"
>`
- Smaller bundle size (~200 lines removed)
- Simpler API (fewer required attributes)
- More flexible styling (CSS variables work everywhere)
- Consistent Shadow DOM isolation (no edge cases)
- No API keys to manage (uses existing IDP tokens)Development
bash``
pnpm --filter @mcp-b/char dev # Watch TS build
pnpm --filter @mcp-b/char dev:css # Watch Tailwind CSS
pnpm --filter @mcp-b/char storybook # http://localhost:6006
pnpm --filter @mcp-b/char build
pnpm --filter @mcp-b/char check:types
pnpm --filter @mcp-b/char test