Skip to content

Commit 945afd8

Browse files
RecoDemoclaude
andcommitted
Add get_usage_stats tool for session efficiency metrics
Tracks tool call counts and characters returned per session. Reports estimated token savings vs reading full source files. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 9364721 commit 945afd8

1 file changed

Lines changed: 85 additions & 2 deletions

File tree

src/mcp_codebase_index/server.py

Lines changed: 85 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
import json
3333
import os
3434
import sys
35+
import time
3536
import traceback
3637

3738
from mcp.server import Server
@@ -54,6 +55,11 @@
5455
_query_fns: dict | None = None
5556
_is_git: bool = False
5657

58+
# Session usage stats
59+
_session_start: float = time.time()
60+
_tool_call_counts: dict[str, int] = {}
61+
_total_chars_returned: int = 0
62+
5763

5864
def _format_result(value: object) -> str:
5965
"""Format a query result as readable text."""
@@ -64,6 +70,66 @@ def _format_result(value: object) -> str:
6470
return str(value)
6571

6672

73+
def _format_usage_stats() -> str:
74+
"""Format session usage statistics."""
75+
elapsed = time.time() - _session_start
76+
total_calls = sum(_tool_call_counts.values())
77+
# Don't count get_usage_stats itself in the query total
78+
query_calls = total_calls - _tool_call_counts.get("get_usage_stats", 0)
79+
80+
# Calculate total source size from the index
81+
source_chars = 0
82+
if _indexer and _indexer._project_index:
83+
source_chars = sum(m.total_chars for m in _indexer._project_index.files.values())
84+
85+
lines = [
86+
f"Session duration: {_format_duration(elapsed)}",
87+
f"Total queries: {query_calls}",
88+
]
89+
90+
if _tool_call_counts:
91+
lines.append("")
92+
lines.append("Queries by tool:")
93+
for tool_name, count in sorted(_tool_call_counts.items(), key=lambda x: -x[1]):
94+
if tool_name == "get_usage_stats":
95+
continue
96+
lines.append(f" {tool_name}: {count}")
97+
98+
lines.append("")
99+
lines.append(f"Total chars returned: {_total_chars_returned:,}")
100+
101+
if source_chars > 0:
102+
lines.append(f"Total source in index: {source_chars:,} chars")
103+
if query_calls > 0 and source_chars > _total_chars_returned:
104+
# Each query could have required reading the full source
105+
naive_chars = source_chars * query_calls
106+
reduction = (1 - _total_chars_returned / naive_chars) * 100 if naive_chars > 0 else 0
107+
lines.append(
108+
f"Estimated without indexer: {naive_chars:,} chars "
109+
f"({naive_chars // 4:,} tokens) over {query_calls} queries"
110+
)
111+
lines.append(
112+
f"Estimated with indexer: {_total_chars_returned:,} chars "
113+
f"({_total_chars_returned // 4:,} tokens)"
114+
)
115+
lines.append(f"Estimated token savings: {reduction:.1f}%")
116+
117+
return "\n".join(lines)
118+
119+
120+
def _format_duration(seconds: float) -> str:
121+
"""Format seconds into a human-readable duration."""
122+
if seconds < 60:
123+
return f"{seconds:.0f}s"
124+
minutes = int(seconds // 60)
125+
secs = int(seconds % 60)
126+
if minutes < 60:
127+
return f"{minutes}m {secs}s"
128+
hours = minutes // 60
129+
mins = minutes % 60
130+
return f"{hours}h {mins}m"
131+
132+
67133
def _build_index() -> None:
68134
"""Build (or rebuild) the project index and query functions."""
69135
global _project_root, _indexer, _query_fns, _is_git
@@ -442,6 +508,14 @@ def _maybe_incremental_update() -> None:
442508
"properties": {},
443509
},
444510
),
511+
Tool(
512+
name="get_usage_stats",
513+
description="Session efficiency stats: tool calls, characters returned vs total source, estimated token savings.",
514+
inputSchema={
515+
"type": "object",
516+
"properties": {},
517+
},
518+
),
445519
]
446520

447521

@@ -457,14 +531,21 @@ async def list_tools() -> list[Tool]:
457531

458532
@server.call_tool()
459533
async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
460-
global _query_fns
534+
global _query_fns, _total_chars_returned
535+
536+
# Track tool call counts (including reindex/stats themselves)
537+
_tool_call_counts[name] = _tool_call_counts.get(name, 0) + 1
461538

462539
try:
463540
# Handle reindex separately since it rebuilds state
464541
if name == "reindex":
465542
_build_index()
466543
return [TextContent(type="text", text="Project re-indexed successfully.")]
467544

545+
# Handle usage stats
546+
if name == "get_usage_stats":
547+
return [TextContent(type="text", text=_format_usage_stats())]
548+
468549
_maybe_incremental_update()
469550

470551
if _query_fns is None:
@@ -557,7 +638,9 @@ async def call_tool(name: str, arguments: dict) -> list[types.TextContent]:
557638
else:
558639
return [TextContent(type="text", text=f"Error: unknown tool '{name}'")]
559640

560-
return [TextContent(type="text", text=_format_result(result))]
641+
formatted = _format_result(result)
642+
_total_chars_returned += len(formatted)
643+
return [TextContent(type="text", text=formatted)]
561644

562645
except Exception as e:
563646
tb = traceback.format_exc()

0 commit comments

Comments
 (0)