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

@codenotary/amon-mcp

v0.3.0

Published

MCP server for AgentMon — query AI agent observability telemetry (cost, traces, security findings) from an AI agent.

Readme

amon-mcp

MCP server for AgentMon. Lets an AI agent query its own observability data — cost, token usage, traces, and security findings — through the AgentMon REST API.

Read-only. Speaks to both AgentMon products — 20 tools against the full server, 14 against the standalone one; see Tools.

Install

claude mcp add --transport stdio amon \
  --env AMON_URL=https://agentmon.example.com \
  --env AMON_TOKEN=amk_… \
  -- npx -y @codenotary/amon-mcp

Or from a checkout:

cd mcp && npm ci && npm run build
claude mcp add --transport stdio amon \
  --env AMON_URL=http://localhost:8080 --env AMON_TOKEN=v4.local.… \
  -- node /path/to/AgentMon/mcp/dist/index.js

Project scope: copy .mcp.json.example from the repo root to .mcp.json.

Claude Desktop — download amon-mcp.mcpb from the codenotary/amon releases and open it. That is a different repo on purpose: codenotary/agentmon is private, so its releases are not downloadable — the .mcpb ships alongside the amon CLI binaries, which take the same route. The bundle prompts for AMON_URL and AMON_TOKEN on install.

The package is @codenotary/amon-mcp; the command it installs is amon-mcp.

Configuration

| Variable | Required | Meaning | |---|---|---| | AMON_URL | yes | Base URL of the deployment. Include the scheme; omit any /api prefix — the server adds the one its dialect needs. | | AMON_TOKEN | yes | Full server: v4.local.… PASETO, amk_… API key, or a Trust token. Standalone: amonl_pat_… access token. | | AMON_API_DIALECT | no | cloud or light. Normally unset — the dialect comes from the token. See Which server am I pointing at? | | AMON_MCP_ALLOW_WRITE | no | 1 registers mutating tools. None exist yet; the gate ships ahead of them. |

Startup probes, all reported on stderr. Against the full server: /auth/me (fails fast on a bad token), /auth/my-systems (scope — see below), and /licence/status (entitlement, which decides whether the tools register). Against the standalone server: /api/me and /api/license — there is no scope probe there, because that product applies no per-user filter.

Which server am I pointing at?

AgentMon ships as two products with two different APIs, and this package speaks both. They are not versions of each other: the full server serves 342 routes under /api/v1, the standalone server ~45 under a bare /api, and they share no route paths at all — so the tool list differs by product.

| | Full server (cloud) | Standalone server (light) | |---|---|---| | Backing store | ClickHouse | SQLite, single binary | | Token | amk_…, v4.local.…, or a Trust JWT | amonl_pat_… access token | | Tools | 20 | 14 |

You do not normally configure this. The dialect is derived from the token, because the standalone server issues its own prefix — no network guess. The one ambiguous credential is an opaque Trust JWT, which is confirmed by a startup probe. Set AMON_API_DIALECT=cloud|light only to override that.

Two tokens are rejected at startup rather than failing on every read: a Personal Ingest Token on the full server, and a system's amonl_… ingest key on the standalone one. Both are ingest-scoped and can never read.

On the standalone server

Mint a token under Settings → Access tokens. It is shown once. Available to every user, including viewers — a token grants no more than the session that created it, and never more than read: it cannot write, reach admin endpoints, manage tokens, or read raw prompt and completion text.

Some tools are licence-gated there and return 423 when no licence is installed. That means the whole premium family is locked, not that one tool failed — the tools stay listed either way, because installing a licence takes effect immediately on the server and hiding them would leave them missing until this process restarted. These keep working unlicensed: amon_overview, amon_systems, amon_timeseries, amon_insights, amon_recommendations, amon_alerts.

What is never returned. GET /api/session-trace is not wired and will not be: it returns every span's input/output verbatim — whole prompts and completions — and the standalone server has no redaction anywhere. The server also refuses it to an access token outright. amon_session_threads and amon_session_detail do carry a ~140-character excerpt of each session's first prompt, which is the only user-written text this dialect exposes.

Getting a token

Three forms work on the full server, matching how the API's require_auth routes a bearer (crates/api/src/auth/mod.rs:486-518). For the standalone server, see the section above.

API key (amk_…) — preferred on a licensed deployment. Dashboard → Settings → API Keys, or:

curl -X POST https://agentmon.example.com/api/v1/auth/local/api-keys \
  -H 'Content-Type: application/json' -b "$SESSION_COOKIE" \
  -d '{"name":"mcp","ttl_days":365}'

The key is shown once. Three constraints worth knowing up front: an API key cannot mint another API key (bootstrap from a logged-in session); API keys are disabled entirely when the deployment runs AUTH_MODE=trust — use the Trust-issued token there; and minting one needs an active licence.

On a deployment with no licence installed — which includes a fresh local stack — that POST returns 423 Locked:

{"error":"license read-only — install a valid license via Admin → Licensing","state":"missing"}

That is the read-only guard (crates/api/src/auth/license_guard.rs), which 423s every mutating verb outside Trust mode when the licence state is ReadOnly or Missing. Use the session token instead (below).

Session token — what to use on a local or unlicensed stack. The amon_session cookie set by the login endpoint is a v4.local.… PASETO, and works unchanged as a bearer:

curl -sc cookies.txt -X POST http://localhost:8080/api/v1/auth/local/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin@local","password":"AgentMonLocal!1"}'
AMON_TOKEN=$(awk '$6=="amon_session"{print $7}' cookies.txt)

Note the field is username, not email, even though the value is an email address — posting {"email": …} returns 422 missing field 'username'. The token carries the session's TTL, so it is right for a smoke test and wrong for anything long-lived.

PASETO (v4.local.…) — for operators who hold PASETO_KEY:

PASETO_KEY=… amon token mint --sub [email protected] --org-id your-org --role viewer --ttl-days 365

viewer is enough for every tool here. Note the subs admin, user, dev-user and the empty string are rejected by the API as legacy shared tokens.

A Personal Ingest Token will NOT work, even though it is also a v4.local.… PASETO. This is the easiest wrong turn here: the PAT from Settings → Personal Ingest Token (the one amon watch --api-key takes) is minted ingest-scoped, and every dashboard read rejects it:

$ curl -H "Authorization: Bearer v4.local.…" https://…/api/v1/auth/me
{"error":"ingest-scoped token cannot be used for dashboard access"}   [HTTP 403]

That is deliberate scope separation (crates/api/src/auth/mod.rs), not a misconfiguration — ingest credentials are handed to watchers on developer machines and must not also read the fleet. The prefix cannot tell the two apart, so if amon-mcp reports a 403 on startup with that message, you have reached for the ingest token: mint a read token by one of the routes above instead.

Trust-issued token — on a deployment running AUTH_MODE=trust, use whatever token Trust issues. It carries no fixed prefix: the API forwards any bearer it does not recognise to the Trust provider. This is the only option in that mode, since API keys are disabled there. The startup banner notes when it takes this path, because on any other deployment an unprefixed token is the usual cause of a 401.

⚠️ Scoping: why the tools may show you almost nothing

AgentMon filters telemetry reads server-side, in SQL, by the token's identity. On a fresh OIDC installation the default resolves to Scoped{self} — every query gains AND account_sub = <your sub>.

This applies even to an admin. Role controls which endpoints you may call; scope controls which rows come back. An admin with no org-scope RBAC binding sees only their own agents, and a fleet-wide question truthfully returns one agent.

The server surfaces this two ways: a startup warning on stderr, and a scope caveat attached in-band to every empty result — so the model reports "this token is restricted" rather than "your fleet is empty". If you need whole-org visibility, ask an AgentMon admin for an org-scope binding (see docs/auth.md).

Check what your token can see:

curl -H "Authorization: Bearer $AMON_TOKEN" https://…/api/v1/auth/my-systems
# {"unrestricted":true}  → you see the whole org
# {"unrestricted":false,"scope_level":"self"} → you see only your own rows

Tools

Two sets, one per product — see "Which server am I pointing at?" above. The full-server set is documented first; the standalone set follows.

Full server (cloud)

All read-only. Most tools take a window as hours (default 24) or an explicit from/to ISO-8601 pair; the server maximum span is 90 days. Five take none, because their handlers deserialise no window and would silently ignore one: amon_dora_metrics and amon_delivery_report report over the server's own fixed horizon, amon_forecast_cost looks forward via horizon_days, and amon_stale_agents is anchored to now() - lookback_hours — its handler declares time_from/time_to and reads neither, so offering a window would mean echoing back a range that was never applied.

| Tool | What it answers | |---|---| | amon_fleet_rollup | Headline totals: cost, spans, tokens, distinct agents | | amon_fleet_overview | The dashboard's own snapshot: digest, security digest, fired alerts | | amon_fleet_metric | One of 13 fleet metrics: rankings, anomalies, hotspots, burn rate, cache, health, … | | amon_stale_agents | Agents that reported and then went quiet — lookback-anchored, no window | | amon_agent_rankings | Agents ranked by rpm or error_rate | | amon_list_agents | The entity index — resolve an agent id here first (the field is id, see below) | | amon_agent_detail | One agent: timeline, traces, tools, sessions, reliability, uptime | | amon_cost_breakdown | Cost grouped by model, agent, or team | | amon_forecast_cost | Projected spend over the coming days | | amon_model_compare | Models side by side, by cost or by behavior | | amon_recommendations | The platform's own recommendations or the raw insights behind them | | amon_analytics_search | Filtered span search across 11 dimensions — the escape hatch | | amon_security_overview | Security posture: counts by severity and category, with the trend | | amon_security_findings | Dangerous commands, secret leaks, injection attempts, spawn anomalies, off-hours activity, scan findings/posture, licence violations | | amon_agent_risk_profile | Which agent is the problem — findings attributed per agent | | amon_secret_files | Credential-shaped files agents touched (paths, never contents) | | amon_policy_decisions | What the policy engine allowed or denied, and which rules fired | | amon_team_metric | 14 team metrics: contention, PR audit, yield, governance, … † | | amon_delivery_report | bottlenecks, velocity-forecast, knowledge-silos † | | amon_dora_metrics | The four DORA metrics † |

† Licence-gated on team_intelligence. These three are absent from tools/list when the startup probe (GET /licence/status) reports the feature denied, rather than present and erroring. If the probe cannot answer they are registered anyway and may return a 403 — hiding a tool the org paid for is the worse failure, because nothing surfaces it. amon_forecast_cost is NOT gated despite living in the same family.

Standalone server (light)

All read-only. Windowed tools take the same hours / from+to inputs.

| Tool | What it answers | Licensed only | |---|---|---| | amon_overview | Headline totals: systems, sessions, tokens, cost, findings, with deltas | | | amon_systems | The machines pushing telemetry — resolve a system_id here first | | | amon_timeseries | Cost/token/session trend, bucketed (bucket size is server-derived) | | | amon_insights | Prompt-cache effectiveness and a findings rollup | | | amon_recommendations | Detector recommendations with estimated savings — takes no parameters | | | amon_alerts | Firing alerts and recent history — optional lower bound, no upper; omit for all history | | | amon_cost_breakdown | Cost by models, agents, projects or tools | ✓ | | amon_compare_systems | Up to four systems side by side | ✓ | | amon_list_sessions | Individual sessions: duration, model, tokens, cost, tool counts | ✓ | | amon_session_threads | Sessions grouped into work threads by workspace ‡ | ✓ | | amon_session_detail | One session's rollup, per-tool breakdown and background jobs ‡ | ✓ | | amon_security_summary | Secret leaks, injection attempts, dangerous commands — counts only | ✓ | | amon_workspaces | Activity by machine + user + repository | ✓ | | amon_shadow_ai | Unknown agent types, unattributed sessions, spawn anomalies | ✓ |

‡ Carries a ~140-character excerpt of the session's first user prompt. The only user-written text this dialect returns; full conversation content is not exposed at all.

Unlike the cloud team family, the licensed-only tools are always registered. A locked deployment returns 423, and the message says the whole family is locked so the model does not work through the siblings one at a time. The reason for the difference: installing a licence on the standalone server takes effect on the next request, so a tool withheld at startup would stay missing for a session that could otherwise have recovered.

Chaining the per-agent tools: the field is id

amon_agent_detail takes an agent_id argument, but the rows from amon_list_agents carry no field of that name — the identifier is id:

// one amon_list_agents row
{ "id": "ae938e80f71a166e2", "name": "codenotary/agentx-next",
  "parent_agent_id": "…", /* 43 more fields, none called agent_id */ }

Pass that id value as amon_agent_detail's agent_id. The API names the same identifier id on the list and agent_id on every per-agent response — /agents/{id}/timeline echoes it back under the second name — so the round trip reads oddly but is correct.

Worth stating because it is invisible on an empty deployment: with no telemetry the list returns zero rows, which agrees with every expectation you might have about its shape.

What is deliberately not reachable

Secret values are never returned. Neither is raw prompt or completion text: /security/secret-leaks/…/reveal, /traces/:id/spans/:id/content, /traces/:id/waterfall and /traces/:id/conversation are all unwired.

The last two are worth naming because they look harmless. WaterfallSpan.input/.output are filled from gen_ai.prompt / gen_ai.completion, and ConversationTurn.content is the message body verbatim — the same data as the /content route. Both redact per row, and both skip redaction entirely for admin and security-auditor claims, which is the role the token-minting instructions above produce. So there is no "trace detail" tool. Identify and rank traces with amon_agent_detail view="traces" or amon_fleet_metric metric="slow-spans"; reading one is the boundary.

Enforcement is an allow-list, not a deny-list: mcp/test/routes.test.ts walks the TypeScript AST for every request site and fails unless the path is one complete literal on a reviewed list.

Development

npm ci
npm run typecheck
npm test
npm run build

The test that matters

test/contract.test.ts parses the query-parameter structs straight out of crates/api/src and checks every tool call against them.

It exists because crates/api has zero #[serde(deny_unknown_fields)]:

grep -rn 'deny_unknown_fields' crates/api/src | wc -l   →   0

Axum's Query<T> silently drops unknown keys, so a misspelled or invented parameter returns HTTP 200 carrying the server's default 1-hour window — never a 400. There is no runtime signal at all. A mocked-client test proves "we sent what we meant to send"; only this proves "what we meant to send is accepted".

This is not hypothetical. The original implementation plan specified from_ns/to_ns/hours as the time parameters (they do not exist; the real shape is time_from/time_to in Unix seconds). Every tool would have returned one hour of data labelled as twenty-four, and every mock test would have been green. src/time.ts therefore ships exactly one converter, which always emits both halves of the window.

The workflow runs on changes to crates/api/src/** as well as mcp/**, so an API-side rename breaks CI rather than a released package.

Live smoke against a dev stack

cd deploy && docker compose -f docker-compose.local.yml up -d

# The stack ships no licence, so mint the token from the session (see above).
curl -sc /tmp/amon-cookies -X POST http://localhost:8080/api/v1/auth/local/login \
  -H 'Content-Type: application/json' \
  -d '{"username":"admin@local","password":"AgentMonLocal!1"}'
export AMON_TOKEN=$(awk '$6=="amon_session"{print $7}' /tmp/amon-cookies)
export AMON_URL=http://localhost:8080

Then either run the server by hand:

node mcp/dist/index.js

or run the automated sweep, which registers every tool module against a real client and calls every tool and every enum variant:

cd mcp && AMON_MCP_LIVE=1 npm test -- test/live.test.ts

It prints what it covered and what it skipped — an empty deployment has no agent to resolve, so the six per-agent views are unreachable and are reported rather than silently passed over.

Take that skip seriously rather than as a footnote. It is what hid the id/agent_id mismatch above through four rounds of review: with zero rows the list agrees with any assumption about its shape, and only a run against a deployment with real telemetry disagrees.

To confirm the window truly reaches ClickHouse rather than being dropped, call a tool at hours=1 and hours=168 and check the API log: the emitted SQL carries fromUnixTimestamp64Nano(...) bounds whose span must match what you asked for.

License

Apache-2.0