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

@listo-ai/mcp-observability

v0.4.1

Published

Lightweight telemetry SDK for MCP servers and web applications. Captures HTTP requests, MCP tool invocations, business events, and UI interactions with built-in payload sanitization.

Readme

@listo-ai/mcp-observability

Lightweight telemetry SDK for MCP servers and web applications. Captures HTTP requests, MCP tool invocations, business events, and UI interactions with built-in payload sanitization and automatic data redaction.

Overview

This SDK provides comprehensive observability for Model Context Protocol (MCP) servers and Express.js applications. It supports centralized analytics via a remote sink, and includes a local development dashboard for real-time metrics.

Key features:

  • Easy setup -- Get started in 4 lines of code with createMcpObservabilityEasy()
  • Local dashboard -- Real-time metrics dashboard during development at /telemetry/dashboard
  • Browser client -- Reusable TelemetryClient for browser apps with batching and sendBeacon fallback
  • Centralized analytics -- Send telemetry data to a remote endpoint in production
  • Automatic tracking -- HTTP requests via Express middleware, MCP tool invocations via handler wrapper
  • Payload sanitization -- Built-in redaction of sensitive keys (password, token, apiKey, secret, authorization)
  • Sampling -- Configurable sampling rates with guaranteed capture of errors and session events
  • Zero runtime dependencies -- Only relies on Node.js built-ins (crypto, http)

Table of Contents

Installation

npm install @listo-ai/mcp-observability

Quick Start

1. Initialize observability

import { createMcpObservabilityEasy } from '@listo-ai/mcp-observability';

const observability = createMcpObservabilityEasy({
  serviceName: 'my-mcp-server',
  serviceVersion: '1.0.0',
});

2. Add telemetry routes to your Express app

import { createTelemetryRouter } from '@listo-ai/mcp-observability';

app.use('/telemetry', createTelemetryRouter(express));

3. Set environment variables (optional, for remote telemetry)

LISTO_API_URL=https://your-api.example.com
LISTO_API_KEY=your_api_key

4. Access the local dashboard

http://localhost:3000/telemetry/dashboard

Browser Client

For browser applications, use the lightweight client module with automatic batching and session management:

import { createTelemetryClient } from '@listo-ai/mcp-observability/client';

const client = createTelemetryClient({
  endpoint: 'https://myapp.com/telemetry',
  deviceId: localStorage.getItem('device_id') ?? undefined,
});

// Share the session ID across widget instances
const sessionId = client.getSessionId();

client.track('page_view', { path: '/home' });
client.track('purchase', { amount: 99 }, 'conversion');
client.trackUi('button_click', {
  action: 'click',
  category: 'engagement',
  widgetId: 'cta-1',
});

The client has no Node.js dependencies — it uses fetch(), crypto.randomUUID(), and navigator.sendBeacon(). See Browser Client docs for full API reference.

How It Works

Environment-Based Auto-Configuration

createMcpObservabilityEasy() automatically configures observability based on NODE_ENV:

| Setting | Development | Production | | --- | --- | --- | | Console logging | Errors only | Disabled | | In-memory sink | Enabled (2000 event buffer) | Disabled | | Local dashboard | Enabled | Disabled | | Remote sink | Only if API key set | Enabled (if API key set) | | Sample rate | 100% | 10% | | Event batching | N/A | Every 5s, 50 events/batch |

Data Flow

Your MCP Server / Express App
    |
    v
Observability SDK (captures events)
    |
    +---> Console Sink (dev only, errors)
    +---> InMemory Sink (local dashboard)
    +---> Remote Sink (remote endpoint, batched)

Sink Architecture

Events flow through configurable sinks -- decoupled consumers that process telemetry data:

  • InMemorySink -- Circular buffer (default 2000 events) powering the local dashboard
  • RemoteSink -- Batches events and sends them to a remote endpoint with exponential backoff retries
  • ConsoleSink -- Logs errors to stdout
  • combineSinks() -- Routes events to multiple sinks simultaneously

API Reference

createMcpObservabilityEasy(config)

Creates an observability instance with sensible defaults. This is the recommended entry point.

interface EasySetupConfig {
  serviceName: string;
  serviceVersion?: string;           // default: "1.0.0"
  environment?: "dev" | "staging" | "production";
  listoApiUrl?: string;              // default: process.env.LISTO_API_URL
  listoApiKey?: string;              // default: process.env.LISTO_API_KEY
  enableLocalDashboard?: boolean;    // default: true in dev, false in prod
  sampleRate?: number;               // default: 1 in dev, 0.1 in prod
}
const observability = createMcpObservabilityEasy({
  serviceName: 'my-mcp-server',
  serviceVersion: '0.1.0',
  enableLocalDashboard: true,
});

createMcpObservability(options)

Lower-level factory for full control over sinks, sampling, and redaction.

interface ObservabilityOptions {
  serviceName: string;
  serviceVersion?: string;
  environment?: string;
  sinks?: EventSink[];
  sampleRate?: number;
  redactKeys?: string[];
  capturePayloads?: boolean;
  tenantResolver?: (req: IncomingMessage) => string | undefined;
}

createTelemetryRouter(express)

Express router providing telemetry endpoints:

| Endpoint | Method | Description | | --- | --- | --- | | / | GET | JSON metrics data | | /dashboard | GET | HTML dashboard with auto-refresh | | /event | POST | Single UI event ingestion | | /events | POST | Batch UI event ingestion (max 100) |

import { createTelemetryRouter } from '@listo-ai/mcp-observability';

app.use('/telemetry', createTelemetryRouter(express));
// Dashboard: http://localhost:3000/telemetry/dashboard

expressTelemetry(observability)

Express middleware for automatic HTTP request tracking.

import { expressTelemetry } from '@listo-ai/mcp-observability';

app.use(expressTelemetry(observability));

observability.wrapMcpHandler(requestKind, handler, context?)

Wraps an MCP handler function for automatic invocation tracking.

server.setRequestHandler(
  CallToolRequestSchema,
  observability.wrapMcpHandler(
    'CallTool',
    async (request) => {
      // Your tool implementation
    },
    (request) => ({
      toolName: request.params.name,
      sessionId: request.params.sessionId,
    })
  )
);

observability.recordBusinessEvent(name, options?)

Record custom business events for domain-specific analytics.

observability.recordBusinessEvent('product_search', {
  properties: { q: 'premium items', results: 42 },
  status: 'ok',
  category: 'conversion',
  sessionId: 'sess-abc',
  deviceId: 'device-xyz',
  tenantId: 'tenant-1',
});

Options:

| Field | Type | Description | | --- | --- | --- | | properties | Record<string, unknown>? | Arbitrary key-value metadata | | status | 'ok' \| 'error'? | Event status. Error events bypass sampling | | category | EventCategory? | 'conversion', 'engagement', 'impression', 'navigation', or 'system' | | sessionId | string? | Session identifier for journey tracking | | deviceId | string? | Device identifier for cross-session user identification | | tenantId | string? | Tenant identifier for multi-tenant apps |

observability.recordUiEvent(event)

Record UI interaction events from the browser.

observability.recordUiEvent({
  name: 'cta_click',
  action: 'click',
  category: 'engagement',
  widgetId: 'signup-btn',
  properties: { itemId: 'i-123' },
  sessionId: 'session-456',
  tenantId: 'tenant-789',
});

observability.recordSession(event)

Record MCP session lifecycle events.

observability.recordSession({
  type: 'mcp_session',
  action: 'open',
  sessionId: 'session-123',
});

RemoteSink

Standalone remote sink for custom configurations.

import { RemoteSink } from '@listo-ai/mcp-observability';

const sink = new RemoteSink({
  endpoint: 'https://your-api.example.com/v1/events/batch',
  apiKey: 'your_api_key',
  batchSize: 50,           // events per batch
  flushIntervalMs: 5000,   // flush every 5s
  maxRetries: 3,           // retry with exponential backoff
  debug: false,
  onError: (err, events) => console.error('Failed to send', err),
});

Integration Guide

Express.js

import express from 'express';
import {
  createMcpObservabilityEasy,
  createTelemetryRouter,
  expressTelemetry,
} from '@listo-ai/mcp-observability';

const app = express();
const observability = createMcpObservabilityEasy({
  serviceName: 'my-api',
});

// Track all HTTP requests
app.use(expressTelemetry(observability));

// Telemetry endpoints
app.use('/telemetry', createTelemetryRouter(express));

app.get('/products', (req, res) => {
  // Automatically tracked
  res.json({ products: [] });
});

app.listen(3000);

MCP Server

import { Server } from '@modelcontextprotocol/sdk/server/index.js';
import {
  createMcpObservabilityEasy,
  createTelemetryRouter,
} from '@listo-ai/mcp-observability';
import express from 'express';

const app = express();
const observability = createMcpObservabilityEasy({
  serviceName: 'my-mcp-server',
  serviceVersion: '0.1.0',
});

app.use('/telemetry', createTelemetryRouter(express));

const server = new Server({
  name: 'my-mcp-server',
  version: '0.1.0',
});

server.setRequestHandler(
  CallToolRequestSchema,
  observability.wrapMcpHandler(
    'CallTool',
    async (request) => {
      // Your tool implementation
    },
    (request) => ({ toolName: request.params.name })
  )
);

Event Types

The SDK captures five event types:

HTTP Request Events

Automatically captured via expressTelemetry() middleware.

{
  type: 'http_request',
  method: 'GET',
  path: '/products',
  route: '/products/:id',
  statusCode: 200,
  status: 'ok',
  latencyMs: 45.2,
  tenantId: 'tenant-123',
}

MCP Request Events

Captured via wrapMcpHandler().

{
  type: 'mcp_request',
  requestKind: 'CallTool',
  toolName: 'search_products',
  status: 'ok',
  latencyMs: 234.5,
  inputBytes: 156,
  outputBytes: 2048,
}

MCP Session Events

Recorded via recordSession(). Always captured regardless of sample rate.

{
  type: 'mcp_session',
  action: 'open',  // or 'close'
  sessionId: 'session-123',
}

Business Events

Custom domain events via recordBusinessEvent().

{
  type: 'business_event',
  name: 'product_search',
  status: 'ok',
  category: 'engagement',
  sessionId: 'sess-abc',
  tenantId: 'tenant-1',
  properties: { q: 'premium', results: 42 },
}

UI Events

Browser interaction events via recordUiEvent() or the POST /telemetry/event endpoint.

{
  type: 'ui_event',
  name: 'cta_click',
  action: 'click',
  category: 'engagement',
  sessionId: 'session-123',
  properties: { itemId: 'i-456' },
}

Environment Variables

| Variable | Required | Description | | --- | --- | --- | | LISTO_API_URL | For remote | Listo API endpoint | | LISTO_API_KEY | For remote | API key for authentication | | NODE_ENV | No | development / staging / production -- controls defaults | | TELEMETRY_SAMPLE_RATE | No | Override sampling percentage (0.0 - 1.0) | | TELEMETRY_CAPTURE_PAYLOADS | No | Enable/disable payload capture |

Security

Payload Sanitization

The SDK automatically redacts sensitive keys from all captured payloads. Redaction is applied recursively up to 6 levels deep.

Default redacted keys: password, token, apiKey, secret, authorization, userId, userLocation, email, userEmail, phone, phoneNumber

Custom redaction:

const observability = createMcpObservability({
  serviceName: 'my-api',
  redactKeys: ['password', 'token', 'creditCard', 'ssn'],
});

Sampling

  • Errors are always captured regardless of sample rate
  • Session events are always captured regardless of sample rate
  • All other events are sampled at the configured rate (default: 100% dev, 10% prod)

Content Security Policy

The HTML dashboard sets a Content-Security-Policy header restricting resource loading to inline styles and scripts only. No external resources are loaded.

Data Minimization

The SDK collects no PII fields. There are no userId, userLocation, or similar fields in any event type. User search text (userQuery) is truncated to 240 characters and SHA-256 hashed. Error messages are captured verbatim -- ensure your errors do not contain sensitive data.

const observability = createMcpObservabilityEasy({
  serviceName: 'my-api',
  sampleRate: 0.01, // 1% sampling
});

Documentation

Full Amplitude-style documentation is available in the docs/ directory:

| Page | Description | | --- | --- | | Overview | Architecture, quick start | | Installation | npm install, prerequisites, ESM-only note | | Configuration | Both init methods, full config tables | | Track HTTP | expressTelemetry() + trackHttpRequest() | | Wrap MCP Handlers | wrapMcpHandler(), context fields | | Sessions | recordSession(), always-capture behavior | | Business Events | recordBusinessEvent() | | UI Events | recordUiEvent() + POST endpoint | | Browser Client | TelemetryClient for browser apps | | Sinks | InMemorySink, RemoteSink, ConsoleSink, combineSinks | | Endpoints | Dashboard, JSON API, event ingestion | | Easy Setup | createMcpObservabilityEasy() deep-dive | | Sampling & Privacy | Sampling, redaction, data minimization | | Advanced | Custom sinks, metrics API, roadmap | | Troubleshooting | Common issues, debug mode |

Development

Prerequisites

  • Node.js >= 20
  • npm >= 10

Setup

git clone [email protected]:Listo-Labs-Ltd/mcp-observability.git
cd mcp-observability
npm install

Scripts

| Command | Description | | --- | --- | | npm run build | Compile TypeScript to dist/ | | npm run lint | Run ESLint | | npm run lint:fix | Run ESLint with auto-fix | | npm run format | Format source files with Prettier | | npm run format:check | Check formatting without writing | | npm test | Run tests with Vitest | | npm run test:watch | Run tests in watch mode | | npm run test:coverage | Run tests with coverage report | | npm run clean | Remove dist/ directory |

Making Changes

  1. Create a feature branch from main:

    git checkout -b feat/my-feature
  2. Make your changes in src/.

  3. Ensure code quality:

    npm run lint
    npm run format:check
    npm test
    npm run build
  4. Open a pull request against main. CI will automatically run lint, test, and build checks.

Project Structure

mcp-observability/
├── .github/
│   └── workflows/
│       ├── ci.yml          # Lint, test, build on push/PR
│       └── publish.yml     # Publish to npm on release
├── docs/                  # Comprehensive SDK documentation
├── src/
│   ├── index.ts            # Core SDK: types, classes, metrics
│   ├── client.ts           # Browser telemetry client
│   ├── endpoints.ts        # Express router and middleware
│   ├── easy-setup.ts       # Simplified configuration factory
│   └── remote-sink.ts      # Remote event batching and transmission
├── package.json
├── tsconfig.json
├── eslint.config.js
├── vitest.config.ts
├── .prettierrc
├── .npmrc                  # npm registry config
└── .gitignore

Source Files

| File | Description | | --- | --- | | src/index.ts | Core SDK -- event types, McpObservability class, InMemorySink, sampling, sanitization | | src/client.ts | Browser telemetry client -- TelemetryClient class with batching, session management, sendBeacon fallback | | src/endpoints.ts | expressTelemetry() middleware and createTelemetryRouter(express) for JSON metrics, HTML dashboard, and event ingestion | | src/easy-setup.ts | createMcpObservabilityEasy() -- auto-configures sinks and sampling based on environment | | src/remote-sink.ts | RemoteSink class -- batches events and sends to remote API with retry logic |

CI/CD and Publishing

Continuous Integration

Every push to main and every pull request triggers the CI workflow (.github/workflows/ci.yml):

  1. Lint -- Checks code formatting (Prettier) and linting rules (ESLint)
  2. Test -- Runs the test suite on Node.js 20 and 22
  3. Build -- Compiles TypeScript and verifies dist/ output

Publishing a New Version

The package is automatically published to npm when a push to main contains a new version in package.json. The publish workflow (.github/workflows/publish.yml) runs the full lint/test/build pipeline, then:

  1. Reads the version from package.json
  2. Checks if a v<version> git tag already exists
  3. If the version is new: creates the git tag, publishes to npm, and creates a GitHub Release with auto-generated notes
  4. If the version already exists: skips publish (the CI checks still run)

To release a new version:

  1. Bump the version in package.json:

    npm version patch   # or minor, major
  2. Push to main (or merge a PR):

    git push origin main

The tag, npm publish, and GitHub Release are all handled automatically by CI.

Troubleshooting

Local dashboard not showing

  • Ensure enableLocalDashboard: true is set (default in dev)
  • Visit http://localhost:PORT/telemetry/dashboard
  • Confirm createTelemetryRouter(express) is mounted: app.use('/telemetry', createTelemetryRouter(express))

Data not reaching remote endpoint

  • Verify LISTO_API_KEY is set and valid
  • Check LISTO_API_URL is correct
  • Look for warnings in server logs

Performance overhead

  • Use sampling: sampleRate: 0.1 (10% of events)
  • Disable payload capture: capturePayloads: false
  • Increase batch size for remote sink: batchSize: 100

Build errors after cloning

  • Ensure Node.js >= 20 is installed
  • Run npm install to install all dependencies
  • Run npm run build to compile TypeScript

License

MIT