{"_id":"@codenotary/amon-mcp","name":"@codenotary/amon-mcp","dist-tags":{"latest":"0.3.0"},"versions":{"0.3.0":{"name":"@codenotary/amon-mcp","version":"0.3.0","description":"MCP server for AgentMon — query AI agent observability telemetry (cost, traces, security findings) from an AI agent.","license":"Apache-2.0","repository":{"type":"git","url":"git+https://github.com/codenotary/agentmon.git","directory":"mcp"},"type":"module","bin":{"amon-mcp":"dist/index.js"},"publishConfig":{"access":"public"},"engines":{"node":">=20"},"scripts":{"build":"tsc","typecheck":"tsc --noEmit && tsc -p tsconfig.test.json","test":"vitest run","test:watch":"vitest","lint":"tsc --noEmit && tsc -p tsconfig.test.json","prepublishOnly":"npm run build"},"keywords":["mcp","modelcontextprotocol","agentmon","observability","llm"],"dependencies":{"@modelcontextprotocol/sdk":"^1.22.0","zod":"^3.25.76"},"devDependencies":{"@types/node":"^22.10.2","typescript":"^5.7.3","vitest":"^4.1.0"},"gitHead":"36e6c2625215b0629711e31a2ad9e8e1144055b1","_id":"@codenotary/amon-mcp@0.3.0","bugs":{"url":"https://github.com/codenotary/agentmon/issues"},"homepage":"https://github.com/codenotary/agentmon#readme","_nodeVersion":"24.12.0","_npmVersion":"11.6.2","dist":{"integrity":"sha512-O5sre5P+eIFUm/JUHwX5RXMm8cLyOGJjsCKeICbkqH09oIPciUK58Jo65ZWVpZkXGwWKIy0nHrpbgILnTdcp9w==","shasum":"b0962a89b8d63671ad710be829baabdeea553cba","tarball":"https://registry.npmjs.org/@codenotary/amon-mcp/-/amon-mcp-0.3.0.tgz","fileCount":56,"unpackedSize":392327,"signatures":[{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIGDIht09MgGaB8c1TrnUQwvNQc/4SiIudAwZl4Pmd72MAiEAre37oHFvq2z7qyVs1FpLAWloEKsqQcv0vxEZrCVY9VU="}]},"_npmUser":{"name":"vchain","email":"dennis@vchain.us"},"directories":{},"maintainers":[{"name":"tomekkolo","email":"tomek.kolo@gmail.com"},{"name":"vchain","email":"dennis@vchain.us"},{"name":"iamvs","email":"mailtoiamvs@gmail.com"},{"name":"senelalp","email":"alp@codenotary.com"}],"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/amon-mcp_0.3.0_1785764180381_0.899655810705204"},"_hasShrinkwrap":false}},"time":{"created":"2026-08-03T13:36:20.148Z","0.3.0":"2026-08-03T13:36:20.585Z","modified":"2026-08-03T13:36:20.893Z"},"maintainers":[{"name":"tomekkolo","email":"tomek.kolo@gmail.com"},{"name":"vchain","email":"dennis@vchain.us"},{"name":"iamvs","email":"mailtoiamvs@gmail.com"},{"name":"senelalp","email":"alp@codenotary.com"}],"description":"MCP server for AgentMon — query AI agent observability telemetry (cost, traces, security findings) from an AI agent.","homepage":"https://github.com/codenotary/agentmon#readme","keywords":["mcp","modelcontextprotocol","agentmon","observability","llm"],"repository":{"type":"git","url":"git+https://github.com/codenotary/agentmon.git","directory":"mcp"},"bugs":{"url":"https://github.com/codenotary/agentmon/issues"},"license":"Apache-2.0","readme":"# amon-mcp\n\nMCP server for [AgentMon](https://github.com/codenotary/agentmon). Lets an AI\nagent query its own observability data — cost, token usage, traces, and\nsecurity findings — through the AgentMon REST API.\n\nRead-only. Speaks to both AgentMon products — 20 tools against the full\nserver, 14 against the standalone one; see [Tools](#tools).\n\n## Install\n\n```bash\nclaude mcp add --transport stdio amon \\\n  --env AMON_URL=https://agentmon.example.com \\\n  --env AMON_TOKEN=amk_… \\\n  -- npx -y @codenotary/amon-mcp\n```\n\nOr from a checkout:\n\n```bash\ncd mcp && npm ci && npm run build\nclaude mcp add --transport stdio amon \\\n  --env AMON_URL=http://localhost:8080 --env AMON_TOKEN=v4.local.… \\\n  -- node /path/to/AgentMon/mcp/dist/index.js\n```\n\nProject scope: copy `.mcp.json.example` from the repo root to `.mcp.json`.\n\n**Claude Desktop** — download `amon-mcp.mcpb` from the\n[`codenotary/amon` releases](https://github.com/codenotary/amon/releases) and\nopen it. That is a different repo on purpose: `codenotary/agentmon` is private,\nso its releases are not downloadable — the `.mcpb` ships alongside the `amon`\nCLI binaries, which take the same route. The bundle prompts for `AMON_URL` and\n`AMON_TOKEN` on install.\n\nThe package is `@codenotary/amon-mcp`; the command it installs is `amon-mcp`.\n\n### Configuration\n\n| Variable | Required | Meaning |\n|---|---|---|\n| `AMON_URL` | yes | Base URL of the deployment. Include the scheme; omit any `/api` prefix — the server adds the one its dialect needs. |\n| `AMON_TOKEN` | yes | Full server: `v4.local.…` PASETO, `amk_…` API key, or a Trust token. Standalone: `amonl_pat_…` access token. |\n| `AMON_API_DIALECT` | no | `cloud` or `light`. Normally unset — the dialect comes from the token. See [Which server am I pointing at?](#which-server-am-i-pointing-at) |\n| `AMON_MCP_ALLOW_WRITE` | no | `1` registers mutating tools. None exist yet; the gate ships ahead of them. |\n\nStartup probes, all reported on stderr. Against the **full server**: `/auth/me`\n(fails fast on a bad token), `/auth/my-systems` (scope — see below), and\n`/licence/status` (entitlement, which decides whether the `†` tools register).\nAgainst the **standalone server**: `/api/me` and `/api/license` — there is no\nscope probe there, because that product applies no per-user filter.\n\n## Which server am I pointing at?\n\nAgentMon ships as two products with two different APIs, and this package speaks\nboth. They are not versions of each other: the full server serves 342 routes\nunder `/api/v1`, the standalone server ~45 under a bare `/api`, and they share\nno route paths at all — so the tool list differs by product.\n\n| | Full server (`cloud`) | Standalone server (`light`) |\n|---|---|---|\n| Backing store | ClickHouse | SQLite, single binary |\n| Token | `amk_…`, `v4.local.…`, or a Trust JWT | `amonl_pat_…` access token |\n| Tools | 20 | 14 |\n\n**You do not normally configure this.** The dialect is derived from the token,\nbecause the standalone server issues its own prefix — no network guess. The one\nambiguous credential is an opaque Trust JWT, which is confirmed by a startup\nprobe. Set `AMON_API_DIALECT=cloud|light` only to override that.\n\nTwo tokens are rejected at startup rather than failing on every read: a\nPersonal Ingest Token on the full server, and a system's `amonl_…` ingest key\non the standalone one. Both are ingest-scoped and can never read.\n\n### On the standalone server\n\nMint a token under **Settings → Access tokens**. It is shown once. Available to\nevery user, including viewers — a token grants no more than the session that\ncreated it, and never more than read: it cannot write, reach admin endpoints,\nmanage tokens, or read raw prompt and completion text.\n\nSome tools are licence-gated there and return **423** when no licence is\ninstalled. That means the whole premium family is locked, not that one tool\nfailed — the tools stay listed either way, because installing a licence takes\neffect immediately on the server and hiding them would leave them missing until\nthis process restarted. These keep working unlicensed: `amon_overview`,\n`amon_systems`, `amon_timeseries`, `amon_insights`, `amon_recommendations`,\n`amon_alerts`.\n\n**What is never returned.** `GET /api/session-trace` is not wired and will not\nbe: it returns every span's `input`/`output` verbatim — whole prompts and\ncompletions — and the standalone server has no redaction anywhere. The server\nalso refuses it to an access token outright. `amon_session_threads` and\n`amon_session_detail` do carry a ~140-character excerpt of each session's first\nprompt, which is the only user-written text this dialect exposes.\n\n## Getting a token\n\nThree forms work on the full server, matching how the API's `require_auth`\nroutes a bearer (`crates/api/src/auth/mod.rs:486-518`). For the standalone\nserver, see the section above.\n\n**API key (`amk_…`)** — preferred on a licensed deployment. Dashboard →\nSettings → API Keys, or:\n\n```bash\ncurl -X POST https://agentmon.example.com/api/v1/auth/local/api-keys \\\n  -H 'Content-Type: application/json' -b \"$SESSION_COOKIE\" \\\n  -d '{\"name\":\"mcp\",\"ttl_days\":365}'\n```\n\nThe key is shown once. Three constraints worth knowing up front: an API key\n**cannot mint another API key** (bootstrap from a logged-in session); API keys\nare **disabled entirely** when the deployment runs `AUTH_MODE=trust` — use the\nTrust-issued token there; and minting one needs an **active licence**.\n\nOn a deployment with no licence installed — which includes a fresh local\nstack — that `POST` returns **423 Locked**:\n\n```json\n{\"error\":\"license read-only — install a valid license via Admin → Licensing\",\"state\":\"missing\"}\n```\n\nThat is the read-only guard (`crates/api/src/auth/license_guard.rs`), which\n423s every mutating verb outside Trust mode when the licence state is\n`ReadOnly` or `Missing`. Use the session token instead (below).\n\n**Session token** — what to use on a local or unlicensed stack. The\n`amon_session` cookie set by the login endpoint *is* a `v4.local.…` PASETO,\nand works unchanged as a bearer:\n\n```bash\ncurl -sc cookies.txt -X POST http://localhost:8080/api/v1/auth/local/login \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"username\":\"admin@local\",\"password\":\"AgentMonLocal!1\"}'\nAMON_TOKEN=$(awk '$6==\"amon_session\"{print $7}' cookies.txt)\n```\n\nNote the field is **`username`**, not `email`, even though the value is an\nemail address — posting `{\"email\": …}` returns `422 missing field 'username'`.\nThe token carries the session's TTL, so it is right for a smoke test and wrong\nfor anything long-lived.\n\n**PASETO (`v4.local.…`)** — for operators who hold `PASETO_KEY`:\n\n```bash\nPASETO_KEY=… amon token mint --sub you@example.com --org-id your-org --role viewer --ttl-days 365\n```\n\n`viewer` is enough for every tool here. Note the subs `admin`, `user`,\n`dev-user` and the empty string are rejected by the API as legacy shared\ntokens.\n\n> **A Personal Ingest Token will NOT work, even though it is also a\n> `v4.local.…` PASETO.** This is the easiest wrong turn here: the PAT from\n> Settings → Personal Ingest Token (the one `amon watch --api-key` takes) is\n> minted ingest-scoped, and every dashboard read rejects it:\n>\n> ```\n> $ curl -H \"Authorization: Bearer v4.local.…\" https://…/api/v1/auth/me\n> {\"error\":\"ingest-scoped token cannot be used for dashboard access\"}   [HTTP 403]\n> ```\n>\n> That is deliberate scope separation (`crates/api/src/auth/mod.rs`), not a\n> misconfiguration — ingest credentials are handed to watchers on developer\n> machines and must not also read the fleet. The prefix cannot tell the two\n> apart, so if `amon-mcp` reports a 403 on startup with that message, you have\n> reached for the ingest token: mint a read token by one of the routes above\n> instead.\n\n**Trust-issued token** — on a deployment running `AUTH_MODE=trust`, use\nwhatever token Trust issues. It carries no fixed prefix: the API forwards any\nbearer it does not recognise to the Trust provider. This is the *only* option\nin that mode, since API keys are disabled there. The startup banner notes when\nit takes this path, because on any other deployment an unprefixed token is the\nusual cause of a 401.\n\n## ⚠️ Scoping: why the tools may show you almost nothing\n\nAgentMon filters telemetry reads **server-side, in SQL, by the token's\nidentity**. On a fresh OIDC installation the default resolves to\n`Scoped{self}` — every query gains `AND account_sub = <your sub>`.\n\n**This applies even to an `admin`.** Role controls which endpoints you may\ncall; scope controls which rows come back. An admin with no org-scope RBAC\nbinding sees only their own agents, and a fleet-wide question truthfully\nreturns one agent.\n\nThe server surfaces this two ways: a startup warning on stderr, and a scope\ncaveat attached in-band to every empty result — so the model reports \"this\ntoken is restricted\" rather than \"your fleet is empty\". If you need whole-org\nvisibility, ask an AgentMon admin for an org-scope binding (see `docs/auth.md`).\n\nCheck what your token can see:\n\n```bash\ncurl -H \"Authorization: Bearer $AMON_TOKEN\" https://…/api/v1/auth/my-systems\n# {\"unrestricted\":true}  → you see the whole org\n# {\"unrestricted\":false,\"scope_level\":\"self\"} → you see only your own rows\n```\n\n## Tools\n\nTwo sets, one per product — see \"Which server am I pointing at?\" above. The\nfull-server set is documented first; the standalone set follows.\n\n### Full server (`cloud`)\n\nAll read-only. Most tools take a window as `hours` (default 24) or an explicit\n`from`/`to` ISO-8601 pair; the server maximum span is 90 days. Five take none,\nbecause their handlers deserialise no window and would silently ignore one:\n`amon_dora_metrics` and `amon_delivery_report` report over the server's own\nfixed horizon, `amon_forecast_cost` looks forward via `horizon_days`, and\n`amon_stale_agents` is anchored to `now() - lookback_hours` — its handler\ndeclares `time_from`/`time_to` and reads neither, so offering a window would\nmean echoing back a range that was never applied.\n\n| Tool | What it answers |\n|---|---|\n| `amon_fleet_rollup` | Headline totals: cost, spans, tokens, distinct agents |\n| `amon_fleet_overview` | The dashboard's own snapshot: digest, security digest, fired alerts |\n| `amon_fleet_metric` | One of 13 fleet metrics: rankings, anomalies, hotspots, burn rate, cache, health, … |\n| `amon_stale_agents` | Agents that reported and then went quiet — lookback-anchored, no window |\n| `amon_agent_rankings` | Agents ranked by `rpm` or `error_rate` |\n| `amon_list_agents` | The entity index — resolve an agent id here first (the field is `id`, see below) |\n| `amon_agent_detail` | One agent: `timeline`, `traces`, `tools`, `sessions`, `reliability`, `uptime` |\n| `amon_cost_breakdown` | Cost grouped by `model`, `agent`, or `team` |\n| `amon_forecast_cost` | Projected spend over the coming days |\n| `amon_model_compare` | Models side by side, by `cost` or by `behavior` |\n| `amon_recommendations` | The platform's own `recommendations` or the raw `insights` behind them |\n| `amon_analytics_search` | Filtered span search across 11 dimensions — the escape hatch |\n| `amon_security_overview` | Security posture: counts by severity and category, with the trend |\n| `amon_security_findings` | Dangerous commands, secret leaks, injection attempts, spawn anomalies, off-hours activity, scan findings/posture, licence violations |\n| `amon_agent_risk_profile` | Which agent is the problem — findings attributed per agent |\n| `amon_secret_files` | Credential-shaped files agents touched (paths, never contents) |\n| `amon_policy_decisions` | What the policy engine allowed or denied, and which rules fired |\n| `amon_team_metric` | 14 team metrics: contention, PR audit, yield, governance, … † |\n| `amon_delivery_report` | `bottlenecks`, `velocity-forecast`, `knowledge-silos` † |\n| `amon_dora_metrics` | The four DORA metrics † |\n\n† Licence-gated on `team_intelligence`. These three are **absent from\n`tools/list`** when the startup probe (`GET /licence/status`) reports the\nfeature denied, rather than present and erroring. If the probe cannot answer\nthey are registered anyway and may return a 403 — hiding a tool the org paid\nfor is the worse failure, because nothing surfaces it. `amon_forecast_cost` is\nNOT gated despite living in the same family.\n\n### Standalone server (`light`)\n\nAll read-only. Windowed tools take the same `hours` / `from`+`to` inputs.\n\n| Tool | What it answers | Licensed only |\n|---|---|---|\n| `amon_overview` | Headline totals: systems, sessions, tokens, cost, findings, with deltas | |\n| `amon_systems` | The machines pushing telemetry — resolve a `system_id` here first | |\n| `amon_timeseries` | Cost/token/session trend, bucketed (bucket size is server-derived) | |\n| `amon_insights` | Prompt-cache effectiveness and a findings rollup | |\n| `amon_recommendations` | Detector recommendations with estimated savings — takes no parameters | |\n| `amon_alerts` | Firing alerts and recent history — optional lower bound, no upper; omit for all history | |\n| `amon_cost_breakdown` | Cost by `models`, `agents`, `projects` or `tools` | ✓ |\n| `amon_compare_systems` | Up to four systems side by side | ✓ |\n| `amon_list_sessions` | Individual sessions: duration, model, tokens, cost, tool counts | ✓ |\n| `amon_session_threads` | Sessions grouped into work threads by workspace ‡ | ✓ |\n| `amon_session_detail` | One session's rollup, per-tool breakdown and background jobs ‡ | ✓ |\n| `amon_security_summary` | Secret leaks, injection attempts, dangerous commands — counts only | ✓ |\n| `amon_workspaces` | Activity by machine + user + repository | ✓ |\n| `amon_shadow_ai` | Unknown agent types, unattributed sessions, spawn anomalies | ✓ |\n\n‡ Carries a ~140-character excerpt of the session's first user prompt. The only\nuser-written text this dialect returns; full conversation content is not\nexposed at all.\n\nUnlike the cloud team family, the licensed-only tools are **always registered**.\nA locked deployment returns 423, and the message says the whole family is\nlocked so the model does not work through the siblings one at a time. The\nreason for the difference: installing a licence on the standalone server takes\neffect on the next request, so a tool withheld at startup would stay missing\nfor a session that could otherwise have recovered.\n\n### Chaining the per-agent tools: the field is `id`\n\n`amon_agent_detail` takes an `agent_id` argument, but the rows from\n`amon_list_agents` **carry no field of that name** — the identifier is `id`:\n\n```jsonc\n// one amon_list_agents row\n{ \"id\": \"ae938e80f71a166e2\", \"name\": \"codenotary/agentx-next\",\n  \"parent_agent_id\": \"…\", /* 43 more fields, none called agent_id */ }\n```\n\nPass that `id` value as `amon_agent_detail`'s `agent_id`. The API names the\nsame identifier `id` on the list and `agent_id` on every per-agent response —\n`/agents/{id}/timeline` echoes it back under the second name — so the round\ntrip reads oddly but is correct.\n\nWorth stating because it is invisible on an empty deployment: with no\ntelemetry the list returns zero rows, which agrees with every expectation you\nmight have about its shape.\n\n### What is deliberately not reachable\n\nSecret **values** are never returned. Neither is raw prompt or completion text:\n`/security/secret-leaks/…/reveal`, `/traces/:id/spans/:id/content`,\n`/traces/:id/waterfall` and `/traces/:id/conversation` are all unwired.\n\nThe last two are worth naming because they look harmless.\n`WaterfallSpan.input`/`.output` are filled from `gen_ai.prompt` /\n`gen_ai.completion`, and `ConversationTurn.content` is the message body\nverbatim — the same data as the `/content` route. Both redact per row, and both\nskip redaction entirely for `admin` and security-auditor claims, which is the\nrole the token-minting instructions above produce. So there is no \"trace\ndetail\" tool. Identify and rank traces with `amon_agent_detail view=\"traces\"`\nor `amon_fleet_metric metric=\"slow-spans\"`; reading one is the boundary.\n\nEnforcement is an allow-list, not a deny-list: `mcp/test/routes.test.ts` walks\nthe TypeScript AST for every request site and fails unless the path is one\ncomplete literal on a reviewed list.\n\n## Development\n\n```bash\nnpm ci\nnpm run typecheck\nnpm test\nnpm run build\n```\n\n### The test that matters\n\n`test/contract.test.ts` parses the query-parameter structs straight out of\n`crates/api/src` and checks every tool call against them.\n\nIt exists because **`crates/api` has zero `#[serde(deny_unknown_fields)]`**:\n\n```\ngrep -rn 'deny_unknown_fields' crates/api/src | wc -l   →   0\n```\n\nAxum's `Query<T>` silently drops unknown keys, so a misspelled or invented\nparameter returns **HTTP 200 carrying the server's default 1-hour window** —\nnever a 400. There is no runtime signal at all. A mocked-client test proves\n\"we sent what we meant to send\"; only this proves \"what we meant to send is\naccepted\".\n\nThis is not hypothetical. The original implementation plan specified\n`from_ns`/`to_ns`/`hours` as the time parameters (they do not exist; the real\nshape is `time_from`/`time_to` in Unix seconds). Every tool would have\nreturned one hour of data labelled as twenty-four, and every mock test would\nhave been green. `src/time.ts` therefore ships exactly one converter, which\nalways emits both halves of the window.\n\nThe workflow runs on changes to `crates/api/src/**` as well as `mcp/**`, so an\nAPI-side rename breaks CI rather than a released package.\n\n### Live smoke against a dev stack\n\n```bash\ncd deploy && docker compose -f docker-compose.local.yml up -d\n\n# The stack ships no licence, so mint the token from the session (see above).\ncurl -sc /tmp/amon-cookies -X POST http://localhost:8080/api/v1/auth/local/login \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"username\":\"admin@local\",\"password\":\"AgentMonLocal!1\"}'\nexport AMON_TOKEN=$(awk '$6==\"amon_session\"{print $7}' /tmp/amon-cookies)\nexport AMON_URL=http://localhost:8080\n```\n\nThen either run the server by hand:\n\n```bash\nnode mcp/dist/index.js\n```\n\nor run the automated sweep, which registers every tool module against a real\nclient and calls every tool and every enum variant:\n\n```bash\ncd mcp && AMON_MCP_LIVE=1 npm test -- test/live.test.ts\n```\n\nIt prints what it covered *and what it skipped* — an empty deployment has no\nagent to resolve, so the six per-agent views are unreachable and are reported\nrather than silently passed over.\n\nTake that skip seriously rather than as a footnote. It is what hid the\n`id`/`agent_id` mismatch above through four rounds of review: with zero rows\nthe list agrees with any assumption about its shape, and only a run against a\ndeployment with real telemetry disagrees.\n\nTo confirm the window truly reaches ClickHouse rather than being dropped, call\na tool at `hours=1` and `hours=168` and check the API log: the emitted SQL\ncarries `fromUnixTimestamp64Nano(...)` bounds whose span must match what you\nasked for.\n\n## License\n\nApache-2.0\n","readmeFilename":"README.md","_rev":"1-7a9b31175f143df78a2a007c93b665cc"}