@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.
- package/README.md +609 -0
- package/dist/easy-setup.d.ts +34 -0
- package/dist/easy-setup.d.ts.map +1 -0
- package/dist/easy-setup.js +75 -0
- package/dist/endpoints.d.ts +34 -0
- package/dist/endpoints.d.ts.map +1 -0
- package/dist/endpoints.js +307 -0
- package/dist/index.d.ts +305 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +790 -0
- package/dist/remote-sink.d.ts +26 -0
- package/dist/remote-sink.d.ts.map +1 -0
- package/dist/remote-sink.js +123 -0
- package/package.json +63 -0
|
@@ -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
|
+
}
|