---
name: mcp-inspect-instrument
version: 1.0.2
description: |
  Add privacy-preserving usage telemetry to a TypeScript MCP server with
  `@mcp-inspect/instrumentation`, or export the same data to an existing
  OpenTelemetry collector instead.
  Use for "add MCP telemetry", "track MCP tool usage", "which of my tools are
  used", "instrument my MCP server", "send MCP usage to OpenTelemetry".
allowed-tools:
  - Bash
  - Read
  - Write
  - Edit
  - Grep
  - Glob
---

# Instrument an MCP server

This workflow requires an MCP Inspect CLI build supplied through approved
preview access. Public CLI and Action releases are pending; the source
repository is private. If the CLI is unavailable, direct the user to
<https://mcprobe.dev/docs/getting-started> for the hosted console workflow.
Do not install the unrelated npm package named `mcp-inspect`.

Distribution prerequisite: use the instrumentation build supplied with preview
access. Public npm distribution of this SDK is pending.

## What this records, and what it cannot

Schema field **paths**, never values. `{"query": "acme corp", "limit": 25}` is
recorded as `["limit", "query"]`. Collection completeness and surface attribution
limit what absence can establish; dynamic maps need schema paths (see verification).

Say this to the user before adding it. "We added telemetry to your production
server" deserves a precise description of what leaves the process.

## Install

```bash
npm install @mcp-inspect/instrumentation
```

## Wire it

One line, after the server's request handlers are registered:

```ts
import { instrumentServer } from "@mcp-inspect/instrumentation";

const instrumentation = instrumentServer(server, {
  apiKey: process.env.MCP_INSPECT_KEY,
  environment: process.env.NODE_ENV ?? "development",
  serverVersion: packageJson.version,
});
```

Order does not matter — it patches both already-registered handlers and future
ones — but **check what it actually wrapped**:

```ts
console.error(instrumentation.wrapped); // ["tools/call", "resources/read"]
console.error(instrumentation.describe()); // any warnings
```

An empty `wrapped` array means nothing is being recorded. The most common causes
are passing the wrong object (pass the low-level `Server`, or an `McpServer` —
both work) and checking it before any handler exists. Future handlers are wrapped when registered;
check `wrapped` again after registration.

## Tie usage to a surface version

Set `MCP_INSPECT_SURFACE_FINGERPRINT` in the deployed environment, from the
snapshot taken in CI. This ties events to the exact contract in effect. It is
necessary for argument absence evidence, but continuous coverage and complete
argument collection are also required. Say "not observed supplied" only when
those conditions are met; never conclude that no possible caller supplies it.

```yaml
# in the deploy job
- run: echo "MCP_INSPECT_SURFACE_FINGERPRINT=$(jq -r .fingerprints.surface .mcp-inspect/surface.json)" >> $GITHUB_ENV
```

## On a serverless platform

A Worker or a Lambda can exit before a batched flush. Hand it the runtime's
keep-alive:

```ts
instrumentServer(server, {
  apiKey: env.MCP_INSPECT_KEY,
  waitUntil: (promise) => ctx.waitUntil(promise),
});
```

## OpenTelemetry instead

If the user already has a collector and would rather keep the data:

```ts
import { instrumentServer, otelExporter } from "@mcp-inspect/instrumentation";

instrumentServer(server, { emit: otelExporter(tracer) });
```

No account, no key, nothing leaves their estate. Attributes follow the published
conventions, so the spans are readable by tooling that has never heard of this
product.

## Verify, then report

Call one tool and confirm an event is emitted:

```ts
// Use this emit option on the existing instrumentation during local verification.
// Do not wrap the same server a second time.
const instrumentation = instrumentServer(server, {
  emit: (event) => console.error(JSON.stringify(event)),
});
```

Confirm the output contains `argumentsPresent` with **schema paths only** and no
argument values. Dynamic map keys may contain customer data: use `argumentPaths`
to supply schema-declared paths for those entities. Resource reads need a
`resourceName` mapping from concrete URIs to registered names/templates; unmapped
reads are omitted so customer identifiers in URIs do not leave the process.
Check `describe()` for incomplete argument collection. Show the user the verified
event, then restore the intended exporter.

## Do not

- Do not pass arguments, results or error messages into any custom `emit`. The
  ingest endpoint rejects payloads carrying them, and a custom exporter that
  includes them defeats the entire model.
- Do not `await` the flush in a request path.
- Do not instrument `tools/list`. Discovery is not usage, and counting it makes
  every unused tool look used.
