# Scanner methodology Source: https://docs.armature.tech/agent-discoverability/scanner-methodology What the Agent Discoverability scanner checks, why each check matters, and how results work The Agent Discoverability scanner tests one public domain. It checks whether agent systems can reach, identify, and find the product's official public surfaces. The scan is a static audit. It does not run Claude, Codex, or another live agent. It does not measure whether an agent selects the product during a task. ## How the scan works 1. We normalize the submitted domain and require public HTTPS. 2. We read the homepage, robots rules, sitemap, selected documentation pages, and public discovery files. 3. We check supported public registries and directories. 4. We run two Exa searches. One searches for the brand. One searches for official developer resources. 5. We evaluate the same 45 deterministic tests against the collected evidence. We do not assign a score or grade. The report shows the full result mix and the actions that can improve discoverability. ## Result meanings | Result | Meaning | | ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | | **Pass** | The scan found the expected public evidence. | | **Warning** | The surface exists, but the evidence is incomplete or weak. | | **Fail** | The expected public evidence is missing or invalid. | | **Not applicable** | The product does not advertise the related surface. For example, MCP checks do not apply when the product does not advertise an MCP server. | | **Not tested** | The scan did not collect enough evidence for a safe result. | | **Error** | The scanner could not finish the test. This is a scanner-side result, not a failure by the website. | ## Agent access and indexability These 22 tests check whether agent systems can reach and read official public content. | Test | What we verify | Why it matters | | ------------------------------------- | ---------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | Public homepage reachability | The HTTPS homepage returns useful public content. | Agents need a stable first entry point. | | Redirect outcome | Entry URLs resolve to one related canonical HTTPS host. | Agents must keep the correct product identity through redirects. | | Server-rendered content | The initial HTML contains the product name, description, and main heading. | Many crawlers do not run page JavaScript. | | Search-crawler robots policy | `robots.txt` permits supported search crawlers at public paths. | Search indexes cannot retrieve blocked content. | | User-triggered-agent robots policy | `robots.txt` permits `ChatGPT-User` and `Claude-User` at public paths. | User-requested agent visits need explicit access. | | Crawler user-agent reachability | Approved crawler user agents receive the same useful page as a normal browser. | Bot challenges and empty bot pages can hide the product. | | Sitemap presence | A sitemap exists at a standard path and is declared in `robots.txt` when available. | A sitemap gives crawlers a stable page inventory. | | Sitemap URL validity | Sampled sitemap URLs are public, safe, and reachable. | Broken or unrelated entries reduce discovery quality. | | Sitemap freshness | Sitemap entries use valid `lastmod` dates when dates are present. | Freshness data helps indexes select current pages. | | Documentation linked from the website | The homepage links to public docs with a clear label. | Agents need a direct route from product identity to product knowledge. | | First-party documentation domain | External docs are linked by the official homepage and keep the same product identity. | The homepage link proves that an external docs host is official. | | Documentation page sample | The scan can read linked overview, setup, and reference pages. | A single docs page is not enough for product use. | | Public documentation access | Sampled docs work without sign-in, CAPTCHA, consent wall, or bot challenge. | Public discovery fails when core knowledge needs a session. | | `llms.txt` presence | `/llms.txt` or `/.well-known/llms.txt` returns real text or Markdown. | The file gives agents a compact map of official resources. | | `llms.txt` structure | The file has a title, a short product description, and grouped official links. | Clear structure helps agents select the right source. | | `llms.txt` linked pages | Sampled links are public, valid, and tied to verified product surfaces. | A map is useful only when its destinations work. | | Agent instruction file | `/agents.md` explains purpose, supported tasks, limits, and official resources. | Agents need clear product-specific operating guidance. | | Advertised Markdown alternate | HTML or HTTP headers advertise a valid Markdown version. | Agents can choose a compact, machine-readable representation. | | Markdown content negotiation | `Accept: text/markdown` returns Markdown for the same page and sets the correct headers. | One URL can serve both browser and agent formats. | | HTTP discovery links | HTTP `Link` entries expose at least two useful first-party resources. | Protocol-level links help clients find machine-readable surfaces. | | Canonical page metadata | Canonical URL, title, description, and Open Graph data use one product identity. | Consistent identity helps search systems verify the official source. | | Retrieval size | Sampled pages stay within a bounded normalized token size. | Focused pages are easier and cheaper for agents to retrieve. | ## Agent-facing distribution surfaces These 21 tests check whether the product appears where agents and developers look for tools. Surface-specific tests become **Not applicable** when the product does not advertise that surface. | Test | What we verify | Why it matters | | --------------------------- | -------------------------------------------------------------------------------------------- | ------------------------------------------------------------ | | ARD catalog | `/.well-known/ai-catalog.json` is valid and lists public first-party resources. | A standard catalog gives agents one discovery endpoint. | | ARD entry validity | Catalog items have stable IDs, names, media types, and working content URLs. | Valid entries let clients use the catalog without guessing. | | Agent Skills index | The standard index links to useful public skill artifacts. | Skills can tell agents how to use the product. | | A2A agent card | The standard agent card has a consistent product identity and useful capabilities. | A2A clients need verified metadata before interaction. | | Public OpenAPI document | A valid OpenAPI 3 document is public at a standard or linked URL. | Agents can discover and understand the public API. | | API catalog | The RFC 9727 catalog links to public first-party API descriptions. | A catalog lets clients find several official APIs. | | Public MCP advertisement | An advertised MCP server has a public endpoint and identity tied to the domain. | Agents need proof that an MCP endpoint is official. | | Official MCP Registry | The advertised MCP server has a registry entry tied to the domain or verified repository. | The registry is a common MCP discovery channel. | | OpenAI Apps directory | An advertised OpenAI app has an official directory result with verified product links. | Directory presence helps ChatGPT users find the integration. | | Claude Connectors directory | An advertised Claude connector has an official directory result with verified product links. | Directory presence helps Claude users find the integration. | | Vercel Marketplace | A matching marketplace result links to official product surfaces. | The marketplace is a developer discovery channel. | | Context7 | A matching Context7 project links to the official domain or repository. | Context7 can surface product documentation to coding agents. | | Smithery | An advertised MCP server has a matching, identity-verified Smithery result. | Smithery is an MCP discovery channel. | | mcp.so | An advertised MCP server has a matching, identity-verified mcp.so result. | mcp.so is an MCP discovery channel. | | Glama | An advertised MCP server has a matching, identity-verified Glama result. | Glama is an MCP discovery channel. | | PulseMCP | An advertised MCP server has a matching, identity-verified PulseMCP result. | PulseMCP is an MCP discovery channel. | | skills.sh | A matching skill links to the verified repository and domain. | Skill directories can distribute agent instructions. | | Official GitHub repository | The homepage or docs link to a public repository with matching identity. | Repositories are a major source for coding agents. | | Repository agent files | The verified repository includes relevant `AGENTS.md`, `SKILL.md`, or discovery links. | Agent files expose guidance where coding agents work. | | Official packages | Advertised packages link back to verified official surfaces. | Package registries are a common developer discovery path. | | Developer portal | A public developer entry page links to setup, reference, examples, updates, and support. | One clear entry page reduces search and navigation work. | ## Agent-oriented search presence These two tests use Exa as an agent-oriented search index. Each search stores at most 10 result rows with URL, title, and an identity verdict. | Test | What we verify | Why it matters | | ------------------------- | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------- | | Brand search | A search for the exact brand returns an official first-party surface near the top. | Agents often start with a product or company name. | | Developer-resource search | A search for the brand with `developer docs API MCP` returns official developer resources. | Agents need to find implementation content, not only marketing pages. | The scan does not generate category queries, problem queries, comparison queries, or agent selection experiments. Those are separate Agent Discoverability runs. ## Recommendations Every warning and failure includes a direct action. The report ranks the most important actions first. It also keeps the complete test table so you can inspect the result, evidence, and action for each test. ## Cache and public results A completed result stays fresh for exactly 24 hours. A repeat scan during that period returns the same result and starts no new work. An expired result stays idle until a user submits the domain again. Completed results can appear in the public recent-results list. The scanner does not publish failed runs, internal IDs, request identifiers, raw provider records, or scanner secrets. ## Safety limits The scanner makes read-only requests to public HTTPS resources. It blocks private, local, link-local, metadata, and other prohibited IP ranges. It rechecks DNS and every redirect to prevent rebinding. Requests have strict limits for time, redirects, response bytes, decompression, and total request count. The scanner never sends target credentials. It does not create accounts, make purchases, or call state-changing MCP tools. # AI Traffic Source: https://docs.armature.tech/ai-traffic/overview See which AI crawlers visit your website and which pages they request AI Traffic records requests from AI assistants, search crawlers, and training crawlers. It runs on your server, edge function, or proxy. Browser analytics cannot record crawlers that do not run JavaScript. ```mermaid theme={null} flowchart LR A["Crawler requests a page"] --> B["Your server, edge, or proxy"] B --> C["Your page response"] B -. "small background event" .-> D["Armature"] D --> E["Classify and verify"] E --> F["AI Traffic dashboard"] ``` Armature shows: * Total AI requests and verified requests. * Traffic over time by purpose, AI company, or bot. * Top crawlers and top pages. * Recent requests and verification status. * Optional non-AI bot traffic. The package does not change your page response. It sends crawler events in the background. Copy one prompt into Codex, Claude Code, Cursor, or another coding agent. Put a programmable proxy in front of Mintlify, GitBook, ReadMe, or static HTML. AI Traffic supports Vercel, Cloudflare, Next.js, Express, Hono, and generic Fetch handlers. # Install with an AI agent Source: https://docs.armature.tech/ai-traffic/quickstart Copy one prompt to install Armature AI Traffic ## Copy this prompt ```text theme={null} Install Armature AI Traffic in this project. Use @armature-tech/ai-traffic and follow https://docs.armature.tech/ai-traffic/agent-install. Complete the code, secret, deployment, and production test. Ask me only for values or access that you cannot get. Do not expose the write key or add client-side tracking. ``` The agent does the rest. Armature gives you the write key and adds your production hosts to the site. # Connect your agent Source: https://docs.armature.tech/armature-mcp/connect Query your Armature analytics from Claude Code, Cursor, or any MCP client Armature is itself an MCP server. Connect it to your coding agent and ask things like *"what are my top issues this week?"*, *"show me a failed session for bulk refunds"*, or *"which use cases have the worst pass rate?"* — the agent reads your live analytics through read-only tools. On workspaces with [Evaluation](/evaluation/overview), the same connection also lets the agent run your eval suites and read the verdicts. ## Endpoint | Region | URL | | ------ | ---------------------------------- | | US | `https://mcp.armature.tech/mcp` | | EU | `https://eu.armature.tech/api/mcp` | Connect to the region your workspace lives in. The snippets below use the US endpoint — EU workspaces swap in the EU URL. ## Authentication Two options: * **OAuth (recommended)** — connect without copying a key: your client is challenged on first contact and walks you through authorization in the browser. Active grants are listed and revocable on the **Connect Armature MCP** page in the dashboard. * **API key** — mint a workspace key at **Settings → API keys** and send it as a bearer token. Keys are shown once at creation. ## Set up your client The dashboard's **Connect Armature MCP** page generates ready-to-paste, pre-authenticated snippets for your client. The shapes: ```bash theme={null} claude mcp add --transport http armature https://mcp.armature.tech/mcp \ --header "Authorization: Bearer " ``` With OAuth, omit the header — Claude Code runs the authorization flow on first use: ```bash theme={null} claude mcp add --transport http armature https://mcp.armature.tech/mcp ``` Add to `.cursor/mcp.json` (or use the one-click install link from the dashboard): ```json theme={null} { "mcpServers": { "armature": { "url": "https://mcp.armature.tech/mcp", "headers": { "Authorization": "Bearer " } } } } ``` ```bash theme={null} code --add-mcp '{"name":"armature","type":"http","url":"https://mcp.armature.tech/mcp","headers":{"Authorization":"Bearer "}}' ``` ```bash theme={null} codex mcp add armature \ --url https://mcp.armature.tech/mcp \ --header "Authorization: Bearer " ``` * **Claude**: Settings → Connectors → **Add custom connector**, with the endpoint URL. OAuth is handled in the flow. * **ChatGPT**: Settings → Connectors → Advanced → add the endpoint as a connector. Treat API keys like passwords. The server rejects secret-shaped values in tool arguments, but the bearer token itself belongs in your client's MCP configuration — never in a prompt. ## Try it Once connected, ask your agent: > Rank this workspace's issues by blast radius, open the worst one, and show me a session that hits it. That prompt ships with the server as `triage_issues` — it exercises the whole read surface. The full tool list is on the [next page](/armature-mcp/tools). # Tool reference Source: https://docs.armature.tech/armature-mcp/tools Every tool the Armature MCP exposes, for analytics and evaluation All tools are scoped to your workspace and its retention window. Responses include both readable JSON and structured content, and mirror what the dashboard shows. The analytics tools are **read-only** against your data — the one exception, `request_capability`, records an unmet-capability signal and changes nothing else. Workspaces with [Evaluation](/evaluation/overview) enabled see six more tools, listed under [Evaluation tools](#evaluation-tools) below. Two of them start eval runs, and they are the only tools here that change anything. ## `list_mcp_servers` Your registered production MCP servers, with connection metadata. No secrets are ever returned. ## `list_sessions` Reconstructed agent sessions, newest first — each with its primary use case and latest judged outcome. Mirrors the [Sessions](/dashboard/sessions) page. | Parameter | Description | | ---------------------- | ------------------------------------------------------------------------------------ | | `range` | Time range to query. | | `view` | `all`, `succeeded`, `partial`, `failed`, `high_frustration`, `repeat`, or `flagged`. | | `useCaseId` / `intent` | Filter to one use case or intent phrasing. | | `server` | Filter to one MCP server. | | `limit` | Max sessions to return. | ## `get_session` One session end-to-end: the intent, every call in order, classification, and the judge's verdict. This is [session replay](/dashboard/sessions#session-replay) as data. | Parameter | Description | | ----------- | ----------- | | `sessionId` | Required. | ## `list_use_cases` Use-case clusters with volume, pass rate, and share of traffic. Mirrors [Use cases](/dashboard/use-cases). On young workspaces it falls back to raw-intent grouping and says so. | Parameter | Description | | --------- | ----------------------------------------------- | | `range` | Time range to query. | | `sort` | `volume`, `frustration`, `failure`, or `trend`. | | `server` | Filter to one MCP server. | | `limit` | Max use cases to return. | ## `list_issues` Issues ranked by how many sessions they affect, with session KPIs, daily pass-rate buckets, cause category, and affected use cases. Mirrors [Issues](/dashboard/issues). | Parameter | Description | | --------- | ------------------------- | | `range` | Time range to query. | | `status` | `open` or `archived`. | | `server` | Filter to one MCP server. | ## `get_issue` One issue in full: the root cause, the sessions, tools, use cases, and clients it affects in the range, and example sessions to replay. | Parameter | Description | | ----------------- | ----------------- | | `issueId` | Required. | | `range`, `server` | Optional scoping. | ## `request_capability` Agents call it when the user needs something no listed tool can do. The call is acknowledged and recorded as [unmet demand](/dashboard/use-cases#unmet-demand) — it's the same tool the [analytics SDK injects](/sdks/telemetry#the-request_capability-tool) into instrumented servers. | Parameter | Description | | ------------ | ------------------------------------------------------------ | | `capability` | Required. The capability the agent needed and couldn't find. | ## Evaluation tools These six tools appear on workspaces with [Evaluation](/evaluation/overview) enabled. They cover the full loop an agent needs: find the suite, run it, and read the verdict. The two `run_*` tools require the editor, admin, or owner role. ### `list_eval_suites` Your active eval suites, each with the MCP server it tests and the number of eval cases it would actually run. Start here to get a `testSuiteId`. The case count reflects what **Run suite** dispatches, so archived cases are already excluded. | Parameter | Description | | ------------- | --------------------------------------------------------------------------- | | `mcpServerId` | Restrict to one server's suites. Omit to list every suite in the workspace. | ### `list_eval_cases` The eval cases in one suite, exactly the set `run_eval_suite` dispatches. Each case carries its criteria count, schedule type, the other suites it belongs to, its last outcome, and a pass rate over recent runs. Runs the judge could not score are excluded from that rate, never counted as failures. | Parameter | Description | | ------------- | --------------------------------------------------------------------------------------- | | `testSuiteId` | Optional when the workspace has exactly one active suite; required when it has several. | | `mcpServerId` | Narrows suite inference to one server when `testSuiteId` is omitted. | ### `get_eval_case` One eval case in full: what the tester is instructed to do, the criteria it is judged against, its tool policy and limits, the suites it belongs to, and its recent individual runs with outcome, score, duration, and any error. Use it to understand why a case failed before changing anything. | Parameter | Description | | ------------ | --------------------------------------------------------- | | `evalCaseId` | Required. | | `runLimit` | How many recent runs to include, up to 20. Defaults to 5. | ### `run_eval_case` Starts one individual run of one case, on one target. When the case has exactly one active target, that target runs; when it has several, the case's default model runs. To cover every configured target, run the case from the dashboard or pass `testerTargets` to the [CI endpoint](/evaluation/ci). The tool returns the run id immediately and the run executes asynchronously, so poll `get_eval_case` for the outcome. | Parameter | Description | | ------------ | --------------------------------------------------------------------------------- | | `evalCaseId` | Required. | | `reason` | Why this run was triggered. The reason is shown next to the run in the dashboard. | ### `run_eval_suite` Starts one suite run over every case in a suite, the same action as **Run suite** in the dashboard. Membership is snapshotted at dispatch. The tool returns the suite run id and how many cases were queued; poll `get_suite_run` for the result. | Parameter | Description | | ------------- | ----------------------------------------------------------------- | | `testSuiteId` | Optional when the workspace has exactly one active suite. | | `mcpServerId` | Narrows suite inference when `testSuiteId` is omitted. | | `name` | Optional label for the suite run, for example "pre-release v2.3". | ### `get_suite_run` One suite run: overall status, how many cases completed and passed, whether the result still matches the suite's current membership, and the per-case outcomes with each underlying run. Omit `suiteRunId` to read the latest suite run, which answers "is this MCP server ready to ship right now". | Parameter | Description | | ------------- | ------------------------------------------------------------------------------- | | `suiteRunId` | Omit to read the suite's latest run. | | `testSuiteId` | Used only when `suiteRunId` is omitted. Optional with exactly one active suite. | | `mcpServerId` | Narrows suite inference when both ids are omitted. | ## Prompts **`triage_issues`** — "Rank this workspace's issues by blast radius, open the worst one, and show a session that hits it." A guided tour of `list_issues` → `get_issue` → `get_session`. # Issues Source: https://docs.armature.tech/dashboard/issues Recurring problems grouped by root cause, ranked by impact **Issues** is an inbox of recurring problems your agents hit, grouped by root cause and ranked by affected sessions. Work the top of the list, and archive what's fixed. These are product failures, not just exceptions — an agent looping on a missing auth scope, search missing the word "refund", an export truncated by pagination. Armature finds them **even when every API response was 200 OK**. ## The inbox Two tabs — **Open** and **Archived** — with counts. Each issue row shows: | Column | Meaning | | ----------------------- | -------------------------------------------------------------------------------------------------------------------------- | | **Issue** | Title, one-line summary, and a cause category pill (schema change, error message, tool description, performance, auth, …). | | **# affected sessions** | How many sessions hit this issue in the selected range, with a comparison bar. | | **Last seen** | When a session last hit this issue (or when it was archived). | Filter by category, search by title or text, and sort stays impact-first: *ranked by affected sessions*. ## Issue detail Opening an issue shows the root cause, the sessions, tools, use cases, and clients it affects in the range, and example sessions. Jump into a [session replay](/dashboard/sessions) to watch a real user hit the problem, then fix it at the source. ## Lifecycle * **Detection** — when a session fails, models extract concrete problem statements ("`search_invoices` returns empty results when the query includes a date filter"). Statements that recur across sessions are clustered into one issue with a generated title, summary, and cause category. * **Archive** — click **Archive** when an issue is fixed; **Restore** brings it back. Issues that stop recurring for three weeks archive automatically, so the open tab reflects what's happening *now*. # Overview Source: https://docs.armature.tech/dashboard/overview Sessions, pass rate, and issues at a glance **Overview** is your workspace's home. It answers "how is my product doing through agents?" in one screen, for the time range you pick — 24 hours, 7, 30, or 90 days — across all servers or one. ## Headline metrics Three cards, each with a sparkline and a trend against the previous period: | Metric | What it measures | | ------------- | --------------------------------------------------------------------------------------------------- | | **Sessions** | Tracked sessions in the selected time range. | | **Pass rate** | Successful judged sessions divided by all judged sessions. The delta is shown in percentage points. | | **Issues** | Recurring problem clusters grouped by root cause; the sparkline shows failed-session volume. | New workspaces show **NEW** instead of a delta until a full comparison window exists. ## Use cases and issues, side by side * **Use cases by volume** — your top five use cases as ranked bars. Rows open the use-case detail in place; unmet demand is badged **UNMET**. While the classifier is still warming up on a new workspace, the panel is labeled *provisional*. * **Top issues by impact** — the open issues affecting the most sessions. Rows open the issue detail with the cause and the sessions it affects. Both panels link through to their full pages: [Use cases](/dashboard/use-cases) and [Issues](/dashboard/issues). ## Recent sessions A feed of the latest sessions grouped by day, each row showing the outcome dot, the session's goal, call count and duration, and the client that drove it (Claude, ChatGPT, Cursor, …). Click any row to open the [session replay](/dashboard/sessions). ## Before data arrives A fresh workspace shows a connect banner instead of charts: mint an ingest key in **MCP Setup**, wire in the SDK, and the Overview fills with live sessions, use cases, and issues. See the [Quickstart](/quickstart). # Sessions & replay Source: https://docs.armature.tech/dashboard/sessions Every conversation your MCP sees, scored and replayable Every conversation your MCP sees lands under **Sessions**, newest first. Open one to read the agent's goal, tool calls, outcome, and the reconstructed conversation. ## The session list Sessions are goal-phrased — you scan *what users wanted*, not session IDs. Each row shows an outcome dot, the session goal, and the client it came from. Filter by outcome with one click: | Filter | Meaning | | ------------- | ------------------------------------------------- | | **All** | Everything, newest first. | | **Succeeded** | The judge found the user got what they asked for. | | **Partial** | Some of the ask was delivered, some wasn't. | | **Failed** | The user didn't get what they came for. | You can also filter by use case, and deep links from [Use cases](/dashboard/use-cases) and [Issues](/dashboard/issues) arrive here pre-filtered — the active filter shows as a clearable chip. Some sessions show no outcome yet: judging runs after a session has been idle for a few minutes, and sessions without enough evidence to judge stay unscored (they're excluded from pass rate). ## Session replay Opening a session gives you two views: **Overview** — the verdict card (outcome, score, and the judge's reasoning) above a waterfall of every tool call with timing and status. Failures and errors are visible exactly where they happened, including tool results your server returned as errors. **Conversation** — the session reconstructed as a dialogue: the user's asks as they evolved turn by turn, the agent's thinking on the left, and the calls it made in between. This is where `user_intent` and `agent_thinking` reported by the SDK surface — see [What the SDK captures](/sdks/telemetry). When something breaks, you see exactly where: which turn, which call, what the agent believed at the time, and what your server returned. ## How outcomes are decided Once a session goes idle, a judge model reads it end-to-end and scores whether the user got what they asked for. The verdict includes the outcome label, a score, and the evidence. Pass rate everywhere in the dashboard is **successful ÷ judged** sessions. # Use cases Source: https://docs.armature.tech/dashboard/use-cases What your users come to do — ranked by volume and success **Use cases** answers the product question behind the traffic: *what are people actually trying to do?* Armature's models read every session, capture the user's intent, and group semantically similar intents into named use cases. ## The ranking Each use case shows: | Column | Meaning | | -------------------- | -------------------------------------------------------- | | **Volume** | Sessions in this use case for the selected range. | | **Pass rate** | How often those sessions succeed. | | **Share of traffic** | This use case's slice of all sessions. | | **Frustration** | Low / medium / high, rolled up from per-session signals. | A use case with high volume and a low pass rate is usually the first thing to fix. ## Unmet demand Use cases are discovered from what users *ask for*, not from what your tools support, so demand you don't serve yet still shows up, badged **UNMET**. For example, a "Bulk refunds" use case appears even when no refund tool exists. Agents can also report unmet needs directly: the SDK ships an optional [`request_capability` tool](/sdks/telemetry#the-request_capability-tool) whose calls land here. ## Drilling in Click a use case to see its sessions and stats; jump straight into [session replay](/dashboard/sessions) to watch real conversations from that use case. The Sessions page arrives pre-filtered. ## How grouping works * Every session's intent is embedded and matched against existing use cases; close matches join, the rest wait in an **Others** bucket. * When enough similar intents accumulate, they're promoted into a new named use case automatically — no manual taxonomy to maintain. * On a brand-new workspace, the Overview panel is labeled *provisional* while the first clusters form; expect named use cases to stabilize as volume comes in. # Workspace & settings Source: https://docs.armature.tech/dashboard/workspace Servers, keys, usage, and your team ## MCP Setup **MCP Setup** is where servers get registered and connected. For each server: * **Add MCP server** registers the target (admin-only). * **Connect SDK analytics** mints the server's **ingest key** — shown once, then identified by its region-marked prefix (`ami_us_…` / `ami_eu_…`). The dialog also shows your region's ingest URL: set both as `ANALYTICS_INGEST_API_KEY` and `ANALYTICS_INGEST_URL` in that server's deployment and instrument it with the SDK. EU workspaces must keep the URL — the SDK defaults to the US endpoint. * Each key card shows its prefix, creation time, call count, and last-used time. **Rotate** issues a replacement; **Revoke** stops ingestion immediately. One ingest key per server keeps rotation blast-radius small and attribution clean. ## API keys **Settings → API keys** manages your workspace's **Armature API keys** — used to connect *outbound*, for example authenticating your coding agent against the [Armature MCP](/armature-mcp/connect) or triggering [eval runs from CI](/evaluation/ci). Keys are shown once at creation. The two key types differ by direction: **ingest keys** let your servers send data *into* Armature, and **API keys** let your tools read analytics *out of* Armature and trigger eval runs. ## Usage **Settings → Usage** shows sessions used this month against your plan's monthly limit, plus your data retention window. On the Free plan the sidebar also shows a live session meter. If you pass the monthly limit, **nothing is dropped** — new sessions are still captured; the dashboard shows an over-limit banner and history is limited to your retention window. For higher limits and longer retention, use the **Upgrade plan** contact link or see [pricing](https://armature.tech/#pricing). ## Team Invite teammates from the account menu (**Invite team members**). Organization details and your profile live under **Settings → Organization** and **Profile**. # Run evals from CI Source: https://docs.armature.tech/evaluation/ci Trigger eval runs from your pipeline and read the verdict Continuous integration (CI) is the pipeline that checks every change you push. Armature exposes an HTTP API so that pipeline can start an eval run and read the verdict, which lets you gate a deploy on your eval cases. The API triggers individual eval cases, one request per case. Suite runs start from the dashboard or from the `run_eval_suite` tool on the [Armature MCP](/armature-mcp/tools#evaluation-tools); there is no HTTP endpoint for them. ## Authenticate Mint an API key under **Settings → API keys** in the dashboard. Triggering runs requires a key from a member with the editor, admin, or owner role. Send the key as a bearer token on every request. US workspaces call `https://app.armature.tech`, and EU workspaces call `https://eu.armature.tech`. ## Trigger a run ```bash theme={null} curl -X POST "https://app.armature.tech/api/armature/v1/workflows//runs" \ -H "Authorization: Bearer $ARMATURE_API_KEY" \ -H "Content-Type: application/json" \ -d '{"reason": "release 2.3 gate"}' ``` The API keeps the older route name `workflows`: a workflow id is an eval case id. Your agent can read case ids with `list_eval_cases` on the [Armature MCP](/armature-mcp/tools#evaluation-tools), and the dashboard puts the id in every case link (the `drawer=workflow:` part of the URL). The endpoint answers `202` right away with the run's id, and the run executes asynchronously. Runs started this way appear in the run history tagged **ci**. Useful body fields: | Field | What it does | | ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `reason` | The reason is shown next to the run in the dashboard, so give it your release or commit reference. | | `idempotencyKey` | The key makes a retried request return the same run instead of starting another one. It works when the request dispatches a single run. | | `testerTargets` | A list of `{ "modelId": "" }` entries, one run per entry. Each model must be one of the case's configured targets. Without this field, the case runs on one target: its single active target, or its default model when it has several. | ## Read the verdict Poll the run until its status is terminal, then read the evaluation: ```bash theme={null} curl "https://app.armature.tech/api/armature/v1/runs/" \ -H "Authorization: Bearer $ARMATURE_API_KEY" curl "https://app.armature.tech/api/armature/v1/runs//evaluation" \ -H "Authorization: Bearer $ARMATURE_API_KEY" ``` The first call returns the run with its status and score. The second returns the judge's verdict in full: every criterion mark with its evidence. Fail your pipeline step when the outcome is not **Passed**, or apply your own threshold from the score. One caution for strict gates: **Passed** means no applied criterion failed. The judge marks a criterion as not applicable when the run gave it nothing to check, and such a mark does not lower the score. When the gate must confirm every required behavior, check each criterion mark in the evaluation response instead of the outcome alone. [Suites, runs, and scoring](/evaluation/suites-and-runs#the-judge) explains the rule. ## Or let your agent do it The same loop works through the [Armature MCP](/armature-mcp/tools#evaluation-tools) without writing the HTTP calls yourself: `run_eval_suite` starts a suite run, and `get_suite_run` answers whether the suite is ready to ship. This fits a coding agent that just changed your MCP server and wants to verify the change before opening a pull request. ## Limits The trigger endpoint rate limits bursts, so trigger runs one by one rather than in a tight loop. Runs started from CI count toward the same [included runs](/evaluation/suites-and-runs#included-runs) as every other run. # Eval cases Source: https://docs.armature.tech/evaluation/eval-cases Write the user goals your MCP server must keep handling An eval case describes one job a user brings to your product, and how to judge whether your MCP server handled it. ## What a case contains | Part | What it does | | ------------ | -------------------------------------------------------------------------------------------- | | **Prompt** | The prompt tells the tester agent what the user wants. | | **Criteria** | The criteria are the checks the judge marks after the run. Each one is required or optional. | | **Targets** | The targets set which model and harness the tester runs as. | | **Schedule** | The schedule decides whether the case also runs on its own. New cases start as manual. | | **Suites** | The memberships name the [eval suites](/evaluation/suites-and-runs) this case runs in. | ## Write the prompt as a user goal The tester agent receives your prompt and your MCP server's tools, and works toward the goal on its own. So write the goal, not a click path: > Find the March invoices for the customer Acme Corp and export them as a CSV file. The agent decides which tools to call, in what order, and how to react when a call fails. This tests whether your tools let an agent succeed, not whether one fixed sequence still executes. ## Write criteria the judge can verify The judge marks every criterion as pass, partial, or fail, and links each mark to evidence in the trace. A criterion the run never exercises is marked not applicable and does not count toward the score, so write criteria the run always reaches. Mark a criterion as required when the case must fail without it. Five rules make criteria reliable: 1. Write each criterion as an outcome someone could check from the outside, not as a style preference. "The export contains 3 invoices" works. "The agent communicates clearly" does not. 2. Test one behavior per criterion. When a criterion fails, it should point at one missing behavior. 3. Spell out the side effects the tester must avoid, for example "the tester does not modify any invoice". 4. Name the evidence you expect: a table row, a tool call status, a resource id. 5. Use data names scoped to the test, such as run-specific customer names, so cleanup and diagnosis can tell test data from real data. ## Pick the targets A target is a model running in an agent harness. A harness is the agent product that drives the model, and it shapes how the model reads schemas, retries, and recovers. New targets use a Claude model in Claude Code or a GPT model in Codex. Your plan sets how many targets one case can run: 1 on Free, 2 on Starter, 4 on Pro, and no cap on Enterprise. Give an important case more than one target. A change that passes in Claude Code and fails in Codex is exactly the kind of regression a single target hides. ## Create cases three ways * **From the Evaluation pages.** Click **Create eval case**, then **Create and run** or **Create without running**. You pick the suites the case joins as you create it. * **From a use case.** If you use Armature analytics, every use case drawer carries a **Create eval** action. Armature pre-fills the prompt and criteria from real sessions in that use case, and you review the draft before saving. * **From an issue.** Issue drawers carry the same **Create eval** action. The draft becomes a regression case: it replays the situation that failed and checks that the problem stays fixed. ## Schedules New cases start manual: they run when you click run, when a suite run includes them, or when [CI triggers them](/evaluation/ci). Add a schedule to a case and it also runs on its own at the interval you pick. Your plan sets how many cases can carry a schedule (3 on Starter, 25 on Pro, no cap on Enterprise) and the shortest interval (5 minutes, 1 minute, and 30 seconds respectively). Free workspaces run cases by hand or from CI. # Evaluation Source: https://docs.armature.tech/evaluation/overview Run real agent sessions against your MCP server and judge the results Evaluation answers one question before you ship: does my MCP server still handle the jobs users bring to it? You describe a user goal, a real agent tries to reach that goal through your tools, and a judge model checks the result against criteria you wrote. Armature enables Evaluation workspace by workspace during the rollout. If your sidebar does not show an **Evaluation** section yet, write to [contact@armature.tech](mailto:contact@armature.tech) and we will turn it on. ## The pieces | Term | What it means | | ------------------ | -------------------------------------------------------------------------------------------------------------------------------------------- | | **Eval case** | One scripted user goal plus the criteria that decide success. A real agent plays the user and works toward the goal against your MCP server. | | **Eval suite** | A named set of eval cases for one MCP server, for example "Product core" or "Billing". | | **Suite run** | One execution of every case in a suite. This is the check you run before shipping a change. | | **Individual run** | One execution of one case. | The sidebar's **Evaluation** section has three pages. **Eval suites** lists your suites, **Eval cases** lists every case across suites, and **Eval runs** holds the run history. Every suite, case, and run opens as an overlay with its own link, so you can share a result with a teammate. ## Before you start Evaluation runs against a deployed MCP server, so Armature must know how to reach it. Register the server with its URL in **MCP Setup**, together with credentials when it needs them. The tester connects to your server over HTTP like any other MCP client, so point it at a deployed environment rather than your laptop. ## How a run works 1. A tester agent receives your case prompt and connects to your MCP server. It works toward the goal the way a real user session would: it reads your tool list, calls tools, and reacts to the results. 2. Armature records the full trace of the run: every tool call, every result, and the agent's reasoning. 3. A judge reads the trace. The judge is a separate model that marks each of your criteria as pass, partial, or fail, links each mark to its evidence in the trace, and marks a criterion the run never exercised as not applicable. 4. The marks produce a score from 0 to 5 and an outcome: **Passed**, **Partial**, or **Failed**. [Suites, runs, and scoring](/evaluation/suites-and-runs) explains the exact rule. Runs start in three ways: you click run, a schedule fires, or your continuous integration (CI) pipeline calls the API. The run history labels each run with its trigger. ## Evaluation and analytics work alone or together Evaluation does not require Armature session analytics. You can write every eval case yourself and run suites on their own. If you do use analytics, Armature drafts eval cases for you from production evidence. Every use case and every issue in the dashboard carries a **Create eval** action: Armature pre-fills the prompt and criteria from real sessions, and you review the draft before saving it. A use case becomes a case that replays what users actually ask for. An issue becomes a regression case that proves the problem stays fixed. ## Plan limits | Plan | Targets per case | Scheduled cases | Shortest interval | Runs per month | | ---------- | ---------------- | --------------- | ----------------- | ----------------- | | Free | 1 | none | n/a | 100 finished runs | | Starter | 2 | 3 | 5 minutes | no cap | | Pro | 4 | 25 | 1 minute | no cap | | Enterprise | no cap | no cap | 30 seconds | no cap | A target is a model running in an agent harness, and each case can run against several targets. Free workspaces cannot schedule cases, but they can run cases by hand and from CI within the included 100 runs. A run only counts once it finishes, and a run that fails because of Armature does not count. ## Go deeper Learn how to write prompts and criteria that catch real regressions. See how the judge scores a run and how to read the results. Gate your releases on a suite run from your pipeline. Let your coding agent list suites, start runs, and read verdicts. # Suites, runs, and scoring Source: https://docs.armature.tech/evaluation/suites-and-runs How eval runs are produced, judged, and read ## Eval suites A suite is a named set of eval cases for one MCP server. Your first case lands in a suite named **Default suite**, and **Create eval suite** adds more, so you can group cases by purpose: the product core, one risky feature, one customer's setup. A case can belong to several suites at once, and each suite keeps its own run history. Archiving a case removes it from every suite's next run without deleting the memberships. Restore the case and its suites take it back. ## Running **Run one case** starts an individual run. **Run suite** starts a suite run, which dispatches one individual run for every case in the suite. The suite run rolls the results up into one answer: how many cases ran, how many passed, and whether the suite passed as a whole. A suite run snapshots the suite's membership when it starts. If you add or remove cases afterwards, the old result is labeled as historical instead of being presented as a result for the current suite. Runs carry their trigger in the run history: you started it by hand, a schedule fired, or your pipeline called the API and the run is tagged **ci**. ## The judge After the tester finishes, a judge reads the full trace of the run. The judge is a separate model: it never talks to your server, it only reads what the tester did. It marks every criterion in the case as pass, partial, or fail, and links each mark to its evidence, for example the tool call that satisfied the criterion or the error that broke it. When the run gave a criterion nothing to check, the judge marks it not applicable instead. The marks turn into a score through a fixed rule, so the same marks always give the same score: | Score | Meaning | | ----- | -------------------------------------------------------------------------------------------------- | | **5** | Every criterion the judge applied passed. | | **4** | Every required criterion the judge applied passed, and at least one optional criterion fell short. | | **3** | No required criterion failed, and at least one required criterion passed only partially. | | **2** | Exactly one required criterion failed. | | **1** | Two or more required criteria failed. | | **0** | The tester never engaged the MCP server. | Scores 4 and 5 read as **Passed**, 2 and 3 read as **Partial**, and 0 and 1 read as **Failed**, everywhere in the product. A criterion marked not applicable does not lower the score, even when it is required. **Passed** therefore means no applied criterion failed, not that every criterion was verified. When a release gate must confirm every required behavior, read the per-criterion marks in the run's evaluation instead of the outcome alone, and write criteria the run always exercises. ## Run states | Label | Meaning | | ------------------ | -------------------------------------------------------------------------------------------------------------------- | | **Passed** | The judge scored the run 4 or 5. | | **Partial** | The judge scored the run 2 or 3. | | **Failed** | The judge scored the run 0 or 1, or the run itself broke: the tester crashed, judging crashed, or the run timed out. | | **Not judged** | The run hit a problem on Armature's side or at the model provider, so no verdict exists. | | **Not run** | Armature declined to start the run, for example when a Free workspace has used its monthly runs. | | **Canceled** | You stopped the run before it finished. | | **Running** | The tester or the judge is still working. | | **Waiting to run** | The run sits in the queue. | Pass rates count only judged runs: **Not judged** runs are excluded, and they are never counted as failures. This keeps a problem on Armature's side from showing up as a regression in your MCP server. ## Included runs Free workspaces include 100 finished runs per month, pooled across every case, suite, and target. Past that number, new runs show **Not run** until the month resets. A run counts once it finishes, and a run that fails because of Armature does not count. Paid plans have no monthly run cap. # How Armature works Source: https://docs.armature.tech/how-it-works From scattered tool calls to replayable, scored sessions Without instrumentation, agent traffic looks like disconnected tool-call logs: `list_customers`, `send_invoice 402`, a retry, silence. You can't tell who wanted what, whether they got it, or why they left. Armature reconstructs each session. ## The SDK observes, your server behaves the same The SDK wraps your MCP server. It doesn't proxy traffic, change responses, or add latency to your tools — it records each call as it happens and posts events to Armature off the request path. If the analytics backend is unreachable, your server doesn't notice. ## Agents report intent as they work The SDK adds one optional, well-described `telemetry` field to each tool's input schema, carrying `user_intent`, `agent_thinking`, and `user_frustration`. Modern agents read tool schemas carefully and fill these in as they call your tools. The SDK strips the field out **before your handler runs**, records it, and passes clean arguments through. That's how a session shows *"user wants to bulk-refund March invoices"* next to the calls — the agent told us, in flight. Agents that ignore the fields still produce complete sessions. See [What the SDK captures](/sdks/telemetry) for the exact schema. ## Sessions are rebuilt and scored Events stream into your workspace, where Armature: 1. **Rebuilds sessions** — tool calls are grouped by MCP session and ordered into a conversation: user turns, agent thinking, calls, results. 2. **Scores outcomes** — once a session goes idle, a judge model reads it end-to-end and scores whether the user got what they asked for: **Succeeded**, **Partial**, or **Failed**, with the evidence attached. Pass rate on the dashboard is successful ÷ judged sessions. 3. **Groups use cases** — every session's intent is embedded and clustered. Recurring intents become named [use cases](/dashboard/use-cases) ranked by volume and success rate — including demand you don't support yet. 4. **Surfaces issues** — failing sessions yield concrete problem statements ("search misses 'refund' phrasing"), which are clustered by root cause into [issues](/dashboard/issues) ranked by how many sessions they affect. An issue that stops recurring for three weeks auto-archives. ## Privacy is built into the pipeline Detection models scan incoming sessions and redact PII and secrets before anything reaches storage. Previews are truncated at the SDK, actor identities are hashed on your server, and secret-shaped values are rejected at the boundary. Details in [Privacy & security](/security/privacy). ## Where the answers show up * The [dashboard](/dashboard/overview): Overview, Use cases, Issues, Sessions. * Your coding agent, via the [Armature MCP](/armature-mcp/connect) — the same analytics as read-only MCP tools. # Introduction Source: https://docs.armature.tech/index Product analytics for agent sessions Your users reach your product through Claude, ChatGPT, and coding agents — and those sessions live inside their AI client, not your UI. Armature captures every session on your MCP server, Claude Connector, or ChatGPT App, and shows you what users ask, what agents think, and how they use your product. Add the SDK, deploy, and sessions start flowing in minutes: Create an ingest key, wire in the SDK, and watch your first sessions arrive. How scattered tool calls become replayable, scored sessions with user intent. ## What you get Every conversation your MCP sees, scored on whether the user got what they asked for. Replay the full trace: the ask, the agent's thinking, and every call. Models read every session and identify what the user came to do — grouped, ranked by volume and success rate, including the use cases you don't support yet. Failures, loops, and dead ends grouped by root cause and ranked by how many users hit them — even when every API response was 200 OK. Sessions, pass rate, and issues at a glance, with trends against the previous period. ## Evaluate changes before you ship Armature also runs evals against your MCP server. You write eval cases, a real agent plays the user, and a judge scores every run against your criteria. If you use the analytics above, Armature drafts the cases for you from real use cases and issues. Run real agent sessions against your server, by hand, on a schedule, or from CI. ## Works with your stack The SDK wraps your server and changes nothing about how it behaves. If an AI client can reach your server, Armature can capture the session — Claude, ChatGPT, Cursor, Codex, Gemini CLI, and the rest. Official MCP SDK (1.x and 2.x), Mastra, fastmcp, and mcp-handler FastMCP and the official MCP SDK mark3labs/mcp-go and the official go-sdk The official PHP MCP SDK (mcp/sdk) ## Bring the answers to your agent Armature is also an MCP server. Connect it to Claude Code, Cursor, or any MCP client and ask questions like *"what are my top issues this week?"* — the agent reads your live analytics. One command adds your analytics to your coding agent. # Quickstart Source: https://docs.armature.tech/quickstart From zero to live sessions in a few minutes Three steps: create an ingest key, wire in the SDK, deploy. Sign in at [app.armature.tech](https://app.armature.tech) — [eu.armature.tech](https://eu.armature.tech) for EU workspaces — and open **MCP Setup**. 1. Click **Add MCP server** and register your server. 2. On the server's card, click **Connect SDK analytics** to mint an ingest key. The key is shown once, together with your region's ingest URL — copy both. 3. Add them to your server's deployment secrets: ```bash theme={null} ANALYTICS_INGEST_API_KEY= ANALYTICS_INGEST_URL= ``` US workspaces can omit `ANALYTICS_INGEST_URL` (the SDK defaults to the US endpoint); EU workspaces must set it. Each MCP server gets its own key. You can rotate or revoke keys from the same card at any time. The fastest path: paste this prompt into your coding agent and let it do the wiring. ```text Prompt for your coding agent theme={null} Add Armature session analytics to my MCP server. Follow the docs at https://docs.armature.tech/sdks/typescript (Python: /sdks/python, Go: /sdks/go, PHP: /sdks/php). The ingest key and URL are already set as ANALYTICS_INGEST_API_KEY and ANALYTICS_INGEST_URL in the deployment environment. ``` Or do it by hand — it's one wrapper around your server: ```typescript TypeScript theme={null} import { createMcpAnalyticsServer } from "@armature-tech/mcp-analytics"; const server = createMcpAnalyticsServer(() => { const mcp = new McpServer({ name: "customer-mcp", version: "1.0.0" }); // ...register your tools exactly as before return mcp; }); ``` ```python Python theme={null} from fastmcp import FastMCP from armature_mcp_analytics import instrument_fastmcp mcp = FastMCP("Customer MCP") instrument_fastmcp(mcp) # ...register your tools exactly as before ``` ```go Go theme={null} s, shutdown := armatureanalytics.NewMCPServer("Customer MCP", "1.0.0") defer shutdown(context.Background()) armatureanalytics.InstrumentTool(s, tool, handler) ``` ```php PHP theme={null} $builder = Server::builder()->setServerInfo('Customer MCP', '1.0.0'); $analytics = Analytics::instrument( builder: $builder, config: Config::fromEnvironment(), ); // ...add your tools exactly as before $server = $builder->build(); ``` Install commands and full options are on the per-language pages: [TypeScript](/sdks/typescript), [Python](/sdks/python), [Go](/sdks/go), [PHP](/sdks/php). Deploying to serverless? Set delivery to `await` and add the one-line session helper — see [Stateless & serverless servers](/sdks/stateless-servers). Deploy your server. As soon as an agent talks to it, sessions appear in the dashboard — the [Overview](/dashboard/overview) fills with live sessions, use cases, and issues. To sanity-check locally first, point a client at your dev server (for example `claude mcp add` with the local command) and run one conversation. You should see it under **Sessions** within a minute or two. ## Next steps Read the ask, the agent's thinking, and every call. Telemetry fields, events, size limits, and delivery. Attribute sessions to real accounts, privacy-preserved. Query your analytics from your coding agent. # Go Source: https://docs.armature.tech/sdks/go Instrument a Go MCP server with mcp-analytics-go The Go SDK instruments servers built with [mark3labs/mcp-go](https://github.com/mark3labs/mcp-go) (v0.49–v0.56) or the official [modelcontextprotocol/go-sdk](https://github.com/modelcontextprotocol/go-sdk) (v1.7.0, MCP protocol 2026-07-28 and earlier). Go 1.25.12+. ## Install ```bash mark3labs/mcp-go theme={null} go get github.com/armature-tech/mcp-analytics-go/armatureanalytics@latest ``` ```bash Official go-sdk theme={null} go get github.com/armature-tech/mcp-analytics-go/armatureanalytics/official@latest ``` ## Set your API key ```bash theme={null} ANALYTICS_INGEST_API_KEY= ANALYTICS_INGEST_URL= ``` Copy both values from the dashboard when you mint the key. US workspaces can omit `ANALYTICS_INGEST_URL` (the SDK defaults to the US endpoint); EU workspaces must set it to `https://eu.armature.tech/api/mcp-analytics/ingest`. If the key is missing, the SDK quietly no-ops — your server keeps working, and no data is sent. ## Instrument your server Create the server with `NewMCPServer` and register tools with `InstrumentTool`: ```go main.go theme={null} package main import ( "context" "fmt" "log" "time" "github.com/armature-tech/mcp-analytics-go/armatureanalytics" "github.com/mark3labs/mcp-go/mcp" "github.com/mark3labs/mcp-go/server" ) func main() { s, shutdown := armatureanalytics.NewMCPServer( "Customer MCP", "1.0.0", server.WithToolCapabilities(true), ) defer func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = shutdown(ctx) }() armatureanalytics.InstrumentTool(s, mcp.NewTool("lookup_customer", mcp.WithString("customer_id", mcp.Required()), ), func(_ context.Context, req mcp.CallToolRequest) (*mcp.CallToolResult, error) { id := req.GetArguments()["customer_id"] return mcp.NewToolResultText(fmt.Sprintf("customer %v is active", id)), nil }, ) if err := server.ServeStdio(s); err != nil { log.Fatal(err) } } ``` ```go main.go theme={null} package main import ( "context" "time" "github.com/armature-tech/mcp-analytics-go/armatureanalytics/official" "github.com/modelcontextprotocol/go-sdk/mcp" ) type LookupInput struct { CustomerID string `json:"customer_id"` } type LookupOutput struct { Active bool `json:"active"` } func main() { s, shutdown := official.NewMCPServer( &mcp.Implementation{Name: "Customer MCP", Version: "1.0.0"}, nil, ) defer func() { ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() _ = shutdown(ctx) }() official.InstrumentTool(s, &mcp.Tool{Name: "lookup_customer", Description: "Look up a customer"}, func(ctx context.Context, req *mcp.CallToolRequest, input LookupInput) (*mcp.CallToolResult, LookupOutput, error) { return nil, LookupOutput{Active: true}, nil }, ) // serve over stdio or HTTP as usual } ``` The typed `InstrumentTool` derives the input schema from your input struct automatically. ## Existing servers Attach a recorder to a server you already construct: ```go theme={null} config := armatureanalytics.EnvConfig() config.Disabled = config.APIKey == "" rec, err := armatureanalytics.NewRecorder(config) if err != nil { return err } s := server.NewMCPServer("Customer MCP", "1.0.0", server.WithToolCapabilities(true), server.WithHooks(rec.Hooks()), ) // on shutdown: rec.Close(ctx) ``` For the official SDK, use `official.NewRecorder(config)` followed by `rec.Install(server)`. Running on serverless or stateless HTTP? See [Stateless & serverless servers](/sdks/stateless-servers) and set `Delivery: armatureanalytics.DeliveryAwait`. ## Configuration ```go theme={null} config := armatureanalytics.EnvConfig() config.OnError = func(err error, batch armatureanalytics.Batch) { log.Printf("analytics delivery failed: %v", err) } s, shutdown := armatureanalytics.NewMCPServerWithConfig("Customer MCP", "1.0.0", config) ``` | Field | Default | Description | | ------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | | `APIKey` | `ANALYTICS_INGEST_API_KEY` env var (via `EnvConfig`) | API key used to authenticate ingestion. | | `EndpointURL` | US ingest endpoint | Override with `ANALYTICS_INGEST_URL` or this field; EU workspaces must use the EU endpoint. | | `Disabled` | `false` | Set `true` to disable capture entirely. | | `Delivery` | `DeliveryBackground` | `DeliveryBackground` posts on a goroutine off the request path; `DeliveryAwait` sends before returning (use in serverless). | | `Timeout` | `5 * time.Second` | Delivery timeout. | | `ActorSeed` | `Authorization` header | Function used to attribute calls to a user. See [Identifying users](/sdks/identifying-users). | | `ActorIdentifier` | `nil` | Attach a caller-provided identifier verbatim. See [Identifying users](/sdks/identifying-users). | | `Emit` | Network delivery | Replace delivery with your own function — useful in tests. Makes `APIKey` optional. | | `OnError` | `nil` (silent) | Called when delivery fails. Set this in production so ingest failures are visible. | | `CaptureTelemetry` | `nil` (on) | Point at `false` to disable conversation-derived telemetry while keeping call analytics. | | `RequestCapability` | `nil` (on) | Injects the `request_capability` tool (see [What the SDK captures](/sdks/telemetry#the-request_capability-tool)); point a `*bool` at `false` to disable. | `OnError` is silent by default in Go. Set it in production — otherwise an invalid key or network failure drops events with no signal. ## Flush on shutdown `NewMCPServer` returns a `Shutdown` function that drains pending deliveries — call it with a timeout on exit (as in the quickstart above). With a manual recorder, call `rec.Close(ctx)`. ## What's not captured The Go SDK records one `session_init` per session and every `tools/call`. On the classic protocol the `session_init` comes from the `initialize` handshake; on the 2026-07-28 stateless protocol there is no handshake, so the SDK emits it when a new session identity first appears on any method. Prompts, resources, and OAuth flows are not captured. ## Troubleshooting With the official SDK adapter, install the analytics middleware **before** your auth middleware if your `ActorSeed` reads values that auth injects into the context. Check `ANALYTICS_INGEST_API_KEY` is set (missing keys disable delivery silently by design), check `ANALYTICS_INGEST_URL` matches your workspace region — EU keys are rejected by the US endpoint — and set `Config.OnError` to surface failures. # Identifying users Source: https://docs.armature.tech/sdks/identifying-users Attribute sessions to the right user or account Armature attributes every tool call to an **actor** — a stable, privacy-preserving identifier for the user or account behind the session. ## The default Out of the box, the SDK derives an actor seed from the request's authentication, in order: 1. A configured `actorId` (if you set one — see below) 2. The request's auth info: token, client ID, API key, or principal ID 3. The `Authorization` header 4. `"anonymous"` as the last resort The seed is **SHA-256 hashed before transmission** — raw tokens and header values never leave your server. Two sessions from the same authenticated user hash to the same actor, so per-user views line up without Armature ever seeing the credential. ## Custom actor resolution If your server knows the real user (for example, after validating an OAuth token), resolve the actor yourself so sessions group by *your* user IDs: ```typescript TypeScript theme={null} createMcpAnalyticsServer(buildServer, { armature: { actorId: (context) => context?.authInfo?.principalId ?? "anonymous", }, }); ``` ```python Python theme={null} instrument_fastmcp(mcp, { "armature": { # sync or async callable, or a plain string "actor_id": lambda context: resolve_user_id(context), } }) ``` ```go Go theme={null} config := armatureanalytics.EnvConfig() config.ActorSeed = func(ctx context.Context) string { if user, ok := auth.UserFromContext(ctx); ok { return user.ID } return "" } ``` ```php PHP theme={null} $config = new Config( actorId: static fn (array $context): ?string => $context['attributes']['principal_id'] ?? null, ); ``` Whatever you return is still hashed before it is sent. ## Attach a readable identifier Hashes group sessions; they don't tell you *who* a session was. To label sessions with an identifier you choose — an internal user ID, an account slug, an email — set the identifier option (`actorIdentifier` in TypeScript and PHP, `actor_identifier` in Python, `ActorIdentifier` in Go): ```typescript TypeScript theme={null} createMcpAnalyticsServer(buildServer, { armature: { actorIdentifier: (context) => lookupUserEmail(context), }, }); ``` ```python Python theme={null} instrument_fastmcp(mcp, { "armature": { "actor_identifier": lambda context: resolve_user_email(context), } }) ``` ```go Go theme={null} config.ActorIdentifier = func(ctx context.Context) string { return userEmail(ctx) } ``` ```php PHP theme={null} $config = new Config( actorIdentifier: static fn (array $context): ?string => $context['attributes']['principal_id'] ?? null, ); ``` The value doubles as the actor seed (hashed into the actor ID) **and** is sent verbatim in a small identity event whenever it changes, so the dashboard can display it. The SDK doesn't interpret it — any non-empty string works. Unlike `actorId` seeds, the identifier is transmitted as-is by design: don't put anything in it you wouldn't show in the dashboard. In the Go official-SDK adapter, install the analytics middleware **before** your auth middleware if `ActorSeed` reads values that auth injects into the context — otherwise the actor resolves before auth has run. ## Anonymous traffic Unauthenticated servers still get useful sessions: the actor falls back to `anonymous`, and session identity (which conversation the calls belong to) is tracked separately via the MCP session ID. You lose per-user grouping, not session replay. # PHP Source: https://docs.armature.tech/sdks/php Instrument a PHP MCP server with armature/mcp-analytics The PHP SDK instruments servers built with the official PHP MCP SDK, [`mcp/sdk`](https://github.com/modelcontextprotocol/php-sdk) 0.7.x, on PHP 8.1 and newer. It captures sessions and tool calls without changing how your server behaves. ## Install ```bash theme={null} composer require armature/mcp-analytics ``` ## Set your API key The SDK reads your configuration from the environment via `Config::fromEnvironment()`: ```bash theme={null} ANALYTICS_INGEST_API_KEY= ANALYTICS_INGEST_URL= ``` Copy both values from the dashboard when you mint the key. US workspaces can omit `ANALYTICS_INGEST_URL` (the SDK defaults to the US endpoint); EU workspaces must set it to `https://eu.armature.tech/api/mcp-analytics/ingest`. If the key is missing, the SDK quietly no-ops — your server keeps working, and no data is sent. ## Instrument your server Call `Analytics::instrument()` on the builder before adding or discovering tools: ```php server.php theme={null} setServerInfo('Customer MCP', '1.0.0'); $analytics = Analytics::instrument( builder: $builder, config: Config::fromEnvironment(), ); $builder->addTool( handler: static fn (string $customerId): array => [ 'customer_id' => $customerId, 'status' => 'active', ], name: 'lookup_customer', description: 'Look up a customer.', inputSchema: [ 'type' => 'object', 'properties' => [ 'customerId' => ['type' => 'string'], ], 'required' => ['customerId'], ], ); $server = $builder->build(); try { $server->run(new StdioTransport()); } finally { $analytics->close(); } ``` Manual tools, explicit `Builder::add(...)` definitions, custom loaders, and attribute discovery are all covered — the instrumentation decorates the official registry after definitions are finalized. If your application already passes a custom `container`, `registry`, or `referenceHandler` to the builder, hand the same instances to `Analytics::instrument(...)` — otherwise the SDK installs its own and tools registered through yours could bypass the wrapper: ```php theme={null} $analytics = Analytics::instrument( builder: $builder, config: Config::fromEnvironment(), container: $container, registry: $registry, referenceHandler: $referenceHandler, ); ``` Supplying a custom registry makes `mcp/sdk` 0.7 finalize builder loaders eagerly during `build()`; loader failures stay visible rather than being hidden. ## Streamable HTTP Add the analytics PSR-15 middleware after your authentication middleware and keep the official transport defaults: ```php theme={null} use Mcp\Server\Transport\StreamableHttpTransport; use Nyholm\Psr7\Factory\Psr17Factory; use Nyholm\Psr7Server\ServerRequestCreator; $factory = new Psr17Factory(); $request = (new ServerRequestCreator($factory, $factory, $factory, $factory)) ->fromGlobals(); $transport = new StreamableHttpTransport( request: $request, responseFactory: $factory, streamFactory: $factory, middleware: [ ...StreamableHttpTransport::defaultMiddleware(), $authenticationMiddleware, $analytics->httpMiddleware(), ], ); try { $response = $server->run($transport); } finally { $analytics->flush(); } ``` `Server::run()` returns the PSR-7 response for HTTP transports — return `$response` from your framework handler, or pass it to your response emitter in a standalone script; nothing is sent to the client otherwise. If your application doesn't already provide PSR-17 factories and a PSR-7 server-request creator, install one implementation: `composer require nyholm/psr7 nyholm/psr7-server`. The middleware retains only the request headers and scalar authentication attributes needed for attribution. It never reads the request body or changes the request or response. PHP is request-scoped by design, so delivery is **awaited by default** — events are sent before the response returns. That's the right mode for PHP-FPM, Lambda, and other serverless runtimes with no extra configuration. ## Configuration `Config::fromEnvironment()` is the only path that reads `ANALYTICS_INGEST_API_KEY` and `ANALYTICS_INGEST_URL` — the bare constructor defaults `apiKey` to `null`, which disables delivery. To customize options, carry the environment values over: ```php theme={null} use Armature\McpAnalytics\Config; $environment = Config::fromEnvironment(); $config = new Config( apiKey: $environment->apiKey, endpointUrl: $environment->endpointUrl, timeoutMs: 5000, onError: static function (Throwable $error, array $batch): void { // Log the safe error code/class. Do not log the batch. }, ); ``` | Option | Default | Description | | ------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `apiKey` | `null` | API key used to authenticate ingestion. `Config::fromEnvironment()` fills it from `ANALYTICS_INGEST_API_KEY`; without a key (or a custom `emitter`) the SDK no-ops. | | `endpointUrl` | US ingest endpoint | `Config::fromEnvironment()` fills it from `ANALYTICS_INGEST_URL`; EU workspaces must use the EU endpoint. | | `enabled` | `true` | Set `false` to disable capture entirely. | | `delivery` | `DeliveryMode::Await` | Sends before the response returns. `DeliveryMode::Deferred` is accepted only with a `scheduler`. | | `timeoutMs` | `5000` | Delivery timeout in milliseconds. | | `actorId` | Derived from request auth | A string or callable used to attribute calls to a user. See [Identifying users](/sdks/identifying-users). | | `actorIdentifier` | `null` | Attach a caller-provided identifier verbatim. See [Identifying users](/sdks/identifying-users). | | `emitter` | Network emitter | Replace delivery with your own `EmitterInterface` — useful in tests. | | `onError` | `null` | Called when delivery fails. Failures never reach your tools; log only the safe `DeliveryError` fields (`errorCode`, `status`, `retryable`, `attempts`, `causeClass`). | | `captureTelemetry` | `true` | Set `false` to disable conversation-derived telemetry while keeping call analytics. | | `requestCapability` | Auto | Injects the `request_capability` tool whenever a delivery path is configured (see [What the SDK captures](/sdks/telemetry#the-request_capability-tool)). Set `false` to disable; set `true` to reserve the name — a colliding customer tool then throws during `build()`. | | `scheduler` | `null` | `SchedulerInterface` required for `DeliveryMode::Deferred`; it must guarantee the scheduled task runs. | | `logger` | PHP error log | PSR-3 logger for configuration warnings. Warning context never includes tool arguments or credentials. | The redaction callbacks `redact` and `redactEvent` are documented in the [SDK README](https://github.com/armature-tech/mcp-analytics-php#readme). Two more options live next to them: `redactSecrets` (boolean, default `true`) turns built-in credential redaction off, and `telemetryFieldMap` maps existing tool arguments onto telemetry fields. ## Flush on shutdown For long-running processes call `$analytics->close()` when the server exits (as in the stdio example above); in request-scoped runtimes call `$analytics->flush()` after the transport runs. ## Verify locally The language-independent doctor can inspect a running server, verify the telemetry contract on every tool, and authenticate your ingest key. The doctor is Node-based and, outside a JavaScript project, its MCP client package must be listed explicitly (it is an optional peer dependency, so bare `npx @armature-tech/mcp-analytics doctor` fails with `ERR_MODULE_NOT_FOUND`): ```bash theme={null} npx --yes --package=@armature-tech/mcp-analytics \ --package=@modelcontextprotocol/sdk \ mcp-analytics doctor --url http://localhost:3000/mcp ``` Use `--skip-ingest` for an offline schema check and `--json` for machine-readable output. ## Troubleshooting `mcp/sdk` 0.7 accepts closures, class/method pairs, and invokable class strings — not invokable object instances. Adapt a bare named function or invokable object with `Closure::fromCallable(...)` before passing it to `addTool()`. Keep your application's existing PSR-17 implementation, or install the Nyholm packages shown above, then pass the factories explicitly to the transport. The SDK supports PHP 8.1+ and `mcp/sdk >=0.7.0 <0.8.0`. Upgrade or constrain your application explicitly rather than bypassing Composer's platform checks. Check that `ANALYTICS_INGEST_API_KEY` is set in the server's environment (missing keys no-op silently by design), that `ANALYTICS_INGEST_URL` matches your workspace region — EU keys are rejected by the US endpoint — and pass an `onError` callback to surface delivery failures. If you construct `Config` yourself, remember the bare constructor never reads environment variables — start from `Config::fromEnvironment()`. # Python Source: https://docs.armature.tech/sdks/python Instrument a Python MCP server with armature-mcp-analytics The Python SDK instruments servers built with [FastMCP](https://gofastmcp.com) (2.x–4.x) and the official [MCP Python SDK](https://github.com/modelcontextprotocol/python-sdk) — both the 1.27+ `FastMCP` class and the 2.x `MCPServer`. Python 3.10+. ## Install ```bash FastMCP (standalone) theme={null} pip install "armature-mcp-analytics[fastmcp]" ``` ```bash Official MCP SDK theme={null} pip install "armature-mcp-analytics[mcp]" ``` The base package has no runtime dependencies; the extras pull in the framework you use (`fastmcp>=2,<5` or `mcp>=1.27,<3`). ## Set your API key ```bash theme={null} ANALYTICS_INGEST_API_KEY= ANALYTICS_INGEST_URL= ``` Copy both values from the dashboard when you mint the key. US workspaces can omit `ANALYTICS_INGEST_URL` (the SDK defaults to the US endpoint); EU workspaces must set it to `https://eu.armature.tech/api/mcp-analytics/ingest`. If the key is missing, the SDK quietly no-ops — your server keeps working, and no data is sent. ## Instrument your server Call `instrument_fastmcp` once, before registering tools: ```python server.py theme={null} from fastmcp import FastMCP from armature_mcp_analytics import instrument_fastmcp mcp = FastMCP("Customer MCP") instrument_fastmcp(mcp) @mcp.tool def lookup_customer(customer_id: str) -> dict: """Look up a customer by ID.""" return {"customer_id": customer_id, "status": "active"} mcp.run() ``` Works identically with the official SDK's FastMCP: ```python theme={null} from mcp.server.fastmcp import FastMCP from armature_mcp_analytics import instrument_fastmcp mcp = FastMCP("Customer MCP") instrument_fastmcp(mcp) ``` And with the official SDK 2.x, which renamed the server class: ```python theme={null} from mcp.server.mcpserver import MCPServer from armature_mcp_analytics import instrument_fastmcp mcp = MCPServer("Customer MCP") instrument_fastmcp(mcp) ``` Instrumentation is idempotent — calling `instrument_fastmcp` twice does not double-count. Sync and async tool handlers are both supported. ## Custom dispatcher If you handle `tools/list` and `tools/call` yourself, use the recorder directly: ```python theme={null} from armature_mcp_analytics import create_analytics_recorder analytics = create_analytics_recorder() async def lookup_customer(args, context): return {"customer_id": args["customer_id"], "status": "active"} analytics.tool( { "name": "lookup_customer", "description": "Look up a customer by ID.", "inputSchema": { "type": "object", "properties": {"customer_id": {"type": "string"}}, "required": ["customer_id"], }, }, lookup_customer, ) tools = analytics.tool_definitions() # tools/list result = await analytics.dispatch(name, args, context) # tools/call ``` Using `stateless_http=True`, or deploying to serverless? Wrap your ASGI app in `StatelessHttpSessionMiddleware` and set `"delivery": "await"`. Without the middleware, a stateless FastMCP app issues no `Mcp-Session-Id` at all, so calls arrive with no session identity: Armature falls back to heuristic grouping, separate conversations from the same caller can run together, and Client shows as Unknown. See [Stateless & serverless servers](/sdks/stateless-servers). ## Configuration Pass options as a dict under the `armature` key. Both `snake_case` and `camelCase` keys are accepted: ```python theme={null} instrument_fastmcp(mcp, { "armature": { "delivery": "background", "timeout_ms": 5000, "on_error": lambda err, batch: print("analytics delivery failed", err), } }) ``` | Option | Default | Description | | -------------------- | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `api_key` | `ANALYTICS_INGEST_API_KEY` env var | API key used to authenticate ingestion. | | `endpoint_url` | US ingest endpoint | Override with `ANALYTICS_INGEST_URL` or this option; EU workspaces must use the EU endpoint. | | `enabled` | `True` | Set `False` to disable capture entirely. | | `delivery` | `"background"` | `"background"` posts events off the request path; `"await"` sends before returning (use in serverless). | | `timeout_ms` | `5000` | Delivery timeout in milliseconds. | | `actor_id` | Derived from request auth | A string or callable (sync or async) used to attribute calls to a user. See [Identifying users](/sdks/identifying-users). | | `actor_identifier` | `None` | Attach a caller-provided identifier verbatim. See [Identifying users](/sdks/identifying-users). | | `emit` | Network emitter | Replace delivery with your own callable — useful in tests. | | `on_error` | `None` (silent) | Called when delivery fails. Failures never reach your tools — set this in production so they're visible. | | `capture_telemetry` | `True` | Set `False` to disable conversation-derived telemetry while keeping call analytics. | | `request_capability` | On | Injects the `request_capability` tool (see [What the SDK captures](/sdks/telemetry#the-request_capability-tool)); set `False` to disable. | ## Flush on shutdown With background delivery, drain pending events before the process exits: ```python theme={null} instrumentation = instrument_fastmcp(mcp) # ... on shutdown: await instrumentation.recorder.flush() ``` ## Verify locally Capture batches in memory instead of sending them: ```python theme={null} batches = [] instrument_fastmcp(mcp, {"armature": {"emit": batches.append, "delivery": "await"}}) # call a tool through the MCP client, then: event = next(e for b in batches for e in b["events"] if e["kind"] == "tool_call") assert event["metadata"]["tool_name"] == "lookup_customer" ``` ## Troubleshooting FastMCP builds the advertised schema from your function's type hints, so the SDK adds a keyword-only `telemetry` parameter to instrumented functions. If you post-process signatures or schemas yourself, make sure that parameter survives. The SDK reads the `Mcp-Session-Id` header, including on FastMCP versions that exclude it from `get_http_headers()` by default. If you run a custom ASGI stack, make sure the header reaches the app. Check that `ANALYTICS_INGEST_API_KEY` is set in the server's environment (missing keys no-op silently by design), that `ANALYTICS_INGEST_URL` matches your workspace region — EU keys are rejected by the US endpoint — and pass an `on_error` hook to surface delivery failures. # Stateless & serverless servers Source: https://docs.armature.tech/sdks/stateless-servers Keep sessions intact when every request hits a fresh instance Two things break when your server does not keep session state between requests: session identity (calls stop lining up into one session) and delivery (the platform freezes your process before background events are sent). This page applies to you if either of these is true: * You deploy to serverless (Vercel, Lambda, Cloud Run scaled to zero), so `initialize` and each tool call land on **different instances**. * You set `stateless_http=True`, or the equivalent in your SDK, even when you run a single long-lived container or process. ## How to tell you need this Open your Sessions page. Without the two steps below, you will see: * **Sessions don't line up.** Calls arrive with no session identity, so Armature falls back to heuristic grouping: separate conversations from the same caller can merge into one session, and others land as fallback sessions instead of one session per conversation. * **Client shown as Unknown.** The client name and version never reach us. Both symptoms come from the same cause, and step 2 below fixes both at once. Both fixes are one-liners. They cover clients on the classic handshake protocol, which is still most agent traffic today. Requests on the 2026-07-28 protocol revision carry no `initialize` handshake and are attributed automatically. ## 1. Use `await` delivery Background delivery relies on the process staying alive after the response. In serverless, send events before returning instead: ```typescript TypeScript theme={null} createMcpAnalyticsServer(buildServer, { armature: { delivery: "await" }, }); ``` ```python Python theme={null} instrument_fastmcp(mcp, {"armature": {"delivery": "await"}}) ``` ```go Go theme={null} config.Delivery = armatureanalytics.DeliveryAwait ``` ## 2. Resolve the session per request The SDK mints **identity-bearing session IDs** (`mcp__v__`) on `initialize` and reads them back on later requests, so client attribution survives across instances. Call `resolveStatelessHttpSession` per request and pass the results to the transport and dispatch context: ```typescript theme={null} import { resolveStatelessHttpSession } from "@armature-tech/mcp-analytics"; import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js"; const session = resolveStatelessHttpSession({ body: requestBody, headers: requestHeaders, }); const transport = new StreamableHTTPServerTransport({ sessionIdGenerator: session.sessionIdGenerator, enableJsonResponse: true, }); // with a custom dispatcher: await analytics.dispatch(name, args, { ...context, ...session.dispatchContext }); ``` Wrap your ASGI app in the dependency-free middleware. It handles minting and echoing automatically: ```python theme={null} from armature_mcp_analytics import StatelessHttpSessionMiddleware # standalone FastMCP app = StatelessHttpSessionMiddleware( mcp.http_app(stateless_http=True, json_response=True) ) # official MCP Python SDK # mcp = FastMCP("Customer MCP", stateless_http=True, json_response=True) # app = StatelessHttpSessionMiddleware(mcp.streamable_http_app()) ``` On go-sdk **v1.7.0**, `StreamableHTTPHandler{Stateless: true}` ignores `Mcp-Session-Id` entirely (`GetSessionID` is never consulted), so wrap the HTTP handler with the analytics middleware. This is required for legacy-client attribution: ```go theme={null} rec, err := official.NewRecorder(config) if err != nil { return err } http.Handle("/mcp", rec.WrapStatelessHTTPHandler(mcpHandler)) ``` On mark3labs/mcp-go (and official go-sdk **≤ v1.6**), resolve the session per request, then hand it to the transport options (mark3labs) or the server options (official): ```go theme={null} session := armatureanalytics.ResolveStatelessHTTPSession(armatureanalytics.StatelessHTTPInput{ Body: body, Headers: r.Header, }) // mark3labs/mcp-go: the manager goes on the HTTP transport, not the server s, shutdown := armatureanalytics.NewMCPServer(name, version, server.WithToolCapabilities(true), ) defer shutdown(r.Context()) handler := server.NewStreamableHTTPServer(s, server.WithSessionIdManager(session.Mark3labsSessionIDManager()), ) handler.ServeHTTP(w, r) // official go-sdk <= v1.6 s, shutdown := official.NewMCPServer(impl, &mcp.ServerOptions{ GetSessionID: session.SessionIDGenerator(), }) // serve with &mcp.StreamableHTTPOptions{Stateless: true, JSONResponse: true} ``` Session IDs are attribution metadata, not a security boundary. Clients echo them back unsigned. Keep authorization on your own auth layer. # What the SDK captures Source: https://docs.armature.tech/sdks/telemetry Telemetry fields, session reconstruction, and exactly what leaves your server All four SDKs share one wire format, so sessions land in the same dashboard whether your server is TypeScript, Python, Go, or PHP. ## Telemetry fields The SDK adds one optional `telemetry` object to each instrumented tool's input schema, with three well-described fields. The calling agent fills them in as it works; the SDK strips the object out **before your handler runs**, so your business logic never sees it: | Field | Type | What the agent reports | | ------------------ | --------------------------- | ------------------------------------------------------------------------------------ | | `user_intent` | string | What the user asked for, in their words. Powers [Use cases](/dashboard/use-cases). | | `agent_thinking` | string | The agent's reasoning for this call. Shown in [session replay](/dashboard/sessions). | | `user_frustration` | `low` \| `medium` \| `high` | The agent's read of user frustration. | Agents are told to send `agent_thinking` on every call, and `user_intent` and `user_frustration` on the first call after each new user message — their absence on later calls means the same turn continues, which is how the conversation view knows where turns begin. All fields are optional — agents that ignore them still produce complete sessions with tool calls, timing, and outcomes. Older field spellings (`intent`, `context`, `frustration_level`) are accepted and normalized, and `user_turn` values from cached schemas are ignored, so agents with stale schemas keep working. This is why instrumented sessions show *what the user asked* and *what the agent was thinking* — not just a list of tool calls. The description hint the SDK appends to each tool tells agents how to fill the fields. ## Events The SDK emits two event kinds: **`session_init`** — once per session: client name and version (Claude, ChatGPT, Cursor, …), protocol version, and capabilities. **`tool_call`** — one per tool invocation: * Tool name, start/finish timestamps, duration * Outcome (`ok` or the error), including MCP results returned with `isError: true` * Telemetry fields reported by the agent * A truncated input preview and result preview ## The `request_capability` tool With analytics enabled, the SDK also registers one small tool of its own: `request_capability`. Its description tells agents to call it when the user needs something no existing tool can do; each call is recorded like any other and feeds [unmet demand](/dashboard/use-cases#unmet-demand). A tool of the same name that you define yourself takes precedence, and the injection is skipped when analytics is disabled or no delivery is configured. Set `requestCapability: false` (`request_capability` in Python, `RequestCapability` in Go) to opt out. ## Size limits Payloads are truncated UTF-8-safely before they leave your process: | Payload | Limit | | ----------------------- | ---------- | | Input / result previews | 8 KiB each | | Script source | 32 KiB | | Client capabilities | 4 KiB | ## Sessions and actors * **Session identity** comes from the MCP session: HTTP servers use the `Mcp-Session-Id` header; stdio servers get a process-scoped session ID, so each CLI conversation is its own session. Stateless HTTP servers need [one extra helper](/sdks/stateless-servers). * **Actor identity** is a SHA-256 hash of a seed derived from the request's auth (token, client ID, or `Authorization` header), falling back to `anonymous`. Raw seeds never leave your server; the opt-in `actorIdentifier` is the deliberate exception — see [Identifying users](/sdks/identifying-users). ## Delivery guarantees * Events post to Armature's ingest API over HTTPS, authenticated with your ingest key. * Delivery runs off the request path by default in the TypeScript, Python, and Go SDKs (`background` mode). The PHP SDK awaits delivery by default, because PHP is request-scoped. In every SDK, delivery never throws into your tools — failures go to your `onError` hook. * If no ingest key is configured, the SDK no-ops: schemas are still decorated, but nothing is sent. The agent-facing schema hint instructs agents not to put secrets or PII in telemetry fields, and Armature [scans and redacts](/security/privacy) incoming sessions before storage. # TypeScript Source: https://docs.armature.tech/sdks/typescript Instrument a TypeScript MCP server with @armature-tech/mcp-analytics The TypeScript SDK wraps servers built with the official [MCP TypeScript SDK](https://github.com/modelcontextprotocol/typescript-sdk) (1.x and 2.x), [Mastra](https://mastra.ai), [punkpeye/fastmcp](https://github.com/punkpeye/fastmcp), or Vercel's mcp-handler. It captures sessions and tool calls without changing how your server behaves. ## Install ```bash theme={null} npm install @armature-tech/mcp-analytics ``` Both MCP SDK peers are optional — install the one your server uses (or both): `@modelcontextprotocol/sdk` (`>=1.20 <2`) for the package root, the `/mastra` subpath, and the `doctor` CLI; `@modelcontextprotocol/server` 2.x for the `/v2` subpath. If your tools use Zod 4 raw-shape schemas, use MCP SDK **1.29 or later** — 1.20 silently drops fields added to Zod 4 raw shapes. ## Set your API key The SDK reads your configuration from the environment: ```bash theme={null} ANALYTICS_INGEST_API_KEY= ANALYTICS_INGEST_URL= ``` Copy both values from the dashboard when you mint the key. US workspaces can omit `ANALYTICS_INGEST_URL` (the SDK defaults to the US endpoint); EU workspaces must set it to `https://eu.armature.tech/api/mcp-analytics/ingest`. If the key is missing, the SDK quietly no-ops — your server keeps working, and no data is sent. ## Wrap your server If you build your server in a factory function, wrap the factory with `createMcpAnalyticsServer`. Every tool registered inside is instrumented automatically: ```typescript server.ts theme={null} import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js"; import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"; import { createMcpAnalyticsServer } from "@armature-tech/mcp-analytics"; import { z } from "zod"; const server = createMcpAnalyticsServer(() => { const mcp = new McpServer({ name: "customer-mcp", version: "1.0.0" }); mcp.registerTool( "lookup_customer", { description: "Look up a customer by ID", inputSchema: { customer_id: z.string() }, }, async ({ customer_id }) => ({ content: [{ type: "text", text: `customer ${customer_id} is active` }], }), ); return mcp; }); await server.connect(new StdioServerTransport()); ``` That's the whole integration. Deploy, and sessions appear in the dashboard within minutes. ## Other server shapes If you already have an `McpServer` instance and a list of tool definitions, instrument them in place: ```typescript theme={null} import { instrumentMcpServerTools } from "@armature-tech/mcp-analytics"; const { server: instrumented, recorder } = instrumentMcpServerTools({ server, tools, // array or record of { ...toolRegistration, handler } }); ``` If you handle `tools/list` and `tools/call` yourself, use the recorder directly: ```typescript theme={null} import { createAnalyticsRecorder } from "@armature-tech/mcp-analytics"; const analytics = createAnalyticsRecorder(); analytics.tool(toolDefinition, toolHandler); // tools/list const tools = analytics.toolDefinitions(); // tools/call const result = await analytics.dispatch(name, args, context); ``` Wrap a Mastra tool map with the dedicated adapter: ```typescript theme={null} import { wrapMastraTools } from "@armature-tech/mcp-analytics/mastra"; const instrumentedTools = wrapMastraTools(tools); ``` The adapter reads MCP request context from `context.mcp.extra` or `context.requestContext.get("mcp.extra")` automatically. Wrap each tool with the adapter from the dedicated subpath: ```typescript theme={null} import { withFastmcpAnalytics, flushFastmcpAnalytics, } from "@armature-tech/mcp-analytics/fastmcp"; // Hoist the options: wrap calls sharing this object share one pipeline, // and flush must receive the same object. const analyticsOptions = { transport: "httpStream" as const }; server.addTool(withFastmcpAnalytics(tool, analyticsOptions)); // before shutdown: await flushFastmcpAnalytics(analyticsOptions); ``` On an `httpStream` server, keep `transport: "httpStream"` in the options — without it, anonymous HTTP sessions collapse onto one process-scoped session id. `instrumentFastMCP(server, options)` wraps every tool at once and detects `httpStream` on its own. fastmcp validates schemas before `execute`, so telemetry schema decoration is off by default — opt in with `declareTelemetry: true`, a top-level key next to `armature`. The shutdown flush is unnecessary with `delivery: "await"` or a `schedule` hook. Details in the [SDK README](https://github.com/armature-tech/mcp-analytics#fastmcp). mcp-handler runs your initialize callback per request with a real official-SDK `McpServer`, so wrap the server inside the callback and the handler outside it: ```typescript theme={null} import { createMcpHandler } from "mcp-handler"; import { withMcpAnalytics } from "@armature-tech/mcp-analytics"; import { withMcpHandlerAnalytics } from "@armature-tech/mcp-analytics/mcp-handler"; // Hoist the config; use "await" delivery — the callback runs per // request and a background queue may be frozen. const analyticsConfig = { armature: { delivery: "await" as const } }; const handler = withMcpHandlerAnalytics( createMcpHandler((server) => { withMcpAnalytics(analyticsConfig, () => { server.registerTool(/* … */); return server; }); }), analyticsConfig, ); export { handler as GET, handler as POST }; ``` Servers on the v2 package family use the dedicated `/v2` subpath — the package root still targets the 1.x SDK: ```typescript theme={null} import { createMcpHandler, McpServer } from "@modelcontextprotocol/server"; import { instrumentedFactory, wrapMcpHandler, } from "@armature-tech/mcp-analytics/v2"; // Hoist the config: every per-request server shares one pipeline. const analyticsConfig = { armature: { apiKey: process.env.ANALYTICS_INGEST_API_KEY }, }; const handler = wrapMcpHandler( createMcpHandler( instrumentedFactory(() => { const server = new McpServer(/* … */); // ...register your tools exactly as before return server; }, analyticsConfig), ), analyticsConfig, ); ``` `wrapMcpHandler` is strongly recommended: without it, clients on the pre-2026-07-28 protocol silently lose session identity through v2's stateless legacy leg. The v2 adapter doesn't decorate tool schemas with the `telemetry` field by default — restore that with `declareTelemetry: true` in the adapter options, the third argument: `instrumentedFactory(buildServer, analyticsConfig, { declareTelemetry: true })`. It is not a key of `analyticsConfig`. Details in the [SDK README](https://github.com/armature-tech/mcp-analytics#mcp-sdk-v2-protocol-revision-2026-07-28). Running on serverless or stateless HTTP? See [Stateless & serverless servers](/sdks/stateless-servers) for session handling, and set `delivery: "await"` so events are sent before the function freezes. ## Configuration Pass options under the `armature` key: ```typescript theme={null} const server = createMcpAnalyticsServer(buildServer, { armature: { delivery: "background", timeoutMs: 5000, onError: (error) => console.warn("analytics delivery failed", error), }, }); ``` | Option | Default | Description | | ------------------- | ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `apiKey` | `ANALYTICS_INGEST_API_KEY` env var | API key used to authenticate ingestion. | | `endpointUrl` | US ingest endpoint | Override with `ANALYTICS_INGEST_URL` or this option; EU workspaces must use the EU endpoint. | | `enabled` | `true` | Set `false` to disable capture entirely. | | `delivery` | `"background"` | `"background"` posts events off the request path; `"await"` sends before returning (use in serverless). | | `timeoutMs` | `5000` | Delivery timeout in milliseconds. | | `actorId` | Derived from request auth | A string or resolver used to attribute calls to a user. See [Identifying users](/sdks/identifying-users). | | `actorIdentifier` | None | Attach a caller-provided identifier verbatim. See [Identifying users](/sdks/identifying-users). | | `emit` | Network emitter | Replace delivery with your own function — useful in tests. | | `onError` | None — failures are logged with `console.warn` | Called when delivery fails, including in-body ingest rejections. Failures never reach your tools — set this in production to route them to your logger. | | `captureTelemetry` | `true` | Set `false` to disable conversation-derived telemetry while keeping call analytics. | | `requestCapability` | On | Injects the `request_capability` tool (see [What the SDK captures](/sdks/telemetry#the-request_capability-tool)); set `false` to disable. | ## Flush on shutdown With background delivery, drain pending events before the process exits: ```typescript theme={null} import { withMcpAnalytics } from "@armature-tech/mcp-analytics"; const { result: server, recorder } = withMcpAnalytics({}, buildServer); process.on("SIGTERM", async () => { await recorder.flush(); process.exit(0); }); ``` ## Troubleshooting Fixed in current releases — update the SDK. Older versions crashed when a tool declared a Zod 4 raw-shape schema while the SDK injected its telemetry field with Zod 3. Make sure `@modelcontextprotocol/sdk` is 1.29+. Version 1.20 silently drops fields the SDK adds to Zod 4 raw-shape schemas, so agents never see them. Check that `ANALYTICS_INGEST_API_KEY` is set in the server's environment (missing keys no-op silently by design), that `ANALYTICS_INGEST_URL` matches your workspace region — EU keys are rejected by the US endpoint — and search the server log for `[mcp-analytics] telemetry emit failed:`, the SDK's default delivery-failure warning. Add an `onError` handler to route those failures to your own logger. # Privacy & security Source: https://docs.armature.tech/security/privacy How session data is protected, end to end Agent sessions contain what your users typed. Armature treats them accordingly. ## Redaction before storage Detection models scan incoming sessions and **redact PII and secrets by default, before anything reaches storage**. An email address becomes a `PII` marker; a live API key becomes a `SECRET` marker. What you replay in the dashboard is the redacted session. ## What leaves your server (and what doesn't) The SDK runs inside your process, and you can read [exactly what it sends](/sdks/telemetry): * **Sent**: tool names, timing, outcomes, agent-reported telemetry fields, and size-capped previews of inputs and results (8 KiB each). * **Hashed on your server**: actor identity. The seed — a token, client ID, or header — is SHA-256 hashed before transmission; raw credentials never leave your process. The one exception is the opt-in `actorIdentifier`: an identifier you choose (for example your internal user ID) is sent verbatim so the dashboard can display it. See [Identifying users](/sdks/identifying-users). * **Never sent**: anything when no ingest key is configured. The SDK no-ops rather than buffering. Delivery is HTTPS, authenticated by your per-server ingest key, which you can [rotate or revoke](/dashboard/workspace#mcp-setup) at any time — revocation stops ingestion immediately. ## Agent-side guardrails * The telemetry field descriptions instruct agents not to include secrets or PII when reporting intent. * The hosted Armature MCP rejects secret-shaped values passed as tool arguments and never returns credentials in tool responses. ## Data retention Session history is kept for your plan's retention window (7 days on the Free plan; custom retention on the Enterprise plan). Usage and retention for your workspace are always visible under **Settings → Usage**. ## Access control * Ingest keys are scoped to one MCP server and write-only: they can send events, not read analytics. * Workspace API keys and OAuth grants control read access via the [Armature MCP](/armature-mcp/connect); grants are listed and revocable in the dashboard. For SSO/SAML, audit logs, custom retention, or a full security review, [talk to us](https://armature.tech/#pricing).