Skip to main content
withWaniwani(server) instruments an MCP server so every tool call emits a tool.called event with duration, status, input, and output. One line, no changes inside your tool handlers.

Usage

server/src/index.ts
import express from "express";
import { withWaniwani } from "@waniwani/sdk/mcp";
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import "dotenv/config";

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

// Register tools as usual
server.registerTool(/* ... */);
server.registerTool(/* ... */);

// Instrument the server
withWaniwani(server);

// Connect the Streamable HTTP transport
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: undefined });
await server.connect(transport);

const app = express();
app.use(express.json());
app.post("/mcp", (req, res) => transport.handleRequest(req, res, req.body));
app.listen(3000);
With no client option, withWaniwani creates one internally via waniwani(), which reads WANIWANI_API_KEY from the environment.
Call order does not matter. withWaniwani walks existing _registeredTools at call time and intercepts registerTool for any future registrations. It is idempotent, so a second call on the same server is a no-op.

What gets tracked

Every tool invocation produces a tool.called event containing:
FieldDescription
nameRegistered tool name
typeCategory derived from the toolType option
durationMsHandler execution time
status"ok" or "error"
errorMessagePresent when the handler threw or returned isError
inputTool arguments
outputHandler response
clientNameMCP client name (for example chatgpt, claude)
clientVersionMCP client version
sessionIdCorrelated from _meta (see Sessions)
Errors (thrown or { isError: true }) are tracked and then re-thrown or returned unchanged, so your tool’s error contract is preserved.

Options

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

withWaniwani(server, {
  client: waniwani(),
  toolType: (name) => (name.startsWith("get_price") ? "pricing" : "other"),
  metadata: { deployment: "production" },
  flushAfterToolCall: false,
  injectWidgetToken: true,
  onError: (err) => console.error("[waniwani]", err),
});
client
WaniWaniClient
The client used for tracking. Defaults to waniwani(), which picks up WANIWANI_API_KEY from the environment.
toolType
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.
metadata
Record<string, unknown>
Extra fields merged into every tool.called event. Use this to tag deployments, tenants, or releases.
flushAfterToolCall
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.
injectWidgetToken
boolean
default:true
Inject a short-lived widget tracking token into tool responses under _meta.waniwani, so browser widgets can post events directly to WaniWani without a proxy route. Leave enabled unless you need to disable token minting.
onError
(error: Error) => void
Callback for non-fatal tracking errors. Tracking failures never block tool execution; use this callback when you want visibility into them.

Custom events inside tools

For anything beyond tool.called, call client.track() from inside a handler. Pass meta: extra._meta so the event is linked to the active MCP session.
import { waniwani } from "@waniwani/sdk";

const wani = waniwani();

server.registerTool(
  "get_quote",
  { /* tool config */ },
  async ({ amount, currency }, extra) => {
    await wani.track({
      event: "quote.succeeded",
      properties: { amount, currency },
      meta: extra._meta,
    });

    return { content: [{ type: "text", text: `Quoted ${amount} ${currency}` }] };
  },
);

Also supported

  • Session-scoped client in flows. Inside a flow node, ctx.waniwani is a scoped client that auto-attaches session correlation, so you can call ctx.waniwani.track({ event, properties }) without passing meta.
  • Widget metadata forwarding. Widget _meta declared on a tool definition (Skybridge’s registerWidget, raw ui/resourceUri, OpenAI’s openai/outputTemplate) is copied onto each tool result, so chat UIs that only see tool results can still render widgets.