npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@gomcp/analytics

v0.2.0

Published

Lightweight analytics and observability for MCP servers

Readme

@gomcp/analytics

Lightweight analytics and observability for Model Context Protocol (MCP) servers. Zero required dependencies, framework-agnostic, works at the JSON-RPC transport level.

Features

  • Transport-level interception — works with any MCP server (official SDK, FastMCP, custom)
  • Handler wrapping — instrument individual tool handlers for granular control
  • Multiple exporters — console, JSON file, OpenTelemetry OTLP, or custom functions
  • In-memory stats — p50/p95/p99 latencies, error rates, call counts per tool
  • Session analytics — aggregated metrics per sessionId with top-session ranking
  • Sampling — configurable sample rate to control overhead
  • Bounded percentile memory — keeps a fixed recent latency window per accumulator
  • Zero required deps — only @modelcontextprotocol/sdk as a peer dependency

Installation

npm install @gomcp/analytics

Quick Start

import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
import { McpAnalytics } from "@gomcp/analytics";

// 1. Create analytics instance
const analytics = new McpAnalytics({
  exporter: "console",
});

// 2. Create your server and transport
const server = new McpServer({ name: "my-server", version: "1.0.0" });
const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: () => crypto.randomUUID() });

// 3. Instrument the transport (intercepts all tool calls automatically)
const trackedTransport = analytics.instrument(transport);
await server.connect(trackedTransport);

// 4. Access stats at any time
console.log(analytics.getStats());
// { totalCalls: 42, errorRate: 0.02, tools: { search: { count: 30, p50Ms: 120, ... } } }

// 5. Clean shutdown
await analytics.shutdown();

API

new McpAnalytics(config)

Create an analytics instance.

| Option | Type | Default | Description | |-------------------|----------------------------------------------------------|---------|-----------------------------------------------------------| | exporter | "console" \| "json" \| "otlp" \| Function | — | Where to send metrics (required) | | json | { path: string } | — | JSON file config (required when exporter: "json") | | otlp | { endpoint: string, headers?: Record<string, string> } | — | OTLP config (required when exporter: "otlp") | | sampleRate | number | 1.0 | Fraction of calls to sample (0.0 to 1.0) | | flushIntervalMs | number | 5000 | How often to flush events to the exporter | | maxBufferSize | number | 10000 | Max events in the ring buffer | | metadata | Record<string, string> | — | Metadata added to every event | | samplingStrategy| "per_call" \| "per_session" | "per_call" | Sampling behavior for transport instrumentation | | toolWindowSize | number | 2048 | Recent durations kept per accumulator for percentiles | | tracing | boolean | false | Create OpenTelemetry spans via the global tracer provider |

analytics.instrument(transport)

Wrap an MCP transport to automatically intercept all tools/call requests and responses. Returns a proxy transport that can be used in place of the original.

const trackedTransport = analytics.instrument(transport);
await server.connect(trackedTransport);

analytics.track(handler, toolName?)

Wrap a tool handler function to record metrics. Use this when you want per-handler control instead of transport-level interception.

server.tool("search", schema, analytics.track(async (params) => {
  return await doSearch(params);
}, "search"));

analytics.getStats()

Returns an AnalyticsSnapshot with aggregated metrics:

interface AnalyticsSnapshot {
  totalCalls: number;
  totalErrors: number;
  errorRate: number;
  uptimeMs: number;
  tools: Record<string, ToolStats>;
  sessions: Record<string, SessionStats>;
}

interface ToolStats {
  count: number;
  errorCount: number;
  errorRate: number;
  p50Ms: number;
  p95Ms: number;
  p99Ms: number;
  avgMs: number;
  lastCalledAt: number;  // Unix timestamp ms
}

interface SessionStats {
  count: number;
  errorCount: number;
  errorRate: number;
  avgMs: number;
  lastCalledAt: number;  // Unix timestamp ms
  tools: Record<string, ToolStats>;
}

analytics.getToolStats(toolName)

Get stats for a specific tool. Returns undefined if the tool hasn't been called.

analytics.getSessionStats(sessionId)

Get stats for a specific session. Returns undefined if the session hasn't been observed.

analytics.getTopSessions(limit?)

Returns sessions ordered by call count (descending). Default limit is 10.

analytics.flush()

Force-flush all pending events to the exporter.

analytics.reset()

Clear all collected data.

analytics.shutdown()

Stop the flush timer and flush remaining events. Call this on process exit.

Reliability Semantics

  • Flush failures do not drop events. Failed batches are re-queued and retried on the next flush.
  • Periodic flush errors are handled internally (they are reported, but they do not crash the process).
  • Percentile memory is bounded via toolWindowSize (recent-window percentile calculation).

Migration Note

  • otlp.useGlobalProvider has been removed. The OTLP exporter now always uses its own OTLP provider.
    If you want spans in your app's global tracer context, use tracing: true.

Exporters

Console

Pretty-prints batches to stdout:

new McpAnalytics({ exporter: "console" });

JSON File

Appends events as JSONL (one JSON object per line):

new McpAnalytics({
  exporter: "json",
  json: { path: "./analytics.jsonl" },
});

OpenTelemetry OTLP

Sends events as OpenTelemetry spans. Requires @opentelemetry/api, @opentelemetry/sdk-trace-base, and @opentelemetry/exporter-trace-otlp-http as peer dependencies (dynamically imported only when used):

npm install @opentelemetry/api @opentelemetry/sdk-trace-base @opentelemetry/exporter-trace-otlp-http
new McpAnalytics({
  exporter: "otlp",
  otlp: {
    endpoint: "http://localhost:4318/v1/traces",
    headers: { "Authorization": "Bearer ..." },
  },
});

Note: OTLP export emits synthetic spans derived from collected tool-call events.

Custom Function

Provide your own export function:

new McpAnalytics({
  exporter: async (events) => {
    await fetch("https://my-analytics.example.com/ingest", {
      method: "POST",
      body: JSON.stringify(events),
    });
  },
});

Tracing (dd-trace / OpenTelemetry)

When you use an APM like dd-trace that registers itself as the global OpenTelemetry provider, you can make MCP tool calls appear as spans in your existing traces with zero extra configuration:

import "dd-trace/init"; // sets up dd-trace as global OTel provider

import { McpAnalytics } from "@gomcp/analytics";

const analytics = new McpAnalytics({
  exporter: "console",
  tracing: true, // creates spans via the global tracer provider
});

const tracked = analytics.instrument(transport);
await server.connect(tracked);
// Tool calls now appear as "mcp.tool_call" spans in Datadog

This works with any OTel-compatible provider (Datadog, New Relic, Honeycomb, etc.). The tracing flag dynamically imports @opentelemetry/api and uses the global tracer — no OTLP exporter setup needed.

When using analytics.track() (handler wrapping), the handler executes inside the span context, so any downstream OTel-instrumented calls (HTTP, DB, etc.) become children of the MCP tool span.

Span attributes

Each mcp.tool_call span includes these attributes:

| Attribute | Description | |--------------------------|-------------------------------------------------| | mcp.tool.name | Tool name | | mcp.tool.input_size | Input size in bytes | | mcp.tool.duration_ms | Duration (OTLP exporter only) | | mcp.tool.success | Whether the call succeeded (OTLP exporter only) | | mcp.tool.output_size | Output size in bytes (OTLP exporter only) | | mcp.tool.error_message | Error message if failed (OTLP exporter only) |

License

MIT