"""credential-free mcp server for secret detection"""
import json
import logging
from typing import Literal, Optional, List
from mcp.server.fastmcp import FastMCP
from .patterns import SECRET_PATTERNS, get_pattern_count
from .scanner import SecretScanner
from .utils import mask_secret
logger = logging.getLogger(__name__)
mcp = FastMCP("credential-free")
scanner = SecretScanner()
def finding_to_dict(finding) -> dict:
return {
"type": finding.type,
"value": mask_secret(finding.value, visible_chars=4),
"file_path": finding.file_path,
"line_number": finding.line_number,
"severity": finding.severity,
"category": finding.category,
"entropy": finding.entropy,
}
@mcp.tool()
def scan_file(
file_path: str,
format_hint: Optional[str] = None,
profile: Literal["fast", "balanced", "deep"] = "balanced",
) -> str:
"""
Scan a file for secrets.
Args:
file_path: Path to file
format_hint: Override format (e.g. "text:json", "archive:zip")
profile: "fast", "balanced", or "deep"
"""
try:
findings = scanner.scan_file(file_path, format_hint=format_hint, profile=profile)
results = [finding_to_dict(f) for f in findings if not f.is_false_positive]
return json.dumps({"success": True, "findings": results, "count": len(results)})
except Exception as e:
logger.error(f"scan_file error: {e}")
return json.dumps({"success": False, "error": str(e)})
@mcp.tool()
def scan_directory(
directory_path: str,
profile: Literal["fast", "balanced", "deep"] = "balanced",
exclude_patterns: Optional[List[str]] = None,
) -> str:
"""
Scan a directory for secrets.
Args:
directory_path: Path to directory
profile: "fast", "balanced", or "deep"
exclude_patterns: List of regex patterns to exclude files
"""
try:
result = scanner.scan_directory(directory_path, profile=profile, exclude_patterns=exclude_patterns)
# Convert Finding objects to dictionaries and mask secrets
findings_dict = [finding_to_dict(f) for f in result.get("findings", []) if not f.is_false_positive]
return json.dumps({
"success": True,
"findings": findings_dict,
"count": len(findings_dict),
"stats": result.get("stats", {})
})
except Exception as e:
logger.error(f"scan_directory error: {e}")
return json.dumps({"success": False, "error": str(e)})
@mcp.tool()
def scan_content(content: str, file_name: str = "content.txt") -> str:
"""
Scan text content for secrets.
Args:
content: Text to scan
file_name: Virtual filename for context
"""
try:
findings = scanner.scan_content(content, file_name)
results = [finding_to_dict(f) for f in findings if not f.is_false_positive]
return json.dumps({"success": True, "findings": results, "count": len(results)})
except Exception as e:
logger.error(f"scan_content error: {e}")
return json.dumps({"success": False, "error": str(e)})
@mcp.tool()
def get_patterns() -> str:
"""Get available detection patterns."""
patterns = [{"name": p.name, "category": p.category, "severity": p.severity} for p in SECRET_PATTERNS]
return json.dumps({"count": get_pattern_count(), "patterns": patterns})
@mcp.resource("patterns://list")
def patterns_resource() -> str:
"""Detection patterns."""
return get_patterns()
def main():
mcp.run()
if __name__ == "__main__":
main()