spacehub

surshar / claude-code

Claude Code is an agentic coding tool that lives in your terminal, understands your codebase, and helps you code faster by executing routine tasks, explaining complex code, and handling git workflows - all through natural language commands.

Built by Anthropic · github.com/anthropics

claude-code / plugins / mcp-context-warning / hooks / check_mcp_context.py
164 lines · 4.8 KBRaw
1#!/usr/bin/env python3
2"""
3MCP Context Warning Hook for Claude Code
4
5This hook runs at session start and warns users when their MCP servers
6are consuming significant context (>20k tokens). This helps users understand
7why they might be burning through context quickly.
8"""
9
10import json
11import os
12import sys
13from pathlib import Path
14
15# Threshold in tokens for showing a warning
16TOKEN_WARNING_THRESHOLD = 20000
17
18# Estimated tokens per MCP tool (based on real-world data:
19# Asana: ~836/tool, Gmail: ~833/tool, Google Calendar: ~1867/tool, Google Drive: ~1260/tool)
20# Using 1000 as a conservative average
21TOKENS_PER_TOOL_ESTIMATE = 1000
22
23# Base overhead per MCP server (connection info, auth, etc.)
24BASE_TOKENS_PER_SERVER = 500
25
26
27def find_mcp_configs(cwd: str) -> list[tuple[str, dict]]:
28 """
29 Find all MCP configuration files that might be loaded.
30
31 Returns list of (path, config) tuples.
32 """
33 configs = []
34
35 # Check project-level .mcp.json
36 project_mcp = Path(cwd) / ".mcp.json"
37 if project_mcp.exists():
38 try:
39 with open(project_mcp) as f:
40 configs.append((str(project_mcp), json.load(f)))
41 except (json.JSONDecodeError, IOError):
42 pass
43
44 # Check user-level MCP config (~/.claude/.mcp.json)
45 user_mcp = Path.home() / ".claude" / ".mcp.json"
46 if user_mcp.exists():
47 try:
48 with open(user_mcp) as f:
49 configs.append((str(user_mcp), json.load(f)))
50 except (json.JSONDecodeError, IOError):
51 pass
52
53 return configs
54
55
56def count_servers_and_estimate_tools(configs: list[tuple[str, dict]]) -> tuple[dict, int]:
57 """
58 Count MCP servers and estimate their tool counts.
59
60 Returns (server_info dict, total_estimated_tokens).
61 """
62 server_info = {}
63
64 for path, config in configs:
65 # MCP config format: {"mcpServers": {"server-name": {...}}}
66 mcp_servers = config.get("mcpServers", {})
67
68 for server_name, server_config in mcp_servers.items():
69 if server_name in server_info:
70 continue # Skip duplicates
71
72 # Try to get tool count from config if available
73 # Otherwise use a default estimate
74 tool_count = server_config.get("toolCount", 10) # Default 10 tools
75
76 server_info[server_name] = {
77 "source": path,
78 "estimated_tools": tool_count,
79 }
80
81 # Calculate total estimated tokens
82 total_tokens = 0
83 for server_name, info in server_info.items():
84 server_tokens = BASE_TOKENS_PER_SERVER + (info["estimated_tools"] * TOKENS_PER_TOOL_ESTIMATE)
85 info["estimated_tokens"] = server_tokens
86 total_tokens += server_tokens
87
88 return server_info, total_tokens
89
90
91def format_warning_message(server_info: dict, total_tokens: int) -> str:
92 """Format a warning message about MCP context usage."""
93
94 # Build server breakdown table
95 lines = [
96 f"Your MCP servers are using an estimated ~{total_tokens:,} tokens of context.",
97 "",
98 "Server breakdown:",
99 ]
100
101 # Sort servers by token usage (highest first)
102 sorted_servers = sorted(
103 server_info.items(),
104 key=lambda x: x[1]["estimated_tokens"],
105 reverse=True
106 )
107
108 for server_name, info in sorted_servers:
109 tokens = info["estimated_tokens"]
110 tools = info["estimated_tools"]
111 lines.append(f" - {server_name}: ~{tokens:,} tokens ({tools} tools)")
112
113 lines.extend([
114 "",
115 "Consider disabling MCP servers you're not actively using to conserve context.",
116 "You can manage MCP servers with `/mcp disable <server-name>` or by editing .mcp.json.",
117 ])
118
119 return "\n".join(lines)
120
121
122def main():
123 """Main hook function."""
124 # Read input from stdin
125 try:
126 raw_input = sys.stdin.read()
127 input_data = json.loads(raw_input) if raw_input.strip() else {}
128 except json.JSONDecodeError:
129 # If we can't parse input, exit silently
130 sys.exit(0)
131
132 # Get current working directory from hook input or environment
133 cwd = input_data.get("cwd", os.environ.get("CLAUDE_PROJECT_DIR", os.getcwd()))
134
135 # Find MCP configurations
136 configs = find_mcp_configs(cwd)
137
138 if not configs:
139 # No MCP configs found, nothing to warn about
140 sys.exit(0)
141
142 # Count servers and estimate tokens
143 server_info, total_tokens = count_servers_and_estimate_tools(configs)
144
145 if not server_info:
146 # No servers configured
147 sys.exit(0)
148
149 # Check if we're over the warning threshold
150 if total_tokens >= TOKEN_WARNING_THRESHOLD:
151 warning_message = format_warning_message(server_info, total_tokens)
152
153 # Output JSON with systemMessage for Claude to see
154 output = {
155 "continue": True,
156 "systemMessage": warning_message
157 }
158 print(json.dumps(output))
159
160 sys.exit(0)
161
162
163if __name__ == "__main__":
164 main()