hanzo-mcp 0.8.8__py3-none-any.whl → 0.9.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 (167) hide show
  1. hanzo_mcp/__init__.py +1 -3
  2. hanzo_mcp/analytics/posthog_analytics.py +4 -17
  3. hanzo_mcp/bridge.py +9 -25
  4. hanzo_mcp/cli.py +8 -17
  5. hanzo_mcp/cli_enhanced.py +5 -14
  6. hanzo_mcp/cli_plugin.py +3 -9
  7. hanzo_mcp/config/settings.py +6 -20
  8. hanzo_mcp/config/tool_config.py +2 -4
  9. hanzo_mcp/core/base_agent.py +88 -88
  10. hanzo_mcp/core/model_registry.py +238 -210
  11. hanzo_mcp/dev_server.py +5 -15
  12. hanzo_mcp/prompts/__init__.py +2 -6
  13. hanzo_mcp/prompts/project_todo_reminder.py +3 -9
  14. hanzo_mcp/prompts/tool_explorer.py +1 -3
  15. hanzo_mcp/prompts/utils.py +7 -21
  16. hanzo_mcp/server.py +6 -7
  17. hanzo_mcp/tools/__init__.py +29 -32
  18. hanzo_mcp/tools/agent/__init__.py +2 -1
  19. hanzo_mcp/tools/agent/agent.py +10 -30
  20. hanzo_mcp/tools/agent/agent_tool.py +23 -17
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +3 -9
  22. hanzo_mcp/tools/agent/cli_agent_base.py +7 -24
  23. hanzo_mcp/tools/agent/cli_tools.py +76 -75
  24. hanzo_mcp/tools/agent/code_auth.py +1 -3
  25. hanzo_mcp/tools/agent/code_auth_tool.py +2 -6
  26. hanzo_mcp/tools/agent/critic_tool.py +8 -24
  27. hanzo_mcp/tools/agent/iching_tool.py +12 -36
  28. hanzo_mcp/tools/agent/network_tool.py +7 -18
  29. hanzo_mcp/tools/agent/prompt.py +1 -5
  30. hanzo_mcp/tools/agent/review_tool.py +10 -25
  31. hanzo_mcp/tools/agent/swarm_alias.py +1 -3
  32. hanzo_mcp/tools/agent/unified_cli_tools.py +38 -38
  33. hanzo_mcp/tools/common/batch_tool.py +15 -45
  34. hanzo_mcp/tools/common/config_tool.py +9 -28
  35. hanzo_mcp/tools/common/context.py +1 -3
  36. hanzo_mcp/tools/common/critic_tool.py +1 -3
  37. hanzo_mcp/tools/common/decorators.py +2 -6
  38. hanzo_mcp/tools/common/enhanced_base.py +2 -6
  39. hanzo_mcp/tools/common/fastmcp_pagination.py +4 -12
  40. hanzo_mcp/tools/common/forgiving_edit.py +9 -28
  41. hanzo_mcp/tools/common/mode.py +1 -5
  42. hanzo_mcp/tools/common/paginated_base.py +3 -11
  43. hanzo_mcp/tools/common/paginated_response.py +10 -30
  44. hanzo_mcp/tools/common/pagination.py +3 -9
  45. hanzo_mcp/tools/common/path_utils.py +34 -0
  46. hanzo_mcp/tools/common/permissions.py +14 -13
  47. hanzo_mcp/tools/common/personality.py +983 -701
  48. hanzo_mcp/tools/common/plugin_loader.py +3 -15
  49. hanzo_mcp/tools/common/stats.py +7 -19
  50. hanzo_mcp/tools/common/thinking_tool.py +1 -3
  51. hanzo_mcp/tools/common/tool_disable.py +2 -6
  52. hanzo_mcp/tools/common/tool_list.py +2 -6
  53. hanzo_mcp/tools/common/validation.py +1 -3
  54. hanzo_mcp/tools/compiler/__init__.py +8 -0
  55. hanzo_mcp/tools/compiler/sandboxed_compiler.py +681 -0
  56. hanzo_mcp/tools/config/config_tool.py +7 -13
  57. hanzo_mcp/tools/config/index_config.py +1 -3
  58. hanzo_mcp/tools/config/mode_tool.py +5 -15
  59. hanzo_mcp/tools/database/database_manager.py +3 -9
  60. hanzo_mcp/tools/database/graph.py +1 -3
  61. hanzo_mcp/tools/database/graph_add.py +3 -9
  62. hanzo_mcp/tools/database/graph_query.py +11 -34
  63. hanzo_mcp/tools/database/graph_remove.py +3 -9
  64. hanzo_mcp/tools/database/graph_search.py +6 -20
  65. hanzo_mcp/tools/database/graph_stats.py +11 -33
  66. hanzo_mcp/tools/database/sql.py +4 -12
  67. hanzo_mcp/tools/database/sql_query.py +6 -10
  68. hanzo_mcp/tools/database/sql_search.py +2 -6
  69. hanzo_mcp/tools/database/sql_stats.py +5 -15
  70. hanzo_mcp/tools/editor/neovim_command.py +1 -3
  71. hanzo_mcp/tools/editor/neovim_session.py +7 -13
  72. hanzo_mcp/tools/environment/__init__.py +8 -0
  73. hanzo_mcp/tools/environment/environment_detector.py +594 -0
  74. hanzo_mcp/tools/filesystem/__init__.py +28 -26
  75. hanzo_mcp/tools/filesystem/ast_multi_edit.py +14 -43
  76. hanzo_mcp/tools/filesystem/ast_tool.py +3 -0
  77. hanzo_mcp/tools/filesystem/base.py +20 -12
  78. hanzo_mcp/tools/filesystem/content_replace.py +7 -12
  79. hanzo_mcp/tools/filesystem/diff.py +2 -10
  80. hanzo_mcp/tools/filesystem/directory_tree.py +285 -51
  81. hanzo_mcp/tools/filesystem/edit.py +10 -18
  82. hanzo_mcp/tools/filesystem/find.py +312 -179
  83. hanzo_mcp/tools/filesystem/git_search.py +12 -24
  84. hanzo_mcp/tools/filesystem/multi_edit.py +10 -18
  85. hanzo_mcp/tools/filesystem/read.py +14 -30
  86. hanzo_mcp/tools/filesystem/rules_tool.py +9 -17
  87. hanzo_mcp/tools/filesystem/search.py +1160 -0
  88. hanzo_mcp/tools/filesystem/watch.py +2 -4
  89. hanzo_mcp/tools/filesystem/write.py +7 -10
  90. hanzo_mcp/tools/framework/__init__.py +8 -0
  91. hanzo_mcp/tools/framework/framework_modes.py +714 -0
  92. hanzo_mcp/tools/jupyter/base.py +6 -20
  93. hanzo_mcp/tools/jupyter/jupyter.py +4 -12
  94. hanzo_mcp/tools/llm/consensus_tool.py +8 -24
  95. hanzo_mcp/tools/llm/llm_manage.py +2 -6
  96. hanzo_mcp/tools/llm/llm_tool.py +17 -58
  97. hanzo_mcp/tools/llm/llm_unified.py +18 -59
  98. hanzo_mcp/tools/llm/provider_tools.py +1 -3
  99. hanzo_mcp/tools/lsp/lsp_tool.py +621 -481
  100. hanzo_mcp/tools/mcp/mcp_add.py +3 -5
  101. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  102. hanzo_mcp/tools/mcp/mcp_stats.py +1 -3
  103. hanzo_mcp/tools/mcp/mcp_tool.py +9 -23
  104. hanzo_mcp/tools/memory/__init__.py +33 -40
  105. hanzo_mcp/tools/memory/conversation_memory.py +636 -0
  106. hanzo_mcp/tools/memory/knowledge_tools.py +7 -25
  107. hanzo_mcp/tools/memory/memory_tools.py +7 -19
  108. hanzo_mcp/tools/search/find_tool.py +12 -34
  109. hanzo_mcp/tools/search/unified_search.py +27 -81
  110. hanzo_mcp/tools/shell/__init__.py +16 -4
  111. hanzo_mcp/tools/shell/auto_background.py +2 -6
  112. hanzo_mcp/tools/shell/base.py +1 -5
  113. hanzo_mcp/tools/shell/base_process.py +5 -7
  114. hanzo_mcp/tools/shell/bash_session.py +7 -24
  115. hanzo_mcp/tools/shell/bash_session_executor.py +5 -15
  116. hanzo_mcp/tools/shell/bash_tool.py +3 -7
  117. hanzo_mcp/tools/shell/command_executor.py +26 -79
  118. hanzo_mcp/tools/shell/logs.py +4 -16
  119. hanzo_mcp/tools/shell/npx.py +2 -8
  120. hanzo_mcp/tools/shell/npx_tool.py +1 -3
  121. hanzo_mcp/tools/shell/pkill.py +4 -12
  122. hanzo_mcp/tools/shell/process_tool.py +2 -8
  123. hanzo_mcp/tools/shell/processes.py +5 -17
  124. hanzo_mcp/tools/shell/run_background.py +1 -3
  125. hanzo_mcp/tools/shell/run_command.py +1 -3
  126. hanzo_mcp/tools/shell/run_command_windows.py +1 -3
  127. hanzo_mcp/tools/shell/run_tool.py +56 -0
  128. hanzo_mcp/tools/shell/session_manager.py +2 -6
  129. hanzo_mcp/tools/shell/session_storage.py +2 -6
  130. hanzo_mcp/tools/shell/streaming_command.py +7 -23
  131. hanzo_mcp/tools/shell/uvx.py +4 -14
  132. hanzo_mcp/tools/shell/uvx_background.py +2 -6
  133. hanzo_mcp/tools/shell/uvx_tool.py +1 -3
  134. hanzo_mcp/tools/shell/zsh_tool.py +12 -20
  135. hanzo_mcp/tools/todo/todo.py +1 -3
  136. hanzo_mcp/tools/vector/__init__.py +97 -50
  137. hanzo_mcp/tools/vector/ast_analyzer.py +6 -20
  138. hanzo_mcp/tools/vector/git_ingester.py +10 -30
  139. hanzo_mcp/tools/vector/index_tool.py +3 -9
  140. hanzo_mcp/tools/vector/infinity_store.py +11 -30
  141. hanzo_mcp/tools/vector/mock_infinity.py +159 -0
  142. hanzo_mcp/tools/vector/node_tool.py +538 -0
  143. hanzo_mcp/tools/vector/project_manager.py +4 -12
  144. hanzo_mcp/tools/vector/unified_vector.py +384 -0
  145. hanzo_mcp/tools/vector/vector.py +2 -6
  146. hanzo_mcp/tools/vector/vector_index.py +8 -8
  147. hanzo_mcp/tools/vector/vector_search.py +7 -21
  148. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/METADATA +2 -2
  149. hanzo_mcp-0.9.0.dist-info/RECORD +191 -0
  150. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +0 -645
  151. hanzo_mcp/tools/agent/swarm_tool.py +0 -723
  152. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +0 -577
  153. hanzo_mcp/tools/filesystem/batch_search.py +0 -900
  154. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +0 -350
  155. hanzo_mcp/tools/filesystem/find_files.py +0 -369
  156. hanzo_mcp/tools/filesystem/grep.py +0 -467
  157. hanzo_mcp/tools/filesystem/search_tool.py +0 -767
  158. hanzo_mcp/tools/filesystem/symbols_tool.py +0 -515
  159. hanzo_mcp/tools/filesystem/tree.py +0 -270
  160. hanzo_mcp/tools/jupyter/notebook_edit.py +0 -317
  161. hanzo_mcp/tools/jupyter/notebook_read.py +0 -147
  162. hanzo_mcp/tools/todo/todo_read.py +0 -143
  163. hanzo_mcp/tools/todo/todo_write.py +0 -374
  164. hanzo_mcp-0.8.8.dist-info/RECORD +0 -192
  165. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/WHEEL +0 -0
  166. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/entry_points.txt +0 -0
  167. {hanzo_mcp-0.8.8.dist-info → hanzo_mcp-0.9.0.dist-info}/top_level.txt +0 -0
@@ -168,9 +168,7 @@ Note: Requires Neovim to be installed.
168
168
  project_path,
169
169
  )
170
170
  elif action == "restore":
171
- return await self._restore_session(
172
- tool_ctx, session_name, project_session_dir
173
- )
171
+ return await self._restore_session(tool_ctx, session_name, project_session_dir)
174
172
  elif action == "list":
175
173
  return self._list_sessions(project_session_dir, project_path)
176
174
  elif action == "delete":
@@ -214,9 +212,7 @@ Note: Requires Neovim to be installed.
214
212
  # Run Neovim to save session
215
213
  # First, check if Neovim is already running
216
214
  # For now, we'll create a new instance
217
- result = subprocess.run(
218
- ["nvim", "-c", vim_script.strip()], capture_output=True, text=True
219
- )
215
+ result = subprocess.run(["nvim", "-c", vim_script.strip()], capture_output=True, text=True)
220
216
 
221
217
  if result.returncode != 0 and result.stderr:
222
218
  return f"Error saving session: {result.stderr}"
@@ -246,15 +242,15 @@ Or manually in Neovim:
246
242
  except Exception as e:
247
243
  return f"Error saving session: {str(e)}"
248
244
 
249
- async def _restore_session(
250
- self, tool_ctx, session_name: Optional[str], project_dir: Path
251
- ) -> str:
245
+ async def _restore_session(self, tool_ctx, session_name: Optional[str], project_dir: Path) -> str:
252
246
  """Restore Neovim session."""
253
247
  if not session_name:
254
248
  # List available sessions
255
249
  sessions = list(project_dir.glob("*.vim"))
256
250
  if not sessions:
257
- return "Error: No sessions found for this project. Use 'neovim_session --action list' to see all sessions."
251
+ return (
252
+ "Error: No sessions found for this project. Use 'neovim_session --action list' to see all sessions."
253
+ )
258
254
 
259
255
  # Use most recent
260
256
  sessions.sort(key=lambda x: x.stat().st_mtime, reverse=True)
@@ -330,9 +326,7 @@ Or manually in Neovim:
330
326
  if len(other_sessions) > 10:
331
327
  output.append(f" ... and {len(other_sessions) - 10} more")
332
328
 
333
- output.append(
334
- "\nUse 'neovim_session --action restore --session-name <name>' to restore a session."
335
- )
329
+ output.append("\nUse 'neovim_session --action restore --session-name <name>' to restore a session.")
336
330
 
337
331
  return "\n".join(output)
338
332
 
@@ -0,0 +1,8 @@
1
+ """Environment detection and configuration tools."""
2
+
3
+ from .environment_detector import EnvironmentDetector, create_environment_detector
4
+
5
+ __all__ = [
6
+ "EnvironmentDetector",
7
+ "create_environment_detector",
8
+ ]
@@ -0,0 +1,594 @@
1
+ """Environment detection tool for automatic development environment configuration.
2
+
3
+ This tool automatically detects installed development tools, frameworks, and
4
+ languages in the user's environment and dynamically loads appropriate MCP tools.
5
+ """
6
+
7
+ import os
8
+ import sys
9
+ import json
10
+ import shutil
11
+ import subprocess
12
+ import logging
13
+ from typing import Dict, List, Any, Optional, Set
14
+ from pathlib import Path
15
+ from dataclasses import dataclass, field
16
+
17
+ from hanzo_mcp.types import MCPResourceDocument
18
+ from hanzo_mcp.tools.common.base import BaseTool
19
+
20
+
21
+ @dataclass
22
+ class ToolDetection:
23
+ """Represents a detected development tool."""
24
+ name: str
25
+ version: Optional[str]
26
+ path: str
27
+ category: str # language, framework, database, etc.
28
+ mcp_tools: List[str] = field(default_factory=list) # Related MCP tools to enable
29
+ environment: Dict[str, str] = field(default_factory=dict) # Environment variables
30
+
31
+
32
+ # Tool detection configurations
33
+ TOOL_DETECTIONS = {
34
+ # Programming Languages
35
+ "python": {
36
+ "check_cmd": ["python", "--version"],
37
+ "category": "language",
38
+ "mcp_tools": ["run_command", "uvx", "uvx_background"],
39
+ "parse_version": lambda out: out.split()[1] if out else None,
40
+ "frameworks": {
41
+ "django": {
42
+ "check": ["python", "-c", "import django; print(django.get_version())"],
43
+ "mcp_tools": ["django_manage", "django_shell"],
44
+ },
45
+ "fastapi": {
46
+ "check": ["python", "-c", "import fastapi; print(fastapi.__version__)"],
47
+ "mcp_tools": ["uvicorn", "fastapi_routes"],
48
+ },
49
+ "flask": {
50
+ "check": ["python", "-c", "import flask; print(flask.__version__)"],
51
+ "mcp_tools": ["flask_run"],
52
+ },
53
+ },
54
+ },
55
+ "node": {
56
+ "check_cmd": ["node", "--version"],
57
+ "category": "language",
58
+ "mcp_tools": ["npx", "npx_background"],
59
+ "parse_version": lambda out: out.strip().lstrip("v") if out else None,
60
+ "frameworks": {
61
+ "next": {
62
+ "check": ["npm", "list", "next", "--json"],
63
+ "mcp_tools": ["next_dev", "next_build"],
64
+ },
65
+ "react": {
66
+ "check": ["npm", "list", "react", "--json"],
67
+ "mcp_tools": ["react_dev"],
68
+ },
69
+ "vue": {
70
+ "check": ["npm", "list", "vue", "--json"],
71
+ "mcp_tools": ["vue_serve"],
72
+ },
73
+ "angular": {
74
+ "check": ["ng", "version"],
75
+ "mcp_tools": ["ng_serve", "ng_build"],
76
+ },
77
+ },
78
+ },
79
+ "go": {
80
+ "check_cmd": ["go", "version"],
81
+ "category": "language",
82
+ "mcp_tools": ["go_build", "go_run", "go_test"],
83
+ "parse_version": lambda out: out.split()[2].lstrip("go") if out else None,
84
+ "frameworks": {
85
+ "gin": {
86
+ "check_file": "go.mod",
87
+ "check_content": "github.com/gin-gonic/gin",
88
+ "mcp_tools": ["gin_server"],
89
+ },
90
+ "echo": {
91
+ "check_file": "go.mod",
92
+ "check_content": "github.com/labstack/echo",
93
+ "mcp_tools": ["echo_server"],
94
+ },
95
+ },
96
+ },
97
+ "rust": {
98
+ "check_cmd": ["rustc", "--version"],
99
+ "category": "language",
100
+ "mcp_tools": ["cargo_build", "cargo_run", "cargo_test"],
101
+ "parse_version": lambda out: out.split()[1] if out else None,
102
+ "frameworks": {
103
+ "actix": {
104
+ "check_file": "Cargo.toml",
105
+ "check_content": 'actix-web',
106
+ "mcp_tools": ["actix_server"],
107
+ },
108
+ "rocket": {
109
+ "check_file": "Cargo.toml",
110
+ "check_content": 'rocket',
111
+ "mcp_tools": ["rocket_server"],
112
+ },
113
+ },
114
+ },
115
+ "java": {
116
+ "check_cmd": ["java", "--version"],
117
+ "category": "language",
118
+ "mcp_tools": ["javac", "java_run"],
119
+ "parse_version": lambda out: out.split()[1] if out else None,
120
+ "frameworks": {
121
+ "spring": {
122
+ "check_file": "pom.xml",
123
+ "check_content": "spring-boot",
124
+ "mcp_tools": ["spring_boot", "mvn"],
125
+ },
126
+ },
127
+ },
128
+ "cpp": {
129
+ "check_cmd": ["clang++", "--version"],
130
+ "category": "language",
131
+ "mcp_tools": ["cmake", "make", "clang_compile"],
132
+ "parse_version": lambda out: out.split()[2] if "version" in out else None,
133
+ },
134
+ "c": {
135
+ "check_cmd": ["clang", "--version"],
136
+ "category": "language",
137
+ "mcp_tools": ["cmake", "make", "clang_compile"],
138
+ "parse_version": lambda out: out.split()[2] if "version" in out else None,
139
+ },
140
+ "zig": {
141
+ "check_cmd": ["zig", "version"],
142
+ "category": "language",
143
+ "mcp_tools": ["zig_build", "zig_run", "zig_test"],
144
+ "parse_version": lambda out: out.strip() if out else None,
145
+ },
146
+ # Databases
147
+ "postgresql": {
148
+ "check_cmd": ["psql", "--version"],
149
+ "category": "database",
150
+ "mcp_tools": ["sql_query", "sql_search", "sql_stats"],
151
+ "parse_version": lambda out: out.split()[-1] if out else None,
152
+ },
153
+ "mysql": {
154
+ "check_cmd": ["mysql", "--version"],
155
+ "category": "database",
156
+ "mcp_tools": ["sql_query", "sql_search", "sql_stats"],
157
+ "parse_version": lambda out: out.split()[4].rstrip(",") if out else None,
158
+ },
159
+ "redis": {
160
+ "check_cmd": ["redis-cli", "--version"],
161
+ "category": "database",
162
+ "mcp_tools": ["redis_get", "redis_set", "redis_keys"],
163
+ "parse_version": lambda out: out.split()[2] if out else None,
164
+ },
165
+ "mongodb": {
166
+ "check_cmd": ["mongod", "--version"],
167
+ "category": "database",
168
+ "mcp_tools": ["mongo_query", "mongo_insert"],
169
+ "parse_version": lambda out: out.split()[2] if "version" in out else None,
170
+ },
171
+ # Container & Orchestration
172
+ "docker": {
173
+ "check_cmd": ["docker", "--version"],
174
+ "category": "container",
175
+ "mcp_tools": ["docker_build", "docker_run", "docker_ps"],
176
+ "parse_version": lambda out: out.split()[2].rstrip(",") if out else None,
177
+ },
178
+ "kubernetes": {
179
+ "check_cmd": ["kubectl", "version", "--client", "--short"],
180
+ "category": "orchestration",
181
+ "mcp_tools": ["kubectl_apply", "kubectl_get", "kubectl_logs"],
182
+ "parse_version": lambda out: out.split()[-1] if out else None,
183
+ },
184
+ # Version Control
185
+ "git": {
186
+ "check_cmd": ["git", "--version"],
187
+ "category": "vcs",
188
+ "mcp_tools": ["git_search", "git_log", "git_diff"],
189
+ "parse_version": lambda out: out.split()[-1] if out else None,
190
+ },
191
+ # Package Managers
192
+ "npm": {
193
+ "check_cmd": ["npm", "--version"],
194
+ "category": "package_manager",
195
+ "mcp_tools": ["npx", "npm_install"],
196
+ "parse_version": lambda out: out.strip() if out else None,
197
+ },
198
+ "yarn": {
199
+ "check_cmd": ["yarn", "--version"],
200
+ "category": "package_manager",
201
+ "mcp_tools": ["yarn_install", "yarn_run"],
202
+ "parse_version": lambda out: out.strip() if out else None,
203
+ },
204
+ "pnpm": {
205
+ "check_cmd": ["pnpm", "--version"],
206
+ "category": "package_manager",
207
+ "mcp_tools": ["pnpm_install", "pnpm_run"],
208
+ "parse_version": lambda out: out.strip() if out else None,
209
+ },
210
+ "pip": {
211
+ "check_cmd": ["pip", "--version"],
212
+ "category": "package_manager",
213
+ "mcp_tools": ["pip_install", "pip_freeze"],
214
+ "parse_version": lambda out: out.split()[1] if out else None,
215
+ },
216
+ "cargo": {
217
+ "check_cmd": ["cargo", "--version"],
218
+ "category": "package_manager",
219
+ "mcp_tools": ["cargo_build", "cargo_run"],
220
+ "parse_version": lambda out: out.split()[1] if out else None,
221
+ },
222
+ # Build Tools
223
+ "make": {
224
+ "check_cmd": ["make", "--version"],
225
+ "category": "build",
226
+ "mcp_tools": ["make_build", "make_clean"],
227
+ "parse_version": lambda out: out.split()[-1] if out else None,
228
+ },
229
+ "cmake": {
230
+ "check_cmd": ["cmake", "--version"],
231
+ "category": "build",
232
+ "mcp_tools": ["cmake_configure", "cmake_build"],
233
+ "parse_version": lambda out: out.split()[2] if out else None,
234
+ },
235
+ }
236
+
237
+
238
+ class EnvironmentDetector(BaseTool):
239
+ """Tool for detecting and configuring development environment."""
240
+
241
+ name = "env_detect"
242
+ description = """Detect development environment and auto-configure tools.
243
+
244
+ Actions:
245
+ - detect: Detect all installed development tools
246
+ - check: Check specific tool availability
247
+ - suggest: Suggest tools to install for project
248
+ - configure: Auto-configure MCP tools based on detection
249
+ - export: Export environment configuration
250
+
251
+ This tool helps automatically configure your development environment
252
+ by detecting installed languages, frameworks, databases, and tools.
253
+ """
254
+
255
+ def __init__(self):
256
+ super().__init__()
257
+ self.logger = logging.getLogger(__name__)
258
+ self.detected_tools: List[ToolDetection] = []
259
+ self.project_root = self._find_project_root()
260
+
261
+ def _find_project_root(self) -> Path:
262
+ """Find project root directory."""
263
+ markers = [
264
+ ".git", "package.json", "pyproject.toml", "Cargo.toml",
265
+ "go.mod", "pom.xml", "build.gradle", "CMakeLists.txt"
266
+ ]
267
+
268
+ cwd = Path.cwd()
269
+ for parent in [cwd] + list(cwd.parents):
270
+ for marker in markers:
271
+ if (parent / marker).exists():
272
+ return parent
273
+
274
+ return cwd
275
+
276
+ def _run_command(self, cmd: List[str]) -> Optional[str]:
277
+ """Run command and return output."""
278
+ try:
279
+ result = subprocess.run(
280
+ cmd,
281
+ capture_output=True,
282
+ text=True,
283
+ timeout=5,
284
+ check=False,
285
+ )
286
+
287
+ if result.returncode == 0:
288
+ return result.stdout.strip()
289
+
290
+ except (subprocess.TimeoutExpired, FileNotFoundError):
291
+ pass
292
+
293
+ return None
294
+
295
+ def _check_file_content(self, file_path: str, content: str) -> bool:
296
+ """Check if file contains specific content."""
297
+ try:
298
+ file = self.project_root / file_path
299
+ if file.exists():
300
+ with open(file, "r") as f:
301
+ return content in f.read()
302
+ except Exception:
303
+ pass
304
+
305
+ return False
306
+
307
+ def _detect_tool(self, tool_name: str, config: Dict[str, Any]) -> Optional[ToolDetection]:
308
+ """Detect a specific tool."""
309
+ # Check main tool
310
+ output = self._run_command(config["check_cmd"])
311
+ if not output:
312
+ return None
313
+
314
+ # Parse version
315
+ version = None
316
+ if "parse_version" in config:
317
+ try:
318
+ version = config["parse_version"](output)
319
+ except Exception:
320
+ pass
321
+
322
+ # Find executable path
323
+ executable = config["check_cmd"][0]
324
+ path = shutil.which(executable) or executable
325
+
326
+ # Create detection
327
+ detection = ToolDetection(
328
+ name=tool_name,
329
+ version=version,
330
+ path=path,
331
+ category=config["category"],
332
+ mcp_tools=config.get("mcp_tools", []),
333
+ )
334
+
335
+ # Check frameworks if applicable
336
+ if "frameworks" in config:
337
+ for fw_name, fw_config in config["frameworks"].items():
338
+ if self._check_framework(fw_config):
339
+ detection.mcp_tools.extend(fw_config.get("mcp_tools", []))
340
+ self.logger.info(f"Detected {fw_name} framework")
341
+
342
+ return detection
343
+
344
+ def _check_framework(self, config: Dict[str, Any]) -> bool:
345
+ """Check if framework is present."""
346
+ if "check" in config:
347
+ output = self._run_command(config["check"])
348
+ return output is not None
349
+
350
+ if "check_file" in config and "check_content" in config:
351
+ return self._check_file_content(config["check_file"], config["check_content"])
352
+
353
+ return False
354
+
355
+ def detect_all(self) -> List[ToolDetection]:
356
+ """Detect all available tools."""
357
+ self.detected_tools = []
358
+
359
+ for tool_name, config in TOOL_DETECTIONS.items():
360
+ detection = self._detect_tool(tool_name, config)
361
+ if detection:
362
+ self.detected_tools.append(detection)
363
+ self.logger.info(f"Detected {tool_name} v{detection.version}")
364
+
365
+ return self.detected_tools
366
+
367
+ def suggest_tools(self) -> Dict[str, List[str]]:
368
+ """Suggest tools to install based on project."""
369
+ suggestions = {
370
+ "languages": [],
371
+ "frameworks": [],
372
+ "tools": [],
373
+ }
374
+
375
+ # Check project files
376
+ files = list(self.project_root.glob("*"))
377
+ file_names = [f.name for f in files]
378
+
379
+ # Language suggestions
380
+ if "package.json" in file_names and "node" not in [d.name for d in self.detected_tools]:
381
+ suggestions["languages"].append("node")
382
+
383
+ if "requirements.txt" in file_names and "python" not in [d.name for d in self.detected_tools]:
384
+ suggestions["languages"].append("python")
385
+
386
+ if "Cargo.toml" in file_names and "rust" not in [d.name for d in self.detected_tools]:
387
+ suggestions["languages"].append("rust")
388
+
389
+ if "go.mod" in file_names and "go" not in [d.name for d in self.detected_tools]:
390
+ suggestions["languages"].append("go")
391
+
392
+ # Tool suggestions
393
+ if any(d.category == "language" for d in self.detected_tools):
394
+ if "git" not in [d.name for d in self.detected_tools]:
395
+ suggestions["tools"].append("git")
396
+
397
+ if "docker" not in [d.name for d in self.detected_tools]:
398
+ suggestions["tools"].append("docker")
399
+
400
+ return suggestions
401
+
402
+ def get_enabled_mcp_tools(self) -> Set[str]:
403
+ """Get list of MCP tools to enable based on detection."""
404
+ tools = set()
405
+
406
+ for detection in self.detected_tools:
407
+ tools.update(detection.mcp_tools)
408
+
409
+ # Always include essential tools
410
+ essential = [
411
+ "read", "write", "edit", "multi_edit",
412
+ "directory_tree", "grep", "find", "search",
413
+ "run_command", "bash", "ast", "lsp"
414
+ ]
415
+ tools.update(essential)
416
+
417
+ return tools
418
+
419
+ def generate_configuration(self) -> Dict[str, Any]:
420
+ """Generate MCP configuration based on detection."""
421
+ config = {
422
+ "detected_environment": {
423
+ "project_root": str(self.project_root),
424
+ "tools": [
425
+ {
426
+ "name": d.name,
427
+ "version": d.version,
428
+ "category": d.category,
429
+ "path": d.path,
430
+ }
431
+ for d in self.detected_tools
432
+ ],
433
+ },
434
+ "enabled_mcp_tools": sorted(self.get_enabled_mcp_tools()),
435
+ "environment_variables": {},
436
+ "modes": [],
437
+ }
438
+
439
+ # Add environment variables
440
+ for detection in self.detected_tools:
441
+ config["environment_variables"].update(detection.environment)
442
+
443
+ # Determine modes to enable
444
+ categories = {d.category for d in self.detected_tools}
445
+ languages = {d.name for d in self.detected_tools if d.category == "language"}
446
+
447
+ if "python" in languages:
448
+ config["modes"].append("python")
449
+ if any("django" in t for d in self.detected_tools for t in d.mcp_tools):
450
+ config["modes"].append("django")
451
+ if any("fastapi" in t for d in self.detected_tools for t in d.mcp_tools):
452
+ config["modes"].append("fastapi")
453
+
454
+ if "node" in languages or "typescript" in languages:
455
+ config["modes"].append("javascript")
456
+ if any("next" in t for d in self.detected_tools for t in d.mcp_tools):
457
+ config["modes"].append("nextjs")
458
+ if any("react" in t for d in self.detected_tools for t in d.mcp_tools):
459
+ config["modes"].append("react")
460
+
461
+ if "rust" in languages:
462
+ config["modes"].append("rust")
463
+
464
+ if "go" in languages:
465
+ config["modes"].append("go")
466
+
467
+ return config
468
+
469
+ async def run(
470
+ self,
471
+ action: str = "detect",
472
+ tool: Optional[str] = None,
473
+ export_path: Optional[str] = None,
474
+ **kwargs,
475
+ ) -> MCPResourceDocument:
476
+ """Execute environment detection action."""
477
+
478
+ if action == "detect":
479
+ # Detect all tools
480
+ detections = self.detect_all()
481
+
482
+ return MCPResourceDocument(
483
+ data={
484
+ "detected_tools": [
485
+ {
486
+ "name": d.name,
487
+ "version": d.version,
488
+ "category": d.category,
489
+ "path": d.path,
490
+ "mcp_tools": d.mcp_tools,
491
+ }
492
+ for d in detections
493
+ ],
494
+ "total": len(detections),
495
+ "categories": list(set(d.category for d in detections)),
496
+ }
497
+ )
498
+
499
+ elif action == "check":
500
+ # Check specific tool
501
+ if not tool:
502
+ return MCPResourceDocument(data={"error": "Tool name required for check action"})
503
+
504
+ if tool in TOOL_DETECTIONS:
505
+ detection = self._detect_tool(tool, TOOL_DETECTIONS[tool])
506
+ if detection:
507
+ return MCPResourceDocument(
508
+ data={
509
+ "available": True,
510
+ "name": detection.name,
511
+ "version": detection.version,
512
+ "path": detection.path,
513
+ "mcp_tools": detection.mcp_tools,
514
+ }
515
+ )
516
+ else:
517
+ return MCPResourceDocument(
518
+ data={
519
+ "available": False,
520
+ "name": tool,
521
+ "install_hint": f"Install {tool} to enable related MCP tools",
522
+ }
523
+ )
524
+ else:
525
+ return MCPResourceDocument(data={"error": f"Unknown tool: {tool}"})
526
+
527
+ elif action == "suggest":
528
+ # Suggest tools to install
529
+ if not self.detected_tools:
530
+ self.detect_all()
531
+
532
+ suggestions = self.suggest_tools()
533
+
534
+ return MCPResourceDocument(
535
+ data={
536
+ "suggestions": suggestions,
537
+ "detected": [d.name for d in self.detected_tools],
538
+ }
539
+ )
540
+
541
+ elif action == "configure":
542
+ # Generate and apply configuration
543
+ if not self.detected_tools:
544
+ self.detect_all()
545
+
546
+ config = self.generate_configuration()
547
+
548
+ # Save configuration if export path provided
549
+ if export_path:
550
+ export_file = Path(export_path)
551
+ export_file.write_text(json.dumps(config, indent=2))
552
+ config["exported_to"] = str(export_file)
553
+
554
+ return MCPResourceDocument(data=config)
555
+
556
+ elif action == "export":
557
+ # Export configuration
558
+ if not self.detected_tools:
559
+ self.detect_all()
560
+
561
+ config = self.generate_configuration()
562
+
563
+ if export_path:
564
+ export_file = Path(export_path)
565
+ export_file.write_text(json.dumps(config, indent=2))
566
+
567
+ return MCPResourceDocument(
568
+ data={
569
+ "exported": True,
570
+ "path": str(export_file),
571
+ "configuration": config,
572
+ }
573
+ )
574
+ else:
575
+ return MCPResourceDocument(data=config)
576
+
577
+ else:
578
+ return MCPResourceDocument(
579
+ data={
580
+ "error": f"Unknown action: {action}",
581
+ "valid_actions": ["detect", "check", "suggest", "configure", "export"],
582
+ }
583
+ )
584
+
585
+ async def call(self, **kwargs) -> str:
586
+ """Tool interface for MCP."""
587
+ result = await self.run(**kwargs)
588
+ return result.to_json_string()
589
+
590
+
591
+ # Factory function
592
+ def create_environment_detector():
593
+ """Create environment detection tool."""
594
+ return EnvironmentDetector()