@assemble-inc/chat-widget
v0.3.1
Published
Embeddable AI chat widget powered by Anthropic and MCP
Readme
@assemble-inc/chat-widget
An embeddable, general-purpose AI chat widget. Drop it into any React app — or any plain HTML page — to give users a floating chat interface backed by Anthropic Claude and MCP (Model Context Protocol). Fully configurable at embed time: system prompt, model, knowledge base, UI copy, and more.
Installation
npm install @assemble-inc/chat-widget
# or
yarn add @assemble-inc/chat-widgetPeer dependencies — make sure these are already in your project:
npm install react react-domQuick Start
1. Mount the widget
import { Widget } from "@assemble-inc/chat-widget";
import "@assemble-inc/chat-widget/style.css";
export default function App() {
return (
<>
{/* your app */}
<Widget
config={{
systemPrompt: "You are a helpful assistant for Acme Corp.",
title: "Ask Us Anything",
placeholder: "Type your question...",
}}
/>
</>
);
}The widget renders as a fixed floating button in the bottom-right corner (configurable). Clicking it opens the chat panel.
2. Add the server-side handler
The simplest setup is to mount the handler on your existing app server so that the widget's default endpoint: '/api/chat' resolves correctly:
// your existing Express/Next/etc. server
import { createChatHandler } from "@assemble-inc/chat-widget/server";
app.use(express.json());
app.post(
"/api/chat",
createChatHandler({
apiKey: process.env.ANTHROPIC_API_KEY!,
mcpServerUrl: process.env.MCP_SERVER_URL!,
mcpBearerToken: process.env.MCP_BEARER_TOKEN,
}),
);If you prefer to run a separate API server (e.g. on port 3006), you must tell the widget where to find it — otherwise it will POST to your frontend server and get a 404:
// Pass the full URL when the API server is on a different port
<Widget config={{ endpoint: "http://localhost:3006/api/chat" }} />Or proxy /api to the API server in your frontend dev config (Vite example):
// vite.config.ts
server: {
proxy: {
"/api": "http://localhost:3006",
},
},3. Environment variables
ANTHROPIC_API_KEY=sk-ant-...
MCP_SERVER_URL=https://your-mcp-server.example.com/mcp
MCP_BEARER_TOKEN=your-bearer-token # optional
SYSTEM_PROMPT="You are..." # optional server-side overrideScript-tag Embed (no React required)
For non-React apps, use the self-contained IIFE bundle:
<link rel="stylesheet" href="https://cdn.example.com/asm-widget/index.css" />
<script src="https://cdn.example.com/asm-widget/embed.js"></script>
<script>
AsmWidget.init({
endpoint: "https://my-server.com/api/chat",
systemPrompt: "You are a helpful assistant.",
title: "Help",
});
</script>Build the embed bundle:
npm run build:embed # outputs dist/embed.jsConfiguration Reference
WidgetConfig
All configuration is passed as a single config prop. Every field is optional.
| Field | Type | Default | Description |
| ----------------- | --------------------------------- | ---------------- | -------------------------------------------------------------------------------------------------------------- |
| endpoint | string | '/api/chat' | Backend chat endpoint URL. |
| systemPrompt | string | — | System prompt for this embed. Forwarded to the server; server-side value wins when both are set. |
| context | string | — | Additional context appended after the system prompt (e.g. current page, user role). Forwarded to the server. |
| mcpServerUrl | string | — | MCP server URL for this embed. Must appear in the server's mcpCredentials allowlist; falls back to default. |
| model | string | — | Claude model ID (e.g. 'claude-opus-4-5'). Forwarded to the server; server-side value wins when both are set. |
| temperature | number | — | Response creativity, 0–1. Forwarded to the server; server-side value wins when both are set. |
| initialMessages | Array<{ role, content }> | — | Seed messages shown when the widget first opens. |
| persist | boolean | false | Save the conversation to sessionStorage across page navigations. |
| user | { id?, name?, role? } | — | Forwarded in every request body for server-side personalisation or logging. |
| title | string | 'Ask ASMBL' | Header title. |
| logo | string | Built-in logo | URL to a custom logo image. |
| welcomeHeading | string | — | Heading shown in the empty state. |
| placeholder | string | — | Textarea placeholder text. |
| position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Widget anchor corner. |
WidgetProps
| Prop | Type | Default | Description |
| -------------- | -------------------------------------------------- | ------- | ------------------------------------------------------------------------------------ |
| config | WidgetConfig | — | All widget configuration (see above). |
| open | boolean | — | Controls open/close state from outside (controlled mode). |
| defaultOpen | boolean | false | Initial open state when uncontrolled. |
| onOpenChange | (open: boolean) => void | — | Called whenever the widget opens or closes. |
| onMessage | (msg: { role: string; content: string }) => void | — | Called after each new assistant message is received. |
| onError | (error: Error) => void | — | Called when a stream error occurs. |
| children | ReactNode | — | Custom content shown in the empty state. Replaces the built-in empty state entirely. |
Server Reference
createChatHandler(config)
Returns an Express-compatible (req, res) => Promise<void> request handler that streams Claude responses via your MCP server.
| Option | Type | Required | Description |
| ---------------- | ------------------------ | -------- | --------------------------------------------------------------------------------------------------------------------------- |
| apiKey | string | Yes | Anthropic API key. |
| mcpServerUrl | string | Yes | Default MCP server URL. Used when no per-embed URL is provided or the requested URL is not in the allowlist. |
| mcpBearerToken | string | No | Bearer token for the default MCP server. |
| model | string | No | Claude model ID. Takes precedence over any model sent by the widget. Defaults to claude-sonnet-4-5. |
| systemPrompt | string | No | Operator system prompt. Takes precedence over any system prompt sent by the widget. |
| temperature | number | No | Model temperature (0–1). Takes precedence over any temperature sent by the widget. |
| mcpCredentials | Record<string, string> | No | Allowlist of additional MCP server URLs → bearer tokens. Enables per-embed MCP routing. Credentials never leave the server. |
Precedence rules
For systemPrompt, model, temperature, and mcpServerUrl, the server-side value always takes precedence over what the widget sends. The resolution order is:
ChatHandlerConfig value → widget config value → built-in defaultMulti-context deployments
A single server can serve multiple embeds pointing at different MCP servers:
createChatHandler({
apiKey: process.env.ANTHROPIC_API_KEY!,
mcpServerUrl: process.env.MCP_SERVER_URL!, // default fallback
mcpBearerToken: process.env.MCP_BEARER_TOKEN,
mcpCredentials: {
"https://mcp.acme.com/hr": process.env.MCP_HR_TOKEN,
"https://mcp.acme.com/it": process.env.MCP_IT_TOKEN,
},
});Each embed specifies which server it wants via config.mcpServerUrl. Bearer tokens are looked up server-side — they are never exposed to the browser.
Examples
Minimal embed
<Widget config={{ systemPrompt: "You are a helpful assistant." }} />Fully configured embed
<Widget
config={{
endpoint: "https://api.acme.com/chat",
systemPrompt: "You are an HR assistant for Acme Corp.",
mcpServerUrl: "https://mcp.acme.com/hr",
model: "claude-opus-4-5",
temperature: 0.3,
title: "Ask HR",
welcomeHeading: "How can we help?",
placeholder: "Ask an HR question...",
position: "bottom-left",
persist: true,
user: { id: currentUser.id, name: currentUser.name },
}}
onMessage={({ content }) => analytics.track("chat_message", { content })}
onOpenChange={(open) => setHelpOpen(open)}
/>Custom empty state (children)
<Widget config={{ title: "Support", placeholder: "Describe your issue..." }}>
<div>
<h3>How can we help?</h3>
<button onClick={() => sendMessage("Reset my password")}>
Reset my password
</button>
<button onClick={() => sendMessage("Billing question")}>
Billing question
</button>
</div>
</Widget>Local Development
# Install dependencies
npm install
# Copy and fill in environment variables
cp .env.example .env
# Start the Vite dev server (port 3003) + Express API server (port 3006)
npm run devThe Vite dev server proxies /api/* to http://localhost:3006.
npm run lint # type-check without emitting
npm run build # build the React library (dist/)
npm run build:embed # build the standalone IIFE script (dist/embed.js)Publishing
npm version patch # bug fixes (0.1.x)
npm version minor # new features (0.x.0)
npm version major # breaking changes (x.0.0)
npm publish