Skip to main content
A session represents one conversation between a user and an MCP client. Events are grouped into a session by a sessionId that the SDK extracts from the MCP request metadata (_meta). You do not create sessions. You pass meta: extra._meta on every track() call, and the SDK does the correlation.

How session correlation works

The SDK reads the first non-empty string value it finds under these keys of _meta, in order:
  1. waniwani/sessionId
  2. openai/sessionId
  3. openai/session
  4. sessionId
  5. conversationId
  6. anthropic/sessionId
The matched value becomes the correlation.sessionId field on the outgoing V2 envelope. The SDK also extracts requestId, traceId, correlationId, and externalUserId from _meta using the same approach. Source is derived from the session key (openai/sessionId -> chatgpt, anthropic/sessionId -> claude, waniwani/sessionId -> chatbar).

The rule

Always pass meta: extra._meta when tracking events from inside a tool handler.
server.registerTool(
  "get_quote",
  { /* config */ },
  async ({ amount, currency }, extra) => {
    // ... run your business logic

    await wani.track({
      event: "quote.succeeded",
      properties: { amount, currency },
      meta: extra._meta,
    });

    return { content: [{ type: "text", text: "Quoted" }] };
  },
);
If you omit meta and do not set sessionId explicitly, the envelope ships without a session id and the dashboard cannot group it with the rest of the conversation.

Where _meta lives

MCP libraryLocation
@modelcontextprotocol/sdkrequest.params._meta (on the raw request), surfaced as extra._meta in tool handlers
@vercel/mcp-handlerextra._meta
Wherever you see extra._meta in these docs, substitute the equivalent for your runtime.

The scoped client inside tools and flows

When a server is wrapped with withWaniwani(server), each tool invocation receives a request-scoped WaniWani client on extra["waniwani/client"]. The scoped client is also surfaced as context.waniwani inside createTool handlers and as waniwani inside flow nodes. Its track() and identify() methods merge the current request’s _meta automatically, so you do not pass it yourself.
import { createTool } from "@waniwani/sdk/mcp";
import { z } from "zod";

createTool(
  {
    id: "get_quote",
    title: "Get quote",
    description: "Return a price quote",
    inputSchema: { amount: z.number() },
  },
  async ({ amount }, context) => {
    // context.waniwani is the scoped client, meta is already attached
    await context.waniwani?.track({
      event: "quote.succeeded",
      properties: { amount, currency: "USD" },
    });
    return { text: `Quoted ${amount}` };
  },
);
Inside a flow node:
.addNode("record", async ({ state, waniwani }) => {
  await waniwani?.track({
    event: "quote.succeeded",
    properties: { amount: state.amount },
  });
  return {};
})
See Flows for the full node context.

Manual session ids

For background jobs, backfills, or tests where _meta is not available, you can set sessionId explicitly:
await wani.track({
  event: "purchase.completed",
  properties: { amount: 4999, currency: "USD" },
  sessionId: "session_01HX...",
});
Use manual sessionId only when there is no _meta to pass. Inside tool handlers, always prefer meta: extra._meta.

Session lifecycle

The WaniWani backend emits session.started automatically the first time it sees a new sessionId. You do not send it yourself, and session.started in the SDK’s EventType union exists only to type the ingestion envelope. Sessions do not have a hard close. The backend treats them as closed after an idle window. If the user comes back, they either resume the same session (within the window) or start a fresh one, depending on what the host client sends in _meta.