# Connect your AI client
Source: https://docs.waniwani.ai/mcp-server/connect
Add the Waniwani MCP server to claude.ai, Claude Code, Cursor, or ChatGPT and sign in with your Waniwani account — no API keys.
Waniwani runs the MCP server in two regions. Add the URL for the region where your organization lives:
| Region | MCP URL |
| ------ | -------------------------------- |
| US | `https://mcp.waniwani.ai/mcp` |
| EU | `https://eu.mcp.waniwani.ai/mcp` |
It's a remote MCP server (Streamable HTTP) that authenticates with OAuth 2.1. There is no API key to paste: you add the URL, your client opens a browser window, you sign in to Waniwani and approve the requested permissions. From then on the connection acts as *you* — same organizations, same role.
Each region is an independent stack with its own data. Sign in on the endpoint that matches your **data residency** — the region you chose when you [created your organization](/platform/regions). The US endpoint won't show organizations that live in the EU, and vice versa. The examples below use the US URL; if your data is in the EU, swap in `https://eu.mcp.waniwani.ai/mcp`.
## Claude (claude.ai and desktop)
1. Open **Settings → Connectors**.
2. Click **Add custom connector**.
3. Enter `https://mcp.waniwani.ai/mcp` and confirm.
4. A Waniwani sign-in window opens — sign in and approve the permissions.
On Team and Enterprise plans, an admin may need to enable custom connectors for your workspace first.
## Claude Code
```bash theme={null}
claude mcp add --transport http waniwani https://mcp.waniwani.ai/mcp
```
Then run `/mcp` inside Claude Code and select **waniwani** to complete the browser sign-in.
## Cursor
Add the server to `~/.cursor/mcp.json` (or **Settings → MCP → Add new MCP server**):
```json ~/.cursor/mcp.json theme={null}
{
"mcpServers": {
"waniwani": {
"url": "https://mcp.waniwani.ai/mcp"
}
}
}
```
Cursor prompts you to authenticate in the browser the first time it connects.
## ChatGPT
Custom MCP connectors require developer mode:
1. Open **Settings → Apps & Connectors → Advanced settings** and enable **Developer mode**.
2. Back in **Connectors**, click **Create** and enter `https://mcp.waniwani.ai/mcp`.
3. Complete the sign-in when prompted.
Client UIs change quickly. If a menu has moved, follow your client's own guide for adding a **remote MCP server** and use the URL above — the server implements the standard discovery flow, so any MCP-capable client that supports OAuth can connect.
## What happens when you sign in
The client reads the server's metadata and learns which Waniwani stack handles sign-in for that region (`app.waniwani.ai` for US, `eu.app.waniwani.ai` for EU). It registers itself automatically — no manual client IDs.
The browser opens your usual Waniwani login. If you're already signed in, you go straight to consent.
The consent screen itemizes exactly what the connection may do (read environments and analytics, create environments and keys, and so on). You only see permissions your account is entitled to.
The client receives a token bound to this server and your approved permissions. Every call it makes runs under your identity.
## First thing to try
> Who am I signed in to Waniwani as, and what can you do for me here?
Your client will call the server's `whoami` and `search` tools and report your account, your organizations, and the available operations. From there, ask for what you want in plain language — see [the overview](/mcp-server/overview) for example prompts.
## Troubleshooting
That's by design. The permission list is narrowed to what your account is entitled to — for example, internal Waniwani staff permissions never appear for customer accounts. Approve what's shown; it's everything available to you.
Your token expired or was invalidated. Most clients refresh automatically; if yours doesn't, disconnect and reconnect the server (re-running the sign-in) to mint a fresh token.
`set_active_org` only switches to organizations you belong to — the API rejects anything else with a `NOT_ORG_MEMBER` error. Ask *"who am I?"* — the `whoami` tool lists the organizations you can act on and their ids.
Expected. Each connection carries its own active organization, independent of the dashboard — switching in one place doesn't switch the other. Ask the model to switch here too (it calls `set_active_org`), or check where you stand with *"who am I?"*.
The available surface is curated and grows over time. Ask the model to run `search` again — it returns the live catalog with exact signatures. If what you need isn't there yet, tell us.
# How it works
Source: https://docs.waniwani.ai/mcp-server/how-it-works
Four tools, OAuth scopes, org context, and sandboxed execution: what happens between your prompt and the Waniwani API.
The server is deliberately small: four tools that compose, rather than one tool per API endpoint.
## The four tools
| Tool | What it does |
| ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `whoami` | Returns your identity: user id, granted permissions, the organizations you can act on, and which one is active. |
| `set_active_org` | Switches your active organization for subsequent calls. Membership is enforced server-side. |
| `search` | Lists the Waniwani API operations available to `execute`, with full TypeScript signatures — inputs *and* response shapes. Takes an optional keyword filter. |
| `execute` | Runs a short piece of JavaScript in an isolated sandbox. The code calls operations as `api.(input)` and returns a value. |
## The loop
A request like *"give me a digest of activity for the last 7 days"* plays out as:
1. The model calls `search("analytics")` and reads the typed signatures.
2. It writes a snippet and calls `execute`:
```js theme={null}
const envs = await api.listEnvironments();
// arguments elided — `search` returns the exact signatures
const summaries = await Promise.all(
envs.map((env) => api.getEnvironmentAnalytics(/* ... */)),
);
return summaries;
```
3. The returned data lands back in the conversation, and the model writes your digest.
Because `execute` runs real code, the model can chain, filter, and aggregate in a single round trip instead of stitching together a dozen separate tool calls. Top-level `await` is supported, and whatever the snippet `return`s comes back as the tool result.
## Org context
Operations run against your **active organization**:
* When you first connect, it starts as the organization you last used in the dashboard.
* `set_active_org` switches it, and the switch is enforced server-side: you can only activate organizations you belong to.
* The switch applies to **this connection only**. The dashboard and any other connected clients each keep their own active organization, so driving Waniwani from your AI client never silently moves your browser session — or vice versa.
* It takes effect on your next call: the server invalidates the connection's current token and your client silently refreshes into the new organization.
`whoami` always tells you where you currently stand.
## Permissions: scopes and consent
When you connect, the consent screen itemizes the connection's permissions as OAuth scopes:
| Scope | Grants |
| -------------------------- | -------------------------------------------------------------------------------- |
| `orgs:read` / `orgs:write` | See your organizations and membership / invite members |
| `mcp:read` / `mcp:write` | Read environments and analytics / create environments and API keys |
| `kb:read` / `kb:write` | Read / write the knowledge base *(operations not yet exposed through `execute`)* |
Two properties worth knowing:
* **You only see what you're entitled to.** The consent screen narrows the list to your account's role — permissions reserved for Waniwani staff never appear for customer accounts.
* **Scopes are enforced on every API call, server-side.** The MCP server doesn't get to be generous: each `api.*` operation is re-checked against your token's scopes and your org membership by the Waniwani API itself.
## Security model
* **OAuth 2.1, no API keys.** You sign in through `app.waniwani.ai` — the same login as the dashboard. The token your client receives is bound to this server specifically and to the scopes you approved.
* **The sandbox never sees your token.** Code submitted through `execute` runs in an isolated sandbox that can reach only the curated `api.*` operations. Your credentials are attached to outbound API calls *outside* the sandbox — code running inside it has no way to read or exfiltrate them.
* **Every call is your call.** All operations execute under your identity with your permissions. The connection can do nothing your account can't do in the dashboard.
* **Stateless by design.** The server keeps no session state and stores no conversation content; each request is authenticated independently by your token. Disconnecting the server from your client severs its access.
## Limits worth knowing
* The exposed operation surface is curated and grows over time — `search` is the source of truth, not this page.
* The active organization is per-connection: switching it here doesn't affect the dashboard or other connected clients, and switching in the dashboard doesn't affect this connection.
* Long or heavy aggregations should be scoped (a date range, a single environment): `execute` snippets run within a bounded execution window.
# Waniwani MCP server
Source: https://docs.waniwani.ai/mcp-server/overview
Connect Claude, Cursor, or ChatGPT to the Waniwani platform and drive it from a conversation — environments, API keys, analytics digests, session breakdowns.
The Waniwani MCP server lets your AI client operate the Waniwani platform for you. Add the server URL for your region, sign in with your Waniwani account, and ask for the things you would otherwise click through the dashboard for.
## Server URL
Use the URL for the region your organization's data lives in — the region you chose when you created the organization:
```text US theme={null}
https://mcp.waniwani.ai/mcp
```
```text EU theme={null}
https://eu.mcp.waniwani.ai/mcp
```
If you sign in to the dashboard at `app.waniwani.ai`, use the **US** URL. If you sign in at `eu.app.waniwani.ai`, use the **EU** URL. Each region is an independent stack: the US endpoint won't show organizations that live in the EU, and vice versa. See [Regions & data residency](/platform/regions) for how residency is set, and [Connect your AI client](/mcp-server/connect) for the per-client setup.
This is **Waniwani's own MCP server** — the one that drives *the platform*. It is not the MCP funnel server you build with the SDK. Everywhere else in these docs, "your MCP server" means the one you ship to your users; this tab is about the one we host for you.
## What you can ask it
* "Give me a digest of activity across my environments for the last 7 days."
* "Create a new environment called *staging* and hand me its API key."
* "Which sessions hit production yesterday? Where did people drop off?"
* "What sources are sending events to my production environment?"
* "Compare this week's activity to last week's across all my environments."
* "Invite [jane@acme.com](mailto:jane@acme.com) to our organization."
## What it can do
| Category | Examples |
| --------------------------- | ---------------------------------------------------------------------------------------------------- |
| **Environments & API keys** | List your environments, create one, issue an environment's API key |
| **Analytics & sessions** | Activity summaries over a date range, funnel flow breakdowns, per-session drill-downs, event sources |
| **Organization** | Check who you're signed in as, switch your active org, invite a teammate |
The surface grows over time, so this table stays deliberately coarse. The server's own `search` tool is the live catalog: once connected, ask *"what Waniwani operations are available?"* and your client will list every operation with its exact signature.
## How you use it
1. [Connect your client](/mcp-server/connect) — add the URL above, sign in, approve the permissions. No API keys involved.
2. Ask in plain language. The model discovers the right operations and calls them with your identity — every call is permission-checked server-side.
3. Results come back into the conversation.
Everything the connection can do is bounded by your own Waniwani account: your organizations, your role, the permissions you approved at sign-in. The mechanics are on [How it works](/mcp-server/how-it-works).
# Submit to the ChatGPT Plugin Store
Source: https://docs.waniwani.ai/platform/guides/chatgpt
A tab-by-tab checklist for listing your Waniwani-built MCP server as a plugin in the ChatGPT Plugin Store: account setup, MCP server, tool justification, test cases, starter prompts, screenshots, and policy compliance.
This guide covers everything you need to prepare to list your MCP server as a plugin in the ChatGPT Plugin Store. We recommend submitting from **your own OpenAI account**, so the listing and its Developer name belong to your organization; if you prefer, Waniwani can handle the submission on your behalf. Either way, most of the engineering work (server, versioning, tool annotations) is already handled. Your job is to prepare the assets and decisions below, then they go into the form.
You create the plugin at [platform.openai.com/plugins](https://platform.openai.com/plugins) and fill in a form split across tabs — **Info → MCP → Skills → Prompts → Testing → Global → Submit**. This guide follows that order, with a short setup section first. OpenAI's own walkthrough is at [Submit plugins](https://learn.chatgpt.com/docs/submit-plugins).
Submitting the form does not guarantee a listing. OpenAI reviews every plugin, and the **Tool justification** and **Test cases** sections are where most rejections happen, so give them the most attention.
## Before you start: OpenAI account and access
Sort these out before you open the form, or you will be blocked at the first tab.
* **Apps Management role.** The account that submits needs the **"Apps Management"** permission set to **"Write"** in your organization role. Check it under [Platform → organization roles](https://platform.openai.com/settings/organization/people/roles).
* **Verified identity.** You need a **verified developer or business identity** in the OpenAI Platform. Complete individual or business verification in your organization settings first; verification can take time, so start early.
* **Public URLs live.** Website, support contact, privacy policy, and terms of service must be publicly reachable (see [Plugin info](#3-info-tab-plugin-info) below).
* **Reviewer credentials.** If your plugin requires sign-in, prepare **demo credentials** that work for a reviewer **without MFA, SMS, email confirmation, or private-network access**. Reviewers cannot complete an auth flow that has extra confirmation steps.
If Waniwani submits on your behalf, we handle the account, role, and identity verification. If you submit from your own OpenAI account, these are your responsibility.
### Creating the plugin
1. Go to [platform.openai.com/plugins](https://platform.openai.com/plugins) and select **Create plugin**.
2. Choose a submission type:
* **Skills only** — a package of reusable workflows, no MCP server.
* **With MCP** — a plugin backed by your MCP server (MCP-only, or MCP-plus-skills).
For a Waniwani-built plugin, choose **With MCP**. Skills are optional (see the [Skills tab](#5-skills-tab-optional)).
## 1. MCP tab: server, versioned and deployed
The form requires a stable production MCP Server URL, entered on the **MCP** tab.
1. Go to [app.waniwani.ai/mcp](https://app.waniwani.ai/mcp) and confirm your app is listed.
2. Use the dropdown next to your app to copy its production MCP URL.
3. Verify it follows the versioned pattern, for example `https://v1..mcp.waniwani.run/mcp`.
4. On the MCP tab, enter the URL, configure authentication, set the content security policy, then complete domain verification and select **Scan Tools** so OpenAI can read your tool definitions.
**Domain verification is a common blocker.** OpenAI generates a verification token that must be served at a specific well-known path on your challenge host:
`https:///.well-known/openai-apps-challenge`
Plan for this step in advance rather than discovering it mid-form. If you are a managed Waniwani customer, coordinate the timing with us so we can host the token at that path together.
Before you run **Scan Tools**, make sure tool responses contain no secrets, undisclosed user fields, or unnecessary personal data — reviewers inspect the scanned tool output. For managed Waniwani apps this is handled in the server code.
## 2. Logo: light and dark versions
Provide two versions of your plugin's icon.
* **Format:** PNG.
* **Shape:** square. Do not add borders or rounded corners; ChatGPT applies circular cropping automatically.
* One **light mode** version and one **dark mode** version.
## 3. Info tab: plugin info
Prepare these fields in advance. Use the **customer-facing product or workflow name**, and make sure every URL matches your verified publisher identity.
| Field | Requirements | Example |
| ------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------- |
| **Name** | Your plugin's display name. How users find and recognize it. Keep it short and on-brand. | `Your Plugin Name` |
| **Subtitle** | Short, plain-language phrase. **30 characters max** (the limit is strict, count carefully). Focus on function and value, not marketing. | `Find the best deals near you` |
| **Description** | Clear description of what the plugin does and why people want it. Concrete value, no exaggerated or misleading claims. Appears publicly. | See guidance below |
| **Category** | One of ChatGPT's predefined categories. Pick the one matching your primary function. | `Finance` |
| **Developer** | Your company name when you submit from your own OpenAI account (recommended). OpenAI verifies this against the submission account, so it must match whoever submits: if Waniwani submits on your behalf, it is `WaniWani`. | `Your Company` |
| **Website URL** | Your main company or product website. Use the top-level URL, not a deep link, and make sure it loads cleanly. | `https://yourcompany.com` |
| **Support URL or Email** | A working, monitored channel. Reviewers may test it. | `support@yourcompany.com` |
| **Privacy Policy URL** | A live, publicly accessible page. Confirm it in a private window: if it 404s or needs a login, it is rejected. | `https://yourcompany.com/privacy` |
| **Terms of Service URL** | A live, publicly accessible page (same check as above). | `https://yourcompany.com/terms` |
| **Demo Recording URL** | A video showing your plugin working across all main use cases and tools. | A public YouTube or Loom link |
### Writing the description
Structure it like this:
1. **Opening paragraph:** what the plugin does and the value it provides (two to three sentences).
2. **"What you can do with \[Plugin Name]":** three to five bullet points of key capabilities.
3. Keep it factual and user-centric, and avoid hype.
For tone and length, study how live plugins write theirs, for example [Compare the Market](https://chatgpt.com/apps/compare-the-market/asdk_app_69a41e8393c48191bf5b3ecfb7963af9), [Aviva](https://chatgpt.com/apps/aviva/asdk_app_69a6eb598b4c8191b10712ed489e8e11), and [Tuio](https://chatgpt.com/apps/tuio/asdk_app_6970fc7c958881918cfa39d58fc37e82).
### Recording the demo
The video must cover all main use cases and tools across every platform you support (web, iOS, Android). A practical recipe:
1. Record several runs of the plugin working across different scenarios.
2. Stitch them into one video, one scenario after another.
3. Speed it up to at least 2x. The point is for a reviewer to see what the plugin does quickly.
### If your plugin involves commerce
You will be asked to confirm your purchasing flow and to describe the products you intend to sell. For regulated products such as insurance, be transparent that you help users get a **quote estimate**: if they like it, they are redirected to your website to finalize. Do not present a "final price" inside the chat.
## 4. Tool justification (critical step)
This is the hardest part and the most common reason for rejection. For **every tool** your MCP server exposes, OpenAI requires three annotation hints, each with a written justification. If they are not set, OpenAI assumes the worst defaults: `readOnlyHint = false`, `openWorldHint = true`, `destructiveHint = true`.
| Annotation | Question it answers | Values |
| --------------- | -------------------------------------------------------------------- | -------- |
| **Read Only** | Does this tool only read data without modifying anything? | Yes / No |
| **Open World** | Does this tool call an external API or service outside your control? | Yes / No |
| **Destructive** | Can this tool delete, overwrite, or irreversibly modify data? | Yes / No |
For each annotation on each tool, the form wants a short written justification explaining why the value is accurate. Tools that touch external services may also need CSP metadata (`connect_domains`, `resource_domains`).
Use the [Tool justification template](/platform/guides/templates/tool-justification) to lay out every tool, its annotation values, and the justification text before you open the form. For managed Waniwani apps, the annotations and CSP metadata are set in the server code for you, and you receive the justifications pre-filled to review and paste.
## 5. Skills tab (optional)
Skills are self-contained, reusable workflow packages. You can submit **skills only** (no MCP server) or bundle skills alongside your plugin. If you have no skills to ship, leave this tab empty.
* Upload the final **skill bundle** with a tested file structure.
* Confirm the bundle works end to end before uploading — a broken bundle is a rejection.
For most Waniwani-built plugins you can skip this tab; talk to us if you want to package workflows as skills.
## 6. Prompts tab: starter prompts
Provide specific **starter prompts** that demonstrate your plugin's key workflows. These are the example prompts ChatGPT surfaces to users.
* Make each prompt concrete and adaptable, not generic ("Find pet insurance for a 3-year-old labrador," not "Help me with insurance").
* Cover your main tools and use cases, aligning with your positive test cases below.
## 7. Testing tab: 5 positive and 3 negative test cases
OpenAI reviewers run these to validate your plugin, and they are reviewed carefully.
* **At least 5 positive cases:** scenarios where the plugin should trigger and work. For each, the form asks for the scenario, the user prompt, which tools fire, and the expected output.
* **At least 3 negative cases:** prompts where the plugin should **not** trigger. For each, the form asks for the scenario and the prompt.
Lay them out first with the [Test cases template](/platform/guides/templates/test-cases).
If your plugin requires sign-in, provide the **reviewer demo credentials** here (or where the form asks for them). They must let a reviewer complete every test case **without MFA, SMS, email confirmation, or private-network access**.
## 8. Global tab: regional availability
Decide whether the plugin is worldwide or restricted.
* **All countries:** available in every [ChatGPT-supported country](https://help.openai.com/en/articles/7947663-chatgpt-supported-countries).
* **Specific countries:** list them. Restrict if your service only operates in certain regions or the content is only relevant there.
## 9. Translations and locale
Parts of the form must be in English.
* **English (US)** is always required as the base locale.
* You can add other locales (for example français, español, Deutsch). For each, provide a translated **Subtitle** and **Description**.
* ChatGPT may auto-translate some fields, but your own translations ensure quality.
## 10. Screenshots
Provide screenshots of your plugin's widgets in action.
* **Format:** PNG.
* **Width:** 706px. **Height:** minimum 400px, recommended maximum 860px.
* At least 1 screenshot at 2x (retina quality), maximum 4.
* Each screenshot needs an example user message that would produce what's shown.
* The first three appear on install views and are reused across sizes and locales, so lead with your strongest states.
## 11. Submit tab: policy compliance and release notes
On the final **Submit** tab you confirm that your plugin:
* Complies with [OpenAI's Terms](https://openai.com/policies/connectors-actions-terms/) and [Submission Guidelines](https://developers.openai.com/apps-sdk/app-developer-guidelines), which you have reviewed.
* Complies with applicable industry laws and regulations.
* Does **not** initiate or execute money, crypto, or investment transfers.
* Does **not** serve or display advertisements.
* Uses third-party content and API endpoints you have the rights to.
* Is **not** designed for or marketed to children under 13, and you confirm it is suitable for 13+ or 18+.
You also provide **Release Notes**, a required field that may be shown publicly, then attest that everything is accurate.
Approval is not the same as going live. After OpenAI approves your plugin, you choose **when to publish** from the plugins portal; the listing appears in the ChatGPT Plugin Store only once you publish it.
## Summary checklist
* [ ] "Apps Management" role set to Write, and developer/business identity verified
* [ ] Reviewer demo credentials that work without MFA/SMS/email confirmation
* [ ] Versioned MCP server deployed and reachable
* [ ] Domain verified at `/.well-known/openai-apps-challenge`, tools scanned
* [ ] Light and dark PNG logos (square, no borders)
* [ ] All plugin info fields prepared (mind the 30-character subtitle limit)
* [ ] Tool annotations set in the server with written justifications
* [ ] Skill bundle uploaded, if applicable
* [ ] Starter prompts written
* [ ] 5 positive and 3 negative test cases written
* [ ] Regional availability decided
* [ ] English base locale, plus translations if needed
* [ ] Screenshots (PNG, 706px wide, 400 to 860px tall, max 4)
* [ ] Demo recording ready
* [ ] Privacy Policy and Terms of Service URLs live
* [ ] Commerce disclosures prepared, if applicable
Lay out every tool's annotations and justification text before you open the form.
The 5 positive and 3 negative cases, ready to fill in.
Listing the same app in Claude's Connectors Directory.
Drive traffic to your listing once it is live.
# Submit to the Claude Connectors Directory
Source: https://docs.waniwani.ai/platform/guides/claude
How to list your Waniwani-built MCP app in Claude's Connectors Directory: the Team/Enterprise prerequisite, what to prepare in advance, and a step-by-step walkthrough of the in-product submission portal.
This guide takes you from zero to a submitted app in Claude's [Connectors Directory](https://claude.com/docs/connectors/building/submission). Apps built on Waniwani have most of the engineering handled (server, tool annotations, assets); your job is to meet one prerequisite, prepare a handful of assets, and walk through the portal.
**You submit from your own Claude account.** Unlike ChatGPT, where Waniwani can submit on your behalf if you prefer, Anthropic's in-product portal requires your own **Claude Team or Enterprise** organization and an Owner of it. Waniwani prepares the server, annotations, assets, and the wording for every field; the final submit happens from your workspace.
## Before you start
Submission happens inside [claude.ai](https://claude.ai), not an external form. To reach the portal you need a Team or Enterprise organization (admin settings are not available on individual or Pro plans).
* [ ] Create a **Claude Team** (or Enterprise) organization at [claude.ai](https://claude.ai).
* [ ] Make sure the person submitting is an **Owner or Primary Owner**. Only Enterprise orgs can delegate this through a custom role with the *Directory management* permission; Team plans keep it with Owners.
* [ ] Find the portal at **Settings → "Your MCP servers" (Beta) → "Submit a server"** (direct link: `https://claude.ai/admin-settings/directory/submissions/new`).
Once approved, the connector appears at [claude.ai/directory](https://claude.ai/directory) for other Claude organizations to install.
## What to prepare
The portal is a wizard of about 11 steps (Introduction, Connection, Tools, Listing, Use cases, Company, Authentication, Data handling, Test and launch, Compliance, Review). Your progress is saved in the browser, but you can only have one submission open at a time, so gather everything below before you start.
### Production MCP server URL
The submission needs a stable production MCP Server URL over HTTPS.
1. Go to [app.waniwani.ai/mcp](https://app.waniwani.ai/mcp) and confirm your app is listed.
2. Use the dropdown next to your app to copy its production MCP URL.
### Tool annotations
This is the most common reason for rejection. Annotations must be set in the server code **before** submission. For managed Waniwani apps they are set for you; see the [Tool justification template](/platform/guides/templates/tool-justification) for what each value means.
What Claude enforces:
* **Every tool needs a `title`** plus the correct hint (`readOnlyHint: true` for reads, `destructiveHint: true` for writes/deletes). These control Claude's auto-permission behavior.
* **Read/write separation.** A single tool may not combine safe reads (GET/HEAD/OPTIONS) with writes (POST/PUT/PATCH/DELETE). Catch-all tools are rejected.
* **Tool names of 64 characters or fewer**, with descriptions that accurately state what the tool does and when it runs (reviewers test against real behavior).
* **Freeform or custom-query tools** (that accept caller-built endpoints, query strings, or bodies) must name or link the target API in their description.
* **No prompt-injection-style instructions** in descriptions. Describe what the tool does, not how Claude should behave. No hidden, encoded, or behavior-overriding directives.
Put the annotations inside the `annotations` object, not at the top level. Set `annotations.title` plus `annotations.readOnlyHint: true` for read-only tools, or `annotations.destructiveHint: true` for tools that write or delete. A top-level `title` alone still trips a "Missing annotations: title" error on the Tools step.
### Listing copy
Claude's character limits differ from ChatGPT's.
| Field | Requirements | Notes |
| --------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- |
| **Name** | Your connector's display name | Max 100 characters. Short, recognizable, on-brand. |
| **Slug** | URL identifier, prefilled from the name | **Permanent after submission.** Lowercase letters, numbers, and hyphens only. |
| **One-liner** | A short phrase describing function and value | Max 200 characters (the form calls this "One-liner"). Avoid marketing language. |
| **Description** | What the app does and why users value it. Appears publicly. | Max 2,000 characters. Factual, user-centric, no hype. |
| **Categories** | The 1 to 5 categories matching your primary function | The label is **Financial Services** (not "Finance"). Insurance or health apps may also fit **Consumer Health** or **Health & Life Sciences**. |
| **Icon** | Optional custom icon URL | A URL field, not a file upload. SVG and PNG both work. Preview with the **Check icon** button. |
Claude listings are much shorter than ChatGPT's: a tight one-liner plus a sentence or two, not a multi-paragraph blurb. Model yours on live consumer-facing listings with interactive widgets, such as **Era Context** ("Manage your personal finances using Claude") or **Booking.com** ("Find hotels, homes and more"), and note how short the copy is.
### Icon
The icon is an optional custom icon URL in the Listing step. Leave it blank and the directory falls back to your MCP server's favicon.
* SVG works (verified on a live submission); PNG works too. A square asset renders most consistently.
* The favicon fallback often renders dark-on-dark and disappears. Set a contrast-safe custom icon URL and preview it with the **Check icon** button.
* Design guidance is in the [MCP Apps for Claude Figma file](https://www.figma.com/community/file/1597641111449594397/mcp-apps-for-claude).
### Screenshots
Most MCP apps ship interactive widgets, so screenshots are required (a tool-only server with no UI skips this step). Hard requirements:
* **Format:** PNG (no video or GIF).
* **Width:** at least 1000px.
* **Count:** 3 to 5 images.
* **Crop to the app response only.** Do not include the prompt or Claude's chat chrome.
* **Aspect ratio:** any, kept consistent across the set.
* Provide the prompt text separately for each screenshot.
* No separate mobile assets; one batch covers all surfaces.
What to capture:
* Lead with your highest-value widget state (the populated quote card, the storefront answer, the analytics view), not an empty or loading state.
* Show populated data using the same fully-populated test account you give reviewers. No lorem ipsum.
* Use 3 to 5 distinct moments that tell a flow (answer, then personalized quote, then confirmation) rather than near-identical frames.
* Capture the widget at its natural rendered size so text stays legible at 1000px or wider.
* Avoid PII or real customer data; use representative seed data.
### Public URLs
All three must be live and public at review time.
| Asset | Requirement |
| ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------ |
| **Documentation URL** | Public setup and usage docs, live by submission. A help-center article, a blog post, or a page in these docs all work. |
| **Privacy Policy URL** | A live, publicly accessible HTTPS page covering what data is collected, why, who sees it, retention, user control, and contact info. |
| **Support URL or email** | A monitored support channel. Required in the Listing step. |
A missing or incomplete privacy policy is an immediate rejection. Open each URL in a private window to confirm it loads without a login.
### Company and contact details
Because you submit from your own org, the listing's company details are **yours**, not Waniwani's.
* [ ] Organization name and details.
* [ ] A primary contact on a working, monitored channel (reviewers may reach out).
## The form, step by step
Work through the wizard in order. Two quirks to know up front:
* **Re-selecting the connector resets fields.** Re-verify everything after any reconnect.
* **One submission at a time.** The wizard behaves like a single session; state is saved in the browser, but you cannot have more than one form submission open at once.
### Connection
Paste your production MCP URL. Set **URL configuration** to **Universal URL** (everyone connects to the same URL). This is correct in almost every case. Only use **Multiple URLs** (a fixed list) or **URL pattern** (a regex, for per-tenant or versioned subdomains) if you are specifically instructed to.
### Tools
Tools, prompts, and resources sync automatically from your server URL. If your annotations are set correctly (see [What to prepare](#tool-annotations)), this step passes on its own.
The portal reads tools from your cached custom-connector schema. If you change the MCP app, reconnect or refresh the custom connector on claude.ai: open it in another tab and refresh the connector you are submitting for.
### Listing
Paste the [listing copy](#listing-copy) you prepared: name, slug, one-liner, description, categories, icon URL, and support URL. Remember the slug is **permanent after submission**, and preview the icon with the **Check icon** button.
### Use cases
The Use cases step asks for:
* **Primary use cases:** the main tasks users accomplish, with a few example prompts.
* **Connection requirements:** what accounts, permissions, or setup a user needs before connecting.
* **Read / write capabilities:** a radio (Read only / Write only / Read and write).
The read/write radio is validated against your tool annotations. Choosing "Read only" while any tool is annotated as a write or destructive triggers an error. Match it to your tools.
### Company
Enter your organization's details and primary contact (see [Company and contact details](#company-and-contact-details)).
### Authentication
In almost every case a Waniwani MCP app needs **no authentication**, so pick *No authentication* unless told otherwise.
If your connector accesses **private user data**, Claude requires **OAuth 2.0**. The Authentication step offers several modes: OAuth with Dynamic Client Registration (DCR), OAuth with a Client ID Metadata Document (CIMD), OAuth with Anthropic-held client credentials, custom URL/credentials at connection time, and no authentication, plus a "Partial auth" toggle.
* With **DCR** (clients self-register, as Waniwani-hosted servers do) there is nothing to register; the form shows it is supported out of the box.
* For **CIMD, Anthropic-held, or static-client** modes, register the redirect URI `https://claude.ai/api/mcp/auth_callback`.
### Data handling
* [ ] **API ownership:** confirm your server calls first-party APIs (or legitimately proxied services). The MCP server domain should match your service domain.
* [ ] **Sensitive data:** flag any sensitive data your tools touch.
* [ ] **External links:** declare allowed link URIs (HTTPS origins or custom URI schemes you own). Third-party or unowned schemes are removed during review.
### Test and launch
Reviewers must be able to exercise every tool end to end. In most cases no special instructions are needed.
If a test account is required, we recommend stating that one will be provided on request, so you do not need it ready at submission, only when a reviewer asks.
* Provide **fully populated** credentials when asked (empty accounts are rejected).
* Provide step-by-step access instructions detailed enough to reach and run every tool.
* Confirm every tool runs end to end via MCP Inspector or as a custom connector in Claude.
### Compliance
Review and acknowledge all of Anthropic's policies (the Software Directory Terms and Policy, and the connector directory guidelines).
Anthropic does not support connectors that execute financial asset transfers (money or crypto) or perform AI image, video, or audio generation. Design tools that produce diagrams or charts are fine.
### Review and submit
* [ ] Resolve any quality warnings shown on the Review step.
* [ ] Submit.
Submitting does not guarantee a listing.
## Common rejection reasons
* Missing tool titles or annotations.
* Behavioral instructions embedded in tool descriptions.
* A missing or incomplete privacy policy.
## References
* [Submitting to the Connectors Directory](https://claude.com/docs/connectors/building/submission)
* [Pre-submission checklist and review criteria](https://claude.com/docs/connectors/building/review-criteria)
* [Connectors Directory FAQ](https://support.claude.com/en/articles/11596036-anthropic-connectors-directory-faq)
* [Submission portal](https://claude.ai/admin-settings/directory/submissions/new)
Lay out every tool's annotations before you submit.
Listing the same app in the ChatGPT Plugin Store.
Drive traffic to your app once it is live.
Add the Waniwani MCP server to Claude, Cursor, or ChatGPT.
# Submit your app to ChatGPT and Claude
Source: https://docs.waniwani.ai/platform/guides/overview
How to list an MCP app built on Waniwani in the ChatGPT Plugin Store and Claude's Connectors Directory: prerequisites, who submits each one, and what each review needs.
Once your app is built and deployed on Waniwani, the next step is getting it listed in the assistant directories so people can find and install it. There are two directories today: the **ChatGPT Plugin Store** and **Claude's Connectors Directory**. This section walks through what each one needs and gives you copy-pasteable templates for the parts reviewers scrutinize most.
A listing is never guaranteed. Both OpenAI and Anthropic review every app for quality and policy, and they may follow up with questions. These guides get you through review with the fewest surprises, but completing a form is not the same as being approved.
## Who submits
For both directories, we recommend submitting from **your own account**: the listing belongs to your organization and the Developer name points to your company, not Waniwani. The difference is how much flexibility each directory allows:
| | ChatGPT Plugin Store | Claude Connectors Directory |
| -------------------------------------- | -------------------------------------------------------------------------------------------------- | --------------------------------------------------------------- |
| Who submits | You, from your own OpenAI account (recommended). Waniwani can submit on your behalf if you prefer. | You, from your own Claude organization (required by Anthropic) |
| Account you need | An OpenAI developer account (none if Waniwani submits for you) | A Claude **Team or Enterprise** workspace, with you as an Owner |
| Where the listing's "Developer" points | Your company (`WaniWani` if we submit for you) | Your company |
For ChatGPT, the choice is yours: submit yourself or have Waniwani handle the mechanics. For Claude, Anthropic requires the listing to come from the app owner's own workspace, so the final submit always happens on your side. In both cases Waniwani prepares everything you paste in.
## What both directories require
Prepare these before you start either form. They are common to both reviews:
The form needs a stable HTTPS MCP URL. Find yours at [app.waniwani.ai/mcp](https://app.waniwani.ai/mcp): your apps are listed there, and the dropdown next to each one copies its production URL. It follows a versioned pattern, for example `https://v1..mcp.waniwani.run/mcp`.
Every tool needs accurate hints (read-only, destructive, and for ChatGPT also open-world) plus a written justification. This is the single most common reason apps get rejected. See the [Tool justification template](/platform/guides/templates/tool-justification). For managed Waniwani apps these are set in the server code for you before submission.
Both reviews require a publicly accessible privacy policy URL and a monitored support URL or email. Open each link in a private browser window to confirm it loads without a login. A missing or gated privacy policy is an immediate rejection.
Claude requires a live documentation URL at review time, and it helps your ChatGPT listing too. These submission guides themselves can serve as your documentation URL if you do not have a dedicated page yet.
A name, a short tagline, a description, and a square logo (a contrast-safe icon that reads on dark backgrounds). The exact limits differ per directory and are covered in each guide.
## Two hard eligibility limits
Both assistants reject the same two categories of app, so confirm yours does neither inside the chat:
* **No moving money or crypto inside the conversation.** Handing the user off to your own site to finish a purchase is fine. Showing a quote or an estimate is fine.
* **No AI generation of images, video, or audio.** Tools that produce diagrams or charts are fine.
## Pick your path
The full checklist for the ChatGPT Plugin Store: app info, tool justification, test cases, screenshots, and compliance.
The Connectors Directory flow: Team/Enterprise prerequisite, the in-product portal, annotations, and listing details.
Submitting is the start. How to drive traffic to your app and capture it: agent on your site, GEO, and PR.
Copy-pasteable tool justification and test-case templates for the parts reviewers check hardest.
# After you submit: drive traffic to your app
Source: https://docs.waniwani.ai/platform/guides/post-submission
Submitting your app is the start, not the finish. The three levers that get traffic to your app and capture it: putting the agent on your website, GEO, and PR, plus the KPIs to track.
Getting listed is the start, not the finish. Today, ChatGPT and Claude do **not** suggest apps proactively: your app shows up when the assistant searches the web and finds content saying it exists and how to use it. So once you are submitted, the real work is to **drive traffic to the app** and **capture that value on your own site**. This page explains what to do, in what order, and how to measure it.
## The two milestones
1. **Traffic on the app** inside ChatGPT and Claude: installs, conversations, and the quotes or leads it generates.
2. **Traffic on the agent on your website:** sessions, leads captured, and conversion compared to your classic funnel.
## The three levers
| Lever | What it is for |
| --------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| **Put the agent on your website** | Capture value where you control the experience, without depending on the assistant's recommendation. |
| **Do GEO** | Make the assistants aware the app exists so they suggest it when a user asks. |
| **Do PR and comms** | Create third-party corroboration (assistants weigh information that appears across several sources) and drive direct traffic. |
### Lever 1: put the agent on your website
The fastest way to capture leads is to run the same agent on your own site, where you control the experience and do not depend on an assistant surfacing your app. The Waniwani [chat widget](/sdk/chat/embed) embeds the agent on any page. This is the highest-leverage step, so do it first.
### Lever 2: GEO (generative engine optimization)
GEO is the practice of publishing content that teaches assistants your app exists and how to install it, so they describe it correctly and suggest it when relevant.
It works. We ran a controlled experiment on an app with zero starting authority (NA Drink Finder, an independent ChatGPT app) on a brand-new domain, with no other marketing input, just content: 74 GEO-optimized articles over 33 days, with 1,881 prompts tracked across ChatGPT, Perplexity, and Google AI Overview.
| Metric | Before | After |
| ------------------------------------------------------------------- | ------ | --------------------------------------- |
| ChatGPT describes the app correctly as a ChatGPT app | 4.5% | 45.7% (10x) |
| Hardest case: prompts where the app name is **not** in the question | 0% | about 30% |
| ChatGPT wrongly describes it as a mobile app | 40.9% | 17.1% |
| ChatGPT denies the ChatGPT app exists | 4.5% | 0% |
| Install syntax `@AppName` present in the answer | 0% | up to 50% of answers citing our content |
The takeaway: before GEO, ChatGPT did not suggest the app, described it as a mobile app, or said it did not exist. After publishing content, it described the app correctly **and** explained how to install it. Answers citing our content described the app correctly 2.3x more often.
Read the full write-up at [waniwani.ai/research/chatgpt-app-geo-experiment](https://waniwani.ai/research/chatgpt-app-geo-experiment).
### Lever 3: PR and comms
Comms has two goals: create third-party corroboration and generate direct traffic. A reusable plan for a launch:
1. **Press release** (under 500 words, with a "first" or "new" angle, your site URL, and your app URL).
2. **Pitch one reference industry publication** (one trade outlet beats ten generic tech blogs).
3. **LinkedIn:** one post from the company page and one personal post from the founder (personal posts reach further).
4. **Email** your prospect and partner base announcing the launch.
5. **A results post at day 30 or 60** ("we generated X quotes through ChatGPT in one month"). This is highly citable and creates a second wave of content.
A strong angle compounds: the first live insurance app on ChatGPT got picked up by industry press precisely because "first" is newsworthy. Find the equivalent "first" or "new" angle for your category.
## KPIs to track
**The app (ChatGPT and Claude)**
| KPI | Source |
| ------------------------------------------------------------- | ---------------------------- |
| App installs | OpenAI / Anthropic dashboard |
| Sessions and conversations on the app | Waniwani |
| Quotes or leads generated via the app | Waniwani |
| Redirects to the website (UTM) | Your analytics |
| Share of tracked prompts where the assistant mentions the app | Waniwani (GEO) |
| Answer quality (app described correctly, install flow given) | Waniwani (GEO) |
| Citation rate of your content | Waniwani (GEO) |
**The agent on your website**
| KPI | Source |
| ------------------------------------------------------------------------------- | ---------------------- |
| Sessions on the agent | Waniwani |
| Funnel: visitors, engaged (more than 1 prompt), email captured, qualified leads | Waniwani |
| Email capture rate | Waniwani |
| Conversion vs the classic form funnel | Waniwani and your data |
## What to expect
Set expectations early: the assistants will not surface your app on day one. Treat the weeks after launch as an ongoing program rather than a single event. Review the KPIs above on a regular cadence (a bi-weekly check-in works well), and let the data point you at which lever to push next.
Put the same agent on your website with the chat widget.
The sessions, events, and funnel metrics behind these KPIs.
# Test cases template
Source: https://docs.waniwani.ai/platform/guides/templates/test-cases
A copy-pasteable template for the ChatGPT Plugin Store test-case step: the 5 positive cases where your app should trigger and the 3 negative cases where it should not.
OpenAI reviewers run your test cases to validate your app, so this section is reviewed carefully. You need at least **5 positive** cases (where the app should trigger and work) and at least **3 negative** cases (where it should not). Lay them out here before you open the form, then paste each value in.
For managed Waniwani apps you receive these pre-filled based on your app's tools and behavior. Review them to confirm they match how your app actually works before submitting.
## Positive test cases (minimum 5)
Scenarios where your app **should** trigger and work correctly. For each, the form asks for the scenario, the user prompt, which tools fire, and the expected output.
| # | Scenario | User prompt (paste into form) | Tools triggered | Expected output |
| - | ----------------------------- | ---------------------------------- | --------------- | -------------------------- |
| 1 | What the user is trying to do | The exact prompt a user would type | `tool_name` | What the app should return |
| 2 | | | | |
| 3 | | | | |
| 4 | | | | |
| 5 | | | | |
Add rows for any additional use cases worth covering.
## Negative test cases (minimum 3)
Prompts where your app should **not** trigger. These confirm your app does not activate inappropriately. For each, the form asks for the scenario and a prompt.
| # | Scenario | User prompt (paste into form) | Why the app should NOT trigger |
| - | -------------------- | ---------------------------------- | ------------------------------ |
| 1 | An unrelated request | A prompt outside your app's domain | Why this is out of scope |
| 2 | | | |
| 3 | | | |
## Writing good test cases
* **Positive cases should be realistic.** Use prompts a real user would actually type, phrased naturally, not keyword-stuffed prompts engineered to trigger the app.
* **Cover distinct paths.** Spread the five across different tools and use cases rather than five variations of the same request.
* **Negative cases should be plausibly nearby.** The strongest negatives are requests in an adjacent area where the app should stay quiet, not random unrelated prompts. They show the app has well-defined boundaries.
* **Match the expected output to reality.** Reviewers run these against your live app, so describe what it genuinely returns.
Where this template fits in the full checklist.
The other section reviewers check hardest.
# Tool justification template
Source: https://docs.waniwani.ai/platform/guides/templates/tool-justification
A copy-pasteable template for the ChatGPT Plugin Store tool-annotation step: list every tool, its read-only / open-world / destructive values, and the written justification reviewers require.
The tool-annotation step is the most common reason a ChatGPT submission is rejected. For **every tool** your MCP server exposes, OpenAI requires three annotation values and a written justification for each. This page gives you a template to lay them all out before you open the form, and a reference for what each value means.
For managed Waniwani apps, the annotations are already set in the server code and you receive these justifications pre-filled to review and paste. This template is for laying them out yourself or for checking that the prepared values match what your app actually does.
## How to use this
1. Add one row per tool to the table below.
2. Fill the Yes/No values to match how each tool behaves (use the reference at the bottom if unsure).
3. Write a one or two sentence justification for each value, stating plainly why it is accurate.
4. When the form scans your server, confirm the values it shows match this table, then paste your justifications.
## Tool annotations
Copy this table and fill one row per tool:
| Tool name | Read Only | Read Only justification | Open World | Open World justification | Destructive | Destructive justification |
| ------------- | --------- | -------------------------- | ---------- | -------------------------- | ----------- | -------------------------- |
| `tool_name_1` | Yes / No | Why this value is accurate | Yes / No | Why this value is accurate | Yes / No | Why this value is accurate |
| `tool_name_2` | Yes / No | Why this value is accurate | Yes / No | Why this value is accurate | Yes / No | Why this value is accurate |
| `tool_name_3` | Yes / No | Why this value is accurate | Yes / No | Why this value is accurate | Yes / No | Why this value is accurate |
Add or remove rows so the count matches your server exactly.
## CSP metadata
If any tool interacts with external services, list its domains:
| Tool name | Connect domains | Resource domains |
| ------------- | --------------------- | --------------------- |
| `tool_name_1` | `api.yourcompany.com` | `cdn.yourcompany.com` |
## What each annotation means
| Annotation | Question | Set to **Yes** when | Set to **No** when |
| --------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------ |
| **Read Only** | Does this tool only read data without modifying anything? | The tool only fetches or displays data; nothing is created, updated, or deleted. | The tool creates, updates, or deletes any data. |
| **Open World** | Does the tool interact with an open world of external entities, or is its domain closed? | The tool calls an external API or service the user defines (a web search, an arbitrary backend). | The tool only uses local data, renders UI with no external calls, or uses pre-approved internal APIs fully under your control. |
| **Destructive** | Can this tool delete, overwrite, or irreversibly modify data? | The tool can delete records, overwrite data, or cause irreversible changes. | The tool only reads or creates new data without modifying or deleting existing records. |
If the annotations are not set, OpenAI assumes the worst defaults: `readOnlyHint = false`, `openWorldHint = true`, `destructiveHint = true`. That is why setting and justifying them accurately matters. For more background, see the [MCP tool annotations post](https://blog.modelcontextprotocol.io/posts/2026-03-16-tool-annotations/).
Where this template fits in the full checklist.
The other section reviewers check hardest.
# What is Waniwani?
Source: https://docs.waniwani.ai/platform/introduction
Waniwani turns AI assistants like ChatGPT and Claude into a channel you can show up in, convert from, and measure.
People used to start with a search engine. Now they ask ChatGPT, Claude, and other AI assistants — and they act on what those assistants tell them. That shift moves the first, most important moment of your funnel out of your website and into a conversation you don't control.
**Waniwani is how you show up in that conversation, turn it into a customer, and measure what happened.**
## What Waniwani does
Instead of a static page a person reads, Waniwani lets you publish a guided, multi-step experience that runs *inside* the assistant. The assistant asks the right questions, captures the answers, branches on what it learns, and walks the person all the way to a booking, a quote, a signup, or a qualified lead — without ever leaving the chat.
You get the three things a channel needs to matter:
* **Presence** — be discoverable and citable when an assistant answers questions in your space.
* **Conversion** — run real funnels in the conversation, not just a link out to your site.
* **Measurement** — see which assistants drive sessions, where people drop off, and what converts.
## How it fits together
Waniwani is built on the [Model Context Protocol (MCP)](https://modelcontextprotocol.io), the open standard AI assistants use to connect to outside tools and data. Connecting your funnel to an assistant is the same kind of integration as connecting any other MCP tool — so the same experience works across every MCP-capable assistant at once.
There's an open-source SDK for building the experience, a hosted platform for state, analytics, and content, and a managed MCP server that exposes it to assistants. You'll find each of those in its own section.
## Where to go next
This page is the high-level picture. The rest of the docs go deep:
* **SDK** — build and ship your funnels.
* **MCP Server** — connect Waniwani to ChatGPT, Claude, and other assistants.
* **[Regions & data residency](/platform/regions)** — pick EU or US when you create an organization.
# Regions & data residency
Source: https://docs.waniwani.ai/platform/regions
Waniwani runs in two regions — EU and US. You pick where an organization's data lives when you create it, and everything for that org stays on that stack.
Waniwani runs in **two regions**, EU and US. Each is an independent stack with its own database: an organization's data lives entirely in the region you choose and never crosses to the other.
| Region | Dashboard | MCP server |
| ------ | ---------------------------- | -------------------------------- |
| **US** | `https://app.waniwani.ai` | `https://mcp.waniwani.ai/mcp` |
| **EU** | `https://eu.app.waniwani.ai` | `https://eu.mcp.waniwani.ai/mcp` |
## Choosing a region
You choose a region **when you create an organization**. Before the org-creation wizard runs, Waniwani prompts you to pick your **data residency**:
* **EU** — for teams that need European storage and processing (EU residency requirements).
* **US** — for US-based teams and global teams without EU residency needs.
Your choice applies to **that organization**. Everything downstream — the workspace, members, environments, API keys, conversations, analytics, and stored content — is created on the region's stack and stays there.
Data residency is set per organization and can't be changed after creation. If you need both regions, create a separate organization on each stack.
## Signing in
Sign in on the dashboard for the region your organization lives in:
* **US:** [app.waniwani.ai](https://app.waniwani.ai)
* **EU:** [eu.app.waniwani.ai](https://eu.app.waniwani.ai)
The US dashboard won't show organizations that live in the EU, and vice versa. If you pick a region during onboarding that differs from the stack you signed in on, Waniwani moves you to the right stack and you continue there — your account (email, name, linked sign-in providers) is portable, but sessions and org data always stay local to each region.
## Connecting AI clients
The [Waniwani MCP server](/mcp-server/overview) — the one that lets an AI client operate the platform for you — is also regional. Use the MCP URL for the region your data lives in, and sign in on the matching stack:
* **US:** `https://mcp.waniwani.ai/mcp`
* **EU:** `https://eu.mcp.waniwani.ai/mcp`
See [Connect your AI client](/mcp-server/connect) for the per-region setup in Claude, Cursor, and ChatGPT.
# Service accounts
Source: https://docs.waniwani.ai/platform/scopes
Issue a workspace-wide service-account key, choose the scopes it can hold, and control destructive operations.
When you connect WaniWani to a workspace-wide tool — for example the [Claude-in-Slack connector](/mcp-server/connect) — an organization admin issues a **service-account key** (`wws_…`) from **Settings → Service accounts**. The key acts on its own labelled identity, and the admin chooses up front exactly how much of the organization it may touch:
* a set of **scopes** — the resources the key can read and write, and
* a **destructive-operations** toggle — whether it may deploy, connect a repo, or delete whole resources.
The key is shown once, at creation. You can revoke it at any time, which takes effect immediately.
## Permissions & scopes
A key carries a fixed set of scopes, chosen when you issue it. Each area has a `read` and a `write` scope, and the key holds exactly the ones you select.
A new key is **read-only** — `mcp:read` and `kb:read` — until you add more scopes.
| Scope | Description |
| --------------------- | ------------------------------------------------------------------------------------------ |
| `mcp:read` | View agents and their environments, channels, and deployments |
| `mcp:write` | Create and edit agents, environments, and channels |
| `kb:read` | View knowledge base sources and their content |
| `kb:write` | Add, edit, and remove knowledge base content |
| `analytics:read` | View traffic, sessions, funnels, and insights |
| `analytics:write` | Create and edit funnels and analytics views |
| `evals:read` | View evaluations, evaluators, and their results |
| `evals:write` | Create, edit, and run evaluations |
| `conversations:read` | View personas, their conversations, and shared submissions |
| `conversations:write` | Manage personas and conversation submissions |
| `competition:read` | View tracked competitor companies |
| `competition:write` | Add and edit tracked competitor companies |
| `geo:read` | View GEO visibility: brand presence in AI answers, monitored prompts, and competitor stats |
| `geo:write` | Manage GEO tracking: monitored prompts, sources, and tags |
## Destructive operations
Some actions can't be undone or take a running agent offline. A service-account key may perform them **only when the “Allow destructive operations” toggle is enabled** for that key. The toggle is **off by default** and is independent of the scopes above: a key with `mcp:write` can edit content, but it still cannot run a destructive action unless the toggle is on.
The toggle only affects service-account keys. It does not change what a signed-in teammate can do, and it does not widen the key's scopes — it only unlocks the operations below for scopes the key already holds.
With the toggle enabled, the key may:
**Deploy & connect**
* Clone managed project repo to GitHub
* Connect project to a user-owned GitHub repo
* Deploy the latest commit on an environment's branch
* Migrate a managed project to a user-owned repo
* Promote a channel
* Redeploy a past deployment's commit
* Roll back production to a past deployment
**Delete a whole resource**
* Delete a project
* Delete environment
* Delete environment channel
Everyday content changes — including deleting individual items such as a knowledge-base source, an evaluator, or an email template — need only the matching `write` scope and are **not** gated by this toggle.
# Changelog
Source: https://docs.waniwani.ai/sdk/changelog
Release notes for the Waniwani SDK, including breaking changes and deprecated APIs.
This page tracks API changes that require code updates, organized by the version that introduced them. Deprecations are safe to ignore short-term — the old shape keeps working until the removal version listed in the deprecation notice.
## Deprecations at a glance
| API | Status | Since | Removed in |
| ------------------------------------------------------------------------------------------- | ----------- | ------ | ----------------------------- |
| `track.lead()` (alias of `track.leadQualified()`) | **Removed** | 0.15.1 | 0.16.0 |
| `.addNode(id, run, options?)` (positional) | Deprecated | 0.12.0 | 0.13.0 |
| `createTool`, `createResource`, `registerTools` from `@waniwani/sdk/mcp` | Deprecated | 0.12.0 | 0.14.0 |
| MCP-widget React host (`WidgetProvider`, `useToolOutput`, …) from `@waniwani/sdk/mcp/react` | Deprecated | 0.12.0 | 0.14.0 |
| `toNextJsHandler` from `@waniwani/sdk/next-js` | Deprecated | 0.12.0 | 0.14.0 |
| `toExpressJsHandler` from `@waniwani/sdk/express-js` | Deprecated | 0.12.0 | 0.14.0 |
| Chat-server types from `@waniwani/sdk/chat/server` | Deprecated | 0.12.0 | 0.14.0 |
| `ChatCard` from `@waniwani/sdk/chat` | Deprecated | 0.12.0 | — still exported as of 0.18.1 |
| `SuggestionsConfig.dynamic` on `` (`@waniwani/sdk/chat`) — use `origins` | Deprecated | 0.19.1 | — |
| `evals/*` (entire subtree) | **Removed** | 0.12.0 | 0.12.0 |
TypeScript will strike through deprecated signatures in your IDE. Hover for the replacement.
## Breaking changes at a glance
| Change | Kind | Version | Auto-fixable |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| Flow `interrupt({ suggestions })` now renders as clickable pills in the Waniwani chat widget, gated by a named `suggestionOrigins` list instead of a single `dynamic` boolean | Behavior change | 0.19.1 | No action needed to read events; only code that constructs a `suggestion.clicked` event literal must add `origin` (mechanical, see below) |
| Generic funnel events `quote.requested` / `quote.succeeded` / `quote.failed` / `purchase.completed` removed from the `track()` taxonomy (with their property types and legacy input fields) | Removal | 0.18.0 | Yes (codemod below) |
| `useWaniwani()` no longer auto-discovers config (removed `WidgetProvider` context read + self-connecting host bridge); resolves from explicit `{ endpoint, source }` or a passed `toolResponseMetadata` only | Behavior change | 0.17.0 | Yes (codemod below) |
| `useWaniwani()` returns the typed server `track` surface; string `track(name, props)`, `step()`, `conversion()`, and the `capture` option are removed | API change | 0.16.0 | Yes (codemod below) |
| Widget auto-capture event names (`widget_click`, `widget_scroll`, `widget_form_*`, `widget_link_click`, `widget_error`) removed from `EventType` | Removal | 0.16.0 | Yes (codemod below) |
| Widget config `_meta` key: `waniwani` becomes `waniwani/widget` | Rename | 0.16.0 | Yes (codemod below) |
| `track.lead()` alias removed (deprecated since 0.15.1) | Removal | 0.16.0 | Yes (codemod below) |
| `createTrackingRoute` accepts the V2 batch envelope instead of the snake\_case widget payload | Contract change | 0.16.0 | Yes (codemod below) |
| `track.lead()` → `track.leadQualified()`, event `"lead"` → `"lead_qualified"` | Rename | 0.15.0 | Yes (codemod below) |
| `addConditionalEdge(from, condition)` → `addConditionalEdge(from, to, condition)` | Signature change | 0.14.0 | Yes (codemod below) |
When you bump `@waniwani/sdk` across a minor version (`0.x` minors can break), scan this page for every breaking change between your old and new version and apply the documented migration. Each one below is a mechanical rewrite an agent or codemod can apply in a single pass, followed by `bun run typecheck && bun test`.
**Migrate automatically.** Every version hop with breaking changes ships a self-contained migration skill named `migrate-waniwani-sdk--to-`. Add the one for your hop and an agent applies the whole section for you:
```bash theme={null}
npx skills add Waniwani-AI/sdk -s migrate-waniwani-sdk-0.18-to-0.19
```
***
## 0.19.1: flow suggestions render as pills
Apply this entire section automatically: `npx skills add Waniwani-AI/sdk -s migrate-waniwani-sdk-0.18-to-0.19`, then invoke it from your project on 0.18.x.
**Behavior change: no code change required unless you construct `suggestion.clicked` event literals.**
`suggestions` declared on an `interrupt()` question already reached the
assistant as candidate answers. They now also render as clickable pills above
the input in the Waniwani chat widget, on any step where exactly one question is
still open. Clicking a pill sends it as the visitor's answer.
```ts theme={null}
// unchanged code, new behavior
interrupt({
plan: {
question: "Which plan fits you?",
suggestions: ["Bronze", "Silver", "Gold"],
},
})
```
What this means in practice:
* **If your flows already declare `suggestions`, pills will start appearing**
once you opt the `"flow"` origin in (see below). Nothing renders for steps
that declare none, and nothing renders for steps with two or more open
questions.
* **Other MCP hosts are unaffected.** ChatGPT and Claude have no pill row, and the
flow tool's `structuredContent` and `outputSchema` are unchanged.
* **The pill row is now gated by four named origins instead of one `dynamic`
boolean**: `"channel"` (starter prompts), `"page"` (per-URL starter prompts),
`"flow"` (a flow's `interrupt({ suggestions })`), and `"followup"` (pills
generated from the conversation, self-hosted backends only). Unset, the
default is `["channel", "page", "followup"]`: starter prompts and generated
follow-ups render, flow-driven pills stay opt-in. To render flow-driven
pills, include `"flow"`:
* ``: `overrides={{ suggestionOrigins: ["channel", "page", "flow", "followup"] }}`
* `
```
If you want React-component-level control over the inline mount, see [Chat in React](/sdk/chat/react).
## Sizing
Inline mode only — in `floating` mode the panel sizes itself.
The embed fills its container. Give the `
` a definite size via CSS, or set `data-height` on the script tag:
```html theme={null}
```
A container you leave unstyled is `500px` tall. That default is written with `:where()`, so it carries no specificity and any rule you write beats it — including `height: auto` to let the chat grow with its content. A `ResizeObserver` mirrors the container's `max-height` onto the embed so padding and borders are respected.
## Overriding attributes
You can edit `data-*` attributes on the `
```
`init()` returns an instance with `destroy()` and `sendMessage(text)`. You can also call `window.WaniWani.chat.destroy()` and `window.WaniWani.chat.sendMessage("...")` directly on the global.
`init()` also accepts an `onEvent` callback that mirrors chat lifecycle events (opens, messages, errors, link clicks) into your own analytics — Amplitude, Segment, gtag — with your page's identity attached automatically. See [Widget events (onEvent)](/sdk/chat/widget-events).
## Link visitors to your analytics
By default the embed mints its own anonymous **visitor id**, a stable opaque value persisted in the browser's `localStorage`. It rides on every chat request and tracking event, so a visit is attributable before the first message mints a session (see [Sessions](/sdk/tracking/sessions)).
If your site already tracks visitors with PostHog, Amplitude, Segment, or a first-party cookie, override that id with your own. Waniwani then correlates its sessions and events to the same visitor you see in your analytics, and your server-side MCP tools and flows read the id back as `context.waniwani.visitorId`, so they can send events straight to the same analytics tool the id came from.
There are three ways to set it. Pick the one that matches when your id is available.
### If you know the id up front
Set it declaratively on the script tag:
```html theme={null}
```
Or pass it to `init()` when you initialize programmatically. `init()` accepts a string or a resolver (sync or async), so you can read the id inline:
```js theme={null}
window.WaniWani.chat.init({
token: "wwp_...",
visitorId: "the-id-from-your-system",
// or a resolver: visitorId: () => posthog.get_distinct_id(),
});
```
### If the id resolves asynchronously (the common case)
Analytics SDKs usually assign a distinct id only after they bootstrap, so read it when it's ready and hand it to the widget. `setVisitorId()` is safe to call at any time, before or after `init()`, and the new id applies to the next chat request and tracking event:
```js theme={null}
// PostHog
posthog.onFeatureFlags(() => {
window.WaniWani.chat.setVisitorId(posthog.get_distinct_id());
});
// Amplitude
const amplitudeId = amplitude.getDeviceId();
if (amplitudeId) {
window.WaniWani.chat.setVisitorId(amplitudeId);
}
```
A blank value is ignored, so an id that isn't ready yet never wipes a good one. Read the id the widget will send with `window.WaniWani.chat.getVisitorId()`.
The visitor id identifies a **device / browser**, not a signed-in account. When a visitor logs in, keep the visitor id and additionally call [`identify()`](/sdk/tracking/identify) with your account id, so anonymous and known activity stitch together.
## Self-hosting the JS bundle
If you'd rather not load from a CDN, pin a specific SDK version and serve the bundled file yourself. The `data-*` attributes still come from the dashboard snippet — you're only swapping where `embed.js` is served from:
```bash theme={null}
bun add @waniwani/sdk
cp node_modules/@waniwani/sdk/dist/chat/embed.js public/waniwani-embed.js
```
Then point the dashboard-issued snippet at your local copy:
```html theme={null}
```
## How it works
1. The script reads its own `data-*` attributes (token, channel ID, mode, theming, content overrides).
2. On `DOMContentLoaded` (or immediately, if the DOM is already ready), it mounts the chat via Shadow DOM — inside `[data-waniwani-embed]` in `inline` mode, or in its own anchored container in `floating` mode.
3. The chat calls `app.waniwani.ai/api/mcp/chat` with the project token and streams responses back through Server-Sent Events.
4. Conversation history lives only in memory by default; set `data-enable-thread-history` to persist threads in IndexedDB on the user's device.
No server-side integration on your side.
# Chat in React
Source: https://docs.waniwani.ai/sdk/chat/react
Embed the Waniwani chat as a React component in your app.
Two React components ship from `@waniwani/sdk/chat`. Pick based on who runs the chat backend:
| Component | Use when | Backend |
| ------------------ | ----------------------------------------- | -------------- |
| **`WaniwaniChat`** | You want the hosted chat. **Start here.** | Ours, no setup |
| `ChatEmbed` | You self-host the chat backend | Yours |
The React components mount inline: they render wherever you place them in your layout, and you control the container. There is no launcher or floating bubble in the React path — for that, use the [embed script](/sdk/chat/embed) in `floating` mode.
**Platform feature.** Requires a Waniwani project token (`wwp_...`). [Get one at app.waniwani.ai](https://app.waniwani.ai) or read more about the [Platform](/sdk/platform/overview).
## Install
```bash theme={null}
bun add @waniwani/sdk
```
Add the stylesheet to your app once (e.g. in `_app.tsx` or your root layout):
```ts theme={null}
import "@waniwani/sdk/chat/styles.css";
```
## WaniwaniChat
The hosted chat. Pass your project token and channel ID and it talks to `app.waniwani.ai` directly — no API route, no proxy, no backend work on your side.
```tsx theme={null}
import { WaniwaniChat } from "@waniwani/sdk/chat";
;
```
The system prompt, welcome message, placeholder, suggestions, thread history and tool-call display are configured per channel in the dashboard, so business users can change them without a deploy. Theming has no dashboard equivalent — set it in code via `overrides.appearance`.
### Sizing and placement
`WaniwaniChat` fills its parent. Put it wherever you want it and size the container:
```tsx theme={null}
```
To use it as a full-width page hero, give the wrapper the height you want and let the chat fill it. `className` is applied to the root element if you'd rather style it directly.
### Per-page overrides
The dashboard is the source of truth. Use `overrides` only for a local tweak that doesn't justify a separate channel:
```tsx theme={null}
;
```
| Override | Notes |
| --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `title` | Sticky header title |
| `hideHeader` | Force-hide the header when your page already provides chrome |
| `welcomeMessage` | Greeting before the first user message |
| `welcome` | Rich welcome screen (icon, title, suggestion cards). Takes precedence over `welcomeMessage` |
| `placeholder` | Input placeholder |
| `suggestions` | Initial suggestion chips |
| `suggestionOrigins` | Which providers may fill the per-turn pill row: any of `"channel"`, `"page"`, `"flow"`, `"followup"`. Defaults to `["channel", "page", "followup"]` — include `"flow"` to render pills from a flow's `interrupt({ suggestions })` |
| `enableThreadHistory` | Persist threads in IndexedDB |
| `showToolCalls` | `true` (default), `"titles-only"`, or `false` |
| `allowAttachments` | Enable file attachments in the input |
| `appearance` | Theme preset plus per-property overrides. Not dashboard-backed — this is where theming is set — see [Theming](/sdk/chat/theming) |
| `classNames` | Per-slot class overrides — see [Deep customization](#deep-customization) |
| `disclaimer` | AI transparency notice under the input. A string overrides the wording, `false` hides it |
| `locale` | UI language for built-in labels: `"en"`, `"fr"`, `"es"`. Detected from `` / `navigator.language` when unset |
| `messages` | Per-key overrides on the resolved locale catalog, to tweak individual built-in strings |
| `disablePageView` | Opt out of the `page.viewed` event fired once on mount. Set on surfaces where a page view would pollute the funnel |
| `api` | Chat API URL. Rarely needed — defaults to `https://app.waniwani.ai/api/mcp/chat` |
| `mcpServerUrl` | Override the MCP server URL. Rarely needed |
### Tracking from the host page
The `ref` exposes a `ChatHandle` with `track` and `identify` alongside the chat controls. `identify` takes the user id as its first argument, with optional traits second — it is not an object:
```tsx theme={null}
import { WaniwaniChat, type ChatHandle } from "@waniwani/sdk/chat";
const chat = useRef(null);
;
chat.current?.identify(user.id, { plan: "pro" });
chat.current?.track.leadQualified({ /* … */ });
```
Both are present on `WaniwaniChat` only — the bare `ChatEmbed` primitive has no Waniwani credential to send events with. See [Identify](/sdk/tracking/identify).
## ChatEmbed
A bare-bones primitive for when you run the chat backend yourself. `api` is required and there is no default; pass any endpoint implementing the [Vercel AI SDK chat protocol](https://ai-sdk.dev) and the component handles the rest.
Reach for `ChatEmbed` only when self-hosting. If you're using the Waniwani Platform, `WaniwaniChat` gives you the same UI plus dashboard configuration, visitor correlation, and tracking.
```tsx theme={null}
import { ChatEmbed } from "@waniwani/sdk/chat";
;
```
## Link visitors to your analytics
Both components accept a `visitorId` prop to override the anonymous id the widget generates with one you already track (a PostHog / Amplitude / Segment distinct id, your own cookie). Waniwani correlates its sessions and events to the same visitor your analytics sees, and your server-side MCP tools and flows read it back as `context.waniwani.visitorId`. Leave it unset to keep the auto-generated, `localStorage`-persisted id.
`visitorId` accepts a string, or a resolver that returns one. The resolver may be sync or async, which is handy when your analytics SDK only exposes its id after it bootstraps:
```tsx theme={null}
// A string you already hold
;
// A sync resolver
posthog.get_distinct_id()} />;
// An async resolver — the widget uses the auto id until this settles
(await analytics.ready()).anonymousId}
/>;
```
A blank or failed result is ignored, so a not-ready id never wipes a good one. The value is read live on every request, so updating it applies to the next message. See [Sessions](/sdk/tracking/sessions) for how the three correlation ids fit together.
For an expensive async resolver, pass a stable reference (`useCallback`) so it isn't re-invoked on every render.
## Theming
Both components take an appearance preset (`light`, `dark`, or `auto`) with optional per-property overrides layered on top:
```tsx theme={null}
;
```
`auto` follows the host's `prefers-color-scheme`. `variables` accepts every `ChatTheme` token — colors, message-bubble radius/padding/max-width, base font size and line height. The full token and CSS-variable list lives on [Theming & customization](/sdk/chat/theming).
## Deep customization
Two escape hatches when tokens aren't enough:
* **`appearance.assistantBubble`** — opt-in filled bubble for assistant replies (plain text by default), styled by `assistantBubbleColor` / `assistantBubbleTextColor`.
* **`classNames`** — per-slot class overrides merged onto the widget's own classes: `root`, `header`, `message`, `userBubble`, `assistantBubble`, `input`. Pass it directly on `ChatEmbed`, or as `overrides.classNames` on `WaniwaniChat`.
```tsx theme={null}
;
```
Recipes and the list of what remains non-themeable: [Theming & customization](/sdk/chat/theming).
## Mirroring events into your analytics
The hosted `WaniwaniChat` component accepts an `onEvent` prop that fires on chat lifecycle events (messages, session start, errors, link clicks) so you can mirror widget activity into your own analytics — Segment, Amplitude, gtag — with the page's identity attached automatically. `ChatEmbed`, the bring-your-own-backend primitive, does not expose it. See [Widget events (onEvent)](/sdk/chat/widget-events).
# Theming & customization
Source: https://docs.waniwani.ai/sdk/chat/theming
Rebrand the chat widget — colors, typography, message bubbles, and deep CSS customization — on both the script embed and the React component.
Every surface of the chat widget is themeable through the same token system, whether you use the [script embed](/sdk/chat/embed) or the [React component](/sdk/chat/react). Defaults are pinned so an untouched widget looks the same across releases — you only override what you care about.
## Presets
Pick `light` (default), `dark`, or `auto` (follows the visitor's `prefers-color-scheme`):
```html theme={null}
```
```tsx theme={null}
// React
```
## Theme tokens
Layer per-property overrides on top of a preset. The same keys work everywhere:
```tsx theme={null}
// React — ChatEmbed or WaniwaniChat (via overrides.appearance)
```
```js theme={null}
// Script embed — programmatic init
window.WaniWani.chat.init({
token: "wwp_...",
appearance: {
theme: "light",
variables: { primaryColor: "#0a6c74", messageBorderRadius: 20 },
},
});
```
## CSS variables
The widget exposes a `--ww-*` namespace that pierces the Shadow DOM boundary, so the script embed can be themed from your page's own CSS:
```html theme={null}
```
In `floating` mode there is no container of your own, so declare the variables on any ancestor — `:root` is the simplest. They inherit down into the widget either way.
Unset variables fall back to the preset's defaults, and your overrides win in both light and dark modes.
`borderWidth` and `boxShadow` are the panel's opt-in chrome — both are no-ops until you set them, so a widget slotted into your own bordered container doesn't draw a second border. In the script embed they apply to the `[data-waniwani-embed]` container rather than the chat inside it, since the container clips anything drawn within.
| CSS Variable | Theme token | Property | Default (light) | Default (dark) |
| ---------------------------- | -------------------------- | -------------------------------- | --------------- | -------------- |
| `--ww-primary` | `primaryColor` | Primary brand colour | `#6366f1` | `#6366f1` |
| `--ww-primary-fg` | `primaryForeground` | Text on primary | `#1f2937` | `#ffffff` |
| `--ww-bg` | `backgroundColor` | Panel background | `#ffffff` | `#212121` |
| `--ww-text` | `textColor` | Default text colour | `#1f2937` | `#ececec` |
| `--ww-muted` | `mutedColor` | Secondary text | `#6b7280` | `#8e8ea0` |
| `--ww-border` | `borderColor` | Border colour | `#e5e7eb` | `#444444` |
| `--ww-border-width` | `borderWidth` | Panel border width (px) | `0` | `0` |
| `--ww-shadow` | `boxShadow` | Panel box-shadow shorthand | `none` | `none` |
| `--ww-assistant-bubble` | `assistantBubbleColor` | Assistant bubble bg | `#f3f4f6` | `#2f2f2f` |
| `--ww-assistant-bubble-text` | `assistantBubbleTextColor` | Assistant bubble text | `#1f2937` | `#ececec` |
| `--ww-user-bubble` | `userBubbleColor` | User bubble bg | `#f4f4f4` | `#303030` |
| `--ww-user-bubble-text` | `userBubbleTextColor` | User bubble text | `#1f2937` | `#ffffff` |
| `--ww-input-bg` | `inputBackgroundColor` | Input field bg | `#f9fafb` | `#2f2f2f` |
| `--ww-header-bg` | `headerBackgroundColor` | Header background | `#ffffff` | `#1e1e1e` |
| `--ww-header-text` | `headerTextColor` | Header text | `#1f2937` | `#ececec` |
| `--ww-status` | `statusColor` | Status dot | `#22c55e` | `#22c55e` |
| `--ww-tool-card` | `toolCardColor` | Tool call card bg | `#f4f4f5` | `#262626` |
| `--ww-glass-tint` | `glassTint` | Floating bar glass tint | `unset` | `unset` |
| `--ww-glass-tint-opacity` | `glassTintStrength` | Floating bar glass tint strength | `unset` | `unset` |
| `--ww-radius` | `borderRadius` | Panel border-radius | `16px` | `16px` |
| `--ww-msg-radius` | `messageBorderRadius` | Message bubble radius | `8px` | `8px` |
| `--ww-msg-pad-x` | `messagePaddingX` | Message bubble padding X | `16px` | `16px` |
| `--ww-msg-pad-y` | `messagePaddingY` | Message bubble padding Y | `12px` | `12px` |
| `--ww-msg-max-width` | `messageMaxWidth` | Message bubble max width | `80%` | `80%` |
| `--ww-font` | `fontFamily` | Font family | system stack | system stack |
| `--ww-font-size` | `fontSize` | Base message font size | `1rem` | `1rem` |
| `--ww-line-height` | `lineHeight` | Base message line height | `1.5` | `1.5` |
`glassTint` and `glassTintStrength` tint the floating bar's frosted-glass surface — the expanded suggestion card and open panel in `floating` mode — layering a colour over the existing backdrop blur. `glassTint` takes any CSS colour string; `glassTintStrength` is a unitless string from `"0"` to `"1"` setting how strongly the tint is applied. Both are set programmatically through `appearance.variables`:
```tsx theme={null}
// React — tint the floating bar's frosted glass with a brand colour
```
Like `borderWidth` and `boxShadow`, they're additive: leave them unset and the floating bar renders exactly as before.
## Assistant message bubble (opt-in)
Assistant replies render as plain text by default. Turn them into filled bubbles — styled by `assistantBubbleColor` / `assistantBubbleTextColor`, sharing the user bubble's radius and padding — with `assistantBubble`:
```html theme={null}
```
```tsx theme={null}
// React
```
Leave it unset and assistant messages stay bubble-less — existing widgets are unchanged.
## Deep customization
Tokens cover colors, typography, and bubble shape. To restyle an element beyond what a token exposes, target it directly.
**React** — pass `classNames` (available on `ChatEmbed` directly, or `overrides.classNames` on `WaniwaniChat`). Each string is merged onto that slot:
```tsx theme={null}
```
Slots: `root`, `header`, `message`, `userBubble`, `assistantBubble`, `input`.
**Script embed** — the widget renders in a Shadow DOM, so your page's own selectors cannot reach inside. Inject a stylesheet with `data-css` and target the stable semantic classes:
```html theme={null}
```
```css theme={null}
/* widget.css — loaded into the widget's Shadow DOM */
.ww-header { letter-spacing: 0.02em; }
.ww-input { box-shadow: 0 0 0 2px rgba(10, 108, 116, 0.25); }
.ww-message-user .ww-bubble { box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1); }
.ww-message-assistant .ww-bubble { border: 1px solid #0a6c74; }
```
Stable hooks: `.ww-message`, `.ww-message-user`, `.ww-message-assistant`, `.ww-bubble`, `.ww-header`, `.ww-input`. These are guaranteed selectors — the internal `ww:`-prefixed utility classes are not.
## What is not themeable
To set expectations for a full rebrand, these are intentionally fixed today:
* **Typographic scale beyond the base** — `fontSize` / `lineHeight` set the message base; per-heading sizes inside markdown and built-in label sizes/weights are not individually exposed.
* **Chrome corner radii** — `messageBorderRadius` shapes message bubbles only; tool-call cards, the chain-of-thought accordion, menus, and welcome-screen buttons keep their fixed radii.
* **Floating launcher accent** — the dock's animated border-glow uses a fixed accent gradient.
* **"Powered by" mark** — the footer logo is not removable via theming.
For anything in this list, [reach out](https://waniwani.ai) — some are candidates for future tokens.
Full type definitions: [`ChatTheme` and `ChatClassNames`](https://github.com/WaniWani-AI/sdk/blob/main/src/chat/web/%40types.ts).
# Widget events (onEvent)
Source: https://docs.waniwani.ai/sdk/chat/widget-events
Mirror chat widget activity into your own analytics with the onEvent callback.
The chat widget can mirror its lifecycle events into the host page through a single `onEvent` callback. Both hosted surfaces accept it: `WaniWani.chat.init({ onEvent })` on the script embed, and the `onEvent` prop on the React component.
The embed mounts in your page's DOM (the shadow root isolates styles only), so the callback runs in your page's own JS context. Forward events straight into `window.amplitude`, `window.analytics`, or `gtag` and your page-side analytics SDK attaches its own user identity automatically — no server-side identity plumbing, and no Amplitude/Segment SDK is ever loaded inside the widget.
**When to use which channel.** `onEvent` mirrors widget activity into **your** analytics on the host page — it never leaves the browser unless your callback sends it somewhere. It is separate from platform ingest tracking (`WaniWani.chat.track`, funnel events, the typed event catalog), which sends events **to the Waniwani platform** for funnel analytics. Use `onEvent` to see chat activity next to your existing product analytics; use [tracking events](/sdk/tracking/events) to power Waniwani funnels. The two channels are independent and can be used together.
## The events
| Event | Fired when | Extra `properties` |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `chat.ready` | Widget mounted and config resolved, or the readiness timeout elapses (all modes; fires even on pages where visibility rules hide the floating bar) | — |
| `chat.opened` | Full chat panel opened (floating mode only; the collapsed dock does not count) | — |
| `chat.closed` | Full chat panel closed (floating mode only; a page-gate hiding an open panel also emits this) | — |
| `message.sent` | The visitor (or the imperative API, e.g. `sendMessage`) submits a message (never includes the message text) | — |
| `message.received` | Assistant reply finished (never includes the message text) | — |
| `session.started` | Server assigned the session id on the first exchange (restored threads don't re-fire) | `{ sessionId }` |
| `thread.changed` | Thread created or switched (requires thread history) | `{ threadId }` |
| `chat.error` | Chat request failed | `{ message }` (truncated to 200 chars) |
| `suggestion.clicked` | Suggestion pill or welcome card clicked (dock pills, in-chat pills, welcome cards) | `{ text, index, origin }` (`index` is `-1` when the position is unknown; `origin` is `"channel"`, `"page"`, `"flow"`, or `"followup"` — which provider supplied the pill) |
| `link.clicked` | Anchor clicked inside the conversation | `{ url }` (absolute URL) |
Do not assume `chat.opened` precedes the first `message.sent`: a send from the docked bar opens the panel and sends in one action, and the open transition is reported after the send.
**Separate from the `track()` taxonomy.** Some `onEvent` names (`session.started`, `link.clicked`) also exist as event names in the hosted `track()` funnel taxonomy — same concepts, but a separate client-side channel with different payload shapes. `onEvent` events are not `client.track()` events, do not feed the platform funnel dashboard, and should not be forwarded into `client.track()` as-is even where names coincide.
## The payload
Every callback receives a `WidgetEvent` — exported from `@waniwani/sdk/chat` alongside `WidgetEventName` and `WidgetMode`:
```ts theme={null}
import type { WidgetEvent, WidgetEventName, WidgetMode } from "@waniwani/sdk/chat";
// WidgetEvent is a discriminated union on `name`:
// {
// name: WidgetEventName; // one of the 10 events above
// mode: WidgetMode; // "inline" | "floating"
// sessionId?: string; // undefined before the first exchange
// timestamp: number; // epoch milliseconds at emit time
// properties?: { ... }; // per-event extras from the table above
// }
```
`mode` is the embed surface: `"floating"` for the floating bar, `"inline"` for an in-page mount. `` reports `"inline"` — it is an in-page mount, and this is the same `mode` tag its server-side events (chat requests, `page.viewed`) carry, so both streams correlate.
Narrowing on `name` types `properties` automatically:
```ts theme={null}
function handle(event: WidgetEvent) {
if (event.name === "suggestion.clicked") {
// event.properties is { text: string; index: number }
}
}
```
Event names are neutral and fixed — the widget never adopts your analytics naming. Map them to your own schema inside the callback.
## Script embed → Amplitude
`onEvent` is programmatic-only on the script embed: a function cannot be expressed as a `data-*` attribute, so pass it to `WaniWani.chat.init()`.
```html theme={null}
```
Because the Amplitude snippet on your page already identified the user, every mirrored event lands in Amplitude under that same identity.
## React → Segment
The `WaniwaniChat` component takes the same callback as an `onEvent` prop (`chat.opened`/`chat.closed` are floating-embed-only and never fire here):
```tsx theme={null}
import { WaniwaniChat, type WidgetEvent } from "@waniwani/sdk/chat";
function Support() {
const handleWidgetEvent = (event: WidgetEvent) => {
window.analytics.track(event.name, {
mode: event.mode,
sessionId: event.sessionId,
...(event.name === "chat.error"
? { errorMessage: event.properties.message }
: {}),
});
};
return (
);
}
```
## Privacy
Message content never passes through this channel. `message.sent` and `message.received` tell the host *that* a message was exchanged, never what was said. The one text field that does flow through is `suggestion.clicked`'s `text` — suggestion pills are operator-authored config, not user input.
## Error isolation
Exceptions thrown by your callback are swallowed and logged as a console warning; they never break the widget or block other subscribers. Still, keep the callback fast — it runs synchronously on the widget's event path.
`ChatEmbed`, the bring-your-own-backend React primitive, does **not** expose `onEvent`. Like `track`/`identify`, it is a hosted-surface feature of `WaniwaniChat` and the script embed.
# CLI overview
Source: https://docs.waniwani.ai/sdk/cli/overview
The @waniwani/cli connects a local repo to a Waniwani agent and runs your MCP server against the hosted playground in one command.
`@waniwani/cli` is the official command-line tool for [Waniwani](https://app.waniwani.ai). It wires a local repo to a Waniwani agent, runs your MCP server locally against the hosted playground, and removes the manual steps of provisioning GitHub, Vercel, and API keys.
The CLI is **optional**. [`@waniwani/sdk`](/sdk/configuration/installation) works on its own — the CLI is the fastest way to skip the dashboard for project setup and local development.
## Install
```bash bun theme={null}
bun add -g @waniwani/cli
```
```bash pnpm theme={null}
pnpm add -g @waniwani/cli
```
```bash npm theme={null}
npm install -g @waniwani/cli
```
```bash yarn theme={null}
yarn global add @waniwani/cli
```
Requires Node.js 20 or later.
## Quickstart
```bash theme={null}
waniwani login # browser-based OAuth2 PKCE
waniwani connect # pick an org + agent, writes waniwani.json
waniwani dev # run local MCP, open playground bridged to localhost
```
Three commands. Your local MCP server is now reachable from ChatGPT, Claude, and Cursor through the Waniwani playground, with no tunnel setup.
## Commands
| Command | What it does |
| ------------------------------ | --------------------------------------------------------------------------- |
| [`waniwani login`](#login) | Browser-based OAuth2 PKCE. Stores tokens in `.waniwani/settings.json`. |
| [`waniwani logout`](#logout) | Clear local credentials. |
| [`waniwani connect`](#connect) | Pick an org, pick or create an agent, write the binding to `waniwani.json`. |
| [`waniwani dev`](#dev) | Run your MCP locally and open the Waniwani playground against it. |
### login
```bash theme={null}
waniwani login [--no-browser] [--json]
```
Runs the OAuth2 PKCE flow: spins up a local callback server on port `54321`, opens your browser to `app.waniwani.ai`, exchanges the code for an access token, and persists access + refresh tokens to `.waniwani/settings.json` in the current directory.
If you're already logged in with an expired token, it auto-refreshes and exits. Pass `--no-browser` to print the URL instead of launching a browser (useful on headless machines).
### logout
```bash theme={null}
waniwani logout [--json]
```
Clears the stored auth tokens. Keeps non-auth config (like `apiUrl`) intact. No-op if you're already logged out.
### connect
```bash theme={null}
waniwani connect
```
Interactive only. Walks you through:
1. **Pick an organization.** If you're in one org, it's auto-selected.
2. **Pick or create an agent.** Either select an existing project, create a new **managed** agent (Waniwani provisions a GitHub repo + Vercel project from the [MCP distribution template](https://github.com/WaniWani-AI/mcp-distribution-template) with API keys pre-wired), or create a new **external** agent (you host the MCP server; the CLI gives you a production API key to set as `WANIWANI_API_KEY`).
3. **Write the binding.** Writes `waniwani.json` at the repo root with `$schema`, `orgId`, and `projectId`. If the file already exists, the CLI merges the keys in; if the shape isn't recognized, it prints the snippet for you to paste. If a legacy `waniwani.config.ts` is present, it's removed after the JSON file is written.
Re-run anytime to switch the repo to a different agent.
#### Managed vs external agents
| | Managed | External |
| --------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
| Where the server runs | Waniwani-hosted Vercel project | Your infra (Docker, Cloudflare Workers, fly.io, etc.) |
| Source code | Auto-provisioned from [mcp-distribution-template](https://github.com/WaniWani-AI/mcp-distribution-template) | Yours |
| API key | Pre-configured in the Vercel env | Printed once by the CLI — save it as `WANIWANI_API_KEY` |
| Deploys on | Push to `main` | Whatever you already use |
See [Deployment](/sdk/deployment/overview) for the full picture.
### dev
```bash theme={null}
waniwani dev [-p ]
```
Interactive only. The end-to-end local dev loop:
1. Ensures you're logged in (runs `login` if not) and that `waniwani.json` has a `projectId` (runs `connect` if not).
2. Detects your package manager (`bun` / `pnpm` / `yarn` / `npm`) and spawns the dev script with `PORT` set.
3. Waits for the local server to bind to the port.
4. Creates a dev session against the Waniwani API and starts a heartbeat.
5. Opens the playground at `app.waniwani.ai/agents//playground?localMode=1` so chat traffic routes to your local server.
6. Ctrl-C tears the dev session down cleanly.
**Options:**
* `-p, --port ` — Port the local MCP listens on. Defaults to `devPort` in `waniwani.json`, or `3000`.
`dev` runs your existing dev script — it does not start the MCP for you. The script must bind to `process.env.PORT`.
## Global flags
| Flag | Description |
| ----------- | -------------------------------------------------------------------------------------------------------------------- |
| `--json` | Output as JSON. Supported by `login` and `logout`. Interactive commands (`connect`, `dev`) error out under `--json`. |
| `--verbose` | Enable verbose logging. |
| `--version` | Print the CLI version. |
| `--help` | Print help. |
## Configuration
### Auth & local state — `.waniwani/settings.json`
Per-repo, gitignored. Holds OAuth tokens and the resolved API URL:
```json theme={null}
{
"apiUrl": "https://app.waniwani.ai",
"accessToken": "…",
"refreshToken": "…",
"expiresAt": "2026-05-20T12:00:00Z",
"clientId": "…"
}
```
Access tokens auto-refresh on 401. Add `.waniwani/` to your `.gitignore`.
### Project config — `waniwani.json`
Shared with [`@waniwani/sdk`](/sdk/configuration/installation). `waniwani connect` writes `$schema`, `orgId`, and `projectId`; other fields are optional and consumed by the SDK and the `dev` command:
```json waniwani.json theme={null}
{
"$schema": "https://docs.waniwani.ai/waniwani.json",
"orgId": "org_…",
"projectId": "proj_…",
"devPort": 3000
}
```
The `$schema` field unlocks autocomplete and validation in editors that support JSON Schema (VS Code, JetBrains, Neovim with LSP). It's ignored at runtime.
Legacy `waniwani.config.ts` files are still read for now (with a deprecation warning), but new projects should use `waniwani.json`. Running `waniwani connect` against a repo that has a `.ts` file removes it and writes a `.json` in its place.
### Environment variables
| Variable | Purpose |
| ------------------ | ----------------------------------------------------------------------------------------------------- |
| `WANIWANI_API_URL` | Override the API base URL. Defaults to `https://app.waniwani.ai`. |
| `WANIWANI_API_KEY` | Use a long-lived API key instead of OAuth (useful in CI). See [API keys](/sdk/configuration/api-key). |
Resolution order for the API URL: env var → `waniwani.json` → `.waniwani/settings.json` → default. For auth: API key → OAuth tokens (with auto-refresh).
## Typical workflow
```bash theme={null}
# One-time
bun add -g @waniwani/cli
waniwani login
# Per-project
cd my-mcp-server
waniwani connect # creates / picks the agent, writes waniwani.json
waniwani dev # runs the dev loop bridged to the playground
```
To start a fresh project, clone the template first:
```bash theme={null}
git clone https://github.com/WaniWani-AI/mcp-distribution-template.git my-mcp-server
cd my-mcp-server
bun install
waniwani connect
waniwani dev
```
## For AI coding agents
If you're [Claude Code](https://claude.com/claude-code), Cursor, or another AI agent setting up Waniwani in a user's repo:
1. Confirm the user is in an MCP project (`@waniwani/sdk` in `package.json`, or it's the [distribution template](https://github.com/WaniWani-AI/mcp-distribution-template)). If not, scaffold one with the [SDK quickstart](/sdk/quickstart) first.
2. Run `waniwani login` — opens the browser; the user authenticates once.
3. Run `waniwani connect` — interactive; ask the user which org / agent they want, or recommend "managed" if they don't already host.
4. Run `waniwani dev` to verify the bridge works.
The CLI is interactive — `connect` and `dev` cannot run under `--json`. Pair it with the [agent skill](/sdk/guides/skills) for code generation:
```bash theme={null}
bunx skills add Waniwani-AI/sdk -s waniwani-sdk
```
The skill teaches your agent the SDK APIs (flows, tracking, widgets, knowledge base). The CLI handles platform wiring. They're independent — install whichever you need.
## Source
* **npm:** [`@waniwani/cli`](https://www.npmjs.com/package/@waniwani/cli)
* **GitHub:** [Waniwani-AI/cli](https://github.com/WaniWani-AI/cli)
* **Issues:** [github.com/WaniWani-AI/cli/issues](https://github.com/WaniWani-AI/cli/issues)
MIT licensed, like the rest of the Waniwani stack.
# Waniwani vs LangGraph
Source: https://docs.waniwani.ai/sdk/compare/vs-langgraph
When to use createFlow vs LangChain's LangGraph for MCP funnels and multi-step conversational tools.
LangGraph is LangChain's graph framework for agent workflows. `@waniwani/sdk` is a graph framework specifically for MCP funnels. The two overlap. They are not interchangeable.
This page is for developers who already know LangGraph and are evaluating whether to use it for a multi-step MCP tool.
## TL;DR
| | LangGraph | `@waniwani/sdk` (createFlow) |
| ------------------------------ | ------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
| **Scope** | General-purpose agent graphs | MCP funnels specifically |
| **Primary output** | A runnable graph | An MCP tool registered on a server |
| **State persistence** | Pluggable checkpointer, Python-first | Pluggable KvStore, MCP-session-keyed |
| **Built-in funnel ergonomics** | Build them yourself | Built in: interrupts, re-ask on validation, multi-field interrupts, auto-skip filled fields, widget signals |
| **MCP integration** | None, you wire it | One call: `flow.register(server)` |
| **Typed state** | Pydantic / TypedDict | Zod, end-to-end type inference |
| **Language** | Python first, JS port secondary | TypeScript only |
| **License** | MIT | MIT |
If you are building a general-purpose agent (multiple LLM calls, retrievers, tool selection, planning), pick LangGraph. If you are building a funnel that runs as one MCP tool inside ChatGPT or Claude, pick `createFlow`.
## What each one is
**LangGraph** is a graph runtime. You declare nodes (functions), edges (direct or conditional), and a shared state object. The runtime drives the graph. It is unopinionated about what the nodes do, what the state contains, or how the graph is exposed.
**`createFlow`** is also a graph runtime. The difference is opinion. A flow compiles to a single MCP tool. Nodes return state updates, interrupt signals, or widget signals. Interrupts pause the graph to ask a typed question, validate the answer, and re-ask on failure. State is keyed by the MCP session id and persisted to a `KvStore`. The graph is funnel-shaped on purpose.
## When LangGraph is the better choice
* You need an agent that plans, picks tools, and loops over its own output.
* You want multiple LLM calls inside the graph itself, not just a single tool surface.
* You need retrievers, parallel branches, or supervisor patterns.
* Your team is Python-first and the rest of your stack lives in LangChain.
* You are not building for MCP, or MCP is one surface among many.
## When `createFlow` is the better choice
* You are building a multi-step MCP tool: sales funnel, lead generation, booking, quote, intake.
* You want deterministic step order with typed validation, not agent improvisation.
* You want to skip the work of wiring an MCP tool around your graph.
* You want state persistence keyed to the MCP session, not a homegrown checkpointer.
* You want widget cards for confirmation steps without inventing a protocol.
## Side by side: a two-question funnel
The simplest case where the difference shows up.
**LangGraph (conceptual):**
```python theme={null}
from langgraph.graph import StateGraph, START, END
from typing import TypedDict
class State(TypedDict, total=False):
name: str
email: str
def ask_name(state):
# You implement: pause the graph, expose a question to whatever UI is
# driving, parse the response, validate, re-ask on failure, write to state.
...
def ask_email(state):
# Same again. Validation, re-ask, persistence: all on you.
...
graph = StateGraph(State)
graph.add_node("ask_name", ask_name)
graph.add_node("ask_email", ask_email)
graph.add_edge(START, "ask_name")
graph.add_edge("ask_name", "ask_email")
graph.add_edge("ask_email", END)
app = graph.compile()
# You still have to: expose this as an MCP tool, route the MCP session id to
# the checkpointer, decide how interrupts are rendered to the model, etc.
```
**`createFlow`:**
```ts theme={null}
import { createFlow, END, MemoryKvStore, START } from "@waniwani/sdk/mcp";
import { z } from "zod";
const flow = createFlow({
id: "intake",
title: "Intake",
description: "Collect name and email.",
state: {
name: z.string().describe("Full name"),
email: z.string().email().describe("Email address"),
},
})
.addNode({
id: "ask_name",
run: ({ interrupt }) => interrupt({ name: { question: "What's your name?" } }),
})
.addNode({
id: "ask_email",
run: ({ interrupt }) => interrupt({ email: { question: "What's your email?" } }),
})
.addEdge(START, "ask_name")
.addEdge("ask_name", "ask_email")
.addEdge("ask_email", END)
.compile({ store: new MemoryKvStore() });
await flow.register(server);
```
Both are graph definitions. The `createFlow` version compiles to a working MCP tool. The LangGraph version is a graph that still needs to be wrapped, exposed, and given an interaction protocol.
## What you would re-implement on top of LangGraph
If you choose LangGraph for an MCP funnel, you take on:
1. **MCP tool surface.** Declare the tool, marshal arguments, route results.
2. **Session-keyed state.** Map the MCP session id to a checkpointer thread id.
3. **Interrupt semantics.** Pause the graph, emit a structured question, resume on the next tool call.
4. **Validation loop.** Re-ask on schema failure without advancing the graph.
5. **Auto-skip filled fields.** If state already has `email`, skip `ask_email`.
6. **Widget signals.** If you want non-text rendering (cards, pickers), invent a protocol.
7. **Multi-field interrupts.** Ask several questions in one turn when the model can fill them at once.
These are the funnel ergonomics `createFlow` ships built in. None are hard individually. Together they are a project.
## What `createFlow` does not do
To be clear about scope:
* **No agent loop.** A flow is a deterministic graph driven by tool calls, not an LLM-controlled agent.
* **No retrieval.** Use [`@waniwani/sdk/kb`](/sdk/knowledge-base/overview) or your own retriever inside a node.
* **No Python.** TypeScript only.
* **No graph composition primitives** (subgraphs, supervisors). A flow is a single graph compiled to a single tool.
If you need any of those, LangGraph or a hybrid is the right call. You can also call into a LangGraph (or any other) agent from inside a flow node.
## Migration sketch
If you have a LangGraph workflow that is funnel-shaped:
1. Replace `TypedDict` state with a Zod schema map.
2. Replace each node body with either `interrupt({...})`, a state-returning function, or a widget signal.
3. Replace `add_edge` and `add_conditional_edges` with `.addEdge` and `.addConditionalEdge`.
4. Replace the checkpointer with a [KV store adapter](/sdk/flows/kv-store).
5. Call `.register(server)` instead of wiring an MCP tool by hand.
The graph shape stays. The wrapping disappears.
## Next
The distribution shift behind the SDK.
A complete flow in under 30 lines.
Nodes, edges, interrupts, branching.
Redis, Upstash, Cloudflare KV, DynamoDB.
# API Keys
Source: https://docs.waniwani.ai/sdk/configuration/api-key
Create, configure, and rotate Waniwani API keys.
Every Waniwani client authenticates with a single API key scoped to one **MCP environment**. When you create a project, two environments are provisioned automatically: **Staging** and **Production**, each with its own unique API key.
The key you use determines which environment your data (events, sessions, knowledge base) is stored in. Use the Staging key during development and the Production key for live traffic. This keeps your data cleanly separated.
## Managed projects
Both API keys (Staging and Production) are pre-configured in your deployed instance. You can view and rotate them from your MCP app page under **Settings > API Keys**.
No manual setup needed for managed projects. API keys are pre-configured in your environment.
## Self-hosted projects
When you create a self-hosted project, you get your **Production** API key right away. Set it in the environment where your MCP server runs:
```bash .env theme={null}
WANIWANI_API_KEY=wwk_...
```
```ts theme={null}
import { waniwani } from "@waniwani/sdk";
const wani = waniwani(); // reads WANIWANI_API_KEY from env
```
Manage the variable wherever you normally keep secrets (`.env` file, your platform's secret manager, etc.).
To set up a **Staging** environment, create one from your project's dashboard and use its key for local dev and preview deploys.
API keys grant rights to ingest events and mint widget tokens for their environment. Treat them as server-side secrets. Never commit them to git, never ship them in client-side bundles.
## Per-environment keys
Use the **Staging** key during development and the **Production** key for your live deployment. Mixing environments pollutes your metrics and makes rotation harder.
| Environment | When to use | Where the env var lives |
| ----------- | ----------------------------- | ------------------------------------- |
| Production | Live traffic | Platform secret manager |
| Staging | Local dev and preview deploys | `.env` (gitignored) / preview secrets |
## Rotate a key
Rotate from the environment settings in the dashboard. Clients using the old key start receiving `401 Unauthorized`; the SDK's transport detects auth failures and stops retrying, so you get a clean shutdown instead of a retry storm.
## Troubleshooting
If events do not appear in the dashboard:
1. Confirm `WANIWANI_API_KEY` is set in the process running the MCP server (setting it only in your shell is not enough).
2. Check for typos. The key is a single string, no quotes, no trailing whitespace.
3. Look for `[waniwani]` lines in your server logs.
4. Trigger a tool call. The dashboard only shows events from real traffic.
## Also supported
Pass the key explicitly when you need multiple clients in one process (multi-tenant setups):
```ts theme={null}
import { waniwani } from "@waniwani/sdk";
const wani = waniwani({ apiKey: process.env.TENANT_A_KEY });
```
Next: [wrap your MCP server](/sdk/configuration/wrap-server).
# Environment Variables
Source: https://docs.waniwani.ai/sdk/configuration/environment-variables
All environment variables recognized by the Waniwani SDK.
The SDK reads the following environment variables at runtime. None are required at import time; they are checked when the relevant feature is used.
## Authentication
| Variable | Required | Description |
| ------------------ | -------- | --------------------------------------------------------------------------------------------------------------------- |
| `WANIWANI_API_KEY` | Yes | API key for your MCP environment. See [API Keys](/sdk/configuration/api-key). |
| `WANIWANI_API_URL` | No | Override the API base URL. Defaults to `https://app.waniwani.ai`. Use `https://eu.app.waniwani.ai` for the EU region. |
## Logging
By default the SDK is silent. Set one of these to enable internal logs (prefixed `[waniwani:*]`):
| Variable | Values | Description |
| -------------------- | -------------------------------------- | ----------------------------------------------------------- |
| `WANIWANI_LOG_LEVEL` | `debug` \| `warn` \| `error` \| `none` | Controls SDK log verbosity. Defaults to `none`. |
| `WANIWANI_DEBUG` | Any truthy value | Legacy shorthand. Equivalent to `WANIWANI_LOG_LEVEL=debug`. |
If both are set, `WANIWANI_LOG_LEVEL` takes precedence.
```bash .env theme={null}
# Enable debug logs
WANIWANI_LOG_LEVEL=debug
```
The chat server adapters (`toNextJsHandler`, `toExpressJsHandler`) also accept a `debug` option that overrides the env var for that handler only.
## Encryption
| Variable | Required | Description |
| ------------------------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `WANIWANI_ENCRYPTION_KEY` | No | Base64-encoded 32-byte key for AES-256-GCM encryption of KV store values. When set, all values written through the KV store are encrypted at rest. |
# Installation
Source: https://docs.waniwani.ai/sdk/configuration/installation
Install @waniwani/sdk in a new or existing project.
## Choose your approach
Clone a ready-to-go MCP app (server + widgets + UI) with the SDK pre-configured. Best for **new projects**.
Install the SDK package into your existing MCP server or app. Best for **existing projects**.
The SDK is open source and works without an API key. Set `WANIWANI_API_KEY` to connect the [Waniwani Platform](/sdk/platform/overview) and unlock hosted state, tracking, knowledge base, and the chat widget on top.
***
## Option A: Start from template
The [MCP Distribution Template](https://github.com/WaniWani-AI/mcp-distribution-template) gives you a production-ready **MCP app** (server + widgets + UI) with the SDK, event tracking, flows, and widgets already wired up.
The template is an MCP app. If you only need a bare MCP server (no widgets or embedded UI), skip to [Option B](#option-b-add-to-existing-project) or follow the [Quickstart](/sdk/quickstart). The SDK works for both.
Click **"Use this template"** on [GitHub](https://github.com/WaniWani-AI/mcp-distribution-template) to create a new repository from the template, or clone it directly:
```bash theme={null}
git clone https://github.com/WaniWani-AI/mcp-distribution-template.git my-mcp-server
cd my-mcp-server
```
```bash theme={null}
bun install
cp .env.example .env
bun dev
```
The template runs without an API key. Set `WANIWANI_API_KEY` in `.env` to connect the [Platform](/sdk/platform/overview) and enable hosted state, tracking, and the dashboard.
The template includes a reference flow (Alpine ski lessons) you can replace with your own. If you have the [agent skill](/sdk/guides/skills) installed, run `/waniwani-sdk initialize` to interactively scaffold a flow tailored to your business.
***
## Option B: Add to existing project
### Requirements
* Node.js 18.17 or newer (Node 20+ recommended)
* An MCP server runtime. The SDK is tested against:
* [`@modelcontextprotocol/sdk`](https://www.npmjs.com/package/@modelcontextprotocol/sdk)
* [Skybridge](https://github.com/alpic-ai/skybridge) (Alpic)
* [`@vercel/mcp-handler`](https://www.npmjs.com/package/@vercel/mcp-handler)
* A Waniwani account at [app.waniwani.ai](https://app.waniwani.ai) — optional, only needed for [Platform](/sdk/platform/overview) features.
`@modelcontextprotocol/sdk` and `zod` are peer dependencies. Install them in your project if you do not already have them.
### Install
```bash bun theme={null}
bun add @waniwani/sdk
```
```bash pnpm theme={null}
pnpm add @waniwani/sdk
```
```bash npm theme={null}
npm install @waniwani/sdk
```
```bash yarn theme={null}
yarn add @waniwani/sdk
```
The SDK has zero runtime dependencies and targets a sub-5KB core bundle. It runs in long-lived Node processes, serverless functions, and edge runtimes.
### Pick what you need
Three things you can add independently to an existing MCP server. Any subset works.
| Add | What it gives you | API key |
| ---------------------- | ---------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `createFlow` | Multi-step typed flows registered as a single MCP tool. The primary entry point. | Not required. Required only if you want hosted state instead of bringing a `KvStore`. |
| `withWaniwani(server)` | Wraps existing `registerTool` handlers to emit tracking events and bridge session IDs. No handler changes. | Required for events. Bridging works without. |
| `waniwani()` client | Direct calls to Platform APIs (custom events, `identify()`, KB search). | Required. |
### Add a flow to an existing server
Assume you already have an `McpServer` instance with some tools registered on it. Define a flow and `.register()` it alongside them:
```ts theme={null}
import { createFlow, END, MemoryKvStore, START } from "@waniwani/sdk/mcp";
import { z } from "zod";
import { server } from "./mcp-server"; // your existing McpServer
const onboardingFlow = createFlow({
id: "onboarding",
title: "User Onboarding",
description: "Use when a new user wants to get started.",
state: { name: z.string().describe("Their name") },
})
.addNode({
id: "ask_name",
run: ({ interrupt }) =>
interrupt({ name: { question: "What is your name?" } }),
})
.addEdge(START, "ask_name")
.addEdge("ask_name", END)
.compile({ store: new MemoryKvStore() });
await onboardingFlow.register(server);
```
`MemoryKvStore` is fine for development and tests. For production, plug in a real backend (Redis, Upstash, Cloudflare KV, DynamoDB) or set `WANIWANI_API_KEY` to use hosted state. See [KV store adapters](/sdk/flows/kv-store).
### Wrap an existing server for Platform tracking
If your server already has plain `registerTool` handlers and you just want them tracked, wrap it once. No changes to the handlers themselves:
```ts theme={null}
import { withWaniwani } from "@waniwani/sdk/mcp";
import { server } from "./mcp-server"; // your existing McpServer
withWaniwani(server);
```
```bash .env theme={null}
WANIWANI_API_KEY=wwk_...
```
Every wrapped tool call now emits a `tool.called` event to your [Waniwani dashboard](https://app.waniwani.ai). Without an API key, the wrapper still bridges session IDs and forwards widget metadata but emits no events. Safe to wrap unconditionally. See [Wrap your server](/sdk/configuration/wrap-server) for all options.
## Create a Platform client
The `waniwani()` client is needed only when you call Platform APIs directly (custom event tracking via `track()`, `identify()`, KB search). It is **not** required for `createFlow` or `withWaniwani`.
```ts theme={null}
import { waniwani } from "@waniwani/sdk";
const wani = waniwani();
```
With no arguments, the client reads `WANIWANI_API_KEY` from the environment and uses `https://app.waniwani.ai` as the base URL.
See [API keys](/sdk/configuration/api-key) for how to obtain and configure the key, and [Client & KV store reference](/sdk/reference/kv-store-api) for the full client config.
## Also supported
* **Project config file.** Drop a `waniwani.json` at the project root with `$schema` set to `https://docs.waniwani.ai/waniwani.json`. The SDK auto-loads it on the first `waniwani()` call; the CLI uses the same file for `connect`/`dev`/evals. No explicit import required.
Next: [get your API key](/sdk/configuration/api-key) (if you want Platform features), then [wrap your MCP server](/sdk/configuration/wrap-server).
# Security
Source: https://docs.waniwani.ai/sdk/configuration/security
Encryption at rest, key management, and security best practices.
## Encryption at rest
Flow state is stored server-side via the Waniwani KV store. The SDK supports **AES-256-GCM encryption** -- values are encrypted before they leave your MCP server process and decrypted on read. The Waniwani server never sees plaintext flow state.
Encryption is automatic. Waniwani generates and manages the key for you -- nothing to configure.
You generate a key and add it to your MCP server's environment.
***
### Managed projects
Encryption is enabled automatically. Waniwani generates and stores the encryption key as an environment variable in your deployed instance. No setup is needed -- flow state is encrypted out of the box and the key is never exposed.
### Self-hosted projects
You need to generate a key and add it to your MCP server's environment:
```bash theme={null}
openssl rand -base64 32
```
```bash .env theme={null}
WANIWANI_ENCRYPTION_KEY=
```
The SDK picks up the key automatically -- no code changes required. When the key is not set, values are stored as plain JSON.
### How it works
When `WANIWANI_ENCRYPTION_KEY` is set:
1. **On write** -- the SDK serializes the value to JSON, encrypts it with AES-256-GCM using a random 12-byte IV, and stores an encrypted envelope in the KV store.
2. **On read** -- the SDK detects the encrypted envelope, decrypts the ciphertext, and returns the original value.
### Key rotation
The SDK does not support automatic key rotation. To rotate:
1. Set the new key in your environment.
2. Trigger each active flow so its state is re-read with the old key (which will fail) and re-written with the new key.
In practice, the simplest approach is to let existing sessions expire naturally and apply the new key to new sessions only.
If you lose the encryption key, encrypted flow state cannot be recovered. Back up the key in your secret manager.
## API key handling
API keys (`WANIWANI_API_KEY`) authenticate your MCP server to the Waniwani platform. Treat them as server-side secrets:
* Never commit them to version control.
* Never include them in client-side bundles.
* Store them in your platform's secret manager or a gitignored `.env` file.
* Use separate keys for Staging and Production environments.
See [API Keys](/sdk/configuration/api-key) for setup and rotation details.
## Transport security
All communication between the SDK and the Waniwani API uses HTTPS (TLS 1.2+). No additional configuration is needed.
## Tenant isolation
Each API key is scoped to a single MCP environment. The Waniwani API enforces tenant isolation at the key level -- one key cannot access another environment's data (events, sessions, KV store, knowledge base).
# Wrap your MCP server
Source: https://docs.waniwani.ai/sdk/configuration/wrap-server
Optional convenience wrapper that adds tool tracking, session bridging, and widget metadata forwarding.
`withWaniwani(server)` is an optional convenience that instruments an MCP server. It activates Platform tracking when an API key is configured, and stays a no-op-but-still-useful wrapper when it isn't.
**Not required for OSS flows.** `createFlow` works without `withWaniwani`. Use this wrapper when you want auto-tracking, session-id bridging, or widget metadata forwarding on top.
## What it does, with `WANIWANI_API_KEY`
Every tool invocation produces a `tool.called` event containing name, type, duration, status, input, output, client info, and session correlation. Flow graphs are synced to the dashboard so the funnel view lights up automatically.
```ts theme={null}
import { withWaniwani } from "@waniwani/sdk/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
const server = new McpServer({ name: "my-mcp-app", version: "0.0.1" });
server.registerTool(/* ... */);
withWaniwani(server);
```
## What it does, without an API key
The wrapper still adds two genuinely useful things, even with no key configured:
1. **Transport session-ID bridging.** When the MCP transport carries a session ID via the `Mcp-Session-Id` header but the request `_meta` doesn't include one, the wrapper copies it into `_meta.waniwani/sessionId` so flow nodes and downstream tools can see the correlation.
2. **Widget definition metadata forwarding.** Widget keys declared on a tool's definition `_meta` (`openai/outputTemplate`, `ui/resourceUri`, etc.) are forwarded into each tool result's `_meta`, so chat UIs that only see results can still render widgets.
The wrapper's own auto-capture (`tool.called`) silently no-ops without a key, so wrapping unconditionally is safe. Your own `client.track.*` and `identify()` calls are different: they throw `WANIWANI_API_KEY is not set` when no key is configured, so guard them (`waniwani?.track...` on the scoped client) on code paths that must also run keyless.
## When to use it
| You want | Use `withWaniwani`? |
| --------------------------------------------------- | -------------------------------------------- |
| Only run OSS flows, no telemetry | Optional (still useful for session bridging) |
| OSS flows + Platform tracking | Yes |
| Hosted everything (flow state + tracking + funnel) | Yes |
| Just custom event tracking via `waniwani().track()` | No, call `track()` directly |
## Options
```ts theme={null}
import { withWaniwani } from "@waniwani/sdk/mcp";
import { waniwani } from "@waniwani/sdk";
withWaniwani(server, {
client: waniwani(),
toolType: (name) => (name.startsWith("get_price") ? "pricing" : "other"),
metadata: { deployment: "production" },
flushAfterToolCall: false,
injectWidgetToken: true,
onError: (err) => console.error("[waniwani]", err),
});
```
The client used for tracking. Defaults to `waniwani()`, which picks up `WANIWANI_API_KEY` from the environment.
Classify tools into dashboard buckets: `"pricing"`, `"product_info"`, `"availability"`, `"support"`, or `"other"`. Accepts a literal or a function that maps a tool name to a type.
Extra fields merged into every `tool.called` event. Use this to tag deployments, tenants, or releases.
Force a flush after every tool call. Turn this on in serverless runtimes where the process may be frozen between invocations. Leave it off for long-running Node processes.
Inject the widget tracking config (endpoint, a short-lived widget token, session id, source) into tool responses under `_meta["waniwani/widget"]`, so browser widgets can post events directly to Waniwani without a proxy route. `useWaniwani()` reads this automatically. Without an API key, the token is omitted and only the endpoint metadata is injected.
Callback for non-fatal tracking errors. Tracking failures never block tool execution; use this callback when you want visibility into them.
Call order does not matter. `withWaniwani` walks existing `_registeredTools` at call time and intercepts `registerTool` for any future registrations. It is idempotent: a second call on the same server is a no-op.
## Tracked event shape
| Field | Description |
| --------------- | ---------------------------------------------------------------- |
| `name` | Registered tool name |
| `type` | Category derived from the `toolType` option |
| `durationMs` | Handler execution time |
| `status` | `"ok"` or `"error"` |
| `errorMessage` | Present when the handler threw or returned `isError` |
| `input` | Tool arguments |
| `output` | Handler response |
| `clientName` | MCP client name (for example `chatgpt`, `claude`) |
| `clientVersion` | MCP client version |
| `sessionId` | Correlated from `_meta` (see [Sessions](/sdk/tracking/sessions)) |
Errors (thrown or `{ isError: true }`) are tracked and then re-thrown or returned unchanged, so your tool's error contract is preserved.
## Custom events inside tools
For anything beyond `tool.called`, use the request-scoped client the wrapper attaches to `extra["waniwani/client"]`. Session correlation is already merged in; you pass nothing.
```ts theme={null}
import { extractScopedClient } from "@waniwani/sdk/mcp";
server.registerTool(
"get_quote",
{ /* tool config */ },
async ({ amount, currency }, extra) => {
const waniwani = extractScopedClient(extra);
await waniwani?.track.priceShown({ amount, currency });
return { content: [{ type: "text", text: `Quoted ${amount} ${currency}` }] };
},
);
```
## Session-scoped client in flows
Inside a [flow](/sdk/flows/overview) node, `ctx.waniwani` is the same scoped client, so `ctx.waniwani.track({ event, properties })` needs no session plumbing either. The resolved session id is readable as `ctx.waniwani.sessionId`; store it with your own records to attribute later off-platform events back to the conversation (see [Sessions](/sdk/tracking/sessions)).
# Deployment
Source: https://docs.waniwani.ai/sdk/deployment/overview
Run your Waniwani-powered MCP app on Managed Hosting or self-host it yourself (Docker, Alpic, Vercel).
Where your MCP server runs is independent of whether you connect the [Waniwani Platform](/sdk/platform/overview). This page covers the two server-hosting options. The Platform layer (tracking, KB, chat, hosted state) works the same in both.
Waniwani hosts your app. Provisioned, pre-configured, push to main and it deploys. Fastest path.
You host the app yourself. Docker, Alpic, or Vercel. Connect by giving us the MCP URL.
Both paths use the same [MCP Distribution Template](https://github.com/WaniWani-AI/mcp-distribution-template). Pick based on where you want the server to live.
## Managed Hosting
When you create a managed project in the [Waniwani dashboard](https://app.waniwani.ai), we provision a copy of the distribution template fully wired up: API keys set, hosted URL assigned, everything ready. You don't configure anything.
Typical workflow:
From [app.waniwani.ai](https://app.waniwani.ai), create a managed project. We spin up a repo from the template and deploy it on our managed hosting. Your tools, flows, widgets, and chat surface are live immediately.
Move the provisioned repo into your own GitHub organization when you want full control over the codebase. You keep the managed hosting; you just own the source now.
Every push to `main` auto-deploys to our managed hosting environment. API keys, URLs, and tracking continue to flow without any setup on your side.
Best for: getting started fast, teams that don't want to run infrastructure, and projects that want one less moving part to maintain.
## Self-hosted server
You run the server. By default we recommend connecting the [Platform](/sdk/platform/overview): set `WANIWANI_API_KEY` and you get hosted state + dashboards on top of your self-hosted server, with no extra infra. If you want to push that further and self-host the state layer too, see [Advanced: bring your own session cache](#advanced-bring-your-own-session-cache) below. If you want zero Platform dependency at all (no API key, no outbound calls), see the [open-source self-hosting walkthrough](/sdk/deployment/self-hosting).
The [MCP Distribution Template](https://github.com/WaniWani-AI/mcp-distribution-template) ships three deployment targets. Pick whichever fits your stack.
Run anywhere a container runs.
MCP-native runtime.
One command from the template root.
### Docker
The template ships a `Dockerfile`. Build and run anywhere a container runs (Fly.io, Render, Railway, ECS, GCP Cloud Run, your own VPS, Kubernetes).
```bash theme={null}
docker build -t my-mcp-app .
docker run --env-file .env -p 3000:3000 my-mcp-app
```
Set `WANIWANI_API_KEY` and any backend secrets in the runtime's environment.
### Alpic
Deploy to [Alpic](https://github.com/alpic-ai/skybridge), an MCP-native hosting runtime. Follow Alpic's deployment instructions for the template repo. The SDK is tested against the Skybridge runtime, so flows, tracking, and widgets work out of the box.
### Vercel
```bash theme={null}
vercel deploy
```
From the template root. Set `WANIWANI_API_KEY` and any backend secrets in your Vercel project's environment variables.
### Connect to Waniwani
Once your app is deployed, copy its public MCP URL (the endpoint where MCP clients send `initialize` / `tools/call` requests). In the Waniwani dashboard, create a self-hosted project and paste that URL. Waniwani uses it as the entry point for chat requests, widget rendering, and analytics ingestion.
### Advanced: bring your own session cache
By default a self-hosted server uses `WaniwaniKvStore` for flow state when `WANIWANI_API_KEY` is set (encrypted at rest, hosted by Waniwani, no infra). That's the recommended setup.
If you want zero dependency on `app.waniwani.ai` for state too, plug in your own `KvStore` adapter at `.compile()` time:
```ts theme={null}
import { createFlow } from "@waniwani/sdk/mcp";
import { flowStore } from "./kv-store"; // your Redis / Upstash / DynamoDB / SQLite adapter
const flow = createFlow({ /* ... */ }).compile({ store: flowStore });
```
See [KV store adapters](/sdk/flows/kv-store) for recipes (Redis, Upstash, Cloudflare KV, DynamoDB, SQLite) and the full [open-source self-hosting](/sdk/deployment/self-hosting) walkthrough.
Your server, your state. You can still send tracking events to the Platform by calling `withWaniwani(server)`, or skip that entirely for a fully open-source setup.
## Platform features are orthogonal
Tracking, KB, chat, funnel analytics, and hosted state are Platform features. They are independent of where your server lives and where your state lives. Both Managed Hosting and self-hosted servers emit events through `withWaniwani(server)` and `client.track()`, and both surface in the same Waniwani dashboard. See the [Platform overview](/sdk/platform/overview) for what's included and [Tracking](/sdk/tracking/overview) for the event pipeline.
# Self-hosting
Source: https://docs.waniwani.ai/sdk/deployment/self-hosting
Two pieces you can self-host independently: the MCP app and the KV store.
A Waniwani deployment has **two pieces you can self-host independently**:
1. **The MCP app** — the server that hosts your `createFlow` tools and serves MCP clients.
2. **The KV store** — where flow state lives between tool calls.
These are orthogonal. The most common setup is "self-host the MCP app, let Waniwani host the KV store" — your infra for the server, the [Waniwani Platform](/sdk/platform/overview) for state and dashboards. The next step is self-hosting the KV too, eliminating any outbound call to `app.waniwani.ai`.
| | KV on `WaniwaniKvStore` | KV self-hosted |
| ------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------- |
| **MCP app self-hosted** | Hybrid: your infra for the server, hosted state. Recommended starting point. | Fully open source. No outbound calls. |
| **MCP app on Managed Hosting** | Fully managed. | Not supported. |
## 1. Self-host the MCP app
Deploy the server you wrote on infrastructure you control. By itself this gets you self-hosted MCP + hosted state (the Hybrid row above).
### Scaffold
Start from the [MCP Distribution Template](https://github.com/WaniWani-AI/mcp-distribution-template) or any MCP server of your own:
```bash theme={null}
bun add @waniwani/sdk
```
### Wire up the server
```ts server.ts theme={null}
import express from "express";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { onboardingFlow } from "./flow";
const server = new McpServer({ name: "my-mcp-app", version: "0.0.1" });
await onboardingFlow.register(server);
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);
const app = express();
app.use(express.json());
app.post("/mcp", (req, res) => transport.handleRequest(req, res, req.body));
app.listen(3000);
```
When `.compile()` is called with no `store` and `WANIWANI_API_KEY` is set, the SDK uses `WaniwaniKvStore` automatically. That's the Hybrid setup.
### Deploy
Any platform that can run Node 18.17+ works: Vercel, Render, Railway, Fly.io, Cloudflare Workers, AWS Lambda, Google Cloud Run. See [Deployment overview](/sdk/deployment/overview) for the choice between this and Managed Hosting.
### Register with Waniwani
In the [Waniwani dashboard](https://app.waniwani.ai), create a self-hosted project and paste your deployed MCP URL. Waniwani routes chat requests, widget rendering, and analytics ingestion through that URL.
## 2. Self-host the KV store
Pass your own `KvStore` to `.compile()` and the engine never touches `app.waniwani.ai` for state. Useful when you have data-residency constraints, run in fully open-source mode, or want all state inside your existing infrastructure.
### Pick a backend
See [KV store adapters](/sdk/flows/kv-store) for recipes. Quick decision tree:
* **Vercel / Netlify / Cloudflare Workers** → Upstash Redis or Cloudflare KV
* **AWS Lambda** → DynamoDB
* **Long-running Node** → ioredis against a managed Redis (Render, Railway, Fly)
* **Local box** → SQLite via `better-sqlite3`
### Implement the interface
```ts kv-store.ts theme={null}
import type { KvStore } from "@waniwani/sdk/mcp";
import { Redis } from "@upstash/redis";
const redis = new Redis({
url: process.env.UPSTASH_REDIS_REST_URL!,
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
});
export const flowStore: KvStore = {
async get(key) { return (await redis.get(key)) as never; },
async set(key, value) { await redis.set(key, value); },
async delete(key) { await redis.del(key); },
};
```
### Pass it to `.compile()`
```ts flow.ts theme={null}
import { createFlow, END, START } from "@waniwani/sdk/mcp";
import { flowStore } from "./kv-store";
export const onboardingFlow = createFlow({ /* ... */ })
.addNode({ /* ... */ })
.addEdge(START, "...")
.addEdge("...", END)
.compile({ store: flowStore });
```
State now reads and writes against your backend. Nothing about flow state ever leaves your infrastructure.
## Fully open-source mode
Combine the two: self-host the MCP app, pass a custom `KvStore`, omit `WANIWANI_API_KEY`. Result: no outbound calls to `app.waniwani.ai`, no telemetry, no hosted dashboards. The engine is fully usable in this mode.
### Verifying it's truly offline
```bash theme={null}
# 1. No WANIWANI_API_KEY in env
echo $WANIWANI_API_KEY
# (empty)
# 2. Run with network observability
DEBUG=* bun run start 2>&1 | grep waniwani
# (no requests to app.waniwani.ai)
```
The only outbound calls the SDK makes are when the `waniwani()` client is instantiated with an API key, or when `WaniwaniKvStore` is used. Neither happens in fully open-source mode.
## Switching modes later
The same code moves between modes by editing one or two lines:
* **OSS → Hybrid:** drop the `store` argument from `.compile()`, set `WANIWANI_API_KEY`.
* **Hybrid → OSS:** pass a custom `store` to `.compile()`.
* **Self-hosted → Managed Hosting:** see [Deployment overview](/sdk/deployment/overview#managed-hosting).
Existing sessions stay where they started; there is no automatic state migration between stores.
# Architecture
Source: https://docs.waniwani.ai/sdk/flows/architecture
What happens between an MCP tool call and the flow engine's response, and why a KV store is required.
A flow is a single MCP tool. The model calls that tool, the engine drives the graph one step at a time, and the response tells the model whether to ask the user something, render a widget, or stop. Between calls, everything the engine knows about the conversation lives in the KV store, keyed by the MCP session id.
This page walks through one full round-trip. If you understand the picture below, debugging "why doesn't the flow remember what I said?" becomes trivial.
## The pieces
```mermaid theme={null}
flowchart LR
Client["MCP client (ChatGPT, Claude, ...)"]
subgraph Server["Your MCP server"]
Tool["Flow tool"]
KV[("KV store")]
Tool <-->|loads / saves| KV
end
Client -->|tools/call| Tool
Tool -->|response| Client
```
The KV store is the only place the flow engine looks for state between two tool calls. Your MCP server itself can be either stateful (a long-running process, e.g. stdio or single-instance HTTP) or stateless (a serverless function where every call is a fresh process). The engine doesn't assume either — it always requires a `KvStore`. In a stateful runtime, `MemoryKvStore` is enough, because the process holds the state across calls. In a stateless runtime, you need a real backing store (Redis, Upstash, Cloudflare KV, DynamoDB, or `WaniwaniKvStore`) so state survives between cold starts and instances.
## First call: `action: "start"`
```mermaid theme={null}
sequenceDiagram
participant Client as MCP client
participant Tool as Flow tool handler
participant Engine as Flow engine
participant KV as KV store
Client->>Tool: tools/call ({ action: "start", intent, stateUpdates })
Tool->>Engine: invoke
Engine->>KV: get(sessionId)
KV-->>Engine: null (first call)
Engine->>Engine: init state from stateUpdates
Engine->>Engine: run START → action nodes...
Engine->>Engine: hit ask_email (interrupt)
Engine->>KV: set(sessionId, { step: "ask_email", state: {} })
Engine-->>Tool: { status: "interrupt", questions: {...} }
Tool-->>Client: response
```
After this call, the KV holds:
```json theme={null}
{
"step": "ask_email",
"state": {}
}
```
The flow has paused. The next tool call (with the user's answer) will resume from here.
## Next call: `action: "continue"`
```mermaid theme={null}
sequenceDiagram
participant Client as MCP client
participant Tool as Flow tool handler
participant Engine as Flow engine
participant KV as KV store
Client->>Tool: tools/call ({ action: "continue", stateUpdates: { email: "..." } })
Tool->>Engine: invoke
Engine->>KV: get(sessionId)
KV-->>Engine: { step: "ask_email", state: {} }
Engine->>Engine: apply stateUpdates, run validators
Engine->>Engine: advance along edge to next node
Engine->>Engine: run action nodes... hit ask_use_case
Engine->>KV: set(sessionId, { step: "ask_use_case", state: { email: "..." } })
Engine-->>Tool: { status: "interrupt", questions: {...} }
Tool-->>Client: response
```
Every `continue` is the same shape: load → apply updates → advance → save → respond.
When the engine reaches `END`, the response status is `"complete"` and the KV key is deleted, so a stale `continue` call can't accidentally resume a finished session.
## What's in the stored value
The value at a given session key is small and deterministic:
```ts theme={null}
type StoredValue = {
step: string; // current node name (or "END" briefly)
state: Partial; // merged state so far, typed against schema
pendingWidget?: { // present only when paused at a showWidget node
tool: string;
data?: Record;
};
};
```
Each session maps to one key. The key is derived from the MCP session identifier (`_meta.waniwani/sessionId`, or `Mcp-Session-Id` from the transport). One session, one key, one ongoing flow run.
## What's *not* in the KV store
The KV powers the flow engine only. The following live in a separate pipeline:
* **Tracking events.** `tool.called`, `price_shown`, etc. are emitted by `withWaniwani(server)` and `client.track()`, batched, and POSTed to the events endpoint. Independent of the KV store.
* **Funnel analytics and dashboards.** Built from tracking events on the server side. Reading them never touches your KV.
* **Knowledge base content.** Lives in Waniwani's hosted KB service, unrelated to flow state.
You can self-host the KV and still get hosted dashboards by also calling `withWaniwani(server)` with `WANIWANI_API_KEY` set. The two systems compose.
## Common gotchas
The KV reset between calls. Two common causes:
1. **`MemoryKvStore` in a serverless runtime.** Each invocation is a fresh process, so the in-memory `Map` is empty every time. Use a real backend (Redis, Upstash, Cloudflare KV) or set `WANIWANI_API_KEY` to use `WaniwaniKvStore`.
2. **Missing session id in `_meta`.** If the MCP client doesn't propagate a session id, the engine derives a different key on every call. `withWaniwani(server)` bridges the transport-level session id into `_meta` for you. Install it.
Same root cause as above (missing or changing session id), but with a different symptom: the engine never finds a stored value, so it always behaves like `action: "start"`. Inspect what your transport puts in `Mcp-Session-Id` and what arrives in `_meta` on the server. They should be stable across a conversation.
You changed the flow's state schema, then deployed. Sessions that were mid-flow have stored values that no longer match the new schema. The safest fix is to invalidate old sessions: delete the prefix in your KV, or version your flow id (`"onboarding_v2"`).
MCP clients serialize calls within a conversation, so concurrent `continue`s on one session are rare. If you see racy reads/writes in your KV, the cause is usually two clients sharing one session id by accident (often a misconfigured proxy). Make sure each conversation has a unique session id.
A `showWidget` node stores `pendingWidget` in the KV and returns `status: "widget"`. The engine resumes only when the next `continue` carries `stateUpdates` that fill the `field` declared in `showWidget(...)`. If your display tool never writes that field, the flow stays paused forever. Confirm the display tool's handler is producing the expected key in `stateUpdates`.
## Next
The three kinds of nodes and the context they receive.
Pick a backend or write your own in 10 lines.
Wire-level reference for `FlowToolInput` and response statuses.
What runs without an API key, what unlocks when you connect the Platform.
# Edges
Source: https://docs.waniwani.ai/sdk/flows/edges
Wire nodes together: direct edges, conditional branching, loops.
Edges define the order in which nodes run. Every node needs an outgoing edge. Every flow needs an edge out of `START` and must be able to reach `END`.
## Sentinels
Two reserved node names control flow boundaries:
* `START`: the entry point. Exactly one edge must originate from it.
* `END`: the terminal marker. Any node pointing to `END` finishes the flow when reached.
```ts theme={null}
import { createFlow, END, START } from "@waniwani/sdk/mcp";
```
## Direct edges
Use `.addEdge(from, to)` for unconditional transitions. Each node can have at most one outgoing edge.
```ts theme={null}
flow
.addEdge(START, "ask_email")
.addEdge("ask_email", "ask_use_case")
.addEdge("ask_use_case", "record")
.addEdge("record", END);
```
Calling `addEdge` twice from the same node throws at build time. If you need a second outgoing path, replace the first call with `addConditionalEdge`.
## Conditional edges
Use `.addConditionalEdge(from, to, condition)` when the next node depends on state. Declare every node the branch can reach in the `to` array, then `condition` receives `Partial` and returns which of those nodes to go to next. It can be async.
The condition's return type is **constrained to `to`** — returning a node you did not list is a compile error. Because `to` is the declared set of targets, graph introspection (funnel analytics, Mermaid diagrams) reads it directly, so the graph stays correct even when the branch target is computed dynamically.
```ts theme={null}
flow
.addNode({
id: "ask_email",
run: ({ interrupt }) =>
interrupt({ email: { question: "What's your email?" } }),
})
.addNode({
id: "analyze_email",
run: ({ state }) => {
const domain = state.email?.split("@")[1] ?? "";
const generic = new Set(["gmail.com", "yahoo.com", "outlook.com"]);
return { isCompanyEmail: !generic.has(domain) };
},
})
.addNode({
id: "ask_company",
run: ({ interrupt }) =>
interrupt({ companyName: { question: "What company are you with?" } }),
})
.addNode({ id: "done", run: () => ({ ready: true }) })
.addEdge(START, "ask_email")
.addEdge("ask_email", "analyze_email")
.addConditionalEdge("analyze_email", ["done", "ask_company"], (state) =>
state.isCompanyEmail ? "done" : "ask_company",
)
.addEdge("ask_company", "done")
.addEdge("done", END);
```
Async conditions work for branches that depend on external lookups:
```ts theme={null}
.addConditionalEdge(
"check_inventory",
["confirm_order", "offer_alternative"],
async (state) => {
const inStock = await inventoryService.check(state.sku!);
return inStock ? "confirm_order" : "offer_alternative";
},
)
```
The return type is checked against the `to` list, so a typo in a branch target — or a target you forgot to declare — fails type-checking.
## Reaching END
When execution reaches `END`, the engine returns `status: "complete"` to the model and deletes the session's flow state so a stale `continue` call cannot resume it. Any node can point to `END`, directly or conditionally.
```ts theme={null}
.addConditionalEdge("review", [END, "ask_corrections"], (state) =>
state.approved ? END : "ask_corrections",
)
```
## Loops
A conditional edge that points back to an earlier node creates a loop. Useful for retries or collection gathering.
```ts theme={null}
.addNode({
id: "ask_item",
run: ({ interrupt }) =>
interrupt({ item: { question: "Add an item (or say 'done')" } }),
})
.addNode({
id: "append",
run: ({ state }) => ({
items: [...(state.items ?? []), state.item].filter(Boolean) as string[],
}),
})
.addEdge("ask_item", "append")
.addConditionalEdge("append", ["finalize", "ask_item"], (state) =>
state.item === "done" ? "finalize" : "ask_item",
)
```
The engine enforces a hard cap of 50 iterations per tool call to prevent runaway loops between action nodes. You will still need `END` reachable from every branch or the flow can stall.
## Compile-time validation
`.compile()` validates the graph before returning. It throws if:
* No edge exists from `START`.
* A `START` edge or direct edge points at an undeclared node.
* A conditional edge declares a `to` target that is not a registered node.
* A conditional edge declares an empty `to` list.
* A node has no outgoing edge.
* An edge is declared `from` a non-existent node.
Errors surface immediately at startup, not at the first user interaction.
## Visualizing the graph
Both the builder and the compiled flow expose `graph()`, which returns a Mermaid `flowchart TD` string. Conditional edges render one dashed arrow per declared `to` target.
```ts theme={null}
console.log(flow.graph());
```
```mermaid theme={null}
flowchart TD
__start__((Start))
ask_email[ask_email]
analyze_email[analyze_email]
ask_company[ask_company]
done[done]
__end__((End))
__start__ --> ask_email
ask_email --> analyze_email
analyze_email -.-> done
analyze_email -.-> ask_company
ask_company --> done
done --> __end__
```
# Interrupts
Source: https://docs.waniwani.ai/sdk/flows/interrupts
Pause the flow to ask the user one or more questions.
An interrupt is the mechanism by which a flow collects information from the user. When a node returns `interrupt(...)`, the engine returns `status: "interrupt"` to the model, persists the current step, and waits. On the next `action: "continue"` call, the user's answers arrive in `stateUpdates`, validators run, and the engine advances along the node's outgoing edge.
## What gets persisted
Between an interrupt and the matching continue call, the flow store holds:
* The current step name (which node is paused).
* The full state snapshot at the time of pause.
* For single-question interrupts, the field being asked.
Everything else (handler closures, validator functions) lives in the compiled flow in memory. Validators are registered when a handler first returns an interrupt and are keyed by `"nodeName:fieldName"`. They are not serialized.
This has one operational consequence: if the server process restarts between the interrupt and the continue call, persisted state is reloaded but validator functions are rebuilt from the handler's return value as the interrupt re-executes. The interrupt handler must therefore be deterministic enough to produce the same validator for a given state.
## Single question
The smallest interrupt asks one question that writes its answer to one field.
```ts theme={null}
.addNode({
id: "ask_email",
run: ({ interrupt }) =>
interrupt({
email: { question: "What's your work email?" },
}),
})
```
The key (`email`) must be a field in the flow's state schema. TypeScript enforces this.
## Suggestions
Pass `suggestions` to give the model a concrete list to offer. Pairs well with `z.enum()` state fields so the model validates the user's choice against a fixed set.
```ts theme={null}
.addNode({
id: "ask_level",
run: ({ interrupt }) =>
interrupt({
level: {
question: "What's your ski level?",
suggestions: ["beginner", "intermediate", "advanced"],
},
}),
})
```
Suggestions serve two audiences at once. The assistant receives them as candidate answers on every host, regardless of any configuration. In the Waniwani chat widget they can also render as clickable pills above the input — clicking one sends it as the visitor's answer — but only when the host chat surface opts the `"flow"` suggestion origin in:
* ``: `overrides={{ suggestionOrigins: ["channel", "page", "flow", "followup"] }}`
* `
```
With the React component, the same surface lives on the ref handle:
```tsx theme={null}
import { useRef } from "react";
import { WaniwaniChat, type ChatHandle } from "@waniwani/sdk/chat";
function Page() {
const chat = useRef(null);
return (
<>
>
);
}
```
`track` and `identify` are absent on the bare `ChatEmbed` primitive, which is bring-your-own-backend and holds no Waniwani credential.
To mirror chat lifecycle events (opened, closed, message sent/received) into your own analytics instead, use the embed's `onEvent` callback; it runs in the page's JS context and carries the `sessionId`.
## Custom surfaces: `createFrontendClient`
Both surfaces above are wrappers around one primitive, exported for anything they do not cover:
```ts theme={null}
import { createFrontendClient } from "@waniwani/sdk";
const client = createFrontendClient({
endpoint: "https://app.waniwani.ai/api/mcp/events/v2/batch",
token: "wwp_...", // public token or widget JWT
source: "web",
identity: () => ({ sessionId: currentSessionId, visitorId }),
});
await client.track.priceShown({ amount: 49, currency: "EUR" });
```
It shares the server client's mapper and batching transport, adds keepalive flushing on page hide/unload, and reads `identity()` at emit time so late-arriving ids (a session assigned mid-page) are picked up without recreating the client.
## Delivery behavior
* Events batch in memory and flush on a short timer, on batch size, and with `keepalive` requests when the page is hidden or unloading.
* Tracking never throws into the host page; failures are logged and dropped.
* On an auth failure (expired widget token, revoked public token) the transport stops and logs the reason once, instead of retrying forever.
# Why MCP funnels
Source: https://docs.waniwani.ai/sdk/why-mcp-funnels
Why ChatGPT, Claude, and Cursor change distribution, and why conversational funnels are the new web forms.
A distribution shift is underway. ChatGPT, Claude, and Cursor are becoming the new browsers. MCP is the store. The way users find products, get qualified, book, and buy is moving from web forms inside web pages to conversational funnels inside AI clients.
This page explains why we believe that, and why `@waniwani/sdk` exists.
## The distribution shift
For twenty years the web ran on the same loop: a user lands on a page, fills a form, gets routed to the next step. Sales funnels, lead generation, booking, quoting. Money moved through forms.
That loop is moving. The interface a user starts a buying journey in is increasingly an AI client, not a browser tab. ChatGPT, Claude, and Cursor are where attention concentrates. MCP is the protocol those clients use to call third-party tools. From the user's perspective: they describe what they want, the AI client calls your MCP server, your server runs the funnel.
The implications are not subtle:
* **One server, every surface.** One MCP server reaches ChatGPT, Claude, Cursor, and any other MCP-capable client. One edit to your messaging deploys everywhere.
* **No website, no form, no chatbot widget.** The funnel runs inside the AI client the user already uses.
* **The funnel is a tool call.** The same tool call drives the conversation, validates input, branches, persists state.
## Conversational funnels are the new web forms
The shape of the work has not changed. A funnel still has steps. A booking funnel still asks for a service, a slot, and a confirmation. An insurance funnel still collects details, validates, and returns a quote. A lead funnel still captures email, role, and use case.
What changes is the rendering surface. The funnel runs inside an AI client. The model is the front-end. The funnel logic still lives on your server.
This is good news: the business outcomes you care about (qualified leads, booked appointments, returned quotes, completed purchases) translate directly. It is bad news only if you assume the AI client will run the funnel for you. It will not.
## Why LLMs cannot run funnels on their own
A funnel needs five things:
1. **Deterministic order.** Steps run in a known sequence.
2. **Typed fields.** Email is an email, date is a date.
3. **Validation.** Bad input is rejected and re-asked.
4. **Branching.** The next step depends on the current answer.
5. **Resumable state.** Every step builds on the last, across many tool calls.
LLMs left to themselves do none of these reliably. They paraphrase questions, skip fields, accept malformed input, drop state between turns. This is not a bug in the model. It is a mismatch: a stateless system cannot run a stateful process.
The fix is to make the funnel deterministic on the server, and let the model do what it is good at: rendering the next question, interpreting the answer, handing back to the server.
## Why generic agent frameworks are not the right fit
Frameworks like LangChain and LangGraph are general-purpose agent builders. They expose every primitive: graphs, tools, memory, retrievers, planners. You can build a funnel on top of them. You can also build a database on top of `malloc`.
The cost is that every funnel ergonomic (interrupt on a missing field, re-ask on validation failure, skip a step whose state is already known, hand off to a widget for confirmation, persist state under the MCP session id) is something you re-invent on every project. The frameworks give you the road. They do not give you the car.
`createFlow` is funnel-shaped by design. A node is a step. An interrupt is a form field. An edge is a transition. A conditional edge is a branching question. Typed state via Zod is your lead data. The graph compiles to one MCP tool. The engine runs it.
See [Waniwani vs LangGraph](/sdk/compare/vs-langgraph) for a direct comparison.
## Production-validated
We did not start with this abstraction. We arrived at it.
`@waniwani/sdk` is forked from internal distribution MCPs we shipped for paying customers: insurance quoting, pet care, lead capture, booking. We hit the same pattern enough times to extract it, and open-sourced once the shape stabilized.
The hosted tier (analytics, KV store, embeddable widgets, knowledge base) is opt-in via a single env var. The engine works standalone with any get/set/delete backend.
## What this means for you
If you are building distribution into AI clients, you have three options:
1. **Hand-roll on the raw MCP SDK.** You serialize state through the model on every turn. The model forgets, paraphrases, skips. You re-invent re-ask, validation, and branching on every project.
2. **Build on a generic agent framework.** You get graph primitives but write all the funnel ergonomics yourself.
3. **Use `createFlow`.** You declare state, nodes, and edges. The engine handles interrupts, validation, branching, and resumption, and compiles to one MCP tool.
This SDK exists because we believe option three should be the default. The funnel is the unit of distribution in the AI era, and the abstraction should be funnel-shaped.
## Next
Boot a flow-driven MCP server in 60 seconds.
Sales funnels, lead gen, booking, quote flows.
Direct comparison with LangChain's graph framework.
Nodes, edges, interrupts, conditional branching.