madb-mcp-server 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- madb_mcp_server-0.1.0/.gitignore +54 -0
- madb_mcp_server-0.1.0/PKG-INFO +131 -0
- madb_mcp_server-0.1.0/README.md +111 -0
- madb_mcp_server-0.1.0/__main__.py +4 -0
- madb_mcp_server-0.1.0/madb_mcp_analytics.py +343 -0
- madb_mcp_server-0.1.0/madb_mcp_report.py +422 -0
- madb_mcp_server-0.1.0/madb_mcp_server.py +632 -0
- madb_mcp_server-0.1.0/pyproject.toml +39 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# Rust build artifacts
|
|
2
|
+
target/
|
|
3
|
+
**/target/
|
|
4
|
+
**/*.rs.bk
|
|
5
|
+
|
|
6
|
+
# Cargo — keep Cargo.lock at workspace root (we're an application, not a library)
|
|
7
|
+
# Individual crate Cargo.lock files should not be committed.
|
|
8
|
+
crates/*/Cargo.lock
|
|
9
|
+
|
|
10
|
+
# Python (maturin develop install side-effects + venvs)
|
|
11
|
+
__pycache__/
|
|
12
|
+
*.py[cod]
|
|
13
|
+
*$py.class
|
|
14
|
+
*.so
|
|
15
|
+
*.egg-info/
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
.Python
|
|
19
|
+
build/
|
|
20
|
+
dist/
|
|
21
|
+
*.whl
|
|
22
|
+
|
|
23
|
+
# Editor / OS
|
|
24
|
+
.DS_Store
|
|
25
|
+
.idea/
|
|
26
|
+
.vscode/
|
|
27
|
+
*.swp
|
|
28
|
+
*.swo
|
|
29
|
+
*~
|
|
30
|
+
.env
|
|
31
|
+
.env.local
|
|
32
|
+
|
|
33
|
+
# Maturin develop side-effects — the installed wheel lives in the caller's
|
|
34
|
+
# venv; we do not want any artifact under the project tree.
|
|
35
|
+
.maturin/
|
|
36
|
+
|
|
37
|
+
# Test coverage reports
|
|
38
|
+
target/criterion/
|
|
39
|
+
*.profraw
|
|
40
|
+
*.profdata
|
|
41
|
+
coverage/
|
|
42
|
+
|
|
43
|
+
# MADB runtime data directories (should never be in the repo).
|
|
44
|
+
# Intentionally NOT using a global *.lock pattern so the workspace
|
|
45
|
+
# Cargo.lock stays tracked (Rust workspace Cargo.lock must be committed).
|
|
46
|
+
data/
|
|
47
|
+
/tmp/madb*
|
|
48
|
+
**/data/madb*
|
|
49
|
+
**/data/maos_memory*
|
|
50
|
+
**/wal/*.log
|
|
51
|
+
**/hlc.json
|
|
52
|
+
# MADB data-dir lockfile is literally named "LOCK" (no extension)
|
|
53
|
+
# inside the data directory, not at the workspace root.
|
|
54
|
+
**/data/**/LOCK
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: madb-mcp-server
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MCP server for Meta-AgentsDB — persistent causal memory for AI agents
|
|
5
|
+
Project-URL: Homepage, https://meta-agents.ai
|
|
6
|
+
Project-URL: Repository, https://github.com/spshkar84/meta-agents-db
|
|
7
|
+
Author-email: Pushkar Singh <01@meta-agents.ai>, Pushkar Singh <spshkar84@gmail.com>, Pushkar Singh <spshkar84@meta-agents.ai>
|
|
8
|
+
License-Expression: Apache-2.0
|
|
9
|
+
Keywords: agents,claude,database,mcp,memory
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Database
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Requires-Dist: mcp>=1.0.0
|
|
18
|
+
Requires-Dist: meta-agents-db>=0.1.0
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# madb-mcp-server
|
|
22
|
+
|
|
23
|
+
Persistent causal memory for Claude Code — **4.65x recall ROI** measured in production.
|
|
24
|
+
|
|
25
|
+
`madb-mcp-server` gives Claude Code (and any MCP-compatible client) a real database backend for agent memory: causal chains, composite-scored recall, policy-gated access, and built-in analytics.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install madb-mcp-server
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
Or run directly with uvx (no install needed):
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
uvx madb-mcp-server
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Claude Code Setup
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
claude mcp add madb -- uvx madb-mcp-server
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Or add to your project's `.mcp.json`:
|
|
46
|
+
|
|
47
|
+
```json
|
|
48
|
+
{
|
|
49
|
+
"mcpServers": {
|
|
50
|
+
"madb": {
|
|
51
|
+
"command": "uvx",
|
|
52
|
+
"args": ["madb-mcp-server"],
|
|
53
|
+
"env": {
|
|
54
|
+
"MADB_DATA_DIR": "~/.madb/data",
|
|
55
|
+
"MADB_TENANT_ID": "default"
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Claude Desktop Setup
|
|
63
|
+
|
|
64
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
{
|
|
68
|
+
"mcpServers": {
|
|
69
|
+
"madb": {
|
|
70
|
+
"command": "uvx",
|
|
71
|
+
"args": ["madb-mcp-server"],
|
|
72
|
+
"env": {
|
|
73
|
+
"MADB_DATA_DIR": "~/.madb/data",
|
|
74
|
+
"MADB_TENANT_ID": "default"
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Tools (9)
|
|
82
|
+
|
|
83
|
+
| Tool | Description |
|
|
84
|
+
|------|-------------|
|
|
85
|
+
| `remember` | Store a memory with causal links, tags, and importance scoring |
|
|
86
|
+
| `recall` | Semantic recall — vector similarity + recency + causal proximity + importance |
|
|
87
|
+
| `get_memory` | Point lookup by event_id |
|
|
88
|
+
| `forget` | Soft-delete a memory (tombstone, preserved in causal chain) |
|
|
89
|
+
| `search` | Structured query by tenant + scope |
|
|
90
|
+
| `list_recent` | Last N memories for a tenant |
|
|
91
|
+
| `stats` | Engine metrics snapshot (writes, reads, flushes, compactions, WAL state) |
|
|
92
|
+
| `trace_cause` | Walk the causal DAG forward or backward from any memory |
|
|
93
|
+
| `analytics` | MCP usage analytics — call counts, latencies, recall precision, Memory ROI |
|
|
94
|
+
|
|
95
|
+
## Memory ROI
|
|
96
|
+
|
|
97
|
+
MADB tracks how much value its memory provides to your Claude sessions:
|
|
98
|
+
|
|
99
|
+
- **Tokens served from recall** — context delivered from memory instead of re-reading files
|
|
100
|
+
- **File reads avoided** — estimated file re-reads saved by recalling from memory
|
|
101
|
+
- **Recall precision** — hit rate and average relevance score
|
|
102
|
+
- **Write-to-read ratio** — tokens recalled per token written (measured 4.65x in production)
|
|
103
|
+
|
|
104
|
+
Run the `analytics` tool at any time to see your session's Memory ROI.
|
|
105
|
+
|
|
106
|
+
## Resources
|
|
107
|
+
|
|
108
|
+
| URI | Description |
|
|
109
|
+
|-----|-------------|
|
|
110
|
+
| `madb://memories/{event_id}` | Read a single memory record |
|
|
111
|
+
|
|
112
|
+
## Environment Variables
|
|
113
|
+
|
|
114
|
+
| Variable | Default | Description |
|
|
115
|
+
|----------|---------|-------------|
|
|
116
|
+
| `MADB_DATA_DIR` | `~/.madb/data` | Database storage directory |
|
|
117
|
+
| `MADB_TENANT_ID` | `default` | Default tenant for tool calls |
|
|
118
|
+
|
|
119
|
+
## How It Works
|
|
120
|
+
|
|
121
|
+
MADB is an agent-native database built in Rust with 5 novel primitives:
|
|
122
|
+
|
|
123
|
+
1. **Causal DAG** — every memory links to what caused it, forming an evidence chain
|
|
124
|
+
2. **Composite-scored recall** — vector similarity + recency + causal proximity + importance
|
|
125
|
+
3. **Storage-layer policy** — scope and retention rules enforced at the engine level
|
|
126
|
+
4. **Partitioned WAL** — per-tenant write-ahead log for crash safety
|
|
127
|
+
5. **Time-bucketed HNSW** — vector index organized by time for recency-aware search
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
Apache-2.0 (patent pending)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# madb-mcp-server
|
|
2
|
+
|
|
3
|
+
Persistent causal memory for Claude Code — **4.65x recall ROI** measured in production.
|
|
4
|
+
|
|
5
|
+
`madb-mcp-server` gives Claude Code (and any MCP-compatible client) a real database backend for agent memory: causal chains, composite-scored recall, policy-gated access, and built-in analytics.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install madb-mcp-server
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or run directly with uvx (no install needed):
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
uvx madb-mcp-server
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Claude Code Setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
claude mcp add madb -- uvx madb-mcp-server
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
Or add to your project's `.mcp.json`:
|
|
26
|
+
|
|
27
|
+
```json
|
|
28
|
+
{
|
|
29
|
+
"mcpServers": {
|
|
30
|
+
"madb": {
|
|
31
|
+
"command": "uvx",
|
|
32
|
+
"args": ["madb-mcp-server"],
|
|
33
|
+
"env": {
|
|
34
|
+
"MADB_DATA_DIR": "~/.madb/data",
|
|
35
|
+
"MADB_TENANT_ID": "default"
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Claude Desktop Setup
|
|
43
|
+
|
|
44
|
+
Add to `~/Library/Application Support/Claude/claude_desktop_config.json`:
|
|
45
|
+
|
|
46
|
+
```json
|
|
47
|
+
{
|
|
48
|
+
"mcpServers": {
|
|
49
|
+
"madb": {
|
|
50
|
+
"command": "uvx",
|
|
51
|
+
"args": ["madb-mcp-server"],
|
|
52
|
+
"env": {
|
|
53
|
+
"MADB_DATA_DIR": "~/.madb/data",
|
|
54
|
+
"MADB_TENANT_ID": "default"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Tools (9)
|
|
62
|
+
|
|
63
|
+
| Tool | Description |
|
|
64
|
+
|------|-------------|
|
|
65
|
+
| `remember` | Store a memory with causal links, tags, and importance scoring |
|
|
66
|
+
| `recall` | Semantic recall — vector similarity + recency + causal proximity + importance |
|
|
67
|
+
| `get_memory` | Point lookup by event_id |
|
|
68
|
+
| `forget` | Soft-delete a memory (tombstone, preserved in causal chain) |
|
|
69
|
+
| `search` | Structured query by tenant + scope |
|
|
70
|
+
| `list_recent` | Last N memories for a tenant |
|
|
71
|
+
| `stats` | Engine metrics snapshot (writes, reads, flushes, compactions, WAL state) |
|
|
72
|
+
| `trace_cause` | Walk the causal DAG forward or backward from any memory |
|
|
73
|
+
| `analytics` | MCP usage analytics — call counts, latencies, recall precision, Memory ROI |
|
|
74
|
+
|
|
75
|
+
## Memory ROI
|
|
76
|
+
|
|
77
|
+
MADB tracks how much value its memory provides to your Claude sessions:
|
|
78
|
+
|
|
79
|
+
- **Tokens served from recall** — context delivered from memory instead of re-reading files
|
|
80
|
+
- **File reads avoided** — estimated file re-reads saved by recalling from memory
|
|
81
|
+
- **Recall precision** — hit rate and average relevance score
|
|
82
|
+
- **Write-to-read ratio** — tokens recalled per token written (measured 4.65x in production)
|
|
83
|
+
|
|
84
|
+
Run the `analytics` tool at any time to see your session's Memory ROI.
|
|
85
|
+
|
|
86
|
+
## Resources
|
|
87
|
+
|
|
88
|
+
| URI | Description |
|
|
89
|
+
|-----|-------------|
|
|
90
|
+
| `madb://memories/{event_id}` | Read a single memory record |
|
|
91
|
+
|
|
92
|
+
## Environment Variables
|
|
93
|
+
|
|
94
|
+
| Variable | Default | Description |
|
|
95
|
+
|----------|---------|-------------|
|
|
96
|
+
| `MADB_DATA_DIR` | `~/.madb/data` | Database storage directory |
|
|
97
|
+
| `MADB_TENANT_ID` | `default` | Default tenant for tool calls |
|
|
98
|
+
|
|
99
|
+
## How It Works
|
|
100
|
+
|
|
101
|
+
MADB is an agent-native database built in Rust with 5 novel primitives:
|
|
102
|
+
|
|
103
|
+
1. **Causal DAG** — every memory links to what caused it, forming an evidence chain
|
|
104
|
+
2. **Composite-scored recall** — vector similarity + recency + causal proximity + importance
|
|
105
|
+
3. **Storage-layer policy** — scope and retention rules enforced at the engine level
|
|
106
|
+
4. **Partitioned WAL** — per-tenant write-ahead log for crash safety
|
|
107
|
+
5. **Time-bucketed HNSW** — vector index organized by time for recency-aware search
|
|
108
|
+
|
|
109
|
+
## License
|
|
110
|
+
|
|
111
|
+
Apache-2.0 (patent pending)
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# Copyright 2025-2026 Pushkar Singh / Meta-Agents.AI
|
|
3
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
# Part of Meta-AgentsDB — the first database designed for autonomous AI agents.
|
|
5
|
+
# https://meta-agents.ai
|
|
6
|
+
"""
|
|
7
|
+
madb_mcp_analytics — MCP usage instrumentation for Meta-AgentsDB
|
|
8
|
+
================================================================
|
|
9
|
+
|
|
10
|
+
Lightweight, stdlib-only analytics collector that records every MCP tool
|
|
11
|
+
invocation and produces point-in-time usage snapshots with Memory ROI.
|
|
12
|
+
|
|
13
|
+
Storage: append-only JSONL log at ``$MADB_DATA_DIR/mcp_analytics.jsonl``.
|
|
14
|
+
Hot path: in-memory counters (dict of tool -> count / latency / errors).
|
|
15
|
+
Flush: background thread every 60 s + ``atexit``.
|
|
16
|
+
|
|
17
|
+
Session tracking:
|
|
18
|
+
Each MCP server process lifetime = one Claude Code session.
|
|
19
|
+
A ``session_id`` (UUID4) is generated at init and stamped on every
|
|
20
|
+
JSONL line + snapshot. This lets the report group calls by session
|
|
21
|
+
and compute per-session Memory ROI.
|
|
22
|
+
|
|
23
|
+
Memory ROI model:
|
|
24
|
+
When Claude recalls memories instead of re-reading source files,
|
|
25
|
+
each recalled token represents context served "for free" — without
|
|
26
|
+
a file read round-trip. We estimate:
|
|
27
|
+
|
|
28
|
+
recall_tokens_saved = tokens_served_from_recall
|
|
29
|
+
avg_file_read_tokens = 1500 (conservative median for a source file)
|
|
30
|
+
file_reads_avoided = recall_tokens_saved / avg_file_read_tokens
|
|
31
|
+
memory_roi = recall_tokens_saved / max(memories_written_tokens, 1)
|
|
32
|
+
|
|
33
|
+
The ``recall_precision`` metric tracks the quality of recalled results
|
|
34
|
+
via the composite similarity score (avg top_score across all recalls).
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
from __future__ import annotations
|
|
38
|
+
|
|
39
|
+
import atexit
|
|
40
|
+
import json
|
|
41
|
+
import os
|
|
42
|
+
import platform
|
|
43
|
+
import threading
|
|
44
|
+
import time
|
|
45
|
+
import uuid
|
|
46
|
+
from collections import defaultdict
|
|
47
|
+
from typing import Any
|
|
48
|
+
|
|
49
|
+
# Typical token count when Claude reads a source file via Read tool.
|
|
50
|
+
# Used as the denominator for "file reads avoided" estimation.
|
|
51
|
+
AVG_FILE_READ_TOKENS = 1500
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _resolve_user_fingerprint() -> dict[str, str]:
|
|
55
|
+
"""Resolve machine + user identity once at process start.
|
|
56
|
+
|
|
57
|
+
Returns a dict with ``user`` (login name) and ``host`` (hostname).
|
|
58
|
+
Both fall back to ``"unknown"`` if the OS call fails (e.g. inside
|
|
59
|
+
a sandboxed container with no /etc/passwd).
|
|
60
|
+
"""
|
|
61
|
+
try:
|
|
62
|
+
user = os.getlogin()
|
|
63
|
+
except OSError:
|
|
64
|
+
user = os.environ.get("USER", os.environ.get("USERNAME", "unknown"))
|
|
65
|
+
host = platform.node() or "unknown"
|
|
66
|
+
return {"user": user, "host": host}
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class _ToolStats:
|
|
70
|
+
"""Mutable per-tool accumulator (not thread-safe on its own — the
|
|
71
|
+
caller must hold the lock)."""
|
|
72
|
+
|
|
73
|
+
__slots__ = (
|
|
74
|
+
"calls", "errors", "latency_sum", "latency_max",
|
|
75
|
+
"latencies", "result_count_sum", "tokens_served",
|
|
76
|
+
"recall_hits", "top_scores",
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
def __init__(self) -> None:
|
|
80
|
+
self.calls: int = 0
|
|
81
|
+
self.errors: int = 0
|
|
82
|
+
self.latency_sum: float = 0.0
|
|
83
|
+
self.latency_max: float = 0.0
|
|
84
|
+
self.latencies: list[float] = []
|
|
85
|
+
self.result_count_sum: int = 0
|
|
86
|
+
self.tokens_served: int = 0
|
|
87
|
+
self.recall_hits: int = 0 # recalls returning >= 1 result
|
|
88
|
+
self.top_scores: list[float] = [] # best score per recall (precision signal)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class McpAnalytics:
|
|
92
|
+
"""In-process analytics collector for the MADB MCP server.
|
|
93
|
+
|
|
94
|
+
Usage::
|
|
95
|
+
|
|
96
|
+
analytics = McpAnalytics("/path/to/mcp_analytics.jsonl")
|
|
97
|
+
|
|
98
|
+
t0 = time.perf_counter()
|
|
99
|
+
# ... tool work ...
|
|
100
|
+
analytics.record_call("recall", (time.perf_counter()-t0)*1000,
|
|
101
|
+
True, {"result_count": 5, "tokens": 420,
|
|
102
|
+
"top_score": 0.87})
|
|
103
|
+
|
|
104
|
+
print(json.dumps(analytics.snapshot(), indent=2))
|
|
105
|
+
"""
|
|
106
|
+
|
|
107
|
+
def __init__(self, log_path: str, flush_interval: float = 60.0) -> None:
|
|
108
|
+
self._log_path = log_path
|
|
109
|
+
self._flush_interval = flush_interval
|
|
110
|
+
self._fingerprint = _resolve_user_fingerprint()
|
|
111
|
+
self._session_id = str(uuid.uuid4())
|
|
112
|
+
self._session_seq = 0 # monotonic call counter within session
|
|
113
|
+
|
|
114
|
+
self._lock = threading.Lock()
|
|
115
|
+
self._tools: dict[str, _ToolStats] = defaultdict(_ToolStats)
|
|
116
|
+
self._total_calls: int = 0
|
|
117
|
+
self._total_errors: int = 0
|
|
118
|
+
self._start_ts: float = time.time()
|
|
119
|
+
|
|
120
|
+
# Track writes for ROI denominator (tokens written into memory)
|
|
121
|
+
self._memories_written_tokens: int = 0
|
|
122
|
+
|
|
123
|
+
# Pending JSONL lines waiting to be flushed
|
|
124
|
+
self._pending: list[str] = []
|
|
125
|
+
|
|
126
|
+
# Background flush thread
|
|
127
|
+
self._stop = threading.Event()
|
|
128
|
+
self._thread = threading.Thread(
|
|
129
|
+
target=self._flush_loop, daemon=True, name="madb-mcp-analytics"
|
|
130
|
+
)
|
|
131
|
+
self._thread.start()
|
|
132
|
+
atexit.register(self.flush_to_disk)
|
|
133
|
+
|
|
134
|
+
@property
|
|
135
|
+
def session_id(self) -> str:
|
|
136
|
+
return self._session_id
|
|
137
|
+
|
|
138
|
+
# ------------------------------------------------------------------
|
|
139
|
+
# Public API
|
|
140
|
+
# ------------------------------------------------------------------
|
|
141
|
+
|
|
142
|
+
def record_call(
|
|
143
|
+
self,
|
|
144
|
+
tool: str,
|
|
145
|
+
latency_ms: float,
|
|
146
|
+
success: bool,
|
|
147
|
+
meta: dict[str, Any] | None = None,
|
|
148
|
+
) -> None:
|
|
149
|
+
"""Record a single MCP tool invocation."""
|
|
150
|
+
meta = meta or {}
|
|
151
|
+
|
|
152
|
+
with self._lock:
|
|
153
|
+
self._session_seq += 1
|
|
154
|
+
seq = self._session_seq
|
|
155
|
+
|
|
156
|
+
line = json.dumps({
|
|
157
|
+
"ts": time.time(),
|
|
158
|
+
"session_id": self._session_id,
|
|
159
|
+
"session_seq": seq,
|
|
160
|
+
"tool": tool,
|
|
161
|
+
"latency_ms": round(latency_ms, 2),
|
|
162
|
+
"ok": success,
|
|
163
|
+
"user": self._fingerprint["user"],
|
|
164
|
+
"host": self._fingerprint["host"],
|
|
165
|
+
"tenant": meta.get("tenant", ""),
|
|
166
|
+
"result_count": meta.get("result_count", 0),
|
|
167
|
+
"tokens": meta.get("tokens", 0),
|
|
168
|
+
"top_score": meta.get("top_score", 0.0),
|
|
169
|
+
"payload_tokens": meta.get("payload_tokens", 0),
|
|
170
|
+
})
|
|
171
|
+
|
|
172
|
+
with self._lock:
|
|
173
|
+
ts = self._tools[tool]
|
|
174
|
+
ts.calls += 1
|
|
175
|
+
ts.latency_sum += latency_ms
|
|
176
|
+
ts.latencies.append(latency_ms)
|
|
177
|
+
if latency_ms > ts.latency_max:
|
|
178
|
+
ts.latency_max = latency_ms
|
|
179
|
+
if not success:
|
|
180
|
+
ts.errors += 1
|
|
181
|
+
self._total_errors += 1
|
|
182
|
+
ts.result_count_sum += meta.get("result_count", 0)
|
|
183
|
+
ts.tokens_served += meta.get("tokens", 0)
|
|
184
|
+
self._total_calls += 1
|
|
185
|
+
self._pending.append(line)
|
|
186
|
+
|
|
187
|
+
# Recall-specific tracking
|
|
188
|
+
if tool == "recall":
|
|
189
|
+
rc = meta.get("result_count", 0)
|
|
190
|
+
if rc > 0:
|
|
191
|
+
ts.recall_hits += 1
|
|
192
|
+
top = meta.get("top_score", 0.0)
|
|
193
|
+
if top > 0:
|
|
194
|
+
ts.top_scores.append(top)
|
|
195
|
+
|
|
196
|
+
# Track write payload size for ROI denominator
|
|
197
|
+
if tool == "remember":
|
|
198
|
+
self._memories_written_tokens += meta.get("payload_tokens", 0)
|
|
199
|
+
|
|
200
|
+
def snapshot(self) -> dict[str, Any]:
|
|
201
|
+
"""Point-in-time usage summary for the ``analytics`` MCP tool.
|
|
202
|
+
|
|
203
|
+
Includes session context, per-tool stats, recall precision,
|
|
204
|
+
and Memory ROI computation.
|
|
205
|
+
"""
|
|
206
|
+
with self._lock:
|
|
207
|
+
tools: dict[str, Any] = {}
|
|
208
|
+
total_recalls = 0
|
|
209
|
+
recall_hits = 0
|
|
210
|
+
total_tokens_served = 0
|
|
211
|
+
total_results = 0
|
|
212
|
+
all_latencies: list[float] = []
|
|
213
|
+
all_top_scores: list[float] = []
|
|
214
|
+
memories_written = 0
|
|
215
|
+
|
|
216
|
+
for name, ts in self._tools.items():
|
|
217
|
+
avg_lat = ts.latency_sum / ts.calls if ts.calls else 0
|
|
218
|
+
lats = sorted(ts.latencies)
|
|
219
|
+
p50 = _percentile(lats, 50)
|
|
220
|
+
p99 = _percentile(lats, 99)
|
|
221
|
+
tools[name] = {
|
|
222
|
+
"calls": ts.calls,
|
|
223
|
+
"errors": ts.errors,
|
|
224
|
+
"avg_latency_ms": round(avg_lat, 1),
|
|
225
|
+
"p50_latency_ms": round(p50, 1),
|
|
226
|
+
"p99_latency_ms": round(p99, 1),
|
|
227
|
+
"max_latency_ms": round(ts.latency_max, 1),
|
|
228
|
+
}
|
|
229
|
+
all_latencies.extend(ts.latencies)
|
|
230
|
+
|
|
231
|
+
if name == "recall":
|
|
232
|
+
total_recalls = ts.calls
|
|
233
|
+
recall_hits = ts.recall_hits
|
|
234
|
+
total_tokens_served = ts.tokens_served
|
|
235
|
+
total_results = ts.result_count_sum
|
|
236
|
+
all_top_scores = ts.top_scores[:]
|
|
237
|
+
tools[name]["tokens_served"] = ts.tokens_served
|
|
238
|
+
tools[name]["total_results"] = ts.result_count_sum
|
|
239
|
+
tools[name]["recall_hits"] = ts.recall_hits
|
|
240
|
+
|
|
241
|
+
if name == "remember":
|
|
242
|
+
memories_written = ts.calls
|
|
243
|
+
tools[name]["memories_written"] = ts.calls
|
|
244
|
+
|
|
245
|
+
all_sorted = sorted(all_latencies)
|
|
246
|
+
|
|
247
|
+
# --- Recall precision ---
|
|
248
|
+
recall_hit_rate = (
|
|
249
|
+
round(recall_hits / max(total_recalls, 1) * 100, 1)
|
|
250
|
+
if total_recalls > 0 else 0.0
|
|
251
|
+
)
|
|
252
|
+
avg_top_score = (
|
|
253
|
+
round(sum(all_top_scores) / len(all_top_scores), 3)
|
|
254
|
+
if all_top_scores else 0.0
|
|
255
|
+
)
|
|
256
|
+
avg_results_per_recall = (
|
|
257
|
+
round(total_results / max(total_recalls, 1), 1)
|
|
258
|
+
if total_recalls > 0 else 0.0
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
# --- Memory ROI ---
|
|
262
|
+
file_reads_avoided = round(
|
|
263
|
+
total_tokens_served / AVG_FILE_READ_TOKENS, 1
|
|
264
|
+
)
|
|
265
|
+
memory_roi = (
|
|
266
|
+
round(total_tokens_served / max(self._memories_written_tokens, 1), 2)
|
|
267
|
+
if self._memories_written_tokens > 0
|
|
268
|
+
else 0.0
|
|
269
|
+
)
|
|
270
|
+
session_duration = round(time.time() - self._start_ts, 1)
|
|
271
|
+
calls_per_minute = (
|
|
272
|
+
round(self._total_calls / max(session_duration / 60, 0.01), 1)
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
return {
|
|
276
|
+
"session_id": self._session_id,
|
|
277
|
+
"user": self._fingerprint["user"],
|
|
278
|
+
"host": self._fingerprint["host"],
|
|
279
|
+
"session_duration_s": session_duration,
|
|
280
|
+
"session_calls": self._total_calls,
|
|
281
|
+
"calls_per_minute": calls_per_minute,
|
|
282
|
+
"total_calls": self._total_calls,
|
|
283
|
+
"total_errors": self._total_errors,
|
|
284
|
+
"error_rate": round(
|
|
285
|
+
self._total_errors / max(self._total_calls, 1) * 100, 2
|
|
286
|
+
),
|
|
287
|
+
"all_tool_p50_ms": round(_percentile(all_sorted, 50), 1),
|
|
288
|
+
"all_tool_p99_ms": round(_percentile(all_sorted, 99), 1),
|
|
289
|
+
"recall_precision": {
|
|
290
|
+
"hit_rate_pct": recall_hit_rate,
|
|
291
|
+
"avg_top_score": avg_top_score,
|
|
292
|
+
"avg_results_per_recall": avg_results_per_recall,
|
|
293
|
+
"total_recalls": total_recalls,
|
|
294
|
+
"recalls_with_results": recall_hits,
|
|
295
|
+
},
|
|
296
|
+
"memory_roi": {
|
|
297
|
+
"tokens_served_from_recall": total_tokens_served,
|
|
298
|
+
"memories_written": memories_written,
|
|
299
|
+
"memories_written_tokens": self._memories_written_tokens,
|
|
300
|
+
"est_file_reads_avoided": file_reads_avoided,
|
|
301
|
+
"write_to_read_ratio": memory_roi,
|
|
302
|
+
},
|
|
303
|
+
"tools": tools,
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
def flush_to_disk(self) -> None:
|
|
307
|
+
"""Write pending JSONL lines to disk."""
|
|
308
|
+
with self._lock:
|
|
309
|
+
if not self._pending:
|
|
310
|
+
return
|
|
311
|
+
lines = self._pending[:]
|
|
312
|
+
self._pending.clear()
|
|
313
|
+
|
|
314
|
+
# Ensure parent directory exists
|
|
315
|
+
os.makedirs(os.path.dirname(self._log_path) or ".", exist_ok=True)
|
|
316
|
+
with open(self._log_path, "a") as f:
|
|
317
|
+
for line in lines:
|
|
318
|
+
f.write(line + "\n")
|
|
319
|
+
|
|
320
|
+
def stop(self) -> None:
|
|
321
|
+
"""Stop the background flush thread (for clean shutdown)."""
|
|
322
|
+
self._stop.set()
|
|
323
|
+
self._thread.join(timeout=2)
|
|
324
|
+
|
|
325
|
+
# ------------------------------------------------------------------
|
|
326
|
+
# Internal
|
|
327
|
+
# ------------------------------------------------------------------
|
|
328
|
+
|
|
329
|
+
def _flush_loop(self) -> None:
|
|
330
|
+
while not self._stop.wait(self._flush_interval):
|
|
331
|
+
self.flush_to_disk()
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _percentile(sorted_values: list[float], pct: float) -> float:
|
|
335
|
+
"""Compute a percentile from a pre-sorted list."""
|
|
336
|
+
if not sorted_values:
|
|
337
|
+
return 0.0
|
|
338
|
+
k = (len(sorted_values) - 1) * (pct / 100)
|
|
339
|
+
f = int(k)
|
|
340
|
+
c = f + 1
|
|
341
|
+
if c >= len(sorted_values):
|
|
342
|
+
return sorted_values[-1]
|
|
343
|
+
return sorted_values[f] + (k - f) * (sorted_values[c] - sorted_values[f])
|