Skip to content

ASH MCP Server Guide

This guide provides comprehensive documentation for the ASH MCP server implementation, which uses a file-based approach to track scan progress and completion.

Overview

The Automated Security Helper (ASH) MCP server provides a reliable interface for AI assistants to perform security scans on your codebase. The server implements the Model Context Protocol (MCP) to enable seamless integration with AI assistants.

The MCP server uses a file-based approach to track scan progress and completion, making it more reliable than event-based tracking. This approach ensures that scan progress and results are accurately tracked even in complex threading scenarios.

Transport choice

ash mcp supports two transports:

  • stdio (default) — local, single-user, launched as a subprocess by an IDE or local agent. This is what every example below assumes.
  • streamable-HTTP — networked, multi-tenant, supports remote clients with per-session workspaces and config profiles. See Streamable-HTTP MCP Deployment Guide.

Pick stdio for local IDE integration. Pick streamable-HTTP when ASH must be reachable over the network.

Architecture

The ASH MCP server architecture consists of the following components:

  1. MCP Server: The main server component that registers and exposes MCP tools
  2. Scan Registry: A thread-safe registry that tracks active scans
  3. File-Based Tracking: A system that uses file existence to track scan progress
  4. Error Handling: Comprehensive error handling for various scenarios

File-Based Tracking Approach

The file-based tracking approach works as follows:

  1. When a scan is started, a unique scan ID is generated and registered in the scan registry
  2. The scan process is started asynchronously
  3. The scan progress is tracked by checking for the existence of result files:
  4. ash_aggregated_results.json: Indicates the scan has completed
  5. Individual scanner result files: Indicate which scanners have completed
  6. The scan results are retrieved by parsing these files

This approach is more reliable than event-based tracking because it doesn't depend on in-memory state that could be lost due to threading issues.

File Structure

The file structure used for tracking is as follows:

.ash/ash_output/
├── ash_aggregated_results.json       # Complete scan results
├── scanners/
│   ├── bandit/
│   │   ├── source/
│   │   │   └── ASH.ScanResults.json  # Bandit scanner results
│   ├── detect-secrets/
│   │   ├── source/
│   │   │   └── ASH.ScanResults.json  # Detect-secrets scanner results
│   └── ...
└── reports/
    ├── ash_report.csv                # CSV report
    ├── ash_report.html               # HTML report
    └── ash_report.md                 # Markdown report

MCP Tools

The ASH MCP server provides the following tools:

scan_directory

Starts a security scan asynchronously and returns a scan ID for tracking.

Parameters: - directory_path (required): Path to the directory to scan - severity_threshold (optional): Minimum severity threshold (LOW, MEDIUM, HIGH, CRITICAL) - config_path (optional): Path to ASH configuration file

Returns: A dictionary with scan ID and status information.

Example:

result = await mcp_scan_directory(
    directory_path="/path/to/your/code",
    severity_threshold="MEDIUM",
    config_path=None
)

Example Response:

{
  "success": true,
  "scan_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "pending",
  "directory_path": "/path/to/your/code",
  "output_directory": "/path/to/your/code/.ash/ash_output",
  "severity_threshold": "MEDIUM",
  "config_path": null,
  "start_time": "2025-07-16T12:34:56.789012",
  "message": "Scan started successfully. Use get_scan_progress to track progress."
}

get_scan_progress

Gets the current progress of a running scan using file-based tracking.

Parameters: - scan_id (required): The scan ID returned from scan_directory

Returns: A dictionary with scan progress information.

Example:

progress = await mcp_get_scan_progress(
    scan_id="550e8400-e29b-41d4-a716-446655440000"
)

Example Response:

{
  "success": true,
  "scan_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "running",
  "completed_scanners": 2,
  "total_scanners": 5,
  "scanners": {
    "bandit": {
      "status": "completed",
      "target_type": "source",
      "finding_count": 3,
      "severity_counts": {
        "LOW": 1,
        "MEDIUM": 2,
        "HIGH": 0,
        "CRITICAL": 0
      }
    },
    "detect-secrets": {
      "status": "completed",
      "target_type": "source",
      "finding_count": 0,
      "severity_counts": {}
    },
    "semgrep": {
      "status": "running",
      "target_type": "source"
    },
    "checkov": {
      "status": "pending",
      "target_type": "source"
    },
    "cdk-nag": {
      "status": "pending",
      "target_type": "source"
    }
  },
  "start_time": "2025-07-16T12:34:56.789012",
  "current_time": "2025-07-16T12:36:23.456789",
  "duration": 86.667777,
  "timestamp": "2025-07-16T12:36:23.456789"
}

get_scan_results

Gets the final results of a completed scan using file-based tracking.

Parameters: - scan_id (required): The scan ID returned from scan_directory

Returns: A dictionary with scan results information.

Example:

results = await mcp_get_scan_results(
    scan_id="550e8400-e29b-41d4-a716-446655440000"
)

Example Response:

{
  "success": true,
  "scan_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "completed",
  "finding_count": 5,
  "severity_counts": {
    "LOW": 1,
    "MEDIUM": 3,
    "HIGH": 1,
    "CRITICAL": 0
  },
  "findings": [
    {
      "id": "finding-1",
      "title": "Hardcoded password",
      "severity": "HIGH",
      "scanner": "detect-secrets",
      "file_path": "config.py",
      "line_number": 42,
      "description": "Hardcoded password found in configuration file"
    },
    // Additional findings...
  ],
  "scan_duration": 124.567890,
  "start_time": "2025-07-16T12:34:56.789012",
  "end_time": "2025-07-16T12:37:01.356902",
  "timestamp": "2025-07-16T12:37:01.356902"
}

list_active_scans

Lists all active and recent scans with their current status.

Parameters: None

Returns: A dictionary with information about all scans in the registry.

Example:

scans = await mcp_list_active_scans()

Example Response:

{
  "success": true,
  "active_scans": [
    {
      "scan_id": "550e8400-e29b-41d4-a716-446655440000",
      "status": "running",
      "directory_path": "/path/to/your/code",
      "start_time": "2025-07-16T12:34:56.789012"
    }
  ],
  "all_scans": [
    {
      "scan_id": "550e8400-e29b-41d4-a716-446655440000",
      "status": "running",
      "directory_path": "/path/to/your/code",
      "start_time": "2025-07-16T12:34:56.789012"
    },
    {
      "scan_id": "660e8400-e29b-41d4-a716-446655440000",
      "status": "completed",
      "directory_path": "/path/to/another/code",
      "start_time": "2025-07-16T11:22:33.444555",
      "end_time": "2025-07-16T11:25:12.345678"
    }
  ],
  "stats": {
    "total_scans": 2,
    "active_scans": 1,
    "status_counts": {
      "running": 1,
      "completed": 1,
      "failed": 0,
      "cancelled": 0
    }
  },
  "timestamp": "2025-07-16T12:36:23.456789"
}

cancel_scan

Cancels a running scan and cleans up its resources.

Parameters: - scan_id (required): The scan ID to cancel

Returns: A dictionary with cancellation result information.

Example:

result = await mcp_cancel_scan(
    scan_id="550e8400-e29b-41d4-a716-446655440000"
)

Example Response:

{
  "success": true,
  "scan_id": "550e8400-e29b-41d4-a716-446655440000",
  "status": "cancelled",
  "message": "Scan cancelled successfully",
  "timestamp": "2025-07-16T12:36:45.123456"
}

check_installation

Checks if ASH is properly installed and ready to use.

Parameters: None

Returns: A dictionary with installation status information.

Example:

result = await mcp_check_installation()

Example Response:

{
  "success": true,
  "installed": true,
  "version": "3.0.0",
  "ash_command_available": true,
  "ash_command_output": "ASH v3.4.1",
  "ash_dir_exists": true,
  "timestamp": "2025-07-16T12:37:12.345678"
}

Error Handling

The ASH MCP server provides comprehensive error handling for various scenarios. Each error response includes:

  • success: Always false for error responses
  • operation: The operation that was being performed
  • error: Error message
  • error_type: Type of error
  • error_category: Category of error
  • context: Additional context about the error
  • suggestions: List of suggestions for resolving the error

Error Categories

The following error categories are used:

  • file_not_found: A required file or directory was not found
  • permission_denied: Permission issues accessing files or directories
  • invalid_format: File format or structure is invalid
  • invalid_parameter: Invalid parameter values provided
  • invalid_path: Path is invalid or improperly formatted
  • resource_exhausted: System resources are exhausted
  • operation_timeout: Operation took too long and timed out
  • scan_not_found: Requested scan ID was not found in the registry
  • scan_incomplete: Scan has not completed yet
  • unexpected_error: An unexpected error occurred

Example Error Response

{
  "success": false,
  "operation": "get_scan_results",
  "error": "Scan 550e8400-e29b-41d4-a716-446655440000 not found",
  "error_type": "MCPResourceError",
  "error_category": "scan_not_found",
  "context": {
    "scan_id": "550e8400-e29b-41d4-a716-446655440000",
    "error_category": "scan_not_found"
  },
  "suggestions": [
    "Check that the scan ID is correct",
    "Verify that the scan exists in the registry",
    "The scan may have been cleaned up if it was completed a long time ago"
  ],
  "timestamp": "2025-07-16T12:37:12.345678"
}

Configuration

The ASH MCP server can be configured using the mcp-resource-management section in the ASH configuration file:

# .ash/.ash.yaml
mcp-resource-management:
  # Concurrent operations
  max_concurrent_scans: 5
  max_concurrent_tasks: 30
  thread_pool_max_workers: 8

  # Timeouts
  scan_timeout_seconds: 2400
  operation_timeout_seconds: 300

  # Resource monitoring
  enable_health_checks: true
  health_check_interval_seconds: 30
  memory_warning_threshold_mb: 1024
  memory_critical_threshold_mb: 2048
  task_count_warning_threshold: 20

  # Message limits
  max_message_size_bytes: 10485760  # 10MB
  max_directory_size_mb: 1024       # 1GB

Configuration Options

  • max_concurrent_scans: Maximum number of concurrent scans
  • max_concurrent_tasks: Maximum number of concurrent tasks
  • thread_pool_max_workers: Maximum number of worker threads
  • scan_timeout_seconds: Timeout for scans in seconds
  • operation_timeout_seconds: Timeout for operations in seconds
  • enable_health_checks: Whether to enable health checks
  • health_check_interval_seconds: Interval between health checks in seconds
  • memory_warning_threshold_mb: Memory usage warning threshold in MB
  • memory_critical_threshold_mb: Memory usage critical threshold in MB
  • task_count_warning_threshold: Task count warning threshold
  • max_message_size_bytes: Maximum message size in bytes
  • max_directory_size_mb: Maximum directory size in MB

Restricting which directories can be scanned

An MCP client names the directory it wants scanned, and ASH writes its output tree inside that directory (<target>/.ash/ash_output). ASH_MCP_ALLOWED_ROOTS controls which directories the server will accept. It takes a list separated by the platform path separator (: on Linux and macOS, ; on Windows):

ASH_MCP_ALLOWED_ROOTS=/srv/repos:/home/build/work ash mcp

With it set, a scan target must resolve to one of those directories or something beneath it; anything else is refused. Symlinks and .. components are resolved before the comparison, so a link inside an allowed root that points outside it is refused. The per-session MCP workspace root is always allowed as well, so source trees delivered over the protocol stay scannable.

With it unset, ASH refuses a short fixed list of system directories as scan targets — on Linux and macOS /boot, /dev, /etc, /proc, /root, /sys and the filesystem root; on Windows the Windows directory, Program Files, ProgramData and a bare drive root. To scan one of these deliberately, name it in ASH_MCP_ALLOWED_ROOTS, which replaces the list rather than adding to it.

That default is a safety net, not a security boundary. It declines a handful of directories that hold host configuration and kernel interfaces rather than source code, and it does not restrict anything else — home directories, /usr, /var and the rest of the filesystem all remain valid scan targets, because that is where code lives. If you need the scan surface actually bounded, set ASH_MCP_ALLOWED_ROOTS; it is the only setting here that does that.

The same roots apply to the tools that take a caller-supplied results directory: get_scan_results, get_scan_summary, get_scan_result_paths and explain_finding. An output directory for a permitted scan lives at <source_dir>/.ash/ash_output, beneath the target, so a root that allows a scan already allows its results to be read; you do not list output directories separately.

The roots do not govern the tools that take a path to an individual fileconfig_path on get_config, validate_config and run_ash_scan, and the two result files diff_scan_results compares. Those are a separate surface with different requirements: profile configs are conventionally kept outside the scanned tree (the deployment guide puts them in /etc/ash/), so applying the scan roots to them would refuse the documented layout. If you need those restricted, restrict them at the filesystem or with a fronting proxy.

get_scan_progress is not affected. It takes a scan ID, and the output directory comes from the scan registry rather than from the caller, so polling a running scan works regardless of what the roots name.

Best Practices

When using the ASH MCP server, follow these best practices:

  1. Store scan IDs: Always store the scan ID returned by scan_directory for later use
  2. Check scan progress: Periodically check scan progress using get_scan_progress
  3. Handle errors gracefully: Check the success field in responses and handle errors appropriately
  4. Clean up resources: Cancel scans that are no longer needed using cancel_scan
  5. Validate parameters: Ensure all parameters are valid before calling MCP tools
  6. Check installation: Use check_installation to verify ASH is properly installed
  7. Configure appropriately: Adjust configuration options based on your system resources and usage patterns
  8. Monitor resource usage: Keep an eye on system resources during scans
  9. Implement timeouts: Set appropriate timeouts for scans and operations
  10. Handle concurrent scans: Limit the number of concurrent scans based on your system resources

Examples

Basic Scan Workflow

# Start a scan
result = await mcp_scan_directory(
    directory_path="/path/to/your/code",
    severity_threshold="MEDIUM"
)

# Get the scan ID
scan_id = result["scan_id"]

# Check scan progress periodically
while True:
    progress = await mcp_get_scan_progress(scan_id=scan_id)

    if progress["status"] in ["completed", "failed", "cancelled"]:
        break

    # Wait before checking again
    await asyncio.sleep(5)

# Get scan results
if progress["status"] == "completed":
    results = await mcp_get_scan_results(scan_id=scan_id)
    # Process results
else:
    # Handle failure or cancellation
    print(f"Scan {scan_id} {progress['status']}")

Handling Multiple Scans

# Start multiple scans
scan_ids = []
for directory in directories:
    result = await mcp_scan_directory(
        directory_path=directory,
        severity_threshold="MEDIUM"
    )
    scan_ids.append(result["scan_id"])

# Check progress of all scans
completed_scans = set()
while len(completed_scans) < len(scan_ids):
    for scan_id in scan_ids:
        if scan_id in completed_scans:
            continue

        progress = await mcp_get_scan_progress(scan_id=scan_id)

        if progress["status"] in ["completed", "failed", "cancelled"]:
            completed_scans.add(scan_id)

    # Wait before checking again
    await asyncio.sleep(5)

# Get results for all completed scans
for scan_id in scan_ids:
    progress = await mcp_get_scan_progress(scan_id=scan_id)

    if progress["status"] == "completed":
        results = await mcp_get_scan_results(scan_id=scan_id)
        # Process results
    else:
        # Handle failure or cancellation
        print(f"Scan {scan_id} {progress['status']}")

Error Handling Example

# Start a scan with error handling
try:
    result = await mcp_scan_directory(
        directory_path="/path/to/your/code",
        severity_threshold="MEDIUM"
    )

    if not result["success"]:
        # Handle error
        print(f"Error: {result['error']}")
        print(f"Suggestions: {result['suggestions']}")
        return

    scan_id = result["scan_id"]

    # Check scan progress
    while True:
        progress = await mcp_get_scan_progress(scan_id=scan_id)

        if not progress["success"]:
            # Handle error
            print(f"Error: {progress['error']}")
            print(f"Suggestions: {progress['suggestions']}")
            return

        if progress["status"] in ["completed", "failed", "cancelled"]:
            break

        # Wait before checking again
        await asyncio.sleep(5)

    # Get scan results
    if progress["status"] == "completed":
        results = await mcp_get_scan_results(scan_id=scan_id)

        if not results["success"]:
            # Handle error
            print(f"Error: {results['error']}")
            print(f"Suggestions: {results['suggestions']}")
            return

        # Process results
    else:
        # Handle failure or cancellation
        print(f"Scan {scan_id} {progress['status']}")

except Exception as e:
    # Handle unexpected errors
    print(f"Unexpected error: {str(e)}")