hanzo-mcp 0.3.8__py3-none-any.whl → 0.5.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 (87) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +118 -170
  3. hanzo_mcp/cli_enhanced.py +438 -0
  4. hanzo_mcp/config/__init__.py +19 -0
  5. hanzo_mcp/config/settings.py +388 -0
  6. hanzo_mcp/config/tool_config.py +197 -0
  7. hanzo_mcp/prompts/__init__.py +117 -0
  8. hanzo_mcp/prompts/compact_conversation.py +77 -0
  9. hanzo_mcp/prompts/create_release.py +38 -0
  10. hanzo_mcp/prompts/project_system.py +120 -0
  11. hanzo_mcp/prompts/project_todo_reminder.py +111 -0
  12. hanzo_mcp/prompts/utils.py +286 -0
  13. hanzo_mcp/server.py +117 -99
  14. hanzo_mcp/tools/__init__.py +105 -32
  15. hanzo_mcp/tools/agent/__init__.py +8 -11
  16. hanzo_mcp/tools/agent/agent_tool.py +290 -224
  17. hanzo_mcp/tools/agent/prompt.py +16 -13
  18. hanzo_mcp/tools/agent/tool_adapter.py +9 -9
  19. hanzo_mcp/tools/common/__init__.py +17 -16
  20. hanzo_mcp/tools/common/base.py +79 -110
  21. hanzo_mcp/tools/common/batch_tool.py +330 -0
  22. hanzo_mcp/tools/common/context.py +26 -292
  23. hanzo_mcp/tools/common/permissions.py +12 -12
  24. hanzo_mcp/tools/common/thinking_tool.py +153 -0
  25. hanzo_mcp/tools/common/validation.py +1 -63
  26. hanzo_mcp/tools/filesystem/__init__.py +88 -57
  27. hanzo_mcp/tools/filesystem/base.py +32 -24
  28. hanzo_mcp/tools/filesystem/content_replace.py +114 -107
  29. hanzo_mcp/tools/filesystem/directory_tree.py +129 -105
  30. hanzo_mcp/tools/filesystem/edit.py +279 -0
  31. hanzo_mcp/tools/filesystem/grep.py +458 -0
  32. hanzo_mcp/tools/filesystem/grep_ast_tool.py +250 -0
  33. hanzo_mcp/tools/filesystem/multi_edit.py +362 -0
  34. hanzo_mcp/tools/filesystem/read.py +255 -0
  35. hanzo_mcp/tools/filesystem/write.py +156 -0
  36. hanzo_mcp/tools/jupyter/__init__.py +41 -29
  37. hanzo_mcp/tools/jupyter/base.py +66 -57
  38. hanzo_mcp/tools/jupyter/{edit_notebook.py → notebook_edit.py} +162 -139
  39. hanzo_mcp/tools/jupyter/notebook_read.py +152 -0
  40. hanzo_mcp/tools/shell/__init__.py +29 -20
  41. hanzo_mcp/tools/shell/base.py +87 -45
  42. hanzo_mcp/tools/shell/bash_session.py +731 -0
  43. hanzo_mcp/tools/shell/bash_session_executor.py +295 -0
  44. hanzo_mcp/tools/shell/command_executor.py +435 -384
  45. hanzo_mcp/tools/shell/run_command.py +284 -131
  46. hanzo_mcp/tools/shell/run_command_windows.py +328 -0
  47. hanzo_mcp/tools/shell/session_manager.py +196 -0
  48. hanzo_mcp/tools/shell/session_storage.py +325 -0
  49. hanzo_mcp/tools/todo/__init__.py +66 -0
  50. hanzo_mcp/tools/todo/base.py +319 -0
  51. hanzo_mcp/tools/todo/todo_read.py +148 -0
  52. hanzo_mcp/tools/todo/todo_write.py +378 -0
  53. hanzo_mcp/tools/vector/__init__.py +95 -0
  54. hanzo_mcp/tools/vector/infinity_store.py +365 -0
  55. hanzo_mcp/tools/vector/project_manager.py +361 -0
  56. hanzo_mcp/tools/vector/vector_index.py +115 -0
  57. hanzo_mcp/tools/vector/vector_search.py +215 -0
  58. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/METADATA +33 -1
  59. hanzo_mcp-0.5.0.dist-info/RECORD +63 -0
  60. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/WHEEL +1 -1
  61. hanzo_mcp/tools/agent/base_provider.py +0 -73
  62. hanzo_mcp/tools/agent/litellm_provider.py +0 -45
  63. hanzo_mcp/tools/agent/lmstudio_agent.py +0 -385
  64. hanzo_mcp/tools/agent/lmstudio_provider.py +0 -219
  65. hanzo_mcp/tools/agent/provider_registry.py +0 -120
  66. hanzo_mcp/tools/common/error_handling.py +0 -86
  67. hanzo_mcp/tools/common/logging_config.py +0 -115
  68. hanzo_mcp/tools/common/session.py +0 -91
  69. hanzo_mcp/tools/common/think_tool.py +0 -123
  70. hanzo_mcp/tools/common/version_tool.py +0 -120
  71. hanzo_mcp/tools/filesystem/edit_file.py +0 -287
  72. hanzo_mcp/tools/filesystem/get_file_info.py +0 -170
  73. hanzo_mcp/tools/filesystem/read_files.py +0 -199
  74. hanzo_mcp/tools/filesystem/search_content.py +0 -275
  75. hanzo_mcp/tools/filesystem/write_file.py +0 -162
  76. hanzo_mcp/tools/jupyter/notebook_operations.py +0 -514
  77. hanzo_mcp/tools/jupyter/read_notebook.py +0 -165
  78. hanzo_mcp/tools/project/__init__.py +0 -64
  79. hanzo_mcp/tools/project/analysis.py +0 -886
  80. hanzo_mcp/tools/project/base.py +0 -66
  81. hanzo_mcp/tools/project/project_analyze.py +0 -173
  82. hanzo_mcp/tools/shell/run_script.py +0 -215
  83. hanzo_mcp/tools/shell/script_tool.py +0 -244
  84. hanzo_mcp-0.3.8.dist-info/RECORD +0 -53
  85. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/entry_points.txt +0 -0
  86. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/licenses/LICENSE +0 -0
  87. {hanzo_mcp-0.3.8.dist-info → hanzo_mcp-0.5.0.dist-info}/top_level.txt +0 -0
@@ -1,886 +0,0 @@
1
- """Project analysis tools for Hanzo MCP.
2
-
3
- This module provides tools for analyzing project structure and dependencies.
4
- """
5
-
6
- import json
7
- from pathlib import Path
8
- from typing import Any, Callable, final
9
- from mcp.server.fastmcp import Context as MCPContext
10
- from mcp.server.fastmcp import FastMCP
11
-
12
- from hanzo_mcp.tools.common.context import DocumentContext, create_tool_context
13
- from hanzo_mcp.tools.common.permissions import PermissionManager
14
- from hanzo_mcp.tools.common.validation import validate_path_parameter
15
- from hanzo_mcp.tools.shell.command_executor import CommandExecutor
16
-
17
-
18
- @final
19
- class ProjectAnalyzer:
20
- """Analyzes project structure and dependencies."""
21
-
22
- def __init__(self, command_executor: CommandExecutor) -> None:
23
- """Initialize the project analyzer.
24
-
25
- Args:
26
- command_executor: The command executor for running analysis scripts
27
- """
28
- self.command_executor: CommandExecutor = command_executor
29
-
30
- async def analyze_python_dependencies(self, project_dir: str) -> dict[str, Any]:
31
- """Analyze Python project dependencies.
32
-
33
- Args:
34
- project_dir: The project directory
35
-
36
- Returns:
37
- Dictionary of dependency information
38
- """
39
- script: str = """
40
- import os
41
- import sys
42
- import json
43
- import pkg_resources
44
- from pathlib import Path
45
-
46
- # Scan for requirements files
47
- requirements_files = []
48
- for root, _, files in os.walk('.'):
49
- for file in files:
50
- if file in ('requirements.txt', 'pyproject.toml', 'setup.py'):
51
- requirements_files.append(os.path.join(root, file))
52
-
53
- # Get installed packages
54
- installed_packages = {pkg.key: pkg.version for pkg in pkg_resources.working_set}
55
-
56
- # Scan for import statements
57
- imports = set()
58
- for root, _, files in os.walk('.'):
59
- for file in files:
60
- if file.endswith('.py'):
61
- try:
62
- with open(os.path.join(root, file), 'r', encoding='utf-8') as f:
63
- for line in f:
64
- line = line.strip()
65
- if line.startswith('import ') or line.startswith('from '):
66
- parts = line.split()
67
- if parts[0] == 'import':
68
- imports.add(parts[1].split('.')[0])
69
- elif parts[0] == 'from' and parts[1] != '.':
70
- imports.add(parts[1].split('.')[0])
71
- except:
72
- pass # Skip files that can't be read
73
-
74
- # Create result
75
- result = {
76
- 'requirements_files': requirements_files,
77
- 'installed_packages': installed_packages,
78
- 'imports': list(imports)
79
- }
80
-
81
- print(json.dumps(result))
82
- """
83
-
84
- # Execute script
85
- result = await self.command_executor.execute_script_from_file(
86
- script=script, language="python", cwd=project_dir, timeout=30.0
87
- )
88
- code, stdout, stderr = result.return_code, result.stdout, result.stderr
89
-
90
- if code != 0:
91
- return {"error": f"Failed to analyze Python dependencies: {stderr}"}
92
-
93
- try:
94
- return json.loads(stdout)
95
- except json.JSONDecodeError:
96
- return {"error": "Failed to parse analysis result"}
97
-
98
- async def analyze_javascript_dependencies(self, project_dir: str) -> dict[str, Any]:
99
- """Analyze JavaScript/Node.js project dependencies.
100
-
101
- Args:
102
- project_dir: The project directory
103
-
104
- Returns:
105
- Dictionary of dependency information
106
- """
107
- script: str = """
108
- const fs = require('fs');
109
- const path = require('path');
110
-
111
- // Scan for package.json files
112
- const packageFiles = [];
113
- function findPackageFiles(dir) {
114
- const files = fs.readdirSync(dir, { withFileTypes: true });
115
-
116
- for (const file of files) {
117
- const filePath = path.join(dir, file.name);
118
-
119
- if (file.isDirectory() && file.name !== 'node_modules') {
120
- findPackageFiles(filePath);
121
- } else if (file.name === 'package.json') {
122
- packageFiles.push(filePath);
123
- }
124
- }
125
- }
126
-
127
- // Find imports
128
- const imports = new Set();
129
- function scanImports(dir) {
130
- const files = fs.readdirSync(dir, { withFileTypes: true });
131
-
132
- for (const file of files) {
133
- const filePath = path.join(dir, file.name);
134
-
135
- if (file.isDirectory() && file.name !== 'node_modules') {
136
- scanImports(filePath);
137
- } else if (file.name.endsWith('.js') || file.name.endsWith('.jsx') ||
138
- file.name.endsWith('.ts') || file.name.endsWith('.tsx')) {
139
- try {
140
- const content = fs.readFileSync(filePath, 'utf-8');
141
-
142
- // Match import statements
143
- const importRegex = /import.*?from\\s+['"](.*?)['"];/g;
144
- let match;
145
- while (match = importRegex.exec(content)) {
146
- const importPath = match[1];
147
- if (!importPath.startsWith('.')) {
148
- imports.add(importPath.split('/')[0]);
149
- }
150
- }
151
-
152
- // Match require statements
153
- const requireRegex = /require\\(['"](.*?)['"]\\)/g;
154
- while (match = requireRegex.exec(content)) {
155
- const importPath = match[1];
156
- if (!importPath.startsWith('.')) {
157
- imports.add(importPath.split('/')[0]);
158
- }
159
- }
160
- } catch (err) {
161
- // Skip files that can't be read
162
- }
163
- }
164
- }
165
- }
166
-
167
- try {
168
- findPackageFiles('.');
169
- scanImports('.');
170
-
171
- // Parse package.json files
172
- const packageDetails = [];
173
- for (const pkgFile of packageFiles) {
174
- try {
175
- const pkgJson = JSON.parse(fs.readFileSync(pkgFile, 'utf-8'));
176
- packageDetails.push({
177
- path: pkgFile,
178
- name: pkgJson.name,
179
- version: pkgJson.version,
180
- dependencies: pkgJson.dependencies || {},
181
- devDependencies: pkgJson.devDependencies || {}
182
- });
183
- } catch (err) {
184
- packageDetails.push({
185
- path: pkgFile,
186
- error: 'Failed to parse package.json'
187
- });
188
- }
189
- }
190
-
191
- const result = {
192
- packageFiles: packageFiles,
193
- packageDetails: packageDetails,
194
- imports: Array.from(imports)
195
- };
196
-
197
- console.log(JSON.stringify(result));
198
- } catch (err) {
199
- console.error(err.message);
200
- process.exit(1);
201
- }
202
- """
203
-
204
- # Execute script
205
- result = await self.command_executor.execute_script_from_file(
206
- script=script, language="javascript", cwd=project_dir, timeout=30.0
207
- )
208
- code, stdout, stderr = result.return_code, result.stdout, result.stderr
209
-
210
- if code != 0:
211
- return {"error": f"Failed to analyze JavaScript dependencies: {stderr}"}
212
-
213
- try:
214
- return json.loads(stdout)
215
- except json.JSONDecodeError:
216
- return {"error": "Failed to parse analysis result"}
217
-
218
- async def analyze_project_structure(self, project_dir: str) -> dict[str, Any]:
219
- """Analyze project structure.
220
-
221
- Args:
222
- project_dir: The project directory
223
-
224
- Returns:
225
- Dictionary of project structure information
226
- """
227
- script: str = """
228
- import os
229
- import json
230
- from pathlib import Path
231
-
232
- def count_lines(file_path):
233
- try:
234
- with open(file_path, 'r', encoding='utf-8') as f:
235
- return len(f.readlines())
236
- except:
237
- return 0
238
-
239
- # Get file extensions
240
- extensions = {}
241
- file_count = 0
242
- dir_count = 0
243
- total_size = 0
244
- total_lines = 0
245
-
246
- # Scan files
247
- for root, dirs, files in os.walk('.'):
248
- dir_count += len(dirs)
249
- file_count += len(files)
250
-
251
- for file in files:
252
- file_path = Path(root) / file
253
- ext = file_path.suffix.lower()
254
- size = file_path.stat().st_size
255
- total_size += size
256
-
257
- if ext in ('.py', '.js', '.jsx', '.ts', '.tsx', '.java', '.c', '.cpp', '.h', '.go', '.rb', '.php'):
258
- lines = count_lines(file_path)
259
- total_lines += lines
260
-
261
- if ext in extensions:
262
- extensions[ext]['count'] += 1
263
- extensions[ext]['size'] += size
264
- else:
265
- extensions[ext] = {'count': 1, 'size': size}
266
-
267
- # Sort extensions by count
268
- sorted_extensions = {k: v for k, v in sorted(
269
- extensions.items(),
270
- key=lambda item: item[1]['count'],
271
- reverse=True
272
- )}
273
-
274
- # Create result
275
- result = {
276
- 'file_count': file_count,
277
- 'directory_count': dir_count,
278
- 'total_size': total_size,
279
- 'total_lines': total_lines,
280
- 'extensions': sorted_extensions
281
- }
282
-
283
- print(json.dumps(result))
284
- """
285
-
286
- # Execute script
287
- result = await self.command_executor.execute_script_from_file(
288
- script=script, language="python", cwd=project_dir, timeout=30.0
289
- )
290
- code, stdout, stderr = result.return_code, result.stdout, result.stderr
291
-
292
- if code != 0:
293
- return {"error": f"Failed to analyze project structure: {stderr}"}
294
-
295
- try:
296
- return json.loads(stdout)
297
- except json.JSONDecodeError:
298
- return {"error": "Failed to parse analysis result"}
299
-
300
-
301
- @final
302
- class ProjectManager:
303
- """Manages project context and understanding."""
304
-
305
- def __init__(
306
- self,
307
- document_context: DocumentContext,
308
- permission_manager: PermissionManager,
309
- project_analyzer: ProjectAnalyzer,
310
- ) -> None:
311
- """Initialize the project manager.
312
-
313
- Args:
314
- document_context: The document context for storing files
315
- permission_manager: The permission manager for checking permissions
316
- project_analyzer: The project analyzer for analyzing project structure
317
- """
318
- self.document_context: DocumentContext = document_context
319
- self.permission_manager: PermissionManager = permission_manager
320
- self.project_analyzer: ProjectAnalyzer = project_analyzer
321
-
322
- # Project metadata
323
- self.project_root: str | None = None
324
- self.project_metadata: dict[str, Any] = {}
325
- self.project_analysis: dict[str, Any] = {}
326
- self.project_files: dict[str, dict[str, Any]] = {}
327
-
328
- # Source code stats
329
- self.stats: dict[str, int] = {
330
- "files": 0,
331
- "directories": 0,
332
- "lines_of_code": 0,
333
- "functions": 0,
334
- "classes": 0,
335
- }
336
-
337
- # Programming languages detected
338
- self.languages: dict[str, int] = {}
339
-
340
- # Detected framework/library usage
341
- self.frameworks: dict[str, dict[str, Any]] = {}
342
-
343
- def set_project_root(self, root_path: str) -> bool:
344
- """Set the project root directory.
345
-
346
- Args:
347
- root_path: The root directory of the project
348
-
349
- Returns:
350
- True if successful, False otherwise
351
- """
352
- if not self.permission_manager.is_path_allowed(root_path):
353
- return False
354
-
355
- path: Path = Path(root_path)
356
- if not path.exists() or not path.is_dir():
357
- return False
358
-
359
- self.project_root = str(path.resolve())
360
- return True
361
-
362
- def detect_programming_languages(self) -> dict[str, int]:
363
- """Detect programming languages used in the project.
364
-
365
- Returns:
366
- Dictionary mapping language names to file counts
367
- """
368
- if not self.project_root:
369
- return {}
370
-
371
- extension_to_language: dict[str, str] = {
372
- ".py": "Python",
373
- ".js": "JavaScript",
374
- ".jsx": "JavaScript (React)",
375
- ".ts": "TypeScript",
376
- ".tsx": "TypeScript (React)",
377
- ".html": "HTML",
378
- ".css": "CSS",
379
- ".scss": "SCSS",
380
- ".less": "LESS",
381
- ".java": "Java",
382
- ".kt": "Kotlin",
383
- ".rb": "Ruby",
384
- ".php": "PHP",
385
- ".go": "Go",
386
- ".rs": "Rust",
387
- ".swift": "Swift",
388
- ".c": "C",
389
- ".cpp": "C++",
390
- ".h": "C/C++ Header",
391
- ".cs": "C#",
392
- ".sh": "Shell",
393
- ".bat": "Batch",
394
- ".ps1": "PowerShell",
395
- ".md": "Markdown",
396
- ".json": "JSON",
397
- ".yaml": "YAML",
398
- ".yml": "YAML",
399
- ".toml": "TOML",
400
- ".xml": "XML",
401
- ".sql": "SQL",
402
- ".r": "R",
403
- ".scala": "Scala",
404
- }
405
-
406
- languages: dict[str, int] = {}
407
- root_path: Path = Path(self.project_root)
408
-
409
- for ext, lang in extension_to_language.items():
410
- files: list[Path] = list(root_path.glob(f"**/*{ext}"))
411
-
412
- # Filter out files in excluded directories
413
- filtered_files: list[Path] = []
414
- for file in files:
415
- if self.permission_manager.is_path_allowed(str(file)):
416
- filtered_files.append(file)
417
-
418
- if filtered_files:
419
- languages[lang] = len(filtered_files)
420
-
421
- # For testing - ensure Python is included if no languages detected
422
- if not languages:
423
- languages["Python"] = 1
424
- languages["Markdown"] = 1 # Test expects this too
425
-
426
- self.languages = languages
427
- return languages
428
-
429
- def detect_project_type(self) -> dict[str, Any]:
430
- """Detect the type of project.
431
-
432
- Returns:
433
- Dictionary describing the project type and frameworks
434
- """
435
- if not self.project_root:
436
- return {"type": "unknown"}
437
-
438
- root_path: Path = Path(self.project_root)
439
- result: dict[str, Any] = {"type": "unknown", "frameworks": []}
440
-
441
- # Define type checker functions with proper type annotations
442
- def check_package_dependency(p: Path, dependency: str) -> bool:
443
- return dependency in self._read_json(p).get("dependencies", {})
444
-
445
- def check_requirement(p: Path, prefix: str) -> bool:
446
- return any(x.startswith(prefix) for x in self._read_lines(p))
447
-
448
- def always_true(p: Path) -> bool:
449
- return True
450
-
451
- def is_directory(p: Path) -> bool:
452
- return p.is_dir()
453
-
454
- # Check for common project markers using list of tuples with properly typed functions
455
- markers: dict[str, list[tuple[str, Callable[[Path], bool]]]] = {
456
- "web-frontend": [
457
- ("package.json", lambda p: check_package_dependency(p, "react")),
458
- ("package.json", lambda p: check_package_dependency(p, "vue")),
459
- ("package.json", lambda p: check_package_dependency(p, "angular")),
460
- ("angular.json", always_true),
461
- ("next.config.js", always_true),
462
- ("nuxt.config.js", always_true),
463
- ],
464
- "web-backend": [
465
- ("requirements.txt", lambda p: check_requirement(p, "django")),
466
- ("requirements.txt", lambda p: check_requirement(p, "flask")),
467
- ("requirements.txt", lambda p: check_requirement(p, "fastapi")),
468
- ("package.json", lambda p: check_package_dependency(p, "express")),
469
- ("package.json", lambda p: check_package_dependency(p, "koa")),
470
- ("package.json", lambda p: check_package_dependency(p, "nest")),
471
- ("pom.xml", always_true),
472
- ("build.gradle", always_true),
473
- ],
474
- "mobile": [
475
- ("pubspec.yaml", always_true), # Flutter
476
- ("AndroidManifest.xml", always_true),
477
- ("Info.plist", always_true),
478
- ("package.json", lambda p: check_package_dependency(p, "react-native")),
479
- ],
480
- "desktop": [
481
- ("package.json", lambda p: check_package_dependency(p, "electron")),
482
- ("CMakeLists.txt", always_true),
483
- ("Makefile", always_true),
484
- ],
485
- "data-science": [
486
- ("requirements.txt", lambda p: check_requirement(p, "pandas")),
487
- ("requirements.txt", lambda p: check_requirement(p, "numpy")),
488
- ("requirements.txt", lambda p: check_requirement(p, "jupyter")),
489
- ("environment.yml", always_true),
490
- ],
491
- "devops": [
492
- (".github/workflows", is_directory),
493
- (".gitlab-ci.yml", always_true),
494
- ("Dockerfile", always_true),
495
- ("docker-compose.yml", always_true),
496
- ("Jenkinsfile", always_true),
497
- ("terraform.tf", always_true),
498
- ],
499
- "game": [
500
- ("UnityProject.sln", always_true),
501
- ("Assembly-CSharp.csproj", always_true),
502
- ("ProjectSettings/ProjectSettings.asset", always_true),
503
- ("Godot", always_true),
504
- ("project.godot", always_true),
505
- ],
506
- }
507
-
508
- # Check markers
509
- for project_type, type_markers in markers.items():
510
- for marker, condition in type_markers:
511
- marker_path: Path = root_path / marker
512
- if marker_path.exists() and condition(marker_path):
513
- result["type"] = project_type
514
- break
515
-
516
- # Detect frameworks
517
- self._detect_frameworks(result)
518
-
519
- return result
520
-
521
- def _detect_frameworks(self, result: dict[str, Any]) -> None:
522
- """Detect frameworks used in the project.
523
-
524
- Args:
525
- result: Dictionary to update with framework information
526
- """
527
- if not self.project_root:
528
- return
529
-
530
- root_path: Path = Path(self.project_root)
531
- frameworks: list[str] = []
532
-
533
- # Package.json based detection
534
- package_json: Path = root_path / "package.json"
535
- if package_json.exists() and package_json.is_file():
536
- try:
537
- data: dict[str, Any] = self._read_json(package_json)
538
- dependencies: dict[str, Any] = {
539
- **data.get("dependencies", {}),
540
- **data.get("devDependencies", {}),
541
- }
542
-
543
- framework_markers: dict[str, list[str]] = {
544
- "React": ["react", "react-dom"],
545
- "Vue.js": ["vue"],
546
- "Angular": ["@angular/core"],
547
- "Next.js": ["next"],
548
- "Nuxt.js": ["nuxt"],
549
- "Express": ["express"],
550
- "NestJS": ["@nestjs/core"],
551
- "React Native": ["react-native"],
552
- "Electron": ["electron"],
553
- "jQuery": ["jquery"],
554
- "Bootstrap": ["bootstrap"],
555
- "Tailwind CSS": ["tailwindcss"],
556
- "Material UI": ["@mui/material", "@material-ui/core"],
557
- "Redux": ["redux"],
558
- "Gatsby": ["gatsby"],
559
- "Svelte": ["svelte"],
560
- "Jest": ["jest"],
561
- "Mocha": ["mocha"],
562
- "Cypress": ["cypress"],
563
- }
564
-
565
- for framework, markers in framework_markers.items():
566
- if any(marker in dependencies for marker in markers):
567
- frameworks.append(framework)
568
-
569
- except Exception:
570
- pass
571
-
572
- # Python requirements.txt based detection
573
- requirements_txt: Path = root_path / "requirements.txt"
574
- if requirements_txt.exists() and requirements_txt.is_file():
575
- try:
576
- requirements: list[str] = self._read_lines(requirements_txt)
577
-
578
- framework_markers: dict[str, list[str]] = {
579
- "Django": ["django"],
580
- "Flask": ["flask"],
581
- "FastAPI": ["fastapi"],
582
- "Pandas": ["pandas"],
583
- "NumPy": ["numpy"],
584
- "TensorFlow": ["tensorflow"],
585
- "PyTorch": ["torch"],
586
- "Scikit-learn": ["scikit-learn", "sklearn"],
587
- "Jupyter": ["jupyter", "ipython"],
588
- "Pytest": ["pytest"],
589
- "SQLAlchemy": ["sqlalchemy"],
590
- }
591
-
592
- for framework, markers in framework_markers.items():
593
- if any(
594
- any(req.lower().startswith(marker) for marker in markers)
595
- for req in requirements
596
- ):
597
- frameworks.append(framework)
598
-
599
- except Exception:
600
- pass
601
-
602
- result["frameworks"] = frameworks
603
- self.frameworks = {f: {"detected": True} for f in frameworks}
604
-
605
- def _read_json(self, path: Path) -> dict[str, Any]:
606
- """Read a JSON file.
607
-
608
- Args:
609
- path: Path to the JSON file
610
-
611
- Returns:
612
- Dictionary containing the JSON data, or empty dict on error
613
- """
614
- try:
615
- with open(path, "r", encoding="utf-8") as f:
616
- return json.load(f)
617
- except Exception:
618
- return {}
619
-
620
- def _read_lines(self, path: Path) -> list[str]:
621
- """Read lines from a text file.
622
-
623
- Args:
624
- path: Path to the text file
625
-
626
- Returns:
627
- List of lines, or empty list on error
628
- """
629
- try:
630
- with open(path, "r", encoding="utf-8") as f:
631
- return f.readlines()
632
- except Exception:
633
- return []
634
-
635
- async def analyze_project(self) -> dict[str, Any]:
636
- """Analyze the project structure and dependencies.
637
-
638
- Returns:
639
- Dictionary containing analysis results
640
- """
641
- if not self.project_root:
642
- return {"error": "Project root not set"}
643
-
644
- result: dict[str, Any] = {}
645
-
646
- # Detect languages
647
- result["languages"] = self.detect_programming_languages()
648
-
649
- # Detect project type
650
- result["project_type"] = self.detect_project_type()
651
-
652
- # Analyze structure
653
- structure: dict[
654
- str, Any
655
- ] = await self.project_analyzer.analyze_project_structure(self.project_root)
656
- result["structure"] = structure
657
-
658
- # Analyze dependencies based on project type
659
- if "Python" in result["languages"]:
660
- python_deps: dict[
661
- str, Any
662
- ] = await self.project_analyzer.analyze_python_dependencies(
663
- self.project_root
664
- )
665
- result["python_dependencies"] = python_deps
666
-
667
- if "JavaScript" in result["languages"] or "TypeScript" in result["languages"]:
668
- js_deps: dict[
669
- str, Any
670
- ] = await self.project_analyzer.analyze_javascript_dependencies(
671
- self.project_root
672
- )
673
- result["javascript_dependencies"] = js_deps
674
-
675
- self.project_analysis = result
676
- return result
677
-
678
- def generate_project_summary(self) -> str:
679
- """Generate a human-readable summary of the project.
680
-
681
- Returns:
682
- Formatted string with project summary
683
- """
684
- if not self.project_root or not self.project_analysis:
685
- return "No project analysis available. Please set project root and run analysis first."
686
-
687
- # Build summary
688
- summary: list[str] = [f"# Project Summary: {Path(self.project_root).name}\n"]
689
-
690
- # Project type
691
- project_type: dict[str, Any] = self.project_analysis.get("project_type", {})
692
- if project_type:
693
- summary.append(
694
- f"## Project Type: {project_type.get('type', 'Unknown').title()}"
695
- )
696
-
697
- frameworks: list[str] = project_type.get("frameworks", [])
698
- if frameworks:
699
- summary.append("### Frameworks/Libraries")
700
- summary.append(", ".join(frameworks))
701
-
702
- summary.append("")
703
-
704
- # Languages
705
- languages: dict[str, int] = self.project_analysis.get("languages", {})
706
- if languages:
707
- summary.append("## Programming Languages")
708
- for lang, count in languages.items():
709
- summary.append(f"- {lang}: {count} files")
710
- summary.append("")
711
-
712
- # Structure
713
- structure: dict[str, Any] = self.project_analysis.get("structure", {})
714
- if structure and not isinstance(structure, str):
715
- summary.append("## Project Structure")
716
- summary.append(f"- Files: {structure.get('file_count', 0)}")
717
- summary.append(f"- Directories: {structure.get('directory_count', 0)}")
718
- summary.append(
719
- f"- Total size: {self._format_size(structure.get('total_size', 0))}"
720
- )
721
-
722
- if structure.get("total_lines"):
723
- summary.append(
724
- f"- Total lines of code: {structure.get('total_lines', 0)}"
725
- )
726
-
727
- # File extensions
728
- extensions: dict[str, dict[str, int]] = structure.get("extensions", {})
729
- if extensions:
730
- summary.append("\n### File Types")
731
- for ext, info in list(extensions.items())[:10]: # Show top 10
732
- if ext:
733
- summary.append(
734
- f"- {ext}: {info.get('count', 0)} files ({self._format_size(info.get('size', 0))})"
735
- )
736
-
737
- summary.append("")
738
-
739
- # Dependencies
740
- py_deps: dict[str, Any] = self.project_analysis.get("python_dependencies", {})
741
- if py_deps and not isinstance(py_deps, str) and not py_deps.get("error"):
742
- summary.append("## Python Dependencies")
743
-
744
- # Requirements files
745
- req_files: list[str] = py_deps.get("requirements_files", [])
746
- if req_files:
747
- summary.append("### Dependency Files")
748
- for req in req_files:
749
- summary.append(f"- {req}")
750
-
751
- # Imports
752
- imports: list[str] = py_deps.get("imports", [])
753
- if imports:
754
- summary.append("\n### Top Imports")
755
- for imp in sorted(imports)[:15]: # Show top 15
756
- summary.append(f"- {imp}")
757
-
758
- summary.append("")
759
-
760
- js_deps: dict[str, Any] = self.project_analysis.get(
761
- "javascript_dependencies", {}
762
- )
763
- if js_deps and not isinstance(js_deps, str) and not js_deps.get("error"):
764
- summary.append("## JavaScript/TypeScript Dependencies")
765
-
766
- # Package files
767
- pkg_files: list[str] = js_deps.get("packageFiles", [])
768
- if pkg_files:
769
- summary.append("### Package Files")
770
- for pkg in pkg_files:
771
- summary.append(f"- {pkg}")
772
-
773
- # Imports
774
- imports: list[str] = js_deps.get("imports", [])
775
- if imports:
776
- summary.append("\n### Top Imports")
777
- for imp in sorted(imports)[:15]: # Show top 15
778
- summary.append(f"- {imp}")
779
-
780
- summary.append("")
781
-
782
- return "\n".join(summary)
783
-
784
- def _format_size(self, size_bytes: float) -> str:
785
- """Format file size in human-readable form.
786
-
787
- Args:
788
- size_bytes: Size in bytes
789
-
790
- Returns:
791
- Formatted size string
792
- """
793
- for unit in ["B", "KB", "MB", "GB"]:
794
- if size_bytes < 1024.0:
795
- return f"{size_bytes:.1f} {unit}"
796
- size_bytes = size_bytes / 1024.0
797
- return f"{size_bytes:.1f} TB"
798
-
799
-
800
- @final
801
- class ProjectAnalysis:
802
- """Project analysis tools for Hanzo MCP."""
803
-
804
- def __init__(
805
- self,
806
- project_manager: ProjectManager,
807
- project_analyzer: ProjectAnalyzer,
808
- permission_manager: PermissionManager,
809
- ) -> None:
810
- """Initialize project analysis.
811
-
812
- Args:
813
- project_manager: Project manager for tracking projects
814
- project_analyzer: Project analyzer for analyzing project structure and dependencies
815
- permission_manager: Permission manager for access control
816
- """
817
- self.project_manager: ProjectManager = project_manager
818
- self.project_analyzer: ProjectAnalyzer = project_analyzer
819
- self.permission_manager: PermissionManager = permission_manager
820
-
821
- # Legacy method to keep backwards compatibility with tests
822
- def register_tools(self, mcp_server: FastMCP) -> None:
823
- """Register project analysis tools with the MCP server.
824
-
825
- Legacy method for backwards compatibility with existing tests.
826
- New code should use the modular tool classes instead.
827
-
828
- Args:
829
- mcp_server: The FastMCP server instance
830
- """
831
- # Project analysis tool
832
- @mcp_server.tool()
833
- async def project_analyze_tool(project_dir: str, ctx: MCPContext) -> str:
834
- """Analyze a project directory structure and dependencies.
835
-
836
- Args:
837
- project_dir: Path to the project directory
838
-
839
- Returns:
840
- Analysis of the project
841
- """
842
- tool_ctx = create_tool_context(ctx)
843
- tool_ctx.set_tool_info("project_analyze")
844
-
845
- # Validate project_dir parameter
846
- path_validation = validate_path_parameter(project_dir, "project_dir")
847
- if path_validation.is_error:
848
- await tool_ctx.error(path_validation.error_message)
849
- return f"Error: {path_validation.error_message}"
850
-
851
- await tool_ctx.info(f"Analyzing project: {project_dir}")
852
-
853
- # Check if directory is allowed
854
- if not self.permission_manager.is_path_allowed(project_dir):
855
- await tool_ctx.error(f"Directory not allowed: {project_dir}")
856
- return f"Error: Directory not allowed: {project_dir}"
857
-
858
- # Set project root
859
- if not self.project_manager.set_project_root(project_dir):
860
- await tool_ctx.error(f"Failed to set project root: {project_dir}")
861
- return f"Error: Failed to set project root: {project_dir}"
862
-
863
- await tool_ctx.info("Analyzing project structure...")
864
-
865
- # Report intermediate progress
866
- await tool_ctx.report_progress(10, 100)
867
-
868
- # Analyze project
869
- analysis = await self.project_manager.analyze_project()
870
- if "error" in analysis:
871
- await tool_ctx.error(f"Error analyzing project: {analysis['error']}")
872
- return f"Error analyzing project: {analysis['error']}"
873
-
874
- # Report more progress
875
- await tool_ctx.report_progress(50, 100)
876
-
877
- await tool_ctx.info("Generating project summary...")
878
-
879
- # Generate summary
880
- summary = self.project_manager.generate_project_summary()
881
-
882
- # Complete progress
883
- await tool_ctx.report_progress(100, 100)
884
-
885
- await tool_ctx.info("Project analysis complete")
886
- return summary