waniwani-sdk

Installation
SKILL.md

WaniWani SDK (@waniwani/sdk)

SDK for MCP event tracking, multi-step conversational flows, dual-platform widget creation, knowledge base search, and embeddable chat components. Works with @modelcontextprotocol/sdk, @vercel/mcp-handler, and Skybridge.

Docs: docs.waniwani.ai Dashboard: app.waniwani.ai

Install

bun add @waniwani/sdk     # or: pnpm add / npm install

Peer dependencies vary by export path (see table below). The core tracking module has zero runtime dependencies.

Quick Start

  1. Get an API key from app.waniwani.ai (create an MCP project, copy wwk_... key)
  2. Set the env var:
# .env
WANIWANI_API_KEY=wwk_...
  1. Create a client singleton:
// lib/waniwani.ts
import { waniwani } from "@waniwani/sdk";

export const wani = waniwani();
// Reads WANIWANI_API_KEY from env — one instance, import everywhere
  1. Wrap your MCP server for automatic tracking:
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { withWaniwani } from "@waniwani/sdk/mcp";
import { wani } from "../lib/waniwani";

const server = new McpServer({ name: "my-server", version: "1.0.0" });

// Every tool call now emits a tool.called event with timing and status
withWaniwani(server, { client: wani });

server.registerTool("get_pricing", /* ... */);
  1. Verify: trigger any tool call, then check your WaniWani dashboard.

Export Paths

Export Purpose Reference Peer Dependencies
@waniwani/sdk Event tracking client (this file) None
@waniwani/sdk/mcp Server-side tools, widgets, flows, tracking tools-and-widgets, flows @modelcontextprotocol/sdk, zod
@waniwani/sdk/mcp/react Client-side widget React hooks widget-react-hooks react
@waniwani/sdk/chat Chat React component + embed script chat-widget react, react-dom, @ai-sdk/react, ai
@waniwani/sdk/chat/styles.css Chat widget stylesheet chat-widget --
@waniwani/sdk/next-js Next.js route handler adapter chat-server --
@waniwani/sdk/kb Knowledge base client knowledge-base None
@waniwani/sdk/chat/server Chat API handler (server-side) chat-server ai
@waniwani/sdk/evals Eval framework (chat, conversation, scenarios) -- ai

Core: Event Tracking (@waniwani/sdk)

waniwani(config?)

Creates a client instance. Reads WANIWANI_API_KEY and WANIWANI_API_URL from env vars when called with no arguments.

import { waniwani } from "@waniwani/sdk";

const client = waniwani();

// Or with explicit config:
const client = waniwani({
  apiKey: process.env.WANIWANI_API_KEY,
  apiUrl: "https://app.waniwani.ai",  // default
});

Create one client in lib/waniwani.ts and import it everywhere. Do not call waniwani() in multiple files.

defineConfig(config)

Used in waniwani.config.ts to define project-level configuration:

import { defineConfig } from "@waniwani/sdk";

export default defineConfig({
  // WaniWaniProjectConfig fields
});

client.track(event)

Enqueues an event for batched delivery. Returns immediately after enqueue.

await client.track({
  event: "tool.called",
  properties: { name: "get_pricing", type: "pricing" },
  meta: extra._meta,  // MCP request metadata
});

client.identify(userId, properties?)

Sends a one-shot user.identified event.

await client.identify("user@example.com", { plan: "pro", company: "Acme" });

Event Types

Event Key Properties
session.started --
tool.called name, type ("pricing", "product_info", "availability", "support", "other")
quote.requested --
quote.succeeded amount, currency
quote.failed --
link.clicked url
purchase.completed amount, currency

meta Field

Pass MCP request metadata to auto-extract session and user info:

  • @modelcontextprotocol/sdk: request.params._meta
  • @vercel/mcp-handler: extra._meta

client.flush() / client.shutdown(options?)

// Flush buffered events
await client.flush();

// Flush and stop transport (for serverless/tests)
const result = await client.shutdown({ timeoutMs: 2000 });
// => { timedOut: boolean, pendingEvents: number }

In Node environments, the SDK auto-flushes on beforeExit, SIGINT, and SIGTERM. For serverless or edge runtimes, call shutdown() explicitly.

Auto-Tracking: withWaniwani (@waniwani/sdk/mcp)

Wraps an MCP server so all tool handlers automatically emit tool.called events after execution with durationMs, status ("ok" or "error"), and errorMessage (on failure).

import { withWaniwani } from "@waniwani/sdk/mcp";
import { wani } from "../lib/waniwani";

withWaniwani(server, { client: wani });

Options (all optional):

Field Type Default Description
client WaniWaniClient auto from env Pre-built client
toolType string | (name) => string "other" Default tool type for events
metadata Record<string, unknown> -- Extra metadata on every event
flushAfterToolCall boolean false Flush after each tool call
onError (error) => void -- Non-fatal tracking error callback
injectWidgetToken boolean true Inject JWT into _meta.waniwani for browser widget tracking

Combined example: auto-tracking + manual events

const client = waniwani();

withWaniwani(server, { client, flushAfterToolCall: true });

server.registerTool("get_quote", config, async (input, extra) => {
  // tool.called is tracked automatically
  const quote = await generateQuote(input.product);

  // Additional event tracked manually
  await client.track({
    event: "quote.succeeded",
    properties: { amount: quote.amount, currency: "USD" },
    meta: extra._meta,
  });

  return { content: [{ type: "text", text: `Quote: $${quote.amount}` }] };
});

Building Flows

Multi-step conversational flows with server-side state. Define a state graph, compile it into an MCP tool, and let the AI drive the flow step by step.

import { createFlow, START, END, registerTools } from "@waniwani/sdk/mcp";
import { z } from "zod";

const flow = createFlow({
  id: "demo_qualification",
  title: "Demo Qualification",
  description: "Qualify a lead for a demo.",
  state: {
    email: z.string().describe("Work email"),
    role: z.string().describe("Role at company"),
  },
})
  .addNode("ask_email", ({ interrupt }) =>
    interrupt({ email: { question: "What is your work email?" } })
  )
  .addNode("ask_role", ({ interrupt }) =>
    interrupt({ role: { question: "What is your role?" } })
  )
  .addNode("done", ({ state }) => ({ summary: `${state.email}, ${state.role}` }))
  .addEdge(START, "ask_email")
  .addEdge("ask_email", "ask_role")
  .addEdge("ask_role", "done")
  .addEdge("done", END)
  .compile();

await registerTools(server, [flow]);

Flows support interrupt validation, conditional edges, widget steps, nested state, and pre-filling. See references/flows.md for the full guide.

Guided Playbooks

Step-by-step scripts for common tasks. Follow these when the user wants to build something from scratch.

User wants to... Playbook
Initialize a new MCP distribution from the template scripts/initialize.md
Create their first flow scripts/create-flow.md
Tunnel the dev server for remote testing scripts/tunnel.md

When a playbook exists for the user's task, follow the playbook step by step instead of writing code directly. The playbooks include prerequisite checks, interactive design steps, and testing instructions.

Reading Guide

You want to... Read
Add analytics to an existing MCP server setup + auto-tracking section above
Create tools with widget UIs tools-and-widgets + widget-react-hooks
Build multi-step conversational flows flows + flows API reference
Add a knowledge base with search knowledge-base
Embed a chat widget on a website chat-widget + chat-server

Common Mistakes

  • Missing WANIWANI_API_KEY env var -- Flow state and tracking will throw. Set it in all environments (dev, Vercel, production).
  • Creating multiple clients -- Create one in lib/waniwani.ts and import everywhere.
  • Wrong import paths -- Hooks: @waniwani/sdk/mcp/react. Chat: @waniwani/sdk/chat. Tools: @waniwani/sdk/mcp.
  • Missing WidgetProvider in page component -- The page MUST wrap children in <WidgetProvider>. Never call hooks (useToolOutput, useTheme, etc.) directly in the page -- they throw useWidgetClient must be used within a WidgetProvider. Put all hook usage in a child component rendered inside WidgetProvider.
  • Missing InitializeNextJsInIframe in layout -- Add <InitializeNextJsInIframe baseUrl={...} /> in the root layout's <head> so fetch/navigation work inside cross-origin iframes.
  • Forgetting START/END edges in flows -- Every flow needs addEdge(START, firstNode) and addEdge(lastNode, END).
  • Importing interrupt/showWidget directly -- These come from the handler context: ({ interrupt }) => interrupt(...).
Related skills
Installs
9
Repository
waniwani-ai/sdk
GitHub Stars
2
First Seen
Apr 14, 2026