Back to Resources

How to Add Analytics to Your MCP Server in 3 Lines of Code

A step-by-step tutorial for adding full product analytics to your MCP server using Yavio. Covers automatic instrumentation, user identification, custom funnels, and self-hosting.

You have an MCP server running in production. Users are calling your tools through Claude, ChatGPT, or Cursor. But your visibility ends at server logs — you don't know which tools get used most, where users drop off, what errors they hit, or whether anyone comes back.

This tutorial shows you how to add full product analytics to your MCP server in under five minutes using Yavio, an open-source analytics platform built for MCP Apps and ChatGPT Apps.

What You'll Get

After following this guide, your MCP server will automatically capture every tool call, resource read, and prompt execution — with zero changes to your existing tool handlers.

On the Yavio dashboard, you'll see an overview of all activity and traffic trends, per-tool breakdowns (call volume, success rate, latency), an error analysis view showing which tools fail and why, a live event feed of real-time tool calls, and (once you add user identification) funnels, retention curves, and per-user analytics.

Prerequisites

An MCP server built with the official @modelcontextprotocol/sdk. If you're using the Python SDK, Yavio supports that too — the pattern is similar.

Node.js 18 or later.

A Yavio account (free at app.yavio.ai) or a self-hosted Yavio instance.

Step 1: Install the SDK

npm install @yavio/sdk

Step 2: Initialize Your Project

npx @yavio/cli init

This creates a .yavio config file in your project with your project ID and API key. If you're using Yavio Cloud, it walks you through authentication. For self-hosted, point it to your ingestion endpoint.

Step 3: Wrap Your Server

This is the entire integration. Find the line where you create your MCP server and wrap it with withYavio():

Before:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";

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

After:

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { withYavio } from "@yavio/sdk";

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

That's it. Two new lines (the import and the wrapper), and your existing server.tool(), server.resource(), and server.prompt() definitions stay exactly the same. withYavio() creates a transparent proxy around your MCP server that captures every interaction before passing it through to your handlers.

Deploy this change. Within seconds, events start flowing into your Yavio dashboard.

What Gets Captured Automatically

With zero additional code, withYavio() captures:

Tool calls — every time an AI client invokes one of your tools. Captured data includes the tool name, input parameters, output content, execution duration, and whether the call succeeded or threw an error.

Resource reads — every time an AI client reads one of your resources. Captured data includes the resource URI, content type, and response time.

Prompt executions — every time a prompt template is resolved. Captured data includes the prompt name and arguments.

Errors — any exception thrown inside a tool handler. Captured with the full error message, stack trace, and the input parameters that triggered it.

All of this happens without touching your tool handlers. Your business logic stays clean.

Going Beyond Automatic: User Identification

Automatic instrumentation tells you what's happening. User identification tells you who's doing it.

Inside any tool handler, you can call ctx.yavio.identify() to tie events to a known user:

server.tool("get_account", { accountId: z.string() }, async (params, ctx) => {
  // Identify the user — all subsequent events in this session
  // will be attributed to this user
  ctx.yavio.identify(params.accountId, {
    plan: "pro",
    company: "Acme Inc"
  });

  const account = await db.getAccount(params.accountId);
  return { content: [{ type: "text", text: JSON.stringify(account) }] };
});

Once users are identified, the dashboard unlocks retention analysis (do users come back after day 1, day 7, day 30?), cohort breakdowns (how do "pro" users behave vs. "free" users?), and per-user event timelines (what did user X do in their last session?).

Tracking Custom Funnels and Conversions

For multi-step workflows, use .step() and .conversion() to define funnel stages:

server.tool("search_products", { query: z.string() }, async (params, ctx) => {
  ctx.yavio.step("product_search");
  const results = await catalog.search(params.query);
  return { content: [{ type: "text", text: JSON.stringify(results) }] };
});

server.tool("add_to_cart", { productId: z.string() }, async (params, ctx) => {
  ctx.yavio.step("add_to_cart");
  await cart.add(params.productId);
  return { content: [{ type: "text", text: "Added to cart." }] };
});

server.tool("checkout", { cartId: z.string() }, async (params, ctx) => {
  ctx.yavio.conversion("purchase", { value: cart.total });
  const order = await payments.charge(params.cartId);
  return { content: [{ type: "text", text: `Order ${order.id} confirmed.` }] };
});

On the dashboard, this becomes a visual funnel: search → add to cart → purchase, with exact drop-off percentages at each stage.

Using .track() for Custom Events

For events that don't fit into a funnel, use .track():

server.tool("export_report", { format: z.enum(["csv", "pdf"]) }, async (params, ctx) => {
  ctx.yavio.track("report_exported", { format: params.format });
  const file = await reports.export(params.format);
  return { content: [{ type: "text", text: `Report exported as ${params.format}.` }] };
});

Custom events show up in the live event feed and can be used in dashboard filters.

Privacy: PII Stripping

Yavio automatically strips personally identifiable information before storing events. Email addresses, phone numbers, and other PII patterns are removed from event payloads at the ingestion layer. This is on by default — no configuration needed.

If you're in a regulated industry and need additional controls, the self-hosted option gives you full control over data handling and storage.

Self-Hosted Setup

If you prefer running analytics on your own infrastructure:

git clone https://github.com/yavio-ai/yavio-analytics.git
cd yavio-analytics
cp .env.example .env
# Edit .env with your secrets
docker compose up -d

Then point your SDK config to your local ingestion endpoint (default: http://localhost:3001). The dashboard runs on http://localhost:3000. Same features as Cloud, running entirely on your hardware.

What to Do After Setup

Once events are flowing, here's where to look first:

Check the overview dashboard. Verify that tool calls are coming in and the numbers look plausible. This confirms the integration is working.

Look at per-tool breakdowns. Identify which tools get the most traffic and which have the highest error rates. Fix the errors first — reliability is the foundation.

Set up your first funnel. If your app has a multi-step workflow, define the funnel stages using .step() and .conversion(). Watch where users drop off.

Add user identification. Even basic identification (a user ID without traits) unlocks retention analysis. This is the metric that tells you whether you have a product or a toy.

The whole setup — from npm install to seeing your first events on the dashboard — takes less than five minutes. The insights you get from it will change how you build your MCP app.


Yavio is open source (MIT). Star us on GitHub or try Yavio Cloud free.