hanzo-mcp 0.7.6__py3-none-any.whl → 0.8.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 (178) hide show
  1. hanzo_mcp/__init__.py +7 -1
  2. hanzo_mcp/__main__.py +1 -1
  3. hanzo_mcp/analytics/__init__.py +2 -2
  4. hanzo_mcp/analytics/posthog_analytics.py +76 -82
  5. hanzo_mcp/cli.py +31 -36
  6. hanzo_mcp/cli_enhanced.py +94 -72
  7. hanzo_mcp/cli_plugin.py +27 -17
  8. hanzo_mcp/config/__init__.py +2 -2
  9. hanzo_mcp/config/settings.py +112 -88
  10. hanzo_mcp/config/tool_config.py +32 -34
  11. hanzo_mcp/dev_server.py +66 -67
  12. hanzo_mcp/prompts/__init__.py +94 -12
  13. hanzo_mcp/prompts/enhanced_prompts.py +809 -0
  14. hanzo_mcp/prompts/example_custom_prompt.py +6 -5
  15. hanzo_mcp/prompts/project_todo_reminder.py +0 -1
  16. hanzo_mcp/prompts/tool_explorer.py +10 -7
  17. hanzo_mcp/server.py +17 -21
  18. hanzo_mcp/server_enhanced.py +15 -22
  19. hanzo_mcp/tools/__init__.py +56 -28
  20. hanzo_mcp/tools/agent/__init__.py +16 -19
  21. hanzo_mcp/tools/agent/agent.py +82 -65
  22. hanzo_mcp/tools/agent/agent_tool.py +152 -122
  23. hanzo_mcp/tools/agent/agent_tool_v1_deprecated.py +66 -62
  24. hanzo_mcp/tools/agent/clarification_protocol.py +55 -50
  25. hanzo_mcp/tools/agent/clarification_tool.py +11 -10
  26. hanzo_mcp/tools/agent/claude_cli_tool.py +21 -20
  27. hanzo_mcp/tools/agent/claude_desktop_auth.py +130 -144
  28. hanzo_mcp/tools/agent/cli_agent_base.py +59 -53
  29. hanzo_mcp/tools/agent/code_auth.py +102 -107
  30. hanzo_mcp/tools/agent/code_auth_tool.py +28 -27
  31. hanzo_mcp/tools/agent/codex_cli_tool.py +20 -19
  32. hanzo_mcp/tools/agent/critic_tool.py +86 -73
  33. hanzo_mcp/tools/agent/gemini_cli_tool.py +21 -20
  34. hanzo_mcp/tools/agent/grok_cli_tool.py +21 -20
  35. hanzo_mcp/tools/agent/iching_tool.py +404 -139
  36. hanzo_mcp/tools/agent/network_tool.py +89 -73
  37. hanzo_mcp/tools/agent/prompt.py +2 -1
  38. hanzo_mcp/tools/agent/review_tool.py +101 -98
  39. hanzo_mcp/tools/agent/swarm_alias.py +87 -0
  40. hanzo_mcp/tools/agent/swarm_tool.py +246 -161
  41. hanzo_mcp/tools/agent/swarm_tool_v1_deprecated.py +134 -92
  42. hanzo_mcp/tools/agent/tool_adapter.py +21 -11
  43. hanzo_mcp/tools/common/__init__.py +1 -1
  44. hanzo_mcp/tools/common/base.py +3 -5
  45. hanzo_mcp/tools/common/batch_tool.py +46 -39
  46. hanzo_mcp/tools/common/config_tool.py +120 -84
  47. hanzo_mcp/tools/common/context.py +1 -5
  48. hanzo_mcp/tools/common/context_fix.py +5 -3
  49. hanzo_mcp/tools/common/critic_tool.py +4 -8
  50. hanzo_mcp/tools/common/decorators.py +58 -56
  51. hanzo_mcp/tools/common/enhanced_base.py +29 -32
  52. hanzo_mcp/tools/common/fastmcp_pagination.py +91 -94
  53. hanzo_mcp/tools/common/forgiving_edit.py +91 -87
  54. hanzo_mcp/tools/common/mode.py +15 -17
  55. hanzo_mcp/tools/common/mode_loader.py +27 -24
  56. hanzo_mcp/tools/common/paginated_base.py +61 -53
  57. hanzo_mcp/tools/common/paginated_response.py +72 -79
  58. hanzo_mcp/tools/common/pagination.py +50 -53
  59. hanzo_mcp/tools/common/permissions.py +4 -4
  60. hanzo_mcp/tools/common/personality.py +186 -138
  61. hanzo_mcp/tools/common/plugin_loader.py +54 -54
  62. hanzo_mcp/tools/common/stats.py +65 -47
  63. hanzo_mcp/tools/common/test_helpers.py +31 -0
  64. hanzo_mcp/tools/common/thinking_tool.py +4 -8
  65. hanzo_mcp/tools/common/tool_disable.py +17 -12
  66. hanzo_mcp/tools/common/tool_enable.py +13 -14
  67. hanzo_mcp/tools/common/tool_list.py +36 -28
  68. hanzo_mcp/tools/common/truncate.py +23 -23
  69. hanzo_mcp/tools/config/__init__.py +4 -4
  70. hanzo_mcp/tools/config/config_tool.py +42 -29
  71. hanzo_mcp/tools/config/index_config.py +37 -34
  72. hanzo_mcp/tools/config/mode_tool.py +175 -55
  73. hanzo_mcp/tools/database/__init__.py +15 -12
  74. hanzo_mcp/tools/database/database_manager.py +77 -75
  75. hanzo_mcp/tools/database/graph.py +137 -91
  76. hanzo_mcp/tools/database/graph_add.py +30 -18
  77. hanzo_mcp/tools/database/graph_query.py +178 -102
  78. hanzo_mcp/tools/database/graph_remove.py +33 -28
  79. hanzo_mcp/tools/database/graph_search.py +97 -75
  80. hanzo_mcp/tools/database/graph_stats.py +91 -59
  81. hanzo_mcp/tools/database/sql.py +107 -79
  82. hanzo_mcp/tools/database/sql_query.py +30 -24
  83. hanzo_mcp/tools/database/sql_search.py +29 -25
  84. hanzo_mcp/tools/database/sql_stats.py +47 -35
  85. hanzo_mcp/tools/editor/neovim_command.py +25 -28
  86. hanzo_mcp/tools/editor/neovim_edit.py +21 -23
  87. hanzo_mcp/tools/editor/neovim_session.py +60 -54
  88. hanzo_mcp/tools/filesystem/__init__.py +31 -30
  89. hanzo_mcp/tools/filesystem/ast_multi_edit.py +329 -249
  90. hanzo_mcp/tools/filesystem/ast_tool.py +4 -4
  91. hanzo_mcp/tools/filesystem/base.py +1 -1
  92. hanzo_mcp/tools/filesystem/batch_search.py +316 -224
  93. hanzo_mcp/tools/filesystem/content_replace.py +4 -4
  94. hanzo_mcp/tools/filesystem/diff.py +71 -59
  95. hanzo_mcp/tools/filesystem/directory_tree.py +7 -7
  96. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +49 -37
  97. hanzo_mcp/tools/filesystem/edit.py +4 -4
  98. hanzo_mcp/tools/filesystem/find.py +173 -80
  99. hanzo_mcp/tools/filesystem/find_files.py +73 -52
  100. hanzo_mcp/tools/filesystem/git_search.py +157 -104
  101. hanzo_mcp/tools/filesystem/grep.py +8 -8
  102. hanzo_mcp/tools/filesystem/multi_edit.py +4 -8
  103. hanzo_mcp/tools/filesystem/read.py +12 -10
  104. hanzo_mcp/tools/filesystem/rules_tool.py +59 -43
  105. hanzo_mcp/tools/filesystem/search_tool.py +263 -207
  106. hanzo_mcp/tools/filesystem/symbols_tool.py +94 -54
  107. hanzo_mcp/tools/filesystem/tree.py +35 -33
  108. hanzo_mcp/tools/filesystem/unix_aliases.py +13 -18
  109. hanzo_mcp/tools/filesystem/watch.py +37 -36
  110. hanzo_mcp/tools/filesystem/write.py +4 -8
  111. hanzo_mcp/tools/jupyter/__init__.py +4 -4
  112. hanzo_mcp/tools/jupyter/base.py +4 -5
  113. hanzo_mcp/tools/jupyter/jupyter.py +67 -47
  114. hanzo_mcp/tools/jupyter/notebook_edit.py +4 -4
  115. hanzo_mcp/tools/jupyter/notebook_read.py +4 -7
  116. hanzo_mcp/tools/llm/__init__.py +5 -7
  117. hanzo_mcp/tools/llm/consensus_tool.py +72 -52
  118. hanzo_mcp/tools/llm/llm_manage.py +101 -60
  119. hanzo_mcp/tools/llm/llm_tool.py +226 -166
  120. hanzo_mcp/tools/llm/provider_tools.py +25 -26
  121. hanzo_mcp/tools/lsp/__init__.py +1 -1
  122. hanzo_mcp/tools/lsp/lsp_tool.py +228 -143
  123. hanzo_mcp/tools/mcp/__init__.py +2 -3
  124. hanzo_mcp/tools/mcp/mcp_add.py +27 -25
  125. hanzo_mcp/tools/mcp/mcp_remove.py +7 -8
  126. hanzo_mcp/tools/mcp/mcp_stats.py +23 -22
  127. hanzo_mcp/tools/mcp/mcp_tool.py +129 -98
  128. hanzo_mcp/tools/memory/__init__.py +39 -21
  129. hanzo_mcp/tools/memory/knowledge_tools.py +124 -99
  130. hanzo_mcp/tools/memory/memory_tools.py +90 -108
  131. hanzo_mcp/tools/search/__init__.py +7 -2
  132. hanzo_mcp/tools/search/find_tool.py +297 -212
  133. hanzo_mcp/tools/search/unified_search.py +366 -314
  134. hanzo_mcp/tools/shell/__init__.py +8 -7
  135. hanzo_mcp/tools/shell/auto_background.py +56 -49
  136. hanzo_mcp/tools/shell/base.py +1 -1
  137. hanzo_mcp/tools/shell/base_process.py +75 -75
  138. hanzo_mcp/tools/shell/bash_session.py +2 -2
  139. hanzo_mcp/tools/shell/bash_session_executor.py +4 -4
  140. hanzo_mcp/tools/shell/bash_tool.py +24 -31
  141. hanzo_mcp/tools/shell/command_executor.py +12 -12
  142. hanzo_mcp/tools/shell/logs.py +43 -33
  143. hanzo_mcp/tools/shell/npx.py +13 -13
  144. hanzo_mcp/tools/shell/npx_background.py +24 -21
  145. hanzo_mcp/tools/shell/npx_tool.py +18 -22
  146. hanzo_mcp/tools/shell/open.py +19 -21
  147. hanzo_mcp/tools/shell/pkill.py +31 -26
  148. hanzo_mcp/tools/shell/process_tool.py +32 -32
  149. hanzo_mcp/tools/shell/processes.py +57 -58
  150. hanzo_mcp/tools/shell/run_background.py +24 -25
  151. hanzo_mcp/tools/shell/run_command.py +5 -5
  152. hanzo_mcp/tools/shell/run_command_windows.py +5 -5
  153. hanzo_mcp/tools/shell/session_storage.py +3 -3
  154. hanzo_mcp/tools/shell/streaming_command.py +141 -126
  155. hanzo_mcp/tools/shell/uvx.py +24 -25
  156. hanzo_mcp/tools/shell/uvx_background.py +35 -33
  157. hanzo_mcp/tools/shell/uvx_tool.py +18 -22
  158. hanzo_mcp/tools/todo/__init__.py +6 -2
  159. hanzo_mcp/tools/todo/todo.py +50 -37
  160. hanzo_mcp/tools/todo/todo_read.py +5 -8
  161. hanzo_mcp/tools/todo/todo_write.py +5 -7
  162. hanzo_mcp/tools/vector/__init__.py +40 -28
  163. hanzo_mcp/tools/vector/ast_analyzer.py +176 -143
  164. hanzo_mcp/tools/vector/git_ingester.py +170 -179
  165. hanzo_mcp/tools/vector/index_tool.py +96 -44
  166. hanzo_mcp/tools/vector/infinity_store.py +283 -228
  167. hanzo_mcp/tools/vector/mock_infinity.py +39 -40
  168. hanzo_mcp/tools/vector/project_manager.py +88 -78
  169. hanzo_mcp/tools/vector/vector.py +59 -42
  170. hanzo_mcp/tools/vector/vector_index.py +30 -27
  171. hanzo_mcp/tools/vector/vector_search.py +64 -45
  172. hanzo_mcp/types.py +6 -4
  173. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/METADATA +1 -1
  174. hanzo_mcp-0.8.0.dist-info/RECORD +185 -0
  175. hanzo_mcp-0.7.6.dist-info/RECORD +0 -182
  176. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/WHEEL +0 -0
  177. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/entry_points.txt +0 -0
  178. {hanzo_mcp-0.7.6.dist-info → hanzo_mcp-0.8.0.dist-info}/top_level.txt +0 -0
@@ -5,19 +5,18 @@ parallel or serial depending on their characteristics.
5
5
  """
6
6
 
7
7
  import asyncio
8
- from typing import Annotated, Any, TypedDict, Unpack, final, override
8
+ from typing import Any, Unpack, Annotated, TypedDict, final, override
9
9
 
10
- from mcp.server.fastmcp import Context as MCPContext
11
- from mcp.server import FastMCP
12
10
  from pydantic import Field
11
+ from mcp.server import FastMCP
12
+ from mcp.server.fastmcp import Context as MCPContext
13
13
 
14
14
  from hanzo_mcp.tools.common.base import BaseTool
15
15
  from hanzo_mcp.tools.common.context import create_tool_context
16
- from hanzo_mcp.tools.common.truncate import truncate_response, estimate_tokens
16
+ from hanzo_mcp.tools.common.truncate import truncate_response
17
17
  from hanzo_mcp.tools.common.fastmcp_pagination import (
18
- create_paginated_response,
19
18
  CursorData,
20
- TokenAwarePaginator
19
+ create_paginated_response,
21
20
  )
22
21
 
23
22
 
@@ -197,9 +196,9 @@ Not available: think,write,edit,multi_edit,notebook_edit
197
196
 
198
197
  # Execute all tool invocations in parallel
199
198
  tasks: list[asyncio.Future[dict[str, Any]]] = []
200
- invocation_map: dict[
201
- asyncio.Future[dict[str, Any]], dict[str, Any]
202
- ] = {} # Map task Future to invocation
199
+ invocation_map: dict[asyncio.Future[dict[str, Any]], dict[str, Any]] = (
200
+ {}
201
+ ) # Map task Future to invocation
203
202
 
204
203
  for i, invocation in enumerate(invocations):
205
204
  # Extract tool name and input from invocation
@@ -290,7 +289,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
290
289
  # Extract cursor if provided
291
290
  cursor = params.get("cursor")
292
291
  cursor_offset = 0
293
-
292
+
294
293
  # If cursor provided, we need to resume from where we left off
295
294
  if cursor:
296
295
  cursor_data = CursorData.from_cursor(cursor)
@@ -298,63 +297,71 @@ Not available: think,write,edit,multi_edit,notebook_edit
298
297
  # Skip already returned results
299
298
  cursor_offset = cursor_data.offset
300
299
  results = results[cursor_offset:]
301
-
300
+
302
301
  # Format results
303
302
  formatted_results = []
304
303
  for i, result in enumerate(results):
305
304
  invocation = result["invocation"]
306
305
  tool_name = invocation.get("tool_name", "unknown")
307
- formatted_results.append({
308
- "tool": tool_name,
309
- "result": result["result"],
310
- "index": i + cursor_offset
311
- })
312
-
306
+ formatted_results.append(
307
+ {
308
+ "tool": tool_name,
309
+ "result": result["result"],
310
+ "index": i + cursor_offset,
311
+ }
312
+ )
313
+
313
314
  # Create paginated response with token awareness
314
315
  paginated_response = create_paginated_response(
315
- formatted_results,
316
- cursor=cursor,
317
- use_token_limit=True
316
+ formatted_results, cursor=cursor, use_token_limit=True
318
317
  )
319
-
318
+
320
319
  # Convert paginated response to string format for MCP
321
320
  if isinstance(paginated_response, dict) and "items" in paginated_response:
322
321
  # Format the items as a readable string
323
322
  result_parts = []
324
-
323
+
325
324
  # Add header
326
325
  result_parts.append(f"=== Batch operation: {description} ===")
327
326
  result_parts.append(f"Total invocations: {len(invocations)}")
328
- result_parts.append(f"Showing results: {len(paginated_response['items'])} of {len(results)}")
329
- if paginated_response.get('hasMore'):
330
- result_parts.append(f"More results available - use cursor: {paginated_response.get('nextCursor')}")
327
+ result_parts.append(
328
+ f"Showing results: {len(paginated_response['items'])} of {len(results)}"
329
+ )
330
+ if paginated_response.get("hasMore"):
331
+ result_parts.append(
332
+ f"More results available - use cursor: {paginated_response.get('nextCursor')}"
333
+ )
331
334
  result_parts.append("")
332
-
335
+
333
336
  # Format each result
334
- for item in paginated_response['items']:
337
+ for item in paginated_response["items"]:
335
338
  result_parts.append(f"### Result {item['index'] + 1}: {item['tool']}")
336
- result_content = item['result']
337
-
339
+ result_content = item["result"]
340
+
338
341
  # Add the result content - use multi-line code blocks for code outputs
339
342
  if isinstance(result_content, str) and "\n" in result_content:
340
343
  result_parts.append(f"```\n{result_content}\n```")
341
344
  else:
342
345
  result_parts.append(str(result_content))
343
346
  result_parts.append("")
344
-
347
+
345
348
  # Join all parts
346
349
  formatted_output = "\n".join(result_parts)
347
-
350
+
348
351
  # If there's a next cursor, we need to preserve it in the response
349
352
  # For now, append it as a note at the end
350
- if paginated_response.get('hasMore') and paginated_response.get('nextCursor'):
351
- formatted_output += f"\n\n[To continue, use cursor: {paginated_response['nextCursor']}]"
352
-
353
+ if paginated_response.get("hasMore") and paginated_response.get(
354
+ "nextCursor"
355
+ ):
356
+ formatted_output += (
357
+ f"\n\n[To continue, use cursor: {paginated_response['nextCursor']}]"
358
+ )
359
+
353
360
  await tool_ctx.info(
354
361
  f"Batch operation '{description}' completed with {len(paginated_response['items'])} results"
355
362
  f"{' (more available)' if paginated_response.get('hasMore') else ''}"
356
363
  )
357
-
364
+
358
365
  return formatted_output
359
366
  else:
360
367
  # Fallback if pagination didn't work as expected
@@ -376,16 +383,16 @@ Not available: think,write,edit,multi_edit,notebook_edit
376
383
 
377
384
  # Add the result header
378
385
  formatted_parts.append(f"### Result {i + 1}: {tool_name}")
379
-
386
+
380
387
  # Truncate individual results if they're too large
381
388
  result_content = result["result"]
382
389
  if len(result_content) > 50000: # If individual result > 50k chars
383
390
  result_content = truncate_response(
384
391
  result_content,
385
392
  max_tokens=5000, # Limit individual results to ~5k tokens
386
- truncation_message=f"\n\n[Result from {tool_name} truncated. Use the tool directly with pagination/filtering for full output.]"
393
+ truncation_message=f"\n\n[Result from {tool_name} truncated. Use the tool directly with pagination/filtering for full output.]",
387
394
  )
388
-
395
+
389
396
  # Add the result content - use multi-line code blocks for code outputs
390
397
  if "\n" in result_content:
391
398
  formatted_parts.append(f"```\n{result_content}\n```")
@@ -413,7 +420,7 @@ Not available: think,write,edit,multi_edit,notebook_edit
413
420
  description: Description,
414
421
  invocations: Invocations,
415
422
  cursor: Cursor,
416
- ctx: MCPContext
423
+ ctx: MCPContext,
417
424
  ) -> str:
418
425
  return await tool_self.call(
419
426
  ctx, description=description, invocations=invocations, cursor=cursor
@@ -1,30 +1,27 @@
1
1
  """Configuration management tool for dynamic settings updates."""
2
2
 
3
- from typing import Dict, List, Optional, TypedDict, Unpack, Any, final
4
3
  import json
5
- from pathlib import Path
4
+ from typing import Any, Dict, Unpack, Optional, TypedDict, final
6
5
 
7
6
  from mcp.server.fastmcp import Context as MCPContext
8
7
 
9
- from hanzo_mcp.tools.common.base import BaseTool
10
- from hanzo_mcp.tools.common.permissions import PermissionManager
11
8
  from hanzo_mcp.config.settings import (
12
- HanzoMCPSettings,
13
- MCPServerConfig,
14
- ProjectConfig,
15
- load_settings,
9
+ ProjectConfig,
10
+ MCPServerConfig,
11
+ load_settings,
16
12
  save_settings,
17
13
  detect_project_from_path,
18
- ensure_project_hanzo_dir
19
14
  )
15
+ from hanzo_mcp.tools.common.base import BaseTool
16
+ from hanzo_mcp.tools.common.permissions import PermissionManager
20
17
 
21
18
 
22
19
  class ConfigToolParams(TypedDict, total=False):
23
20
  """Parameters for configuration management operations."""
24
-
21
+
25
22
  action: str # get, set, add_server, remove_server, add_project, etc.
26
23
  scope: Optional[str] # global, project, current
27
- setting_path: Optional[str] # dot-notation path like "agent.enabled"
24
+ setting_path: Optional[str] # dot-notation path like "agent.enabled"
28
25
  value: Optional[Any] # new value to set
29
26
  server_name: Optional[str] # for MCP server operations
30
27
  server_config: Optional[Dict[str, Any]] # for adding servers
@@ -35,20 +32,20 @@ class ConfigToolParams(TypedDict, total=False):
35
32
  @final
36
33
  class ConfigTool(BaseTool):
37
34
  """Tool for managing Hanzo AI configuration dynamically."""
38
-
35
+
39
36
  def __init__(self, permission_manager: PermissionManager):
40
37
  """Initialize the configuration tool.
41
-
38
+
42
39
  Args:
43
40
  permission_manager: Permission manager for access control
44
41
  """
45
42
  self.permission_manager = permission_manager
46
-
43
+
47
44
  @property
48
45
  def name(self) -> str:
49
46
  """Get the tool name."""
50
47
  return "config"
51
-
48
+
52
49
  @property
53
50
  def description(self) -> str:
54
51
  """Get the tool description."""
@@ -64,18 +61,18 @@ Perfect for AI-driven configuration where users can say things like:
64
61
  - "Show me the current project configuration"
65
62
 
66
63
  Automatically detects projects based on LLM.md files and manages .hanzo/ directories."""
67
-
64
+
68
65
  async def call(
69
66
  self,
70
67
  ctx: MCPContext,
71
68
  **params: Unpack[ConfigToolParams],
72
69
  ) -> str:
73
70
  """Manage configuration settings.
74
-
71
+
75
72
  Args:
76
73
  ctx: MCP context
77
74
  **params: Tool parameters
78
-
75
+
79
76
  Returns:
80
77
  Configuration operation result
81
78
  """
@@ -87,14 +84,20 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
87
84
  server_config = params.get("server_config")
88
85
  project_name = params.get("project_name")
89
86
  project_path = params.get("project_path")
90
-
87
+
91
88
  try:
92
89
  if action == "get":
93
- return await self._get_config(scope, setting_path, project_name, project_path)
90
+ return await self._get_config(
91
+ scope, setting_path, project_name, project_path
92
+ )
94
93
  elif action == "set":
95
- return await self._set_config(scope, setting_path, value, project_name, project_path)
94
+ return await self._set_config(
95
+ scope, setting_path, value, project_name, project_path
96
+ )
96
97
  elif action == "add_server":
97
- return await self._add_mcp_server(server_name, server_config, scope, project_name)
98
+ return await self._add_mcp_server(
99
+ server_name, server_config, scope, project_name
100
+ )
98
101
  elif action == "remove_server":
99
102
  return await self._remove_mcp_server(server_name, scope, project_name)
100
103
  elif action == "enable_server":
@@ -115,11 +118,17 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
115
118
  return await self._detect_project(project_path)
116
119
  else:
117
120
  return f"Error: Unknown action '{action}'. Available actions: get, set, add_server, remove_server, enable_server, disable_server, trust_server, add_project, set_current_project, list_servers, list_projects, detect_project"
118
-
121
+
119
122
  except Exception as e:
120
123
  return f"Error managing configuration: {str(e)}"
121
-
122
- async def _get_config(self, scope: str, setting_path: Optional[str], project_name: Optional[str], project_path: Optional[str]) -> str:
124
+
125
+ async def _get_config(
126
+ self,
127
+ scope: str,
128
+ setting_path: Optional[str],
129
+ project_name: Optional[str],
130
+ project_path: Optional[str],
131
+ ) -> str:
123
132
  """Get configuration value(s)."""
124
133
  # Load appropriate settings
125
134
  if scope == "project" or project_name or project_path:
@@ -133,14 +142,14 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
133
142
  settings = load_settings()
134
143
  else:
135
144
  settings = load_settings()
136
-
145
+
137
146
  if not setting_path:
138
147
  # Return full config summary
139
148
  if scope == "project" and settings.current_project:
140
149
  project = settings.get_current_project()
141
150
  if project:
142
151
  return f"Current Project Configuration ({project.name}):\\n{json.dumps(project.__dict__, indent=2)}"
143
-
152
+
144
153
  # Return global config summary
145
154
  summary = {
146
155
  "server": settings.server.__dict__,
@@ -149,24 +158,34 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
149
158
  "agent": settings.agent.__dict__,
150
159
  "vector_store": settings.vector_store.__dict__,
151
160
  "hub_enabled": settings.hub_enabled,
152
- "mcp_servers": {name: server.__dict__ for name, server in settings.mcp_servers.items()},
161
+ "mcp_servers": {
162
+ name: server.__dict__
163
+ for name, server in settings.mcp_servers.items()
164
+ },
153
165
  "current_project": settings.current_project,
154
166
  "projects": list(settings.projects.keys()),
155
167
  }
156
168
  return f"Configuration Summary:\\n{json.dumps(summary, indent=2)}"
157
-
169
+
158
170
  # Get specific setting
159
171
  value = self._get_nested_value(settings.__dict__, setting_path)
160
172
  if value is not None:
161
173
  return f"{setting_path}: {json.dumps(value, indent=2)}"
162
174
  else:
163
175
  return f"Setting '{setting_path}' not found"
164
-
165
- async def _set_config(self, scope: str, setting_path: Optional[str], value: Any, project_name: Optional[str], project_path: Optional[str]) -> str:
176
+
177
+ async def _set_config(
178
+ self,
179
+ scope: str,
180
+ setting_path: Optional[str],
181
+ value: Any,
182
+ project_name: Optional[str],
183
+ project_path: Optional[str],
184
+ ) -> str:
166
185
  """Set configuration value."""
167
186
  if not setting_path:
168
187
  return "Error: setting_path is required for set action"
169
-
188
+
170
189
  # Load settings
171
190
  project_dir = None
172
191
  if scope == "project" or project_name or project_path:
@@ -176,9 +195,9 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
176
195
  project_dir = project_info["root_path"]
177
196
  else:
178
197
  return f"No project detected at path: {project_path}"
179
-
198
+
180
199
  settings = load_settings(project_dir)
181
-
200
+
182
201
  # Set the value
183
202
  if self._set_nested_value(settings.__dict__, setting_path, value):
184
203
  # Save settings
@@ -189,14 +208,20 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
189
208
  return f"Successfully set {setting_path} = {json.dumps(value)}"
190
209
  else:
191
210
  return f"Error: Could not set '{setting_path}'"
192
-
193
- async def _add_mcp_server(self, server_name: Optional[str], server_config: Optional[Dict[str, Any]], scope: str, project_name: Optional[str]) -> str:
211
+
212
+ async def _add_mcp_server(
213
+ self,
214
+ server_name: Optional[str],
215
+ server_config: Optional[Dict[str, Any]],
216
+ scope: str,
217
+ project_name: Optional[str],
218
+ ) -> str:
194
219
  """Add a new MCP server."""
195
220
  if not server_name or not server_config:
196
221
  return "Error: server_name and server_config are required"
197
-
222
+
198
223
  settings = load_settings()
199
-
224
+
200
225
  # Create server config
201
226
  mcp_server = MCPServerConfig(
202
227
  name=server_name,
@@ -207,95 +232,105 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
207
232
  description=server_config.get("description", ""),
208
233
  capabilities=server_config.get("capabilities", []),
209
234
  )
210
-
235
+
211
236
  if settings.add_mcp_server(mcp_server):
212
237
  save_settings(settings)
213
238
  return f"Successfully added MCP server '{server_name}'"
214
239
  else:
215
240
  return f"Error: MCP server '{server_name}' already exists"
216
-
217
- async def _remove_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
241
+
242
+ async def _remove_mcp_server(
243
+ self, server_name: Optional[str], scope: str, project_name: Optional[str]
244
+ ) -> str:
218
245
  """Remove an MCP server."""
219
246
  if not server_name:
220
247
  return "Error: server_name is required"
221
-
248
+
222
249
  settings = load_settings()
223
-
250
+
224
251
  if settings.remove_mcp_server(server_name):
225
252
  save_settings(settings)
226
253
  return f"Successfully removed MCP server '{server_name}'"
227
254
  else:
228
255
  return f"Error: MCP server '{server_name}' not found"
229
-
230
- async def _enable_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
256
+
257
+ async def _enable_mcp_server(
258
+ self, server_name: Optional[str], scope: str, project_name: Optional[str]
259
+ ) -> str:
231
260
  """Enable an MCP server."""
232
261
  if not server_name:
233
262
  return "Error: server_name is required"
234
-
263
+
235
264
  settings = load_settings()
236
-
265
+
237
266
  if settings.enable_mcp_server(server_name):
238
267
  save_settings(settings)
239
268
  return f"Successfully enabled MCP server '{server_name}'"
240
269
  else:
241
270
  return f"Error: MCP server '{server_name}' not found"
242
-
243
- async def _disable_mcp_server(self, server_name: Optional[str], scope: str, project_name: Optional[str]) -> str:
271
+
272
+ async def _disable_mcp_server(
273
+ self, server_name: Optional[str], scope: str, project_name: Optional[str]
274
+ ) -> str:
244
275
  """Disable an MCP server."""
245
276
  if not server_name:
246
277
  return "Error: server_name is required"
247
-
278
+
248
279
  settings = load_settings()
249
-
280
+
250
281
  if settings.disable_mcp_server(server_name):
251
282
  save_settings(settings)
252
283
  return f"Successfully disabled MCP server '{server_name}'"
253
284
  else:
254
285
  return f"Error: MCP server '{server_name}' not found"
255
-
286
+
256
287
  async def _trust_mcp_server(self, server_name: Optional[str]) -> str:
257
288
  """Trust an MCP server."""
258
289
  if not server_name:
259
290
  return "Error: server_name is required"
260
-
291
+
261
292
  settings = load_settings()
262
-
293
+
263
294
  if settings.trust_mcp_server(server_name):
264
295
  save_settings(settings)
265
296
  return f"Successfully trusted MCP server '{server_name}'"
266
297
  else:
267
298
  return f"Error: MCP server '{server_name}' not found"
268
-
269
- async def _add_project(self, project_name: Optional[str], project_path: Optional[str]) -> str:
299
+
300
+ async def _add_project(
301
+ self, project_name: Optional[str], project_path: Optional[str]
302
+ ) -> str:
270
303
  """Add a project configuration."""
271
304
  if not project_path:
272
305
  return "Error: project_path is required"
273
-
306
+
274
307
  # Detect or create project
275
308
  project_info = detect_project_from_path(project_path)
276
309
  if not project_info:
277
310
  return f"No LLM.md found in project path: {project_path}"
278
-
311
+
279
312
  if not project_name:
280
313
  project_name = project_info["name"]
281
-
314
+
282
315
  settings = load_settings()
283
-
316
+
284
317
  project_config = ProjectConfig(
285
318
  name=project_name,
286
319
  root_path=project_info["root_path"],
287
320
  )
288
-
321
+
289
322
  if settings.add_project(project_config):
290
323
  save_settings(settings)
291
324
  return f"Successfully added project '{project_name}' at {project_info['root_path']}"
292
325
  else:
293
326
  return f"Error: Project '{project_name}' already exists"
294
-
295
- async def _set_current_project(self, project_name: Optional[str], project_path: Optional[str]) -> str:
327
+
328
+ async def _set_current_project(
329
+ self, project_name: Optional[str], project_path: Optional[str]
330
+ ) -> str:
296
331
  """Set the current active project."""
297
332
  settings = load_settings()
298
-
333
+
299
334
  if project_path:
300
335
  project_info = detect_project_from_path(project_path)
301
336
  if project_info:
@@ -304,62 +339,63 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
304
339
  if project_name not in settings.projects:
305
340
  await self._add_project(project_name, project_path)
306
341
  settings = load_settings() # Reload after adding
307
-
342
+
308
343
  if not project_name:
309
344
  return "Error: project_name or project_path is required"
310
-
345
+
311
346
  if settings.set_current_project(project_name):
312
347
  save_settings(settings)
313
348
  return f"Successfully set current project to '{project_name}'"
314
349
  else:
315
350
  return f"Error: Project '{project_name}' not found"
316
-
351
+
317
352
  async def _list_mcp_servers(self, scope: str, project_name: Optional[str]) -> str:
318
353
  """List MCP servers."""
319
354
  settings = load_settings()
320
-
355
+
321
356
  if not settings.mcp_servers:
322
357
  return "No MCP servers configured"
323
-
358
+
324
359
  servers_info = []
325
360
  for name, server in settings.mcp_servers.items():
326
361
  status = "enabled" if server.enabled else "disabled"
327
362
  trust = "trusted" if server.trusted else "untrusted"
328
363
  servers_info.append(f"- {name}: {server.command} ({status}, {trust})")
329
-
364
+
330
365
  return f"MCP Servers:\\n" + "\\n".join(servers_info)
331
-
366
+
332
367
  async def _list_projects(self) -> str:
333
368
  """List projects."""
334
369
  settings = load_settings()
335
-
370
+
336
371
  if not settings.projects:
337
372
  return "No projects configured"
338
-
373
+
339
374
  projects_info = []
340
375
  for name, project in settings.projects.items():
341
376
  current = " (current)" if name == settings.current_project else ""
342
377
  projects_info.append(f"- {name}: {project.root_path}{current}")
343
-
378
+
344
379
  return f"Projects:\\n" + "\\n".join(projects_info)
345
-
380
+
346
381
  async def _detect_project(self, project_path: Optional[str]) -> str:
347
382
  """Detect project from path."""
348
383
  if not project_path:
349
384
  import os
385
+
350
386
  project_path = os.getcwd()
351
-
387
+
352
388
  project_info = detect_project_from_path(project_path)
353
389
  if project_info:
354
390
  return f"Project detected:\\n{json.dumps(project_info, indent=2)}"
355
391
  else:
356
392
  return f"No project detected at path: {project_path}"
357
-
393
+
358
394
  def _get_nested_value(self, obj: Dict[str, Any], path: str) -> Any:
359
395
  """Get nested value using dot notation."""
360
396
  keys = path.split(".")
361
397
  current = obj
362
-
398
+
363
399
  for key in keys:
364
400
  if isinstance(current, dict) and key in current:
365
401
  current = current[key]
@@ -367,14 +403,14 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
367
403
  current = getattr(current, key)
368
404
  else:
369
405
  return None
370
-
406
+
371
407
  return current
372
-
408
+
373
409
  def _set_nested_value(self, obj: Dict[str, Any], path: str, value: Any) -> bool:
374
410
  """Set nested value using dot notation."""
375
411
  keys = path.split(".")
376
412
  current = obj
377
-
413
+
378
414
  # Navigate to parent
379
415
  for key in keys[:-1]:
380
416
  if isinstance(current, dict) and key in current:
@@ -383,7 +419,7 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
383
419
  current = getattr(current, key)
384
420
  else:
385
421
  return False
386
-
422
+
387
423
  # Set final value
388
424
  final_key = keys[-1]
389
425
  if isinstance(current, dict):
@@ -392,5 +428,5 @@ Automatically detects projects based on LLM.md files and manages .hanzo/ directo
392
428
  elif hasattr(current, final_key):
393
429
  setattr(current, final_key, value)
394
430
  return True
395
-
396
- return False
431
+
432
+ return False
@@ -4,12 +4,8 @@ This module provides an enhanced Context class that wraps the MCP Context
4
4
  and adds additional functionality specific to Hanzo tools.
5
5
  """
6
6
 
7
- import json
8
- import os
9
- import time
7
+ from typing import ClassVar, final
10
8
  from collections.abc import Iterable
11
- from pathlib import Path
12
- from typing import Any, ClassVar, final
13
9
 
14
10
  from mcp.server.fastmcp import Context as MCPContext
15
11
  from mcp.server.lowlevel.helper_types import ReadResourceContents
@@ -9,18 +9,20 @@ DEPRECATED: Use hanzo_mcp.tools.common.decorators directly.
9
9
  # Re-export for backward compatibility
10
10
  from hanzo_mcp.tools.common.decorators import (
11
11
  MockContext,
12
- with_context_normalization,
13
12
  _is_valid_context as is_valid_context,
13
+ with_context_normalization,
14
14
  )
15
15
 
16
+
16
17
  # Backward compatibility function
17
18
  def normalize_context(ctx):
18
19
  """Normalize context - backward compatibility wrapper.
19
-
20
+
20
21
  DEPRECATED: Use decorators.with_context_normalization instead.
21
22
  """
22
23
  if is_valid_context(ctx):
23
24
  return ctx
24
25
  return MockContext()
25
26
 
26
- __all__ = ['MockContext', 'normalize_context', 'with_context_normalization']
27
+
28
+ __all__ = ["MockContext", "normalize_context", "with_context_normalization"]