How the MCP Client Activity Endpoint Tracks Usage by Client Type in ai-memory
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, the AdminState struct maintains the activity cache:
// 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:
- Locks the
ClientActivityBufferand extracts all pending entries - Transforms the data into a list of
(client, day, reads, writes)tuples - 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, the bump_client_activity method (line 1044) receives the entry list and forwards it to the low-level operation:
ops::bump_client_activity(entries)
The 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. 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 (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:
{
"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:
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:
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
ClientActivityBufferdefined incrates/ai-memory-mcp/src/server.rs(line 391). - The
record_client_activityfunction (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 viabump_client_activityin the writer module. - SQLite stores data in the
client_activitytable using UPSERT logic (line 1511 inops.rs) keyed by(client, day). - The reader aggregates 7-day and 30-day windows (line 2075 in
reader.rs) for theGET /admin/activity/by-clientendpoint.
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 (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.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →