npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@armature-tech/mcp-analytics

v0.6.6

Published

MCP analytics wrapper SDK that instruments MCP tool declarations with telemetry.

Readme

@armature-tech/mcp-analytics

Armature analytics for any MCP server — drop in a wrapper, get a dashboard of who's calling your tools, what they're asking for, and where they're getting stuck. On Armature you can see:

  • Who your users are and which tools they actually use
  • What agents are trying to accomplish (intent, context, frustration captured per call)
  • Where tools fail, time out, or get retried
  • Cross-server activity for the same user, even across vendors

All this without rolling your own logging pipeline, schema, or auth.

Getting Started

Cloud: sign in at app.armature.tech, create a server, copy the API key.

Install the SDK in your MCP server repo:

npm install @armature-tech/mcp-analytics @modelcontextprotocol/sdk zod

Wrap your server (the most common shape — an existing McpServer factory):

import { createMcpAnalyticsServer } from "@armature-tech/mcp-analytics";
import { createMyMcpServer } from "./my-mcp-server.js";

const server = createMcpAnalyticsServer(() => createMyMcpServer(), {
  armature: {
    endpointUrl: "https://app.armature.tech/api/mcp-analytics/ingest",
    apiKey: process.env.ANALYTICS_INGEST_API_KEY,
  },
});

That's it. Every tool registered inside your factory is now instrumented. Open the dashboard and the first tool call shows up.

Don't want to wire it up yourself? Ask Claude Code / Cursor / Codex: "install Armature analytics on this MCP server". Run npx skills add armature-tech/mcp-analytics --global first so the agent picks up our integration playbook — it detects which of the four shapes your repo uses and edits the right files.

Why mcp-analytics

1) Generic analytics don't understand MCP.

An MCP tool call has structure that page-view analytics throws away: the tool name, the args the agent constructed, whether the call succeeded, what the agent was trying to do. You want those as first-class fields, not buried in custom dimensions.

2) Instrumenting by hand is the same boilerplate every time.

Decorate input schemas, strip telemetry fields before the handler runs, time the call, batch, retry, dedupe sessions, propagate auth. Every MCP server reinvents it. This package is that boilerplate, packaged once.

3) The agent should be able to tell you what it's doing.

We add a telemetry object to each tool's input schema with intent, context, and frustration_level. Agents fill it in, the SDK strips it before your handler sees args, and Armature shows you the why behind each call. The block and its fields are optional — agents pass what they can, the SDK records what's there.

How it works

Three things happen on every tool call:

  1. The agent sees a telemetry block added to your tool's input schema — intent, context, frustration_level. The block is optional; the SDK never rejects a call for omitting it.
  2. Your handler sees its original args. The SDK strips telemetry before invoking it.
  3. An authenticated batch is POSTed to Armature with timing, status, input/output previews, and whatever the agent put in telemetry. The first call on a new sessionId is preceded by a session_init event.

Other integration shapes

createMcpAnalyticsServer covers most repos. If yours doesn't fit, there are three other entry points — the agent skill picks the right one automatically:

  • Concrete-server registry helperinstrumentMcpServerTools({ server, tools, config, mapTool }). Use when you already own both the McpServer instance and your tool registry; the helper calls server.registerTool(...) directly (no prototype patching), so it survives pnpm virtual-peer layouts where createMcpAnalyticsServer's patch can miss the customer's SDK module copy.
  • Registry-stylecreateAnalyticsRecorder() + analytics.tool(...) + analytics.createMcpServer(...). Use when you're building a server from scratch and want the recorder to own tool registration.
  • Dispatcher-style — same recorder, but you call analytics.toolDefinitions() from your tools/list handler and analytics.dispatch(name, args, ctx) from tools/call. For servers that hand-roll the JSON-RPC layer.
  • MastrawrapMastraTools(tools, config) from @armature-tech/mcp-analytics/mastra. Drop the wrapped map into new MCPServer({ tools }).

Code examples for all three live in SKILL.md.

Serverless / stateless HTTP (Vercel, Lambda, Cloud Run)

Stateless deployments have no memory between invocations: initialize — the only request carrying the client's name/version — lands on one instance, tool calls land on others, and clients only echo an Mcp-Session-Id header if the server issued one. Untreated, the dashboard shows one anonymous session per call and client "unknown".

resolveStatelessHttpSession fixes both with no session store: the session id minted at initialize encodes the client identity (mcp_<name>_v_<version>_<uuid>), the client echoes it on every request, and each invocation parses it back out:

import { resolveStatelessHttpSession } from "@armature-tech/mcp-analytics";

export default async (req, res) => {
  const session = resolveStatelessHttpSession({ body: req.body, headers: req.headers });
  const transport = new StreamableHTTPServerTransport({
    sessionIdGenerator: session.sessionIdGenerator, // defined only at initialize
    enableJsonResponse: true,
  });
  // dispatcher shape:
  await analytics.dispatch(name, args, { ctx, ...session.dispatchContext });
  // ... connect server, transport.handleRequest(req, res, req.body)
};

The recorder also parses identity-bearing session ids on its own as a last-resort fallback, so client attribution works even when only the transport side is wired. Use delivery: "await" in serverless (see Delivery mode below).

Attribution is best-effort telemetry, not a security boundary: the echoed id carries no signature, so a malicious caller can claim any client name. Gate access with real auth and treat client/session attribution as observability.

Configuration

type McpAnalyticsConfig = {
  armature?: {
    endpointUrl?: string;     // default reads ANALYTICS_INGEST_URL
    apiKey?: string;          // default reads ANALYTICS_INGEST_API_KEY
    actorId?: string | ((input) => string | Promise<string>);
    enabled?: boolean;        // default true
    delivery?: "background" | "await"; // default "background"
    timeoutMs?: number;       // default 500
    emit?: (batch) => void | Promise<void>; // override the network emitter
    onError?: (error, batch) => void;
  };
};

The shape of the telemetry block on each tool's input schema is Armature-owned and not customer-configurable. Customers only set operational config (delivery, actor id, transport).

Delivery mode. "background" (default, best for long-lived processes) returns the tool result immediately and posts the batch on setImmediate — call await analytics.flush() at shutdown. "await" (recommended for serverless) resolves only after the batch has been posted; no flush needed.

Actor id. A SHA-256 of an actor seed. By default the seed comes from the request's auth token / client id / authorization header. Pass a static armature.actorId seed for a stable source, or a function to derive the seed from { ctx, extra, headers, authInfo, toolName, telemetry }. Armature scopes the actor id to your server via the API key, so the same seed under two different servers stays linked to the same person (cross-surface analytics).

Missing API key. The SDK silently skips delivery — useful for local development.

Auth. Each batch is POSTed with Authorization: Bearer <apiKey>. Server identity is resolved from the API key — no separate header.

Environment variables

| Variable | Purpose | | --- | --- | | ANALYTICS_INGEST_URL | Ingest endpoint (defaults to https://app.armature.tech/api/mcp-analytics/ingest; override for a local mock or staging) | | ANALYTICS_INGEST_API_KEY | Your Armature API key — identifies the MCP server and signs each batch |

More

  • Custom integrationswithMcpAnalytics, createAnalyticsRecorder, decorateInputSchemaWithTelemetry, and other lower-level primitives are exported for cases the four shapes don't cover. See docs/ and the source.
  • Recording session_init explicitlyrecordToolCall already emits one on the first call per sessionId. Call analytics.recordSessionInit({ sessionId, ctx }) from your initialize handler if you want it at handshake time.
  • Flushing on the McpServer path — use withMcpAnalytics(config, createServer) instead of createMcpAnalyticsServer; it returns { result, recorder } so you can await recorder.flush().
  • AI agents integrating this — read SKILL.md (also shipped in the npm tarball).
  • Support[email protected] or open an issue.