Content
# mcp-telemetry
[](https://github.com/arnavranjan005/mcp-telemetry/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/mcp-telemetry-sdk)
[](https://www.npmjs.com/package/mcp-telemetry-server)
[](LICENSE)
**Socket.IO for AI agents.** Instrument any MCP server's tool calls with a few lines, and any MCP client watching (Claude Code, Cursor, or your own tooling) gets live, structured progress — no polling, no context-flooding tool calls.
```
Your MCP server mcp-telemetry server Agent
───────────────── ──────────────────── ─────
mcp-telemetry-sdk
job.stepStart('build')
│
│ local socket (queued, persistent connection)
▼
collector receives event
store updates job state
│
│ MCP notifications/progress
▼
telemetry_subscribe tool Claude Code
pushes event to agent → sees inline
live status
```
## Why this exists
MCP tool calls are synchronous: an agent calls a tool, waits, gets a result. For anything long-running, that leaves two bad options — block the whole call with no visibility, or have the agent poll a status tool in a loop (which floods the conversation with repeated tool calls and burns context for no new information).
MCP does have one legitimate way for a server to push updates mid-call: `notifications/progress`, keyed to a `progressToken` on the in-flight request. But every MCP server author ends up re-implementing the same plumbing — extracting the token, wiring a timer, tailing output, cleaning up on completion. `mcp-telemetry` is that plumbing, factored out once, plus a companion server so a job started in *one* session can be watched from a completely different one.
## How it fits together
Two packages, one job each:
| Package | Who uses it | What it does |
|---|---|---|
| [`mcp-telemetry-sdk`](packages/sdk) | MCP server authors | Import it, call `job.start()` / `.stepDone()` / `.log()` from your tool handlers. Zero runtime dependencies — it's a socket writer with a persistent, queued connection and nothing else. |
| [`mcp-telemetry-server`](packages/server) | Agent users | An MCP server you register once. Exposes `telemetry_subscribe` (blocks and streams live progress for a job), plus `telemetry_jobs`/`telemetry_job_status` for point-in-time queries. |
These two packages are architecturally independent — the SDK never calls any MCP tool, and the server never imports your tool's code. They only ever meet at a local socket, so a producer with a broken connection can't take down anything, and a collector that's overwhelmed can't block your tool call.
## Quickstart
### 1. Instrument your MCP server (`mcp-telemetry-sdk`)
```bash
npm install mcp-telemetry-sdk
```
```js
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
import { MCPTelemetry } from 'mcp-telemetry-sdk';
const server = new McpServer({ name: 'my-deploy-server', version: '1.0.0' });
const telemetry = new MCPTelemetry(); // zero config — derives a socket path from cwd
server.tool('deploy', { env: z.string() }, async ({ env }) => {
const job = telemetry.createJob({ task: `deploy ${env}` });
job.start();
job.stepStart('build');
await runBuild();
job.stepDone('build', { duration: 2100 });
job.stepStart('test');
const passed = await runTests();
if (!passed) {
job.stepFailed('test', '3 tests failed');
await job.done(1);
return { content: [{ type: 'text', text: 'Deploy failed at test stage' }] };
}
job.stepDone('test');
await job.done(0);
return { content: [{ type: 'text', text: 'Deployed successfully' }] };
});
```
That's the entire integration. If nothing is listening on the socket, every call is a fast no-op — your server behaves identically with or without a collector running.
### 2. Watch it from an agent (`mcp-telemetry-server`)
```bash
npm install -g mcp-telemetry-server
```
Register it as an MCP server, alongside your instrumented one:
```json
{
"mcpServers": {
"deploy": { "command": "npx", "args": ["-y", "deploy-mcp"] },
"telemetry": { "command": "npx", "args": ["-y", "mcp-telemetry-server"] }
}
}
```
Then, in your agent session:
```
You: deploy to staging
Agent: calls the deploy tool, then telemetry_subscribe with the returned job id
▶ deploy staging
↻ build
✓ build (duration=2100)
↻ test
✓ test
✓ job done (exit 0)
Agent: "Deployed to staging successfully."
```
No polling, no separate terminal, no context-flooding tool calls — one `deploy` call plus one `telemetry_subscribe` call, regardless of how long the job runs.
## API reference
### `mcp-telemetry-sdk`
**`new MCPTelemetry(opts?)`**
Creates a telemetry client. `opts.socketPath` overrides the default (derived from `process.cwd()` via `getSocketPath()`). Owns one persistent, queued connection shared by every job it creates.
**`telemetry.createJob({ id?, task })` → `JobHandle`**
Starts tracking a job. `id` defaults to an auto-incrementing `job-N`.
**`telemetry.disconnect()`**
Closes the underlying connection. Call on server shutdown if you want a clean teardown instead of letting it idle.
| `JobHandle` method | Emits | Notes |
|---|---|---|
| `start()` | `job_start` | Call once, at the beginning of the tool handler. |
| `stepStart(name, meta?)` | `step_start` | `name` is any string — `'build'`, `'implement'`, whatever fits your domain. |
| `stepDone(name, meta?)` | `step_done` | `meta` is arbitrary key/value data (shown in `telemetry_subscribe`'s live output). |
| `stepFailed(name, reason?)` | `step_failed` | |
| `log(line, stream?)` | `log` | `stream` is `'stdout' \| 'stderr'`, optional. Rapid log lines are coalesced by the server before being pushed live — see below. |
| `cost(amount, meta?)` | `cost` | `amount` in USD. |
| `done(exitCode?)` | `job_done` | **Async.** This is the terminal event — nothing else may be sent after it, so it actively retries delivery for up to 1.5s instead of relying on a future `send()` to recover from a transient connection failure. Safe to call without `await`. |
`getSocketPath(root?)` is also exported, for advanced cases where you need to compute the same path a producer and a server will independently derive.
### `mcp-telemetry-server`
Exposes three MCP tools:
| Tool | Behavior |
|---|---|
| `telemetry_subscribe({ jobId?, timeoutMs? })` | **Blocks** and streams live `notifications/progress` for the given job (or the next job to start, if `jobId` is omitted) until it finishes or `timeoutMs` elapses (default 5 min). This is the tool your agent calls to watch a job. |
| `telemetry_jobs()` | Lists all jobs the server currently knows about, with status and cost. |
| `telemetry_job_status({ jobId })` | Full state of one job — every step, cost, and any failure reason. |
## Comparison
Three genuinely different categories of approach exist near this space — none of them solve the same problem:
| | **mcp-telemetry** | Async job runners | Completion notifiers | OpenTelemetry MCP instrumentation |
|---|---|---|---|---|
| Mechanism | Push (`notifications/progress`) | Poll (call a status/tail tool yourself) | Push, but only at completion (webhook/sound) | Traces/metrics to an observability backend |
| Live step-by-step progress | Yes | No — you ask, it answers | No — only "it's done" | No — post-hoc analysis |
| Watch from a different session | Yes | No — tied to the session that started it | Partial (a webhook can fire anywhere) | N/A — not agent-facing |
| Who it's for | Any MCP server author + any agent | Anyone needing async shell execution specifically | Anyone wanting a completion ping | Server operators monitoring their own deployment |
**Relationship to [SEP-1686 (MCP Tasks)](https://github.com/modelcontextprotocol/modelcontextprotocol/issues/1686):** the MCP spec's own answer to this problem — **Accepted** into the spec (not just proposed), giving requests a durable task handle (`taskId`) with `tasks/get` polling and a `progressToken` valid for the task's whole lifetime. It's the eventual "correct" fix, backed by real production cases (Amazon cites healthcare data pipelines, CI/CD wrapping, and multi-agent systems in the SEP itself). The catch: it's labeled `awaiting-sdk-change` — the standard is settled, but client/server SDKs haven't implemented it yet, so it isn't something you can rely on today. `mcp-telemetry` solves the same problem now, on the current stable protocol — a working bridge you can adopt today and retire once Tasks lands in the SDKs you depend on, not a competing standard.
## Design notes worth knowing before you rely on this
- **The producer→server connection is a persistent, queued socket**, not one connection per event. Events are flushed in order once connected; a burst that arrives before the connection finishes establishing is queued and delivered in order once it does.
- **Delivery is best-effort, not guaranteed**, with one exception: `done()`. Every other event silently drops if the collector isn't reachable and nothing else triggers a retry — this is deliberate (telemetry should never be able to block or crash your actual tool call). `done()` is the one event that actively retries for a bounded window, since it's usually the last thing a job ever sends.
- **`telemetry_subscribe` only shows live-forward events** — it doesn't replay history. If a job already finished before you subscribed, use `telemetry_job_status` instead.
- **This is not a distributed job queue.** There's no persistence across a collector restart, no cross-machine delivery, and no retry policy beyond what's described above. If you need that, you want a real message queue — this is deliberately just enough to solve "watch a local MCP tool call live," nothing more.
## Development
```bash
git clone https://github.com/arnavranjan005/mcp-telemetry.git
cd mcp-telemetry
npm install
npm run build
npm test
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full setup, monorepo layout, and PR expectations.
## License
[MIT](LICENSE) © [Arnav Ranjan](https://github.com/arnavranjan005)
MCP Config
Below is the configuration for this MCP Server. You can copy it directly to Cursor or other MCP clients.
mcp.json
Connection Info
You Might Also Like
No recommendations available