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.
- hanzo_mcp/__init__.py +2 -2
- hanzo_mcp/analytics/__init__.py +5 -0
- hanzo_mcp/analytics/posthog_analytics.py +364 -0
- hanzo_mcp/cli.py +5 -5
- hanzo_mcp/cli_enhanced.py +7 -7
- hanzo_mcp/cli_plugin.py +91 -0
- hanzo_mcp/config/__init__.py +1 -1
- hanzo_mcp/config/settings.py +70 -7
- hanzo_mcp/config/tool_config.py +20 -6
- hanzo_mcp/dev_server.py +3 -3
- hanzo_mcp/prompts/project_system.py +1 -1
- hanzo_mcp/server.py +40 -3
- hanzo_mcp/server_enhanced.py +69 -0
- hanzo_mcp/tools/__init__.py +140 -31
- hanzo_mcp/tools/agent/__init__.py +85 -4
- hanzo_mcp/tools/agent/agent_tool.py +104 -6
- hanzo_mcp/tools/agent/agent_tool_v2.py +459 -0
- hanzo_mcp/tools/agent/clarification_protocol.py +220 -0
- hanzo_mcp/tools/agent/clarification_tool.py +68 -0
- hanzo_mcp/tools/agent/claude_cli_tool.py +125 -0
- hanzo_mcp/tools/agent/claude_desktop_auth.py +508 -0
- hanzo_mcp/tools/agent/cli_agent_base.py +191 -0
- hanzo_mcp/tools/agent/code_auth.py +436 -0
- hanzo_mcp/tools/agent/code_auth_tool.py +194 -0
- hanzo_mcp/tools/agent/codex_cli_tool.py +123 -0
- hanzo_mcp/tools/agent/critic_tool.py +376 -0
- hanzo_mcp/tools/agent/gemini_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/grok_cli_tool.py +128 -0
- hanzo_mcp/tools/agent/iching_tool.py +380 -0
- hanzo_mcp/tools/agent/network_tool.py +273 -0
- hanzo_mcp/tools/agent/prompt.py +62 -20
- hanzo_mcp/tools/agent/review_tool.py +433 -0
- hanzo_mcp/tools/agent/swarm_tool.py +535 -0
- hanzo_mcp/tools/agent/swarm_tool_v2.py +594 -0
- hanzo_mcp/tools/common/__init__.py +15 -1
- hanzo_mcp/tools/common/base.py +5 -4
- hanzo_mcp/tools/common/batch_tool.py +103 -11
- 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/fastmcp_pagination.py +369 -0
- hanzo_mcp/tools/common/forgiving_edit.py +243 -0
- hanzo_mcp/tools/common/mode.py +116 -0
- hanzo_mcp/tools/common/mode_loader.py +105 -0
- hanzo_mcp/tools/common/paginated_base.py +230 -0
- hanzo_mcp/tools/common/paginated_response.py +307 -0
- hanzo_mcp/tools/common/pagination.py +226 -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 +4 -1
- hanzo_mcp/tools/common/truncate.py +101 -0
- 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 +48 -14
- hanzo_mcp/tools/filesystem/ast_multi_edit.py +562 -0
- hanzo_mcp/tools/filesystem/batch_search.py +3 -3
- hanzo_mcp/tools/filesystem/diff.py +2 -2
- hanzo_mcp/tools/filesystem/directory_tree_paginated.py +338 -0
- 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/lsp/__init__.py +5 -0
- hanzo_mcp/tools/lsp/lsp_tool.py +512 -0
- hanzo_mcp/tools/mcp/__init__.py +2 -2
- hanzo_mcp/tools/mcp/{mcp_unified.py → mcp_tool.py} +3 -3
- hanzo_mcp/tools/memory/__init__.py +76 -0
- hanzo_mcp/tools/memory/knowledge_tools.py +518 -0
- hanzo_mcp/tools/memory/memory_tools.py +456 -0
- hanzo_mcp/tools/search/__init__.py +6 -0
- hanzo_mcp/tools/search/find_tool.py +581 -0
- hanzo_mcp/tools/search/unified_search.py +953 -0
- hanzo_mcp/tools/shell/__init__.py +11 -6
- hanzo_mcp/tools/shell/auto_background.py +203 -0
- hanzo_mcp/tools/shell/base_process.py +57 -29
- hanzo_mcp/tools/shell/bash_session_executor.py +1 -1
- hanzo_mcp/tools/shell/{bash_unified.py → bash_tool.py} +18 -34
- hanzo_mcp/tools/shell/command_executor.py +2 -2
- hanzo_mcp/tools/shell/{npx_unified.py → npx_tool.py} +16 -33
- 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/streaming_command.py +594 -0
- 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} +16 -33
- 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/types.py +23 -0
- hanzo_mcp-0.7.0.dist-info/METADATA +516 -0
- hanzo_mcp-0.7.0.dist-info/RECORD +180 -0
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.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/RECORD +0 -135
- hanzo_mcp-0.6.12.dist-info/licenses/LICENSE +0 -21
- {hanzo_mcp-0.6.12.dist-info → hanzo_mcp-0.7.0.dist-info}/WHEEL +0 -0
- {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
|
|
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
|
-
#
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
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
|
-
|
|
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
|
|
300
|
-
formatted_parts.append(f"```\n{
|
|
390
|
+
if "\n" in result_content:
|
|
391
|
+
formatted_parts.append(f"```\n{result_content}\n```")
|
|
301
392
|
else:
|
|
302
|
-
formatted_parts.append(
|
|
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
|
|
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
|