feat: Major refactoring and improvements v2.11.0

## 🚀 Major Improvements

### Docker Environment Simplification
- **BREAKING**: Simplified Docker configuration by auto-detecting sandbox from WORKSPACE_ROOT
- Removed redundant MCP_PROJECT_ROOT requirement for Docker setups
- Updated all Docker config examples and setup scripts
- Added security validation for dangerous WORKSPACE_ROOT paths

### Security Enhancements
- **CRITICAL**: Fixed insecure PROJECT_ROOT fallback to use current directory instead of home
- Enhanced path validation with proper Docker environment detection
- Removed information disclosure in error messages
- Strengthened symlink and path traversal protection

### File Handling Optimization
- **PERFORMANCE**: Optimized read_files() to return content only (removed summary)
- Unified file reading across all tools using standardized file_utils routines
- Fixed review_changes tool to use consistent file loading patterns
- Improved token management and reduced unnecessary processing

### Tool Improvements
- **UX**: Enhanced ReviewCodeTool to require user context for targeted reviews
- Removed deprecated _get_secure_container_path function and _sanitize_filename
- Standardized file access patterns across analyze, review_changes, and other tools
- Added contextual prompting to align reviews with user expectations

### Code Quality & Testing
- Updated all tests for new function signatures and requirements
- Added comprehensive Docker path integration tests
- Achieved 100% test coverage (95 tests passing)
- Full compliance with ruff, black, and isort linting standards

### Configuration & Deployment
- Added pyproject.toml for modern Python packaging
- Streamlined Docker setup removing redundant environment variables
- Updated setup scripts across all platforms (Windows, macOS, Linux)
- Improved error handling and validation throughout

## 🔧 Technical Changes

- **Removed**: `_get_secure_container_path()`, `_sanitize_filename()`, unused SANDBOX_MODE
- **Enhanced**: Path translation, security validation, token management
- **Standardized**: File reading patterns, error handling, Docker detection
- **Updated**: All tool prompts for better context alignment

## 🛡️ Security Notes

This release significantly improves the security posture by:
- Eliminating broad filesystem access defaults
- Adding validation for Docker environment variables
- Removing information disclosure in error paths
- Strengthening path traversal and symlink protections

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Fahad
2025-06-10 09:50:05 +04:00
parent 7ea790ef88
commit 27add4d05d
34 changed files with 593 additions and 759 deletions

View File

@@ -2,7 +2,7 @@
Analyze tool - General-purpose code and file analysis
"""
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from mcp.types import TextContent
from pydantic import Field
@@ -18,17 +18,13 @@ from .models import ToolOutput
class AnalyzeRequest(ToolRequest):
"""Request model for analyze tool"""
files: List[str] = Field(
..., description="Files or directories to analyze (must be absolute paths)"
)
files: list[str] = Field(..., description="Files or directories to analyze (must be absolute paths)")
question: str = Field(..., description="What to analyze or look for")
analysis_type: Optional[str] = Field(
None,
description="Type of analysis: architecture|performance|security|quality|general",
)
output_format: Optional[str] = Field(
"detailed", description="Output format: summary|detailed|actionable"
)
output_format: Optional[str] = Field("detailed", description="Output format: summary|detailed|actionable")
class AnalyzeTool(BaseTool):
@@ -47,7 +43,7 @@ class AnalyzeTool(BaseTool):
"Always uses file paths for clean terminal output."
)
def get_input_schema(self) -> Dict[str, Any]:
def get_input_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
@@ -101,7 +97,7 @@ class AnalyzeTool(BaseTool):
def get_request_model(self):
return AnalyzeRequest
async def execute(self, arguments: Dict[str, Any]) -> List[TextContent]:
async def execute(self, arguments: dict[str, Any]) -> list[TextContent]:
"""Override execute to check question size before processing"""
# First validate request
request_model = self.get_request_model()
@@ -110,11 +106,7 @@ class AnalyzeTool(BaseTool):
# Check question size
size_check = self.check_prompt_size(request.question)
if size_check:
return [
TextContent(
type="text", text=ToolOutput(**size_check).model_dump_json()
)
]
return [TextContent(type="text", text=ToolOutput(**size_check).model_dump_json())]
# Continue with normal execution
return await super().execute(arguments)
@@ -133,7 +125,7 @@ class AnalyzeTool(BaseTool):
request.files = updated_files
# Read all files
file_content, summary = read_files(request.files)
file_content = read_files(request.files)
# Check token limits
self._validate_token_limit(file_content, "Files")
@@ -154,9 +146,7 @@ class AnalyzeTool(BaseTool):
if request.output_format == "summary":
analysis_focus.append("Provide a concise summary of key findings")
elif request.output_format == "actionable":
analysis_focus.append(
"Focus on actionable insights and specific recommendations"
)
analysis_focus.append("Focus on actionable insights and specific recommendations")
focus_instruction = "\n".join(analysis_focus) if analysis_focus else ""
@@ -185,4 +175,4 @@ Please analyze these files to answer the user's question."""
summary_text = f"Analyzed {len(request.files)} file(s)"
return f"{header}\n{summary_text}\n{'=' * 50}\n\n{response}"
return f"{header}\n{summary_text}\n{'=' * 50}\n\n{response}\n\n---\n\n**Next Steps:** Consider if this analysis reveals areas needing deeper investigation, additional context, or specific implementation details."

View File

@@ -16,7 +16,7 @@ Key responsibilities:
import json
import os
from abc import ABC, abstractmethod
from typing import Any, Dict, List, Literal, Optional
from typing import Any, Literal, Optional
from google import genai
from google.genai import types
@@ -24,7 +24,7 @@ from mcp.types import TextContent
from pydantic import BaseModel, Field
from config import MCP_PROMPT_SIZE_LIMIT
from utils.file_utils import read_file_content
from utils.file_utils import read_file_content, translate_path_for_environment
from .models import ClarificationRequest, ToolOutput
@@ -38,12 +38,8 @@ class ToolRequest(BaseModel):
these common fields.
"""
model: Optional[str] = Field(
None, description="Model to use (defaults to Gemini 2.5 Pro)"
)
temperature: Optional[float] = Field(
None, description="Temperature for response (tool-specific defaults)"
)
model: Optional[str] = Field(None, description="Model to use (defaults to Gemini 2.5 Pro)")
temperature: Optional[float] = Field(None, description="Temperature for response (tool-specific defaults)")
# Thinking mode controls how much computational budget the model uses for reasoning
# Higher values allow for more complex reasoning but increase latency and cost
thinking_mode: Optional[Literal["minimal", "low", "medium", "high", "max"]] = Field(
@@ -100,7 +96,7 @@ class BaseTool(ABC):
pass
@abstractmethod
def get_input_schema(self) -> Dict[str, Any]:
def get_input_schema(self) -> dict[str, Any]:
"""
Return the JSON Schema that defines this tool's parameters.
@@ -197,7 +193,7 @@ class BaseTool(ABC):
return None
def check_prompt_size(self, text: str) -> Optional[Dict[str, Any]]:
def check_prompt_size(self, text: str) -> Optional[dict[str, Any]]:
"""
Check if a text field is too large for MCP's token limits.
@@ -231,9 +227,7 @@ class BaseTool(ABC):
}
return None
def handle_prompt_file(
self, files: Optional[List[str]]
) -> tuple[Optional[str], Optional[List[str]]]:
def handle_prompt_file(self, files: Optional[list[str]]) -> tuple[Optional[str], Optional[list[str]]]:
"""
Check for and handle prompt.txt in the files list.
@@ -245,7 +239,7 @@ class BaseTool(ABC):
mechanism to bypass token constraints while preserving response capacity.
Args:
files: List of file paths
files: List of file paths (will be translated for current environment)
Returns:
tuple: (prompt_content, updated_files_list)
@@ -257,21 +251,47 @@ class BaseTool(ABC):
updated_files = []
for file_path in files:
# Translate path for current environment (Docker/direct)
translated_path = translate_path_for_environment(file_path)
# Check if the filename is exactly "prompt.txt"
# This ensures we don't match files like "myprompt.txt" or "prompt.txt.bak"
if os.path.basename(file_path) == "prompt.txt":
if os.path.basename(translated_path) == "prompt.txt":
try:
prompt_content = read_file_content(file_path)
# Read prompt.txt content and extract just the text
content, _ = read_file_content(translated_path)
# Extract the content between the file markers
if "--- BEGIN FILE:" in content and "--- END FILE:" in content:
lines = content.split("\n")
in_content = False
content_lines = []
for line in lines:
if line.startswith("--- BEGIN FILE:"):
in_content = True
continue
elif line.startswith("--- END FILE:"):
break
elif in_content:
content_lines.append(line)
prompt_content = "\n".join(content_lines)
else:
# Fallback: if it's already raw content (from tests or direct input)
# and doesn't have error markers, use it directly
if not content.startswith("\n--- ERROR"):
prompt_content = content
else:
prompt_content = None
except Exception:
# If we can't read the file, we'll just skip it
# The error will be handled elsewhere
pass
else:
# Keep the original path in the files list (will be translated later by read_files)
updated_files.append(file_path)
return prompt_content, updated_files if updated_files else None
async def execute(self, arguments: Dict[str, Any]) -> List[TextContent]:
async def execute(self, arguments: dict[str, Any]) -> list[TextContent]:
"""
Execute the tool with the provided arguments.
@@ -338,11 +358,7 @@ class BaseTool(ABC):
else:
# Handle cases where the model couldn't generate a response
# This might happen due to safety filters or other constraints
finish_reason = (
response.candidates[0].finish_reason
if response.candidates
else "Unknown"
)
finish_reason = response.candidates[0].finish_reason if response.candidates else "Unknown"
tool_output = ToolOutput(
status="error",
content=f"Response blocked or incomplete. Finish reason: {finish_reason}",
@@ -380,10 +396,7 @@ class BaseTool(ABC):
# Try to parse as JSON to check for clarification requests
potential_json = json.loads(raw_text.strip())
if (
isinstance(potential_json, dict)
and potential_json.get("status") == "requires_clarification"
):
if isinstance(potential_json, dict) and potential_json.get("status") == "requires_clarification":
# Validate the clarification request structure
clarification = ClarificationRequest(**potential_json)
return ToolOutput(
@@ -391,11 +404,7 @@ class BaseTool(ABC):
content=clarification.model_dump_json(),
content_type="json",
metadata={
"original_request": (
request.model_dump()
if hasattr(request, "model_dump")
else str(request)
)
"original_request": (request.model_dump() if hasattr(request, "model_dump") else str(request))
},
)
@@ -408,11 +417,7 @@ class BaseTool(ABC):
# Determine content type based on the formatted content
content_type = (
"markdown"
if any(
marker in formatted_content for marker in ["##", "**", "`", "- ", "1. "]
)
else "text"
"markdown" if any(marker in formatted_content for marker in ["##", "**", "`", "- ", "1. "]) else "text"
)
return ToolOutput(
@@ -479,9 +484,7 @@ class BaseTool(ABC):
f"Maximum is {MAX_CONTEXT_TOKENS:,} tokens."
)
def create_model(
self, model_name: str, temperature: float, thinking_mode: str = "medium"
):
def create_model(self, model_name: str, temperature: float, thinking_mode: str = "medium"):
"""
Create a configured Gemini model instance.
@@ -522,9 +525,7 @@ class BaseTool(ABC):
# Create a wrapper class to provide a consistent interface
# This abstracts the differences between API versions
class ModelWrapper:
def __init__(
self, client, model_name, temperature, thinking_budget
):
def __init__(self, client, model_name, temperature, thinking_budget):
self.client = client
self.model_name = model_name
self.temperature = temperature
@@ -537,9 +538,7 @@ class BaseTool(ABC):
config=types.GenerateContentConfig(
temperature=self.temperature,
candidate_count=1,
thinking_config=types.ThinkingConfig(
thinking_budget=self.thinking_budget
),
thinking_config=types.ThinkingConfig(thinking_budget=self.thinking_budget),
),
)
@@ -617,11 +616,7 @@ class BaseTool(ABC):
"content": type(
"obj",
(object,),
{
"parts": [
type("obj", (object,), {"text": text})
]
},
{"parts": [type("obj", (object,), {"text": text})]},
)(),
"finish_reason": "STOP",
},

View File

@@ -2,7 +2,7 @@
Chat tool - General development chat and collaborative thinking
"""
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from mcp.types import TextContent
from pydantic import Field
@@ -22,7 +22,7 @@ class ChatRequest(ToolRequest):
...,
description="Your question, topic, or current thinking to discuss with Gemini",
)
files: Optional[List[str]] = Field(
files: Optional[list[str]] = Field(
default_factory=list,
description="Optional files for context (must be absolute paths)",
)
@@ -44,7 +44,7 @@ class ChatTool(BaseTool):
"'share my thinking with gemini', 'explain', 'what is', 'how do I'."
)
def get_input_schema(self) -> Dict[str, Any]:
def get_input_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
@@ -81,7 +81,7 @@ class ChatTool(BaseTool):
def get_request_model(self):
return ChatRequest
async def execute(self, arguments: Dict[str, Any]) -> List[TextContent]:
async def execute(self, arguments: dict[str, Any]) -> list[TextContent]:
"""Override execute to check prompt size before processing"""
# First validate request
request_model = self.get_request_model()
@@ -90,11 +90,7 @@ class ChatTool(BaseTool):
# Check prompt size
size_check = self.check_prompt_size(request.prompt)
if size_check:
return [
TextContent(
type="text", text=ToolOutput(**size_check).model_dump_json()
)
]
return [TextContent(type="text", text=ToolOutput(**size_check).model_dump_json())]
# Continue with normal execution
return await super().execute(arguments)
@@ -113,7 +109,7 @@ class ChatTool(BaseTool):
# Add context files if provided
if request.files:
file_content, _ = read_files(request.files)
file_content = read_files(request.files)
user_content = f"{user_content}\n\n=== CONTEXT FILES ===\n{file_content}\n=== END CONTEXT ===="
# Check token limits
@@ -131,5 +127,5 @@ Please provide a thoughtful, comprehensive response:"""
return full_prompt
def format_response(self, response: str, request: ChatRequest) -> str:
"""Format the chat response (no special formatting needed)"""
return response
"""Format the chat response with actionable guidance"""
return f"{response}\n\n---\n\n**Claude's Turn:** Evaluate this perspective alongside your analysis to form a comprehensive solution."

View File

@@ -2,7 +2,7 @@
Debug Issue tool - Root cause analysis and debugging assistance
"""
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from mcp.types import TextContent
from pydantic import Field
@@ -18,22 +18,14 @@ from .models import ToolOutput
class DebugIssueRequest(ToolRequest):
"""Request model for debug_issue tool"""
error_description: str = Field(
..., description="Error message, symptoms, or issue description"
)
error_context: Optional[str] = Field(
None, description="Stack trace, logs, or additional error context"
)
files: Optional[List[str]] = Field(
error_description: str = Field(..., description="Error message, symptoms, or issue description")
error_context: Optional[str] = Field(None, description="Stack trace, logs, or additional error context")
files: Optional[list[str]] = Field(
None,
description="Files or directories that might be related to the issue (must be absolute paths)",
)
runtime_info: Optional[str] = Field(
None, description="Environment, versions, or runtime information"
)
previous_attempts: Optional[str] = Field(
None, description="What has been tried already"
)
runtime_info: Optional[str] = Field(None, description="Environment, versions, or runtime information")
previous_attempts: Optional[str] = Field(None, description="What has been tried already")
class DebugIssueTool(BaseTool):
@@ -48,10 +40,13 @@ class DebugIssueTool(BaseTool):
"Use this when you need help tracking down bugs or understanding errors. "
"Triggers: 'debug this', 'why is this failing', 'root cause', 'trace error'. "
"I'll analyze the issue, find root causes, and provide step-by-step solutions. "
"Include error messages, stack traces, and relevant code for best results."
"Include error messages, stack traces, and relevant code for best results. "
"Choose thinking_mode based on issue complexity: 'low' for simple errors, "
"'medium' for standard debugging (default), 'high' for complex system issues, "
"'max' for extremely challenging bugs requiring deepest analysis."
)
def get_input_schema(self) -> Dict[str, Any]:
def get_input_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
@@ -100,7 +95,7 @@ class DebugIssueTool(BaseTool):
def get_request_model(self):
return DebugIssueRequest
async def execute(self, arguments: Dict[str, Any]) -> List[TextContent]:
async def execute(self, arguments: dict[str, Any]) -> list[TextContent]:
"""Override execute to check error_description and error_context size before processing"""
# First validate request
request_model = self.get_request_model()
@@ -109,21 +104,13 @@ class DebugIssueTool(BaseTool):
# Check error_description size
size_check = self.check_prompt_size(request.error_description)
if size_check:
return [
TextContent(
type="text", text=ToolOutput(**size_check).model_dump_json()
)
]
return [TextContent(type="text", text=ToolOutput(**size_check).model_dump_json())]
# Check error_context size if provided
if request.error_context:
size_check = self.check_prompt_size(request.error_context)
if size_check:
return [
TextContent(
type="text", text=ToolOutput(**size_check).model_dump_json()
)
]
return [TextContent(type="text", text=ToolOutput(**size_check).model_dump_json())]
# Continue with normal execution
return await super().execute(arguments)
@@ -146,31 +133,21 @@ class DebugIssueTool(BaseTool):
request.files = updated_files
# Build context sections
context_parts = [
f"=== ISSUE DESCRIPTION ===\n{request.error_description}\n=== END DESCRIPTION ==="
]
context_parts = [f"=== ISSUE DESCRIPTION ===\n{request.error_description}\n=== END DESCRIPTION ==="]
if request.error_context:
context_parts.append(
f"\n=== ERROR CONTEXT/STACK TRACE ===\n{request.error_context}\n=== END CONTEXT ==="
)
context_parts.append(f"\n=== ERROR CONTEXT/STACK TRACE ===\n{request.error_context}\n=== END CONTEXT ===")
if request.runtime_info:
context_parts.append(
f"\n=== RUNTIME INFORMATION ===\n{request.runtime_info}\n=== END RUNTIME ==="
)
context_parts.append(f"\n=== RUNTIME INFORMATION ===\n{request.runtime_info}\n=== END RUNTIME ===")
if request.previous_attempts:
context_parts.append(
f"\n=== PREVIOUS ATTEMPTS ===\n{request.previous_attempts}\n=== END ATTEMPTS ==="
)
context_parts.append(f"\n=== PREVIOUS ATTEMPTS ===\n{request.previous_attempts}\n=== END ATTEMPTS ===")
# Add relevant files if provided
if request.files:
file_content, _ = read_files(request.files)
context_parts.append(
f"\n=== RELEVANT CODE ===\n{file_content}\n=== END CODE ==="
)
file_content = read_files(request.files)
context_parts.append(f"\n=== RELEVANT CODE ===\n{file_content}\n=== END CODE ===")
full_context = "\n".join(context_parts)
@@ -189,4 +166,4 @@ Focus on finding the root cause and providing actionable solutions."""
def format_response(self, response: str, request: DebugIssueRequest) -> str:
"""Format the debugging response"""
return f"Debug Analysis\n{'=' * 50}\n\n{response}"
return f"Debug Analysis\n{'=' * 50}\n\n{response}\n\n---\n\n**Next Steps:** Evaluate Gemini's recommendations, synthesize the best fix considering potential regressions, test thoroughly, and ensure the solution doesn't introduce new issues."

View File

@@ -2,7 +2,7 @@
Data models for tool responses and interactions
"""
from typing import Any, Dict, List, Literal, Optional
from typing import Any, Literal, Optional
from pydantic import BaseModel, Field
@@ -10,22 +10,20 @@ from pydantic import BaseModel, Field
class ToolOutput(BaseModel):
"""Standardized output format for all tools"""
status: Literal[
"success", "error", "requires_clarification", "requires_file_prompt"
] = "success"
status: Literal["success", "error", "requires_clarification", "requires_file_prompt"] = "success"
content: str = Field(..., description="The main content/response from the tool")
content_type: Literal["text", "markdown", "json"] = "text"
metadata: Optional[Dict[str, Any]] = Field(default_factory=dict)
metadata: Optional[dict[str, Any]] = Field(default_factory=dict)
class ClarificationRequest(BaseModel):
"""Request for additional context or clarification"""
question: str = Field(..., description="Question to ask Claude for more context")
files_needed: Optional[List[str]] = Field(
files_needed: Optional[list[str]] = Field(
default_factory=list, description="Specific files that are needed for analysis"
)
suggested_next_action: Optional[Dict[str, Any]] = Field(
suggested_next_action: Optional[dict[str, Any]] = Field(
None,
description="Suggested tool call with parameters after getting clarification",
)
@@ -35,28 +33,22 @@ class DiagnosticHypothesis(BaseModel):
"""A debugging hypothesis with context and next steps"""
rank: int = Field(..., description="Ranking of this hypothesis (1 = most likely)")
confidence: Literal["high", "medium", "low"] = Field(
..., description="Confidence level"
)
confidence: Literal["high", "medium", "low"] = Field(..., description="Confidence level")
hypothesis: str = Field(..., description="Description of the potential root cause")
reasoning: str = Field(..., description="Why this hypothesis is plausible")
next_step: str = Field(
..., description="Suggested action to test/validate this hypothesis"
)
next_step: str = Field(..., description="Suggested action to test/validate this hypothesis")
class StructuredDebugResponse(BaseModel):
"""Enhanced debug response with multiple hypotheses"""
summary: str = Field(..., description="Brief summary of the issue")
hypotheses: List[DiagnosticHypothesis] = Field(
..., description="Ranked list of potential causes"
)
immediate_actions: List[str] = Field(
hypotheses: list[DiagnosticHypothesis] = Field(..., description="Ranked list of potential causes")
immediate_actions: list[str] = Field(
default_factory=list,
description="Immediate steps to take regardless of root cause",
)
additional_context_needed: Optional[List[str]] = Field(
additional_context_needed: Optional[list[str]] = Field(
default_factory=list,
description="Additional files or information that would help with analysis",
)

View File

@@ -3,17 +3,15 @@ Tool for reviewing pending git changes across multiple repositories.
"""
import os
import re
from typing import Any, Dict, List, Literal, Optional
from typing import Any, Literal, Optional
from mcp.types import TextContent
from pydantic import Field
from config import MAX_CONTEXT_TOKENS
from prompts.tool_prompts import REVIEW_CHANGES_PROMPT
from utils.file_utils import _get_secure_container_path, read_files
from utils.git_utils import (find_git_repositories, get_git_status,
run_git_command)
from utils.file_utils import read_files, translate_path_for_environment
from utils.git_utils import find_git_repositories, get_git_status, run_git_command
from utils.token_utils import estimate_tokens
from .base import BaseTool, ToolRequest
@@ -67,7 +65,7 @@ class ReviewChangesRequest(ToolRequest):
thinking_mode: Optional[Literal["minimal", "low", "medium", "high", "max"]] = Field(
None, description="Thinking depth mode for the assistant."
)
files: Optional[List[str]] = Field(
files: Optional[list[str]] = Field(
None,
description="Optional files or directories to provide as context (must be absolute paths). These files are not part of the changes but provide helpful context like configs, docs, or related code.",
)
@@ -87,10 +85,13 @@ class ReviewChanges(BaseTool):
"provides deep analysis of staged/unstaged changes. Essential for code quality and preventing bugs. "
"Triggers: 'before commit', 'review changes', 'check my changes', 'validate changes', 'pre-commit review', "
"'about to commit', 'ready to commit'. Claude should proactively suggest using this tool whenever "
"the user mentions committing or when changes are complete."
"the user mentions committing or when changes are complete. "
"Choose thinking_mode based on changeset size: 'low' for small focused changes, "
"'medium' for standard commits (default), 'high' for large feature branches or complex refactoring, "
"'max' for critical releases or when reviewing extensive changes across multiple systems."
)
def get_input_schema(self) -> Dict[str, Any]:
def get_input_schema(self) -> dict[str, Any]:
return self.get_request_model().model_json_schema()
def get_system_prompt(self) -> str:
@@ -105,16 +106,7 @@ class ReviewChanges(BaseTool):
return TEMPERATURE_ANALYTICAL
def _sanitize_filename(self, name: str) -> str:
"""Sanitize a string to be a valid filename."""
# Replace path separators and other problematic characters
name = name.replace("/", "_").replace("\\", "_").replace(" ", "_")
# Remove any remaining non-alphanumeric characters except dots, dashes, underscores
name = re.sub(r"[^a-zA-Z0-9._-]", "", name)
# Limit length to avoid filesystem issues
return name[:100]
async def execute(self, arguments: Dict[str, Any]) -> List[TextContent]:
async def execute(self, arguments: dict[str, Any]) -> list[TextContent]:
"""Override execute to check original_request size before processing"""
# First validate request
request_model = self.get_request_model()
@@ -124,11 +116,7 @@ class ReviewChanges(BaseTool):
if request.original_request:
size_check = self.check_prompt_size(request.original_request)
if size_check:
return [
TextContent(
type="text", text=ToolOutput(**size_check).model_dump_json()
)
]
return [TextContent(type="text", text=ToolOutput(**size_check).model_dump_json())]
# Continue with normal execution
return await super().execute(arguments)
@@ -147,7 +135,7 @@ class ReviewChanges(BaseTool):
request.files = updated_files
# Translate the path if running in Docker
translated_path = _get_secure_container_path(request.path)
translated_path = translate_path_for_environment(request.path)
# Check if the path translation resulted in an error path
if translated_path.startswith("/inaccessible/"):
@@ -167,13 +155,10 @@ class ReviewChanges(BaseTool):
all_diffs = []
repo_summaries = []
total_tokens = 0
max_tokens = (
MAX_CONTEXT_TOKENS - 50000
) # Reserve tokens for prompt and response
max_tokens = MAX_CONTEXT_TOKENS - 50000 # Reserve tokens for prompt and response
for repo_path in repositories:
repo_name = os.path.basename(repo_path) or "root"
repo_name = self._sanitize_filename(repo_name)
# Get status information
status = get_git_status(repo_path)
@@ -217,10 +202,10 @@ class ReviewChanges(BaseTool):
)
if success and diff.strip():
# Format diff with file header
diff_header = f"\n--- BEGIN DIFF: {repo_name} / {file_path} (compare to {request.compare_to}) ---\n"
diff_footer = (
f"\n--- END DIFF: {repo_name} / {file_path} ---\n"
diff_header = (
f"\n--- BEGIN DIFF: {repo_name} / {file_path} (compare to {request.compare_to}) ---\n"
)
diff_footer = f"\n--- END DIFF: {repo_name} / {file_path} ---\n"
formatted_diff = diff_header + diff + diff_footer
# Check token limit
@@ -234,58 +219,38 @@ class ReviewChanges(BaseTool):
unstaged_files = []
if request.include_staged:
success, files_output = run_git_command(
repo_path, ["diff", "--name-only", "--cached"]
)
success, files_output = run_git_command(repo_path, ["diff", "--name-only", "--cached"])
if success and files_output.strip():
staged_files = [
f for f in files_output.strip().split("\n") if f
]
staged_files = [f for f in files_output.strip().split("\n") if f]
# Generate per-file diffs for staged changes
for file_path in staged_files:
success, diff = run_git_command(
repo_path, ["diff", "--cached", "--", file_path]
)
success, diff = run_git_command(repo_path, ["diff", "--cached", "--", file_path])
if success and diff.strip():
diff_header = f"\n--- BEGIN DIFF: {repo_name} / {file_path} (staged) ---\n"
diff_footer = (
f"\n--- END DIFF: {repo_name} / {file_path} ---\n"
)
diff_footer = f"\n--- END DIFF: {repo_name} / {file_path} ---\n"
formatted_diff = diff_header + diff + diff_footer
# Check token limit
from utils import estimate_tokens
diff_tokens = estimate_tokens(formatted_diff)
if total_tokens + diff_tokens <= max_tokens:
all_diffs.append(formatted_diff)
total_tokens += diff_tokens
if request.include_unstaged:
success, files_output = run_git_command(
repo_path, ["diff", "--name-only"]
)
success, files_output = run_git_command(repo_path, ["diff", "--name-only"])
if success and files_output.strip():
unstaged_files = [
f for f in files_output.strip().split("\n") if f
]
unstaged_files = [f for f in files_output.strip().split("\n") if f]
# Generate per-file diffs for unstaged changes
for file_path in unstaged_files:
success, diff = run_git_command(
repo_path, ["diff", "--", file_path]
)
success, diff = run_git_command(repo_path, ["diff", "--", file_path])
if success and diff.strip():
diff_header = f"\n--- BEGIN DIFF: {repo_name} / {file_path} (unstaged) ---\n"
diff_footer = (
f"\n--- END DIFF: {repo_name} / {file_path} ---\n"
)
diff_footer = f"\n--- END DIFF: {repo_name} / {file_path} ---\n"
formatted_diff = diff_header + diff + diff_footer
# Check token limit
from utils import estimate_tokens
diff_tokens = estimate_tokens(formatted_diff)
if total_tokens + diff_tokens <= max_tokens:
all_diffs.append(formatted_diff)
@@ -310,7 +275,7 @@ class ReviewChanges(BaseTool):
if not all_diffs:
return "No pending changes found in any of the git repositories."
# Process context files if provided
# Process context files if provided using standardized file reading
context_files_content = []
context_files_summary = []
context_tokens = 0
@@ -318,40 +283,17 @@ class ReviewChanges(BaseTool):
if request.files:
remaining_tokens = max_tokens - total_tokens
# Read context files with remaining token budget
file_content, file_summary = read_files(request.files)
# Use standardized file reading with token budget
file_content = read_files(
request.files, max_tokens=remaining_tokens, reserve_tokens=1000 # Small reserve for formatting
)
# Check if context files fit in remaining budget
if file_content:
context_tokens = estimate_tokens(file_content)
if context_tokens <= remaining_tokens:
# Use the full content from read_files
context_files_content = [file_content]
# Parse summary to create individual file summaries
summary_lines = file_summary.split("\n")
for line in summary_lines:
if line.strip() and not line.startswith("Total files:"):
context_files_summary.append(f"✅ Included: {line.strip()}")
else:
context_files_summary.append(
f"⚠️ Context files too large (~{context_tokens:,} tokens, budget: ~{remaining_tokens:,} tokens)"
)
# Include as much as fits
if remaining_tokens > 1000: # Only if we have reasonable space
truncated_content = file_content[
: int(
len(file_content)
* (remaining_tokens / context_tokens)
* 0.9
)
]
context_files_content.append(
f"\n--- BEGIN CONTEXT FILES (TRUNCATED) ---\n{truncated_content}\n--- END CONTEXT FILES ---\n"
)
context_tokens = remaining_tokens
else:
context_tokens = 0
context_files_content = [file_content]
context_files_summary.append(f"✅ Included: {len(request.files)} context files")
else:
context_files_summary.append("⚠️ No context files could be read or files too large")
total_tokens += context_tokens
@@ -360,9 +302,7 @@ class ReviewChanges(BaseTool):
# Add original request context if provided
if request.original_request:
prompt_parts.append(
f"## Original Request/Ticket\n\n{request.original_request}\n"
)
prompt_parts.append(f"## Original Request/Ticket\n\n{request.original_request}\n")
# Add review parameters
prompt_parts.append("## Review Parameters\n")
@@ -393,9 +333,7 @@ class ReviewChanges(BaseTool):
else:
prompt_parts.append(f"- Branch: {summary['branch']}")
if summary["ahead"] or summary["behind"]:
prompt_parts.append(
f"- Ahead: {summary['ahead']}, Behind: {summary['behind']}"
)
prompt_parts.append(f"- Ahead: {summary['ahead']}, Behind: {summary['behind']}")
prompt_parts.append(f"- Changed Files: {summary['changed_files']}")
if summary["files"]:
@@ -403,9 +341,7 @@ class ReviewChanges(BaseTool):
for file in summary["files"]:
prompt_parts.append(f" - {file}")
if summary["changed_files"] > len(summary["files"]):
prompt_parts.append(
f" ... and {summary['changed_files'] - len(summary['files'])} more files"
)
prompt_parts.append(f" ... and {summary['changed_files'] - len(summary['files'])} more files")
# Add context files summary if provided
if context_files_summary:
@@ -449,3 +385,7 @@ class ReviewChanges(BaseTool):
)
return "\n".join(prompt_parts)
def format_response(self, response: str, request: ReviewChangesRequest) -> str:
"""Format the response with commit guidance"""
return f"{response}\n\n---\n\n**Commit Status:** If no critical issues found, changes are ready for commit. Otherwise, address issues first and re-run review. Check with user before proceeding with any commit."

View File

@@ -14,7 +14,7 @@ Key Features:
- Structured output with specific remediation steps
"""
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from mcp.types import TextContent
from pydantic import Field
@@ -36,19 +36,17 @@ class ReviewCodeRequest(ToolRequest):
review focus and standards.
"""
files: List[str] = Field(
files: list[str] = Field(
...,
description="Code files or directories to review (must be absolute paths)",
)
review_type: str = Field(
"full", description="Type of review: full|security|performance|quick"
)
focus_on: Optional[str] = Field(
None, description="Specific aspects to focus on during review"
)
standards: Optional[str] = Field(
None, description="Coding standards or guidelines to enforce"
context: str = Field(
...,
description="User's summary of what the code does, expected behavior, constraints, and review objectives",
)
review_type: str = Field("full", description="Type of review: full|security|performance|quick")
focus_on: Optional[str] = Field(None, description="Specific aspects to focus on during review")
standards: Optional[str] = Field(None, description="Coding standards or guidelines to enforce")
severity_filter: str = Field(
"all",
description="Minimum severity to report: critical|high|medium|all",
@@ -74,10 +72,13 @@ class ReviewCodeTool(BaseTool):
"Use this for thorough code review with actionable feedback. "
"Triggers: 'review this code', 'check for issues', 'find bugs', 'security audit'. "
"I'll identify issues by severity (Critical→High→Medium→Low) with specific fixes. "
"Supports focused reviews: security, performance, or quick checks."
"Supports focused reviews: security, performance, or quick checks. "
"Choose thinking_mode based on review scope: 'low' for small code snippets, "
"'medium' for standard files/modules (default), 'high' for complex systems/architectures, "
"'max' for critical security audits or large codebases requiring deepest analysis."
)
def get_input_schema(self) -> Dict[str, Any]:
def get_input_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
@@ -86,6 +87,10 @@ class ReviewCodeTool(BaseTool):
"items": {"type": "string"},
"description": "Code files or directories to review (must be absolute paths)",
},
"context": {
"type": "string",
"description": "User's summary of what the code does, expected behavior, constraints, and review objectives",
},
"review_type": {
"type": "string",
"enum": ["full", "security", "performance", "quick"],
@@ -118,7 +123,7 @@ class ReviewCodeTool(BaseTool):
"description": "Thinking depth: minimal (128), low (2048), medium (8192), high (16384), max (32768)",
},
},
"required": ["files"],
"required": ["files", "context"],
}
def get_system_prompt(self) -> str:
@@ -130,7 +135,7 @@ class ReviewCodeTool(BaseTool):
def get_request_model(self):
return ReviewCodeRequest
async def execute(self, arguments: Dict[str, Any]) -> List[TextContent]:
async def execute(self, arguments: dict[str, Any]) -> list[TextContent]:
"""Override execute to check focus_on size before processing"""
# First validate request
request_model = self.get_request_model()
@@ -140,11 +145,7 @@ class ReviewCodeTool(BaseTool):
if request.focus_on:
size_check = self.check_prompt_size(request.focus_on)
if size_check:
return [
TextContent(
type="text", text=ToolOutput(**size_check).model_dump_json()
)
]
return [TextContent(type="text", text=ToolOutput(**size_check).model_dump_json())]
# Continue with normal execution
return await super().execute(arguments)
@@ -177,7 +178,7 @@ class ReviewCodeTool(BaseTool):
request.files = updated_files
# Read all requested files, expanding directories as needed
file_content, summary = read_files(request.files)
file_content = read_files(request.files)
# Validate that the code fits within model context limits
self._validate_token_limit(file_content, "Code")
@@ -185,17 +186,11 @@ class ReviewCodeTool(BaseTool):
# Build customized review instructions based on review type
review_focus = []
if request.review_type == "security":
review_focus.append(
"Focus on security vulnerabilities and authentication issues"
)
review_focus.append("Focus on security vulnerabilities and authentication issues")
elif request.review_type == "performance":
review_focus.append(
"Focus on performance bottlenecks and optimization opportunities"
)
review_focus.append("Focus on performance bottlenecks and optimization opportunities")
elif request.review_type == "quick":
review_focus.append(
"Provide a quick review focusing on critical issues only"
)
review_focus.append("Provide a quick review focusing on critical issues only")
# Add any additional focus areas specified by the user
if request.focus_on:
@@ -207,22 +202,24 @@ class ReviewCodeTool(BaseTool):
# Apply severity filtering to reduce noise if requested
if request.severity_filter != "all":
review_focus.append(
f"Only report issues of {request.severity_filter} severity or higher"
)
review_focus.append(f"Only report issues of {request.severity_filter} severity or higher")
focus_instruction = "\n".join(review_focus) if review_focus else ""
# Construct the complete prompt with system instructions and code
full_prompt = f"""{self.get_system_prompt()}
=== USER CONTEXT ===
{request.context}
=== END CONTEXT ===
{focus_instruction}
=== CODE TO REVIEW ===
{file_content}
=== END CODE ===
Please provide a comprehensive code review following the format specified in the system prompt."""
Please provide a code review aligned with the user's context and expectations, following the format specified in the system prompt."""
return full_prompt
@@ -243,4 +240,4 @@ Please provide a comprehensive code review following the format specified in the
header = f"Code Review ({request.review_type.upper()})"
if request.focus_on:
header += f" - Focus: {request.focus_on}"
return f"{header}\n{'=' * 50}\n\n{response}"
return f"{header}\n{'=' * 50}\n\n{response}\n\n---\n\n**Follow-up Actions:** Address critical issues first, then high priority ones. Consider running tests after fixes and re-reviewing if substantial changes were made."

View File

@@ -2,7 +2,7 @@
Think Deeper tool - Extended reasoning and problem-solving
"""
from typing import Any, Dict, List, Optional
from typing import Any, Optional
from mcp.types import TextContent
from pydantic import Field
@@ -18,17 +18,13 @@ from .models import ToolOutput
class ThinkDeeperRequest(ToolRequest):
"""Request model for think_deeper tool"""
current_analysis: str = Field(
..., description="Claude's current thinking/analysis to extend"
)
problem_context: Optional[str] = Field(
None, description="Additional context about the problem or goal"
)
focus_areas: Optional[List[str]] = Field(
current_analysis: str = Field(..., description="Claude's current thinking/analysis to extend")
problem_context: Optional[str] = Field(None, description="Additional context about the problem or goal")
focus_areas: Optional[list[str]] = Field(
None,
description="Specific aspects to focus on (architecture, performance, security, etc.)",
)
files: Optional[List[str]] = Field(
files: Optional[list[str]] = Field(
None,
description="Optional file paths or directories for additional context (must be absolute paths)",
)
@@ -53,7 +49,7 @@ class ThinkDeeperTool(BaseTool):
"When in doubt, err on the side of a higher mode for truly deep thought and evaluation."
)
def get_input_schema(self) -> Dict[str, Any]:
def get_input_schema(self) -> dict[str, Any]:
return {
"type": "object",
"properties": {
@@ -104,7 +100,7 @@ class ThinkDeeperTool(BaseTool):
def get_request_model(self):
return ThinkDeeperRequest
async def execute(self, arguments: Dict[str, Any]) -> List[TextContent]:
async def execute(self, arguments: dict[str, Any]) -> list[TextContent]:
"""Override execute to check current_analysis size before processing"""
# First validate request
request_model = self.get_request_model()
@@ -113,11 +109,7 @@ class ThinkDeeperTool(BaseTool):
# Check current_analysis size
size_check = self.check_prompt_size(request.current_analysis)
if size_check:
return [
TextContent(
type="text", text=ToolOutput(**size_check).model_dump_json()
)
]
return [TextContent(type="text", text=ToolOutput(**size_check).model_dump_json())]
# Continue with normal execution
return await super().execute(arguments)
@@ -128,30 +120,22 @@ class ThinkDeeperTool(BaseTool):
prompt_content, updated_files = self.handle_prompt_file(request.files)
# Use prompt.txt content if available, otherwise use the current_analysis field
current_analysis = (
prompt_content if prompt_content else request.current_analysis
)
current_analysis = prompt_content if prompt_content else request.current_analysis
# Update request files list
if updated_files is not None:
request.files = updated_files
# Build context parts
context_parts = [
f"=== CLAUDE'S CURRENT ANALYSIS ===\n{current_analysis}\n=== END ANALYSIS ==="
]
context_parts = [f"=== CLAUDE'S CURRENT ANALYSIS ===\n{current_analysis}\n=== END ANALYSIS ==="]
if request.problem_context:
context_parts.append(
f"\n=== PROBLEM CONTEXT ===\n{request.problem_context}\n=== END CONTEXT ==="
)
context_parts.append(f"\n=== PROBLEM CONTEXT ===\n{request.problem_context}\n=== END CONTEXT ===")
# Add reference files if provided
if request.files:
file_content, _ = read_files(request.files)
context_parts.append(
f"\n=== REFERENCE FILES ===\n{file_content}\n=== END FILES ==="
)
file_content = read_files(request.files)
context_parts.append(f"\n=== REFERENCE FILES ===\n{file_content}\n=== END FILES ===")
full_context = "\n".join(context_parts)
@@ -162,9 +146,7 @@ class ThinkDeeperTool(BaseTool):
focus_instruction = ""
if request.focus_areas:
areas = ", ".join(request.focus_areas)
focus_instruction = (
f"\n\nFOCUS AREAS: Please pay special attention to {areas} aspects."
)
focus_instruction = f"\n\nFOCUS AREAS: Please pay special attention to {areas} aspects."
# Combine system prompt with context
full_prompt = f"""{self.get_system_prompt()}{focus_instruction}
@@ -190,24 +172,11 @@ Please provide deep analysis that extends Claude's thinking with:
## Critical Evaluation Required
Claude, now that you've received Gemini's extended analysis, please:
Claude, please critically evaluate Gemini's analysis by considering:
1. **Critically evaluate each suggestion** - Which points are truly valuable? Which might have limitations or trade-offs?
1. **Technical merit** - Which suggestions are valuable vs. have limitations?
2. **Constraints** - Fit with codebase patterns, performance, security, architecture
3. **Risks** - Hidden complexities, edge cases, potential failure modes
4. **Final recommendation** - Synthesize both perspectives, then think deeply further to explore additional considerations and arrive at the best technical solution
2. **Consider technical constraints** - How do these suggestions fit with:
- Existing codebase patterns and conventions
- Performance and scalability requirements
- Security implications and best practices
- Architecture and design principles
3. **Identify potential risks** - What could go wrong with each approach? Are there hidden complexities or edge cases?
4. **Synthesize your final recommendation** - Based on:
- Your original analysis
- Gemini's suggestions and critiques
- Technical feasibility and correctness
- A balanced assessment of trade-offs
5. **Formulate your conclusion** - What is the best technical solution considering all perspectives?
Remember: Gemini's analysis is meant to challenge and extend your thinking, not replace it. Use these insights to arrive at a more robust, well-considered solution."""
Remember: Use Gemini's insights to enhance, not replace, your analysis."""