client.track(), the SDK enqueues the event in memory, and a background transport batches and posts it to POST /api/mcp/events/v2/batch. Tracking never blocks your tool handler.
Mental model
Events
A typed record of something that happened (a tool call, a quote, a purchase). Has a name, properties, and correlation ids.
Sessions
A session groups events from one MCP conversation. The SDK reads the session id from MCP
_meta, so you pass meta: extra._meta on every call.Identify
Attach an
externalUserId to the current session so events get linked to a real user.Automatic tool tracking
withWaniwani(server) wraps every tool handler and emits tool.called for you with timing and status.Track your first event
How the transport behaves
When you calltrack(), the SDK:
- Normalizes the input into a canonical Events API V2 envelope (see events-api-v2-contract).
- Generates a deterministic event id (
evt_<uuid>). - Pushes the envelope on an in-memory buffer and returns
{ eventId }immediately. - A background timer flushes the buffer every
flushIntervalMs(default1000), or sooner when the batch fills (maxBatchSize, default20). - Retries transient failures (
408,425,429,5xx) with exponential backoff, up tomaxRetries(default3). - Stops retrying on
401/403auth errors to avoid retry storms.
Long-running vs. serverless
| Runtime | How to make sure events ship |
|---|---|
| Long-running Node process | Nothing. The SDK attaches beforeExit, SIGINT, and SIGTERM hooks that call shutdown() automatically. |
| Serverless / short-lived | Call await wani.flush() before the function returns, or pass flushAfterToolCall: true to withWaniwani(). |
Client API
track(event)
Enqueue an event.
identify(userId, properties?, meta?)
Send a
user.identified event and attach the user id to the session.flush()
Wait for the current buffer to drain.
shutdown({ timeoutMs? })
Flush and stop the transport. Returns
{ timedOut, pendingEvents }.All four methods throw synchronously if no
apiKey was resolved (from config or WANIWANI_API_KEY). If you instantiate waniwani() without an api key, the client is inert and calling track() will throw.