npm package: @listo-ai/mcp-observability
Description: 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.
Latest version: 0.6.0

# @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)](https://modelcontextprotocol.io/) 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`)
- **Platform detection** -- Automatically identify the AI platform (Claude, ChatGPT, or other) from request headers and metadata
- **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](#installation)
- [Quick Start](#quick-start)
- [Browser Client](#browser-client)
- [How It Works](#how-it-works)
- [API Reference](#api-reference)
- [Platform Detection](#detectplatformhints)
- [Integration Guide](#integration-guide)
- [Event Types](#event-types)
- [Environment Variables](#environment-variables)
- [Security](#security)
- [Documentation](#documentation)
- [Development](#development)
- [CI/CD and Publishing](#cicd-and-publishing)
- [Troubleshooting](#troubleshooting)
- [License](#license)

## Installation

```bash
npm install @listo-ai/mcp-observability
```

## Quick Start

### 1. Initialize observability

```typescript
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

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

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

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

```bash
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:

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

const client = createTelemetryClient({
  endpoint: 'https://myapp.com/telemetry',
  userId: localStorage.getItem('user_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](./docs/client.md) 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.

```typescript
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
}
```

```typescript
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.

```typescript
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) |

```typescript
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.

```typescript
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.

```typescript
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.

```typescript
observability.recordBusinessEvent('product_search', {
  properties: { q: 'premium items', results: 42 },
  status: 'ok',
  category: 'conversion',
  sessionId: 'sess-abc',
  userId: 'user-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 |
| `userId` | `string?` | User identifier for cross-session identification |
| `tenantId` | `string?` | Tenant identifier for multi-tenant apps |

### `observability.recordUiEvent(event)`

Record UI interaction events from the browser.

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

### `detectPlatform(hints?)`

Identify the AI platform calling your MCP server. Returns `'claude'`, `'chatgpt'`, or `'other'`.

```typescript
import { detectPlatform } from '@listo-ai/mcp-observability';

const platform = detectPlatform({
  headers: request.headers,                // HTTP headers (user-agent, x-openai-*, x-anthropic-*)
  meta: mcpRequest.params?._meta,          // _meta key prefixes (openai/*, anthropic/*)
  clientInfo: mcpRequest.params?.clientInfo // clientInfo.name fallback
});
// → 'claude', 'chatgpt', or 'other'
```

Detection checks three signal tiers in order (first match wins): HTTP headers, `_meta` key prefixes, then `clientInfo.name`. See [Platform Detection docs](./docs/platform-detection.md) for details.

### `observability.recordSession(event)`

Record MCP session lifecycle events. Pass `platform` from `detectPlatform()` on session open to attribute the session to an AI platform.

```typescript
observability.recordSession({
  type: 'mcp_session',
  action: 'open',
  sessionId: 'session-123',
  platform: detectPlatform({ headers: request.headers }),
});
```

### `RemoteSink`

Standalone remote sink for custom configurations.

```typescript
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

```typescript
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

```typescript
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.

```typescript
{
  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()`.

```typescript
{
  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.

```typescript
{
  type: 'mcp_session',
  action: 'open',  // or 'close'
  sessionId: 'session-123',
  platform: 'claude',  // 'claude', 'chatgpt', or 'other'
}
```

### Business Events

Custom domain events via `recordBusinessEvent()`.

```typescript
{
  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.

```typescript
{
  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 |
| `HMAC_SECRET` | No | Secret key for HMAC-SHA-256 hashing of `sessionId`/`userId` in MCP events. Without it, falls back to SHA-256 |

## 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:**

```typescript
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.

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

## Documentation

Full Amplitude-style documentation is available in the [`docs/`](./docs/) directory:

| Page | Description |
| --- | --- |
| [Overview](./docs/index.md) | Architecture, quick start |
| [Installation](./docs/installation.md) | npm install, prerequisites, ESM-only note |
| [Configuration](./docs/configuration.md) | Both init methods, full config tables |
| [Track HTTP](./docs/track-http.md) | `expressTelemetry()` + `trackHttpRequest()` |
| [Wrap MCP Handlers](./docs/wrap-mcp-handlers.md) | `wrapMcpHandler()`, context fields |
| [Sessions](./docs/sessions.md) | `recordSession()`, always-capture behavior |
| [Platform Detection](./docs/platform-detection.md) | `detectPlatform()`, header/meta/clientInfo signals |
| [Business Events](./docs/business-events.md) | `recordBusinessEvent()` |
| [UI Events](./docs/ui-events.md) | `recordUiEvent()` + POST endpoint |
| [Browser Client](./docs/client.md) | `TelemetryClient` for browser apps |
| [Sinks](./docs/sinks.md) | InMemorySink, RemoteSink, ConsoleSink, combineSinks |
| [Endpoints](./docs/endpoints.md) | Dashboard, JSON API, event ingestion |
| [Easy Setup](./docs/easy-setup.md) | `createMcpObservabilityEasy()` deep-dive |
| [Sampling & Privacy](./docs/sampling-privacy.md) | Sampling, redaction, data minimization |
| [Advanced](./docs/advanced.md) | Custom sinks, metrics API, roadmap |
| [Troubleshooting](./docs/troubleshooting.md) | Common issues, debug mode |

## Development

### Prerequisites

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

### Setup

```bash
git clone git@github.com: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`:
   ```bash
   git checkout -b feat/my-feature
   ```

2. Make your changes in `src/`.

3. Ensure code quality:
   ```bash
   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](https://www.npmjs.com/package/@listo-ai/mcp-observability) 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`:
   ```bash
   npm version patch   # or minor, major
   ```

2. Push to `main` (or merge a PR):
   ```bash
   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