hanzo-mcp 0.6.12__py3-none-any.whl → 0.7.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 (117) hide show
  1. hanzo_mcp/__init__.py +2 -2
  2. hanzo_mcp/analytics/__init__.py +5 -0
  3. hanzo_mcp/analytics/posthog_analytics.py +364 -0
  4. hanzo_mcp/cli.py +5 -5
  5. hanzo_mcp/cli_enhanced.py +7 -7
  6. hanzo_mcp/cli_plugin.py +91 -0
  7. hanzo_mcp/config/__init__.py +1 -1
  8. hanzo_mcp/config/settings.py +70 -7
  9. hanzo_mcp/config/tool_config.py +20 -6
  10. hanzo_mcp/dev_server.py +3 -3
  11. hanzo_mcp/prompts/project_system.py +1 -1
  12. hanzo_mcp/server.py +40 -3
  13. hanzo_mcp/server_enhanced.py +69 -0
  14. hanzo_mcp/tools/__init__.py +140 -31
  15. hanzo_mcp/tools/agent/__init__.py +85 -4
  16. hanzo_mcp/tools/agent/agent_tool.py +104 -6
  17. hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
  18. hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
  19. hanzo_mcp/tools/agent/clarification_tool.py +68 -0
  20. hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
  21. hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
  22. hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
  23. hanzo_mcp/tools/agent/code_auth.py +436 -0
  24. hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
  25. hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
  26. hanzo_mcp/tools/agent/critic_tool.py +376 -0
  27. hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
  28. hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
  29. hanzo_mcp/tools/agent/iching_tool.py +380 -0
  30. hanzo_mcp/tools/agent/network_tool.py +273 -0
  31. hanzo_mcp/tools/agent/prompt.py +62 -20
  32. hanzo_mcp/tools/agent/review_tool.py +433 -0
  33. hanzo_mcp/tools/agent/swarm_tool.py +535 -0
  34. hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
  35. hanzo_mcp/tools/common/__init__.py +15 -1
  36. hanzo_mcp/tools/common/base.py +5 -4
  37. hanzo_mcp/tools/common/batch_tool.py +103 -11
  38. hanzo_mcp/tools/common/config_tool.py +2 -2
  39. hanzo_mcp/tools/common/context.py +2 -2
  40. hanzo_mcp/tools/common/context_fix.py +26 -0
  41. hanzo_mcp/tools/common/critic_tool.py +196 -0
  42. hanzo_mcp/tools/common/decorators.py +208 -0
  43. hanzo_mcp/tools/common/enhanced_base.py +106 -0
  44. hanzo_mcp/tools/common/fastmcp_pagination.py +369 -0
  45. hanzo_mcp/tools/common/forgiving_edit.py +243 -0
  46. hanzo_mcp/tools/common/mode.py +116 -0
  47. hanzo_mcp/tools/common/mode_loader.py +105 -0
  48. hanzo_mcp/tools/common/paginated_base.py +230 -0
  49. hanzo_mcp/tools/common/paginated_response.py +307 -0
  50. hanzo_mcp/tools/common/pagination.py +226 -0
  51. hanzo_mcp/tools/common/permissions.py +1 -1
  52. hanzo_mcp/tools/common/personality.py +936 -0
  53. hanzo_mcp/tools/common/plugin_loader.py +287 -0
  54. hanzo_mcp/tools/common/stats.py +4 -4
  55. hanzo_mcp/tools/common/tool_list.py +4 -1
  56. hanzo_mcp/tools/common/truncate.py +101 -0
  57. hanzo_mcp/tools/common/validation.py +1 -1
  58. hanzo_mcp/tools/config/__init__.py +3 -1
  59. hanzo_mcp/tools/config/config_tool.py +1 -1
  60. hanzo_mcp/tools/config/mode_tool.py +209 -0
  61. hanzo_mcp/tools/database/__init__.py +1 -1
  62. hanzo_mcp/tools/editor/__init__.py +1 -1
  63. hanzo_mcp/tools/filesystem/__init__.py +48 -14
  64. hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
  65. hanzo_mcp/tools/filesystem/batch_search.py +3 -3
  66. hanzo_mcp/tools/filesystem/diff.py +2 -2
  67. hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
  68. hanzo_mcp/tools/filesystem/rules_tool.py +235 -0
  69. hanzo_mcp/tools/filesystem/{unified_search.py → search_tool.py} +12 -12
  70. hanzo_mcp/tools/filesystem/{symbols_unified.py → symbols_tool.py} +104 -5
  71. hanzo_mcp/tools/filesystem/watch.py +3 -2
  72. hanzo_mcp/tools/jupyter/__init__.py +2 -2
  73. hanzo_mcp/tools/jupyter/jupyter.py +1 -1
  74. hanzo_mcp/tools/llm/__init__.py +3 -3
  75. hanzo_mcp/tools/llm/llm_tool.py +648 -143
  76. hanzo_mcp/tools/lsp/__init__.py +5 -0
  77. hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
  78. hanzo_mcp/tools/mcp/__init__.py +2 -2
  79. hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
  80. hanzo_mcp/tools/memory/__init__.py +76 -0
  81. hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
  82. hanzo_mcp/tools/memory/memory_tools.py +456 -0
  83. hanzo_mcp/tools/search/__init__.py +6 -0
  84. hanzo_mcp/tools/search/find_tool.py +581 -0
  85. hanzo_mcp/tools/search/unified_search.py +953 -0
  86. hanzo_mcp/tools/shell/__init__.py +11 -6
  87. hanzo_mcp/tools/shell/auto_background.py +203 -0
  88. hanzo_mcp/tools/shell/base_process.py +57 -29
  89. hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
  90. hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +18 -34
  91. hanzo_mcp/tools/shell/command_executor.py +2 -2
  92. hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +16 -33
  93. hanzo_mcp/tools/shell/open.py +2 -2
  94. hanzo_mcp/tools/shell/{process_unified.py → process_tool.py} +1 -1
  95. hanzo_mcp/tools/shell/run_command_windows.py +1 -1
  96. hanzo_mcp/tools/shell/streaming_command.py +594 -0
  97. hanzo_mcp/tools/shell/uvx.py +47 -2
  98. hanzo_mcp/tools/shell/uvx_background.py +47 -2
  99. hanzo_mcp/tools/shell/{uvx_unified.py → uvx_tool.py} +16 -33
  100. hanzo_mcp/tools/todo/__init__.py +14 -19
  101. hanzo_mcp/tools/todo/todo.py +22 -1
  102. hanzo_mcp/tools/vector/__init__.py +1 -1
  103. hanzo_mcp/tools/vector/infinity_store.py +2 -2
  104. hanzo_mcp/tools/vector/project_manager.py +1 -1
  105. hanzo_mcp/types.py +23 -0
  106. hanzo_mcp-0.7.0.dist-info/METADATA +516 -0
  107. hanzo_mcp-0.7.0.dist-info/RECORD +180 -0
  108. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/entry_points.txt +1 -0
  109. hanzo_mcp/tools/common/palette.py +0 -344
  110. hanzo_mcp/tools/common/palette_loader.py +0 -108
  111. hanzo_mcp/tools/config/palette_tool.py +0 -179
  112. hanzo_mcp/tools/llm/llm_unified.py +0 -851
  113. hanzo_mcp-0.6.12.dist-info/METADATA +0 -339
  114. hanzo_mcp-0.6.12.dist-info/RECORD +0 -135
  115. hanzo_mcp-0.6.12.dist-info/licenses/LICENSE +0 -21
  116. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
  117. {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/top_level.txt +0 -0
@@ -1,4 +1,4 @@
1
- """Batch tool implementation for Hanzo MCP.
1
+ """Batch tool implementation for Hanzo AI.
2
2
 
3
3
  This module provides the BatchTool that allows for executing multiple tools in
4
4
  parallel or serial depending on their characteristics.
@@ -13,6 +13,12 @@ from pydantic import Field
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
17
+ from hanzo_mcp.tools.common.fastmcp_pagination import (
18
+ create_paginated_response,
19
+ CursorData,
20
+ TokenAwarePaginator
21
+ )
16
22
 
17
23
 
18
24
  class InvocationItem(TypedDict):
@@ -54,6 +60,14 @@ Invocations = Annotated[
54
60
  ),
55
61
  ]
56
62
 
63
+ Cursor = Annotated[
64
+ str | None,
65
+ Field(
66
+ description="Pagination cursor to continue from previous batch results",
67
+ default=None,
68
+ ),
69
+ ]
70
+
57
71
 
58
72
  class BatchToolParams(TypedDict):
59
73
  """Parameters for the BatchTool.
@@ -61,10 +75,12 @@ class BatchToolParams(TypedDict):
61
75
  Attributes:
62
76
  description: A short (3-5 word) description of the batch operation
63
77
  invocations: The list of tool invocations to execute (required -- you MUST provide at least one tool invocation)
78
+ cursor: Optional pagination cursor
64
79
  """
65
80
 
66
81
  description: Description
67
82
  invocations: Invocations
83
+ cursor: Cursor
68
84
 
69
85
 
70
86
  @final
@@ -271,13 +287,78 @@ Not available: think,write,edit,multi_edit,notebook_edit
271
287
  {"invocation": invocation, "result": f"Error: {error_message}"}
272
288
  )
273
289
 
274
- # Format the results
275
- formatted_results = self._format_results(results)
276
- await tool_ctx.info(
277
- f"Batch operation '{description}' completed with {len(results)} results"
290
+ # Extract cursor if provided
291
+ cursor = params.get("cursor")
292
+ cursor_offset = 0
293
+
294
+ # If cursor provided, we need to resume from where we left off
295
+ if cursor:
296
+ cursor_data = CursorData.from_cursor(cursor)
297
+ if cursor_data and cursor_data.offset < len(results):
298
+ # Skip already returned results
299
+ cursor_offset = cursor_data.offset
300
+ results = results[cursor_offset:]
301
+
302
+ # Format results
303
+ formatted_results = []
304
+ for i, result in enumerate(results):
305
+ invocation = result["invocation"]
306
+ 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
+
313
+ # Create paginated response with token awareness
314
+ paginated_response = create_paginated_response(
315
+ formatted_results,
316
+ cursor=cursor,
317
+ use_token_limit=True
278
318
  )
279
-
280
- return formatted_results
319
+
320
+ # Convert paginated response to string format for MCP
321
+ if isinstance(paginated_response, dict) and "items" in paginated_response:
322
+ # Format the items as a readable string
323
+ result_parts = []
324
+
325
+ # Add header
326
+ result_parts.append(f"=== Batch operation: {description} ===")
327
+ 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')}")
331
+ result_parts.append("")
332
+
333
+ # Format each result
334
+ for item in paginated_response['items']:
335
+ result_parts.append(f"### Result {item['index'] + 1}: {item['tool']}")
336
+ result_content = item['result']
337
+
338
+ # Add the result content - use multi-line code blocks for code outputs
339
+ if isinstance(result_content, str) and "\n" in result_content:
340
+ result_parts.append(f"```\n{result_content}\n```")
341
+ else:
342
+ result_parts.append(str(result_content))
343
+ result_parts.append("")
344
+
345
+ # Join all parts
346
+ formatted_output = "\n".join(result_parts)
347
+
348
+ # If there's a next cursor, we need to preserve it in the response
349
+ # 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
+ await tool_ctx.info(
354
+ f"Batch operation '{description}' completed with {len(paginated_response['items'])} results"
355
+ f"{' (more available)' if paginated_response.get('hasMore') else ''}"
356
+ )
357
+
358
+ return formatted_output
359
+ else:
360
+ # Fallback if pagination didn't work as expected
361
+ return self._format_results(results)
281
362
 
282
363
  def _format_results(self, results: list[dict[str, dict[str, Any]]]) -> str:
283
364
  """Format the results from multiple tool invocations.
@@ -295,11 +376,21 @@ Not available: think,write,edit,multi_edit,notebook_edit
295
376
 
296
377
  # Add the result header
297
378
  formatted_parts.append(f"### Result {i + 1}: {tool_name}")
379
+
380
+ # Truncate individual results if they're too large
381
+ result_content = result["result"]
382
+ if len(result_content) > 50000: # If individual result > 50k chars
383
+ result_content = truncate_response(
384
+ result_content,
385
+ 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.]"
387
+ )
388
+
298
389
  # Add the result content - use multi-line code blocks for code outputs
299
- if "\n" in result["result"]:
300
- formatted_parts.append(f"```\n{result['result']}\n```")
390
+ if "\n" in result_content:
391
+ formatted_parts.append(f"```\n{result_content}\n```")
301
392
  else:
302
- formatted_parts.append(result["result"])
393
+ formatted_parts.append(result_content)
303
394
  # Add a separator
304
395
  formatted_parts.append("")
305
396
 
@@ -321,8 +412,9 @@ Not available: think,write,edit,multi_edit,notebook_edit
321
412
  async def batch(
322
413
  description: Description,
323
414
  invocations: Invocations,
415
+ cursor: Cursor,
324
416
  ctx: MCPContext
325
417
  ) -> str:
326
418
  return await tool_self.call(
327
- ctx, description=description, invocations=invocations
419
+ ctx, description=description, invocations=invocations, cursor=cursor
328
420
  )
@@ -34,7 +34,7 @@ class ConfigToolParams(TypedDict, total=False):
34
34
 
35
35
  @final
36
36
  class ConfigTool(BaseTool):
37
- """Tool for managing Hanzo MCP configuration dynamically."""
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 MCP configuration settings through conversation.
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 MCP tools.
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 MCP tools.
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