hanzo-mcp 0.6.12__py3-none-any.whl → 0.7.0__py3-none-any.whl

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.

Potentially problematic release.


This version of hanzo-mcp might be problematic. Click here for more details.

Files changed (117) hide show
  1. hanzo_mcp/__init__.py +2 -2
  2. hanzo_mcp/analytics/__init__.py +5 -0
  3. hanzo_mcp/analytics/posthog_analytics.py +364 -0
  4. hanzo_mcp/cli.py +5 -5
  5. hanzo_mcp/cli_enhanced.py +7 -7
  6. hanzo_mcp/cli_plugin.py +91 -0
  7. hanzo_mcp/config/__init__.py +1 -1
  8. hanzo_mcp/config/settings.py +70 -7
  9. hanzo_mcp/config/tool_config.py +20 -6
  10. hanzo_mcp/dev_server.py +3 -3
  11. hanzo_mcp/prompts/project_system.py +1 -1
  12. hanzo_mcp/server.py +40 -3
  13. hanzo_mcp/server_enhanced.py +69 -0
  14. hanzo_mcp/tools/__init__.py +140 -31
  15. hanzo_mcp/tools/agent/__init__.py +85 -4
  16. hanzo_mcp/tools/agent/agent_tool.py +104 -6
  17. hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
  18. hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
  19. hanzo_mcp/tools/agent/clarification_tool.py +68 -0
  20. hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
  22. hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
  23. hanzo_mcp/tools/agent/code_auth.py +436 -0
  24. hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
  25. hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
  26. hanzo_mcp/tools/agent/critic_tool.py +376 -0
  27. hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
  28. hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
  29. hanzo_mcp/tools/agent/iching_tool.py +380 -0
  30. hanzo_mcp/tools/agent/network_tool.py +273 -0
  31. hanzo_mcp/tools/agent/prompt.py +62 -20
  32. hanzo_mcp/tools/agent/review_tool.py +433 -0
  33. hanzo_mcp/tools/agent/swarm_tool.py +535 -0
  34. hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
  35. hanzo_mcp/tools/common/__init__.py +15 -1
  36. hanzo_mcp/tools/common/base.py +5 -4
  37. hanzo_mcp/tools/common/batch_tool.py +103 -11
  38. hanzo_mcp/tools/common/config_tool.py +2 -2
  39. hanzo_mcp/tools/common/context.py +2 -2
  40. hanzo_mcp/tools/common/context_fix.py +26 -0
  41. hanzo_mcp/tools/common/critic_tool.py +196 -0
  42. hanzo_mcp/tools/common/decorators.py +208 -0
  43. hanzo_mcp/tools/common/enhanced_base.py +106 -0
  44. hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
  45. hanzo_mcp/tools/common/forgiving_edit.py +243 -0
  46. hanzo_mcp/tools/common/mode.py +116 -0
  47. hanzo_mcp/tools/common/mode_loader.py +105 -0
  48. hanzo_mcp/tools/common/paginated_base.py +230 -0
  49. hanzo_mcp/tools/common/paginated_response.py +307 -0
  50. hanzo_mcp/tools/common/pagination.py +226 -0
  51. hanzo_mcp/tools/common/permissions.py +1 -1
  52. hanzo_mcp/tools/common/personality.py +936 -0
  53. hanzo_mcp/tools/common/plugin_loader.py +287 -0
  54. hanzo_mcp/tools/common/stats.py +4 -4
  55. hanzo_mcp/tools/common/tool_list.py +4 -1
  56. hanzo_mcp/tools/common/truncate.py +101 -0
  57. hanzo_mcp/tools/common/validation.py +1 -1
  58. hanzo_mcp/tools/config/__init__.py +3 -1
  59. hanzo_mcp/tools/config/config_tool.py +1 -1
  60. hanzo_mcp/tools/config/mode_tool.py +209 -0
  61. hanzo_mcp/tools/database/__init__.py +1 -1
  62. hanzo_mcp/tools/editor/__init__.py +1 -1
  63. hanzo_mcp/tools/filesystem/__init__.py +48 -14
  64. hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
  65. hanzo_mcp/tools/filesystem/batch_search.py +3 -3
  66. hanzo_mcp/tools/filesystem/diff.py +2 -2
  67. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
  68. hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
  69. hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
  70. hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
  71. hanzo_mcp/tools/filesystem/watch.py +3 -2
  72. hanzo_mcp/tools/jupyter/__init__.py +2 -2
  73. hanzo_mcp/tools/jupyter/jupyter.py +1 -1
  74. hanzo_mcp/tools/llm/__init__.py +3 -3
  75. hanzo_mcp/tools/llm/llm_tool.py +648 -143
  76. hanzo_mcp/tools/lsp/__init__.py +5 -0
  77. hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
  78. hanzo_mcp/tools/mcp/__init__.py +2 -2
  79. hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
  80. hanzo_mcp/tools/memory/__init__.py +76 -0
  81. hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
  82. hanzo_mcp/tools/memory/memory_tools.py +456 -0
  83. hanzo_mcp/tools/search/__init__.py +6 -0
  84. hanzo_mcp/tools/search/find_tool.py +581 -0
  85. hanzo_mcp/tools/search/unified_search.py +953 -0
  86. hanzo_mcp/tools/shell/__init__.py +11 -6
  87. hanzo_mcp/tools/shell/auto_background.py +203 -0
  88. hanzo_mcp/tools/shell/base_process.py +57 -29
  89. hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
  90. hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +18 -34
  91. hanzo_mcp/tools/shell/command_executor.py +2 -2
  92. hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +16 -33
  93. hanzo_mcp/tools/shell/open.py +2 -2
  94. hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
  95. hanzo_mcp/tools/shell/run_command_windows.py +1 -1
  96. hanzo_mcp/tools/shell/streaming_command.py +594 -0
  97. hanzo_mcp/tools/shell/uvx.py +47 -2
  98. hanzo_mcp/tools/shell/uvx_background.py +47 -2
  99. hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +16 -33
  100. hanzo_mcp/tools/todo/__init__.py +14 -19
  101. hanzo_mcp/tools/todo/todo.py +22 -1
  102. hanzo_mcp/tools/vector/__init__.py +1 -1
  103. hanzo_mcp/tools/vector/infinity_store.py +2 -2
  104. hanzo_mcp/tools/vector/project_manager.py +1 -1
  105. hanzo_mcp/types.py +23 -0
  106. hanzo_mcp-0.7.0.dist-info/METADATA +516 -0
  107. hanzo_mcp-0.7.0.dist-info/RECORD +180 -0
  108. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +1 -0
  109. hanzo_mcp/tools/common/palette.py +0 -344
  110. hanzo_mcp/tools/common/palette_loader.py +0 -108
  111. hanzo_mcp/tools/config/palette_tool.py +0 -179
  112. hanzo_mcp/tools/llm/llm_unified.py +0 -851
  113. hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
  114. hanzo_mcp-0.6.12.dist-info/RECORD +0 -135
  115. hanzo_mcp-0.6.12.dist-info/licenses/LICENSE +0 -21
  116. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
  117. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,243 @@
1
+ """Forgiving edit helper for AI-friendly text matching."""
2
+
3
+ import difflib
4
+ import re
5
+ from typing import List, Optional, Tuple
6
+
7
+
8
+ class ForgivingEditHelper:
9
+ """Helper class to make text editing more forgiving for AI usage.
10
+
11
+ This helper normalizes whitespace, handles partial matches, and provides
12
+ suggestions when exact matches fail.
13
+ """
14
+
15
+ @staticmethod
16
+ def normalize_whitespace(text: str) -> str:
17
+ """Normalize whitespace while preserving structure.
18
+
19
+ Args:
20
+ text: Text to normalize
21
+
22
+ Returns:
23
+ Text with normalized whitespace
24
+ """
25
+ # Replace tabs with spaces for comparison
26
+ text = text.replace('\t', ' ')
27
+
28
+ # Normalize multiple spaces to single space (except at line start)
29
+ lines = []
30
+ for line in text.split('\n'):
31
+ # Preserve indentation
32
+ indent = len(line) - len(line.lstrip())
33
+ content = line.strip()
34
+ if content:
35
+ # Normalize spaces in content
36
+ content = ' '.join(content.split())
37
+ lines.append(' ' * indent + content)
38
+
39
+ return '\n'.join(lines)
40
+
41
+ @staticmethod
42
+ def find_fuzzy_match(
43
+ haystack: str,
44
+ needle: str,
45
+ threshold: float = 0.85
46
+ ) -> Optional[Tuple[int, int, str]]:
47
+ """Find a fuzzy match for the needle in the haystack.
48
+
49
+ Args:
50
+ haystack: Text to search in
51
+ needle: Text to search for
52
+ threshold: Similarity threshold (0-1)
53
+
54
+ Returns:
55
+ Tuple of (start_pos, end_pos, matched_text) or None
56
+ """
57
+ # First try exact match
58
+ if needle in haystack:
59
+ start = haystack.index(needle)
60
+ return (start, start + len(needle), needle)
61
+
62
+ # Normalize for comparison
63
+ norm_haystack = ForgivingEditHelper.normalize_whitespace(haystack)
64
+ norm_needle = ForgivingEditHelper.normalize_whitespace(needle)
65
+
66
+ # Try normalized exact match
67
+ if norm_needle in norm_haystack:
68
+ # Find the match in normalized text
69
+ norm_start = norm_haystack.index(norm_needle)
70
+
71
+ # Map back to original text
72
+ # This is approximate but usually good enough
73
+ lines_before = norm_haystack[:norm_start].count('\n')
74
+
75
+ # Find corresponding position in original
76
+ original_lines = haystack.split('\n')
77
+ norm_lines = norm_haystack.split('\n')
78
+
79
+ start_pos = sum(len(line) + 1 for line in original_lines[:lines_before])
80
+
81
+ # Find end position by counting lines in needle
82
+ needle_lines = norm_needle.count('\n') + 1
83
+ end_pos = sum(len(line) + 1 for line in original_lines[:lines_before + needle_lines])
84
+
85
+ matched = '\n'.join(original_lines[lines_before:lines_before + needle_lines])
86
+ return (start_pos, end_pos - 1, matched)
87
+
88
+ # Try fuzzy matching on lines
89
+ haystack_lines = haystack.split('\n')
90
+ needle_lines = needle.split('\n')
91
+
92
+ if len(needle_lines) == 1:
93
+ # Single line - find best match
94
+ needle_norm = ForgivingEditHelper.normalize_whitespace(needle)
95
+ best_ratio = 0
96
+ best_match = None
97
+
98
+ for i, line in enumerate(haystack_lines):
99
+ line_norm = ForgivingEditHelper.normalize_whitespace(line)
100
+ ratio = difflib.SequenceMatcher(None, line_norm, needle_norm).ratio()
101
+
102
+ if ratio > best_ratio and ratio >= threshold:
103
+ best_ratio = ratio
104
+ start_pos = sum(len(l) + 1 for l in haystack_lines[:i])
105
+ best_match = (start_pos, start_pos + len(line), line)
106
+
107
+ return best_match
108
+
109
+ else:
110
+ # Multi-line - find sequence match
111
+ for i in range(len(haystack_lines) - len(needle_lines) + 1):
112
+ candidate_lines = haystack_lines[i:i + len(needle_lines)]
113
+ candidate = '\n'.join(candidate_lines)
114
+ candidate_norm = ForgivingEditHelper.normalize_whitespace(candidate)
115
+ needle_norm = ForgivingEditHelper.normalize_whitespace(needle)
116
+
117
+ ratio = difflib.SequenceMatcher(None, candidate_norm, needle_norm).ratio()
118
+
119
+ if ratio >= threshold:
120
+ start_pos = sum(len(l) + 1 for l in haystack_lines[:i])
121
+ return (start_pos, start_pos + len(candidate), candidate)
122
+
123
+ return None
124
+
125
+ @staticmethod
126
+ def suggest_matches(
127
+ haystack: str,
128
+ needle: str,
129
+ max_suggestions: int = 3
130
+ ) -> List[Tuple[float, str]]:
131
+ """Suggest possible matches when exact match fails.
132
+
133
+ Args:
134
+ haystack: Text to search in
135
+ needle: Text to search for
136
+ max_suggestions: Maximum number of suggestions
137
+
138
+ Returns:
139
+ List of (similarity_score, text) tuples
140
+ """
141
+ suggestions = []
142
+
143
+ # Normalize needle
144
+ needle_norm = ForgivingEditHelper.normalize_whitespace(needle)
145
+ needle_lines = needle.split('\n')
146
+
147
+ if len(needle_lines) == 1:
148
+ # Single line - compare with all lines
149
+ for line in haystack.split('\n'):
150
+ if line.strip(): # Skip empty lines
151
+ line_norm = ForgivingEditHelper.normalize_whitespace(line)
152
+ ratio = difflib.SequenceMatcher(None, line_norm, needle_norm).ratio()
153
+ if ratio > 0.5: # Only reasonably similar lines
154
+ suggestions.append((ratio, line))
155
+
156
+ else:
157
+ # Multi-line - use sliding window
158
+ haystack_lines = haystack.split('\n')
159
+ window_size = len(needle_lines)
160
+
161
+ for i in range(len(haystack_lines) - window_size + 1):
162
+ candidate_lines = haystack_lines[i:i + window_size]
163
+ candidate = '\n'.join(candidate_lines)
164
+ candidate_norm = ForgivingEditHelper.normalize_whitespace(candidate)
165
+
166
+ ratio = difflib.SequenceMatcher(None, candidate_norm, needle_norm).ratio()
167
+ if ratio > 0.5:
168
+ suggestions.append((ratio, candidate))
169
+
170
+ # Sort by similarity and return top matches
171
+ suggestions.sort(reverse=True, key=lambda x: x[0])
172
+ return suggestions[:max_suggestions]
173
+
174
+ @staticmethod
175
+ def create_edit_suggestion(
176
+ file_content: str,
177
+ old_string: str,
178
+ new_string: str
179
+ ) -> dict:
180
+ """Create a helpful edit suggestion when match fails.
181
+
182
+ Args:
183
+ file_content: Current file content
184
+ old_string: String that couldn't be found
185
+ new_string: Replacement string
186
+
187
+ Returns:
188
+ Dict with error message and suggestions
189
+ """
190
+ # Try fuzzy match
191
+ fuzzy_match = ForgivingEditHelper.find_fuzzy_match(file_content, old_string)
192
+
193
+ if fuzzy_match:
194
+ _, _, matched_text = fuzzy_match
195
+ return {
196
+ "error": "Exact match not found, but found similar text",
197
+ "found": matched_text,
198
+ "suggestion": "Use this as old_string instead",
199
+ "confidence": "high"
200
+ }
201
+
202
+ # Get suggestions
203
+ suggestions = ForgivingEditHelper.suggest_matches(file_content, old_string)
204
+
205
+ if suggestions:
206
+ return {
207
+ "error": "Could not find exact or fuzzy match",
208
+ "suggestions": [
209
+ {"similarity": f"{score:.0%}", "text": text}
210
+ for score, text in suggestions
211
+ ],
212
+ "hint": "Try using one of these suggestions as old_string"
213
+ }
214
+
215
+ # No good matches - provide general help
216
+ return {
217
+ "error": "Could not find any matches",
218
+ "hints": [
219
+ "Check for whitespace differences (tabs vs spaces)",
220
+ "Ensure you're including complete lines",
221
+ "Try a smaller, more unique portion of text",
222
+ "Use the streaming_command tool to view the file with visible whitespace"
223
+ ]
224
+ }
225
+
226
+ @staticmethod
227
+ def prepare_edit_string(text: str) -> str:
228
+ """Prepare a string for editing by handling common issues.
229
+
230
+ Args:
231
+ text: Text to prepare
232
+
233
+ Returns:
234
+ Cleaned text ready for editing
235
+ """
236
+ # Remove any line number prefixes (common in AI copy-paste)
237
+ lines = []
238
+ for line in text.split('\n'):
239
+ # Remove common line number patterns
240
+ cleaned = re.sub(r'^\s*\d+[:\|\-\s]\s*', '', line)
241
+ lines.append(cleaned)
242
+
243
+ return '\n'.join(lines)
@@ -0,0 +1,116 @@
1
+ """Mode system for organizing development tools based on programmer personalities."""
2
+
3
+ import os
4
+ from dataclasses import dataclass
5
+ from typing import Dict, List, Optional, Set
6
+
7
+ from hanzo_mcp.tools.common.personality import (
8
+ ToolPersonality,
9
+ register_default_personalities,
10
+ ensure_agent_enabled,
11
+ personalities
12
+ )
13
+
14
+
15
+ @dataclass
16
+ class Mode(ToolPersonality):
17
+ """Development mode combining tool preferences and environment settings."""
18
+ # Inherits all fields from ToolPersonality
19
+ # Adds mode-specific functionality
20
+
21
+ @property
22
+ def is_active(self) -> bool:
23
+ """Check if this mode is currently active."""
24
+ return ModeRegistry.get_active() == self
25
+
26
+
27
+ class ModeRegistry:
28
+ """Registry for development modes."""
29
+
30
+ _modes: Dict[str, Mode] = {}
31
+ _active_mode: Optional[str] = None
32
+
33
+ @classmethod
34
+ def register(cls, mode: Mode) -> None:
35
+ """Register a development mode."""
36
+ # Ensure agent is enabled if API keys present
37
+ mode = ensure_agent_enabled(mode)
38
+ cls._modes[mode.name] = mode
39
+
40
+ @classmethod
41
+ def get(cls, name: str) -> Optional[Mode]:
42
+ """Get a mode by name."""
43
+ return cls._modes.get(name)
44
+
45
+ @classmethod
46
+ def list(cls) -> List[Mode]:
47
+ """List all registered modes."""
48
+ return list(cls._modes.values())
49
+
50
+ @classmethod
51
+ def set_active(cls, name: str) -> None:
52
+ """Set the active mode."""
53
+ if name not in cls._modes:
54
+ raise ValueError(f"Mode '{name}' not found")
55
+ cls._active_mode = name
56
+
57
+ # Apply environment variables from the mode
58
+ mode = cls._modes[name]
59
+ if mode.environment:
60
+ for key, value in mode.environment.items():
61
+ os.environ[key] = value
62
+
63
+ @classmethod
64
+ def get_active(cls) -> Optional[Mode]:
65
+ """Get the active mode."""
66
+ if cls._active_mode:
67
+ return cls._modes.get(cls._active_mode)
68
+ return None
69
+
70
+ @classmethod
71
+ def get_active_tools(cls) -> Set[str]:
72
+ """Get the set of tools from the active mode."""
73
+ mode = cls.get_active()
74
+ if mode:
75
+ return set(mode.tools)
76
+ return set()
77
+
78
+
79
+ def register_default_modes():
80
+ """Register all default development modes."""
81
+ # Convert personalities to modes
82
+ for personality in personalities:
83
+ mode = Mode(
84
+ name=personality.name,
85
+ programmer=personality.programmer,
86
+ description=personality.description,
87
+ tools=personality.tools,
88
+ environment=personality.environment,
89
+ philosophy=personality.philosophy,
90
+ )
91
+ ModeRegistry.register(mode)
92
+
93
+
94
+ def get_mode_from_env() -> Optional[str]:
95
+ """Get mode name from environment variables."""
96
+ # Check for HANZO_MODE, PERSONALITY, or MODE env vars
97
+ return (
98
+ os.environ.get("HANZO_MODE") or
99
+ os.environ.get("PERSONALITY") or
100
+ os.environ.get("MODE")
101
+ )
102
+
103
+
104
+ def activate_mode_from_env():
105
+ """Activate mode based on environment variables."""
106
+ mode_name = get_mode_from_env()
107
+ if mode_name:
108
+ try:
109
+ ModeRegistry.set_active(mode_name)
110
+ return True
111
+ except ValueError:
112
+ # Mode not found, ignore
113
+ pass
114
+ return False
115
+
116
+
@@ -0,0 +1,105 @@
1
+ """Tool mode loader for dynamic tool configuration."""
2
+
3
+ import os
4
+ from typing import Dict, List, Optional, Set
5
+
6
+ from hanzo_mcp.tools.common.mode import ModeRegistry, register_default_modes, activate_mode_from_env
7
+
8
+
9
+ class ModeLoader:
10
+ """Loads and manages tool modes for dynamic configuration."""
11
+
12
+ @staticmethod
13
+ def initialize_modes() -> None:
14
+ """Initialize the mode system with defaults."""
15
+ # Initialize modes
16
+ register_default_modes()
17
+
18
+ # Check for mode from environment
19
+ activate_mode_from_env()
20
+
21
+ # If no mode set, use default
22
+ if not ModeRegistry.get_active():
23
+ default_mode = os.environ.get("HANZO_DEFAULT_MODE", "hanzo")
24
+ if ModeRegistry.get(default_mode):
25
+ ModeRegistry.set_active(default_mode)
26
+
27
+ @staticmethod
28
+ def get_enabled_tools_from_mode(
29
+ base_enabled_tools: Optional[Dict[str, bool]] = None,
30
+ force_mode: Optional[str] = None
31
+ ) -> Dict[str, bool]:
32
+ """Get enabled tools configuration from active mode.
33
+
34
+ Args:
35
+ base_enabled_tools: Base configuration to merge with
36
+ force_mode: Force a specific mode (overrides active)
37
+
38
+ Returns:
39
+ Dictionary of tool enable states
40
+ """
41
+ # Initialize if needed
42
+ if not ModeRegistry.list():
43
+ ModeLoader.initialize_modes()
44
+
45
+ # Get mode to use
46
+ tools_list = None
47
+
48
+ if force_mode:
49
+ # Set and get mode
50
+ if ModeRegistry.get(force_mode):
51
+ ModeRegistry.set_active(force_mode)
52
+ mode = ModeRegistry.get_active()
53
+ tools_list = mode.tools if mode else None
54
+ else:
55
+ # Check active mode
56
+ mode = ModeRegistry.get_active()
57
+ if mode:
58
+ tools_list = mode.tools
59
+
60
+ if not tools_list:
61
+ # No active mode, return base config
62
+ return base_enabled_tools or {}
63
+
64
+ # Start with base configuration
65
+ result = base_enabled_tools.copy() if base_enabled_tools else {}
66
+
67
+ # Get all possible tools from registry
68
+ from hanzo_mcp.config.tool_config import TOOL_REGISTRY
69
+ all_possible_tools = set(TOOL_REGISTRY.keys())
70
+
71
+ # Disable all tools first (clean slate for mode)
72
+ for tool in all_possible_tools:
73
+ result[tool] = False
74
+
75
+ # Enable tools from mode
76
+ for tool in tools_list:
77
+ result[tool] = True
78
+
79
+ # Always enable mode tool (meta)
80
+ result["mode"] = True
81
+
82
+ return result
83
+
84
+ @staticmethod
85
+ def get_environment_from_mode() -> Dict[str, str]:
86
+ """Get environment variables from active mode.
87
+
88
+ Returns:
89
+ Dictionary of environment variables
90
+ """
91
+ # Check mode
92
+ mode = ModeRegistry.get_active()
93
+ if mode and mode.environment:
94
+ return mode.environment.copy()
95
+
96
+ return {}
97
+
98
+ @staticmethod
99
+ def apply_environment_from_mode() -> None:
100
+ """Apply environment variables from active mode."""
101
+ env_vars = ModeLoader.get_environment_from_mode()
102
+ for key, value in env_vars.items():
103
+ os.environ[key] = value
104
+
105
+
@@ -0,0 +1,230 @@
1
+ """Enhanced base class with automatic pagination support.
2
+
3
+ This module provides a base class that automatically handles pagination
4
+ for all tool responses that exceed MCP token limits.
5
+ """
6
+
7
+ import json
8
+ from abc import abstractmethod
9
+ from typing import Any, Dict, Optional, Union
10
+
11
+ from mcp.server.fastmcp import Context as MCPContext
12
+
13
+ from hanzo_mcp.tools.common.base import BaseTool, handle_connection_errors
14
+ from hanzo_mcp.tools.common.paginated_response import paginate_if_needed
15
+ from hanzo_mcp.tools.common.pagination import CursorManager
16
+
17
+
18
+ class PaginatedBaseTool(BaseTool):
19
+ """Base class for tools with automatic pagination support.
20
+
21
+ This base class automatically handles pagination for responses that
22
+ exceed MCP token limits, making all tools pagination-aware by default.
23
+ """
24
+
25
+ def __init__(self):
26
+ """Initialize the paginated base tool."""
27
+ super().__init__()
28
+ self._supports_pagination = True
29
+
30
+ @abstractmethod
31
+ async def execute(self, ctx: MCPContext, **params: Any) -> Any:
32
+ """Execute the tool logic and return raw results.
33
+
34
+ This method should be implemented by subclasses to perform the
35
+ actual tool logic. The base class will handle pagination of
36
+ the returned results automatically.
37
+
38
+ Args:
39
+ ctx: MCP context
40
+ **params: Tool parameters
41
+
42
+ Returns:
43
+ Raw tool results (will be paginated if needed)
44
+ """
45
+ pass
46
+
47
+ @handle_connection_errors
48
+ async def call(self, ctx: MCPContext, **params: Any) -> Union[str, Dict[str, Any]]:
49
+ """Execute the tool with automatic pagination support.
50
+
51
+ This method wraps the execute() method and automatically handles
52
+ pagination if the response exceeds token limits.
53
+
54
+ Args:
55
+ ctx: MCP context
56
+ **params: Tool parameters including optional 'cursor'
57
+
58
+ Returns:
59
+ Tool result, potentially paginated
60
+ """
61
+ # Extract cursor if provided
62
+ cursor = params.pop("cursor", None)
63
+
64
+ # Validate cursor if provided
65
+ if cursor and not CursorManager.parse_cursor(cursor):
66
+ return {"error": "Invalid cursor provided", "code": -32602}
67
+
68
+ # Check if this is a continuation request
69
+ if cursor:
70
+ # For continuation, check if we have cached results
71
+ cursor_data = CursorManager.parse_cursor(cursor)
72
+ if cursor_data and "tool" in cursor_data and cursor_data["tool"] != self.name:
73
+ return {"error": "Cursor is for a different tool", "code": -32602}
74
+
75
+ # Execute the tool
76
+ try:
77
+ result = await self.execute(ctx, **params)
78
+ except Exception as e:
79
+ # Format errors consistently
80
+ return {"error": str(e), "type": type(e).__name__}
81
+
82
+ # Handle pagination automatically
83
+ if self._supports_pagination:
84
+ paginated_result = paginate_if_needed(result, cursor)
85
+
86
+ # If pagination occurred, add tool info to help with continuation
87
+ if isinstance(paginated_result, dict) and "nextCursor" in paginated_result:
88
+ # Enhance the cursor with tool information
89
+ if "nextCursor" in paginated_result:
90
+ cursor_data = CursorManager.parse_cursor(paginated_result["nextCursor"])
91
+ if cursor_data:
92
+ cursor_data["tool"] = self.name
93
+ cursor_data["params"] = params # Store params for continuation
94
+ paginated_result["nextCursor"] = CursorManager.create_cursor(cursor_data)
95
+
96
+ return paginated_result
97
+ else:
98
+ # Return raw result if pagination is disabled
99
+ return result
100
+
101
+ def disable_pagination(self):
102
+ """Disable automatic pagination for this tool.
103
+
104
+ Some tools may want to handle their own pagination logic.
105
+ """
106
+ self._supports_pagination = False
107
+
108
+ def enable_pagination(self):
109
+ """Re-enable automatic pagination for this tool."""
110
+ self._supports_pagination = True
111
+
112
+
113
+ class PaginatedFileSystemTool(PaginatedBaseTool):
114
+ """Base class for filesystem tools with pagination support."""
115
+
116
+ def __init__(self, permission_manager):
117
+ """Initialize filesystem tool with pagination.
118
+
119
+ Args:
120
+ permission_manager: Permission manager for access control
121
+ """
122
+ super().__init__()
123
+ self.permission_manager = permission_manager
124
+
125
+ def is_path_allowed(self, path: str) -> bool:
126
+ """Check if a path is allowed according to permission settings.
127
+
128
+ Args:
129
+ path: Path to check
130
+
131
+ Returns:
132
+ True if the path is allowed, False otherwise
133
+ """
134
+ return self.permission_manager.is_path_allowed(path)
135
+
136
+
137
+ def migrate_tool_to_paginated(tool_class):
138
+ """Decorator to migrate existing tools to use pagination.
139
+
140
+ This decorator can be applied to existing tool classes to add
141
+ automatic pagination support without modifying their code.
142
+
143
+ Usage:
144
+ @migrate_tool_to_paginated
145
+ class MyTool(BaseTool):
146
+ ...
147
+ """
148
+ class PaginatedWrapper(PaginatedBaseTool):
149
+ def __init__(self, *args, **kwargs):
150
+ super().__init__()
151
+ self._wrapped_tool = tool_class(*args, **kwargs)
152
+
153
+ @property
154
+ def name(self):
155
+ return self._wrapped_tool.name
156
+
157
+ @property
158
+ def description(self):
159
+ # Add pagination info to description
160
+ desc = self._wrapped_tool.description
161
+ if not "pagination" in desc.lower():
162
+ desc += "\n\nThis tool supports automatic pagination. If the response is too large, it will be split across multiple requests. Use the returned cursor to continue."
163
+ return desc
164
+
165
+ async def execute(self, ctx: MCPContext, **params: Any) -> Any:
166
+ # Call the wrapped tool's call method
167
+ return await self._wrapped_tool.call(ctx, **params)
168
+
169
+ def register(self, mcp_server):
170
+ # Need to create a new registration that includes cursor parameter
171
+ tool_self = self
172
+
173
+ # Get the original registration function
174
+ original_register = self._wrapped_tool.register
175
+
176
+ # Create a new registration that adds cursor support
177
+ def register_with_pagination(server):
178
+ # First register the original tool
179
+ original_register(server)
180
+
181
+ # Then override with pagination support
182
+ import inspect
183
+
184
+ # Get the registered function
185
+ tool_func = None
186
+ for name, func in server._tools.items():
187
+ if name == self.name:
188
+ tool_func = func
189
+ break
190
+
191
+ if tool_func:
192
+ # Get original signature
193
+ sig = inspect.signature(tool_func)
194
+ params = list(sig.parameters.values())
195
+
196
+ # Add cursor parameter if not present
197
+ has_cursor = any(p.name == "cursor" for p in params)
198
+ if not has_cursor:
199
+ from typing import Optional
200
+ import inspect
201
+
202
+ # Create new parameter with cursor
203
+ cursor_param = inspect.Parameter(
204
+ "cursor",
205
+ inspect.Parameter.KEYWORD_ONLY,
206
+ default=None,
207
+ annotation=Optional[str]
208
+ )
209
+
210
+ # Insert before ctx parameter
211
+ new_params = []
212
+ for p in params:
213
+ if p.name == "ctx":
214
+ new_params.append(cursor_param)
215
+ new_params.append(p)
216
+
217
+ # Create wrapper function
218
+ async def paginated_wrapper(**kwargs):
219
+ return await tool_self.call(kwargs.get("ctx"), **kwargs)
220
+
221
+ # Update registration
222
+ server._tools[self.name] = paginated_wrapper
223
+
224
+ register_with_pagination(mcp_server)
225
+
226
+ # Set the class name
227
+ PaginatedWrapper.__name__ = f"Paginated{tool_class.__name__}"
228
+ PaginatedWrapper.__qualname__ = f"Paginated{tool_class.__qualname__}"
229
+
230
+ return PaginatedWrapper