3232import json
3333import os
3434import sys
35+ import time
3536import traceback
3637
3738from mcp .server import Server
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
5864def _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+
67133def _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 ()
459533async 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