hanzo-mcp 0.6.12__py3-none-any.whl → 0.6.13__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.
- hanzo_mcp/__init__.py +2 -2
- hanzo_mcp/cli.py +2 -2
- hanzo_mcp/cli_enhanced.py +4 -4
- hanzo_mcp/cli_plugin.py +91 -0
- hanzo_mcp/config/__init__.py +1 -1
- hanzo_mcp/config/settings.py +69 -6
- hanzo_mcp/config/tool_config.py +2 -2
- hanzo_mcp/dev_server.py +3 -3
- hanzo_mcp/prompts/project_system.py +1 -1
- hanzo_mcp/server.py +6 -2
- hanzo_mcp/server_enhanced.py +69 -0
- hanzo_mcp/tools/__init__.py +75 -29
- hanzo_mcp/tools/agent/__init__.py +1 -1
- hanzo_mcp/tools/agent/agent_tool.py +2 -2
- hanzo_mcp/tools/common/__init__.py +15 -1
- hanzo_mcp/tools/common/base.py +4 -4
- hanzo_mcp/tools/common/batch_tool.py +1 -1
- hanzo_mcp/tools/common/config_tool.py +2 -2
- hanzo_mcp/tools/common/context.py +2 -2
- hanzo_mcp/tools/common/context_fix.py +26 -0
- hanzo_mcp/tools/common/critic_tool.py +196 -0
- hanzo_mcp/tools/common/decorators.py +208 -0
- hanzo_mcp/tools/common/enhanced_base.py +106 -0
- hanzo_mcp/tools/common/mode.py +116 -0
- hanzo_mcp/tools/common/mode_loader.py +105 -0
- hanzo_mcp/tools/common/permissions.py +1 -1
- hanzo_mcp/tools/common/personality.py +936 -0
- hanzo_mcp/tools/common/plugin_loader.py +287 -0
- hanzo_mcp/tools/common/stats.py +4 -4
- hanzo_mcp/tools/common/tool_list.py +1 -1
- hanzo_mcp/tools/common/validation.py +1 -1
- hanzo_mcp/tools/config/__init__.py +3 -1
- hanzo_mcp/tools/config/config_tool.py +1 -1
- hanzo_mcp/tools/config/mode_tool.py +209 -0
- hanzo_mcp/tools/database/__init__.py +1 -1
- hanzo_mcp/tools/editor/__init__.py +1 -1
- hanzo_mcp/tools/filesystem/__init__.py +19 -14
- hanzo_mcp/tools/filesystem/batch_search.py +3 -3
- hanzo_mcp/tools/filesystem/diff.py +2 -2
- hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
- hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
- hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
- hanzo_mcp/tools/filesystem/watch.py +3 -2
- hanzo_mcp/tools/jupyter/__init__.py +2 -2
- hanzo_mcp/tools/jupyter/jupyter.py +1 -1
- hanzo_mcp/tools/llm/__init__.py +3 -3
- hanzo_mcp/tools/llm/llm_tool.py +648 -143
- hanzo_mcp/tools/mcp/__init__.py +2 -2
- hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
- hanzo_mcp/tools/shell/__init__.py +6 -6
- hanzo_mcp/tools/shell/base_process.py +4 -2
- hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
- hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +1 -1
- hanzo_mcp/tools/shell/command_executor.py +2 -2
- hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +1 -1
- hanzo_mcp/tools/shell/open.py +2 -2
- hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
- hanzo_mcp/tools/shell/run_command_windows.py +1 -1
- hanzo_mcp/tools/shell/uvx.py +47 -2
- hanzo_mcp/tools/shell/uvx_background.py +47 -2
- hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +1 -1
- hanzo_mcp/tools/todo/__init__.py +14 -19
- hanzo_mcp/tools/todo/todo.py +22 -1
- hanzo_mcp/tools/vector/__init__.py +1 -1
- hanzo_mcp/tools/vector/infinity_store.py +2 -2
- hanzo_mcp/tools/vector/project_manager.py +1 -1
- hanzo_mcp-0.6.13.dist-info/METADATA +359 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/RECORD +72 -64
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/entry_points.txt +1 -0
- hanzo_mcp/tools/common/palette.py +0 -344
- hanzo_mcp/tools/common/palette_loader.py +0 -108
- hanzo_mcp/tools/config/palette_tool.py +0 -179
- hanzo_mcp/tools/llm/llm_unified.py +0 -851
- hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/WHEEL +0 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/licenses/LICENSE +0 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.6.13.dist-info}/top_level.txt +0 -0
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
"""Common utilities for Hanzo
|
|
1
|
+
"""Common utilities for Hanzo AI tools."""
|
|
2
2
|
|
|
3
3
|
from mcp.server import FastMCP
|
|
4
4
|
|
|
5
5
|
from hanzo_mcp.tools.common.base import BaseTool, ToolRegistry
|
|
6
6
|
from hanzo_mcp.tools.common.batch_tool import BatchTool
|
|
7
7
|
from hanzo_mcp.tools.common.thinking_tool import ThinkingTool
|
|
8
|
+
from hanzo_mcp.tools.common.critic_tool import CriticTool
|
|
8
9
|
|
|
9
10
|
|
|
10
11
|
def register_thinking_tool(
|
|
@@ -20,6 +21,19 @@ def register_thinking_tool(
|
|
|
20
21
|
return [thinking_tool]
|
|
21
22
|
|
|
22
23
|
|
|
24
|
+
def register_critic_tool(
|
|
25
|
+
mcp_server: FastMCP,
|
|
26
|
+
) -> list[BaseTool]:
|
|
27
|
+
"""Register critic tool with the MCP server.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
mcp_server: The FastMCP server instance
|
|
31
|
+
"""
|
|
32
|
+
critic_tool = CriticTool()
|
|
33
|
+
ToolRegistry.register_tool(mcp_server, critic_tool)
|
|
34
|
+
return [critic_tool]
|
|
35
|
+
|
|
36
|
+
|
|
23
37
|
def register_batch_tool(mcp_server: FastMCP, tools: dict[str, BaseTool]) -> None:
|
|
24
38
|
"""Register batch tool with the MCP server.
|
|
25
39
|
|
hanzo_mcp/tools/common/base.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
"""Base classes for Hanzo
|
|
1
|
+
"""Base classes for Hanzo AI tools.
|
|
2
2
|
|
|
3
3
|
This module provides abstract base classes that define interfaces and common functionality
|
|
4
|
-
for all tools used in Hanzo
|
|
4
|
+
for all tools used in Hanzo AI. These abstractions help ensure consistent tool
|
|
5
5
|
behavior and provide a foundation for tool registration and management.
|
|
6
6
|
"""
|
|
7
7
|
|
|
@@ -62,7 +62,7 @@ def handle_connection_errors(
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
class BaseTool(ABC):
|
|
65
|
-
"""Abstract base class for all Hanzo
|
|
65
|
+
"""Abstract base class for all Hanzo AI tools.
|
|
66
66
|
|
|
67
67
|
This class defines the core interface that all tools must implement, ensuring
|
|
68
68
|
consistency in how tools are registered, documented, and called.
|
|
@@ -156,7 +156,7 @@ class FileSystemTool(BaseTool, ABC):
|
|
|
156
156
|
|
|
157
157
|
@final
|
|
158
158
|
class ToolRegistry:
|
|
159
|
-
"""Registry for Hanzo
|
|
159
|
+
"""Registry for Hanzo AI tools.
|
|
160
160
|
|
|
161
161
|
Provides functionality for registering tool implementations with an MCP server,
|
|
162
162
|
handling the conversion between tool classes and MCP tool functions.
|
|
@@ -34,7 +34,7 @@ class ConfigToolParams(TypedDict, total=False):
|
|
|
34
34
|
|
|
35
35
|
@final
|
|
36
36
|
class ConfigTool(BaseTool):
|
|
37
|
-
"""Tool for managing Hanzo
|
|
37
|
+
"""Tool for managing Hanzo AI configuration dynamically."""
|
|
38
38
|
|
|
39
39
|
def __init__(self, permission_manager: PermissionManager):
|
|
40
40
|
"""Initialize the configuration tool.
|
|
@@ -52,7 +52,7 @@ class ConfigTool(BaseTool):
|
|
|
52
52
|
@property
|
|
53
53
|
def description(self) -> str:
|
|
54
54
|
"""Get the tool description."""
|
|
55
|
-
return """Dynamically manage Hanzo
|
|
55
|
+
return """Dynamically manage Hanzo AI configuration settings through conversation.
|
|
56
56
|
|
|
57
57
|
Can get/set global settings, project-specific settings, manage MCP servers, configure tools,
|
|
58
58
|
and handle project workflows. Supports dot-notation for nested settings like 'agent.enabled'.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""Enhanced Context for Hanzo
|
|
1
|
+
"""Enhanced Context for Hanzo AI tools.
|
|
2
2
|
|
|
3
3
|
This module provides an enhanced Context class that wraps the MCP Context
|
|
4
4
|
and adds additional functionality specific to Hanzo tools.
|
|
@@ -17,7 +17,7 @@ from mcp.server.lowlevel.helper_types import ReadResourceContents
|
|
|
17
17
|
|
|
18
18
|
@final
|
|
19
19
|
class ToolContext:
|
|
20
|
-
"""Enhanced context for Hanzo
|
|
20
|
+
"""Enhanced context for Hanzo AI tools.
|
|
21
21
|
|
|
22
22
|
This class wraps the MCP Context and adds additional functionality
|
|
23
23
|
for tracking tool execution, progress reporting, and resource access.
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Context handling fix for MCP tools.
|
|
2
|
+
|
|
3
|
+
This module provides backward compatibility by re-exporting the
|
|
4
|
+
context normalization utilities from the decorators module.
|
|
5
|
+
|
|
6
|
+
DEPRECATED: Use hanzo_mcp.tools.common.decorators directly.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Re-export for backward compatibility
|
|
10
|
+
from hanzo_mcp.tools.common.decorators import (
|
|
11
|
+
MockContext,
|
|
12
|
+
with_context_normalization,
|
|
13
|
+
_is_valid_context as is_valid_context,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
# Backward compatibility function
|
|
17
|
+
def normalize_context(ctx):
|
|
18
|
+
"""Normalize context - backward compatibility wrapper.
|
|
19
|
+
|
|
20
|
+
DEPRECATED: Use decorators.with_context_normalization instead.
|
|
21
|
+
"""
|
|
22
|
+
if is_valid_context(ctx):
|
|
23
|
+
return ctx
|
|
24
|
+
return MockContext()
|
|
25
|
+
|
|
26
|
+
__all__ = ['MockContext', 'normalize_context', 'with_context_normalization']
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
"""Critic tool implementation.
|
|
2
|
+
|
|
3
|
+
This module provides the CriticTool for Claude to engage in critical analysis and code review.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Annotated, TypedDict, Unpack, final, override
|
|
7
|
+
|
|
8
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
9
|
+
from mcp.server import FastMCP
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
13
|
+
from hanzo_mcp.tools.common.context import create_tool_context
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
Analysis = Annotated[
|
|
17
|
+
str,
|
|
18
|
+
Field(
|
|
19
|
+
description="The critical analysis to perform - code review, error detection, or improvement suggestions",
|
|
20
|
+
min_length=1,
|
|
21
|
+
),
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class CriticToolParams(TypedDict):
|
|
26
|
+
"""Parameters for the CriticTool.
|
|
27
|
+
|
|
28
|
+
Attributes:
|
|
29
|
+
analysis: The critical analysis to perform
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
analysis: Analysis
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@final
|
|
36
|
+
class CriticTool(BaseTool):
|
|
37
|
+
"""Tool for Claude to engage in critical analysis and play devil's advocate."""
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
@override
|
|
41
|
+
def name(self) -> str:
|
|
42
|
+
"""Get the tool name.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Tool name
|
|
46
|
+
"""
|
|
47
|
+
return "critic"
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
@override
|
|
51
|
+
def description(self) -> str:
|
|
52
|
+
"""Get the tool description.
|
|
53
|
+
|
|
54
|
+
Returns:
|
|
55
|
+
Tool description
|
|
56
|
+
"""
|
|
57
|
+
return """Use this tool to perform critical analysis, play devil's advocate, and ensure high standards.
|
|
58
|
+
This tool forces a critical thinking mode that reviews all code for errors, improvements, and edge cases.
|
|
59
|
+
It ensures tests are run, tests pass, and maintains high quality standards.
|
|
60
|
+
|
|
61
|
+
This is your inner critic that:
|
|
62
|
+
- Always questions assumptions
|
|
63
|
+
- Looks for potential bugs and edge cases
|
|
64
|
+
- Ensures proper error handling
|
|
65
|
+
- Verifies test coverage
|
|
66
|
+
- Checks for performance issues
|
|
67
|
+
- Reviews security implications
|
|
68
|
+
- Suggests improvements and refactoring
|
|
69
|
+
- Ensures code follows best practices
|
|
70
|
+
- Questions design decisions
|
|
71
|
+
- Looks for missing documentation
|
|
72
|
+
|
|
73
|
+
Common use cases:
|
|
74
|
+
1. Before finalizing any code changes - review for bugs, edge cases, and improvements
|
|
75
|
+
2. After implementing a feature - critically analyze if it truly solves the problem
|
|
76
|
+
3. When tests pass too easily - question if tests are comprehensive enough
|
|
77
|
+
4. Before marking a task complete - ensure all quality standards are met
|
|
78
|
+
5. When something seems too simple - look for hidden complexity or missing requirements
|
|
79
|
+
6. After fixing a bug - analyze if the fix addresses root cause or just symptoms
|
|
80
|
+
|
|
81
|
+
<critic_example>
|
|
82
|
+
Code Review Analysis:
|
|
83
|
+
- Implementation Issues:
|
|
84
|
+
* No error handling for network failures in API calls
|
|
85
|
+
* Missing validation for user input boundaries
|
|
86
|
+
* Race condition possible in concurrent updates
|
|
87
|
+
* Memory leak potential in event listener registration
|
|
88
|
+
|
|
89
|
+
- Test Coverage Gaps:
|
|
90
|
+
* No tests for error scenarios
|
|
91
|
+
* Missing edge case: empty array input
|
|
92
|
+
* No performance benchmarks for large datasets
|
|
93
|
+
* Integration tests don't cover authentication failures
|
|
94
|
+
|
|
95
|
+
- Security Concerns:
|
|
96
|
+
* SQL injection vulnerability in query construction
|
|
97
|
+
* Missing rate limiting on public endpoints
|
|
98
|
+
* Sensitive data logged in debug mode
|
|
99
|
+
|
|
100
|
+
- Performance Issues:
|
|
101
|
+
* O(n²) algorithm where O(n log n) is possible
|
|
102
|
+
* Database queries in a loop (N+1 problem)
|
|
103
|
+
* No caching for expensive computations
|
|
104
|
+
|
|
105
|
+
- Code Quality:
|
|
106
|
+
* Functions too long and doing multiple things
|
|
107
|
+
* Inconsistent naming conventions
|
|
108
|
+
* Missing type annotations
|
|
109
|
+
* No documentation for complex algorithms
|
|
110
|
+
|
|
111
|
+
- Design Flaws:
|
|
112
|
+
* Tight coupling between modules
|
|
113
|
+
* Hard-coded configuration values
|
|
114
|
+
* No abstraction for external dependencies
|
|
115
|
+
* Violates single responsibility principle
|
|
116
|
+
|
|
117
|
+
Recommendations:
|
|
118
|
+
1. Add comprehensive error handling with retry logic
|
|
119
|
+
2. Implement input validation with clear error messages
|
|
120
|
+
3. Use database transactions to prevent race conditions
|
|
121
|
+
4. Add memory cleanup in component unmount
|
|
122
|
+
5. Parameterize SQL queries to prevent injection
|
|
123
|
+
6. Implement rate limiting middleware
|
|
124
|
+
7. Use environment variables for sensitive config
|
|
125
|
+
8. Refactor algorithm to use sorting approach
|
|
126
|
+
9. Batch database queries
|
|
127
|
+
10. Add memoization for expensive calculations
|
|
128
|
+
</critic_example>"""
|
|
129
|
+
|
|
130
|
+
def __init__(self) -> None:
|
|
131
|
+
"""Initialize the critic tool."""
|
|
132
|
+
pass
|
|
133
|
+
|
|
134
|
+
@override
|
|
135
|
+
async def call(
|
|
136
|
+
self,
|
|
137
|
+
ctx: MCPContext,
|
|
138
|
+
**params: Unpack[CriticToolParams],
|
|
139
|
+
) -> str:
|
|
140
|
+
"""Execute the tool with the given parameters.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
ctx: MCP context
|
|
144
|
+
**params: Tool parameters
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Tool result
|
|
148
|
+
"""
|
|
149
|
+
tool_ctx = create_tool_context(ctx)
|
|
150
|
+
tool_ctx.set_tool_info(self.name)
|
|
151
|
+
|
|
152
|
+
# Extract parameters
|
|
153
|
+
analysis = params.get("analysis")
|
|
154
|
+
|
|
155
|
+
# Validate required analysis parameter
|
|
156
|
+
if not analysis:
|
|
157
|
+
await tool_ctx.error(
|
|
158
|
+
"Parameter 'analysis' is required but was None or empty"
|
|
159
|
+
)
|
|
160
|
+
return "Error: Parameter 'analysis' is required but was None or empty"
|
|
161
|
+
|
|
162
|
+
if analysis.strip() == "":
|
|
163
|
+
await tool_ctx.error("Parameter 'analysis' cannot be empty")
|
|
164
|
+
return "Error: Parameter 'analysis' cannot be empty"
|
|
165
|
+
|
|
166
|
+
# Log the critical analysis
|
|
167
|
+
await tool_ctx.info("Critical analysis recorded")
|
|
168
|
+
|
|
169
|
+
# Return confirmation with reminder to act on the analysis
|
|
170
|
+
return """Critical analysis complete. Remember to:
|
|
171
|
+
1. Address all identified issues before proceeding
|
|
172
|
+
2. Run comprehensive tests to verify fixes
|
|
173
|
+
3. Ensure all tests pass with proper coverage
|
|
174
|
+
4. Document any design decisions or trade-offs
|
|
175
|
+
5. Consider the analysis points in your implementation
|
|
176
|
+
|
|
177
|
+
Continue with improvements based on this critical review."""
|
|
178
|
+
|
|
179
|
+
@override
|
|
180
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
181
|
+
"""Register this critic tool with the MCP server.
|
|
182
|
+
|
|
183
|
+
Creates a wrapper function with explicitly defined parameters that match
|
|
184
|
+
the tool's parameter schema and registers it with the MCP server.
|
|
185
|
+
|
|
186
|
+
Args:
|
|
187
|
+
mcp_server: The FastMCP server instance
|
|
188
|
+
"""
|
|
189
|
+
tool_self = self # Create a reference to self for use in the closure
|
|
190
|
+
|
|
191
|
+
@mcp_server.tool(name=self.name, description=self.description)
|
|
192
|
+
async def critic(
|
|
193
|
+
analysis: Analysis,
|
|
194
|
+
ctx: MCPContext
|
|
195
|
+
) -> str:
|
|
196
|
+
return await tool_self.call(ctx, analysis=analysis)
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Decorators for MCP tools.
|
|
2
|
+
|
|
3
|
+
This module provides decorators that handle common cross-cutting concerns
|
|
4
|
+
for MCP tools, such as context normalization and error handling.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import functools
|
|
8
|
+
import inspect
|
|
9
|
+
from typing import Any, Callable, TypeVar, cast
|
|
10
|
+
from collections.abc import Awaitable, Coroutine
|
|
11
|
+
|
|
12
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
13
|
+
|
|
14
|
+
F = TypeVar('F', bound=Callable[..., Any])
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class MockContext:
|
|
18
|
+
"""Mock context for when no real context is available.
|
|
19
|
+
|
|
20
|
+
This is used when tools are called externally through the MCP protocol
|
|
21
|
+
and the Context parameter is not properly serialized.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
def __init__(self):
|
|
25
|
+
self.request_id = "external-request"
|
|
26
|
+
self.client_id = "external-client"
|
|
27
|
+
|
|
28
|
+
async def info(self, message: str) -> None:
|
|
29
|
+
"""Mock info logging - no-op for external calls."""
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
async def debug(self, message: str) -> None:
|
|
33
|
+
"""Mock debug logging - no-op for external calls."""
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
async def warning(self, message: str) -> None:
|
|
37
|
+
"""Mock warning logging - no-op for external calls."""
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
async def error(self, message: str) -> None:
|
|
41
|
+
"""Mock error logging - no-op for external calls."""
|
|
42
|
+
pass
|
|
43
|
+
|
|
44
|
+
async def report_progress(self, current: int, total: int) -> None:
|
|
45
|
+
"""Mock progress reporting - no-op for external calls."""
|
|
46
|
+
pass
|
|
47
|
+
|
|
48
|
+
async def read_resource(self, uri: str) -> Any:
|
|
49
|
+
"""Mock resource reading - returns empty result."""
|
|
50
|
+
return []
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def with_context_normalization(func: F) -> F:
|
|
54
|
+
"""Decorator that normalizes the context parameter for MCP tools.
|
|
55
|
+
|
|
56
|
+
This decorator intercepts the ctx parameter and ensures it's a valid
|
|
57
|
+
MCPContext object, even when called externally where it might be
|
|
58
|
+
passed as a string, dict, or None.
|
|
59
|
+
|
|
60
|
+
Usage:
|
|
61
|
+
@server.tool()
|
|
62
|
+
@with_context_normalization
|
|
63
|
+
async def my_tool(ctx: MCPContext, param: str) -> str:
|
|
64
|
+
# ctx is guaranteed to be a valid context object
|
|
65
|
+
await ctx.info("Processing...")
|
|
66
|
+
return "result"
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
func: The async function to decorate
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
The decorated function with context normalization
|
|
73
|
+
"""
|
|
74
|
+
@functools.wraps(func)
|
|
75
|
+
async def wrapper(*args: Any, **kwargs: Any) -> Any:
|
|
76
|
+
# Get function signature to find ctx parameter
|
|
77
|
+
sig = inspect.signature(func)
|
|
78
|
+
params = list(sig.parameters.keys())
|
|
79
|
+
|
|
80
|
+
# Handle ctx in kwargs
|
|
81
|
+
if 'ctx' in kwargs:
|
|
82
|
+
ctx_value = kwargs['ctx']
|
|
83
|
+
if not _is_valid_context(ctx_value):
|
|
84
|
+
kwargs['ctx'] = MockContext()
|
|
85
|
+
|
|
86
|
+
# Handle ctx in args (positional)
|
|
87
|
+
elif 'ctx' in params:
|
|
88
|
+
ctx_index = params.index('ctx')
|
|
89
|
+
if ctx_index < len(args):
|
|
90
|
+
ctx_value = args[ctx_index]
|
|
91
|
+
if not _is_valid_context(ctx_value):
|
|
92
|
+
args_list = list(args)
|
|
93
|
+
args_list[ctx_index] = MockContext()
|
|
94
|
+
args = tuple(args_list)
|
|
95
|
+
|
|
96
|
+
# Call the original function
|
|
97
|
+
return await func(*args, **kwargs)
|
|
98
|
+
|
|
99
|
+
return cast(F, wrapper)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _is_valid_context(ctx: Any) -> bool:
|
|
103
|
+
"""Check if an object is a valid MCPContext.
|
|
104
|
+
|
|
105
|
+
Args:
|
|
106
|
+
ctx: The object to check
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
True if ctx is a valid context object
|
|
110
|
+
"""
|
|
111
|
+
# Check for required context methods
|
|
112
|
+
return (
|
|
113
|
+
hasattr(ctx, 'info') and
|
|
114
|
+
hasattr(ctx, 'debug') and
|
|
115
|
+
hasattr(ctx, 'warning') and
|
|
116
|
+
hasattr(ctx, 'error') and
|
|
117
|
+
hasattr(ctx, 'report_progress') and
|
|
118
|
+
# Ensure they're callable
|
|
119
|
+
callable(getattr(ctx, 'info', None)) and
|
|
120
|
+
callable(getattr(ctx, 'debug', None))
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def mcp_tool(
|
|
125
|
+
server: Any,
|
|
126
|
+
name: str | None = None,
|
|
127
|
+
description: str | None = None
|
|
128
|
+
) -> Callable[[F], F]:
|
|
129
|
+
"""Enhanced MCP tool decorator that includes context normalization.
|
|
130
|
+
|
|
131
|
+
This decorator combines the standard MCP tool registration with
|
|
132
|
+
automatic context normalization, providing a single-point solution
|
|
133
|
+
for all tools.
|
|
134
|
+
|
|
135
|
+
Usage:
|
|
136
|
+
@mcp_tool(server, name="my_tool", description="Does something")
|
|
137
|
+
async def my_tool(ctx: MCPContext, param: str) -> str:
|
|
138
|
+
await ctx.info("Processing...")
|
|
139
|
+
return "result"
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
server: The MCP server instance
|
|
143
|
+
name: Optional tool name (defaults to function name)
|
|
144
|
+
description: Optional tool description
|
|
145
|
+
|
|
146
|
+
Returns:
|
|
147
|
+
Decorator function
|
|
148
|
+
"""
|
|
149
|
+
def decorator(func: F) -> F:
|
|
150
|
+
# Apply context normalization first
|
|
151
|
+
normalized_func = with_context_normalization(func)
|
|
152
|
+
|
|
153
|
+
# Then apply the server's tool decorator
|
|
154
|
+
if hasattr(server, 'tool'):
|
|
155
|
+
# Use the server's tool decorator
|
|
156
|
+
server_decorator = server.tool(name=name, description=description)
|
|
157
|
+
return server_decorator(normalized_func)
|
|
158
|
+
else:
|
|
159
|
+
# Fallback if server doesn't have tool method
|
|
160
|
+
return normalized_func
|
|
161
|
+
|
|
162
|
+
return decorator
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def create_tool_handler(server: Any, tool: Any) -> Callable[[], None]:
|
|
166
|
+
"""Create a standardized tool registration handler.
|
|
167
|
+
|
|
168
|
+
This function creates a registration method that automatically applies
|
|
169
|
+
context normalization to any tool handler registered with the server.
|
|
170
|
+
|
|
171
|
+
Usage:
|
|
172
|
+
class MyTool(BaseTool):
|
|
173
|
+
def register(self, mcp_server):
|
|
174
|
+
register = create_tool_handler(mcp_server, self)
|
|
175
|
+
register()
|
|
176
|
+
|
|
177
|
+
Args:
|
|
178
|
+
server: The MCP server instance
|
|
179
|
+
tool: The tool instance with name, description, and handler
|
|
180
|
+
|
|
181
|
+
Returns:
|
|
182
|
+
A function that registers the tool with context normalization
|
|
183
|
+
"""
|
|
184
|
+
def register_with_normalization():
|
|
185
|
+
# Get the original register method
|
|
186
|
+
original_register = tool.__class__.register
|
|
187
|
+
|
|
188
|
+
# Temporarily replace server.tool to wrap with normalization
|
|
189
|
+
original_tool_decorator = server.tool
|
|
190
|
+
|
|
191
|
+
def normalized_tool_decorator(name=None, description=None):
|
|
192
|
+
def decorator(func):
|
|
193
|
+
# Apply context normalization
|
|
194
|
+
normalized = with_context_normalization(func)
|
|
195
|
+
# Apply original decorator
|
|
196
|
+
return original_tool_decorator(name=name, description=description)(normalized)
|
|
197
|
+
return decorator
|
|
198
|
+
|
|
199
|
+
# Monkey-patch temporarily
|
|
200
|
+
server.tool = normalized_tool_decorator
|
|
201
|
+
try:
|
|
202
|
+
# Call original register
|
|
203
|
+
original_register(tool, server)
|
|
204
|
+
finally:
|
|
205
|
+
# Restore original
|
|
206
|
+
server.tool = original_tool_decorator
|
|
207
|
+
|
|
208
|
+
return register_with_normalization
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Enhanced base classes for MCP tools with automatic context handling.
|
|
2
|
+
|
|
3
|
+
This module provides enhanced base classes that automatically handle
|
|
4
|
+
context normalization and other cross-cutting concerns, ensuring
|
|
5
|
+
consistent behavior across all tools.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from abc import ABC, abstractmethod
|
|
9
|
+
from typing import Any, get_type_hints, get_args, get_origin
|
|
10
|
+
import inspect
|
|
11
|
+
|
|
12
|
+
from mcp.server import FastMCP
|
|
13
|
+
from mcp.server.fastmcp import Context as MCPContext
|
|
14
|
+
|
|
15
|
+
from hanzo_mcp.tools.common.base import BaseTool
|
|
16
|
+
from hanzo_mcp.tools.common.decorators import with_context_normalization
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class EnhancedBaseTool(BaseTool, ABC):
|
|
20
|
+
"""Enhanced base class for MCP tools with automatic context normalization.
|
|
21
|
+
|
|
22
|
+
This base class automatically wraps the tool registration to include
|
|
23
|
+
context normalization, ensuring that all tools handle external calls
|
|
24
|
+
properly without requiring manual decoration or copy-pasted code.
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
28
|
+
"""Register this tool with automatic context normalization.
|
|
29
|
+
|
|
30
|
+
This method automatically applies context normalization to the tool
|
|
31
|
+
handler, ensuring it works properly when called externally.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
mcp_server: The FastMCP server instance
|
|
35
|
+
"""
|
|
36
|
+
# Get the tool method from the subclass
|
|
37
|
+
tool_method = self._create_tool_handler()
|
|
38
|
+
|
|
39
|
+
# Apply context normalization decorator
|
|
40
|
+
normalized_method = with_context_normalization(tool_method)
|
|
41
|
+
|
|
42
|
+
# Register with the server
|
|
43
|
+
mcp_server.tool(
|
|
44
|
+
name=self.name,
|
|
45
|
+
description=self.description
|
|
46
|
+
)(normalized_method)
|
|
47
|
+
|
|
48
|
+
@abstractmethod
|
|
49
|
+
def _create_tool_handler(self) -> Any:
|
|
50
|
+
"""Create the tool handler function.
|
|
51
|
+
|
|
52
|
+
Subclasses must implement this to return an async function
|
|
53
|
+
that will be registered as the tool handler.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
An async function that handles tool calls
|
|
57
|
+
"""
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AutoRegisterTool(BaseTool, ABC):
|
|
62
|
+
"""Base class that automatically generates tool handlers from the call method.
|
|
63
|
+
|
|
64
|
+
This base class inspects the call method signature and automatically
|
|
65
|
+
creates a properly typed tool handler with context normalization.
|
|
66
|
+
"""
|
|
67
|
+
|
|
68
|
+
def register(self, mcp_server: FastMCP) -> None:
|
|
69
|
+
"""Register this tool with automatic handler generation.
|
|
70
|
+
|
|
71
|
+
This method inspects the call method signature and automatically
|
|
72
|
+
creates a tool handler with the correct parameters and types.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
mcp_server: The FastMCP server instance
|
|
76
|
+
"""
|
|
77
|
+
# Get the call method signature
|
|
78
|
+
call_method = self.call
|
|
79
|
+
sig = inspect.signature(call_method)
|
|
80
|
+
|
|
81
|
+
# Get type hints for proper typing
|
|
82
|
+
hints = get_type_hints(call_method)
|
|
83
|
+
|
|
84
|
+
# Create a dynamic handler function
|
|
85
|
+
tool_self = self
|
|
86
|
+
|
|
87
|
+
# Build the handler dynamically based on the call signature
|
|
88
|
+
params = list(sig.parameters.items())
|
|
89
|
+
|
|
90
|
+
# Skip 'self' and 'ctx' parameters
|
|
91
|
+
tool_params = [(name, param) for name, param in params
|
|
92
|
+
if name not in ('self', 'ctx')]
|
|
93
|
+
|
|
94
|
+
# Create the handler function dynamically
|
|
95
|
+
async def handler(ctx: MCPContext, **kwargs: Any) -> Any:
|
|
96
|
+
# Call the tool's call method with the context and parameters
|
|
97
|
+
return await tool_self.call(ctx, **kwargs)
|
|
98
|
+
|
|
99
|
+
# Apply context normalization
|
|
100
|
+
normalized_handler = with_context_normalization(handler)
|
|
101
|
+
|
|
102
|
+
# Register with the server
|
|
103
|
+
mcp_server.tool(
|
|
104
|
+
name=self.name,
|
|
105
|
+
description=self.description
|
|
106
|
+
)(normalized_handler)
|