@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,26 @@
1
+ import type { EventSink, ObservabilityEvent } from './index.js';
2
+ export type RemoteSinkOptions = {
3
+ endpoint: string;
4
+ apiKey: string;
5
+ batchSize?: number;
6
+ flushIntervalMs?: number;
7
+ maxRetries?: number;
8
+ maxBufferSize?: number;
9
+ debug?: boolean;
10
+ onError?: (error: Error, events: ObservabilityEvent[]) => void;
11
+ };
12
+ export declare class RemoteSink implements EventSink {
13
+ private buffer;
14
+ private readonly options;
15
+ private flushTimer;
16
+ private isFlushing;
17
+ constructor(options: RemoteSinkOptions);
18
+ emit(event: ObservabilityEvent): void;
19
+ flush(): Promise<void>;
20
+ private sendBatch;
21
+ private delay;
22
+ private startFlushTimer;
23
+ destroy(): void;
24
+ }
25
+ export declare function createRemoteSink(options: RemoteSinkOptions): EventSink;
26
+ //# sourceMappingURL=remote-sink.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"remote-sink.d.ts","sourceRoot":"","sources":["../src/remote-sink.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAShE,MAAM,MAAM,iBAAiB,GAAG;IAC9B,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,kBAAkB,EAAE,KAAK,IAAI,CAAC;CAChE,CAAC;AAEF,qBAAa,UAAW,YAAW,SAAS;IAC1C,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA8B;IACtD,OAAO,CAAC,UAAU,CAA+C;IACjE,OAAO,CAAC,UAAU,CAAS;gBAEf,OAAO,EAAE,iBAAiB;IAatC,IAAI,CAAC,KAAK,EAAE,kBAAkB,GAAG,IAAI;IAgB/B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;YAed,SAAS;IA2DvB,OAAO,CAAC,KAAK;IAIb,OAAO,CAAC,eAAe;IAMvB,OAAO,IAAI,IAAI;CAOhB;AAED,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,iBAAiB,GAAG,SAAS,CAEtE"}
@@ -0,0 +1,123 @@
1
+ class ClientError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'ClientError';
5
+ }
6
+ }
7
+ export class RemoteSink {
8
+ buffer = [];
9
+ options;
10
+ flushTimer = null;
11
+ isFlushing = false;
12
+ constructor(options) {
13
+ this.options = {
14
+ batchSize: 50,
15
+ flushIntervalMs: 5000,
16
+ maxRetries: 3,
17
+ maxBufferSize: 10_000,
18
+ debug: false,
19
+ onError: (err) => console.error('[RemoteSink] Error:', err.message),
20
+ ...options,
21
+ };
22
+ this.startFlushTimer();
23
+ }
24
+ emit(event) {
25
+ if (this.options.debug) {
26
+ console.log(`[RemoteSink] Buffering event: type=${event.type}${'name' in event ? ` name=${event.name}` : ''}`);
27
+ }
28
+ this.buffer.push(event);
29
+ // Drop oldest events if buffer exceeds max size
30
+ while (this.buffer.length > this.options.maxBufferSize) {
31
+ this.buffer.shift();
32
+ }
33
+ if (this.buffer.length >= this.options.batchSize) {
34
+ this.flush().catch(() => { });
35
+ }
36
+ }
37
+ async flush() {
38
+ if (this.buffer.length === 0 || this.isFlushing)
39
+ return;
40
+ this.isFlushing = true;
41
+ const events = this.buffer.splice(0, this.options.batchSize);
42
+ try {
43
+ await this.sendBatch(events);
44
+ }
45
+ catch (error) {
46
+ this.options.onError(error, events);
47
+ }
48
+ finally {
49
+ this.isFlushing = false;
50
+ }
51
+ }
52
+ async sendBatch(events) {
53
+ // Transform events for API compatibility:
54
+ // - timestamp: Unix ms (number) -> ISO string
55
+ // - latencyMs: float -> integer
56
+ const transformedEvents = events.map((event) => ({
57
+ ...event,
58
+ timestamp: new Date(event.timestamp).toISOString(),
59
+ // Round latencyMs to integer if present
60
+ ...('latencyMs' in event && typeof event.latencyMs === 'number'
61
+ ? { latencyMs: Math.round(event.latencyMs) }
62
+ : {}),
63
+ }));
64
+ if (this.options.debug) {
65
+ const eventTypes = transformedEvents.map((e) => e.type);
66
+ console.log(`[RemoteSink] Sending batch of ${transformedEvents.length} events to ${this.options.endpoint}: types=${[...new Set(eventTypes)].join(',')}`);
67
+ }
68
+ for (let attempt = 0; attempt < this.options.maxRetries; attempt++) {
69
+ try {
70
+ const response = await fetch(this.options.endpoint, {
71
+ method: 'POST',
72
+ headers: {
73
+ 'Content-Type': 'application/json',
74
+ 'X-API-Key': this.options.apiKey,
75
+ },
76
+ body: JSON.stringify({ events: transformedEvents }),
77
+ });
78
+ if (response.ok) {
79
+ if (this.options.debug) {
80
+ console.log(`[RemoteSink] Batch sent successfully`);
81
+ }
82
+ return;
83
+ }
84
+ // Do not retry client errors — they won't succeed on retry
85
+ if (response.status >= 400 && response.status < 500) {
86
+ const body = await response.text().catch(() => '');
87
+ throw new ClientError(`Client error ${response.status}: ${body}`);
88
+ }
89
+ if (attempt < this.options.maxRetries - 1) {
90
+ await this.delay(Math.pow(2, attempt) * 1000);
91
+ }
92
+ }
93
+ catch (error) {
94
+ if (error instanceof ClientError) {
95
+ throw error;
96
+ }
97
+ if (attempt === this.options.maxRetries - 1) {
98
+ throw error;
99
+ }
100
+ await this.delay(Math.pow(2, attempt) * 1000);
101
+ }
102
+ }
103
+ }
104
+ delay(ms) {
105
+ return new Promise((resolve) => setTimeout(resolve, ms));
106
+ }
107
+ startFlushTimer() {
108
+ this.flushTimer = setInterval(() => {
109
+ this.flush().catch(() => { });
110
+ }, this.options.flushIntervalMs);
111
+ }
112
+ destroy() {
113
+ if (this.flushTimer) {
114
+ clearInterval(this.flushTimer);
115
+ this.flushTimer = null;
116
+ }
117
+ this.flush().catch(() => { });
118
+ }
119
+ }
120
+ export function createRemoteSink(options) {
121
+ return new RemoteSink(options);
122
+ }
123
+ //# sourceMappingURL=remote-sink.js.map
package/package.json ADDED
@@ -0,0 +1,63 @@
1
+ {
2
+ "name": "@listo-ai/mcp-observability",
3
+ "version": "0.2.0",
4
+ "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.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist/**/*.js",
16
+ "dist/**/*.d.ts",
17
+ "dist/**/*.d.ts.map"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "lint": "eslint .",
22
+ "lint:fix": "eslint . --fix",
23
+ "format": "prettier --write \"src/**/*.ts\"",
24
+ "format:check": "prettier --check \"src/**/*.ts\"",
25
+ "test": "vitest run",
26
+ "test:watch": "vitest",
27
+ "test:coverage": "vitest run --coverage",
28
+ "clean": "rm -rf dist",
29
+ "prepublishOnly": "npm run clean && npm run build"
30
+ },
31
+ "repository": {
32
+ "type": "git",
33
+ "url": "https://github.com/Listo-Labs-Ltd/mcp-observability.git"
34
+ },
35
+ "publishConfig": {
36
+ "registry": "https://registry.npmjs.org/",
37
+ "access": "public"
38
+ },
39
+ "keywords": [
40
+ "mcp",
41
+ "observability",
42
+ "telemetry",
43
+ "model-context-protocol",
44
+ "analytics",
45
+ "monitoring"
46
+ ],
47
+ "engines": {
48
+ "node": ">=20"
49
+ },
50
+ "license": "MIT",
51
+ "dependencies": {},
52
+ "devDependencies": {
53
+ "@types/express": "^4.17.21",
54
+ "@types/node": "^22.10.0",
55
+ "eslint": "^9.17.0",
56
+ "eslint-config-prettier": "^10.0.0",
57
+ "eslint-plugin-prettier": "^5.2.0",
58
+ "prettier": "^3.4.0",
59
+ "typescript": "^5.7.0",
60
+ "typescript-eslint": "^8.18.0",
61
+ "vitest": "^3.0.0"
62
+ }
63
+ }