Skip to main content
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.

What it does, without an API key

The wrapper still adds three 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.
  3. An intent argument on every tool. See Intent capture. The schema change applies with or without a key; the value is only sent anywhere once a key is set.
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.

Intent capture

On ChatGPT and Claude your server never sees the user’s messages. The closest thing to them is what the model writes into tool arguments, so the wrapper asks for one: why the user came. Every wrapped tool’s input schema gains one optional string argument:
The model sends it on its first call to your server in a conversation, so later calls carry none. Your handler never receives intent. The wrapper strips it before calling the handler and records it on tool.called as properties.telemetry.intent, beside input, which holds exactly the arguments your handler got:
A malformed value, such as a number where text belongs, is dropped without failing the call.
  • Flow tools already take top-level intent and context arguments, so their schema is unchanged. The wrapper copies both into properties.telemetry, and they stay in input as well.
  • A tool whose schema declares intent itself keeps that field. The wrapper leaves its schema and arguments alone and records nothing for it.
Each wrapped tool advertises about 40 more tokens in tools/list (about 65 with omitPII).
Directory-reviewed apps. OpenAI’s plugin guidelines allow a brief, task-specific user intent field and rule out broad contextual fields and accumulated conversation context, which is why this is one short string asked for once. It still collects free text about the user’s goal, so mention it in your app’s privacy policy, or pass captureIntent: false if you can’t. A directory that reviews apps (the ChatGPT app directory) shows the tool metadata from your last submission until you submit a new version.

When to use it

Options

WaniWaniClient
The client used for tracking. Defaults to waniwani(), which picks up WANIWANI_API_KEY from the environment.
ToolType | (name: string) => ToolType | undefined
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.
Record<string, unknown>
Extra fields merged into every tool.called event. Use this to tag deployments, tenants, or releases.
boolean
default:false
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.
boolean
default:true
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.
boolean | { tools?: string[]; omitPII?: boolean }
default:true
Add an optional intent string to every tool’s input schema, sent by the model on its first call to your server, strip it before your handler runs, and record it on tool.called as properties.telemetry.intent. Pass { tools } to limit capture to specific tool names, { omitPII: true } to ask the model to keep personal data out of it, or false to leave every schema as declared and record nothing. See Intent capture.
(error: Error) => void
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

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.

Session-scoped client in flows

Inside a flow 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).