# How the MCP Client Activity Endpoint Tracks Usage by Client Type in ai-memory

> Discover how the MCP client activity endpoint in ai-memory tracks tool usage by client type using an in-memory buffer flushing to SQLite. Access 7-day and 30-day rollups.

- Repository: [Fabio Akita/ai-memory](https://github.com/akitaonrails/ai-memory)
- Tags: how-to-guide
- Published: 2026-08-25

---

**The MCP client activity endpoint aggregates tool-call metrics by client type—such as `cursor`, `zed`, or `vscode`—using an in-memory buffer that periodically flushes to SQLite, exposing 7-day and 30-day rollups via `GET /admin/activity/by-client`.**

The `akitaonrails/ai-memory` repository implements a Memory-Capture-Protocol (MCP) server that monitors how different AI clients interact with its tools. Understanding how the **MCP client activity endpoint** tracks usage by client type reveals a three-stage pipeline combining in-process buffering, background persistence, and SQL aggregation.

## Architecture Overview

The tracking system operates as a pipeline with three distinct stages. First, the MCP server buffers incoming requests in memory. Second, a background task periodically flushes this buffered data to persistent storage. Finally, the admin endpoint queries pre-aggregated views to return JSON statistics split by 7-day and 30-day windows.

## Stage 1: In-Process Buffering

When the MCP server receives a tool-call request, it immediately logs the interaction in memory before responding to the client.

### The ClientActivityBuffer Structure

In [`crates/ai-memory-mcp/src/server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/server.rs), the `AdminState` struct maintains the activity cache:

```rust
// Line 391
client_activity: Arc<std::sync::Mutex<ClientActivityBuffer>>

```

This mutex-protected buffer stores per-client counters without blocking the request handler on database I/O.

### Recording Activity

For every incoming MCP request, the server invokes `record_client_activity` (line 4176). This function:

- Extracts the **client name** from the request metadata (e.g., `cursor`, `zed`, `vscode`)
- Determines the operation type: **reads** (GET/HEAD methods) or **writes** (all other methods)
- Increments the corresponding daily counter in the buffer

The buffer organizes data as per-day buckets, allowing the system to track temporal patterns without immediate database writes.

## Stage 2: Periodic Flushing to SQLite

To balance performance with durability, the system uses a background task to persist buffered data.

The `flush_client_activity_loop` function (line 695) runs at a configurable interval and performs the following operations:

1. Locks the `ClientActivityBuffer` and extracts all pending entries
2. Transforms the data into a list of `(client, day, reads, writes)` tuples
3. Sends the batch to the writer actor via `writer.bump_client_activity(entries).await` (line 717)

This batching strategy minimizes database contention by consolidating multiple tool-calls into a single write transaction.

## Stage 3: Persistence and Aggregation

Once the batch reaches the storage layer, the system handles writes and reads through separate specialized modules.

### Writer Operations

In [`crates/ai-memory-store/src/writer.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/writer.rs), the `bump_client_activity` method (line 1044) receives the entry list and forwards it to the low-level operation:

```rust
ops::bump_client_activity(entries)

```

The [`ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/ops.rs) implementation (line 2061) executes an UPSERT-style SQL query (line 1511) against the `client_activity` table. The query uses the composite key `(client, day)` to atomically increment existing counters or insert new rows when a client appears for the first time on a specific date.

### Reader Aggregation

When the admin endpoint receives a request, it queries [`crates/ai-memory-store/src/reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-store/src/reader.rs). The `client_activity_since` method (line 2075) executes the aggregation query shown at line 2086, which:

- Groups records by client name
- Sums reads and writes over the last 7 days and 30 days
- Returns structured data ready for JSON serialization

## The Admin Endpoint

The endpoint handler `handle_activity_by_client` is registered in [`crates/ai-memory-mcp/src/admin.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/admin.rs) (lines 75–76). It calls the reader's aggregation method and serializes the result into a standardized JSON payload.

### Response Format

The endpoint returns activity metrics organized by time window:

```json
{
  "activity_7d": {
    "cursor": { "reads": 214, "writes": 19 },
    "zed": { "reads": 87, "writes": 4 }
  },
  "activity_30d": {
    "cursor": { "reads": 742, "writes": 61 },
    "zed": { "reads": 321, "writes": 23 }
  }
}

```

## Querying the Endpoint

You can inspect client activity by requesting the admin endpoint directly:

```bash
curl http://localhost:49374/admin/activity/by-client

```

The server responds with the aggregated JSON structure, showing which client types generate the most tool-call traffic and whether they favor read-heavy or write-heavy operations.

### Simulating Activity Entries

To understand the data flow programmatically, you can construct activity entries using the store's types:

```rust
use ai_memory_store::{WriterHandle, ClientActivityEntry};

async fn simulate_activity(writer: &WriterHandle) -> Result<(), Box<dyn std::error::Error>> {
    // Simulate cursor client calling read tools 10 times and write tools 3 times today
    let entries = vec![
        ClientActivityEntry::new("cursor".into(), 20250115, 10, 0),
        ClientActivityEntry::new("cursor".into(), 20250115, 0, 3),
    ];
    
    writer.bump_client_activity(entries).await?;
    Ok(())
}

```

## Summary

- The **MCP client activity endpoint** tracks usage through an in-memory `ClientActivityBuffer` defined in [`crates/ai-memory-mcp/src/server.rs`](https://github.com/akitaonrails/ai-memory/blob/main/crates/ai-memory-mcp/src/server.rs) (line 391).
- The `record_client_activity` function (line 4176) classifies each request as a read or write operation based on the HTTP method.
- Background task `flush_client_activity_loop` (line 695) periodically persists buffered data via `bump_client_activity` in the writer module.
- SQLite stores data in the `client_activity` table using UPSERT logic (line 1511 in [`ops.rs`](https://github.com/akitaonrails/ai-memory/blob/main/ops.rs)) keyed by `(client, day)`.
- The reader aggregates 7-day and 30-day windows (line 2075 in [`reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/reader.rs)) for the `GET /admin/activity/by-client` endpoint.

## Frequently Asked Questions

### What client types does the MCP server recognize?

The system accepts any client identifier string provided in the request metadata. Common examples observed in the source include `cursor`, `zed`, and `vscode`, but the implementation is agnostic and will track any client name it receives.

### How often does the activity buffer flush to the database?

The flush interval is configurable. The `flush_client_activity_loop` runs continuously in the background and drains the `ClientActivityBuffer` at whatever interval the server configuration specifies, balancing between durability and performance.

### What's the difference between reads and writes in the metrics?

Reads represent tool calls using GET or HEAD HTTP methods, typically indicating data retrieval operations. Writes encompass all other HTTP methods (POST, PUT, DELETE, etc.), representing operations that modify server state or create new memory entries.

### Can I query historical activity beyond 30 days?

The current admin endpoint implementation returns only 7-day and 30-day aggregated windows as defined in [`reader.rs`](https://github.com/akitaonrails/ai-memory/blob/main/reader.rs) (line 2086). To access older data, you would need to query the `client_activity` table directly in SQLite or modify the `client_activity_since` method to accept custom date ranges.