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
| 1 | #!/usr/bin/env python3 |
| 2 | """ |
| 3 | MCP Context Warning Hook for Claude Code |
| 4 | |
| 5 | This hook runs at session start and warns users when their MCP servers |
| 6 | are consuming significant context (>20k tokens). This helps users understand |
| 7 | why they might be burning through context quickly. |
| 8 | """ |
| 9 | |
| 10 | import json |
| 11 | import os |
| 12 | import sys |
| 13 | from pathlib import Path |
| 14 | |
| 15 | # Threshold in tokens for showing a warning |
| 16 | TOKEN_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 |
| 21 | TOKENS_PER_TOOL_ESTIMATE = 1000 |
| 22 | |
| 23 | # Base overhead per MCP server (connection info, auth, etc.) |
| 24 | BASE_TOKENS_PER_SERVER = 500 |
| 25 | |
| 26 | |
| 27 | def 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 | |
| 56 | def 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 | |
| 91 | def 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 | |
| 122 | def 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 | |
| 163 | if __name__ == "__main__": |
| 164 | main() |