# Armature MCP Analytics for Elixir

[![CI](https://github.com/MarkMarine/armature_mcp_analytics/actions/workflows/ci.yml/badge.svg)](https://github.com/MarkMarine/armature_mcp_analytics/actions/workflows/ci.yml)
[![Hex.pm](https://img.shields.io/hexpm/v/armature_mcp_analytics.svg)](https://hex.pm/packages/armature_mcp_analytics)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)

> [!IMPORTANT]
> This is an unofficial, community-maintained package created by MarkMarine.
> It is not affiliated with or endorsed by Armature.

Privacy-conscious, framework-neutral integration with Armature analytics for
Elixir MCP servers. The published package is available on
[Hex.pm](https://hex.pm/packages/armature_mcp_analytics), with API documentation
on [HexDocs](https://hexdocs.pm/armature_mcp_analytics).

The library:

- decorates MCP tool schemas with optional conversation telemetry;
- strips that telemetry before validation and handler execution;
- emits bounded, redacted schema-v1 `session_init` and `tool_call` events;
- retries transient ingest failures without breaking tool handlers; and
- provides the default-on `request_capability` tool for unmet-demand signals.

It works with JSON-style tool definitions and does not require a particular
Elixir MCP framework.

## Installation

Add `armature_mcp_analytics` to `mix.exs`:

```elixir
def deps do
  [
    {:armature_mcp_analytics, "~> 0.1.0"}
  ]
end
```

To follow the repository directly instead of Hex:

```elixir
{:armature_mcp_analytics,
 github: "MarkMarine/armature_mcp_analytics",
 tag: "v0.1.0"}
```

## Configuration

Set these only in the server deployment environment:

```text
ANALYTICS_INGEST_API_KEY=your-ingest-key
ANALYTICS_INGEST_URL=https://app.armature.tech/api/mcp-analytics/ingest
```

The URL above is the US endpoint. EU accounts must set:

```text
ANALYTICS_INGEST_URL=https://eu.armature.tech/api/mcp-analytics/ingest
```

Missing API keys intentionally no-op, which keeps local development simple.
Never expose the ingest key to MCP clients or browser code.

## Supervision

Add the recorder to your application's supervision tree:

```elixir
children = [
  {Armature.MCP.Analytics,
   api_key: System.get_env("ANALYTICS_INGEST_API_KEY"),
   ingest_url: System.get_env("ANALYTICS_INGEST_URL"),
   delivery: :background}
]
```

Use `:background` for long-lived OTP applications and call
`Armature.MCP.Analytics.flush/0` during an orderly shutdown when practical.
Use `delivery: :await` for short-lived commands and serverless request
handlers.

## Instrument a dispatcher

Decorate the tool list before returning `tools/list`:

```elixir
tools = Armature.MCP.Analytics.decorate_tools(tools)
```

This adds the `request_capability` tool by default. Disable it with:

```elixir
tools = Armature.MCP.Analytics.decorate_tools(tools, request_capability: false)
```

Wrap normal tool dispatch so the handler receives its original arguments:

```elixir
context = %{
  actor_seed: authenticated_subject,
  session_id: mcp_session_id,
  client_info: client_info,
  client_capabilities: client_capabilities,
  protocol_version: protocol_version,
  request_meta: request_meta
}

Armature.MCP.Analytics.track(tool_name, raw_arguments, context, fn clean_arguments ->
  dispatch_tool(tool_name, clean_arguments)
end)
```

Only provide `context.request_id` when it is a genuine, globally unique
per-invocation idempotency key. Do not pass a JSON-RPC message id: those
counters are frequently reused across sessions and can cause deduplication
collisions.

If a customer tool already defines a top-level `telemetry` argument, pass its
tool definition in the context. The library will preserve the native field:

```elixir
context = Map.put(context, :tool_definition, tool_definition)
```

Route the helper tool with:

```elixir
Armature.MCP.Analytics.MCP.handle_request_capability(arguments)
```

## Privacy and failure behavior

Before serialization, the library:

- redacts common credential fields and high-confidence token patterns;
- removes large base64 payloads;
- bounds previews, metadata, and total sanitization work; and
- hashes actor identifiers before transmission.

You can mutate or drop a whole event with `:redact_event`:

```elixir
{Armature.MCP.Analytics,
 redact_event: fn event ->
   update_in(event, ["metadata"], &Map.drop(&1, ["request_meta"]))
 end}
```

Delivery failures go to `:on_error` and never replace tool results:

```elixir
{Armature.MCP.Analytics,
 on_error: fn error ->
   Logger.warning("Analytics delivery failed",
     code: error.code,
     status: error.status
   )
 end}
```

Do not log the batch, API key, or raw HTTP response in an error callback.

## Verify a live MCP server

After deployment, run Armature's content-free doctor with the same regional
environment variables:

```bash
npx @armature-tech/mcp-analytics doctor --url https://your-server.example/mcp
```

The doctor checks the MCP handshake, served tool schemas, and ingest
authentication without sending customer content.

## Development

```bash
mix deps.get
mix check
```

See [CONTRIBUTING.md](CONTRIBUTING.md) for the contribution workflow.

## Releasing

Releases are created from GitHub Actions with the **Publish release** workflow:

1. Update `@version` in `mix.exs` and document the release in `CHANGELOG.md`.
2. Commit the changes to `main` and ensure CI passes.
3. Configure the repository secret `HEX_API_KEY` with permission to publish
   `armature_mcp_analytics`.
4. Run **Actions → Publish release → Run workflow** and enter the exact version.

The workflow verifies the requested version matches `mix.exs`, runs the full
check and coverage gates, publishes the versioned Hex package if it does not
already exist, and creates the matching `vVERSION` GitHub release. Reruns are
safe when the Hex version or GitHub release already exists.

## License

MIT. See [LICENSE](LICENSE).
