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:
- MCP Server: The main server component that registers and exposes MCP tools
- Scan Registry: A thread-safe registry that tracks active scans
- File-Based Tracking: A system that uses file existence to track scan progress
- Error Handling: Comprehensive error handling for various scenarios
File-Based Tracking Approach
The file-based tracking approach works as follows:
- When a scan is started, a unique scan ID is generated and registered in the scan registry
- The scan process is started asynchronously
- The scan progress is tracked by checking for the existence of result files:
ash_aggregated_results.json: Indicates the scan has completed- Individual scanner result files: Indicate which scanners have completed
- 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:
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:
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:
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:
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:
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: Alwaysfalsefor error responsesoperation: The operation that was being performederror: Error messageerror_type: Type of errorerror_category: Category of errorcontext: Additional context about the errorsuggestions: 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 foundpermission_denied: Permission issues accessing files or directoriesinvalid_format: File format or structure is invalidinvalid_parameter: Invalid parameter values providedinvalid_path: Path is invalid or improperly formattedresource_exhausted: System resources are exhaustedoperation_timeout: Operation took too long and timed outscan_not_found: Requested scan ID was not found in the registryscan_incomplete: Scan has not completed yetunexpected_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 scansmax_concurrent_tasks: Maximum number of concurrent tasksthread_pool_max_workers: Maximum number of worker threadsscan_timeout_seconds: Timeout for scans in secondsoperation_timeout_seconds: Timeout for operations in secondsenable_health_checks: Whether to enable health checkshealth_check_interval_seconds: Interval between health checks in secondsmemory_warning_threshold_mb: Memory usage warning threshold in MBmemory_critical_threshold_mb: Memory usage critical threshold in MBtask_count_warning_threshold: Task count warning thresholdmax_message_size_bytes: Maximum message size in bytesmax_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):
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 file —
config_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:
- Store scan IDs: Always store the scan ID returned by
scan_directoryfor later use - Check scan progress: Periodically check scan progress using
get_scan_progress - Handle errors gracefully: Check the
successfield in responses and handle errors appropriately - Clean up resources: Cancel scans that are no longer needed using
cancel_scan - Validate parameters: Ensure all parameters are valid before calling MCP tools
- Check installation: Use
check_installationto verify ASH is properly installed - Configure appropriately: Adjust configuration options based on your system resources and usage patterns
- Monitor resource usage: Keep an eye on system resources during scans
- Implement timeouts: Set appropriate timeouts for scans and operations
- 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)}")