Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.waniwani.ai/llms.txt

Use this file to discover all available pages before exploring further.

Once nodes and edges are in place, you compile the graph and register it. .compile() returns a RegisteredFlow that maps onto the standard server.registerTool(name, config, handler) contract.

Compile

const onboardingFlow = createFlow({
  id: "onboarding",
  title: "User Onboarding",
  description: "Use when a new user wants to get started.",
  state: { /* ... */ },
})
  .addNode({ id: "...", run: () => ({}) })
  .addEdge(/* ... */)
  .compile();
.compile() validates the graph (see Edges) and throws immediately if the graph is malformed. Accepts one optional argument for a custom state store:
.compile({ store: myCustomStore });
The default is WaniwaniFlowStore, which uses the WaniWani API as a key-value store keyed by session id. It reads WANIWANI_API_KEY and WANIWANI_API_URL from env.

The RegisteredFlow shape

type RegisteredFlow = {
  name: string;            // MCP tool name (equals config.id)
  config: ToolConfig;      // title, description, inputSchema, annotations
  handler: FlowToolHandler;
  register: (server: McpServer) => Promise<void>;
  graph: () => string;     // Mermaid flowchart TD
};

Registering

Use the register shortcut for the common case:
await onboardingFlow.register(server);
Or call server.registerTool directly if you prefer:
server.registerTool(
  onboardingFlow.name,
  onboardingFlow.config,
  onboardingFlow.handler,
);
Both are equivalent.

Registering widget display tools alongside flows

When a flow uses showWidget("name", ...), the named display tool must be registered on the same server. Use Skybridge’s server.registerWidget(name, meta, schema, handler) — it binds the widget name to the matching React component under web/src/widgets/<name>.tsx and emits the widget-rendering metadata that ChatGPT, Claude, and other MCP clients understand. You don’t need to set _meta.openai/outputTemplate by hand.
server
  .registerWidget(
    "show_pricing",
    { description: "Display the pricing plan grid. Do NOT list plans in text." },
    { inputSchema: { /* zod fields */ }, annotations: { readOnlyHint: true } },
    async (input) => ({
      structuredContent: input,
      content: [{ type: "text", text: "Pricing shown" }],
      isError: false,
    }),
  )
  .registerTool(quoteFlow.name, quoteFlow.config, quoteFlow.handler);
See the Pet insurance example for a complete server.

Minimal end-to-end wiring

A complete server/src/app.ts looks like this:
server/src/app.ts
import "dotenv/config";
import { McpServer } from "skybridge/server";
import { withWaniwani } from "@waniwani/sdk/mcp";
import { onboardingFlow } from "./flows/onboarding.js";
import { quoteFlow } from "./flows/quote.js";

export const server = new McpServer(
  { name: "my-mcp-app", version: "0.0.1" },
  { capabilities: {} },
)
  .registerTool(onboardingFlow.name, onboardingFlow.config, onboardingFlow.handler)
  .registerTool(quoteFlow.name, quoteFlow.config, quoteFlow.handler);

// Wrap after registrations to auto-track every tool call.
withWaniwani(server);

export type AppType = typeof server;
withWaniwani is idempotent — calling it twice on the same server is a no-op. The skybridge/server import is what the MCP distribution template uses as the dev/build runtime; it provides the .registerWidget(...) fluent API plus widget mounting for the React components under web/src/widgets/.

Debugging a compiled flow

Inspect the graph

flow.graph() returns a Mermaid diagram of the compiled flow. Log it at startup in development:
if (process.env.NODE_ENV !== "production") {
  console.log(onboardingFlow.graph());
}
Paste the output into any Mermaid renderer.

Unit-test with createFlowTestHarness

The SDK ships a test harness that drives a compiled flow without spinning up a real transport or store. Import it from @waniwani/sdk/mcp:
import { createFlowTestHarness } from "@waniwani/sdk/mcp";
import { onboardingFlow } from "./flows/onboarding.js";

const harness = await createFlowTestHarness(onboardingFlow);

const first = await harness.start("Get me onboarded");
// first.status === "interrupt"
// first.field === "email"

const second = await harness.continueWith({ email: "alice@example.com" });
// second.status === "interrupt"
// second.field === "useCase"
The harness API is:
  • start(intent, stateUpdates?): begins a run. intent is positional.
  • continueWith(stateUpdates?): resumes the paused run.
  • lastState(): returns the persisted FlowTokenContent when a store is passed via createFlowTestHarness(flow, { stateStore }).
Each call returns a FlowTestResult that is the parsed FlowContent (interrupt, widget, complete, or error) plus a decodedState field populated when a store is supplied. Use it to write deterministic tests for branching, validation, and state transitions.