@listo-ai/mcp-observability 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,75 @@
1
+ import { createMcpObservability, InMemorySink, createConsoleSink, } from './index.js';
2
+ import { createRemoteSink } from './remote-sink.js';
3
+ import { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal, } from './endpoints.js';
4
+ /**
5
+ * Create an observability instance with environment-based defaults.
6
+ *
7
+ * This is the easiest way to get started with Listo Insights.
8
+ * It automatically:
9
+ * - Configures RemoteSink if INSIGHTS_API_KEY is provided
10
+ * - Configures InMemorySink + local dashboard in development
11
+ * - Applies sensible defaults (sampling, redaction, etc.)
12
+ * - Auto-detects tenant/user from common headers
13
+ *
14
+ * @example
15
+ * ```typescript
16
+ * const observability = createMcpObservabilityEasy({
17
+ * serviceName: "my-mcp-server",
18
+ * serviceVersion: "1.0.0",
19
+ * });
20
+ * ```
21
+ *
22
+ * Environment variables used:
23
+ * - INSIGHTS_API_URL: Insights API endpoint (default: https://api.listoai.co)
24
+ * - INSIGHTS_API_KEY: API key for authentication (generated in dashboard)
25
+ * - NODE_ENV: Determines if dev/production mode
26
+ */
27
+ export function createMcpObservabilityEasy(config) {
28
+ const { serviceName, serviceVersion = '1.0.0', environment = process.env.NODE_ENV === 'production' ? 'production' : 'dev', insightsApiUrl = process.env.INSIGHTS_API_URL, insightsApiKey = process.env.INSIGHTS_API_KEY, enableLocalDashboard = environment === 'dev', sampleRate = environment === 'production' ? 0.1 : 1.0, } = config;
29
+ const sinks = [];
30
+ // Console logging in dev only
31
+ if (environment === 'dev') {
32
+ sinks.push(createConsoleSink({ logSuccess: false }));
33
+ }
34
+ // Local in-memory dashboard for development
35
+ if (enableLocalDashboard) {
36
+ const telemetrySink = new InMemorySink({ limit: 2000 });
37
+ sinks.push(telemetrySink);
38
+ // Export globally for telemetry endpoints to access
39
+ setGlobal(TELEMETRY_SINK_KEY, telemetrySink);
40
+ }
41
+ // Remote Listo Insights sink
42
+ if (insightsApiUrl && insightsApiKey) {
43
+ sinks.push(createRemoteSink({
44
+ endpoint: `${insightsApiUrl}/v1/events/batch`,
45
+ apiKey: insightsApiKey,
46
+ batchSize: 50,
47
+ flushIntervalMs: 5000,
48
+ maxRetries: 3,
49
+ }));
50
+ }
51
+ else if (environment !== 'dev') {
52
+ console.warn('[Listo Insights] No API key configured. ' +
53
+ 'Set INSIGHTS_API_KEY environment variable to enable remote telemetry. ' +
54
+ 'Generate a key at: https://app.listoai.co/settings');
55
+ }
56
+ // Create the observability instance
57
+ const options = {
58
+ serviceName,
59
+ serviceVersion,
60
+ environment,
61
+ sampleRate,
62
+ capturePayloads: true,
63
+ redactKeys: ['password', 'token', 'apiKey', 'secret', 'authorization'],
64
+ sinks: sinks.length > 0 ? sinks : [createConsoleSink({ logSuccess: false })],
65
+ // Auto-detect tenant from common headers
66
+ tenantResolver: (req) => req.headers['x-tenant-id'] ||
67
+ req.headers['x-tenant-slug'] ||
68
+ undefined,
69
+ };
70
+ const observability = createMcpObservability(options);
71
+ // Export globally for telemetry router's POST /event endpoint
72
+ setGlobal(OBSERVABILITY_KEY, observability);
73
+ return observability;
74
+ }
75
+ //# sourceMappingURL=easy-setup.js.map
@@ -0,0 +1,34 @@
1
+ import type { Request, Response, NextFunction } from 'express';
2
+ import type { McpObservability } from './index.js';
3
+ declare const TELEMETRY_SINK_KEY: unique symbol;
4
+ declare const OBSERVABILITY_KEY: unique symbol;
5
+ declare function setGlobal<T>(key: symbol, value: T): void;
6
+ /**
7
+ * Express middleware for automatic HTTP request tracking
8
+ *
9
+ * @example
10
+ * ```typescript
11
+ * import { expressTelemetry } from '@listo-ai/mcp-observability';
12
+ *
13
+ * app.use(expressTelemetry(observability));
14
+ * ```
15
+ */
16
+ export declare function expressTelemetry(observability: McpObservability): (req: Request, res: Response, next: NextFunction) => Promise<void>;
17
+ /**
18
+ * Express router for telemetry endpoints
19
+ *
20
+ * Provides:
21
+ * - GET /telemetry - JSON metrics
22
+ * - GET /telemetry/dashboard - HTML dashboard
23
+ * - POST /telemetry/event - UI event ingestion
24
+ *
25
+ * @example
26
+ * ```typescript
27
+ * import { createTelemetryRouter } from '@listo-ai/mcp-observability';
28
+ *
29
+ * app.use('/telemetry', createTelemetryRouter());
30
+ * ```
31
+ */
32
+ export declare function createTelemetryRouter(): any;
33
+ export { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal };
34
+ //# sourceMappingURL=endpoints.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"endpoints.d.ts","sourceRoot":"","sources":["../src/endpoints.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,gBAAgB,EAAgB,MAAM,YAAY,CAAC;AAEjE,QAAA,MAAM,kBAAkB,eAAoC,CAAC;AAC7D,QAAA,MAAM,iBAAiB,eAAoC,CAAC;AAe5D,iBAAS,SAAS,CAAC,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,CAAC,GAAG,IAAI,CAEjD;AA4BD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,aAAa,EAAE,gBAAgB,IAChD,KAAK,OAAO,EAAE,KAAK,QAAQ,EAAE,MAAM,YAAY,mBAW9D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qBAAqB,QAqQpC;AAED,OAAO,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,SAAS,EAAE,CAAC"}
@@ -0,0 +1,307 @@
1
+ const TELEMETRY_SINK_KEY = Symbol.for('listo.telemetrySink');
2
+ const OBSERVABILITY_KEY = Symbol.for('listo.observability');
3
+ function escapeHtml(str) {
4
+ return str
5
+ .replace(/&/g, '&amp;')
6
+ .replace(/</g, '&lt;')
7
+ .replace(/>/g, '&gt;')
8
+ .replace(/"/g, '&quot;')
9
+ .replace(/'/g, '&#39;');
10
+ }
11
+ function getGlobal(key) {
12
+ return globalThis[key];
13
+ }
14
+ function setGlobal(key, value) {
15
+ globalThis[key] = value;
16
+ }
17
+ const UI_EVENT_FIELDS = new Set([
18
+ 'name',
19
+ 'action',
20
+ 'widgetId',
21
+ 'toolName',
22
+ 'sessionId',
23
+ 'tenantId',
24
+ 'locale',
25
+ 'properties',
26
+ ]);
27
+ const MAX_EVENT_BODY_SIZE = 8192;
28
+ function validateUiEventBody(body) {
29
+ if (!body || typeof body !== 'object' || Array.isArray(body))
30
+ return null;
31
+ const raw = body;
32
+ if (typeof raw.name !== 'string' || raw.name.length === 0)
33
+ return null;
34
+ const cleaned = {};
35
+ for (const key of UI_EVENT_FIELDS) {
36
+ if (key in raw) {
37
+ cleaned[key] = raw[key];
38
+ }
39
+ }
40
+ return cleaned;
41
+ }
42
+ /**
43
+ * Express middleware for automatic HTTP request tracking
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * import { expressTelemetry } from '@listo-ai/mcp-observability';
48
+ *
49
+ * app.use(expressTelemetry(observability));
50
+ * ```
51
+ */
52
+ export function expressTelemetry(observability) {
53
+ return async (req, res, next) => {
54
+ const route = req.route?.path || req.path;
55
+ try {
56
+ await observability.trackHttpRequest(req, res, async (tracking) => {
57
+ tracking.setRoute(route);
58
+ next();
59
+ });
60
+ }
61
+ catch (error) {
62
+ next(error);
63
+ }
64
+ };
65
+ }
66
+ /**
67
+ * Express router for telemetry endpoints
68
+ *
69
+ * Provides:
70
+ * - GET /telemetry - JSON metrics
71
+ * - GET /telemetry/dashboard - HTML dashboard
72
+ * - POST /telemetry/event - UI event ingestion
73
+ *
74
+ * @example
75
+ * ```typescript
76
+ * import { createTelemetryRouter } from '@listo-ai/mcp-observability';
77
+ *
78
+ * app.use('/telemetry', createTelemetryRouter());
79
+ * ```
80
+ */
81
+ export function createTelemetryRouter() {
82
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
83
+ const express = require('express');
84
+ const router = express.Router();
85
+ const getTelemetrySink = () => {
86
+ return getGlobal(TELEMETRY_SINK_KEY);
87
+ };
88
+ // JSON metrics endpoint
89
+ router.get('/', (req, res) => {
90
+ const sink = getTelemetrySink();
91
+ if (!sink) {
92
+ return res.status(404).json({
93
+ error: 'Local telemetry not enabled',
94
+ hint: 'Set enableLocalDashboard: true in createMcpObservabilityEasy config',
95
+ });
96
+ }
97
+ res.json({
98
+ events: sink.getEvents(),
99
+ metrics: sink.getMetrics(),
100
+ });
101
+ });
102
+ // HTML dashboard endpoint
103
+ router.get('/dashboard', (req, res) => {
104
+ const sink = getTelemetrySink();
105
+ if (!sink) {
106
+ return res
107
+ .status(404)
108
+ .type('text/html')
109
+ .set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'").send(`
110
+ <!DOCTYPE html>
111
+ <html>
112
+ <head>
113
+ <title>Listo Telemetry</title>
114
+ <style>
115
+ body { font-family: system-ui, sans-serif; padding: 20px; }
116
+ .error { color: #d32f2f; }
117
+ </style>
118
+ </head>
119
+ <body>
120
+ <h1>Listo Telemetry Dashboard</h1>
121
+ <p class="error">Local telemetry is not enabled.</p>
122
+ <p>To enable, set enableLocalDashboard: true in createMcpObservabilityEasy config.</p>
123
+ </body>
124
+ </html>
125
+ `);
126
+ }
127
+ const metrics = sink.getMetrics();
128
+ const html = `
129
+ <!DOCTYPE html>
130
+ <html>
131
+ <head>
132
+ <meta charset="UTF-8">
133
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
134
+ <title>Listo Telemetry Dashboard</title>
135
+ <style>
136
+ * { margin: 0; padding: 0; box-sizing: border-box; }
137
+ body {
138
+ font-family: system-ui, -apple-system, sans-serif;
139
+ background: #f5f5f5;
140
+ color: #333;
141
+ }
142
+ .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
143
+ h1 { font-size: 28px; margin-bottom: 20px; }
144
+ h2 { font-size: 18px; margin-top: 30px; margin-bottom: 10px; }
145
+ .grid {
146
+ display: grid;
147
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
148
+ gap: 15px;
149
+ margin-bottom: 20px;
150
+ }
151
+ .card {
152
+ background: white;
153
+ border-radius: 8px;
154
+ padding: 20px;
155
+ box-shadow: 0 1px 3px rgba(0,0,0,0.1);
156
+ }
157
+ .metric-value { font-size: 32px; font-weight: bold; margin-top: 10px; }
158
+ .metric-label { font-size: 12px; color: #666; text-transform: uppercase; }
159
+ table {
160
+ width: 100%;
161
+ border-collapse: collapse;
162
+ background: white;
163
+ border-radius: 8px;
164
+ overflow: hidden;
165
+ }
166
+ th, td { padding: 12px; text-align: left; }
167
+ th { background: #f5f5f5; font-weight: 600; }
168
+ tr:hover { background: #fafafa; }
169
+ .refresh { margin-bottom: 20px; }
170
+ button {
171
+ padding: 10px 15px;
172
+ background: #1976d2;
173
+ color: white;
174
+ border: none;
175
+ border-radius: 4px;
176
+ cursor: pointer;
177
+ font-size: 14px;
178
+ }
179
+ button:hover { background: #1565c0; }
180
+ </style>
181
+ </head>
182
+ <body>
183
+ <div class="container">
184
+ <div class="refresh">
185
+ <button onclick="location.reload()">⟲ Refresh</button>
186
+ <small style="margin-left: 10px; color: #666;">Auto-refreshes every 5 seconds</small>
187
+ </div>
188
+
189
+ <h1>📊 Listo Telemetry Dashboard</h1>
190
+
191
+ <div class="grid">
192
+ <div class="card">
193
+ <div class="metric-label">Total Events</div>
194
+ <div class="metric-value">${metrics.totals.events}</div>
195
+ </div>
196
+ <div class="card">
197
+ <div class="metric-label">HTTP Requests</div>
198
+ <div class="metric-value">${metrics.totals.http}</div>
199
+ </div>
200
+ <div class="card">
201
+ <div class="metric-label">MCP Requests</div>
202
+ <div class="metric-value">${metrics.totals.mcp}</div>
203
+ </div>
204
+ <div class="card">
205
+ <div class="metric-label">Sessions</div>
206
+ <div class="metric-value">${metrics.totals.sessions}</div>
207
+ </div>
208
+ </div>
209
+
210
+ ${metrics.http.overall.count > 0
211
+ ? `
212
+ <h2>HTTP Performance</h2>
213
+ <table>
214
+ <tr>
215
+ <th>Route</th>
216
+ <th>Requests</th>
217
+ <th>Errors</th>
218
+ <th>Avg (ms)</th>
219
+ <th>P95 (ms)</th>
220
+ <th>P99 (ms)</th>
221
+ </tr>
222
+ ${Object.entries(metrics.http.byRoute)
223
+ .map(([route, stats]) => {
224
+ const s = stats;
225
+ return `
226
+ <tr>
227
+ <td><code>${escapeHtml(route)}</code></td>
228
+ <td>${s.count}</td>
229
+ <td style="color: ${s.errors > 0 ? '#d32f2f' : '#4caf50'}">${s.errors}</td>
230
+ <td>${(s.avgMs || 0).toFixed(1)}</td>
231
+ <td>${(s.p95 || 0).toFixed(1)}</td>
232
+ <td>${(s.p99 || 0).toFixed(1)}</td>
233
+ </tr>
234
+ `;
235
+ })
236
+ .join('')}
237
+ </table>
238
+ `
239
+ : ''}
240
+
241
+ ${metrics.mcp.tools && Object.keys(metrics.mcp.tools).length > 0
242
+ ? `
243
+ <h2>MCP Tools</h2>
244
+ <table>
245
+ <tr>
246
+ <th>Tool</th>
247
+ <th>Calls</th>
248
+ <th>Errors</th>
249
+ <th>Avg (ms)</th>
250
+ <th>P95 (ms)</th>
251
+ <th>P99 (ms)</th>
252
+ </tr>
253
+ ${Object.entries(metrics.mcp.tools)
254
+ .map(([tool, stats]) => {
255
+ const s = stats;
256
+ return `
257
+ <tr>
258
+ <td><code>${escapeHtml(tool)}</code></td>
259
+ <td>${s.count}</td>
260
+ <td style="color: ${s.errors > 0 ? '#d32f2f' : '#4caf50'}">${s.errors}</td>
261
+ <td>${(s.avgMs || 0).toFixed(1)}</td>
262
+ <td>${(s.p95 || 0).toFixed(1)}</td>
263
+ <td>${(s.p99 || 0).toFixed(1)}</td>
264
+ </tr>
265
+ `;
266
+ })
267
+ .join('')}
268
+ </table>
269
+ `
270
+ : ''}
271
+
272
+ <p style="margin-top: 40px; color: #999; font-size: 12px;">
273
+ Last updated: ${new Date().toLocaleTimeString()} |
274
+ View in <a href="https://app.listoai.co" target="_blank" style="color: #1976d2;">Listo</a>
275
+ </p>
276
+ </div>
277
+
278
+ <script>
279
+ setTimeout(() => location.reload(), 5000);
280
+ </script>
281
+ </body>
282
+ </html>
283
+ `;
284
+ res
285
+ .type('text/html')
286
+ .set('Content-Security-Policy', "default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'")
287
+ .send(html);
288
+ });
289
+ // UI event ingestion endpoint
290
+ router.post('/event', express.json({ limit: MAX_EVENT_BODY_SIZE }), (req, res) => {
291
+ const validated = validateUiEventBody(req.body);
292
+ if (!validated) {
293
+ res
294
+ .status(400)
295
+ .json({ error: 'Invalid event: "name" string required' });
296
+ return;
297
+ }
298
+ const observability = getGlobal(OBSERVABILITY_KEY);
299
+ if (observability) {
300
+ observability.recordUiEvent(validated);
301
+ }
302
+ res.status(201).json({ ok: true });
303
+ });
304
+ return router;
305
+ }
306
+ export { TELEMETRY_SINK_KEY, OBSERVABILITY_KEY, setGlobal };
307
+ //# sourceMappingURL=endpoints.js.map