mcp-telemetry-server 0.1.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 ADDED
@@ -0,0 +1,33 @@
1
+ # mcp-telemetry-server
2
+
3
+ The collector half of [mcp-telemetry](https://github.com/arnavranjan005/mcp-telemetry). Register it as an MCP server to watch live progress from any tool instrumented with [`mcp-telemetry-sdk`](https://www.npmjs.com/package/mcp-telemetry-sdk).
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install -g mcp-telemetry-server
9
+ ```
10
+
11
+ ## Register it
12
+
13
+ ```json
14
+ {
15
+ "mcpServers": {
16
+ "telemetry": { "command": "npx", "args": ["-y", "mcp-telemetry-server"] }
17
+ }
18
+ }
19
+ ```
20
+
21
+ ## Tools it exposes
22
+
23
+ | Tool | Behavior |
24
+ |---|---|
25
+ | `telemetry_subscribe({ jobId?, timeoutMs? })` | Blocks and streams live progress for a job until it finishes. |
26
+ | `telemetry_jobs()` | Lists all known jobs with status and cost. |
27
+ | `telemetry_job_status({ jobId })` | Full state of one job — steps, cost, failure reason. |
28
+
29
+ See the [main README](https://github.com/arnavranjan005/mcp-telemetry#quickstart) for a full worked example.
30
+
31
+ ## License
32
+
33
+ MIT
package/bin/server.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ import '../dist/index.js';
@@ -0,0 +1,11 @@
1
+ import type { MonitorEvent } from 'mcp-telemetry-sdk';
2
+ export declare class Collector {
3
+ private readonly socketPath;
4
+ private readonly server;
5
+ private readonly handlers;
6
+ constructor(socketPath: string);
7
+ onEvent(handler: (event: MonitorEvent) => void): void;
8
+ offEvent(handler: (event: MonitorEvent) => void): void;
9
+ listen(): Promise<void>;
10
+ close(): void;
11
+ }
@@ -0,0 +1,54 @@
1
+ import net from 'net';
2
+ import { existsSync, unlinkSync } from 'fs';
3
+ export class Collector {
4
+ socketPath;
5
+ server;
6
+ handlers = [];
7
+ constructor(socketPath) {
8
+ this.socketPath = socketPath;
9
+ this.server = net.createServer((socket) => {
10
+ let buffer = '';
11
+ socket.on('data', (chunk) => {
12
+ buffer += chunk.toString();
13
+ const lines = buffer.split('\n');
14
+ buffer = lines.pop() ?? '';
15
+ for (const line of lines) {
16
+ if (!line.trim())
17
+ continue;
18
+ try {
19
+ const event = JSON.parse(line);
20
+ for (const h of this.handlers)
21
+ h(event);
22
+ }
23
+ catch { /* ignore malformed NDJSON */ }
24
+ }
25
+ });
26
+ });
27
+ }
28
+ onEvent(handler) {
29
+ this.handlers.push(handler);
30
+ }
31
+ offEvent(handler) {
32
+ const idx = this.handlers.indexOf(handler);
33
+ if (idx !== -1)
34
+ this.handlers.splice(idx, 1);
35
+ }
36
+ listen() {
37
+ return new Promise((resolve, reject) => {
38
+ if (process.platform !== 'win32' && existsSync(this.socketPath)) {
39
+ unlinkSync(this.socketPath);
40
+ }
41
+ this.server.listen(this.socketPath, () => resolve());
42
+ this.server.on('error', reject);
43
+ });
44
+ }
45
+ close() {
46
+ this.server.close();
47
+ if (process.platform !== 'win32' && existsSync(this.socketPath)) {
48
+ try {
49
+ unlinkSync(this.socketPath);
50
+ }
51
+ catch { /* best-effort */ }
52
+ }
53
+ }
54
+ }
@@ -0,0 +1,5 @@
1
+ import type { MonitorEvent } from 'mcp-telemetry-sdk';
2
+ import type { JobState } from './store.js';
3
+ export declare function formatMeta(meta: Record<string, unknown> | undefined): string;
4
+ export declare function formatEvent(event: MonitorEvent): string;
5
+ export declare function formatJob(job: JobState): string;
package/dist/format.js ADDED
@@ -0,0 +1,28 @@
1
+ export function formatMeta(meta) {
2
+ if (!meta || !Object.keys(meta).length)
3
+ return '';
4
+ return ` (${Object.entries(meta).map(([k, v]) => `${k}=${v}`).join(', ')})`;
5
+ }
6
+ export function formatEvent(event) {
7
+ switch (event.type) {
8
+ case 'job_start': return `▶ ${event.task}`;
9
+ case 'job_done': return `${event.exitCode === 0 ? '✓' : '✗'} job done (exit ${event.exitCode})`;
10
+ case 'step_start': return `↻ ${event.step}${formatMeta(event.meta)}`;
11
+ case 'step_done': return `✓ ${event.step}${formatMeta(event.meta)}`;
12
+ case 'step_failed': return `✗ ${event.step}${event.reason ? ` — ${event.reason}` : ''}`;
13
+ case 'log': return event.stream === 'stderr' ? `[stderr] ${event.line}` : event.line;
14
+ case 'cost': return `$${event.amount.toFixed(4)}${formatMeta(event.meta)}`;
15
+ }
16
+ }
17
+ export function formatJob(job) {
18
+ const ICONS = { done: '✓', failed: '✗', running: '↻' };
19
+ const steps = job.steps.length
20
+ ? job.steps.map((s) => ` ${ICONS[s.status] ?? '?'} ${s.name}${s.reason ? ` — ${s.reason}` : ''}`).join('\n')
21
+ : ' (no steps yet)';
22
+ return [
23
+ `[${job.jobId}] ${job.task}`,
24
+ `status: ${job.status} cost: $${job.totalCost.toFixed(4)}`,
25
+ '',
26
+ steps,
27
+ ].join('\n');
28
+ }
@@ -0,0 +1 @@
1
+ export {};
package/dist/index.js ADDED
@@ -0,0 +1,130 @@
1
+ import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
2
+ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
3
+ import { z } from 'zod';
4
+ import { getSocketPath } from 'mcp-telemetry-sdk';
5
+ import { Collector } from './collector.js';
6
+ import { JobStore } from './store.js';
7
+ import { formatEvent, formatJob } from './format.js';
8
+ const JOBS_RESOURCE_URI = 'telemetry://jobs';
9
+ const store = new JobStore();
10
+ const collector = new Collector(getSocketPath());
11
+ const server = new McpServer({ name: 'mcp-telemetry', version: '0.1.0' });
12
+ // ── Tools ─────────────────────────────────────────────────────────────────────
13
+ server.tool('telemetry_jobs', 'List all active and recently completed jobs across all connected MCP servers.', {}, async () => {
14
+ const jobs = store.getJobs();
15
+ if (!jobs.length)
16
+ return { content: [{ type: 'text', text: 'No jobs yet.' }] };
17
+ const lines = jobs.map((j) => {
18
+ const icon = { completed: '✓', failed: '✗', input_required: '!', working: '↻', cancelled: '⊘' }[j.status];
19
+ const cost = j.totalCost > 0 ? ` $${j.totalCost.toFixed(4)}` : '';
20
+ return `${icon} [${j.jobId}] ${j.task}${cost}`;
21
+ });
22
+ return { content: [{ type: 'text', text: lines.join('\n') }] };
23
+ });
24
+ server.tool('telemetry_job_status', 'Get the full status of a specific job — all steps, cost, and any failure reason.', { jobId: z.string().describe('Job ID returned by job_start event or telemetry_jobs.') }, async ({ jobId }) => {
25
+ const job = store.getJob(jobId);
26
+ if (!job)
27
+ return { content: [{ type: 'text', text: `No job found: ${jobId}` }], isError: true };
28
+ return { content: [{ type: 'text', text: formatJob(job) }] };
29
+ });
30
+ server.tool('telemetry_subscribe', 'Watch live job events as they happen — blocks and streams inline progress until the job (or, with no jobId, the next job) finishes, or until timeoutMs elapses.', {
31
+ jobId: z.string().optional().describe('Watch only this job. Omit to watch the next job to start.'),
32
+ timeoutMs: z.number().optional().describe('Give up and return after this many ms. Default 5 minutes.'),
33
+ }, async ({ jobId, timeoutMs }, extra) => {
34
+ const progressToken = extra._meta?.progressToken;
35
+ const deadline = timeoutMs ?? 5 * 60 * 1000;
36
+ const LOG_THROTTLE_MS = 1500;
37
+ const LOG_BATCH_MAX = 20;
38
+ return new Promise((resolve) => {
39
+ let n = 0;
40
+ let settled = false;
41
+ let pendingLogs = [];
42
+ let logTimer = null;
43
+ const push = (message) => {
44
+ n += 1;
45
+ if (progressToken !== undefined) {
46
+ extra.sendNotification({
47
+ method: 'notifications/progress',
48
+ params: { progressToken, progress: n, message },
49
+ }).catch(() => { });
50
+ }
51
+ };
52
+ // Raw log lines are usually too frequent to push one-by-one (a verbose
53
+ // run can emit hundreds) — coalesce them into one notification every
54
+ // LOG_THROTTLE_MS or LOG_BATCH_MAX lines, whichever comes first.
55
+ // Structured events (step/cost/done) always flush any pending logs
56
+ // first so ordering in the stream matches what actually happened, then
57
+ // push immediately themselves — they're infrequent enough not to need
58
+ // throttling.
59
+ const flushLogs = () => {
60
+ if (logTimer) {
61
+ clearTimeout(logTimer);
62
+ logTimer = null;
63
+ }
64
+ if (!pendingLogs.length)
65
+ return;
66
+ push(pendingLogs.join('\n'));
67
+ pendingLogs = [];
68
+ };
69
+ const finish = (text) => {
70
+ if (settled)
71
+ return;
72
+ settled = true;
73
+ clearTimeout(timer);
74
+ flushLogs();
75
+ collector.offEvent(handler);
76
+ resolve({ content: [{ type: 'text', text }] });
77
+ };
78
+ const handler = (event) => {
79
+ if (jobId && event.jobId !== jobId)
80
+ return;
81
+ if (event.type === 'log') {
82
+ pendingLogs.push(formatEvent(event));
83
+ if (pendingLogs.length >= LOG_BATCH_MAX) {
84
+ flushLogs();
85
+ return;
86
+ }
87
+ if (!logTimer)
88
+ logTimer = setTimeout(flushLogs, LOG_THROTTLE_MS);
89
+ return;
90
+ }
91
+ flushLogs();
92
+ push(formatEvent(event));
93
+ if (event.type === 'job_done') {
94
+ finish(`Job ${event.jobId} finished (exit ${event.exitCode}).`);
95
+ }
96
+ };
97
+ collector.onEvent(handler);
98
+ const timer = setTimeout(() => finish('Timed out waiting for job completion.'), deadline);
99
+ });
100
+ });
101
+ // ── Resource — auto-push via MCP resource subscriptions ───────────────────────
102
+ server.resource('telemetry-jobs', JOBS_RESOURCE_URI, { description: 'Live job state for all connected MCP servers. Subscribe to receive updates.' }, async () => ({
103
+ contents: [{
104
+ uri: JOBS_RESOURCE_URI,
105
+ mimeType: 'application/json',
106
+ text: JSON.stringify(store.getJobs(), null, 2),
107
+ }],
108
+ }));
109
+ // ── Collector → store → notify all subscribed agents ─────────────────────────
110
+ collector.onEvent((event) => {
111
+ store.apply(event);
112
+ // Push resource update notification — agents that subscribed to telemetry://jobs
113
+ // will be notified and can re-read the resource to get the latest state.
114
+ server.server.notification({
115
+ method: 'notifications/resources/updated',
116
+ params: { uri: JOBS_RESOURCE_URI },
117
+ }).catch(() => { });
118
+ });
119
+ const pruneTimer = setInterval(() => store.prune(), 5 * 60 * 1000);
120
+ pruneTimer.unref();
121
+ // ── Bootstrap ─────────────────────────────────────────────────────────────────
122
+ async function main() {
123
+ await collector.listen();
124
+ const transport = new StdioServerTransport();
125
+ await server.connect(transport);
126
+ const shutdown = () => { collector.close(); process.exit(0); };
127
+ process.on('SIGINT', shutdown);
128
+ process.on('SIGTERM', shutdown);
129
+ }
130
+ main().catch((err) => { console.error(err); process.exit(1); });
@@ -0,0 +1,27 @@
1
+ import type { MonitorEvent, TaskStatus } from 'mcp-telemetry-sdk';
2
+ export interface StepState {
3
+ name: string;
4
+ status: 'running' | 'done' | 'failed';
5
+ startedAt: string;
6
+ endedAt?: string;
7
+ meta?: Record<string, unknown>;
8
+ reason?: string;
9
+ }
10
+ export interface JobState {
11
+ jobId: string;
12
+ task: string;
13
+ status: TaskStatus;
14
+ steps: StepState[];
15
+ logs: string[];
16
+ totalCost: number;
17
+ startedAt: string;
18
+ endedAt?: string;
19
+ exitCode?: number;
20
+ }
21
+ export declare class JobStore {
22
+ private readonly jobs;
23
+ apply(event: MonitorEvent): void;
24
+ getJob(jobId: string): JobState | undefined;
25
+ getJobs(): JobState[];
26
+ prune(): void;
27
+ }
package/dist/store.js ADDED
@@ -0,0 +1,83 @@
1
+ const JOB_TTL_MS = 5 * 60 * 1000;
2
+ export class JobStore {
3
+ jobs = new Map();
4
+ apply(event) {
5
+ switch (event.type) {
6
+ case 'job_start':
7
+ this.jobs.set(event.jobId, {
8
+ jobId: event.jobId,
9
+ task: event.task,
10
+ status: 'working',
11
+ steps: [],
12
+ logs: [],
13
+ totalCost: 0,
14
+ startedAt: event.timestamp,
15
+ });
16
+ break;
17
+ case 'job_done': {
18
+ const job = this.jobs.get(event.jobId);
19
+ if (job) {
20
+ job.status = event.exitCode === 0 ? 'completed' : 'failed';
21
+ job.exitCode = event.exitCode;
22
+ job.endedAt = event.timestamp;
23
+ }
24
+ break;
25
+ }
26
+ case 'step_start': {
27
+ const job = this.jobs.get(event.jobId);
28
+ job?.steps.push({ name: event.step, status: 'running', startedAt: event.timestamp, meta: event.meta });
29
+ break;
30
+ }
31
+ case 'step_done': {
32
+ const job = this.jobs.get(event.jobId);
33
+ if (job) {
34
+ const step = [...job.steps].reverse().find(s => s.name === event.step && s.status === 'running');
35
+ if (step) {
36
+ step.status = 'done';
37
+ step.endedAt = event.timestamp;
38
+ }
39
+ }
40
+ break;
41
+ }
42
+ case 'step_failed': {
43
+ const job = this.jobs.get(event.jobId);
44
+ if (job) {
45
+ const step = [...job.steps].reverse().find(s => s.name === event.step && s.status === 'running');
46
+ if (step) {
47
+ step.status = 'failed';
48
+ step.endedAt = event.timestamp;
49
+ step.reason = event.reason;
50
+ }
51
+ if (event.reason === 'input_required')
52
+ job.status = 'input_required';
53
+ }
54
+ break;
55
+ }
56
+ case 'log': {
57
+ const job = this.jobs.get(event.jobId);
58
+ job?.logs.push(event.stream === 'stderr' ? `[stderr] ${event.line}` : event.line);
59
+ break;
60
+ }
61
+ case 'cost': {
62
+ const job = this.jobs.get(event.jobId);
63
+ if (job)
64
+ job.totalCost += event.amount;
65
+ break;
66
+ }
67
+ }
68
+ }
69
+ getJob(jobId) {
70
+ return this.jobs.get(jobId);
71
+ }
72
+ getJobs() {
73
+ return [...this.jobs.values()].sort((a, b) => a.startedAt.localeCompare(b.startedAt));
74
+ }
75
+ prune() {
76
+ const cutoff = Date.now() - JOB_TTL_MS;
77
+ for (const [id, job] of this.jobs) {
78
+ if (job.endedAt && new Date(job.endedAt).getTime() < cutoff) {
79
+ this.jobs.delete(id);
80
+ }
81
+ }
82
+ }
83
+ }
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "mcp-telemetry-server",
3
+ "version": "0.1.0",
4
+ "description": "MCP server that collects telemetry from any mcp-telemetry-sdk-instrumented tool and pushes live progress to any connected MCP client (Claude Code, Cursor, and others).",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "author": "Arnav Ranjan <arnavranjan005@gmail.com>",
8
+ "keywords": ["mcp", "model-context-protocol", "telemetry", "observability", "ai-agent", "monitoring", "claude-code", "progress", "notifications", "background-jobs", "long-running-tasks", "mcp-server", "cursor"],
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/arnavranjan005/mcp-telemetry.git",
12
+ "directory": "packages/server"
13
+ },
14
+ "homepage": "https://github.com/arnavranjan005/mcp-telemetry/tree/main/packages/server#readme",
15
+ "bugs": {
16
+ "url": "https://github.com/arnavranjan005/mcp-telemetry/issues"
17
+ },
18
+ "publishConfig": {
19
+ "access": "public"
20
+ },
21
+ "bin": {
22
+ "mcp-telemetry-server": "./bin/server.js"
23
+ },
24
+ "files": ["bin/", "dist/"],
25
+ "engines": {
26
+ "node": ">=18.0.0"
27
+ },
28
+ "scripts": {
29
+ "build": "tsc",
30
+ "dev": "tsc --watch",
31
+ "start": "node bin/server.js",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "mcp-telemetry-sdk": "^0.1.0",
36
+ "@modelcontextprotocol/sdk": "^1.0.0",
37
+ "zod": "^4.4.3"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^20.0.0",
41
+ "typescript": "^5.5.0"
42
+ }
43
+ }