@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
TelemetryClientfor browser apps with batching andsendBeaconfallback - 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
- Quick Start
- Browser Client
- How It Works
- API Reference
- Integration Guide
- Event Types
- Environment Variables
- Security
- Documentation
- Development
- CI/CD and Publishing
- Troubleshooting
- License
Installation
npm install @listo-ai/mcp-observabilityQuick 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_key4. Access the local dashboard
http://localhost:3000/telemetry/dashboardBrowser 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 dashboardRemoteSink-- Batches events and sends them to a remote endpoint with exponential backoff retriesConsoleSink-- Logs errors to stdoutcombineSinks()-- 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/dashboardexpressTelemetry(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 installScripts
| 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
Create a feature branch from
main:git checkout -b feat/my-featureMake your changes in
src/.Ensure code quality:
npm run lint npm run format:check npm test npm run buildOpen 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
└── .gitignoreSource 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):
- Lint -- Checks code formatting (Prettier) and linting rules (ESLint)
- Test -- Runs the test suite on Node.js 20 and 22
- 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:
- Reads the version from
package.json - Checks if a
v<version>git tag already exists - If the version is new: creates the git tag, publishes to npm, and creates a GitHub Release with auto-generated notes
- If the version already exists: skips publish (the CI checks still run)
To release a new version:
Bump the version in
package.json:npm version patch # or minor, majorPush 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: trueis 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_KEYis set and valid - Check
LISTO_API_URLis 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 installto install all dependencies - Run
npm run buildto compile TypeScript
License
MIT
