跳轉至

MCP 工具詳細參考

此頁面列出了 boring.mcp.tools 包中的核心模組。

Core Tools (核心工具)

boring.mcp.tools.core

run_boring(task_description, verification_level='STANDARD', max_loops=5, use_cli=None, project_path=None, interactive=None)

Return CLI commands for autonomous development (Pure CLI Mode).

In MCP mode, this tool CANNOT execute the StatefulAgentLoop directly because it requires external AI (SDK or CLI) which conflicts with the MCP environment.

Instead, it returns a structured workflow template with: 1. The suggested CLI command to run the Boring agent 2. A step-by-step guide for manual execution 3. Project and task information

For TRUE autonomous execution, use the boring start CLI command directly.

Parameters:

Name Type Description Default
task_description Annotated[str, Field(description="Description of the development task to complete (e.g., 'Fix login validation bug')")]

Description of the development task to complete

required
verification_level Annotated[str, Field(description='Verification level: BASIC (syntax), STANDARD (lint), FULL (tests), SEMANTIC (judge)')]

Verification level (BASIC, STANDARD, FULL)

'STANDARD'
max_loops Annotated[int, Field(description='Maximum number of loop iterations (1-20)')]

Maximum number of loop iterations

5
use_cli Annotated[bool, Field(description='Whether to use Gemini CLI (supports extensions). Defaults to auto-detect.')]

Whether to use Gemini CLI

None
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional explicit path to project root

None

Returns:

Type Description
dict

Workflow template with CLI commands for external execution

Source code in src/boring/mcp/tools/core.py
@audited
def run_boring(
    task_description: Annotated[
        str,
        Field(
            description="Description of the development task to complete (e.g., 'Fix login validation bug')"
        ),
    ],
    verification_level: Annotated[
        str,
        Field(
            description="Verification level: BASIC (syntax), STANDARD (lint), FULL (tests), SEMANTIC (judge)"
        ),
    ] = "STANDARD",
    max_loops: Annotated[int, Field(description="Maximum number of loop iterations (1-20)")] = 5,
    use_cli: Annotated[
        bool,
        Field(
            description="Whether to use Gemini CLI (supports extensions). Defaults to auto-detect."
        ),
    ] = None,
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
    interactive: Annotated[
        bool, Field(description="Whether to run in interactive mode (auto-detected usually)")
    ] = None,
) -> dict:
    """
    Return CLI commands for autonomous development (Pure CLI Mode).

    In MCP mode, this tool CANNOT execute the StatefulAgentLoop directly because
    it requires external AI (SDK or CLI) which conflicts with the MCP environment.

    Instead, it returns a structured workflow template with:
    1. The suggested CLI command to run the Boring agent
    2. A step-by-step guide for manual execution
    3. Project and task information

    For TRUE autonomous execution, use the `boring start` CLI command directly.

    Args:
        task_description: Description of the development task to complete
        verification_level: Verification level (BASIC, STANDARD, FULL)
        max_loops: Maximum number of loop iterations
        use_cli: Whether to use Gemini CLI
        project_path: Optional explicit path to project root

    Returns:
        Workflow template with CLI commands for external execution
    """
    # --- Input Validation ---
    if not task_description or not task_description.strip():
        return {
            "status": "ERROR",
            "message": "task_description cannot be empty.",
            "suggestion": "Provide a clear description of the task, e.g., 'Fix login validation bug'.",
        }

    valid_levels = ("BASIC", "STANDARD", "FULL", "SEMANTIC")
    if verification_level.upper() not in valid_levels:
        return {
            "status": "ERROR",
            "message": f"Invalid verification_level: '{verification_level}'.",
            "suggestion": f"Use one of: {', '.join(valid_levels)}",
        }
    verification_level = verification_level.upper()

    if not (1 <= max_loops <= 20):
        return {
            "status": "ERROR",
            "message": f"max_loops must be between 1 and 20, got {max_loops}.",
            "suggestion": "Use a reasonable value like 5 (default) or 10 for complex tasks.",
        }

    # Resolve project root
    project_root, error = get_project_root_or_error(project_path)
    if error:
        return error

    # Check if we're in MCP mode - if so, return template
    is_mcp = os.environ.get("BORING_MCP_MODE") == "1"

    # Build the CLI command
    cli_parts = ["boring", "start"]
    if max_loops != 5:
        cli_parts.append(f"--calls {max_loops}")
    if verification_level != "STANDARD":
        cli_parts.append(f"--verification {verification_level.lower()}")
    if use_cli:
        cli_parts.append("--cli")

    cli_command = " ".join(cli_parts)

    # Create PROMPT.md content
    prompt_content = f"# Task\n\n{task_description}\n"

    # Alternative: Gemini CLI direct execution
    gemini_command = f'gemini --prompt "{task_description[:200]}..."'

    # Build suggested prompt for the AI to guide the user
    suggested_prompt = (
        f"I have prepared the autonomous development workflow for: '{task_description}'. "
        f"To proceed, please execute the following command in your terminal: `{cli_command}`. "
        "I will wait for you to run it and then we can verify the results."
    )

    return {
        "status": "WORKFLOW_TEMPLATE",
        "workflow": "run_boring",
        "project_root": str(project_root),
        "task_description": task_description,
        "verification_level": verification_level,
        "max_loops": max_loops,
        "is_mcp_mode": is_mcp,
        "cli_command": cli_command,
        "gemini_command": gemini_command,
        "suggested_prompt": suggested_prompt,
        "prompt_md_content": prompt_content,
        "message": (
            "🤖 **Autonomous Developer Loop Template**\n\n"
            "In MCP mode, `run_boring` cannot execute the agent loop directly due to "
            "technical constraints with external AI SDKs/CLIs.\n\n"
            "**Recommended Action:**\n"
            f"Run this command in your terminal to start the autonomous agent:\n"
            f"```bash\n{cli_command}\n```\n\n"
            "**Alternative (Direct AI):**\n"
            f"Use the Gemini CLI directly:\n"
            f"```bash\n{gemini_command}\n```\n\n"
            "**Manual Setup:**\n"
            "1. Save your task to `PROMPT.md`\n"
            "2. Run `boring start`"
        ),
        "manual_steps": [
            "1. Create PROMPT.md in the project with your task description",
            f"2. Run: {cli_command}",
            "3. Monitor the terminal for the agent's progress",
            "4. Use boring_verify to check results after execution",
        ],
        "note": "The boring agent runs OUTSIDE MCP to avoid timeouts and communication conflicts.",
    }

boring_health_check(project_path=None)

Check Boring system health.

Parameters:

Name Type Description Default
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional explicit path to project root (to load .env)

None

Returns:

Type Description
dict

Health check results including API key status, dependencies, etc.

Source code in src/boring/mcp/tools/core.py
@audited
def boring_health_check(
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Check Boring system health.

    Args:
        project_path: Optional explicit path to project root (to load .env)

    Returns:
        Health check results including API key status, dependencies, etc.
    """
    try:
        import os
        import shutil

        from ...health import run_health_check

        # Load env if project path provided
        if project_path:
            try:
                root = Path(project_path)
                configure_runtime_for_project(root)
            except Exception:
                pass

        # Detection logic is now centralized in GeminiClient, but for health report:
        has_key = "GOOGLE_API_KEY" in os.environ and os.environ["GOOGLE_API_KEY"]
        has_cli = shutil.which("gemini") is not None

        backend = "api"
        if not has_key and has_cli:
            backend = "cli"
        elif not has_key and not has_cli:
            backend = "none"

        report = run_health_check(backend=backend if backend != "none" else "api")

        checks = []
        for check in report.checks:
            checks.append(
                {
                    "name": check.name,
                    "status": check.status.name,
                    "message": check.message,
                    "suggestion": check.suggestion,
                }
            )

        return {
            "healthy": report.is_healthy,
            "passed": report.passed,
            "warnings": report.warnings,
            "failed": report.failed,
            "checks": checks,
            "backend": backend,
        }

    except ImportError as e:
        return {
            "healthy": False,
            "error": f"Missing dependency: {e}",
            "suggestion": "Run: pip install boring-gemini[health]",
        }
    except Exception as e:
        return {"healthy": False, "error": str(e), "error_type": type(e).__name__}

boring_quickstart(project_path=None)

Get a comprehensive quick start guide for new users.

Returns recommended first steps, available tools, and common workflows. Perfect for onboarding and exploring Boring capabilities.

Parameters:

Name Type Description Default
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional explicit path to project root. If not provided, auto-detects from CWD.

None

Returns:

Type Description
dict

Dict containing:

dict
  • welcome: Welcome message with version
dict
  • project_detected: Whether a valid project was found
dict
  • recommended_first_steps: Ordered list of getting started steps
dict
  • available_workflows: Categorized tools (spec_driven, evolution, verification)
dict
  • tips: Helpful usage tips
Example

boring_quickstart()

Returns:
Source code in src/boring/mcp/tools/core.py
@audited
def boring_quickstart(
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Get a comprehensive quick start guide for new users.

    Returns recommended first steps, available tools, and common workflows.
    Perfect for onboarding and exploring Boring capabilities.

    Args:
        project_path: Optional explicit path to project root.
                      If not provided, auto-detects from CWD.

    Returns:
        Dict containing:
        - welcome: Welcome message with version
        - project_detected: Whether a valid project was found
        - recommended_first_steps: Ordered list of getting started steps
        - available_workflows: Categorized tools (spec_driven, evolution, verification)
        - tips: Helpful usage tips

    Example:
        boring_quickstart()
        # Returns: {"welcome": "...", "project_detected": true, ...}
    """
    try:
        project_root, error = get_project_root_or_error(project_path)
        has_project = project_root is not None

        guide = {
            "welcome": "👋 Welcome to Boring for Gemini V5.2!",
            "project_detected": has_project,
            "recommended_first_steps": [],
            "available_workflows": {
                "spec_driven": [
                    "speckit_plan - Create implementation plan from requirements",
                    "speckit_tasks - Break plan into actionable tasks",
                    "speckit_analyze - Check code vs spec consistency",
                ],
                "evolution": [
                    "speckit_evolve_workflow - Adapt workflows to project needs",
                    "speckit_reset_workflow - Rollback to base template",
                ],
                "verification": [
                    "boring_verify - Run lint, tests, and import checks",
                    "boring_verify_file - Quick single-file validation",
                ],
            },
            "tips": [
                "💡 Use speckit_clarify when requirements are unclear",
                "💡 Check boring_health_check before starting",
                "💡 Workflows are dynamic - evolve them for your project!",
            ],
        }

        if has_project:
            guide["recommended_first_steps"] = [
                "1. boring_health_check - Verify system is ready",
                "2. speckit_plan - Create implementation plan",
                "3. speckit_tasks - Generate task checklist",
                "4. run_boring - Start autonomous development",
            ]
        else:
            guide["recommended_first_steps"] = [
                "1. Create a project directory with PROMPT.md",
                "2. Run boring-setup <project-name> to initialize",
                "3. boring_health_check - Verify system is ready",
            ]

        return guide

    except Exception as e:
        return {"status": "ERROR", "error": str(e)}

boring_status(project_path=None)

Get current Boring project status.

Parameters:

Name Type Description Default
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional explicit path to project root

None

Returns:

Type Description
dict

Project status including loop count, success rate, etc.

Source code in src/boring/mcp/tools/core.py
@audited
def boring_status(
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Get current Boring project status.

    Args:
        project_path: Optional explicit path to project root

    Returns:
        Project status including loop count, success rate, etc.
    """
    try:
        from ...memory import MemoryManager

        # Resolve project root
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error

        # CRITICAL: Update global settings for dependencies
        configure_runtime_for_project(project_root)

        memory = MemoryManager(project_root)
        state = memory.get_project_state()

        return {
            "project_name": state.get("project_name", "Unknown"),
            "total_loops": state.get("total_loops", 0),
            "successful_loops": state.get("successful_loops", 0),
            "failed_loops": state.get("failed_loops", 0),
            "last_activity": state.get("last_activity", "Never"),
        }

    except Exception as e:
        return {"error": str(e)}

boring_done(message)

Report task completion to the user with desktop notification.

Use this tool when you have finished your work and want to show a final message.

Source code in src/boring/mcp/tools/core.py
@audited
def boring_done(
    message: Annotated[str, Field(description="The completion message to display to the user")],
) -> str:
    """
    Report task completion to the user with desktop notification.

    Use this tool when you have finished your work and want to show a final message.
    """
    # Send Windows desktop notification
    notification_sent = False
    try:
        # Try win10toast first (most reliable on Windows)
        from win10toast import ToastNotifier

        toaster = ToastNotifier()
        toaster.show_toast(
            "🤖 Boring Agent",
            message[:200],  # Truncate long messages
            duration=5,
            threaded=True,
        )
        notification_sent = True
    except ImportError:
        try:
            # Fallback to plyer (cross-platform)
            from plyer import notification

            notification.notify(title="🤖 Boring Agent", message=message[:200], timeout=5)
            notification_sent = True
        except ImportError:
            pass  # No notification library available
    except Exception:
        pass  # Notification failed, continue anyway

    status = "✅ Task done"
    if notification_sent:
        status += " (Desktop notification sent)"

    return f"{status}. Message: {message}"

boring_forget_all(keep_current_task=True)

Signal to clear the LLM context (Context Hygiene).

This tool doesn't modify files but sends a strong signal to the IDE or Agent to clear previous conversation history to reduce hallucinations.

Parameters:

Name Type Description Default
keep_current_task Annotated[bool, Field(description='Whether to keep the current task definition in context')]

Whether to keep the current task definition in context.

True
Source code in src/boring/mcp/tools/core.py
@audited
def boring_forget_all(
    keep_current_task: Annotated[
        bool, Field(description="Whether to keep the current task definition in context")
    ] = True,
) -> dict:
    """
    Signal to clear the LLM context (Context Hygiene).

    This tool doesn't modify files but sends a strong signal to the IDE or Agent
    to clear previous conversation history to reduce hallucinations.

    Args:
        keep_current_task: Whether to keep the current task definition in context.
    """
    message = "🧹 **Context Cleanup Signal**\n\n"
    message += (
        "I have requested a context cleanup to maintain accuracy and reduce hallucinations.\n"
    )

    if keep_current_task:
        message += "✅ **Preserving:** Current task definition and critical context.\n"
    else:
        message += "⚠️ **Wiping:** Full context reset.\n"

    message += "\n**For IDE Users (Cursor/VS Code):**\n"
    message += "- Please click 'New Chat' or 'Clear Context' if available.\n"
    message += "- This ensures I don't get confused by old code snippets."

    return {
        "status": "CONTEXT_CLEAR_SIGNAL",
        "action": "forget_all",
        "keep_current_task": keep_current_task,
        "message": message,
    }

boring_call(tool_name, arguments=None)

Execute an internal Boring tool by name and arguments.

Source code in src/boring/mcp/tools/core.py
@mcp.tool(
    description="Execute an internal Boring tool by name and arguments.",
    annotations={"readOnlyHint": False, "openWorldHint": True},
)
def boring_call(tool_name: str, arguments: dict = None) -> any:
    """Execute an internal Boring tool by name and arguments."""
    if arguments is None:
        arguments = {}
    tool = internal_registry.get_tool(tool_name)
    if not tool:
        return f"Error: Tool '{tool_name}' not found."
    return tool.func(**arguments)

boring_inspect_tool(tool_name)

Get the full JSON schema and usage for an internal tool.

Source code in src/boring/mcp/tools/core.py
@mcp.tool(
    description="Get the full JSON schema and usage for an internal tool.",
    annotations={"readOnlyHint": True},
)
def boring_inspect_tool(tool_name: str) -> dict:
    """Get the full JSON schema and usage for an internal tool."""
    tool = internal_registry.get_tool(tool_name)
    if not tool:
        return {"error": f"Tool '{tool_name}' not found."}
    return {
        "name": tool.name,
        "description": tool.description,
        "category": tool.category,
        "schema": tool.schema,
    }

boring_active_skill(skill_name)

Activate a specific skill set and inject tools into the active context.

Source code in src/boring/mcp/tools/core.py
@mcp.tool(
    description="Activate a specific skill set to optimize the tool environment. Injects relevant tools dynamically.",
    annotations={"readOnlyHint": False},
)
def boring_active_skill(skill_name: str) -> dict:
    """Activate a specific skill set and inject tools into the active context."""
    import os

    from ..instance import mcp as smart_mcp

    os.environ["BORING_ACTIVE_SKILL"] = skill_name

    # Renaissance V2: Filter by category (Skill)
    matching_tools = [
        t for t in internal_registry.tools.values() if t.category.lower() == skill_name.lower()
    ]

    if not matching_tools:
        return {
            "status": "error",
            "message": f"Skill set '{skill_name}' not found.",
            "available_skills": list(internal_registry.categories.keys()),
        }

    injected = []
    for tool in matching_tools:
        if smart_mcp.inject_tool(tool.name):
            injected.append(tool.name)

    return {
        "status": "success",
        "message": f"✅ Skill **{skill_name}** activated. {len(injected)} tools injected.",
        "injected_tools": injected,
        "instruction": "These tools may now be available in your tool list. If not visible, you can still use them via boring_call().",
    }

boring_reset_skills()

Reset the tool environment by removing all dynamically injected tools.

Source code in src/boring/mcp/tools/core.py
@mcp.tool(
    description="Reset the tool environment by removing all dynamically injected tools. Useful for Context Hygiene.",
)
def boring_reset_skills() -> str:
    """Reset the tool environment by removing all dynamically injected tools."""
    from ..instance import mcp as smart_mcp

    count = smart_mcp.reset_injected_tools()
    return f"🧹 Tool environment reset. {count} injected tools cleared from profile tracking."

boring_orchestrate(goal)

Execute a multi-step objective by orchestrating internal tools.

Source code in src/boring/mcp/tools/core.py
@mcp.tool(
    description="Execute a multi-step objective by orchestrating internal tools.",
)
def boring_orchestrate(goal: str) -> str:
    """Execute a multi-step objective by orchestrating internal tools."""
    return f"Orchestrating goal: '{goal}'. [Coming soon]"

Git Tools (版本控制)

boring.mcp.tools.git

boring_hooks_install(project_path=None)

Install Boring Git hooks (pre-commit, pre-push) for local code quality enforcement.

This is the "Local Teams" feature - automatic verification before every commit/push.

Parameters:

Name Type Description Default
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional explicit path to project root.

None

Returns:

Type Description
BoringResult

Installation result as dict with status, message, and suggestion.

Source code in src/boring/mcp/tools/git.py
@audited
def boring_hooks_install(
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
) -> BoringResult:
    """
    Install Boring Git hooks (pre-commit, pre-push) for local code quality enforcement.

    This is the "Local Teams" feature - automatic verification before every commit/push.

    Args:
        project_path: Optional explicit path to project root.

    Returns:
        Installation result as dict with status, message, and suggestion.
    """
    try:
        root, error = get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message", "Invalid project root"))

        # Configure runtime
        configure_runtime_for_project(root)

        manager = HooksManager(root)

        # --- Idempotency Check ---
        status = manager.status()
        if status.get("is_git_repo"):
            hooks_info = status.get("hooks", {})
            all_boring = all(
                h.get("is_boring_hook", False)
                for h in hooks_info.values()
                if h.get("installed", False)
            )
            any_installed = any(h.get("installed", False) for h in hooks_info.values())
            if any_installed and all_boring:
                return create_success_result(
                    message="Hooks already installed.",
                    data={"hooks": hooks_info, "status": "SKIPPED"},
                )
        # --- End Idempotency Check ---

        success, msg = manager.install_all()
        if success:
            return create_success_result(
                message="Hooks installed successfully! Your commits and pushes will now be verified automatically.",
                data={"details": msg},
            )
        return create_error_result(message=msg)
    except Exception as e:
        return create_error_result(message=str(e))

boring_hooks_uninstall(project_path=None)

Remove Boring Git hooks.

Parameters:

Name Type Description Default
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional explicit path to project root.

None

Returns:

Type Description
dict

Uninstallation result as dict with status and message.

Source code in src/boring/mcp/tools/git.py
@audited
def boring_hooks_uninstall(
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Remove Boring Git hooks.

    Args:
        project_path: Optional explicit path to project root.

    Returns:
        Uninstallation result as dict with status and message.
    """
    try:
        root, error = get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message", "Invalid root"))

        configure_runtime_for_project(root)

        manager = HooksManager(root)

        success, msg = manager.uninstall_all()
        if success:
            return create_success_result(message=msg)
        return create_error_result(message=msg)
    except Exception as e:
        return create_error_result(message=str(e))

boring_hooks_status(project_path=None)

Get status of installed Git hooks.

Parameters:

Name Type Description Default
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional explicit path to project root.

None

Returns:

Type Description
dict

Dict with hook installation status.

Source code in src/boring/mcp/tools/git.py
@audited
def boring_hooks_status(
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Get status of installed Git hooks.

    Args:
        project_path: Optional explicit path to project root.

    Returns:
        Dict with hook installation status.
    """
    try:
        root, error = get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message", "Invalid root"))

        configure_runtime_for_project(root)

        manager = HooksManager(root)

        status_data = manager.status()
        return create_success_result(
            message=f"Git hooks status retrieved. Repo: {status_data.get('is_git_repo')}",
            data=status_data,
        )
    except Exception as e:
        return create_error_result(message=str(e))

boring_commit(task_file='task.md', commit_type='auto', scope=None, project_path=None)

Generate a semantic Git commit message from completed tasks in task.md.

Parses task.md for completed items ([x]) and generates a Conventional Commits format message. Returns the commit command for you to execute.

Parameters:

Name Type Description Default
task_file Annotated[str, Field(description='Path to task.md file (default: task.md)')]

Path to task.md file (default: task.md)

'task.md'
commit_type Annotated[str, Field(description='Commit type: auto, feat, fix, refactor, docs, chore')]

Commit type (auto, feat, fix, refactor, docs, chore)

'auto'
scope Annotated[str, Field(description="Optional scope for commit message (e.g., 'rag', 'auth')")]

Optional scope for commit message

None
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional explicit path to project root

None

Returns:

Type Description
BoringResult

Generated commit message and command

Source code in src/boring/mcp/tools/git.py
@audited
def boring_commit(
    task_file: Annotated[
        str, Field(description="Path to task.md file (default: task.md)")
    ] = "task.md",
    commit_type: Annotated[
        str, Field(description="Commit type: auto, feat, fix, refactor, docs, chore")
    ] = "auto",
    scope: Annotated[
        str, Field(description="Optional scope for commit message (e.g., 'rag', 'auth')")
    ] = None,
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
) -> BoringResult:
    """
    Generate a semantic Git commit message from completed tasks in task.md.

    Parses task.md for completed items ([x]) and generates a Conventional Commits
    format message. Returns the commit command for you to execute.

    Args:
        task_file: Path to task.md file (default: task.md)
        commit_type: Commit type (auto, feat, fix, refactor, docs, chore)
        scope: Optional scope for commit message
        project_path: Optional explicit path to project root

    Returns:
        Generated commit message and command
    """
    project_root, error = get_project_root_or_error(project_path)
    if error:
        return create_error_result(error.get("message", "Invalid root"))

    # Find task file in common locations
    task_path = project_root / task_file

    if not task_path.exists():
        for alt_path in [
            project_root / ".gemini" / "task.md",
            project_root / "openspec" / "task.md",
            project_root / ".agent" / "task.md",
        ]:
            if alt_path.exists():
                task_path = alt_path
                break

    if not task_path.exists():
        return create_error_result(
            message=f"Task file not found: {task_file}",
            error_details=f"Searched: {[str(project_root / task_file)]}",
        )

    try:
        content = task_path.read_text(encoding="utf-8")
    except Exception as e:
        return create_error_result(message=f"Cannot read task file: {e}")

    # Parse completed tasks (lines with [x])
    completed_pattern = r"^\s*-\s*\[x\]\s*(.+)$"
    completed_tasks = re.findall(completed_pattern, content, re.MULTILINE | re.IGNORECASE)

    if not completed_tasks:
        return create_success_result(
            message="No completed tasks found in task.md",
            data={
                "status": "NO_COMPLETED_TASKS",
                "hint": "Mark tasks as complete with [x] before generating commit",
            },
        )

    # Detect commit type from tasks if auto
    detected_type = commit_type
    if commit_type == "auto":
        task_text = " ".join(completed_tasks).lower()
        if any(word in task_text for word in ["fix", "bug", "error", "issue"]):
            detected_type = "fix"
        elif any(word in task_text for word in ["add", "new", "implement", "create"]):
            detected_type = "feat"
        elif any(word in task_text for word in ["refactor", "clean", "improve"]):
            detected_type = "refactor"
        elif any(word in task_text for word in ["doc", "readme", "comment"]):
            detected_type = "docs"
        else:
            detected_type = "feat"

    # Detect scope from task keywords
    detected_scope = scope
    if not detected_scope:
        scope_keywords = {
            "rag": ["rag", "index", "search", "retrieve"],
            "mcp": ["mcp", "tool", "server"],
            "verify": ["verify", "lint", "test"],
        }
        task_text = " ".join(completed_tasks).lower()
        for scope_name, keywords in scope_keywords.items():
            if any(kw in task_text for kw in keywords):
                detected_scope = scope_name
                break

    # Build commit message from first task
    main_task = completed_tasks[0].strip()
    main_task = re.sub(r"`([^`]+)`", r"\1", main_task)  # Remove backticks
    main_task = re.sub(r"\*\*([^*]+)\*\*", r"\1", main_task)  # Remove bold
    main_task = main_task.lower().rstrip(".")

    scope_str = f"({detected_scope})" if detected_scope else ""
    commit_line = f"{detected_type}{scope_str}: {main_task}"

    # Escape quotes for shell
    escaped_message = commit_line.replace('"', '\\"')

    return create_success_result(
        message=f"Ready to commit: {commit_line}",
        data={
            "commit_type": detected_type,
            "scope": detected_scope,
            "message": commit_line,
            "completed_tasks": len(completed_tasks),
            "command": f'git commit -m "{escaped_message}"',
        },
    )

boring_visualize(scope='module', output_format='mermaid', project_path=None)

Generate architecture visualization from codebase structure.

Scans Python files and generates a Mermaid.js diagram showing module dependencies and relationships.

Parameters:

Name Type Description Default
scope Annotated[str, Field(description='Visualization scope: module, class, or full')]

Visualization scope (module, class, full)

'module'
output_format Annotated[str, Field(description='Output format: mermaid, json')]

Output format (mermaid, json)

'mermaid'
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional explicit path to project root

None

Returns:

Type Description
BoringResult

Generated diagram or structure data

Source code in src/boring/mcp/tools/git.py
@audited
def boring_visualize(
    scope: Annotated[
        str, Field(description="Visualization scope: module, class, or full")
    ] = "module",
    output_format: Annotated[str, Field(description="Output format: mermaid, json")] = "mermaid",
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
) -> BoringResult:
    """
    Generate architecture visualization from codebase structure.

    Scans Python files and generates a Mermaid.js diagram showing
    module dependencies and relationships.

    Args:
        scope: Visualization scope (module, class, full)
        output_format: Output format (mermaid, json)
        project_path: Optional explicit path to project root

    Returns:
        Generated diagram or structure data
    """
    project_root, error = get_project_root_or_error(project_path)
    if error:
        return create_error_result(error.get("message", "Invalid root"))

    # Find Python files
    src_dir = project_root / "src"
    if not src_dir.exists():
        src_dir = project_root

    # Build module graph
    modules = {}
    imports = []

    for py_file in src_dir.rglob("*.py"):
        if "__pycache__" in str(py_file):
            continue

        try:
            rel_path = py_file.relative_to(src_dir)
            module_name = str(rel_path.with_suffix("")).replace("/", ".").replace("\\", ".")
            modules[module_name] = str(rel_path)

            content = py_file.read_text(encoding="utf-8", errors="ignore")
            from_imports = re.findall(r"^from\s+([\w.]+)\s+import", content, re.MULTILINE)

            for imp in from_imports:
                if imp.startswith(".") or any(imp.startswith(m.split(".")[0]) for m in modules):
                    imports.append((module_name, imp))
        except Exception:
            continue

    if output_format == "json":
        return create_success_result(
            message=f"Analyzed {len(modules)} modules.",
            data={"modules": modules, "total": len(modules)},
        )

    # Generate Mermaid diagram
    mermaid_lines = ["graph TD"]
    node_ids = {}

    for i, (mod_name, _) in enumerate(list(modules.items())[:15]):
        node_id = f"M{i}"
        node_ids[mod_name] = node_id
        short_name = mod_name.split(".")[-1]
        mermaid_lines.append(f"    {node_id}[{short_name}]")

    for source, target in imports[:20]:
        src_id = node_ids.get(source)
        tgt_id = None
        for mod_name, mod_id in node_ids.items():
            if target.endswith(mod_name) or mod_name.endswith(target.lstrip(".")):
                tgt_id = mod_id
                break
        if src_id and tgt_id and src_id != tgt_id:
            mermaid_lines.append(f"    {src_id} --> {tgt_id}")

    return create_success_result(
        message="Generated Mermaid diagram.",
        data={
            "format": "mermaid",
            "diagram": "\n".join(mermaid_lines),
            "total_modules": len(modules),
        },
    )

boring_checkpoint(action='list', name=None, stash_changes=True, project_path=None)

Manage project checkpoints (save states) via Git tags.

Use this to save your work before risky operations and restore it if things go wrong.

Actions: - create: Create a new checkpoint (git tag checkpoint/{name}) - restore: Restore a checkpoint (git reset --hard checkpoint/{name}) - list: List all available checkpoints

Parameters:

Name Type Description Default
action Annotated[str, Field(description='Action to perform: create, restore, list')]

create, restore, or list

'list'
name Annotated[str, Field(description='Name of the checkpoint (required for create/restore)')]

Name of the checkpoint (e.g. 'refactor-auth', 'pre-migration')

None
stash_changes Annotated[bool, Field(description='Whether to stash changes before restoring (default: True)')]

If restoring, stash current changes first (default: True)

True
project_path Annotated[str, Field(description='Optional explicit path to project root')]

Optional project root

None
Source code in src/boring/mcp/tools/git.py
@audited
def boring_checkpoint(
    action: Annotated[str, Field(description="Action to perform: create, restore, list")] = "list",
    name: Annotated[
        str, Field(description="Name of the checkpoint (required for create/restore)")
    ] = None,
    stash_changes: Annotated[
        bool, Field(description="Whether to stash changes before restoring (default: True)")
    ] = True,
    project_path: Annotated[
        str, Field(description="Optional explicit path to project root")
    ] = None,
) -> BoringResult:
    """
    Manage project checkpoints (save states) via Git tags.

    Use this to save your work before risky operations and restore it if things go wrong.

    Actions:
    - create: Create a new checkpoint (git tag checkpoint/{name})
    - restore: Restore a checkpoint (git reset --hard checkpoint/{name})
    - list: List all available checkpoints

    Args:
        action: create, restore, or list
        name: Name of the checkpoint (e.g. 'refactor-auth', 'pre-migration')
        stash_changes: If restoring, stash current changes first (default: True)
        project_path: Optional project root
    """
    project_root, error = get_project_root_or_error(project_path)
    if error:
        return create_error_result(error.get("message", "Invalid root"))

    configure_runtime_for_project(project_root)

    import subprocess

    def git_cmd(args):
        try:
            result = subprocess.run(
                ["git"] + args, cwd=project_root, capture_output=True, text=True, check=True
            )
            return True, result.stdout.strip()
        except subprocess.CalledProcessError as e:
            err = e.stderr.strip() if e.stderr else str(e)
            return False, err

    # Validate inputs
    if action in ["create", "restore"] and not name:
        return create_error_result(message="Checkpoint name is required for create/restore")

    prefix = "checkpoint/"

    if action == "create":
        tag_name = f"{prefix}{name}"
        # Check if exists
        ok, _ = git_cmd(["rev-parse", tag_name])
        if ok:
            return create_error_result(
                message=f"Checkpoint '{name}' already exists. Choose a different name."
            )

        success, output = git_cmd(["tag", tag_name])
        if success:
            return create_success_result(
                message=f"Checkpoint '{name}' created.",
                data={
                    "details": f"Created git tag {tag_name}. Use action='restore' name='{name}' to revert matching state."
                },
            )
        return create_error_result(message=f"Failed to create checkpoint: {output}")

    elif action == "restore":
        tag_name = f"{prefix}{name}"
        # detailed check
        ok, _ = git_cmd(["rev-parse", tag_name])
        if not ok:
            return create_error_result(message=f"Checkpoint '{name}' not found.")

        # Stash if requested
        stash_msg = ""
        if stash_changes:
            # check for modifications
            ok, status = git_cmd(["status", "--porcelain"])
            if ok and status:
                ok_stash, out_stash = git_cmd(
                    ["stash", "save", f"Auto-stash before restore {name}"]
                )
                if ok_stash:
                    stash_msg = " (Current changes stashed)"
                else:
                    return create_error_result(message=f"Failed to stash changes: {out_stash}")

        # Reset hard
        success, output = git_cmd(["reset", "--hard", tag_name])
        if success:
            return create_success_result(
                message=f"Restored to checkpoint '{name}'{stash_msg}.", data={"details": output}
            )
        return create_error_result(message=f"Failed to restore: {output}")

    elif action == "list":
        success, output = git_cmd(["tag", "-l", f"{prefix}*"])
        if not success:
            return create_error_result(message=f"Failed to list checkpoints: {output}")

        tags = output.splitlines()
        checkpoints = [t[len(prefix) :] for t in tags if t.strip() and t.startswith(prefix)]
        return create_success_result(
            message=f"Found {len(checkpoints)} checkpoints.",
            data={"checkpoints": checkpoints, "count": len(checkpoints)},
        )

    return create_error_result(message=f"Unknown action: {action}")

RAG Tools (知識檢索)

boring.mcp.tools.rag

MCP Tools for RAG System

Exposes RAG functionality as MCP tools for AI agents.

reload_rag_dependencies()

Attempt to reload RAG dependencies at runtime.

This allows the MCP server to pick up newly installed packages without a full process restart.

Returns:

Type Description
dict

dict with status and message

Source code in src/boring/mcp/tools/rag.py
def reload_rag_dependencies() -> dict:
    """
    Attempt to reload RAG dependencies at runtime.

    This allows the MCP server to pick up newly installed packages
    without a full process restart.

    Returns:
        dict with status and message
    """
    global _RAG_IMPORT_ERROR, RAGRetriever, create_rag_retriever

    import site
    import sys

    # Step 1: Refresh user site packages
    try:
        user_site = site.getusersitepackages()
        if isinstance(user_site, str) and user_site not in sys.path:
            sys.path.insert(0, user_site)
    except Exception:
        pass

    # Step 2: Clear any cached import errors
    for mod_name in ["chromadb", "sentence_transformers", "boring.rag"]:
        if mod_name in sys.modules:
            del sys.modules[mod_name]

    # Step 3: Check again via DependencyManager
    # Reset cached availability first
    if hasattr(DependencyManager, "_chroma_available"):
        DependencyManager._chroma_available = None

    if DependencyManager.check_chroma():
        try:
            from boring.rag import RAGRetriever as _RAGRetriever
            from boring.rag import create_rag_retriever as _create_rag_retriever

            global RAGRetriever, create_rag_retriever
            RAGRetriever = _RAGRetriever
            create_rag_retriever = _create_rag_retriever
            _retrievers.clear()  # Clear cached retrievers

            return {
                "status": "SUCCESS",
                "message": "✅ RAG dependencies reloaded successfully! You can now use boring_rag_index and boring_rag_search.",
            }
        except ImportError as e:
            return {"status": "ERROR", "message": f"❌ Error importing RAG modules: {e}"}

    pip_cmd = f"{sys.executable} -m pip install boring-aicoding[vector]"
    return {
        "status": "ERROR",
        "message": "❌ RAG dependencies still missing via DependencyManager.",
        "fix_command": pip_cmd,
        "hint": f"Run this command first:\n    {pip_cmd}",
    }

get_retriever(project_root)

Get or create RAG retriever for a project. Returns None if RAG is not available.

Source code in src/boring/mcp/tools/rag.py
def get_retriever(project_root: Path) -> "RAGRetriever | None":
    """Get or create RAG retriever for a project. Returns None if RAG is not available."""
    if create_rag_retriever is None:
        return None
    key = str(project_root)
    if key not in _retrievers:
        _retrievers[key] = create_rag_retriever(project_root)
    return _retrievers[key]

register_rag_tools(mcp, helpers)

Register RAG tools with the MCP server.

Parameters:

Name Type Description Default
mcp Any

FastMCP instance

required
helpers dict

Dict with helper functions (get_project_root_or_error, etc.)

required
Source code in src/boring/mcp/tools/rag.py
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
def register_rag_tools(mcp: Any, helpers: dict):
    """
    Register RAG tools with the MCP server.

    Args:
        mcp: FastMCP instance
        helpers: Dict with helper functions (get_project_root_or_error, etc.)
    """
    get_project_root_or_error = helpers.get("get_project_root_or_error")

    @mcp.tool(
        description="Reload RAG dependencies after pip install.",
        annotations={"readOnlyHint": False, "idempotentHint": True},
    )
    def boring_rag_reload() -> BoringResult:
        """
        Attempt to reload RAG dependencies at runtime.

        Use this after installing chromadb/sentence-transformers to enable
        RAG features without restarting the MCP server.

        Returns:
            Status and message indicating success or required actions
        """
        res = reload_rag_dependencies()
        if res["status"] == "SUCCESS":
            return create_success_result(res["message"], data=res)
        return create_error_result(res["message"], error_details=res.get("hint"))

    @mcp.tool(
        description="Index codebase for semantic search.",
        annotations={"readOnlyHint": False, "idempotentHint": True, "openWorldHint": False},
    )
    def boring_rag_index(
        force: Annotated[
            bool,
            Field(
                description="If True, rebuild index even if it exists. Use when codebase structure changed significantly or index appears corrupted. Default: False (skips if index exists)."
            ),
        ] = False,
        project_path: Annotated[
            str,
            Field(
                description="Optional explicit path to project root. If not provided, automatically detects project root by searching for common markers (pyproject.toml, package.json, etc.) starting from current directory. Example: '.' or '/path/to/project'."
            ),
        ] = None,
    ) -> BoringResult:
        """
        Index the codebase for RAG retrieval.

        Creates vector embeddings and dependency graph for semantic code search.
        Run this once per project, or with force=True to rebuild.

        Args:
            force: If True, rebuild index even if it exists
            project_path: Optional explicit path to project root

        Returns:
            Status message with indexing statistics
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message"))
        retriever = get_retriever(project_root)
        if retriever is None or not retriever.is_available:
            pip_cmd = f"{sys.executable} -m pip install boring-aicoding[vector]"
            return create_error_result(
                "❌ RAG module not available.\n\n"
                f"[AUTO-FIX] Run: {pip_cmd}\n\n"
                "Then run `boring_rag_reload` to enable RAG.\n\n"
                "💡 [AI Action]: Execute the pip install command above, then call boring_rag_reload."
            )

        count = retriever.build_index(force=force)
        stats = retriever.get_stats()

        if stats.index_stats:
            idx = stats.index_stats
            msg = (
                f"✅ RAG Index {'rebuilt' if force else 'ready'}\n\n"
                f"📊 Statistics:\n"
                f"- Files indexed: {idx.total_files}\n"
                f"- Total chunks: {idx.total_chunks}\n"
                f"- Functions: {idx.functions}\n"
                f"- Classes: {idx.classes}\n"
                f"- Methods: {idx.methods}\n"
                f"- Script chunks: {getattr(idx, 'script_chunks', 0)}\n"
                f"- Skipped: {idx.skipped_files}\n\n"
                f"💡 Project root: {project_root}"
            )
            return create_success_result(
                msg, data={"stats": idx.model_dump() if hasattr(idx, "model_dump") else str(idx)}
            )

        return create_success_result(
            f"✅ RAG Index ready with {count} chunks for `{project_root}`", data={"chunks": count}
        )

    @mcp.tool(
        description="Semantic code search with RAG.",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    def boring_rag_search(
        query: Annotated[
            str,
            Field(
                description="Natural language query describing what code you're looking for. Uses semantic similarity search. Examples: 'authentication error handling', 'database connection pool', 'user login validation'. Be specific for better results."
            ),
        ],
        max_results: Annotated[
            int,
            Field(
                description="Maximum number of search results to return. Range: 1-50. Default: 10. Higher values provide more context but may include less relevant results."
            ),
        ] = 10,
        expand_graph: Annotated[
            bool,
            Field(
                description="If True, includes related code via dependency graph (callers/callees). Provides more comprehensive context. Default: True. Set to False for faster searches with only direct matches."
            ),
        ] = True,
        file_filter: Annotated[
            str,
            Field(
                description="Optional substring to filter results by file path. Only files containing this substring will be included. Examples: 'auth' (matches auth.py, authentication.py), 'src/api' (matches files in src/api/). Case-sensitive."
            ),
        ] = None,
        threshold: Annotated[
            float,
            Field(
                description="Minimum relevance score (0.0 to 1.0) for results. Higher values return only highly relevant matches. Default: 0.0 (all results). Recommended: 0.3-0.5 for quality filtering."
            ),
        ] = 0.0,
        verbosity: Annotated[
            str,
            Field(
                description="Output verbosity level. 'minimal': only file paths and scores (~20 tokens/result), 'standard': paths + truncated code snippets (~125 tokens/result), 'verbose': full code snippets (~200-500 tokens/result). Default: 'standard'."
            ),
        ] = "standard",
        project_path: Annotated[
            str,
            Field(
                description="Optional explicit path to project root. If not provided, automatically detects project root by searching for common markers (pyproject.toml, package.json, etc.) starting from current directory. Example: '.' or '/path/to/project'."
            ),
        ] = None,
        use_hyde: Annotated[
            bool,
            Field(
                description="Enable HyDE query expansion for better semantic alignment. Default: True."
            ),
        ] = True,
        use_rerank: Annotated[
            bool,
            Field(description="Enable high-precision Cross-Encoder reranking. Default: True."),
        ] = True,
    ) -> BoringResult:
        """
        Search the codebase using semantic RAG retrieval.

        Combines vector similarity search with dependency graph expansion
        to find the most relevant code for your query.

        Args:
            query: What you're looking for (e.g., "authentication error handling")
            max_results: Maximum number of results (default 10)
            expand_graph: Include related code via dependency graph (default True)
            file_filter: Filter by file path substring (e.g., "auth" or "src/api")
            threshold: Minimum relevance score (0.0 to 1.0)
            verbosity: Output detail level (minimal/standard/verbose)
            project_path: Optional explicit path to project root
            use_hyde: Enable HyDE expansion (default True)
            use_rerank: Enable Cross-Encoder reranking (default True)

        Returns:
            Formatted search results with code snippets
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message"))
        retriever = get_retriever(project_root)
        if retriever is None or not retriever.is_available:
            pip_cmd = f"{sys.executable} -m pip install boring-aicoding[vector]"
            return create_error_result(
                "❌ RAG module not available.\n\n"
                f"[AUTO-FIX] Run: {pip_cmd}\n\n"
                "Then run `boring_rag_reload` to enable RAG.\n\n"
                "💡 [AI Action]: Execute the pip install command above, then call boring_rag_reload."
            )

        # Enhanced index health check with diagnostics
        if retriever.collection:
            chunk_count = retriever.collection.count()
            if chunk_count == 0:
                return create_error_result(
                    "❌ RAG index is empty.\n\n"
                    "**Solution:** Run `boring_rag_index` first to index your codebase:\n"
                    "```\n"
                    "boring_rag_index(force=True)\n"
                    "```\n\n"
                    "**Tip:** Use `boring_rag_status` to check index health."
                )
        else:
            return create_error_result(
                "❌ RAG collection not initialized.\n\n"
                "**Solution:** Run `boring_rag_index` to create the index."
            )

        # Normalize file_filter for cross-platform compatibility
        if file_filter:
            file_filter = file_filter.replace("\\", "/")

        results = retriever.retrieve(
            query=query,
            n_results=max_results,
            expand_graph=expand_graph,
            file_filter=file_filter,
            threshold=threshold,
            use_hyde=use_hyde,
            use_rerank=use_rerank,
        )

        if not results:
            return create_success_result(
                message=f"🔍 No results found for: **{query}**\n\n"
                f"**Suggestions:**\n"
                f"- Try a different query\n"
                f"- Check if code exists in indexed files\n"
                f"- Run `boring_rag_status` to verify index health",
                data={"query": query, "results_count": 0, "results": []},
            )

        # Import verbosity helpers
        from boring.mcp.verbosity import Verbosity, get_verbosity

        verb_level = get_verbosity(verbosity)
        parts = [f"🔍 Found {len(results)} results for: **{query}**\n"]

        # Prepare structured data for tool consumer
        structured_results = []

        for i, result in enumerate(results, 1):
            chunk = result.chunk
            method = result.retrieval_method.upper()
            score = f"{result.score:.2f}"

            structured_results.append(
                {
                    "file": chunk.file_path,
                    "name": chunk.name,
                    "line_start": chunk.start_line,
                    "line_end": chunk.end_line,
                    "score": result.score,
                    "type": chunk.chunk_type,
                    "method": method,
                }
            )

            if verb_level == Verbosity.MINIMAL:
                # MINIMAL: Only file path, function name, and score
                parts.append(f"{i}. `{chunk.file_path}::{chunk.name}` (score: {score})\n")
            elif verb_level == Verbosity.STANDARD:
                # STANDARD: Current behavior (truncated at 500 chars)
                parts.append(
                    f"### {i}. [{method}] `{chunk.file_path}` → `{chunk.name}` (score: {score})\n"
                    f"Lines {chunk.start_line}-{chunk.end_line} | Type: {chunk.chunk_type}\n"
                    f"```python\n{chunk.content[:500]}{'...' if len(chunk.content) > 500 else ''}\n```\n"
                )
            else:  # VERBOSE
                # VERBOSE: Full code snippet (no truncation)
                parts.append(
                    f"### {i}. [{method}] `{chunk.file_path}` → `{chunk.name}` (score: {score})\n"
                    f"Lines {chunk.start_line}-{chunk.end_line} | Type: {chunk.chunk_type}\n"
                    f"**Full Code:**\n```python\n{chunk.content}\n```\n"
                )

        # Add verbosity hint for minimal mode
        if verb_level == Verbosity.MINIMAL:
            parts.append("\n💡 Use verbosity='standard' or 'verbose' to see code snippets.")

        return create_success_result(
            message="\n".join(parts),
            data={"query": query, "results_count": len(results), "results": structured_results},
        )

    @mcp.tool(
        description="Check RAG index health and statistics.",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    def boring_rag_status(
        project_path: Annotated[
            str,
            Field(
                description="Optional explicit path to project root. If not provided, automatically detects project root by searching for common markers (pyproject.toml, package.json, etc.) starting from current directory. Example: '.' or '/path/to/project'."
            ),
        ] = None,
    ) -> BoringResult:
        """
        Check RAG index health and provide diagnostic information.

        Use this to verify the index is properly built and contains expected data.

        Args:
            project_path: Optional explicit path to project root

        Returns:
            Comprehensive index health report
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message"))

        report = []  # Initialize report list
        retriever = get_retriever(project_root)
        if retriever is None or not retriever.is_available:
            pip_cmd = f"{sys.executable} -m pip install boring-aicoding[vector]"
            report.append("## ❌ RAG Not Available\n")
            report.append(f"[AUTO-FIX] Run:\n```bash\n{pip_cmd}\n```\n")
            report.append(
                "\n💡 [AI Action]: Execute the pip install command above, then call boring_rag_reload."
            )
            return create_error_result("\n".join(report))

        report.append("## ✅ ChromaDB Available\n")

        # Check collection status
        if retriever.collection:
            chunk_count = retriever.collection.count()
            report.append(f"**Indexed Chunks:** {chunk_count}\n")

            if chunk_count == 0:
                report.append("\n## ⚠️ Index Empty\n")
                report.append("Run `boring_rag_index(force=True)` to build the index.\n")
            else:
                report.append("\n## ✅ Index Healthy\n")

                # Get detailed stats
                stats = retriever.get_stats()
                if stats.index_stats:
                    idx = stats.index_stats
                    report.append(f"- **Files indexed:** {idx.total_files}\n")
                    report.append(f"- **Functions:** {idx.functions}\n")
                    report.append(f"- **Classes:** {idx.classes}\n")
                    report.append(f"- **Methods:** {idx.methods}\n")
                    report.append(f"- **Skipped:** {idx.skipped_files}\n")

                if stats.graph_stats:
                    graph = stats.graph_stats
                    report.append("\n**Dependency Graph:**\n")
                    report.append(f"- Nodes: {graph.total_nodes}\n")
                    report.append(f"- Edges: {graph.total_edges}\n")
        else:
            report.append("## ❌ Collection Not Initialized\n")
            report.append("Run `boring_rag_index` to create the index.\n")

        # Check persist directory
        if retriever.persist_dir.exists():
            report.append(f"\n**Persist Directory:** `{retriever.persist_dir}`\n")

        return create_success_result(
            "\n".join(report),
            data={"available": True, "chunk_count": chunk_count if retriever.collection else 0},
        )

    @mcp.tool(
        description="Get callers and callees for a function.",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    def boring_rag_context(
        file_path: Annotated[
            str,
            Field(
                description="Path to the file relative to project root. Example: 'src/auth/login.py' or 'app/models/user.py'. Must be a valid file path in the indexed codebase."
            ),
        ],
        function_name: Annotated[
            str,
            Field(
                description="Optional name of the function to get context for. If provided, returns callers and callees for this specific function. Example: 'authenticate_user' or 'process_payment'. Leave empty to get file-level context."
            ),
        ] = None,
        class_name: Annotated[
            str,
            Field(
                description="Optional name of the class if getting class-level context. Use when function_name is a method. Example: 'UserService' or 'PaymentProcessor'. Required when function_name is a class method."
            ),
        ] = None,
        project_path: Annotated[
            str,
            Field(
                description="Optional explicit path to project root. If not provided, automatically detects project root by searching for common markers (pyproject.toml, package.json, etc.) starting from current directory. Example: '.' or '/path/to/project'."
            ),
        ] = None,
    ) -> BoringResult:
        """
        Get comprehensive context for modifying a specific code location.

        Returns the target code plus:
        - Callers: Code that calls this (might break if you change it)
        - Callees: Code this depends on (need to understand the interface)
        - Siblings: Other methods in the same class

        Args:
            file_path: Path to the file (relative to project root)
            function_name: Name of the function to get context for
            class_name: Name of the class (if getting class context)
            project_path: Optional explicit path to project root

        Returns:
            Categorized code context for safe modification
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message"))
        retriever = get_retriever(project_root)
        if retriever is None or not retriever.is_available:
            pip_cmd = f"{sys.executable} -m pip install boring-aicoding[vector]"
            return create_error_result(
                "❌ RAG module not available.\n\n"
                f"[AUTO-FIX] Run: {pip_cmd}\n\n"
                "Then run `boring_rag_reload` to enable RAG.\n\n"
                "💡 [AI Action]: Execute the pip install command above, then call boring_rag_reload."
            )

        context = retriever.get_modification_context(
            file_path=file_path, function_name=function_name, class_name=class_name
        )

        parts = [f"📍 Context for `{function_name or class_name}` in `{file_path}`\n"]

        # Target
        if context["target"]:
            chunk = context["target"][0].chunk
            parts.append(f"## 🎯 Target\n```python\n{chunk.content}\n```\n")
        else:
            return create_error_result(
                f"❌ Could not find `{function_name or class_name}` in `{file_path}`"
            )

        # Callers (might break)
        if context["callers"]:
            parts.append(
                f"## ⚠️ Callers ({len(context['callers'])} - might break if you change the interface)\n"
            )
            for r in context["callers"][:5]:
                c = r.chunk
                parts.append(f"- `{c.file_path}` → `{c.name}` (L{c.start_line})\n")

        # Callees (dependencies)
        if context["callees"]:
            parts.append(
                f"## 📦 Dependencies ({len(context['callees'])} - understand these interfaces)\n"
            )
            for r in context["callees"][:5]:
                c = r.chunk
                sig = c.signature or c.content[:100]
                parts.append(f"- `{c.name}`: `{sig[:80]}...`\n")

        # Siblings
        if context["siblings"]:
            parts.append(f"## 👥 Sibling Methods ({len(context['siblings'])})\n")
            for r in context["siblings"][:5]:
                c = r.chunk
                parts.append(f"- `{c.name}` (L{c.start_line}-{c.end_line})\n")

        return create_success_result("\n".join(parts), data=context)

    @mcp.tool(
        description="Expand dependency context for a chunk.",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    def boring_rag_expand(
        chunk_id: Annotated[
            str,
            Field(
                description="The chunk ID to expand from, obtained from boring_rag_search results. Format: string identifier. Example: 'chunk_abc123' or 'func:auth:login'. Use this to get deeper dependency context for a specific code location."
            ),
        ],
        depth: Annotated[
            int,
            Field(
                description="Number of dependency layers to expand recursively. Range: 1-5. Default: 2. Higher values provide more comprehensive context but may include less relevant code. Use 1 for immediate dependencies only, 3-5 for deep dependency analysis."
            ),
        ] = 2,
        project_path: Annotated[
            str,
            Field(
                description="Optional explicit path to project root. If not provided, automatically detects project root by searching for common markers (pyproject.toml, package.json, etc.) starting from current directory. Example: '.' or '/path/to/project'."
            ),
        ] = None,
    ) -> BoringResult:
        """
        Smart expand: Get deeper dependency context for a specific chunk.

        Use this when 1-layer expansion isn't enough. The AI can request
        deeper traversal on-demand.

        Args:
            chunk_id: The chunk ID to expand from (from search results)
            depth: How many layers to expand (default 2)
            project_path: Optional explicit path to project root

        Returns:
            Additional related code chunks
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message"))
        retriever = get_retriever(project_root)
        if retriever is None or not retriever.is_available:
            pip_cmd = f"{sys.executable} -m pip install boring-aicoding[vector]"
            return create_error_result(
                "❌ RAG module not available.\n\n"
                f"[AUTO-FIX] Run: {pip_cmd}\n\n"
                "Then run `boring_rag_reload` to enable RAG.\n\n"
                "💡 [AI Action]: Execute the pip install command above, then call boring_rag_reload."
            )

        results = retriever.smart_expand(chunk_id, depth=depth)

        if not results:
            return create_success_result(
                f"🔍 No additional context found for chunk {chunk_id}", data={"results": []}
            )

        parts = [f"🔗 Smart Expand: +{len(results)} related chunks (depth={depth})\n"]

        for result in results[:10]:
            chunk = result.chunk
            parts.append(
                f"### `{chunk.file_path}` → `{chunk.name}`\n"
                f"```python\n{chunk.content[:300]}...\n```\n"
            )

        return create_success_result("\n".join(parts), data={"results_count": len(results)})

    @mcp.tool(
        description="Visualize the code dependency graph.",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    def boring_rag_graph(
        format: Annotated[
            str,
            Field(
                description="Output format: 'mermaid' (for diagrams) or 'json' (for data analysis). Default: 'mermaid'."
            ),
        ] = "mermaid",
        max_nodes: Annotated[
            int,
            Field(
                description="Maximum number of nodes to include in visualization to prevent overcrowding. Default: 50."
            ),
        ] = 50,
        project_path: Annotated[
            str,
            Field(
                description="Optional explicit path to project root. If not provided, automatically detects project root by searching for common markers (pyproject.toml, package.json, etc.) starting from current directory. Example: '.' or '/path/to/project'."
            ),
        ] = None,
    ) -> BoringResult:
        """
        Visualize the dependency graph.

        Returns a Mermaid diagram (flowchart) or JSON structure representing
        the relationship between code chunks (callers/callees).
        Useful for understanding system architecture.

        Args:
            format: "mermaid" or "json"
            max_nodes: Limit nodes for readability
            project_path: Optional project root

        Returns:
            Visualization string
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message"))
        retriever = get_retriever(project_root)
        if retriever is None or not retriever.is_available:
            pip_cmd = f"{sys.executable} -m pip install boring-aicoding[vector]"
            return create_error_result(
                "❌ RAG module not available.\n\n"
                f"[AUTO-FIX] Run: {pip_cmd}\n\n"
                "Then run `boring_rag_reload` to enable RAG.\n\n"
                "💡 [AI Action]: Execute the pip install command above, then call boring_rag_reload."
            )

        if not retriever.graph:
            # Force load if graph usage is requested
            retriever._load_chunks_from_db()
            if not retriever.graph:
                return create_error_result(
                    "❌ Graph not initialized or empty. Run `boring_rag_index` first."
                )

        viz = retriever.graph.visualize(format=format, max_nodes=max_nodes)

        if format == "mermaid":
            msg = (
                f"## 🕸️ Dependency Graph ({retriever.graph.get_stats().total_nodes} nodes)\n\n{viz}"
            )
            return create_success_result(msg, data={"format": "mermaid"})
        else:
            return create_success_result("Graph data generated.", data={"json": viz})

Shadow Mode (影子模式)

boring.mcp.tools.shadow

MCP Tools for Shadow Mode

Exposes Shadow Mode human-in-the-loop protection as MCP tools.

get_shadow_guard(project_root, mode='ENABLED')

Get or create Shadow Mode guard for a project.

Source code in src/boring/mcp/tools/shadow.py
def get_shadow_guard(project_root: Path, mode: str = "ENABLED") -> ShadowModeGuard:
    """Get or create Shadow Mode guard for a project."""
    # Normalize key to handle case/path variations
    key = str(project_root.resolve().absolute()).lower()
    if key not in _guards:
        _guards[key] = create_shadow_guard(project_root, mode=mode)
    return _guards[key]

register_shadow_tools(mcp, helpers)

Register Shadow Mode tools with the MCP server.

Parameters:

Name Type Description Default
mcp Any

FastMCP instance

required
helpers dict

Dict with helper functions

required
Source code in src/boring/mcp/tools/shadow.py
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
def register_shadow_tools(mcp: Any, helpers: dict):
    """
    Register Shadow Mode tools with the MCP server.

    Args:
        mcp: FastMCP instance
        helpers: Dict with helper functions
    """
    get_project_root_or_error = helpers.get("get_project_root_or_error")

    @mcp.tool(
        description="Get Shadow Mode status and pending operations",
        annotations={"readOnlyHint": True, "openWorldHint": False},
    )
    def boring_shadow_status(
        project_path: Annotated[
            str, Field(description="Optional explicit path to project root")
        ] = None,
    ) -> str:
        """
        Get Shadow Mode status and pending approvals.

        Shows:
        - Current protection level
        - Number of pending operations
        - Details of each pending operation

        Args:
            project_path: Optional explicit path to project root

        Returns:
            Shadow Mode status summary
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error.get("message")
        guard = get_shadow_guard(project_root)

        pending = guard.get_pending_operations()

        output = [
            "# 🛡️ Shadow Mode Status",
            "",
            f"**Mode:** {guard.mode.value}",
            f"**Pending Operations:** {len(pending)}",
            "",
        ]

        if guard.mode == ShadowModeLevel.ENABLED:
            output.insert(
                3,
                "> ℹ️ **Note:** In ENABLED mode, low-risk operations (e.g. file reads, minor edits) are **automatically approved**.",
            )

        if pending:
            output.append("## Pending Approvals")
            for op in pending:
                severity_icon = {"critical": "🔴", "high": "🟠", "medium": "🟡", "low": "🟢"}.get(
                    op.severity.value, "⚪"
                )

                output.append(
                    f"\n### {severity_icon} `{op.operation_id}`\n"
                    f"- **Type:** {op.operation_type}\n"
                    f"- **File:** `{op.file_path}`\n"
                    f"- **Severity:** {op.severity.value}\n"
                    f"- **Description:** {op.description}\n"
                    f"- **Time:** {op.timestamp}"
                )
        else:
            output.append("✅ No pending operations")

        return "\n".join(output)

    @mcp.tool(
        description="Approve a pending Shadow Mode operation",
        annotations={"readOnlyHint": False, "idempotentHint": True, "openWorldHint": False},
    )
    def boring_shadow_approve(
        operation_id: Annotated[
            str,
            Field(
                description="Unique identifier of the operation to approve. Get this from boring_shadow_status output. Format: UUID string. Example: '550e8400-e29b-41d4-a716-446655440000'."
            ),
        ],
        note: Annotated[
            str,
            Field(
                description="Optional human-readable note explaining why this operation is approved. Stored in operation history for audit trail. Example: 'Reviewed changes, safe to proceed'."
            ),
        ] = None,
        project_path: Annotated[
            str,
            Field(
                description="Optional explicit path to project root. If not provided, automatically detects project root by searching for common markers (pyproject.toml, package.json, etc.) starting from current directory. Example: '.' or '/path/to/project'."
            ),
        ] = None,
    ) -> str:
        """
        Approve a pending Shadow Mode operation.

        The operation will be allowed to proceed after approval.

        Args:
            operation_id: ID of the operation to approve
            note: Optional note explaining the approval
            project_path: Optional explicit path to project root
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error.get("message")
        guard = get_shadow_guard(project_root)

        if guard.approve_operation(operation_id, note):
            return f"✅ Operation `{operation_id}` approved" + (
                f" with note: {note}" if note else ""
            )
        else:
            return f"❌ Operation `{operation_id}` not found"

    @mcp.tool(
        description="Reject a pending Shadow Mode operation",
        annotations={"readOnlyHint": False, "idempotentHint": True, "openWorldHint": False},
    )
    def boring_shadow_reject(
        operation_id: Annotated[
            str,
            Field(
                description="Unique identifier of the operation to reject. Get this from boring_shadow_status output. Format: UUID string. Example: '550e8400-e29b-41d4-a716-446655440000'."
            ),
        ],
        note: Annotated[
            str,
            Field(
                description="Optional human-readable note explaining why this operation is rejected. Stored in operation history for audit trail. Example: 'Operation too risky, requires manual review'."
            ),
        ] = None,
        project_path: Annotated[
            str,
            Field(
                description="Optional explicit path to project root. If not provided, automatically detects project root by searching for common markers (pyproject.toml, package.json, etc.) starting from current directory. Example: '.' or '/path/to/project'."
            ),
        ] = None,
    ) -> str:
        """
        Reject a pending Shadow Mode operation.

        The operation will be blocked and removed from the queue.

        Args:
            operation_id: ID of the operation to reject
            note: Optional note explaining the rejection
            project_path: Optional explicit path to project root
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error.get("message")
        guard = get_shadow_guard(project_root)

        if guard.reject_operation(operation_id, note):
            return f"❌ Operation `{operation_id}` rejected" + (
                f" with note: {note}" if note else ""
            )
        else:
            return f"❓ Operation `{operation_id}` not found"

    @mcp.tool(
        description="Change Shadow Mode protection level",
        annotations={"readOnlyHint": False, "idempotentHint": True, "openWorldHint": False},
    )
    def boring_shadow_mode(
        mode: Annotated[
            str,
            Field(
                description="New protection level. Options: 'DISABLED' (no protection, not recommended), 'ENABLED' (auto-approve low-risk, require approval for high-risk, recommended default), 'STRICT' (require approval for all writes, recommended for production)."
            ),
        ],
        project_path: Annotated[
            str, Field(description="Optional explicit path to project root")
        ] = None,
    ) -> str:
        """
        Change Shadow Mode protection level.

        Modes:
        - **DISABLED**: All operations auto-approved (⚠️ dangerous)
        - **ENABLED**: Only HIGH/CRITICAL ops require approval (default)
        - **STRICT**: ALL write operations require approval

        Args:
            mode: New mode (DISABLED, ENABLED, or STRICT)
            project_path: Optional explicit path to project root
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error.get("message")

        # Validate mode
        mode_upper = mode.upper()
        if mode_upper not in ("DISABLED", "ENABLED", "STRICT"):
            return "❌ Invalid mode. Choose: DISABLED, ENABLED, or STRICT"

        # Update existing guard or create new one via singleton accessor
        try:
            level = ShadowModeLevel[mode_upper]
            # FIX: Use get_shadow_guard to ensure correct singleton key is used
            guard = get_shadow_guard(project_root)
            guard.mode = level

            mode_icons = {"DISABLED": "⚠️", "ENABLED": "🛡️", "STRICT": "🔒"}

            return f"{mode_icons.get(mode_upper, '✅')} Shadow Mode set to **{mode_upper}**"
        except Exception as e:
            return f"❌ Failed to set mode: {e}"

    @mcp.tool(
        description="Clear all pending Shadow Mode operations",
        annotations={"readOnlyHint": False, "destructiveHint": True},
    )
    def boring_shadow_clear(
        project_path: Annotated[
            str, Field(description="Optional explicit path to project root")
        ] = None,
    ) -> str:
        """
        Clear all pending Shadow Mode operations.

        Use this to reset the approval queue if operations are stale.

        Args:
            project_path: Optional explicit path to project root

        Returns:
            Count of cleared operations
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error.get("message")
        guard = get_shadow_guard(project_root)

        count = guard.clear_pending()
        return f"✅ Cleared {count} pending operations"

    @mcp.tool(
        description="Add or update a trust rule for auto-approving operations",
        annotations={"readOnlyHint": False, "idempotentHint": True, "openWorldHint": False},
    )
    def boring_shadow_trust(
        tool_name: Annotated[
            str,
            Field(
                description="Tool name to trust, e.g., 'boring_commit'. Use '*' to trust all tools (dangerous)."
            ),
        ],
        auto_approve: Annotated[
            bool,
            Field(description="Whether to auto-approve matching operations. Default: True"),
        ] = True,
        path_pattern: Annotated[
            str,
            Field(
                description="Optional glob pattern to limit trust to specific paths, e.g., 'src/*'. Leave empty for all paths."
            ),
        ] = None,
        max_severity: Annotated[
            str,
            Field(
                description="Maximum severity to auto-approve: 'low', 'medium', 'high'. Default: 'high'. Use 'critical' with extreme caution."
            ),
        ] = "high",
        description: Annotated[
            str,
            Field(description="Optional description for this rule"),
        ] = "",
        project_path: Annotated[
            str, Field(description="Optional explicit path to project root")
        ] = None,
    ) -> str:
        """
        Add a trust rule to auto-approve specific operations.

        Trust rules let you bypass approval prompts for operations you trust.
        Rules are persisted in `.boring_brain/trust_rules.json`.

        Examples:
        - Trust all commit operations: boring_shadow_trust("boring_commit")
        - Trust file writes in src/: boring_shadow_trust("boring_write_file", path_pattern="src/*")

        Args:
            tool_name: Tool to trust
            auto_approve: Whether to auto-approve (True) or explicitly block (False)
            path_pattern: Optional glob pattern for path filtering
            max_severity: Maximum severity level to auto-approve
            description: Optional description
            project_path: Optional explicit path to project root
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error.get("message")

        try:
            from boring.trust_rules import get_trust_manager

            manager = get_trust_manager(project_root)

            manager.add_rule(
                tool_name=tool_name,
                auto_approve=auto_approve,
                path_pattern=path_pattern or None,
                max_severity=max_severity,
                description=description,
            )

            action = "trusted" if auto_approve else "blocked"
            icon = "✅" if auto_approve else "🚫"
            path_info = f" (path: `{path_pattern}`)" if path_pattern else ""

            return f"{icon} Tool `{tool_name}` is now {action}{path_info} up to severity `{max_severity}`"
        except Exception as e:
            return f"❌ Failed to add trust rule: {e}"

    @mcp.tool(
        description="List all trust rules",
        annotations={"readOnlyHint": True, "openWorldHint": False},
    )
    def boring_shadow_trust_list(
        project_path: Annotated[
            str, Field(description="Optional explicit path to project root")
        ] = None,
    ) -> str:
        """
        List all trust rules configured for this project.

        Returns:
            List of trust rules with their settings
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error.get("message")

        try:
            from boring.trust_rules import get_trust_manager

            manager = get_trust_manager(project_root)

            rules = manager.list_rules()

            if not rules:
                return "📋 No trust rules configured. Use `boring_shadow_trust` to add rules."

            output = ["# 📋 Trust Rules", ""]
            for i, rule in enumerate(rules, 1):
                icon = "✅" if rule.get("auto_approve") else "🚫"
                path = f" (path: `{rule.get('path_pattern')}`)" if rule.get("path_pattern") else ""
                desc = f" — {rule.get('description')}" if rule.get("description") else ""
                output.append(
                    f"{i}. {icon} **`{rule.get('tool_name')}`**{path} "
                    f"[max: {rule.get('max_severity')}]{desc}"
                )

            return "\n".join(output)
        except Exception as e:
            return f"❌ Failed to list trust rules: {e}"

    @mcp.tool(
        description="Remove a trust rule",
        annotations={"readOnlyHint": False, "destructiveHint": True, "openWorldHint": False},
    )
    def boring_shadow_trust_remove(
        tool_name: Annotated[
            str,
            Field(description="Tool name of the rule to remove"),
        ],
        path_pattern: Annotated[
            str,
            Field(description="Path pattern of the rule to remove (must match exactly)"),
        ] = None,
        project_path: Annotated[
            str, Field(description="Optional explicit path to project root")
        ] = None,
    ) -> str:
        """
        Remove a trust rule.

        Args:
            tool_name: Tool name of the rule to remove
            path_pattern: Path pattern to match (if rule has one)
            project_path: Optional explicit path to project root
        """
        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error.get("message")

        try:
            from boring.trust_rules import get_trust_manager

            manager = get_trust_manager(project_root)

            if manager.remove_rule(tool_name, path_pattern or None):
                return f"✅ Removed trust rule for `{tool_name}`"
            else:
                return f"❓ No matching trust rule found for `{tool_name}`"
        except Exception as e:
            return f"❌ Failed to remove trust rule: {e}"

Vibe Tools (程式碼感知)

boring.mcp.tools.vibe

Vibe Coder Pro Tools - 讓 Vibe Coder 程式碼達到工程師水準。

包含: - boring_test_gen: 自動生成單元測試 (支援多語言) + RAG 風格參考 - boring_code_review: AI 程式碼審查 (支援多語言) + BrainManager Pattern 整合 - boring_vibe_check: 遊戲化健檢 (整合 Lint, Security, Doc) + Storage 歷史追蹤 - boring_impact_check: 衝擊分析 (多層依賴追蹤) + RAG 語義分析

V10.21 整合: - BrainManager: 參考已學習的 Pattern 進行審查 - RAG: 語義搜尋現有測試風格、依賴分析 - Storage: 記錄 Vibe Score 歷史趨勢

移植自 vibe_tools.py (V10.26.0)

get_vibe_engine()

Get the VibeEngine singleton, initializing it lazily.

Source code in src/boring/mcp/tools/vibe.py
def get_vibe_engine():
    """Get the VibeEngine singleton, initializing it lazily."""
    global _vibe_engine
    if _vibe_engine is None:
        from ...vibe.engine import VibeEngine
        from ...vibe.handlers.generic import GenericHandler
        from ...vibe.handlers.javascript import JavascriptHandler
        from ...vibe.handlers.python import PythonHandler

        _vibe_engine = VibeEngine()
        _vibe_engine.register_handler(PythonHandler())
        _vibe_engine.register_handler(JavascriptHandler())
        _vibe_engine.register_handler(GenericHandler())

    return _vibe_engine

run_vibe_check(target_path='.', project_path=None, max_files=10, verbosity='standard')

Module-level Vibe Check implementation. Can be called directly by FlowEngine or other components.

Source code in src/boring/mcp/tools/vibe.py
def run_vibe_check(
    target_path: str = ".",
    project_path: str | None = None,
    max_files: int = 10,
    verbosity: str = "standard",
) -> BoringResult:
    """
    Module-level Vibe Check implementation.
    Can be called directly by FlowEngine or other components.
    """
    root_str, error = _get_project_root_or_error_impl(project_path)
    if error:
        return create_error_result(error.get("message", "Unknown error"))

    project_root = Path(root_str)
    # Handle both absolute and relative paths
    if target_path.startswith("/") or (len(target_path) > 1 and target_path[1] == ":"):
        # Absolute path (Unix-style or Windows-style)
        target = Path(target_path)
    elif target_path == ".":
        target = project_root
    else:
        # Relative path
        target = project_root / target_path

    if not target.exists():
        return create_error_result(f"❌ 找不到目標: {target}")

    # 1. 收集檔案
    files_to_check = []
    if target.is_file():
        files_to_check.append(target)
    else:
        candidates = [
            p
            for p in target.rglob("*")
            if p.is_file()
            and p.suffix in [".py", ".js", ".ts"]
            and not any(x in p.parts for x in ["node_modules", ".git", "venv"])
        ][:max_files]
        files_to_check.extend(candidates)

    if not files_to_check:
        return create_error_result("⚠️ 找不到可分析的程式碼檔案 (.py, .js, .ts)")

    # Scoring Variables
    base_score = 100
    deductions = 0
    issues_found = []
    doc_missing = 0
    security_issues = []

    # Lazy load engine
    engine = get_vibe_engine()

    # 2. 逐檔分析
    for f in files_to_check:
        try:
            content = f.read_text(encoding="utf-8", errors="ignore")

            # A. Code Review (Lint/Quality)
            rev_res = engine.perform_code_review(str(f), content, focus="all")
            for issue in rev_res.issues:
                deduction = (
                    5 if issue.severity == "low" else 10 if issue.severity == "medium" else 15
                )
                deductions += deduction
                issues_found.append(f"[{f.name}:{issue.line}] {issue.message}")

            # B. Doc Check
            doc_res = engine.extract_documentation(str(f), content)
            for item in doc_res.items:
                if not item.docstring:
                    deductions += 5
                    doc_missing += 1

        except Exception:
            continue

    # 3. Security Scan (Phase 14 Enhancement)
    try:
        scanner = SecurityScanner(project_root)
        sec_report = scanner.scan_for_secrets(target if target.is_dir() else target.parent)
        for sec_issue in sec_report.issues:
            severity_deduction = (
                20
                if sec_issue.severity == "CRITICAL"
                else 15
                if sec_issue.severity == "HIGH"
                else 10
            )
            deductions += severity_deduction
            security_issues.append(
                f"🔒 [{sec_issue.severity}] {sec_issue.description} ({sec_issue.file_path}:{sec_issue.line_number})"
            )
    except Exception:
        pass  # Security scan is optional enhancement

    # 3.5 Memory Injection (Phase 20 Enhancement)
    brain_advice = []
    try:
        from ...intelligence import BrainManager

        brain = BrainManager(project_root)

        seen_patterns = set()
        for issue in issues_found:
            msg_part = issue.split("] ", 1)[-1] if "] " in issue else issue
            patterns = brain.match_error_pattern(msg_part)
            if patterns:
                best_match = patterns[0]
                if best_match.pattern_id not in seen_patterns:
                    brain_advice.append(
                        f"🧠 Brain Recall: For '{msg_part}', previously solving by: {best_match.solution}"
                    )
                    seen_patterns.add(best_match.pattern_id)
                    deductions -= 5
    except Exception as e:
        logger.debug("Brain pattern matching failed: %s", e)

    # 4. 計算分數
    final_score = max(0, base_score - deductions)

    # 5. 評級
    if final_score >= 95:
        tier = "S-Tier (God Like) 🏆"
    elif final_score >= 85:
        tier = "A-Tier (Professional) 🥇"
    elif final_score >= 75:
        tier = "B-Tier (Solid) 🥈"
    elif final_score >= 60:
        tier = "C-Tier (Meh) 🥉"
    else:
        tier = "F-Tier (Spaghetti) 🍝"

    # 6. 生成 One-Click Fix Prompt
    fix_prompt = ""
    if final_score < 100:
        fix_prompt = f"Please act as a Senior Engineer to fix the low Vibe Score ({final_score}) for the following files:\n"
        fix_prompt += f"Target: `{target_path}`\n\n"
        fix_prompt += "Tasks:\n"

        if issues_found:
            fix_prompt += "1. Fix the following code quality issues:\n"
            for i in issues_found[:10]:
                fix_prompt += f"   - {i}\n"
            if len(issues_found) > 10:
                fix_prompt += f"   - ... and {len(issues_found) - 10} more issues.\n"

        if doc_missing > 0:
            fix_prompt += f"2. Add missing docstrings/JSDoc to {doc_missing} functions/classes to meet Google Style Guide.\n"

        if security_issues:
            fix_prompt += "3. ⚠️ CRITICAL: Remove or rotate the following exposed secrets:\n"
            for sec in security_issues[:5]:
                fix_prompt += f"   - {sec}\n"

        fix_prompt += "\nReturn the corrected code directly."
    else:
        fix_prompt = "🎉 Perfect Score! No fixes needed. Maybe go touch some grass? 🌱"

    # 7. V10.21: Storage 歷史追蹤
    score_trend = ""
    previous_score = None
    storage = _get_storage(project_root)
    if storage:
        try:
            # 記錄本次分數
            storage.record_metric(
                name="vibe_score",
                value=float(final_score),
                metadata={
                    "target": target_path,
                    "issues": len(issues_found),
                    "doc_missing": doc_missing,
                    "security_issues": len(security_issues),
                    "tier": tier,
                },
            )

            # 取得歷史分數
            history = storage.get_metrics("vibe_score", limit=5)
            if len(history) > 1:
                previous_score = history[1].get("metric_value")
                if previous_score is not None:
                    diff = final_score - previous_score
                    if diff > 0:
                        score_trend = f"📈 +{diff:.0f} (vs 上次 {previous_score:.0f})"
                    elif diff < 0:
                        score_trend = f"📉 {diff:.0f} (vs 上次 {previous_score:.0f})"
                    else:
                        score_trend = f"➡️ 維持 {previous_score:.0f}"
        except Exception as e:
            logger.debug("Failed to track vibe score history: %s", e)

    storage_status = "✅ 分數已記錄" if storage else "⚠️ Storage 未啟用"

    # Import verbosity control
    from boring.mcp.verbosity import Verbosity, get_verbosity

    verb_level = get_verbosity(verbosity)

    # Generate vibe_summary based on verbosity
    if verb_level == Verbosity.MINIMAL:
        # MINIMAL: Only score and tier (~50 tokens)
        vibe_summary = f"📊 Vibe Score: {final_score}/100 | {tier}"
        if score_trend:
            vibe_summary += f"\n{score_trend}"
        vibe_summary += "\n💡 Use verbosity='standard' for details"

    elif verb_level == Verbosity.VERBOSE:
        # VERBOSE: Full detailed report (~800+ tokens)
        summary_lines = [
            f"📊 Vibe Check: `{target_path}`",
            f"Score: {final_score}/100 | {tier}",
            "",
        ]

        if score_trend:
            summary_lines.append(f"{score_trend}\n")

        # Code Quality Issues
        if issues_found:
            summary_lines.append(f"🔍 Code Quality Issues ({len(issues_found)}):")
            for issue in issues_found[:20]:  # Show up to 20
                summary_lines.append(f"  - {issue}")
            if len(issues_found) > 20:
                summary_lines.append(f"  ... and {len(issues_found) - 20} more")
            summary_lines.append("")

        # Security Issues
        if security_issues:
            summary_lines.append(f"🔒 Security Issues ({len(security_issues)}):")
            for sec in security_issues[:10]:
                summary_lines.append(f"  - {sec}")
            if len(security_issues) > 10:
                summary_lines.append(f"  ... and {len(security_issues) - 10} more")
            summary_lines.append("")

        # Documentation
        if doc_missing > 0:
            summary_lines.append(f"📝 Documentation: {doc_missing} missing docstrings")
            summary_lines.append("")

        summary_lines.append(f"🔗 {storage_status}")
        vibe_summary = "\n".join(summary_lines)

    else:  # STANDARD
        # STANDARD: Score + top issues (~300 tokens)
        summary_lines = [f"📊 Vibe Score: {final_score}/100 | {tier}", ""]

        if score_trend:
            summary_lines.append(f"{score_trend}\n")

        # Top 5 quality issues
        if issues_found:
            summary_lines.append(
                f"🔍 Top Issues ({min(5, len(issues_found))}/{len(issues_found)}):"
            )
            for issue in issues_found[:5]:
                summary_lines.append(f"  - {issue}")
            if len(issues_found) > 5:
                summary_lines.append(f"  ... and {len(issues_found) - 5} more")
            summary_lines.append("")

        # Critical security issues
        if security_issues:
            critical_sec = [s for s in security_issues if "CRITICAL" in s or "HIGH" in s]
            if critical_sec:
                summary_lines.append(f"🔒 Critical Security ({len(critical_sec)}):")
                for sec in critical_sec[:3]:
                    summary_lines.append(f"  - {sec}")
                summary_lines.append("")

        if doc_missing > 0:
            summary_lines.append(f"📝 {doc_missing} missing docstrings\n")

        summary_lines.append(f"🔗 {storage_status}")
        summary_lines.append("\n💡 Use verbosity='verbose' for full report")
        vibe_summary = "\n".join(summary_lines)

    return create_success_result(
        message=vibe_summary,
        data={
            "vibe_score": final_score,
            "tier": tier,
            "vibe_summary": vibe_summary,
        },
    )

run_predict_errors(file_path, limit=5, project_path=None)

🔮 預測錯誤 - 根據歷史模式預測可能發生的錯誤。 Implementation for boring_predict_errors tool and CLI.

V10.22 Intelligence: - 分析檔案類型與過去錯誤的關聯 - 提供信心分數和預防建議 - 學習專案特定的錯誤模式

Source code in src/boring/mcp/tools/vibe.py
def run_predict_errors(
    file_path: str,
    limit: int = 5,
    project_path: str | None = None,
) -> BoringResult:
    """
    🔮 預測錯誤 - 根據歷史模式預測可能發生的錯誤。
    Implementation for boring_predict_errors tool and CLI.

    V10.22 Intelligence:
    - 分析檔案類型與過去錯誤的關聯
    - 提供信心分數和預防建議
    - 學習專案特定的錯誤模式
    """
    project_root_str, error = _get_project_root_or_error_impl(project_path)
    if error:
        return create_error_result(error.get("message", "Unknown error"))

    project_root = Path(project_root_str)
    # Ensure file_path is a string (fix for OptionInfo crash)
    file_path = str(file_path or ".")

    # Fast-Fail Auth Check (V14.0.1)
    try:
        from ...llm.sdk import create_gemini_client

        # This will raise ValueError if no auth is found
        _ = create_gemini_client(log_dir=project_root / "logs")
    except (ValueError, RuntimeError) as e:
        return create_error_result(
            f"🚫 Authentication required for Predictive Intelligence.\n{str(e)}",
            error_details="AUTH_REQUIRED",
        )

    # Try to use PredictiveAnalyzer
    predictions = []
    try:
        from ...intelligence import PredictiveAnalyzer

        analyzer = PredictiveAnalyzer(project_root)
        predictions = analyzer.predict_errors(file_path, limit)
    except ImportError as e:
        logger.debug("PredictiveAnalyzer not available: %s", e)

    # Fallback to storage-based prediction
    if not predictions:
        storage = _get_storage(project_root)
        if storage:
            predictions_data = storage.get_error_predictions(file_path, limit)
            for p in predictions_data:
                predictions.append(type("Prediction", (), p)())

    if not predictions:
        return create_error_result(
            f"📊 尚無足夠歷史資料進行預測。繼續使用系統累積資料!\n檔案: {file_path}",
            error_details="NO_DATA",
        )

    # Format results
    result_items = []
    for p in predictions:
        result_items.append(
            {
                "error_type": p.error_type
                if hasattr(p, "error_type")
                else p.get("error_type", "Unknown"),
                "confidence": getattr(p, "confidence", p.get("confidence", 0.5)),
                "message": getattr(p, "predicted_message", p.get("message", "")),
                "prevention_tip": getattr(p, "prevention_tip", p.get("prevention_tip", "")),
                "frequency": getattr(p, "historical_frequency", p.get("frequency", 0)),
            }
        )

    # Static analysis (Predictor)
    static_items = []
    try:
        from ...intelligence.predictor import Predictor

        predictor = Predictor(project_root)
        static_issues = predictor.analyze_file(Path(file_path))

        for issue in static_issues:
            static_items.append(
                {
                    "severity": issue.severity,
                    "category": issue.category,
                    "message": issue.message,
                    "file_path": issue.file_path,
                    "line_number": issue.line_number,
                    "code_snippet": issue.code_snippet,
                    "confidence": issue.confidence,
                    "pattern_id": issue.pattern_id,
                    "suggested_fix": issue.suggested_fix,
                }
            )
    except Exception:
        static_items = []

    # Build summary
    top = result_items[0] if result_items else None
    summary = f"🔮 **Error Predictions for** `{file_path}`\n\n"
    for i, item in enumerate(result_items[:5], 1):
        conf_bar = (
            "🟢" if item["confidence"] >= 0.7 else "🟡" if item["confidence"] >= 0.4 else "⚪"
        )
        summary += f"{i}. {conf_bar} **{item['error_type']}** ({item['confidence'] * 100:.0f}% confidence)\n"
        summary += f"   💡 {item['prevention_tip']}\n"
    if static_items:
        summary += f"\n🧠 **Static Findings**: {len(static_items)} issue(s) detected."

    return create_success_result(
        message=summary,
        data={
            "predictions": result_items,
            "top_prediction": top,
            "file_path": file_path,
            "static_issues": static_items,
            "vibe_summary": summary,
        },
    )

register_vibe_tools(mcp, audited, helpers, engine=None, brain_manager_factory=None)

Register Vibe Coder Pro tools with the MCP server.

Source code in src/boring/mcp/tools/vibe.py
 538
 539
 540
 541
 542
 543
 544
 545
 546
 547
 548
 549
 550
 551
 552
 553
 554
 555
 556
 557
 558
 559
 560
 561
 562
 563
 564
 565
 566
 567
 568
 569
 570
 571
 572
 573
 574
 575
 576
 577
 578
 579
 580
 581
 582
 583
 584
 585
 586
 587
 588
 589
 590
 591
 592
 593
 594
 595
 596
 597
 598
 599
 600
 601
 602
 603
 604
 605
 606
 607
 608
 609
 610
 611
 612
 613
 614
 615
 616
 617
 618
 619
 620
 621
 622
 623
 624
 625
 626
 627
 628
 629
 630
 631
 632
 633
 634
 635
 636
 637
 638
 639
 640
 641
 642
 643
 644
 645
 646
 647
 648
 649
 650
 651
 652
 653
 654
 655
 656
 657
 658
 659
 660
 661
 662
 663
 664
 665
 666
 667
 668
 669
 670
 671
 672
 673
 674
 675
 676
 677
 678
 679
 680
 681
 682
 683
 684
 685
 686
 687
 688
 689
 690
 691
 692
 693
 694
 695
 696
 697
 698
 699
 700
 701
 702
 703
 704
 705
 706
 707
 708
 709
 710
 711
 712
 713
 714
 715
 716
 717
 718
 719
 720
 721
 722
 723
 724
 725
 726
 727
 728
 729
 730
 731
 732
 733
 734
 735
 736
 737
 738
 739
 740
 741
 742
 743
 744
 745
 746
 747
 748
 749
 750
 751
 752
 753
 754
 755
 756
 757
 758
 759
 760
 761
 762
 763
 764
 765
 766
 767
 768
 769
 770
 771
 772
 773
 774
 775
 776
 777
 778
 779
 780
 781
 782
 783
 784
 785
 786
 787
 788
 789
 790
 791
 792
 793
 794
 795
 796
 797
 798
 799
 800
 801
 802
 803
 804
 805
 806
 807
 808
 809
 810
 811
 812
 813
 814
 815
 816
 817
 818
 819
 820
 821
 822
 823
 824
 825
 826
 827
 828
 829
 830
 831
 832
 833
 834
 835
 836
 837
 838
 839
 840
 841
 842
 843
 844
 845
 846
 847
 848
 849
 850
 851
 852
 853
 854
 855
 856
 857
 858
 859
 860
 861
 862
 863
 864
 865
 866
 867
 868
 869
 870
 871
 872
 873
 874
 875
 876
 877
 878
 879
 880
 881
 882
 883
 884
 885
 886
 887
 888
 889
 890
 891
 892
 893
 894
 895
 896
 897
 898
 899
 900
 901
 902
 903
 904
 905
 906
 907
 908
 909
 910
 911
 912
 913
 914
 915
 916
 917
 918
 919
 920
 921
 922
 923
 924
 925
 926
 927
 928
 929
 930
 931
 932
 933
 934
 935
 936
 937
 938
 939
 940
 941
 942
 943
 944
 945
 946
 947
 948
 949
 950
 951
 952
 953
 954
 955
 956
 957
 958
 959
 960
 961
 962
 963
 964
 965
 966
 967
 968
 969
 970
 971
 972
 973
 974
 975
 976
 977
 978
 979
 980
 981
 982
 983
 984
 985
 986
 987
 988
 989
 990
 991
 992
 993
 994
 995
 996
 997
 998
 999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
1556
1557
1558
1559
1560
1561
1562
1563
1564
1565
1566
1567
1568
1569
1570
1571
1572
1573
1574
1575
1576
1577
1578
1579
1580
1581
1582
1583
1584
1585
1586
1587
1588
1589
1590
1591
1592
1593
1594
1595
1596
1597
1598
1599
1600
1601
1602
1603
1604
1605
1606
1607
1608
1609
1610
1611
1612
1613
1614
1615
1616
1617
1618
1619
1620
1621
1622
1623
1624
1625
1626
1627
1628
1629
1630
1631
1632
1633
1634
1635
1636
1637
1638
1639
1640
1641
1642
1643
1644
1645
1646
1647
1648
1649
1650
1651
1652
1653
1654
1655
1656
1657
1658
1659
1660
1661
1662
1663
1664
1665
1666
1667
1668
1669
1670
1671
1672
1673
1674
1675
1676
1677
1678
1679
1680
1681
1682
1683
1684
1685
1686
1687
1688
1689
1690
1691
1692
1693
1694
1695
1696
1697
1698
1699
1700
1701
1702
1703
1704
def register_vibe_tools(mcp, audited, helpers, engine=None, brain_manager_factory=None):
    """
    Register Vibe Coder Pro tools with the MCP server.
    """
    _get_project_root_or_error = helpers["get_project_root_or_error"]
    global_engine = engine or get_vibe_engine()
    _get_brain = brain_manager_factory or _get_brain_manager

    # === boring_test_gen ===
    @mcp.tool(
        description="自動生成單元測試 (Auto-generate unit tests). "
        "說: '幫我寫測試', '生成 auth.py 的測試', 'Generate tests for api.ts'. "
        "我會分析程式碼並生成 pytest/jest 測試案例!支援 Python, JS, TS. "
        "🆕 V10.21: 整合 RAG 參考現有測試風格!",
        annotations={"readOnlyHint": False, "openWorldHint": False, "idempotentHint": False},
    )
    @audited
    def boring_test_gen(
        file_path: Annotated[str, Field(description="要生成測試的檔案路徑 (相對或絕對)")],
        output_dir: Annotated[
            str | None, Field(description="測試輸出目錄 (預設: tests/unit/ 或 tests/)")
        ] = None,
        project_path: Annotated[str | None, Field(description="專案根目錄 (自動偵測)")] = None,
    ) -> BoringResult:
        """
        🧪 自動生成單元測試 - 分析檔案並生成建議測試程式碼。
        支援平台: Python (pytest), JavaScript/TypeScript (jest/vitest)

        V10.21 整合:
        - RAG 搜尋現有測試,參考專案測試風格
        - 生成一致性更高的測試程式碼
        """
        project_root, error = _get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message", "Unknown error"))

        # 解析檔案路徑
        target_file = Path(file_path)
        if not target_file.is_absolute():
            target_file = project_root / file_path

        if not target_file.exists():
            return create_error_result(f"❌ 找不到檔案: {file_path}")

        try:
            # 1. 使用 Engine 分析
            source = target_file.read_text(encoding="utf-8")
            result = global_engine.analyze_for_test_gen(str(target_file), source)

            if not result.functions and not result.classes:
                return create_success_result(
                    message="😅 沒有找到可測試的導出函式或類別", data={"file": str(target_file)}
                )

            # 2. V10.21: RAG 搜尋現有測試風格
            test_style_hints = []
            rag = _get_rag_retriever(project_root)
            if rag:
                try:
                    # 搜尋現有測試檔案
                    existing_tests = rag.retrieve(
                        query=f"test {target_file.stem} pytest unittest",
                        top_k=3,
                        chunk_types=["function"],
                    )
                    if existing_tests:
                        for r in existing_tests[:2]:
                            if "test_" in r.chunk.name.lower():
                                test_style_hints.append(f"# 參考現有測試: {r.chunk.name}")
                except Exception:
                    pass  # RAG is optional enhancement

            # 3. 生成測試程式碼 (with style hints)
            test_code = global_engine.generate_test_code(result, str(project_root))

            # Prepend style hints if available
            if test_style_hints:
                style_comment = "\n".join(test_style_hints)
                test_code = f"# V10.21: 已參考現有測試風格\n{style_comment}\n\n{test_code}"

            # 4. 決定輸出路徑
            if output_dir:
                test_dir = project_root / output_dir
            else:
                # 自動判斷預設目錄
                if result.source_language == "python":
                    test_dir = project_root / "tests" / "unit"
                else:
                    test_dir = project_root / "tests"

            test_dir.mkdir(parents=True, exist_ok=True)
            test_filename = (
                f"test_{target_file.stem}.py"
                if result.source_language == "python"
                else f"{target_file.stem}.test{target_file.suffix}"
            )
            test_file = test_dir / test_filename

            # 5. 寫入測試檔案
            test_file.write_text(test_code, encoding="utf-8")

            rag_status = "✅ RAG 風格參考" if test_style_hints else "⚠️ RAG 未啟用"

            return create_success_result(
                message=f"✅ 已生成 {result.source_language} 測試!\n檔案: {test_file.relative_to(project_root)}\n{rag_status}",
                data={
                    "test_file": str(test_file),
                    "functions_count": len(result.functions),
                    "classes_count": len(result.classes),
                    "rag_enhanced": bool(test_style_hints),
                    "vibe_summary": f"🧪 為 `{target_file.name}` 生成了 {len(result.functions)} 個測試\n"
                    f"📁 測試檔案: `{test_file.relative_to(project_root)}`\n"
                    f"🌐 語言: {result.source_language}\n"
                    f"🔗 {rag_status}",
                },
            )

        except ValueError as e:
            return create_error_result(
                f"❌ 不支援的檔案類型: {target_file.suffix}", error_details=str(e)
            )
        except Exception as e:
            return create_error_result(f"❌ 分析失敗: {str(e)}")

    # === boring_code_review ===
    @mcp.tool(
        description="AI 程式碼審查 (AI Code Review). "
        "說: '審查我的程式碼', 'Review my code', '幫我看看哪裡可以改進'. "
        "我會分析程式碼品質並給出改善建議!支援 Python, JS, TS. "
        "🆕 V10.21: 整合 BrainManager 參考已學習的 Pattern!",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    @audited
    def boring_code_review(
        file_path: Annotated[str, Field(description="要審查的檔案路徑")],
        focus: Annotated[
            str | None,
            Field(
                description="審查重點: 'all', 'naming', 'error_handling', 'performance', 'security'"
            ),
        ] = "all",
        verbosity: Annotated[
            str,
            Field(
                description="Output verbosity: 'minimal' (summary only ~100 tokens), 'standard' (categorized issues ~500 tokens), 'verbose' (full details with brain patterns ~1000+ tokens). Default: 'standard'."
            ),
        ] = "standard",
        project_path: Annotated[str | None, Field(description="專案根目錄 (自動偵測)")] = None,
    ) -> BoringResult:
        """
        🔍 AI 程式碼審查 - 分析程式碼品質並給出改善建議。
        支援平台: Python, JavaScript, TypeScript

        V10.21 整合:
        - BrainManager: 參考專案已學習的 Pattern,審查更精準
        - 歷史錯誤模式: 識別曾經犯過的錯誤
        """
        project_root, error = _get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message", "Unknown error"))

        target_file = Path(file_path)
        if not target_file.is_absolute():
            target_file = project_root / file_path

        if not target_file.exists():
            return create_error_result(f"❌ 找不到檔案: {file_path}")

        try:
            source = target_file.read_text(encoding="utf-8")
            result = global_engine.perform_code_review(str(target_file), source, focus)

            # V10.21: BrainManager 整合 - 取得相關 Pattern
            brain_patterns = []
            brain = _get_brain(project_root)
            if brain:
                try:
                    # 搜尋與審查相關的 Pattern
                    patterns = brain.get_relevant_patterns(
                        context=f"{focus} review {target_file.name}", limit=3
                    )
                    for p in patterns:
                        if p.get("pattern_type") in ["code_style", "error_solution", "code_fix"]:
                            brain_patterns.append(
                                {
                                    "type": p.get("pattern_type"),
                                    "description": p.get("description", "")[:100],
                                    "suggestion": p.get("solution", "")[:150],
                                }
                            )
                except Exception:
                    pass  # BrainManager is optional enhancement

            if not result.issues:
                brain_status = (
                    f"\n🧠 已參考 {len(brain_patterns)} 個專案 Pattern" if brain_patterns else ""
                )
                message = f"✅ 程式碼品質良好!沒有發現明顯問題。{brain_status}"

                if is_minimal(verbosity):
                    message = f"✅ {target_file.name}: 無問題"

                return create_success_result(
                    message=message,
                    data={
                        "file": str(target_file),
                        "issues_count": 0,
                        "brain_patterns_used": len(brain_patterns),
                        "vibe_summary": message,
                    },
                )

            # 按嚴重程度排序
            result.issues.sort(key=lambda x: {"high": 0, "medium": 1, "low": 2}.get(x.severity, 3))

            # Format output based on verbosity
            if is_minimal(verbosity):
                # MINIMAL: Only summary statistics
                severity_counts = {"high": 0, "medium": 0, "low": 0}
                for issue in result.issues:
                    severity_counts[issue.severity] = severity_counts.get(issue.severity, 0) + 1

                summary_lines = [
                    f"🔍 {target_file.name}: {len(result.issues)} 問題",
                    f"🔴 High: {severity_counts['high']} | 🟡 Medium: {severity_counts['medium']} | 🟢 Low: {severity_counts['low']}",
                ]
                if brain_patterns:
                    summary_lines.append(f"🧠 {len(brain_patterns)} patterns")

                summary_lines.append("💡 Use verbosity='standard' for details")

            elif is_verbose(verbosity):
                # VERBOSE: Full details with brain patterns
                summary_lines = [f"🔍 Code Review: `{target_file.name}`", ""]
                for i, issue in enumerate(result.issues, 1):  # Show ALL issues in verbose
                    severity_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(
                        issue.severity, "⚪"
                    )
                    summary_lines.append(
                        f"{i}. {severity_icon} **{issue.category}** (Line {issue.line}): {issue.message}"
                    )
                    if issue.suggestion:
                        summary_lines.append(f"   💡 建議: {issue.suggestion}")

                # Include all brain patterns in verbose
                if brain_patterns:
                    summary_lines.append("")
                    summary_lines.append(
                        f"🧠 **專案 Pattern 建議** ({len(brain_patterns)} patterns):"
                    )
                    for bp in brain_patterns:
                        summary_lines.append(f"   - [{bp['type']}] {bp['description']}")
                        summary_lines.append(f"     → {bp['suggestion']}")
            else:
                # STANDARD: Current behavior (top 10 issues)
                summary_lines = [f"🔍 Code Review: `{target_file.name}`", ""]
                for i, issue in enumerate(result.issues[:10], 1):
                    severity_icon = {"high": "🔴", "medium": "🟡", "low": "🟢"}.get(
                        issue.severity, "⚪"
                    )
                    summary_lines.append(
                        f"{i}. {severity_icon} **{issue.category}**: {issue.message}"
                    )
                    if issue.suggestion:
                        summary_lines.append(f"   💡 建議: {issue.suggestion}")

                # V10.21: 加入 Brain Pattern 建議
                if brain_patterns:
                    summary_lines.append("")
                    summary_lines.append("🧠 **專案 Pattern 建議**:")
                    for bp in brain_patterns[:2]:
                        summary_lines.append(f"   - {bp['description']}: {bp['suggestion']}")

            # Generate Fix Prompt
            fix_prompt = f"Please review `{target_file.name}` and fix the following {len(result.issues)} issues:\n"
            for issue in result.issues:
                fix_prompt += f"- [{issue.severity.upper()}] {issue.category}: {issue.message}\n"
                if issue.suggestion:
                    fix_prompt += f"  (Suggestion: {issue.suggestion})\n"

            # 加入 Brain Pattern 到 Fix Prompt
            if brain_patterns:
                fix_prompt += "\n🧠 Project-specific patterns to follow:\n"
                for bp in brain_patterns:
                    fix_prompt += f"- {bp['description']}: {bp['suggestion']}\n"

            fix_prompt += "\nReturn the fixed specific functions or class code blocks."

            brain_status = "✅ Brain Pattern 整合" if brain_patterns else "⚠️ BrainManager 未啟用"

            return create_success_result(
                message="\n".join(summary_lines) + f"\n\n🔗 {brain_status}",
                data={
                    "file": str(target_file),
                    "issues_count": len(result.issues),
                    "brain_patterns_used": len(brain_patterns),
                    "brain_enhanced": bool(brain_patterns),
                    "issues": [
                        {
                            "category": i.category,
                            "severity": i.severity,
                            "message": i.message,
                            "line": i.line,
                        }
                        for i in result.issues
                    ],
                    "brain_patterns": brain_patterns,
                    "suggested_fix_prompt": fix_prompt,
                },
            )

        except ValueError:
            return create_error_result(f"❌ 不支援的格式: {target_file.suffix}")
        except Exception as e:
            return create_error_result(f"❌ 審查失敗: {str(e)}")

    # === boring_perf_tips ===
    @mcp.tool(
        description="效能分析提示 (Performance Tips). "
        "說: '分析效能', '效能優化建議', 'Check performance of api.py'. "
        "我會專注檢查效能瓶頸 (如 N+1 query, I/O in loop) 並提供優化建議!支援 Py, JS, TS.",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    @audited
    def boring_perf_tips(
        file_path: Annotated[str, Field(description="要分析的檔案路徑")],
        project_path: Annotated[str | None, Field(description="專案根目錄 (自動偵測)")] = None,
        verbosity: Annotated[
            str,
            Field(description="輸出詳細程度: 'minimal', 'standard' (預設), 'verbose'"),
        ] = "standard",
    ) -> BoringResult:
        """
        ⚡ 效能分析提示 - 專注於程式碼效能瓶頸檢測。
        支援平台: Python, JavaScript, TypeScript
        """
        project_root, error = _get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message", "Unknown error"))

        target_file = Path(file_path)
        if not target_file.is_absolute():
            target_file = project_root / file_path

        if not target_file.exists():
            return create_error_result(f"❌ 找不到檔案: {file_path}")

        try:
            source = target_file.read_text(encoding="utf-8")
            # 僅專注於 performance
            result = global_engine.perform_code_review(
                str(target_file), source, focus="performance"
            )

            if not result.issues:
                msg = "⚡ 效能分析完成:未發現明顯瓶頸。"
                if is_minimal(verbosity):
                    msg = f"⚡ {target_file.name}: 無效能問題"

                return create_success_result(
                    message=msg,
                    data={
                        "file": str(target_file),
                        "tips_count": 0,
                        "vibe_summary": msg,
                    },
                )

            # Generate Perf Fix Prompt
            fix_prompt = f"Please analyze performance bottlenecks in `{target_file.name}` and apply the following optimizations:\n"
            for issue in result.issues:
                fix_prompt += f"- {issue.message} (Line {issue.line})\n"
                if issue.suggestion:
                    fix_prompt += f"  Tip: {issue.suggestion}\n"

            # --- Verbosity Logic ---

            # 1. MINIMAL: Summary only
            if is_minimal(verbosity):
                severity_counts = {"high": 0, "medium": 0, "low": 0}
                for issue in result.issues:
                    severity_counts[issue.severity] = severity_counts.get(issue.severity, 0) + 1

                return create_success_result(
                    message=f"⚡ {target_file.name}: {len(result.issues)} 效能問題",
                    data={
                        "vibe_summary": (
                            f"⚡ {target_file.name}: {len(result.issues)} 效能問題\n"
                            f"🐌 High: {severity_counts['high']} | 🐢 Medium/Low: {severity_counts['medium'] + severity_counts['low']}\n"
                            f"💡 Use verbosity='standard' for tips"
                        ),
                        "file": str(target_file),
                        "tips_count": len(result.issues),
                    },
                )

            # 2. STANDARD: Top 5 issues
            if is_standard(verbosity):
                summary_lines = [f"⚡ Performance Tips: `{target_file.name}`", ""]
                # Show top 5
                for i, issue in enumerate(result.issues[:5], 1):
                    icon = "🐌" if issue.severity == "high" else "🐢"
                    summary_lines.append(f"{i}. {icon} **{issue.message}** (Line {issue.line})")
                    if issue.suggestion:
                        summary_lines.append(f"   🚀 優化: {issue.suggestion}")

                if len(result.issues) > 5:
                    summary_lines.append(f"\n... and {len(result.issues) - 5} more issues.")
                    summary_lines.append("💡 Use verbosity='verbose' for full list.")

                return create_success_result(
                    message="\n".join(summary_lines),
                    data={
                        "file": str(target_file),
                        "tips_count": len(result.issues),
                        "vibe_summary": "\n".join(summary_lines),
                        "suggested_fix_prompt": fix_prompt,
                    },
                )

            # 3. VERBOSE: Full list (Legacy behavior)
            summary_lines = [f"⚡ Performance Tips: `{target_file.name}`", ""]
            for i, issue in enumerate(result.issues, 1):
                icon = "🐌" if issue.severity == "high" else "🐢"
                summary_lines.append(f"{i}. {icon} **{issue.message}** (Line {issue.line})")
                if issue.suggestion:
                    summary_lines.append(f"   🚀 優化: {issue.suggestion}")

            return create_success_result(
                message="\n".join(summary_lines),
                data={
                    "file": str(target_file),
                    "tips_count": len(result.issues),
                    "tips": [
                        {"message": i.message, "line": i.line, "suggestion": i.suggestion}
                        for i in result.issues
                    ],
                    "vibe_summary": "\n".join(summary_lines),
                    "suggested_fix_prompt": fix_prompt,
                },
            )

        except ValueError:
            return create_error_result(f"❌ 不支援的格式: {target_file.suffix}")
        except Exception as e:
            return create_error_result(f"❌ 分析失敗: {str(e)}")

    # === boring_arch_check ===
    @mcp.tool(
        description="架構分析 (Architecture Analysis). "
        "說: '分析專案架構', 'Show me the dependencies', '看看誰引用誰', '該如何重構'. "
        "我會生成 Mermaid 依賴圖,讓你一目了然專案結構!",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    @audited
    def boring_arch_check(
        target_path: Annotated[str, Field(description="File or directory to scan.")] = ".",
        output_format: Annotated[
            str, Field(description="Output format: 'mermaid' or 'json'.")
        ] = "mermaid",
    ) -> BoringResult:
        """
        Analyze project dependencies and architecture.

        Generates a dependency graph showing how files import each other.
        Use this to understand the structure of a codebase.
        """
        root_str, error = _get_project_root_or_error(
            None
        )  # project_path is now optional and handled by get_root
        if error:
            return create_error_result(error.get("message", "Unknown error"))

        project_root = Path(root_str)
        # Handle both absolute and relative paths
        if target_path != "." and (
            target_path.startswith("/") or (len(target_path) > 1 and target_path[1] == ":")
        ):
            target = Path(target_path)
        elif target_path == ".":
            target = project_root
        else:
            target = project_root / target_path

        files_to_scan = []
        if target.is_file():
            files_to_scan.append(target)
        elif target.is_dir():
            files_to_scan.extend(
                [
                    p
                    for p in target.rglob("*")
                    if p.is_file()
                    and p.suffix.lower() in [".py", ".js", ".ts", ".jsx", ".tsx"]
                    and not any(
                        x in p.parts for x in ["node_modules", ".git", "__pycache__", "venv"]
                    )
                ]
            )
        else:
            return create_error_result(f"❌ Target not found: {target}")

        edges = []
        nodes = set()

        for file_path in files_to_scan:
            try:
                rel_path = file_path.relative_to(project_root).as_posix()
                content = file_path.read_text(encoding="utf-8", errors="ignore")

                vibe_engine = get_vibe_engine()
                deps = vibe_engine.extract_dependencies(str(file_path), content)

                if deps:
                    nodes.add(rel_path)
                    for dep in deps:
                        # Simple normalization
                        dep_name = dep
                        if dep.startswith("."):
                            # Attempt simple resolve?
                            # For visualization, generic name is arguably better than huge guess
                            pass

                        edge = (rel_path, dep_name)
                        edges.append(edge)
            except Exception:
                continue

        if output_format == "json":
            return create_success_result(
                message="✅ Dependency analysis complete (JSON)",
                data={"nodes": list(nodes), "edges": edges},
            )

        # Mermaid format
        lines = ["graph TD"]
        max_edges = 100

        # Limit edges to avoid explosion
        processed_count = 0
        for src, dst in edges:
            if processed_count >= max_edges:
                lines.append(f"    %% Truncated after {max_edges} edges")
                break

            # Simple sanitization for Mermaid IDs
            def clean_id(s):
                return s.replace("/", "_").replace(".", "_").replace("-", "_").replace("@", "")

            s_id = clean_id(src)
            d_id = clean_id(dst)

            # Add node labels
            lines.append(f'    {s_id}["{src}"] --> {d_id}["{dst}"]')
            processed_count += 1

        mermaid_graph = "\n".join(lines)
        return create_success_result(
            message=mermaid_graph,
            data={"mermaid": mermaid_graph, "node_count": len(nodes), "edge_count": len(edges)},
        )

    # === boring_doc_gen ===
    @mcp.tool(
        description="自動生成文檔 (Auto-generate Documentation). "
        "說: '幫我寫文檔', 'Generate docs for api.py', 'API 文檔', '自動註解'. "
        "我會擷取 Docstrings/JSDoc 並生成 Markdown 參考文檔!",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    @audited
    def boring_doc_gen(
        target_path: Annotated[str, Field(description="File or directory to scan.")] = ".",
    ) -> BoringResult:
        """
        Extract documentation comments and generate an API reference.

        Supports:
        - Python Docstrings (Module, Class, Function)
        - JavaScript/TypeScript JSDoc (/** ... */)

        Returns Markdown content.
        """
        root_str, error = _get_project_root_or_error(None)
        if error:
            return create_error_result(error.get("message", "Unknown error"))

        project_root = Path(root_str)
        # Handle both absolute and relative paths
        if target_path != "." and (
            target_path.startswith("/") or (len(target_path) > 1 and target_path[1] == ":")
        ):
            target = Path(target_path)
        elif target_path == ".":
            target = project_root
        else:
            target = project_root / target_path

        files_to_scan = []
        if target.is_file():
            files_to_scan.append(target)
        elif target.is_dir():
            files_to_scan.extend(
                [
                    p
                    for p in target.rglob("*")
                    if p.is_file()
                    and p.suffix.lower() in [".py", ".js", ".ts", ".jsx", ".tsx"]
                    and not any(
                        x in p.parts
                        for x in ["node_modules", ".git", "__pycache__", "venv", "dist", "build"]
                    )
                ]
            )
        else:
            return create_error_result(f"❌ Target not found: {target}")

        doc_output = [f"# API Documentation\n\nGenerated for: `{target_path}`\n"]

        for file_path in sorted(files_to_scan):
            try:
                content = file_path.read_text(encoding="utf-8", errors="ignore")
                rel_path = file_path.relative_to(project_root).as_posix()

                vibe_engine = get_vibe_engine()
                result = vibe_engine.extract_documentation(str(file_path), content)

                if not result.items and not result.module_doc:
                    continue

                doc_output.append(f"## File: `{rel_path}`\n")
                if result.module_doc:
                    doc_output.append(f"> {result.module_doc.strip()}\n")

                doc_output.append("")

                for item in result.items:
                    icon = "📦" if item.type == "class" else "ƒ"
                    doc_output.append(f"### {icon} `{item.name}`")
                    doc_output.append(f"**Signature:** `{item.signature}`\n")
                    if item.docstring:
                        doc_output.append(f"{item.docstring}\n")
                    else:
                        doc_output.append("*No documentation.*\n")
                    doc_output.append("---\n")

            except Exception as e:
                doc_output.append(f"<!-- Error processing {file_path.name}: {e} -->\n")

        doc_content = "\n".join(doc_output)
        return create_success_result(
            message=doc_content,
            data={"documentation": doc_content, "files_scanned": len(files_to_scan)},
        )

    # === boring_vibe_check ===
    @mcp.tool(
        description="Vibe Score 健檢 (Gamified Health Check). "
        "說: 'Vibe Check my project', '健檢 utils.py', 'Give me a vibe score'. "
        "我會整合 Lint, Security, Doc 檢查,計算 0-100 分數,並提供一鍵修復 Prompt! "
        "🆕 V10.21: 整合 Storage 記錄歷史分數趨勢!",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    @audited
    def boring_vibe_check(
        target_path: Annotated[str, Field(description="要健檢的檔案或目錄")] = ".",
        project_path: Annotated[str | None, Field(description="專案根目錄 (自動偵測)")] = None,
        max_files: Annotated[int, Field(description="最大掃描檔案數 (預設 10)")] = 10,
        verbosity: Annotated[
            str,
            Field(
                description="Output verbosity: 'minimal' (score + tier only ~50 tokens), 'standard' (score + top issues ~300 tokens), 'verbose' (full report ~800+ tokens). Default: 'standard'."
            ),
        ] = "standard",
    ) -> BoringResult:  # Forward reference or use BoringResult directly if imported
        """
        📊 Vibe Check - 全面健康度檢查與評分。
        整合多項指標 (Lint, Security, Doc),提供遊戲化評分與一鍵修復 Prompt。

        V10.21 整合:
        - Storage: 記錄 Vibe Score 歷史,追蹤專案健康趨勢
        - 顯示與上次分數的對比
        """
        return run_vibe_check(target_path, project_path, max_files, verbosity)

    # === boring_impact_check ===
    @mcp.tool(
        description="衝擊分析 (Impact Analysis). "
        "說: 'Check impact of modifying utils.py', '改這隻檔案會影響誰', 'Impact check'. "
        "我會分析反向依賴 (支援多層追蹤),告訴你修改此檔案會影響哪些模組,並給出驗證 Prompt! "
        "🆕 V10.21: 整合 RAG 語義分析更精準!",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    @audited
    def boring_impact_check(
        target_path: Annotated[str, Field(description="計畫修改的目標檔案")],
        project_path: Annotated[str | None, Field(description="專案根目錄 (自動偵測)")] = None,
        max_depth: Annotated[
            int, Field(description="追蹤深度 (1=直接依賴, 2=間接依賴, 預設 2)")
        ] = 2,
    ) -> BoringResult:
        """
        📡 Impact Analysis - 預判修改帶來的全局衝擊。
        Reverse Dependency Analysis with multi-level tracking (Phase 15 Enhancement).

        V10.21 整合:
        - RAG 語義搜尋: 找出語義相關的檔案(不只是 import)
        - 更精準的衝擊分析
        """
        root_str, error = _get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message", "Unknown error"))

        project_root = Path(root_str)
        # Handle both absolute and relative paths
        if target_path != "." and (
            target_path.startswith("/") or (len(target_path) > 1 and target_path[1] == ":")
        ):
            target = Path(target_path)
        elif target_path == ".":
            target = project_root
        else:
            target = project_root / target_path

        if not target.exists() or not target.is_file():
            return create_error_result(f"❌ 找不到目標檔案: {target_path}")

        # 1. 識別目標特徵 for fuzzy matching
        target_stem = target.stem  # e.g., "utils"
        rel_target = target.relative_to(project_root).as_posix()

        # V10.21: RAG 語義分析 - 找出語義相關的檔案
        semantic_related = []
        rag = _get_rag_retriever(project_root)
        if rag:
            try:
                # 讀取目標檔案內容,提取關鍵詞
                target_content = target.read_text(encoding="utf-8", errors="ignore")[:500]

                # 語義搜尋相關函數/類別
                results = rag.retrieve(
                    query=f"{target_stem} {target_content[:100]}",
                    top_k=5,
                    chunk_types=["function", "class"],
                )
                for r in results:
                    if r.chunk.file_path != str(target):
                        rel_path = Path(r.chunk.file_path).relative_to(project_root).as_posix()
                        if rel_path not in semantic_related:
                            semantic_related.append(rel_path)
            except Exception:
                pass  # RAG is optional enhancement

        # 2. 全專案掃描建立完整依賴圖
        files_to_scan = [
            p
            for p in project_root.rglob("*")
            if p.is_file()
            and p.suffix in [".py", ".js", ".ts", ".jsx", ".tsx"]
            and not any(
                x in p.parts
                for x in ["node_modules", ".git", "venv", "__pycache__", "dist", "build"]
            )
        ]

        # Build dependency graph: { file_rel_path -> [dependencies] }
        dep_graph = {}
        file_stems = {}  # stem -> [rel_paths]

        vibe_engine = get_vibe_engine()
        for f in files_to_scan:
            try:
                content = f.read_text(encoding="utf-8", errors="ignore")
                deps = vibe_engine.extract_dependencies(str(f), content)
                f_rel = f.relative_to(project_root).as_posix()
                dep_graph[f_rel] = deps

                # Index by stem for fuzzy matching
                stem = f.stem
                if stem not in file_stems:
                    file_stems[stem] = []
                file_stems[stem].append(f_rel)
            except Exception:
                continue

        # 3. Fuzzy matching helper
        def matches_target(dep: str, target_stem: str) -> bool:
            if target_stem == dep:
                return True
            if dep.endswith(f".{target_stem}"):
                return True
            if dep.endswith(f"/{target_stem}"):
                return True
            return False

        # 4. Build reverse dependency graph (who imports what)
        # direct_dependents: files that directly import target
        direct_dependents = set()
        for f_rel, deps in dep_graph.items():
            if f_rel == rel_target:
                continue
            for dep in deps:
                if matches_target(dep, target_stem):
                    direct_dependents.add(f_rel)
                    break

        # 5. Multi-level impact tracking (Phase 15 Enhancement)
        all_affected = set(direct_dependents)
        indirect_dependents = set()

        if max_depth >= 2:
            # Level 2: Find files that import direct_dependents
            for direct_dep in direct_dependents:
                direct_stem = Path(direct_dep).stem
                for f_rel, deps in dep_graph.items():
                    if f_rel in all_affected or f_rel == rel_target:
                        continue
                    for dep in deps:
                        if matches_target(dep, direct_stem):
                            indirect_dependents.add(f_rel)
                            all_affected.add(f_rel)
                            break

        # Level 3 (if max_depth >= 3)
        level3_dependents = set()
        if max_depth >= 3:
            for indirect_dep in indirect_dependents:
                indirect_stem = Path(indirect_dep).stem
                for f_rel, deps in dep_graph.items():
                    if f_rel in all_affected or f_rel == rel_target:
                        continue
                    for dep in deps:
                        if matches_target(dep, indirect_stem):
                            level3_dependents.add(f_rel)
                            all_affected.add(f_rel)
                            break

        # 6. 評估衝擊等級
        impact_level = "Low"
        if len(all_affected) > 10:
            impact_level = "Critical"
        elif len(all_affected) > 5:
            impact_level = "High"
        elif len(all_affected) > 0:
            impact_level = "Medium"

        # 7. Mermaid 圖形輸出
        mermaid_lines = ["graph TD", f'    Target["{rel_target}"]:::target']

        # Direct impacts
        for imp in list(direct_dependents)[:15]:
            sanitized_imp = imp.replace("/", "_").replace(".", "_").replace("-", "_")
            mermaid_lines.append(f'    {sanitized_imp}["{imp}"]:::direct -->|L1| Target')

        # Indirect impacts
        for imp in list(indirect_dependents)[:10]:
            sanitized_imp = imp.replace("/", "_").replace(".", "_").replace("-", "_")
            mermaid_lines.append(f'    {sanitized_imp}["{imp}"]:::indirect -->|L2| ...')

        mermaid_lines.append("    classDef target fill:#f96,stroke:#333")
        mermaid_lines.append("    classDef direct fill:#ff9,stroke:#333")
        mermaid_lines.append("    classDef indirect fill:#9ff,stroke:#333")
        mermaid_graph = "\n".join(mermaid_lines)

        # 8. Fix/Verification Prompt
        verify_prompt = ""
        if all_affected:
            verify_prompt = (
                f"⚠️ Impact Warning: Modifying `{rel_target}` affects {len(all_affected)} files.\n\n"
            )

            if direct_dependents:
                verify_prompt += (
                    f"🔴 **Direct Dependents (L1)** - {len(direct_dependents)} files:\n"
                )
                for aff in list(direct_dependents)[:5]:
                    verify_prompt += f"   - `{aff}`\n"
                if len(direct_dependents) > 5:
                    verify_prompt += f"   - ... and {len(direct_dependents) - 5} more.\n"

            if indirect_dependents:
                verify_prompt += (
                    f"\n🟡 **Indirect Dependents (L2)** - {len(indirect_dependents)} files:\n"
                )
                for aff in list(indirect_dependents)[:5]:
                    verify_prompt += f"   - `{aff}`\n"
                if len(indirect_dependents) > 5:
                    verify_prompt += f"   - ... and {len(indirect_dependents) - 5} more.\n"

            # V10.21: 加入 RAG 語義相關檔案
            if semantic_related:
                verify_prompt += (
                    f"\n🧠 **Semantically Related (RAG)** - {len(semantic_related)} files:\n"
                )
                for sem in semantic_related[:3]:
                    verify_prompt += f"   - `{sem}`\n"

            verify_prompt += (
                "\n📋 **Action Required**: Run tests for these files after your changes."
            )
        else:
            verify_prompt = f"✅ Low Impact: `{rel_target}` appears to have no internal dependents."

        rag_status = (
            f"✅ RAG 語義分析 ({len(semantic_related)} 相關)"
            if semantic_related
            else "⚠️ RAG 未啟用"
        )

        return create_success_result(
            message=f"📡 Impact Analysis: {rel_target}\nImpact Level: {impact_level}\nDirect Dependent Count: {len(direct_dependents)}\nIndirect Dependent Count: {len(indirect_dependents)}\n{rag_status}\n\n{verify_prompt}",
            data={
                "impact_level": impact_level,
                "affected_count": len(all_affected),
                "direct_count": len(direct_dependents),
                "indirect_count": len(indirect_dependents),
                "semantic_related_count": len(semantic_related),
                "rag_enhanced": bool(semantic_related),
                "affected_files": list(all_affected),
                "direct_dependents": list(direct_dependents),
                "indirect_dependents": list(indirect_dependents),
                "semantic_related": semantic_related,
                "mermaid": mermaid_graph,
                "vibe_summary": f"📡 **Impact Analysis**: `{rel_target}`\n"
                f"⚠️ **Impact Level**: {impact_level}\n"
                f"🔗 **Direct (L1)**: {len(direct_dependents)}\n"
                f"🔗 **Indirect (L2+)**: {len(indirect_dependents)}\n"
                f"🧠 **Semantic (RAG)**: {len(semantic_related)}\n"
                f"🔗 {rag_status}",
                "suggested_fix_prompt": verify_prompt,
            },
        )

    # =========================================================================
    # V10.22: Intelligence Tools
    # =========================================================================

    @mcp.tool(
        description="預測可能的錯誤 (Predict likely errors before running). "
        "說: '預測這個檔案會有什麼錯誤', 'predict errors for auth.py'. "
        "我會分析歷史模式,預測最可能發生的錯誤並提供預防建議!",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    @audited
    def boring_predict_errors(
        file_path: Annotated[str, Field(description="要預測錯誤的檔案路徑")],
        limit: Annotated[int, Field(description="最多返回幾個預測")] = 5,
        project_path: Annotated[str | None, Field(description="專案根目錄")] = None,
    ) -> BoringResult:
        """
        🔮 預測錯誤 - 根據歷史模式預測可能發生的錯誤。


        V10.22 Intelligence:
        - 分析檔案類型與過去錯誤的關聯
        - 提供信心分數和預防建議
        - 學習專案特定的錯誤模式
        """
        return run_predict_errors(file_path, limit, project_path)

    @mcp.tool(
        description="📊 專案健康評分 (Project health score). "
        "說: '專案健康狀況', '給我健康報告', 'project health score'. "
        "我會分析成功率、錯誤趨勢、解決率,給出綜合健康評分!",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    @audited
    def boring_health_score(
        project_path: Annotated[str | None, Field(description="專案根目錄")] = None,
    ) -> BoringResult:
        """
        📊 專案健康評分 - 綜合分析專案狀態。

        V10.22 Intelligence:
        - 成功率分析 (40% 權重)
        - 錯誤解決率 (30% 權重)
        - 執行效率 (30% 權重)
        - 趨勢分析和建議
        """
        root_str, error = _get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message", "Unknown error"))
        project_root = Path(root_str)

        storage = _get_storage(project_root)
        if not storage:
            return create_error_result("Storage 未初始化")

        # Get health score
        health = storage.get_health_score()

        # Get error trend
        trend = storage.get_error_trend(days=7)

        # Build detailed report
        score = health["score"]
        grade = health["grade"]

        # Emoji for grade
        grade_emoji = {
            "A+": "🏆",
            "A": "🌟",
            "B": "✅",
            "C": "👍",
            "D": "⚠️",
            "F": "🚨",
            "N/A": "📊",
        }.get(grade, "📊")

        breakdown = health.get("breakdown", {})

        summary = f"""# {grade_emoji} 專案健康報告

## 綜合評分: **{score}/100** (等級: {grade})

{health["message"]}

## 📈 指標分解
- 成功率: **{breakdown.get("success_rate", "N/A")}%**
- 錯誤解決率: **{breakdown.get("resolution_rate", "N/A")}%**
- 平均執行時間: **{breakdown.get("avg_loop_duration", "N/A")}s**

## 📊 錯誤趨勢 (7天)
- 趨勢方向: {trend.get("emoji", "➡️")} {trend.get("trend", "N/A")}
- 變化幅度: {trend.get("change_percent", 0)}%
- {trend.get("recommendation", "")}
"""

        return create_success_result(
            message=summary,
            data={
                "score": score,
                "grade": grade,
                "breakdown": breakdown,
                "trend": trend,
                "vibe_summary": summary,
            },
        )

    @mcp.tool(
        description="🧠 優化上下文 (Optimize context for LLM). "
        "說: '幫我壓縮這些程式碼', 'optimize context'. "
        "我會智能壓縮程式碼上下文,減少 token 使用同時保留關鍵資訊!",
        annotations={"readOnlyHint": True, "openWorldHint": False, "idempotentHint": True},
    )
    @audited
    def boring_optimize_context(
        file_paths: Annotated[list[str], Field(description="要優化的檔案路徑列表")],
        max_tokens: Annotated[int, Field(description="最大 token 限制")] = 8000,
        error_message: Annotated[str | None, Field(description="相關錯誤訊息 (最高優先級)")] = None,
        project_path: Annotated[str | None, Field(description="專案根目錄")] = None,
    ) -> BoringResult:
        """
        🧠 上下文優化 - 智能壓縮程式碼以減少 token 使用。

        V10.22 Intelligence:
        - 去重複內容
        - 優先保留關鍵程式碼
        - 壓縮文檔和註釋
        - 保持語義完整性
        """
        root_str, error = _get_project_root_or_error(project_path)
        if error:
            return create_error_result(error.get("message", "Unknown error"))
        project_root = Path(root_str)

        try:
            from ..intelligence import SmartContextBuilder

            builder = SmartContextBuilder(max_tokens=max_tokens, project_root=project_root)
        except ImportError:
            return create_error_result("Intelligence 模組未安裝")

        # Add error context (highest priority)
        if error_message:
            builder.with_error(error_message, priority=1.0)

        # Add code files
        for fp in file_paths:
            try:
                path = Path(project_root) / fp if not Path(fp).is_absolute() else Path(fp)
                if path.exists():
                    content = path.read_text(encoding="utf-8", errors="ignore")
                    rel_path = (
                        str(path.relative_to(project_root))
                        if path.is_relative_to(project_root)
                        else str(path)
                    )
                    builder.with_code_file(rel_path, content, priority=0.8)
            except Exception:
                continue

        # Build optimized context
        optimized = builder.build()
        report = builder.get_compression_report()
        stats = builder.stats

        return create_success_result(
            message=report,
            data={
                "optimized_context": optimized,
                "stats": {
                    "original_tokens": stats.original_tokens if stats else 0,
                    "optimized_tokens": stats.optimized_tokens if stats else 0,
                    "compression_ratio": stats.compression_ratio if stats else 1.0,
                    "sections_removed": stats.sections_removed if stats else 0,
                    "duplicates_merged": stats.duplicates_merged if stats else 0,
                },
                "vibe_summary": report,
            },
        )

    # === boring_done (BoringDone Notification) ===
    @mcp.tool(
        description="🔔 完成通知 (Completion Notification). "
        "說: '任務完成通知我', 'Notify me when done', '好了叫我'. "
        "我會發送桌面通知、音效提醒,讓你不用盯著螢幕!",
        annotations={"readOnlyHint": False, "openWorldHint": False, "idempotentHint": False},
    )
    @audited
    def boring_done(
        task_name: Annotated[str, Field(description="任務名稱")] = "AI Task",
        success: Annotated[bool, Field(description="任務是否成功")] = True,
        details: Annotated[str, Field(description="額外詳情")] = "",
    ) -> BoringResult:
        """
        🔔 BoringDone - 完成通知系統。

        當 AI 任務完成時發送通知:
        - 桌面彈窗 (Windows Toast / macOS / Linux)
        - 音效提醒
        - Terminal Bell

        讓使用者可以放心做其他事,完成後會收到通知!
        """
        try:
            from ...services.notifier import done as notify_done

            result = notify_done(task_name=task_name, success=success, details=details)
            return create_success_result(
                message=f"🔔 已發送通知: {result['title']}",
                data=result,
            )
        except ImportError:
            # Fallback: simple terminal bell
            print("\a", end="", flush=True)
            return create_success_result(
                message=f"🔔 Terminal Bell: {task_name} {'✅ Complete' if success else '❌ Failed'}",
                data={"fallback": True},
            )
        except Exception as e:
            return create_error_result(f"❌ 通知失敗: {str(e)}")

    return {
        "boring_test_gen": boring_test_gen,
        "boring_code_review": boring_code_review,
        "boring_perf_tips": boring_perf_tips,
        "boring_arch_check": boring_arch_check,
        "boring_doc_gen": boring_doc_gen,
        "boring_vibe_check": boring_vibe_check,
        "boring_impact_check": boring_impact_check,
        # V10.22 Intelligence Tools
        "boring_predict_errors": boring_predict_errors,
        "boring_health_score": boring_health_score,
        "boring_optimize_context": boring_optimize_context,
        # V12.2 BoringDone
        "boring_done": boring_done,
    }

Security & Advanced (安全與進階)

boring.mcp.tools.advanced

Advanced MCP Tools for Boring V10.16.

Consolidated tools to reduce context usage: - boring_security_scan - boring_transaction (start/commit/rollback/status) - boring_task (submit/status/list) - boring_context (save/load/list) - boring_profile (get/learn)

boring_security_scan(project_path=None, scan_type='full', verbosity='standard')

Run comprehensive security scan.

Detects: - Hardcoded secrets - Code vulnerabilities (SAST) - Dependency CVEs

Source code in src/boring/mcp/tools/advanced.py
def boring_security_scan(
    project_path: Annotated[
        str | None,
        Field(default=None, description="Path to project root (default: current directory)"),
    ] = None,
    scan_type: Annotated[
        str,
        Field(
            default="full",
            description="Scan type: 'full', 'secrets', 'vulnerabilities', 'dependencies'",
        ),
    ] = "full",
    verbosity: Annotated[
        str,
        Field(description="Output detail level: 'minimal', 'standard' (default), 'verbose'"),
    ] = "standard",
) -> dict:
    """
    Run comprehensive security scan.

    Detects:
    - Hardcoded secrets
    - Code vulnerabilities (SAST)
    - Dependency CVEs
    """
    from pathlib import Path

    from boring.core.config import settings
    from boring.mcp.verbosity import get_verbosity, is_minimal, is_standard
    from boring.security import SecurityScanner

    # Resolve verbosity
    verbosity_level = get_verbosity(verbosity)

    path = Path(project_path) if project_path else settings.PROJECT_ROOT
    scanner = SecurityScanner(path)

    if scan_type == "secrets":
        scanner.scan_secrets()
    elif scan_type == "vulnerabilities":
        scanner.scan_vulnerabilities()
    elif scan_type == "dependencies":
        scanner.scan_dependencies()
    else:
        scanner.full_scan()

    report = scanner.report

    # --- Verbosity Logic ---

    # 1. MINIMAL: Just summary counts
    if is_minimal(verbosity_level):
        return {
            "status": "passed" if report.passed else "failed",
            "summary": (
                f"🛡️ Scan completed. Found {report.total_issues} issues. "
                f"(Secrets: {report.secrets_found}, SAST: {report.vulnerabilities_found}, "
                f"Deps: {report.dependency_issues})"
            ),
            "hint": "💡 Use `verbosity='standard'` to see critical issues.",
        }

    # 2. STANDARD: Summary + Top 5 Issues
    if is_standard(verbosity_level):
        top_issues = report.issues[:5]
        return {
            "status": "passed" if report.passed else "failed",
            "total_issues": report.total_issues,
            "breakdown": {
                "secrets": report.secrets_found,
                "sast": report.vulnerabilities_found,
                "dependencies": report.dependency_issues,
            },
            "top_issues": [
                {
                    "severity": i.severity,
                    "category": i.category,
                    "file": f"{i.file_path}:{i.line_number}",
                    "description": i.description,
                }
                for i in top_issues
            ],
            "message": (
                f"Found {report.total_issues} issues. Showing top {len(top_issues)}."
                if report.issues
                else "No issues found."
            ),
            "hint": "💡 Use `verbosity='verbose'` for full report and recommendations.",
        }

    # 3. VERBOSE: Full Report (Legacy Format)
    return {
        "passed": report.passed,
        "checked_categories": [
            "Secrets Detection",
            "SAST (Code Vulnerabilities)",
            "Dependency Scan",
        ],
        "total_issues": report.total_issues,
        "secrets_found": report.secrets_found,
        "vulnerabilities_found": report.vulnerabilities_found,
        "dependency_issues": report.dependency_issues,
        "issues": [
            {
                "severity": i.severity,
                "category": i.category,
                "file": i.file_path,
                "line": i.line_number,
                "description": i.description,
                "recommendation": i.recommendation,
            }
            # Still cap at 50 for safety, but effectively "all" for most cases
            for i in report.issues[:50]
        ],
        "message": "Scan completed. No issues found."
        if report.passed
        else f"Scan failed. Found {report.total_issues} issues.",
    }

boring_transaction(action, description='Boring transaction', project_path=None)

Manage atomic transactions with Git checkpoints.

Actions: - start: Create a checkpoint (stash changes) - commit: Confirm changes (drop stash) - rollback: Revert code to checkpoint - status: Check if a transaction is active

Source code in src/boring/mcp/tools/advanced.py
def boring_transaction(
    action: Annotated[
        Literal["start", "commit", "rollback", "status"],
        Field(description="Action to perform: start, commit, rollback, status"),
    ],
    description: Annotated[
        str,
        Field(default="Boring transaction", description="Description for 'start' action"),
    ] = "Boring transaction",
    project_path: Annotated[
        str | None,
        Field(default=None, description="Path to project root"),
    ] = None,
) -> dict:
    """
    Manage atomic transactions with Git checkpoints.

    Actions:
    - start: Create a checkpoint (stash changes)
    - commit: Confirm changes (drop stash)
    - rollback: Revert code to checkpoint
    - status: Check if a transaction is active
    """
    from boring.loop.transactions import (
        commit_transaction,
        rollback_transaction,
        start_transaction,
        transaction_status,
    )

    if action == "start":
        return start_transaction(project_path, description)
    elif action == "commit":
        return commit_transaction(project_path)
    elif action == "rollback":
        return rollback_transaction(project_path)
    elif action == "status":
        return transaction_status(project_path)
    else:
        return {"status": "error", "message": f"Unknown action: {action}"}

boring_task(action, task_type=None, task_id=None, task_args=None, project_path=None)

Manage background tasks (async execution).

Actions: - submit: Start a new background task (requires task_type) - status: Check task progress (requires task_id) - list: List all tasks

Source code in src/boring/mcp/tools/advanced.py
def boring_task(
    action: Annotated[
        Literal["submit", "status", "list"],
        Field(description="Action: submit a task, check status, or list tasks"),
    ],
    task_type: Annotated[
        str | None,
        Field(
            default=None, description="Task type for 'submit': verify, test, lint, security_scan"
        ),
    ] = None,
    task_id: Annotated[
        str | None,
        Field(default=None, description="Task ID for 'status' action"),
    ] = None,
    task_args: Annotated[
        dict | None,
        Field(default=None, description="Arguments for 'submit' action"),
    ] = None,
    project_path: Annotated[
        str | None,
        Field(default=None, description="Path to project root"),
    ] = None,
) -> dict:
    """
    Manage background tasks (async execution).

    Actions:
    - submit: Start a new background task (requires task_type)
    - status: Check task progress (requires task_id)
    - list: List all tasks
    """
    from boring.loop.background_agent import (
        get_task_status,
        list_background_tasks,
        submit_background_task,
    )

    if action == "submit":
        if not task_type:
            return {"status": "error", "message": "task_type is required for submit action"}
        return submit_background_task(task_type, task_args or {}, project_path)
    elif action == "status":
        if not task_id:
            return {"status": "error", "message": "task_id is required for status action"}
        return get_task_status(task_id)
    elif action == "list":
        return list_background_tasks()
    else:
        return {"status": "error", "message": f"Unknown action: {action}"}

boring_background_task(task_type, task_args=None, project_path=None)

Legacy alias: submit a background task.

Source code in src/boring/mcp/tools/advanced.py
def boring_background_task(
    task_type: Annotated[str, Field(description="Task type: verify, test, lint, security_scan")],
    task_args: Annotated[
        dict | None, Field(default=None, description="Arguments for submit action")
    ] = None,
    project_path: Annotated[
        str | None, Field(default=None, description="Path to project root")
    ] = None,
) -> dict:
    """Legacy alias: submit a background task."""
    return boring_task(
        "submit",
        task_type=task_type,
        task_args=task_args,
        project_path=project_path,
    )

boring_task_status(task_id)

Legacy alias: check background task status.

Source code in src/boring/mcp/tools/advanced.py
def boring_task_status(
    task_id: Annotated[str, Field(description="Task ID to check")],
) -> dict:
    """Legacy alias: check background task status."""
    return boring_task("status", task_id=task_id)

boring_list_tasks(status=None)

Legacy alias: list background tasks.

Source code in src/boring/mcp/tools/advanced.py
def boring_list_tasks(
    status: Annotated[str | None, Field(default=None, description="Optional status filter")] = None,
) -> dict:
    """Legacy alias: list background tasks."""
    from boring.loop.background_agent import list_background_tasks

    return list_background_tasks(status)

boring_context(action, context_id=None, summary='', project_path=None)

Manage conversation context (Cross-Session Memory).

Actions: - save: Save current context (requires context_id, summary) - load: Load a saved context (requires context_id) - list: List all saved contexts

Source code in src/boring/mcp/tools/advanced.py
def boring_context(
    action: Annotated[
        Literal["save", "load", "list"],
        Field(description="Action: save context, load context, or list saved contexts"),
    ],
    context_id: Annotated[
        str | None,
        Field(default=None, description="Context ID for save/load"),
    ] = None,
    summary: Annotated[
        str,
        Field(default="", description="Summary for 'save' action"),
    ] = "",
    project_path: Annotated[
        str | None,
        Field(default=None, description="Path to project root"),
    ] = None,
) -> dict:
    """
    Manage conversation context (Cross-Session Memory).

    Actions:
    - save: Save current context (requires context_id, summary)
    - load: Load a saved context (requires context_id)
    - list: List all saved contexts
    """
    from boring.context_sync import list_contexts, load_context, save_context

    if action == "save":
        if not context_id or not summary:
            return {"status": "error", "message": "context_id and summary are required for save"}
        return save_context(context_id, summary, project_path)
    elif action == "load":
        if not context_id:
            return {"status": "error", "message": "context_id is required for load"}
        return load_context(context_id, project_path)
    elif action == "list":
        return list_contexts(project_path)
    else:
        return {"status": "error", "message": f"Unknown action: {action}"}

boring_save_context(context_name, data, project_path=None)

Legacy alias: save context summary.

Source code in src/boring/mcp/tools/advanced.py
def boring_save_context(
    context_name: Annotated[str, Field(description="Context name to save")],
    data: Annotated[str | dict, Field(description="Summary or payload to store for the context")],
    project_path: Annotated[
        str | None, Field(default=None, description="Path to project root")
    ] = None,
) -> dict:
    """Legacy alias: save context summary."""
    summary = data if isinstance(data, str) else str(data)
    return boring_context(
        "save", context_id=context_name, summary=summary, project_path=project_path
    )

boring_load_context(context_name, project_path=None)

Legacy alias: load saved context.

Source code in src/boring/mcp/tools/advanced.py
def boring_load_context(
    context_name: Annotated[str, Field(description="Context name to load")],
    project_path: Annotated[
        str | None, Field(default=None, description="Path to project root")
    ] = None,
) -> dict:
    """Legacy alias: load saved context."""
    return boring_context("load", context_id=context_name, project_path=project_path)

boring_list_contexts(project_path=None)

Legacy alias: list saved contexts.

Source code in src/boring/mcp/tools/advanced.py
def boring_list_contexts(
    project_path: Annotated[
        str | None, Field(default=None, description="Path to project root")
    ] = None,
) -> dict:
    """Legacy alias: list saved contexts."""
    return boring_context("list", project_path=project_path)

boring_profile(action, error_pattern=None, fix_pattern=None, context='')

Manage user profile (Cross-Project Memory).

Actions: - get: Get user profile and preferences - learn: Record a learned fix (requires error_pattern, fix_pattern)

Source code in src/boring/mcp/tools/advanced.py
def boring_profile(
    action: Annotated[
        Literal["get", "learn"],
        Field(description="Action: get profile or learn a fix"),
    ],
    error_pattern: Annotated[
        str | None,
        Field(default=None, description="Error pattern for 'learn' action"),
    ] = None,
    fix_pattern: Annotated[
        str | None,
        Field(default=None, description="Fix pattern for 'learn' action"),
    ] = None,
    context: Annotated[
        str,
        Field(default="", description="Context for 'learn' action"),
    ] = "",
) -> dict:
    """
    Manage user profile (Cross-Project Memory).

    Actions:
    - get: Get user profile and preferences
    - learn: Record a learned fix (requires error_pattern, fix_pattern)
    """
    from boring.context_sync import get_user_profile, learn_fix

    if action == "get":
        return get_user_profile()
    elif action == "learn":
        if not error_pattern or not fix_pattern:
            return {
                "status": "error",
                "message": "error_pattern and fix_pattern required for learn",
            }
        return learn_fix(error_pattern, fix_pattern, context)
    else:
        return {"status": "error", "message": f"Unknown action: {action}"}

boring_get_profile()

Legacy alias: get user profile.

Source code in src/boring/mcp/tools/advanced.py
def boring_get_profile() -> dict:
    """Legacy alias: get user profile."""
    return boring_profile("get")

boring_learn_fix(error_pattern, fix_pattern, context='')

Legacy alias: learn a fix pattern.

Source code in src/boring/mcp/tools/advanced.py
def boring_learn_fix(
    error_pattern: Annotated[str, Field(description="Error pattern to learn")],
    fix_pattern: Annotated[str, Field(description="Fix pattern to learn")],
    context: Annotated[str, Field(default="", description="Optional context")] = "",
) -> dict:
    """Legacy alias: learn a fix pattern."""
    return boring_profile(
        "learn",
        error_pattern=error_pattern,
        fix_pattern=fix_pattern,
        context=context,
    )

boring_transaction_start(message='Boring transaction', project_path=None)

Legacy alias: start a transaction.

Source code in src/boring/mcp/tools/advanced.py
def boring_transaction_start(
    message: Annotated[
        str, Field(default="Boring transaction", description="Checkpoint message")
    ] = ("Boring transaction"),
    project_path: Annotated[
        str | None, Field(default=None, description="Path to project root")
    ] = None,
) -> dict:
    """Legacy alias: start a transaction."""
    return boring_transaction("start", description=message, project_path=project_path)

boring_transaction_commit(project_path=None)

Legacy alias: commit a transaction.

Source code in src/boring/mcp/tools/advanced.py
def boring_transaction_commit(
    project_path: Annotated[
        str | None, Field(default=None, description="Path to project root")
    ] = None,
) -> dict:
    """Legacy alias: commit a transaction."""
    return boring_transaction("commit", project_path=project_path)

boring_transaction_rollback(project_path=None)

Legacy alias: rollback a transaction.

Source code in src/boring/mcp/tools/advanced.py
def boring_transaction_rollback(
    project_path: Annotated[
        str | None, Field(default=None, description="Path to project root")
    ] = None,
) -> dict:
    """Legacy alias: rollback a transaction."""
    return boring_transaction("rollback", project_path=project_path)

boring_rollback(project_path=None)

Legacy alias: rollback a transaction.

Source code in src/boring/mcp/tools/advanced.py
def boring_rollback(
    project_path: Annotated[
        str | None, Field(default=None, description="Path to project root")
    ] = None,
) -> dict:
    """Legacy alias: rollback a transaction."""
    return boring_transaction("rollback", project_path=project_path)

register_advanced_tools(mcp)

Register consolidated advanced tools.

Source code in src/boring/mcp/tools/advanced.py
def register_advanced_tools(mcp):
    """Register consolidated advanced tools."""
    # Security (1 tool)
    mcp.tool(
        description="Run security scan (secrets, SAST, dependencies)",
        annotations={"readOnlyHint": True},
    )(boring_security_scan)

    # Transactions (1 tool)
    mcp.tool(description="Manage atomic transactions (start/commit/rollback)")(boring_transaction)

    # Background Tasks (1 tool)
    mcp.tool(description="Manage background tasks (submit/status/list)")(boring_task)
    mcp.tool(description="Legacy alias: submit background task")(boring_background_task)
    mcp.tool(description="Legacy alias: check background task status")(boring_task_status)
    mcp.tool(description="Legacy alias: list background tasks")(boring_list_tasks)

    # Context (1 tool)
    mcp.tool(description="Manage conversation context (save/load/list)")(boring_context)
    mcp.tool(description="Legacy alias: save context")(boring_save_context)
    mcp.tool(description="Legacy alias: load context")(boring_load_context)
    mcp.tool(description="Legacy alias: list contexts")(boring_list_contexts)

    # Profile (1 tool)
    mcp.tool(description="Manage user profile and learned memory")(boring_profile)
    mcp.tool(description="Legacy alias: get profile")(boring_get_profile)
    mcp.tool(description="Legacy alias: learn fix patterns")(boring_learn_fix)

    # Transaction aliases
    mcp.tool(description="Legacy alias: start transaction")(boring_transaction_start)
    mcp.tool(description="Legacy alias: commit transaction")(boring_transaction_commit)
    mcp.tool(description="Legacy alias: rollback transaction")(boring_transaction_rollback)
    mcp.tool(description="Legacy alias: rollback transaction")(boring_rollback)

Session Management (會話管理)

boring.mcp.tools.session

Vibe Session Tools - Human-Aligned AI Coding Workflow (V10.25)

Provides stateful session management for complete AI-human collaboration. Solves the core AI Coding problems: 1. AI vs Human expectation gap 2. Architecture drift during development 3. Quality degradation over time 4. Lack of confirmation checkpoints

Usage

boring_session_start(goal="Build login feature")

-> Returns session_id and enters Phase 1

boring_session_confirm()

-> Confirms current phase and moves to next

boring_session_status()

-> Shows current progress

SessionPhase

Bases: str, Enum

Vibe Session phases.

Source code in src/boring/mcp/tools/session.py
class SessionPhase(str, Enum):
    """Vibe Session phases."""

    ALIGNMENT = "alignment"  # Phase 1: Requirement gathering
    PLANNING = "planning"  # Phase 2: Plan creation
    IMPLEMENTATION = "implementation"  # Phase 3: Step-by-step coding
    VERIFICATION = "verification"  # Phase 4: Final verification
    COMPLETED = "completed"  # Session done
    PAUSED = "paused"  # Session paused

SessionStep dataclass

A single implementation step.

Source code in src/boring/mcp/tools/session.py
@dataclass
class SessionStep:
    """A single implementation step."""

    id: int
    description: str
    status: str = "pending"  # pending, in_progress, completed, failed
    score: float | None = None
    output: str | None = None
    created_at: str = ""
    completed_at: str | None = None

VibeSession dataclass

Complete Vibe Session state.

Source code in src/boring/mcp/tools/session.py
@dataclass
class VibeSession:
    """Complete Vibe Session state."""

    session_id: str
    goal: str
    phase: SessionPhase
    created_at: str
    updated_at: str

    # Phase 1: Alignment
    requirements: dict = field(default_factory=dict)
    tech_stack: str = ""
    quality_level: str = "production"  # prototype, production, enterprise
    constraints: list = field(default_factory=list)
    exclusions: list = field(default_factory=list)

    # Phase 2: Planning
    plan: dict = field(default_factory=dict)
    checklist: list = field(default_factory=list)
    architecture_notes: list = field(default_factory=list)

    # Phase 3: Implementation
    steps: list = field(default_factory=list)
    current_step: int = 0
    auto_mode: bool = False

    # Phase 4: Verification
    verification_results: dict = field(default_factory=dict)
    final_score: float | None = None

    # Learning
    learned_patterns: list = field(default_factory=list)

    def to_dict(self) -> dict:
        """Convert to dictionary."""
        data = asdict(self)
        data["phase"] = self.phase.value
        return data

    @classmethod
    def from_dict(cls, data: dict) -> "VibeSession":
        """Create from dictionary."""
        data["phase"] = SessionPhase(data["phase"])
        return cls(**data)
to_dict()

Convert to dictionary.

Source code in src/boring/mcp/tools/session.py
def to_dict(self) -> dict:
    """Convert to dictionary."""
    data = asdict(self)
    data["phase"] = self.phase.value
    return data
from_dict(data) classmethod

Create from dictionary.

Source code in src/boring/mcp/tools/session.py
@classmethod
def from_dict(cls, data: dict) -> "VibeSession":
    """Create from dictionary."""
    data["phase"] = SessionPhase(data["phase"])
    return cls(**data)

VibeSessionManager

Manages Vibe Session state persistence.

Source code in src/boring/mcp/tools/session.py
class VibeSessionManager:
    """Manages Vibe Session state persistence."""

    def __init__(self, project_root: Path):
        self.project_root = Path(project_root)
        self.session_dir = self.project_root / ".boring_memory" / "sessions"
        self.session_dir.mkdir(parents=True, exist_ok=True)
        self._current_session: VibeSession | None = None

    def _session_path(self, session_id: str) -> Path:
        return self.session_dir / f"{session_id}.json"

    def create_session(self, goal: str) -> VibeSession:
        """Create a new Vibe Session."""
        session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
        now = datetime.now().isoformat()

        session = VibeSession(
            session_id=session_id,
            goal=goal,
            phase=SessionPhase.ALIGNMENT,
            created_at=now,
            updated_at=now,
        )

        self._current_session = session
        self.save_session(session)
        return session

    def save_session(self, session: VibeSession):
        """Save session to disk."""
        session.updated_at = datetime.now().isoformat()
        path = self._session_path(session.session_id)
        with open(path, "w", encoding="utf-8") as f:
            json.dump(session.to_dict(), f, indent=2, ensure_ascii=False)

    def load_session(self, session_id: str) -> VibeSession | None:
        """Load session from disk."""
        path = self._session_path(session_id)
        if not path.exists():
            return None

        with open(path, encoding="utf-8") as f:
            data = json.load(f)

        session = VibeSession.from_dict(data)
        self._current_session = session
        return session

    def get_current_session(self) -> VibeSession | None:
        """Get current active session."""
        return self._current_session

    def list_sessions(self) -> list[dict]:
        """List all sessions."""
        sessions = []
        for path in self.session_dir.glob("*.json"):
            try:
                with open(path, encoding="utf-8") as f:
                    data = json.load(f)
                sessions.append(
                    {
                        "session_id": data["session_id"],
                        "goal": data["goal"][:50] + "..."
                        if len(data["goal"]) > 50
                        else data["goal"],
                        "phase": data["phase"],
                        "created_at": data["created_at"][:10],
                        "updated_at": data["updated_at"][:10],
                    }
                )
            except Exception:
                continue
        return sorted(sessions, key=lambda x: x["created_at"], reverse=True)

    def advance_phase(self, session: VibeSession) -> VibeSession:
        """Move to next phase."""
        phase_order = [
            SessionPhase.ALIGNMENT,
            SessionPhase.PLANNING,
            SessionPhase.IMPLEMENTATION,
            SessionPhase.VERIFICATION,
            SessionPhase.COMPLETED,
        ]

        current_idx = phase_order.index(session.phase)
        if current_idx < len(phase_order) - 1:
            session.phase = phase_order[current_idx + 1]

        self.save_session(session)
        return session
create_session(goal)

Create a new Vibe Session.

Source code in src/boring/mcp/tools/session.py
def create_session(self, goal: str) -> VibeSession:
    """Create a new Vibe Session."""
    session_id = datetime.now().strftime("%Y%m%d_%H%M%S")
    now = datetime.now().isoformat()

    session = VibeSession(
        session_id=session_id,
        goal=goal,
        phase=SessionPhase.ALIGNMENT,
        created_at=now,
        updated_at=now,
    )

    self._current_session = session
    self.save_session(session)
    return session
save_session(session)

Save session to disk.

Source code in src/boring/mcp/tools/session.py
def save_session(self, session: VibeSession):
    """Save session to disk."""
    session.updated_at = datetime.now().isoformat()
    path = self._session_path(session.session_id)
    with open(path, "w", encoding="utf-8") as f:
        json.dump(session.to_dict(), f, indent=2, ensure_ascii=False)
load_session(session_id)

Load session from disk.

Source code in src/boring/mcp/tools/session.py
def load_session(self, session_id: str) -> VibeSession | None:
    """Load session from disk."""
    path = self._session_path(session_id)
    if not path.exists():
        return None

    with open(path, encoding="utf-8") as f:
        data = json.load(f)

    session = VibeSession.from_dict(data)
    self._current_session = session
    return session
get_current_session()

Get current active session.

Source code in src/boring/mcp/tools/session.py
def get_current_session(self) -> VibeSession | None:
    """Get current active session."""
    return self._current_session
list_sessions()

List all sessions.

Source code in src/boring/mcp/tools/session.py
def list_sessions(self) -> list[dict]:
    """List all sessions."""
    sessions = []
    for path in self.session_dir.glob("*.json"):
        try:
            with open(path, encoding="utf-8") as f:
                data = json.load(f)
            sessions.append(
                {
                    "session_id": data["session_id"],
                    "goal": data["goal"][:50] + "..."
                    if len(data["goal"]) > 50
                    else data["goal"],
                    "phase": data["phase"],
                    "created_at": data["created_at"][:10],
                    "updated_at": data["updated_at"][:10],
                }
            )
        except Exception:
            continue
    return sorted(sessions, key=lambda x: x["created_at"], reverse=True)
advance_phase(session)

Move to next phase.

Source code in src/boring/mcp/tools/session.py
def advance_phase(self, session: VibeSession) -> VibeSession:
    """Move to next phase."""
    phase_order = [
        SessionPhase.ALIGNMENT,
        SessionPhase.PLANNING,
        SessionPhase.IMPLEMENTATION,
        SessionPhase.VERIFICATION,
        SessionPhase.COMPLETED,
    ]

    current_idx = phase_order.index(session.phase)
    if current_idx < len(phase_order) - 1:
        session.phase = phase_order[current_idx + 1]

    self.save_session(session)
    return session

get_session_manager(project_root)

Get or create session manager for project.

Source code in src/boring/mcp/tools/session.py
def get_session_manager(project_root: Path) -> VibeSessionManager:
    """Get or create session manager for project."""
    key = str(project_root)
    if key not in _session_managers:
        _session_managers[key] = VibeSessionManager(project_root)
    return _session_managers[key]

boring_session_start(goal='', quality_level='production', project_path=None)

🎯 啟動 Vibe Session - 完整的 AI 協作流程。

解決 AI Coding 核心問題: - AI 與人類期望落差 - 架構遺失 - 品質下降 - 缺乏確認點

Returns:

Type Description
BoringResult

Session 啟動結果和 Phase 1 問題

Source code in src/boring/mcp/tools/session.py
@audited
def boring_session_start(
    goal: Annotated[
        str, PydanticField(description="你想達成什麼目標?例如:'建立用戶登入功能'")
    ] = "",
    quality_level: Annotated[
        str,
        PydanticField(
            description="品質等級: prototype(快速原型), production(生產級), enterprise(企業級)"
        ),
    ] = "production",
    project_path: Annotated[str, PydanticField(description="專案路徑(選填)")] = None,
) -> BoringResult:
    """
    🎯 啟動 Vibe Session - 完整的 AI 協作流程。

    解決 AI Coding 核心問題:
    - AI 與人類期望落差
    - 架構遺失
    - 品質下降
    - 缺乏確認點

    Returns:
        Session 啟動結果和 Phase 1 問題
    """
    allowed, msg = check_rate_limit("boring_session_start")
    if not allowed:
        return create_error_result(f"⏱️ Rate limited: {msg}")

    project_root = detect_project_root(project_path)
    if not project_root:
        return create_error_result("❌ 找不到有效的 Boring 專案。請在專案根目錄執行。")

    try:
        manager = get_session_manager(project_root)
        session = manager.create_session(goal if goal else "待確認")
        session.quality_level = quality_level
        manager.save_session(session)

        # Phase 1 prompt
        goal_display = f"**目標**: {goal}" if goal else "**目標**: 待確認"

        msg_content = f"""# 🎯 Vibe Session 已啟動

**Session ID**: `{session.session_id}`
{goal_display}
**品質等級**: {quality_level}
**當前階段**: Phase 1 - 需求對齊 (Alignment)

---

## 📋 Phase 1: 需求對齊

為了確保我 100% 理解你的需求,請回答以下問題:

### 1️⃣ 核心目標
{f"你說想要「{goal}」,可以更具體描述嗎?例如:" if goal else "你今天想達成什麼?例如:"}
- 要解決什麼問題?
- 預期的結果是什麼?

### 2️⃣ 技術偏好
- 有指定的語言/框架嗎?
- 有需要整合的現有系統嗎?

### 3️⃣ 品質期望 (已選擇: {quality_level})
- 🚀 prototype: 快速驗證,可以有技術債
- 🏗️ production: 需要測試、文檔、錯誤處理
- 🏢 enterprise: 需要安全審計、性能優化、監控

### 4️⃣ 約束條件
- 有必須遵守的架構規範嗎?
- 有時間限制嗎?

### 5️⃣ 明確排除
- 有什麼是你「不要」的?

---

💬 請回答以上問題,或者說:
- `確認` - 如果目標已經足夠清楚
- `調整目標 XXX` - 修改目標
- `取消` - 取消此 Session
"""
        return create_success_result(message=msg_content, data=session.to_dict())
    except Exception as e:
        logger.error(f"Failed to start session: {e}")
        return create_error_result(f"❌ 啟動 Session 失敗: {str(e)}")

boring_session_confirm(notes='', project_path=None)

✅ 確認當前階段並進入下一階段。

Returns:

Type Description
BoringResult

下一階段的指引

Source code in src/boring/mcp/tools/session.py
@audited
def boring_session_confirm(
    notes: Annotated[str, PydanticField(description="補充說明或確認訊息")] = "",
    project_path: Annotated[str, PydanticField(description="專案路徑(選填)")] = None,
) -> BoringResult:
    """
    ✅ 確認當前階段並進入下一階段。

    Returns:
        下一階段的指引
    """
    allowed, msg = check_rate_limit("boring_session_confirm")
    if not allowed:
        return create_error_result(f"⏱️ Rate limited: {msg}")

    project_root = detect_project_root(project_path)
    if not project_root:
        return create_error_result("❌ 找不到有效的 Boring 專案。")

    try:
        manager = get_session_manager(project_root)
        session = manager.get_current_session()

        if not session:
            return create_error_result("❌ 沒有進行中的 Session。請先執行 `boring_session_start`。")

        current_phase = session.phase

        # Update notes if provided
        if notes:
            if current_phase == SessionPhase.ALIGNMENT:
                session.requirements["user_notes"] = notes
            elif current_phase == SessionPhase.PLANNING:
                session.architecture_notes.append(notes)

        # Advance to next phase
        session = manager.advance_phase(session)

        # Return appropriate prompt for new phase
        if session.phase == SessionPhase.PLANNING:
            prompt = _get_planning_prompt(session)
        elif session.phase == SessionPhase.IMPLEMENTATION:
            prompt = _get_implementation_prompt(session)
        elif session.phase == SessionPhase.VERIFICATION:
            prompt = _get_verification_prompt(session)
        elif session.phase == SessionPhase.COMPLETED:
            prompt = _get_completion_prompt(session)
        else:
            prompt = f"✅ 已確認。當前階段: {session.phase.value}"

        return create_success_result(message=prompt, data=session.to_dict())

    except Exception as e:
        logger.error(f"Failed to confirm session: {e}")
        return create_error_result(f"❌ 確認失敗: {str(e)}")

boring_session_status(project_path=None)

📊 查看當前 Vibe Session 狀態。

Returns:

Type Description
BoringResult

Session 狀態報告

Source code in src/boring/mcp/tools/session.py
@audited
def boring_session_status(
    project_path: Annotated[str, PydanticField(description="專案路徑(選填)")] = None,
) -> BoringResult:
    """
    📊 查看當前 Vibe Session 狀態。

    Returns:
        Session 狀態報告
    """
    allowed, msg = check_rate_limit("boring_session_status")
    if not allowed:
        return create_error_result(f"⏱️ Rate limited: {msg}")

    project_root = detect_project_root(project_path)
    if not project_root:
        return create_error_result("❌ 找不到有效的 Boring 專案。")

    try:
        manager = get_session_manager(project_root)
        session = manager.get_current_session()

        if not session:
            # List available sessions
            sessions = manager.list_sessions()
            if not sessions:
                return create_success_result(
                    "📭 沒有任何 Session 記錄。使用 `boring_session_start` 開始新的 Session。",
                    data={"sessions": []},
                )

            session_list = "\n".join(
                [f"  • `{s['session_id']}` - {s['goal']} ({s['phase']})" for s in sessions[:5]]
            )
            msg_content = f"""# 📊 Vibe Session 列表

最近的 Sessions:
{session_list}

使用 `boring_session_load(session_id='...')` 載入特定 Session。
或使用 `boring_session_start` 開始新的 Session。
"""
            return create_success_result(message=msg_content, data={"sessions": sessions})

        # Calculate progress
        phase_progress = {
            SessionPhase.ALIGNMENT: 20,
            SessionPhase.PLANNING: 40,
            SessionPhase.IMPLEMENTATION: 70,
            SessionPhase.VERIFICATION: 90,
            SessionPhase.COMPLETED: 100,
            SessionPhase.PAUSED: 0,
        }

        progress = phase_progress.get(session.phase, 0)
        if session.phase == SessionPhase.IMPLEMENTATION and session.steps:
            total_steps = len(session.steps)
            completed = sum(1 for s in session.steps if s.get("status") == "completed")
            progress = 40 + int(30 * completed / total_steps) if total_steps > 0 else 40

        progress_bar = "█" * (progress // 10) + "░" * (10 - progress // 10)

        # Build status display
        phase_emoji = {
            SessionPhase.ALIGNMENT: "📋",
            SessionPhase.PLANNING: "📐",
            SessionPhase.IMPLEMENTATION: "🔨",
            SessionPhase.VERIFICATION: "✅",
            SessionPhase.COMPLETED: "🎉",
            SessionPhase.PAUSED: "⏸️",
        }

        msg_content = f"""# 📊 Vibe Session 狀態

```
┌─────────────────────────────────────────────────┐
│  🎯 目標: {session.goal[:40]}{"..." if len(session.goal) > 40 else ""}
├─────────────────────────────────────────────────┤
{phase_emoji.get(session.phase, "❓")} 當前階段: {session.phase.value.upper()}
│  📈 進度: [{progress_bar}] {progress}%
├─────────────────────────────────────────────────┤
│  📅 創建時間: {session.created_at[:10]}
│  🔄 更新時間: {session.updated_at[:16]}
│  🎚️ 品質等級: {session.quality_level}
│  🤖 自動模式: {"開啟" if session.auto_mode else "關閉"}
└─────────────────────────────────────────────────┘
```

**Session ID**: `{session.session_id}`

---

**可用指令**:
- `boring_session_confirm` - 確認並進入下一階段
- `boring_session_pause` - 暫停 Session
- `boring_session_auto(enable=True)` - 開啟自動模式
"""
        return create_success_result(message=msg_content, data=session.to_dict())

    except Exception as e:
        logger.error(f"Failed to get session status: {e}")
        return create_error_result(f"❌ 取得狀態失敗: {str(e)}")

boring_session_load(session_id, project_path=None)

📂 載入之前的 Vibe Session。

Returns:

Type Description
BoringResult

Session 狀態和繼續提示

Source code in src/boring/mcp/tools/session.py
@audited
def boring_session_load(
    session_id: Annotated[str, PydanticField(description="要載入的 Session ID")],
    project_path: Annotated[str, PydanticField(description="專案路徑(選填)")] = None,
) -> BoringResult:
    """
    📂 載入之前的 Vibe Session。

    Returns:
        Session 狀態和繼續提示
    """
    allowed, msg = check_rate_limit("boring_session_load")
    if not allowed:
        return create_error_result(f"⏱️ Rate limited: {msg}")

    project_root = detect_project_root(project_path)
    if not project_root:
        return create_error_result("❌ 找不到有效的 Boring 專案。")

    try:
        manager = get_session_manager(project_root)
        session = manager.load_session(session_id)

        if not session:
            return create_error_result(f"❌ 找不到 Session: {session_id}")

        msg_content = f"""# 📂 Session 已載入

**Session ID**: `{session.session_id}`
**目標**: {session.goal}
**當前階段**: {session.phase.value}
**品質等級**: {session.quality_level}

---

使用 `boring_session_status` 查看詳細狀態。
使用 `boring_session_confirm` 繼續下一步。
"""
        return create_success_result(message=msg_content, data=session.to_dict())

    except Exception as e:
        logger.error(f"Failed to load session: {e}")
        return create_error_result(f"❌ 載入失敗: {str(e)}")

boring_session_pause(project_path=None)

⏸️ 暫停當前 Vibe Session。

Returns:

Type Description
BoringResult

暫停確認

Source code in src/boring/mcp/tools/session.py
@audited
def boring_session_pause(
    project_path: Annotated[str, PydanticField(description="專案路徑(選填)")] = None,
) -> BoringResult:
    """
    ⏸️ 暫停當前 Vibe Session。

    Returns:
        暫停確認
    """
    allowed, msg = check_rate_limit("boring_session_pause")
    if not allowed:
        return create_error_result(f"⏱️ Rate limited: {msg}")

    project_root = detect_project_root(project_path)
    if not project_root:
        return create_error_result("❌ 找不到有效的 Boring 專案。")

    try:
        manager = get_session_manager(project_root)
        session = manager.get_current_session()

        if not session:
            return create_error_result("❌ 沒有進行中的 Session。")

        previous_phase = session.phase
        session.phase = SessionPhase.PAUSED
        manager.save_session(session)

        msg_content = f"""# ⏸️ Session 已暫停

**Session ID**: `{session.session_id}`
**暫停前階段**: {previous_phase.value}

進度已保存。稍後使用以下指令繼續:
```
boring_session_load(session_id='{session.session_id}')
```
"""
        return create_success_result(message=msg_content, data=session.to_dict())

    except Exception as e:
        logger.error(f"Failed to pause session: {e}")
        return create_error_result(f"❌ 暫停失敗: {str(e)}")

boring_session_auto(enable=True, project_path=None)

🤖 切換自動模式 - 自動確認並執行所有步驟。

Returns:

Type Description
BoringResult

模式切換確認

Source code in src/boring/mcp/tools/session.py
@audited
def boring_session_auto(
    enable: Annotated[bool, PydanticField(description="是否啟用自動模式")] = True,
    project_path: Annotated[str, PydanticField(description="專案路徑(選填)")] = None,
) -> BoringResult:
    """
    🤖 切換自動模式 - 自動確認並執行所有步驟。

    Returns:
        模式切換確認
    """
    allowed, msg = check_rate_limit("boring_session_auto")
    if not allowed:
        return create_error_result(f"⏱️ Rate limited: {msg}")

    project_root = detect_project_root(project_path)
    if not project_root:
        return create_error_result("❌ 找不到有效的 Boring 專案。")

    try:
        manager = get_session_manager(project_root)
        session = manager.get_current_session()

        if not session:
            return create_error_result("❌ 沒有進行中的 Session。")

        session.auto_mode = enable
        manager.save_session(session)

        if enable:
            msg_content = """# 🤖 自動模式已啟用

⚠️ **警告**: 自動模式下,我將:
- 自動確認每個步驟
- 自動修復遇到的問題
- 只在嚴重錯誤時暫停

使用 `boring_session_auto(enable=False)` 關閉自動模式。
"""
        else:
            msg_content = """# 🎮 手動模式已啟用

✅ 每個步驟都會等待你的確認後才執行。
"""
        return create_success_result(message=msg_content, data={"auto_mode": enable})

    except Exception as e:
        logger.error(f"Failed to toggle auto mode: {e}")
        return create_error_result(f"❌ 切換失敗: {str(e)}")

Knowledge & Learning (知識與學習)

boring.mcp.tools.knowledge

boring_learn(project_path=None)

Trigger learning from .boring/memory to .boring/brain.

Extracts successful patterns from loop history and error solutions, storing them in learned_patterns/ for future reference.

Source code in src/boring/mcp/tools/knowledge.py
@audited
def boring_learn(
    project_path: Annotated[
        str | None, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Trigger learning from .boring/memory to .boring/brain.

    Extracts successful patterns from loop history and error solutions,
    storing them in learned_patterns/ for future reference.
    """
    try:
        from ...config import settings
        from ...intelligence.brain_manager import BrainManager
        from ...services.storage import SQLiteStorage

        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error

        configure_runtime_for_project(project_root)

        # Initialize storage and brain
        storage = SQLiteStorage(project_root / ".boring/memory", settings.LOG_DIR)
        brain = BrainManager(project_root, settings.LOG_DIR)

        # Learn from memory
        return brain.learn_from_memory(storage)

    except Exception as e:
        return {"status": "ERROR", "error": str(e)}

boring_create_rubrics(project_path=None)

Create default evaluation rubrics in .boring/brain/rubrics/.

Source code in src/boring/mcp/tools/knowledge.py
@audited
def boring_create_rubrics(
    project_path: Annotated[
        str | None, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Create default evaluation rubrics in .boring/brain/rubrics/.
    """
    try:
        from ...config import settings
        from ...intelligence.brain_manager import BrainManager

        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error

        configure_runtime_for_project(project_root)

        brain = BrainManager(project_root, settings.LOG_DIR)
        return brain.create_default_rubrics()

    except Exception as e:
        return {"status": "ERROR", "error": str(e)}

boring_brain_status(project_path=None)

Get status of .boring/brain and detected Project Context.

Action 3: Brain Visualization Action 2: Dynamic Discovery (Context Reporting)

Source code in src/boring/mcp/tools/knowledge.py
@audited
def boring_brain_status(
    project_path: Annotated[
        str | None, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Get status of .boring/brain and detected Project Context.

    Action 3: Brain Visualization
    Action 2: Dynamic Discovery (Context Reporting)
    """
    try:
        from ...config import settings
        from ...intelligence.brain_manager import BrainManager

        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error

        configure_runtime_for_project(project_root)

        brain = BrainManager(project_root, settings.LOG_DIR)
        summary = brain.get_brain_summary()

        # Action 2: Add Context Capabilities
        context = detect_context_capabilities(project_root)

        return {
            "status": "SUCCESS",
            "brain_health": "Active" if context["has_boring_brain"] else "Not Initialized",
            "stats": summary,
            "context": context,
            "location": str(project_root / ".boring/brain"),
        }

    except Exception as e:
        return {"status": "ERROR", "error": str(e)}

boring_brain_sync(remote_url=None)

Sync global brain knowledge with a remote Git repository (Push/Pull).

Enable 'Knowledge Swarm' by syncing your ~/.boring_brain/global_patterns.json.

Source code in src/boring/mcp/tools/knowledge.py
@audited
def boring_brain_sync(
    remote_url: Annotated[
        str | None, Field(description="Git remote URL. If None, uses configured origin.")
    ] = None,
) -> dict:
    """
    Sync global brain knowledge with a remote Git repository (Push/Pull).

    Enable 'Knowledge Swarm' by syncing your ~/.boring_brain/global_patterns.json.
    """
    try:
        from ...intelligence.brain_manager import GlobalKnowledgeStore

        store = GlobalKnowledgeStore()
        return store.sync_with_remote(remote_url)

    except Exception as e:
        return {"status": "ERROR", "error": str(e)}

boring_distill_skills(min_success=3)

Distill high-success patterns into Strategic Skills (Skill Compilation).

Converts frequently successful learned patterns into compiled 'Skills' that are given higher priority in future reasoning iterations.

Source code in src/boring/mcp/tools/knowledge.py
@audited
def boring_distill_skills(
    min_success: Annotated[
        int, Field(description="Minimum success count to distill into a skill (default: 3)")
    ] = 3,
) -> dict:
    """
    Distill high-success patterns into Strategic Skills (Skill Compilation).

    Converts frequently successful learned patterns into compiled 'Skills' that
    are given higher priority in future reasoning iterations.
    """
    try:
        from ...intelligence.brain_manager import get_global_store

        store = get_global_store()
        return store.distill_skills(min_success=min_success)

    except Exception as e:
        return {"status": "ERROR", "error": str(e)}

boring_get_relevant_patterns(limit=5, project_path=None)

Get relevant patterns (Skills) for the current project context.

Uses project capabilities (e.g., detected frameworks) to find matching learned patterns in the Brain.

Source code in src/boring/mcp/tools/knowledge.py
@audited
def boring_get_relevant_patterns(
    limit: Annotated[int, Field(description="Maximum patterns to return")] = 5,
    project_path: Annotated[
        str | None, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Get relevant patterns (Skills) for the current project context.

    Uses project capabilities (e.g., detected frameworks) to find matching
    learned patterns in the Brain.
    """
    try:
        from ...config import settings
        from ...intelligence.brain_manager import BrainManager

        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error

        configure_runtime_for_project(project_root)

        # Initialize Brain
        brain = BrainManager(project_root, settings.LOG_DIR)

        # Detected context tags
        context_caps = detect_context_capabilities(project_root)

        # Extract tags from context capabilities
        tags = []
        if context_caps.get("languages"):
            tags.extend(context_caps["languages"])
        if context_caps.get("frameworks"):
            tags.extend(context_caps["frameworks"])

        context_str = f"Project: {project_root.name} " + " ".join(tags)

        return brain.get_relevant_patterns(context=context_str, limit=limit)

    except Exception as e:
        return {"status": "ERROR", "error": str(e)}

boring_remember_constraint(rule, project_path=None)

Remember a user constraint or project rule permanently. The agent will recall this rule in future sessions to avoid mistakes.

Source code in src/boring/mcp/tools/knowledge.py
@audited
def boring_remember_constraint(
    rule: Annotated[
        str, Field(description="The constraint or rule to remember (e.g., 'Do not use any type')")
    ],
    project_path: Annotated[
        str | None, Field(description="Optional explicit path to project root")
    ] = None,
) -> dict:
    """
    Remember a user constraint or project rule permanently.
    The agent will recall this rule in future sessions to avoid mistakes.
    """
    try:
        from ...intelligence.constraints import get_constraint_store

        project_root, error = get_project_root_or_error(project_path)
        if error:
            return error

        configure_runtime_for_project(project_root)

        store = get_constraint_store(project_root)
        constraint = store.add_constraint(rule)

        return {
            "status": "SUCCESS",
            "message": f"Constraint remembered: {rule}",
            "id": constraint.id,
        }
    except Exception as e:
        return {"status": "ERROR", "error": str(e)}

boring_usage_stats(limit=10)

Show your personal tool usage statistics (Adaptive Profile Dashboard).

Returns a formatted markdown report of your most frequently used tools, helping you understand your workflow patterns.

Source code in src/boring/mcp/tools/knowledge.py
@audited
def boring_usage_stats(
    limit: Annotated[int, Field(description="Number of top tools to show (default: 10)")] = 10,
) -> str:
    """
    Show your personal tool usage statistics (Adaptive Profile Dashboard).

    Returns a formatted markdown report of your most frequently used tools,
    helping you understand your workflow patterns.
    """
    import time
    from datetime import datetime

    try:
        from ...intelligence.usage_tracker import get_tracker

        tracker = get_tracker()
        stats = tracker.stats

        if stats.total_calls == 0:
            return (
                "## 📊 Usage Statistics\n\n"
                "No usage data yet. Start using Boring tools to build your profile!\n\n"
                "💡 Enable `BORING_MCP_PROFILE=adaptive` to auto-personalize your toolset."
            )

        # Build markdown report
        lines = ["## 📊 Usage Statistics\n"]

        # Summary
        last_updated = datetime.fromtimestamp(stats.last_updated).strftime("%Y-%m-%d %H:%M")
        lines.append(f"**Total Calls:** {stats.total_calls}")
        lines.append(f"**Last Activity:** {last_updated}")
        lines.append(f"**Tools Tracked:** {len(stats.tools)}\n")

        # Top tools table
        lines.append("### 🏆 Top Tools\n")
        lines.append("| Rank | Tool | Calls | Last Used |")
        lines.append("|------|------|-------|-----------|")

        top_tools = tracker.get_top_tools(limit=limit)

        for i, tool_name in enumerate(top_tools, 1):
            usage = stats.tools.get(tool_name)
            if usage:
                # Relative time
                seconds_ago = time.time() - usage.last_used
                if seconds_ago < 60:
                    relative = "just now"
                elif seconds_ago < 3600:
                    relative = f"{int(seconds_ago / 60)}m ago"
                elif seconds_ago < 86400:
                    relative = f"{int(seconds_ago / 3600)}h ago"
                else:
                    relative = f"{int(seconds_ago / 86400)}d ago"

                lines.append(f"| {i} | `{tool_name}` | {usage.count} | {relative} |")

        lines.append("\n---")
        lines.append("💡 Use `BORING_MCP_PROFILE=adaptive` to auto-include your top tools!")

        return "\n".join(lines)

    except Exception as e:
        return f"❌ Failed to retrieve usage stats: {e}"

register_knowledge_tools(mcp, audited, helpers)

Refactored registration for knowledge tools.

Source code in src/boring/mcp/tools/knowledge.py
def register_knowledge_tools(mcp, audited, helpers):
    """Refactored registration for knowledge tools."""
    mcp.tool(
        description="Learn patterns from memory (brain).",
        annotations={"readOnlyHint": False, "destructiveHint": False},
    )(boring_learn)

    mcp.tool(description="Create evaluation rubrics.", annotations={"readOnlyHint": False})(
        boring_create_rubrics
    )

    mcp.tool(
        description="Get Brain Status & Context (Visualization).",
        annotations={"readOnlyHint": True},
    )(boring_brain_status)

    mcp.tool(
        description="Sync global brain with Git (Knowledge Swarm).",
        annotations={"readOnlyHint": False, "openWorldHint": True},
    )(boring_brain_sync)

    mcp.tool(
        description="Distill patterns into Strategic Skills (Skill Compilation).",
        annotations={"readOnlyHint": False},
    )(boring_distill_skills)

    mcp.tool(
        description="Get relevant patterns (skills) for the current context.",
        annotations={"readOnlyHint": True},
    )(boring_get_relevant_patterns)

    mcp.tool(
        description="Remember a user constraint or project rule permanently.",
        annotations={"readOnlyHint": False},
    )(boring_remember_constraint)

    # P4: Usage Analytics Dashboard
    mcp.tool(
        description="Show your personal tool usage statistics (Adaptive Profile Dashboard).",
        annotations={"readOnlyHint": True},
    )(boring_usage_stats)