npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@assemble-inc/chat-widget

v0.6.4

Published

Embeddable agentic chat widget powered by Anthropic

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

pnpm add @assemble-inc/chat-widget
# or
npm install @assemble-inc/chat-widget
# or
yarn add @assemble-inc/chat-widget

Peer dependencies — make sure these are already in your project:

pnpm add react react-dom

Quick Start

1. Mount the widget

import { Widget } from "@assemble-inc/chat-widget";

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. Styles are injected automatically when you import Widget; you can still import @assemble-inc/chat-widget/style.css explicitly if your bundler strips side effects. If your app uses Tailwind v3 PostCSS, do not re-run Tailwind on the widget stylesheet (only autoprefixer) — the CSS is already compiled with Tailwind v4.

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 override

Script-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:

pnpm run build:embed   # outputs dist/embed.js

Configuration 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 | 'Chat' | Header title. | | logo | string | Built-in logo | URL to a custom logo image. | | theme | WidgetTheme | — | Semantic color and typography overrides for whitelabeling (see below). | | welcomeHeading | string | — | Default assistant message shown when the conversation is empty. | | copy | WidgetCopy | — | Override built-in error and loading status text. | | placeholder | string | — | Textarea placeholder text. | | showCloseButton | boolean | — | Header chevron that dismisses the panel. false hides it. Default: shown in floating layout or when onClose is set in docked layout. | | layout | 'floating' \| 'docked' | 'floating' | 'floating': FAB + overlay panel. 'docked': fills the host container. | | position | 'bottom-right' \| 'bottom-left' | 'bottom-right' | Widget anchor corner. Only applies when layout is 'floating'. |

WidgetTheme

Pass semantic tokens via config.theme to rebrand the widget without writing custom CSS:

| Field | Applies to | | ------------ | -------------------------------------------------------------------------- | | primary | Header title, FAB (closed), icons, MCQ selected state, send button border | | accent | Hover and focus rings | | userBubble | User message bubbles | | fabOpen | FAB when the panel is open (defaults to userBubble) | | surface | Panel shell, assistant bubbles, composer bar | | content | Chat content area background | | muted | Secondary text and loading indicators | | text | Message body text | | fontFamily | Widget font stack |

You can also override tokens with CSS on the widget root:

.asm-widget {
  --asm-primary: #003366;
  --asm-user-bubble: #dbeafe;
  --asm-font-family: 'Helvetica Neue', sans-serif;
}

WidgetCopy

| Field | Default | Description | | --------- | ---------------------------------------------------- | ------------------------------------------------ | | error | "Sorry, I'm having trouble connecting right now." | Shown when the chat endpoint fails. \n for breaks. | | loading | "This should only take a moment…" | Shown while waiting for the first response token. |

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. | | onClose | () => void | — | Called when the close button is clicked. Use config.showCloseButton: false to hide the chevron while keeping this handler. | | className | string | — | Extra class names on the widget root. | | style | React.CSSProperties | — | Extra inline styles on the widget root (merged with config.theme). | | 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 default

Multi-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." }} />

Whitelabeled embed

<Widget
  config={{
    title: "Ask Acme",
    logo: "https://acme.com/logo.svg",
    theme: {
      primary: "#003366",
      accent: "#93c5fd",
      userBubble: "#dbeafe",
      surface: "#f8fafc",
      content: "#ffffff",
      text: "#334155",
      fontFamily: "'Helvetica Neue', sans-serif",
    },
  }}
/>

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
pnpm install

# Copy and fill in environment variables
cp .env.example .env

# Start the Vite dev server (port 3003) + Express API server (port 3006)
pnpm run dev

The Vite dev server proxies /api/* to http://localhost:3006.

pnpm run lint          # type-check without emitting
pnpm run build         # build the React library (dist/)
pnpm run build:embed   # build the standalone IIFE script (dist/embed.js)

Publishing

pnpm version patch   # bug fixes (0.1.x)
pnpm version minor   # new features (0.x.0)
pnpm version major   # breaking changes (x.0.0)

pnpm publish