hanzo-mcp 0.5.2__py3-none-any.whl → 0.6.2__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 (114) hide show
  1. hanzo_mcp/__init__.py +1 -1
  2. hanzo_mcp/cli.py +32 -0
  3. hanzo_mcp/dev_server.py +246 -0
  4. hanzo_mcp/prompts/__init__.py +1 -1
  5. hanzo_mcp/prompts/project_system.py +43 -7
  6. hanzo_mcp/server.py +5 -1
  7. hanzo_mcp/tools/__init__.py +66 -35
  8. hanzo_mcp/tools/agent/__init__.py +1 -1
  9. hanzo_mcp/tools/agent/agent.py +401 -0
  10. hanzo_mcp/tools/agent/agent_tool.py +3 -4
  11. hanzo_mcp/tools/common/__init__.py +1 -1
  12. hanzo_mcp/tools/common/base.py +2 -2
  13. hanzo_mcp/tools/common/batch_tool.py +3 -5
  14. hanzo_mcp/tools/common/config_tool.py +1 -1
  15. hanzo_mcp/tools/common/context.py +1 -1
  16. hanzo_mcp/tools/common/palette.py +344 -0
  17. hanzo_mcp/tools/common/palette_loader.py +108 -0
  18. hanzo_mcp/tools/common/stats.py +1 -1
  19. hanzo_mcp/tools/common/thinking_tool.py +3 -5
  20. hanzo_mcp/tools/common/tool_disable.py +1 -1
  21. hanzo_mcp/tools/common/tool_enable.py +1 -1
  22. hanzo_mcp/tools/common/tool_list.py +49 -52
  23. hanzo_mcp/tools/config/__init__.py +10 -0
  24. hanzo_mcp/tools/config/config_tool.py +212 -0
  25. hanzo_mcp/tools/config/index_config.py +176 -0
  26. hanzo_mcp/tools/config/palette_tool.py +166 -0
  27. hanzo_mcp/tools/database/__init__.py +1 -1
  28. hanzo_mcp/tools/database/graph.py +482 -0
  29. hanzo_mcp/tools/database/graph_add.py +1 -1
  30. hanzo_mcp/tools/database/graph_query.py +1 -1
  31. hanzo_mcp/tools/database/graph_remove.py +1 -1
  32. hanzo_mcp/tools/database/graph_search.py +1 -1
  33. hanzo_mcp/tools/database/graph_stats.py +1 -1
  34. hanzo_mcp/tools/database/sql.py +411 -0
  35. hanzo_mcp/tools/database/sql_query.py +1 -1
  36. hanzo_mcp/tools/database/sql_search.py +1 -1
  37. hanzo_mcp/tools/database/sql_stats.py +1 -1
  38. hanzo_mcp/tools/editor/neovim_command.py +1 -1
  39. hanzo_mcp/tools/editor/neovim_edit.py +1 -1
  40. hanzo_mcp/tools/editor/neovim_session.py +1 -1
  41. hanzo_mcp/tools/filesystem/__init__.py +42 -13
  42. hanzo_mcp/tools/filesystem/base.py +1 -1
  43. hanzo_mcp/tools/filesystem/batch_search.py +4 -4
  44. hanzo_mcp/tools/filesystem/content_replace.py +3 -5
  45. hanzo_mcp/tools/filesystem/diff.py +193 -0
  46. hanzo_mcp/tools/filesystem/directory_tree.py +3 -5
  47. hanzo_mcp/tools/filesystem/edit.py +3 -5
  48. hanzo_mcp/tools/filesystem/find.py +443 -0
  49. hanzo_mcp/tools/filesystem/find_files.py +1 -1
  50. hanzo_mcp/tools/filesystem/git_search.py +1 -1
  51. hanzo_mcp/tools/filesystem/grep.py +2 -2
  52. hanzo_mcp/tools/filesystem/multi_edit.py +3 -5
  53. hanzo_mcp/tools/filesystem/read.py +17 -5
  54. hanzo_mcp/tools/filesystem/{grep_ast_tool.py → symbols.py} +17 -27
  55. hanzo_mcp/tools/filesystem/symbols_unified.py +376 -0
  56. hanzo_mcp/tools/filesystem/tree.py +268 -0
  57. hanzo_mcp/tools/filesystem/unified_search.py +711 -0
  58. hanzo_mcp/tools/filesystem/unix_aliases.py +99 -0
  59. hanzo_mcp/tools/filesystem/watch.py +174 -0
  60. hanzo_mcp/tools/filesystem/write.py +3 -5
  61. hanzo_mcp/tools/jupyter/__init__.py +9 -12
  62. hanzo_mcp/tools/jupyter/base.py +1 -1
  63. hanzo_mcp/tools/jupyter/jupyter.py +326 -0
  64. hanzo_mcp/tools/jupyter/notebook_edit.py +3 -4
  65. hanzo_mcp/tools/jupyter/notebook_read.py +3 -5
  66. hanzo_mcp/tools/llm/__init__.py +4 -0
  67. hanzo_mcp/tools/llm/consensus_tool.py +1 -1
  68. hanzo_mcp/tools/llm/llm_manage.py +1 -1
  69. hanzo_mcp/tools/llm/llm_tool.py +1 -1
  70. hanzo_mcp/tools/llm/llm_unified.py +851 -0
  71. hanzo_mcp/tools/llm/provider_tools.py +1 -1
  72. hanzo_mcp/tools/mcp/__init__.py +4 -0
  73. hanzo_mcp/tools/mcp/mcp_add.py +1 -1
  74. hanzo_mcp/tools/mcp/mcp_remove.py +1 -1
  75. hanzo_mcp/tools/mcp/mcp_stats.py +1 -1
  76. hanzo_mcp/tools/mcp/mcp_unified.py +503 -0
  77. hanzo_mcp/tools/shell/__init__.py +20 -42
  78. hanzo_mcp/tools/shell/base.py +1 -1
  79. hanzo_mcp/tools/shell/base_process.py +303 -0
  80. hanzo_mcp/tools/shell/bash_unified.py +134 -0
  81. hanzo_mcp/tools/shell/logs.py +1 -1
  82. hanzo_mcp/tools/shell/npx.py +1 -1
  83. hanzo_mcp/tools/shell/npx_background.py +1 -1
  84. hanzo_mcp/tools/shell/npx_unified.py +101 -0
  85. hanzo_mcp/tools/shell/open.py +107 -0
  86. hanzo_mcp/tools/shell/pkill.py +1 -1
  87. hanzo_mcp/tools/shell/process_unified.py +131 -0
  88. hanzo_mcp/tools/shell/processes.py +1 -1
  89. hanzo_mcp/tools/shell/run_background.py +1 -1
  90. hanzo_mcp/tools/shell/run_command.py +3 -4
  91. hanzo_mcp/tools/shell/run_command_windows.py +3 -4
  92. hanzo_mcp/tools/shell/uvx.py +1 -1
  93. hanzo_mcp/tools/shell/uvx_background.py +1 -1
  94. hanzo_mcp/tools/shell/uvx_unified.py +101 -0
  95. hanzo_mcp/tools/todo/__init__.py +1 -1
  96. hanzo_mcp/tools/todo/base.py +1 -1
  97. hanzo_mcp/tools/todo/todo.py +265 -0
  98. hanzo_mcp/tools/todo/todo_read.py +3 -5
  99. hanzo_mcp/tools/todo/todo_write.py +3 -5
  100. hanzo_mcp/tools/vector/__init__.py +1 -1
  101. hanzo_mcp/tools/vector/index_tool.py +1 -1
  102. hanzo_mcp/tools/vector/project_manager.py +27 -5
  103. hanzo_mcp/tools/vector/vector.py +311 -0
  104. hanzo_mcp/tools/vector/vector_index.py +1 -1
  105. hanzo_mcp/tools/vector/vector_search.py +1 -1
  106. hanzo_mcp-0.6.2.dist-info/METADATA +336 -0
  107. hanzo_mcp-0.6.2.dist-info/RECORD +134 -0
  108. hanzo_mcp-0.6.2.dist-info/entry_points.txt +3 -0
  109. hanzo_mcp-0.5.2.dist-info/METADATA +0 -276
  110. hanzo_mcp-0.5.2.dist-info/RECORD +0 -106
  111. hanzo_mcp-0.5.2.dist-info/entry_points.txt +0 -2
  112. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.2.dist-info}/WHEEL +0 -0
  113. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.2.dist-info}/licenses/LICENSE +0 -0
  114. {hanzo_mcp-0.5.2.dist-info → hanzo_mcp-0.6.2.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,212 @@
1
+ """Configuration tool for Hanzo MCP.
2
+
3
+ Git-style config tool for managing settings.
4
+ """
5
+
6
+ from typing import Annotated, TypedDict, Unpack, final, override, Optional, Dict, Any
7
+ from pathlib import Path
8
+ import json
9
+
10
+ from mcp.server.fastmcp import Context as MCPContext
11
+ from pydantic import Field
12
+
13
+ from hanzo_mcp.tools.common.base import BaseTool
14
+ from hanzo_mcp.tools.common.permissions import PermissionManager
15
+ from hanzo_mcp.tools.config.index_config import IndexConfig, IndexScope
16
+
17
+
18
+ # Parameter types
19
+ Action = Annotated[
20
+ str,
21
+ Field(
22
+ description="Action: get (default), set, list, toggle",
23
+ default="get",
24
+ ),
25
+ ]
26
+
27
+ Key = Annotated[
28
+ Optional[str],
29
+ Field(
30
+ description="Configuration key (e.g., index.scope, vector.enabled)",
31
+ default=None,
32
+ ),
33
+ ]
34
+
35
+ Value = Annotated[
36
+ Optional[str],
37
+ Field(
38
+ description="Configuration value",
39
+ default=None,
40
+ ),
41
+ ]
42
+
43
+ Scope = Annotated[
44
+ str,
45
+ Field(
46
+ description="Config scope: local (project) or global",
47
+ default="local",
48
+ ),
49
+ ]
50
+
51
+ ConfigPath = Annotated[
52
+ Optional[str],
53
+ Field(
54
+ description="Path for project-specific config",
55
+ default=None,
56
+ ),
57
+ ]
58
+
59
+
60
+ class ConfigParams(TypedDict, total=False):
61
+ """Parameters for config tool."""
62
+ action: str
63
+ key: Optional[str]
64
+ value: Optional[str]
65
+ scope: str
66
+ path: Optional[str]
67
+
68
+
69
+ @final
70
+ class ConfigTool(BaseTool):
71
+ """Git-style configuration management tool."""
72
+
73
+ def __init__(self, permission_manager: PermissionManager):
74
+ """Initialize config tool."""
75
+ super().__init__(permission_manager)
76
+ self.index_config = IndexConfig()
77
+
78
+ @property
79
+ @override
80
+ def name(self) -> str:
81
+ """Get the tool name."""
82
+ return "config"
83
+
84
+ @property
85
+ @override
86
+ def description(self) -> str:
87
+ """Get the tool description."""
88
+ return """Git-style configuration. Actions: get (default), set, list, toggle.
89
+
90
+ Usage:
91
+ config index.scope
92
+ config --action set index.scope project
93
+ config --action list
94
+ config --action toggle index.scope --path ./project"""
95
+
96
+ @override
97
+ async def call(
98
+ self,
99
+ ctx: MCPContext,
100
+ **params: Unpack[ConfigParams],
101
+ ) -> str:
102
+ """Execute config operation."""
103
+ tool_ctx = self.create_tool_context(ctx)
104
+
105
+ # Extract parameters
106
+ action = params.get("action", "get")
107
+ key = params.get("key")
108
+ value = params.get("value")
109
+ scope = params.get("scope", "local")
110
+ path = params.get("path")
111
+
112
+ # Route to handler
113
+ if action == "get":
114
+ return await self._handle_get(key, scope, path, tool_ctx)
115
+ elif action == "set":
116
+ return await self._handle_set(key, value, scope, path, tool_ctx)
117
+ elif action == "list":
118
+ return await self._handle_list(scope, path, tool_ctx)
119
+ elif action == "toggle":
120
+ return await self._handle_toggle(key, scope, path, tool_ctx)
121
+ else:
122
+ return f"Error: Unknown action '{action}'. Valid actions: get, set, list, toggle"
123
+
124
+ async def _handle_get(self, key: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
125
+ """Get configuration value."""
126
+ if not key:
127
+ return "Error: key required for get action"
128
+
129
+ # Handle index scope
130
+ if key == "index.scope":
131
+ current_scope = self.index_config.get_scope(path)
132
+ return f"index.scope={current_scope.value}"
133
+
134
+ # Handle tool-specific settings
135
+ if "." in key:
136
+ tool, setting = key.split(".", 1)
137
+ if setting == "enabled":
138
+ enabled = self.index_config.is_indexing_enabled(tool)
139
+ return f"{key}={enabled}"
140
+
141
+ return f"Unknown key: {key}"
142
+
143
+ async def _handle_set(self, key: Optional[str], value: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
144
+ """Set configuration value."""
145
+ if not key:
146
+ return "Error: key required for set action"
147
+ if not value:
148
+ return "Error: value required for set action"
149
+
150
+ # Handle index scope
151
+ if key == "index.scope":
152
+ try:
153
+ new_scope = IndexScope(value)
154
+ self.index_config.set_scope(new_scope, path if scope == "local" else None)
155
+ return f"Set {key}={value} ({'project' if path else 'global'})"
156
+ except ValueError:
157
+ return f"Error: Invalid scope value '{value}'. Valid: project, global, auto"
158
+
159
+ # Handle tool-specific settings
160
+ if "." in key:
161
+ tool, setting = key.split(".", 1)
162
+ if setting == "enabled":
163
+ enabled = value.lower() in ["true", "yes", "1", "on"]
164
+ self.index_config.set_indexing_enabled(tool, enabled)
165
+ return f"Set {key}={enabled}"
166
+
167
+ return f"Unknown key: {key}"
168
+
169
+ async def _handle_list(self, scope: str, path: Optional[str], tool_ctx) -> str:
170
+ """List all configuration."""
171
+ status = self.index_config.get_status()
172
+
173
+ output = ["=== Configuration ==="]
174
+ output.append(f"\nDefault scope: {status['default_scope']}")
175
+
176
+ if path:
177
+ current_scope = self.index_config.get_scope(path)
178
+ output.append(f"Current path scope: {current_scope.value}")
179
+
180
+ output.append(f"\nProjects with custom config: {status['project_count']}")
181
+
182
+ output.append("\nTool settings:")
183
+ for tool, settings in status["tools"].items():
184
+ output.append(f" {tool}:")
185
+ output.append(f" enabled: {settings['enabled']}")
186
+ output.append(f" per_project: {settings['per_project']}")
187
+
188
+ return "\n".join(output)
189
+
190
+ async def _handle_toggle(self, key: Optional[str], scope: str, path: Optional[str], tool_ctx) -> str:
191
+ """Toggle configuration value."""
192
+ if not key:
193
+ return "Error: key required for toggle action"
194
+
195
+ # Handle index scope toggle
196
+ if key == "index.scope":
197
+ new_scope = self.index_config.toggle_scope(path if scope == "local" else None)
198
+ return f"Toggled index.scope to {new_scope.value}"
199
+
200
+ # Handle tool enable/disable toggle
201
+ if "." in key:
202
+ tool, setting = key.split(".", 1)
203
+ if setting == "enabled":
204
+ current = self.index_config.is_indexing_enabled(tool)
205
+ self.index_config.set_indexing_enabled(tool, not current)
206
+ return f"Toggled {key} to {not current}"
207
+
208
+ return f"Cannot toggle key: {key}"
209
+
210
+ def register(self, mcp_server) -> None:
211
+ """Register this tool with the MCP server."""
212
+ pass
@@ -0,0 +1,176 @@
1
+ """Index configuration for per-project vs global indexing.
2
+
3
+ This module manages indexing configuration for different scopes.
4
+ """
5
+
6
+ import json
7
+ from pathlib import Path
8
+ from typing import Dict, Any, Optional
9
+ from enum import Enum
10
+
11
+
12
+ class IndexScope(Enum):
13
+ """Indexing scope options."""
14
+ PROJECT = "project" # Per-project indexing
15
+ GLOBAL = "global" # Global indexing
16
+ AUTO = "auto" # Auto-detect based on git root
17
+
18
+
19
+ class IndexConfig:
20
+ """Manages indexing configuration."""
21
+
22
+ def __init__(self, config_dir: Optional[Path] = None):
23
+ """Initialize index configuration."""
24
+ self.config_dir = config_dir or Path.home() / ".hanzo" / "mcp"
25
+ self.config_file = self.config_dir / "index_config.json"
26
+ self._config = self._load_config()
27
+
28
+ def _load_config(self) -> Dict[str, Any]:
29
+ """Load configuration from disk."""
30
+ if self.config_file.exists():
31
+ try:
32
+ with open(self.config_file, "r") as f:
33
+ return json.load(f)
34
+ except:
35
+ pass
36
+
37
+ # Default configuration
38
+ return {
39
+ "default_scope": IndexScope.AUTO.value,
40
+ "project_configs": {},
41
+ "global_index_paths": [],
42
+ "index_settings": {
43
+ "vector": {
44
+ "enabled": True,
45
+ "auto_index": True,
46
+ "include_git_history": True,
47
+ },
48
+ "symbols": {
49
+ "enabled": True,
50
+ "auto_index": False,
51
+ },
52
+ "sql": {
53
+ "enabled": True,
54
+ "per_project": True,
55
+ },
56
+ "graph": {
57
+ "enabled": True,
58
+ "per_project": True,
59
+ }
60
+ }
61
+ }
62
+
63
+ def save_config(self) -> None:
64
+ """Save configuration to disk."""
65
+ self.config_dir.mkdir(parents=True, exist_ok=True)
66
+ with open(self.config_file, "w") as f:
67
+ json.dump(self._config, f, indent=2)
68
+
69
+ def get_scope(self, path: Optional[str] = None) -> IndexScope:
70
+ """Get indexing scope for a path."""
71
+ if not path:
72
+ return IndexScope(self._config["default_scope"])
73
+
74
+ # Check project-specific config
75
+ project_root = self._find_project_root(path)
76
+ if project_root:
77
+ project_config = self._config["project_configs"].get(str(project_root))
78
+ if project_config and "scope" in project_config:
79
+ return IndexScope(project_config["scope"])
80
+
81
+ # Use default
82
+ scope = IndexScope(self._config["default_scope"])
83
+
84
+ # Handle auto mode
85
+ if scope == IndexScope.AUTO:
86
+ if project_root:
87
+ return IndexScope.PROJECT
88
+ else:
89
+ return IndexScope.GLOBAL
90
+
91
+ return scope
92
+
93
+ def set_scope(self, scope: IndexScope, path: Optional[str] = None) -> None:
94
+ """Set indexing scope."""
95
+ if path:
96
+ # Set for specific project
97
+ project_root = self._find_project_root(path)
98
+ if project_root:
99
+ if str(project_root) not in self._config["project_configs"]:
100
+ self._config["project_configs"][str(project_root)] = {}
101
+ self._config["project_configs"][str(project_root)]["scope"] = scope.value
102
+ else:
103
+ # Set global default
104
+ self._config["default_scope"] = scope.value
105
+
106
+ self.save_config()
107
+
108
+ def get_index_path(self, tool: str, path: Optional[str] = None) -> Path:
109
+ """Get index path for a tool and location."""
110
+ scope = self.get_scope(path)
111
+
112
+ if scope == IndexScope.PROJECT and path:
113
+ project_root = self._find_project_root(path)
114
+ if project_root:
115
+ return Path(project_root) / ".hanzo" / "index" / tool
116
+
117
+ # Global index
118
+ return self.config_dir / "index" / tool
119
+
120
+ def is_indexing_enabled(self, tool: str) -> bool:
121
+ """Check if indexing is enabled for a tool."""
122
+ return self._config["index_settings"].get(tool, {}).get("enabled", True)
123
+
124
+ def set_indexing_enabled(self, tool: str, enabled: bool) -> None:
125
+ """Enable/disable indexing for a tool."""
126
+ if tool not in self._config["index_settings"]:
127
+ self._config["index_settings"][tool] = {}
128
+ self._config["index_settings"][tool]["enabled"] = enabled
129
+ self.save_config()
130
+
131
+ def toggle_scope(self, path: Optional[str] = None) -> IndexScope:
132
+ """Toggle between project and global scope."""
133
+ current = self.get_scope(path)
134
+
135
+ if current == IndexScope.PROJECT:
136
+ new_scope = IndexScope.GLOBAL
137
+ elif current == IndexScope.GLOBAL:
138
+ new_scope = IndexScope.PROJECT
139
+ else: # AUTO
140
+ # Determine what auto resolves to and toggle
141
+ if path and self._find_project_root(path):
142
+ new_scope = IndexScope.GLOBAL
143
+ else:
144
+ new_scope = IndexScope.PROJECT
145
+
146
+ self.set_scope(new_scope, path)
147
+ return new_scope
148
+
149
+ def _find_project_root(self, path: str) -> Optional[Path]:
150
+ """Find project root (git root or similar)."""
151
+ current = Path(path).resolve()
152
+
153
+ # Walk up looking for markers
154
+ markers = [".git", ".hg", "pyproject.toml", "package.json", "Cargo.toml"]
155
+
156
+ while current != current.parent:
157
+ for marker in markers:
158
+ if (current / marker).exists():
159
+ return current
160
+ current = current.parent
161
+
162
+ return None
163
+
164
+ def get_status(self) -> Dict[str, Any]:
165
+ """Get current configuration status."""
166
+ return {
167
+ "default_scope": self._config["default_scope"],
168
+ "project_count": len(self._config["project_configs"]),
169
+ "tools": {
170
+ tool: {
171
+ "enabled": settings.get("enabled", True),
172
+ "per_project": settings.get("per_project", True),
173
+ }
174
+ for tool, settings in self._config["index_settings"].items()
175
+ }
176
+ }
@@ -0,0 +1,166 @@
1
+ """Tool for managing development tool palettes."""
2
+
3
+ from typing import Optional, override
4
+
5
+ from mcp.server.fastmcp import Context as MCPContext
6
+
7
+ from hanzo_mcp.tools.common.base import BaseTool
8
+ from hanzo_mcp.tools.common.palette import PaletteRegistry, register_default_palettes
9
+ from mcp.server import FastMCP
10
+
11
+
12
+ class PaletteTool(BaseTool):
13
+ """Tool for managing tool palettes."""
14
+
15
+ name = "palette"
16
+
17
+ def __init__(self):
18
+ """Initialize the palette tool."""
19
+ super().__init__()
20
+ # Register default palettes on initialization
21
+ register_default_palettes()
22
+
23
+ @property
24
+ @override
25
+ def description(self) -> str:
26
+ """Get the tool description."""
27
+ return """Manage tool palettes. Actions: list (default), activate, show, current.
28
+
29
+ Usage:
30
+ palette
31
+ palette --action list
32
+ palette --action activate python
33
+ palette --action show javascript
34
+ palette --action current"""
35
+
36
+ @override
37
+ async def run(
38
+ self,
39
+ ctx: MCPContext,
40
+ action: str = "list",
41
+ name: Optional[str] = None,
42
+ ) -> str:
43
+ """Manage tool palettes.
44
+
45
+ Args:
46
+ ctx: MCP context
47
+ action: Action to perform (list, activate, show, current)
48
+ name: Palette name (for activate/show actions)
49
+
50
+ Returns:
51
+ Action result
52
+ """
53
+ if action == "list":
54
+ palettes = PaletteRegistry.list()
55
+ if not palettes:
56
+ return "No palettes registered"
57
+
58
+ output = ["Available tool palettes:"]
59
+ active = PaletteRegistry.get_active()
60
+
61
+ for palette in sorted(palettes, key=lambda p: p.name):
62
+ marker = " (active)" if active and active.name == palette.name else ""
63
+ author = f" by {palette.author}" if palette.author else ""
64
+ output.append(f"\n{palette.name}{marker}:")
65
+ output.append(f" {palette.description}{author}")
66
+ output.append(f" Tools: {len(palette.tools)} enabled")
67
+
68
+ return "\n".join(output)
69
+
70
+ elif action == "activate":
71
+ if not name:
72
+ return "Error: Palette name required for activate action"
73
+
74
+ try:
75
+ PaletteRegistry.set_active(name)
76
+ palette = PaletteRegistry.get(name)
77
+
78
+ output = [f"Activated palette: {palette.name}"]
79
+ if palette.author:
80
+ output.append(f"Author: {palette.author}")
81
+ output.append(f"Description: {palette.description}")
82
+ output.append(f"\nEnabled tools ({len(palette.tools)}):")
83
+
84
+ # Group tools by category
85
+ core_tools = []
86
+ package_tools = []
87
+ other_tools = []
88
+
89
+ for tool in sorted(palette.tools):
90
+ if tool in ["read", "write", "edit", "grep", "tree", "find", "bash"]:
91
+ core_tools.append(tool)
92
+ elif tool in ["npx", "uvx", "pip", "cargo", "gem"]:
93
+ package_tools.append(tool)
94
+ else:
95
+ other_tools.append(tool)
96
+
97
+ if core_tools:
98
+ output.append(f" Core: {', '.join(core_tools)}")
99
+ if package_tools:
100
+ output.append(f" Package managers: {', '.join(package_tools)}")
101
+ if other_tools:
102
+ output.append(f" Specialized: {', '.join(other_tools)}")
103
+
104
+ if palette.environment:
105
+ output.append("\nEnvironment variables:")
106
+ for key, value in palette.environment.items():
107
+ output.append(f" {key}={value}")
108
+
109
+ output.append("\nNote: Restart MCP session for changes to take full effect")
110
+
111
+ return "\n".join(output)
112
+
113
+ except ValueError as e:
114
+ return str(e)
115
+
116
+ elif action == "show":
117
+ if not name:
118
+ return "Error: Palette name required for show action"
119
+
120
+ palette = PaletteRegistry.get(name)
121
+ if not palette:
122
+ return f"Palette '{name}' not found"
123
+
124
+ output = [f"Palette: {palette.name}"]
125
+ if palette.author:
126
+ output.append(f"Author: {palette.author}")
127
+ output.append(f"Description: {palette.description}")
128
+ output.append(f"\nTools ({len(palette.tools)}):")
129
+
130
+ for tool in sorted(palette.tools):
131
+ output.append(f" - {tool}")
132
+
133
+ if palette.environment:
134
+ output.append("\nEnvironment:")
135
+ for key, value in palette.environment.items():
136
+ output.append(f" {key}={value}")
137
+
138
+ return "\n".join(output)
139
+
140
+ elif action == "current":
141
+ active = PaletteRegistry.get_active()
142
+ if not active:
143
+ return "No palette currently active\nUse 'palette --action activate <name>' to activate one"
144
+
145
+ output = [f"Current palette: {active.name}"]
146
+ if active.author:
147
+ output.append(f"Author: {active.author}")
148
+ output.append(f"Description: {active.description}")
149
+ output.append(f"Enabled tools: {len(active.tools)}")
150
+
151
+ return "\n".join(output)
152
+
153
+ else:
154
+ return f"Unknown action: {action}. Use 'list', 'activate', 'show', or 'current'"
155
+
156
+ def register(self, server: FastMCP) -> None:
157
+ """Register the tool with the MCP server."""
158
+ server.tool(name=self.name, description=self.description)(self.call)
159
+
160
+ async def call(self, **kwargs) -> str:
161
+ """Call the tool with arguments."""
162
+ return await self.run(None, **kwargs)
163
+
164
+
165
+ # Create tool instance
166
+ palette_tool = PaletteTool()
@@ -6,7 +6,7 @@ and graph databases in projects.
6
6
 
7
7
  from hanzo_mcp.tools.common.base import BaseTool
8
8
  from hanzo_mcp.tools.common.permissions import PermissionManager
9
- from fastmcp import FastMCP
9
+ from mcp.server import FastMCP
10
10
 
11
11
  # Import database tools
12
12
  from .sql_query import SqlQueryTool