# mcplytics — Usage and Health for a Remote MCP Server

Open-source wrapper for a remote MCP server: which tools get called, which apps call them, how each call ended and how long it took. No SDK, no backend, and it never stores what people asked.

Open source, by dave8172. Page: https://quirkyagents.com/wizard/projects/mcplytics/

## Overview

**In plain terms:** you built an MCP server and put it online. AI apps now call it: Claude, Cursor, ChatGPT, scripts. mcplytics answers the plain questions about that traffic:

- Which tools are used, and which never are?
- Which apps call, and how often?
- How many calls fail, and do they fail because a tool broke or because the request was wrong?
- How fast is it? Median and slowest 5%.

It wraps the one function that answers your server's HTTP requests. You don't change your tools, add an SDK or run a backend. **It never stores what people asked**: no arguments, no answers, no IP addresses.

→ [View on GitHub](https://github.com/dave8172/mcplytics)

---

## What the report looks like

```
mcplytics: 21 calls, 2026-10-20 19:05 to 2026-10-20 19:05 UTC
failed 4.8% · p50 1 ms · p95 4 ms

Tools
name                 calls  failed       p50       p95
search                  12      0%      2 ms     11 ms
get_item                 6      0%      1 ms      2 ms
delete_everything        1    100%      0 ms      0 ms
list_categories          1      0%      2 ms      2 ms

Apps calling (client name)
name            calls  failed       p50       p95
claude-code         9     11%      1 ms     11 ms
cursor              7      0%      1 ms      4 ms
openai-mcp          4      0%      1 ms      4 ms
python-httpx        1      0%      2 ms      2 ms
```

A test run. `delete_everything` doesn't exist; someone tried anyway.

---

## Install

```bash
npm install mcplytics
```

On Cloudflare Workers, wrap your handler and bind an Analytics Engine dataset named `MCPLYTICS`:

```js
import { mcplytics, analyticsEngine } from 'mcplytics';

const handler = async (request, env, ctx) => { /* your MCP endpoint */ };

export default { fetch: mcplytics(handler, { sink: analyticsEngine() }) };
```

On Deno, Bun or Node, write events to a local file instead:

```js
import { mcplytics } from 'mcplytics';
import { jsonl } from 'mcplytics/node';

Bun.serve({ fetch: mcplytics(handler, { sink: jsonl('mcplytics.jsonl') }) });
```

Then read the report:

```bash
npx mcplytics report mcplytics.jsonl --days 7
```

---

## How a call can end

A tool that fails still answers with HTTP 200, so status codes alone can't tell you much. mcplytics reads the answer:

| Outcome | Meaning |
|---|---|
| `ok` | a result |
| `tool_error` | the tool ran and failed (`isError: true`) |
| `protocol_error` | a JSON-RPC error: unknown tool, bad arguments |
| `http_error` | refused before any answer, e.g. 401 sign-in required |
| `no_response` | a stream that closed before answering |
| `crash` | your handler threw |

The full list is in the [README](https://github.com/dave8172/mcplytics#how-a-call-can-end).

---

## What it can't tell you

- **App names are what apps send.** A client names itself in the request, and any name can be claimed. Good enough for counting traffic, never for deciding who gets in.
- **Remote servers only.** A local server talks to its app over stdio, with no HTTP request to wrap.
- **No intent.** It doesn't ask the model why it called a tool. That would mean adding a field to every tool.

---

## Tech Stack

- **Language:** JavaScript, zero dependencies, web APIs only
- **Runs on:** Cloudflare Workers and Pages, Deno, Bun, Node
- **Works with:** any Streamable HTTP MCP server: the official SDK, another framework, or by hand
- **Storage:** Cloudflare Analytics Engine, a JSONL file, or your own callback
