Skip to content

Integration Examples - mcp-scan

This guide provides practical and complete examples of how to integrate mcp-scan in different scenarios.


Table of Contents


Basic Usage

The simplest example of using mcp-scan:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    // Create default configuration
    cfg := scanner.DefaultConfig()

    // Create scanner instance
    s := scanner.New(cfg)

    // Execute scan
    result, err := s.Scan(context.Background(), "./my-mcp-server")
    if err != nil {
        log.Fatalf("Scan error: %v", err)
    }

    // Show summary
    fmt.Printf("Files scanned: %d\n", result.Manifest.TotalFiles)
    fmt.Printf("Total findings: %d\n", len(result.Findings))
    fmt.Printf("Duration: %v\n", result.ScanDuration)

    // Show findings by severity
    for sev, count := range result.Summary.BySeverity {
        fmt.Printf("  %s: %d\n", sev, count)
    }
}

Using the Convenience Function

For simple scans, use the Scan function directly:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    result, err := scanner.Scan(
        context.Background(),
        "./my-mcp-server",
        scanner.DefaultConfig(),
    )
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Found %d findings\n", len(result.Findings))
}

Custom Configuration

Example of advanced configuration:

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/mcphub/mcp-scan/pkg/scanner"
    "github.com/mcphub/mcp-scan/internal/types"
)

func main() {
    cfg := scanner.Config{
        // Only scan Python files
        Include: []string{"**/*.py"},

        // Exclude tests and virtualenvs
        Exclude: []string{
            "**/test/**",
            "**/tests/**",
            "**/*_test.py",
            "**/venv/**",
            "**/.venv/**",
            "**/site-packages/**",
        },

        // Fast mode for CI/CD
        Mode: scanner.ModeFast,

        // 2 minute timeout
        Timeout: 2 * time.Minute,

        // Fail if there are high or critical findings
        FailOn: types.SeverityHigh,

        // Only run class A (RCE) and E (secrets) rules
        Rules: []string{"MCP-A*", "MCP-E*"},

        // Use 4 workers
        Workers: 4,

        // Don't redact snippets (we want to see the code)
        RedactSnippets: false,
    }

    s := scanner.New(cfg)
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    // ExitCode will be 1 if there are high or critical findings
    if result.ExitCode != 0 {
        fmt.Println("FAILED: High severity findings found")
        for _, f := range result.Findings {
            if f.Severity == types.SeverityHigh || f.Severity == types.SeverityCritical {
                fmt.Printf("  - [%s] %s at %s\n",
                    f.Severity, f.RuleID, f.Location.String())
            }
        }
    }
}

Using Baselines

Managing accepted findings with baselines:

Create Baseline from Scan

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    s := scanner.New(scanner.DefaultConfig())

    // First scan
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Findings found: %d\n", len(result.Findings))

    // Create baseline from all current findings
    baseline := s.GenerateBaseline(
        result,
        "Findings reviewed in initial audit Q1-2026",
        "security-team@example.com",
    )

    // Save baseline
    if err := scanner.SaveBaseline(baseline, ".mcp-scan-baseline.json"); err != nil {
        log.Fatal(err)
    }

    fmt.Println("Baseline saved to .mcp-scan-baseline.json")
}

Scan with Baseline

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    // Configuration with baseline
    cfg := scanner.DefaultConfig()
    cfg.Baseline = ".mcp-scan-baseline.json"

    s := scanner.New(cfg)

    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("New findings: %d\n", len(result.Findings))
    fmt.Printf("Findings filtered by baseline: %d\n", result.BaselinedCount)

    // Only process new findings
    if len(result.Findings) > 0 {
        fmt.Println("\nNew findings detected:")
        for _, f := range result.Findings {
            fmt.Printf("  [%s] %s - %s:%d\n",
                f.Severity, f.RuleID, f.Location.File, f.Location.StartLine)
        }
    }
}

Apply Baseline Manually

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    s := scanner.New(scanner.DefaultConfig())

    // Scan without baseline
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Total findings: %d\n", len(result.Findings))

    // Load and apply baseline manually
    baseline, err := scanner.LoadBaseline(".mcp-scan-baseline.json")
    if err != nil {
        log.Fatal(err)
    }

    filteredCount := s.ApplyBaseline(result, baseline)
    fmt.Printf("Filtered: %d\n", filteredCount)
    fmt.Printf("Remaining findings: %d\n", len(result.Findings))
}

Combine Multiple Baselines

package main

import (
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    // Load existing baselines
    baseline1, err := scanner.LoadBaseline("baseline-team-a.json")
    if err != nil {
        log.Fatal(err)
    }

    baseline2, err := scanner.LoadBaseline("baseline-team-b.json")
    if err != nil {
        log.Fatal(err)
    }

    // Combine into one
    added := baseline1.Merge(baseline2)
    fmt.Printf("Entries added from baseline-team-b: %d\n", added)

    // Save combined baseline
    if err := scanner.SaveBaseline(baseline1, "baseline-combined.json"); err != nil {
        log.Fatal(err)
    }

    // View statistics
    stats := baseline1.GetStats()
    fmt.Printf("Total entries: %d\n", stats.TotalEntries)
    fmt.Println("By rule:")
    for ruleID, count := range stats.ByRuleID {
        fmt.Printf("  %s: %d\n", ruleID, count)
    }
}

Deep Mode

Using inter-procedural analysis for higher precision:

package main

import (
    "context"
    "fmt"
    "log"
    "time"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    cfg := scanner.Config{
        Include: []string{"**/*.py", "**/*.ts"},
        Exclude: []string{"**/node_modules/**", "**/venv/**"},

        // Deep mode for inter-procedural analysis
        Mode: scanner.ModeDeep,

        // Longer timeout for deep analysis
        Timeout: 15 * time.Minute,

        // More workers to process call graph
        Workers: 8,
    }

    s := scanner.New(cfg)

    ctx := context.Background()
    result, err := s.Scan(ctx, "./my-project")
    if err != nil {
        log.Fatal(err)
    }

    fmt.Printf("Mode: %s\n", result.Mode)
    fmt.Printf("Duration: %v\n", result.ScanDuration)
    fmt.Printf("Findings: %d\n", len(result.Findings))

    // Findings in deep mode may have longer traces
    for _, f := range result.Findings {
        if f.Trace != nil && len(f.Trace.Steps) > 2 {
            fmt.Printf("\nFinding with inter-procedural trace:\n")
            fmt.Printf("  Rule: %s\n", f.RuleID)
            fmt.Printf("  Source: %s\n", f.Trace.Source.String())
            fmt.Printf("  Sink: %s\n", f.Trace.Sink.String())
            fmt.Printf("  Steps: %d\n", len(f.Trace.Steps))
            for i, step := range f.Trace.Steps {
                fmt.Printf("    %d. [%s] %s at %s\n",
                    i+1, step.Action, step.Variable, step.Location.String())
            }
        }
    }
}

Processing Findings

Examples of how to process and filter findings:

Filter by Severity

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
    "github.com/mcphub/mcp-scan/internal/types"
)

func filterBySeverity(findings []types.Finding, minSeverity types.Severity) []types.Finding {
    var filtered []types.Finding
    minLevel := minSeverity.Level()

    for _, f := range findings {
        if f.Severity.Level() >= minLevel {
            filtered = append(filtered, f)
        }
    }
    return filtered
}

func main() {
    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    // Only high and critical findings
    severe := filterBySeverity(result.Findings, types.SeverityHigh)

    fmt.Printf("Total: %d, Severe: %d\n",
        len(result.Findings), len(severe))

    for _, f := range severe {
        fmt.Printf("[%s] %s: %s\n", f.Severity, f.RuleID, f.Title)
    }
}

Group by Vulnerability Class

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
    "github.com/mcphub/mcp-scan/internal/types"
)

func groupByClass(findings []types.Finding) map[types.VulnClass][]types.Finding {
    groups := make(map[types.VulnClass][]types.Finding)
    for _, f := range findings {
        groups[f.Class] = append(groups[f.Class], f)
    }
    return groups
}

func main() {
    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    groups := groupByClass(result.Findings)

    classNames := map[types.VulnClass]string{
        types.ClassA: "RCE (Remote Code Execution)",
        types.ClassB: "Filesystem Traversal",
        types.ClassC: "SSRF/Network",
        types.ClassD: "SQL Injection",
        types.ClassE: "Secrets/Tokens",
        types.ClassF: "Auth/OAuth",
        types.ClassG: "Tool Poisoning",
    }

    for class, findings := range groups {
        name := classNames[class]
        if name == "" {
            name = string(class)
        }
        fmt.Printf("\n%s (%d findings):\n", name, len(findings))
        for _, f := range findings {
            fmt.Printf("  - %s: %s (%s:%d)\n",
                f.RuleID, f.Title, f.Location.File, f.Location.StartLine)
        }
    }
}

Find Findings in MCP Context

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
    "github.com/mcphub/mcp-scan/internal/types"
)

func findingsInMCPTools(findings []types.Finding) []types.Finding {
    var inTools []types.Finding
    for _, f := range findings {
        if f.MCPContext != nil && f.MCPContext.ToolName != "" {
            inTools = append(inTools, f)
        }
    }
    return inTools
}

func main() {
    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    toolFindings := findingsInMCPTools(result.Findings)

    fmt.Printf("Findings in MCP tools: %d of %d\n",
        len(toolFindings), len(result.Findings))

    for _, f := range toolFindings {
        fmt.Printf("\n[%s] %s\n", f.Severity, f.RuleID)
        fmt.Printf("  Tool: %s\n", f.MCPContext.ToolName)
        fmt.Printf("  Handler: %s\n", f.MCPContext.HandlerName)
        fmt.Printf("  Location: %s\n", f.Location.String())
        if f.Evidence.Snippet != "" {
            fmt.Printf("  Code:\n    %s\n", f.Evidence.Snippet)
        }
    }
}

Generating SARIF Reports

Generate SARIF reports for GitHub/GitLab integration:

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    s := scanner.New(scanner.DefaultConfig())

    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    // Generate SARIF report
    sarifReport, err := s.GenerateReport(result, "sarif")
    if err != nil {
        log.Fatal(err)
    }

    // Save SARIF file
    if err := os.WriteFile("scan-results.sarif", sarifReport, 0644); err != nil {
        log.Fatal(err)
    }

    fmt.Println("SARIF report saved to scan-results.sarif")

    // Also generate JSON for internal analysis
    jsonReport, err := s.GenerateReport(result, "json")
    if err != nil {
        log.Fatal(err)
    }

    if err := os.WriteFile("scan-results.json", jsonReport, 0644); err != nil {
        log.Fatal(err)
    }

    fmt.Println("JSON report saved to scan-results.json")

    // Generate evidence bundle for audit
    evidenceReport, err := s.GenerateReport(result, "evidence")
    if err != nil {
        log.Fatal(err)
    }

    if err := os.WriteFile("scan-evidence.json", evidenceReport, 0644); err != nil {
        log.Fatal(err)
    }

    fmt.Println("Evidence bundle saved to scan-evidence.json")
}

Upload SARIF to GitHub

package main

import (
    "bytes"
    "context"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "os"
    "time"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func uploadSARIFToGitHub(sarifReport []byte, repo, sha, ref string) error {
    token := os.Getenv("GITHUB_TOKEN")
    if token == "" {
        return fmt.Errorf("GITHUB_TOKEN not configured")
    }

    // Compress and encode in base64
    encoded := base64.StdEncoding.EncodeToString(sarifReport)

    payload := map[string]interface{}{
        "commit_sha": sha,
        "ref":        ref,
        "sarif":      encoded,
    }

    jsonPayload, err := json.Marshal(payload)
    if err != nil {
        return err
    }

    url := fmt.Sprintf("https://api.github.com/repos/%s/code-scanning/sarifs", repo)

    req, err := http.NewRequest("POST", url, bytes.NewReader(jsonPayload))
    if err != nil {
        return err
    }

    req.Header.Set("Authorization", "Bearer "+token)
    req.Header.Set("Accept", "application/vnd.github+json")
    req.Header.Set("Content-Type", "application/json")

    client := &http.Client{Timeout: 30 * time.Second}
    resp, err := client.Do(req)
    if err != nil {
        return err
    }
    defer resp.Body.Close()

    if resp.StatusCode != http.StatusAccepted && resp.StatusCode != http.StatusOK {
        return fmt.Errorf("GitHub API error: %s", resp.Status)
    }

    return nil
}

func main() {
    s := scanner.New(scanner.DefaultConfig())

    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    sarifReport, err := s.GenerateReport(result, "sarif")
    if err != nil {
        log.Fatal(err)
    }

    // CI/CD variables
    repo := os.Getenv("GITHUB_REPOSITORY")   // "owner/repo"
    sha := os.Getenv("GITHUB_SHA")           // commit sha
    ref := os.Getenv("GITHUB_REF")           // "refs/heads/main"

    if err := uploadSARIFToGitHub(sarifReport, repo, sha, ref); err != nil {
        log.Fatalf("Error uploading SARIF: %v", err)
    }

    fmt.Println("SARIF uploaded to GitHub Code Scanning")
}

MSSS Compliance Verification

Verify if a server meets security standards:

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    s := scanner.New(scanner.DefaultConfig())

    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    score := result.MSSSScore

    fmt.Println("=== MSSS Compliance Report ===")
    fmt.Printf("MSSS Version: %s\n", score.Version)
    fmt.Printf("Total Score: %.1f/100\n", score.Total)
    fmt.Printf("Level: %d\n", score.Level)

    levelNames := []string{
        "Non-Compliant",
        "Basic",
        "Enterprise",
        "Certified",
    }
    fmt.Printf("Status: %s\n", levelNames[score.Level])

    fmt.Println("\n--- Score by Category ---")
    for class, cat := range score.Categories {
        if cat.Findings > 0 {
            fmt.Printf("  Class %s: %.1f/%.1f (-%d findings)\n",
                class, cat.Score, cat.MaxScore, cat.Findings)
        } else {
            fmt.Printf("  Class %s: %.1f/%.1f (clean)\n",
                class, cat.Score, cat.MaxScore)
        }
    }

    // Calculation details
    if score.Details != nil {
        fmt.Printf("\n--- Details ---\n")
        fmt.Printf("  Base score: %.1f\n", score.Details.BaseScore)
        fmt.Printf("  Total penalties: %.1f\n", score.Details.Penalties)
    }

    // Check requirements by level
    fmt.Println("\n--- Requirements ---")
    fmt.Printf("  Level 1 (Basic):      Score >= 60, <= 3 high    %s\n",
        checkMark(score.Total >= 60))
    fmt.Printf("  Level 2 (Enterprise): Score >= 80, 0 high       %s\n",
        checkMark(score.Total >= 80 && score.Level >= 2))
    fmt.Printf("  Level 3 (Certified):  Score >= 90, 0 high       %s\n",
        checkMark(score.Total >= 90 && score.Level >= 3))

    // Exit with appropriate code
    if !score.Compliant {
        fmt.Println("\nFAILED: Does not meet minimum security level")
        os.Exit(1)
    }

    fmt.Printf("\nOK: Complies with level %d (%s)\n",
        score.Level, levelNames[score.Level])
}

func checkMark(ok bool) string {
    if ok {
        return "[OK]"
    }
    return "[X]"
}

Verify Minimum Required Level

package main

import (
    "context"
    "fmt"
    "log"
    "os"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    // Minimum required level (configurable)
    requiredLevel := 2 // Enterprise

    s := scanner.New(scanner.DefaultConfig())
    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    actualLevel := result.MSSSScore.Level

    if actualLevel < requiredLevel {
        fmt.Printf("FAILED: Current level %d, required %d\n",
            actualLevel, requiredLevel)

        // Show what's needed to reach the level
        if result.MSSSScore.Total < 80 {
            fmt.Printf("  - Need %.1f more points to reach 80\n",
                80-result.MSSSScore.Total)
        }

        // Count high findings
        highCount := result.Summary.BySeverity["high"]
        if highCount > 0 {
            fmt.Printf("  - Must resolve %d high severity findings\n",
                highCount)
        }

        os.Exit(1)
    }

    fmt.Printf("OK: Level %d meets minimum requirement of %d\n",
        actualLevel, requiredLevel)
}

MCP Surface Analysis

Examine the MCP attack surface of a server:

package main

import (
    "context"
    "fmt"
    "log"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    s := scanner.New(scanner.DefaultConfig())

    result, err := s.Scan(context.Background(), "./src")
    if err != nil {
        log.Fatal(err)
    }

    surf := result.MCPSurface

    fmt.Println("=== Detected MCP Surface ===")

    // Transport
    fmt.Printf("\nTransport: %s\n", surf.Transport)

    // Tools
    fmt.Printf("\nMCP Tools (%d):\n", len(surf.Tools))
    for i, tool := range surf.Tools {
        fmt.Printf("  %d. %s\n", i+1, tool.Name)
        if tool.Description != "" {
            fmt.Printf("     Description: %s\n", tool.Description)
        }
        if tool.Handler != nil {
            fmt.Printf("     Handler: %s in %s\n",
                tool.Handler.FunctionName, tool.Handler.FileName)
        }
        fmt.Printf("     Location: %s\n", tool.Location.String())
    }

    // Resources
    if len(surf.Resources) > 0 {
        fmt.Printf("\nMCP Resources (%d):\n", len(surf.Resources))
        for i, res := range surf.Resources {
            fmt.Printf("  %d. %s\n", i+1, res.Name)
            if res.Handler != nil {
                fmt.Printf("     Handler: %s\n", res.Handler.FunctionName)
            }
        }
    }

    // Prompts
    if len(surf.Prompts) > 0 {
        fmt.Printf("\nMCP Prompts (%d):\n", len(surf.Prompts))
        for i, prompt := range surf.Prompts {
            fmt.Printf("  %d. %s\n", i+1, prompt.Name)
        }
    }

    // Auth signals
    if len(surf.AuthSignals) > 0 {
        fmt.Printf("\nAuthentication Signals (%d):\n", len(surf.AuthSignals))
        for _, signal := range surf.AuthSignals {
            fmt.Printf("  - %s: %s at %s\n",
                signal.Type, signal.Name, signal.Location.String())
        }
    }

    // Attack surface summary
    fmt.Println("\n=== Attack Surface Summary ===")
    fmt.Printf("  Total entry points: %d\n",
        len(surf.Tools)+len(surf.Resources)+len(surf.Prompts))
    fmt.Printf("  Tools (high risk): %d\n", len(surf.Tools))
    fmt.Printf("  Resources: %d\n", len(surf.Resources))
    fmt.Printf("  Prompts: %d\n", len(surf.Prompts))

    if surf.Transport == "http" || surf.Transport == "websocket" {
        fmt.Println("  ALERT: Network transport detected - verify authentication")
    }
}

Error Handling

Examples of robust error handling:

package main

import (
    "context"
    "errors"
    "fmt"
    "log"
    "os"
    "time"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func scanWithErrorHandling(path string) (*scanner.Result, error) {
    cfg := scanner.Config{
        Include: []string{"**/*.py", "**/*.ts", "**/*.js"},
        Exclude: []string{"**/node_modules/**"},
        Mode:    scanner.ModeFast,
        Timeout: 5 * time.Minute,
    }

    s := scanner.New(cfg)

    // Create context with timeout
    ctx, cancel := context.WithTimeout(context.Background(), cfg.Timeout)
    defer cancel()

    result, err := s.Scan(ctx, path)
    if err != nil {
        // Check error type
        if errors.Is(err, context.DeadlineExceeded) {
            return nil, fmt.Errorf("timeout: scan exceeded %v", cfg.Timeout)
        }
        if errors.Is(err, context.Canceled) {
            return nil, fmt.Errorf("canceled: scan was canceled")
        }

        // Other errors
        return nil, fmt.Errorf("scan error: %w", err)
    }

    return result, nil
}

func main() {
    path := "./src"

    // Verify that the path exists
    if _, err := os.Stat(path); os.IsNotExist(err) {
        log.Fatalf("Error: path '%s' does not exist", path)
    }

    result, err := scanWithErrorHandling(path)
    if err != nil {
        log.Fatal(err)
    }

    // Check if there are files
    if result.Manifest.TotalFiles == 0 {
        fmt.Println("Warning: no files found to scan")
        return
    }

    fmt.Printf("Scan completed: %d files, %d findings\n",
        result.Manifest.TotalFiles, len(result.Findings))
}

Manual Cancellation

package main

import (
    "context"
    "fmt"
    "log"
    "os"
    "os/signal"
    "syscall"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func main() {
    s := scanner.New(scanner.DefaultConfig())

    // Create cancelable context
    ctx, cancel := context.WithCancel(context.Background())

    // Capture Ctrl+C
    sigChan := make(chan os.Signal, 1)
    signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

    go func() {
        <-sigChan
        fmt.Println("\nCancellation signal received...")
        cancel()
    }()

    // Execute scan
    result, err := s.Scan(ctx, "./src")
    if err != nil {
        if ctx.Err() != nil {
            fmt.Println("Scan canceled by user")
            os.Exit(130) // Standard code for Ctrl+C
        }
        log.Fatal(err)
    }

    fmt.Printf("Findings: %d\n", len(result.Findings))
}

Concurrent Scanning

Scan multiple directories in parallel:

package main

import (
    "context"
    "fmt"
    "log"
    "sync"
    "time"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

type ScanJob struct {
    Path string
    Name string
}

type ScanResult struct {
    Job    ScanJob
    Result *scanner.Result
    Error  error
}

func scanMultiple(jobs []ScanJob) []ScanResult {
    cfg := scanner.DefaultConfig()
    cfg.Timeout = 3 * time.Minute

    var wg sync.WaitGroup
    results := make(chan ScanResult, len(jobs))

    // Create one scanner per goroutine
    for _, job := range jobs {
        wg.Add(1)
        go func(j ScanJob) {
            defer wg.Done()

            s := scanner.New(cfg)
            result, err := s.Scan(context.Background(), j.Path)

            results <- ScanResult{
                Job:    j,
                Result: result,
                Error:  err,
            }
        }(job)
    }

    // Wait for completion
    go func() {
        wg.Wait()
        close(results)
    }()

    // Collect results
    var allResults []ScanResult
    for r := range results {
        allResults = append(allResults, r)
    }

    return allResults
}

func main() {
    jobs := []ScanJob{
        {Path: "./service-a", Name: "Service A"},
        {Path: "./service-b", Name: "Service B"},
        {Path: "./service-c", Name: "Service C"},
    }

    fmt.Printf("Scanning %d projects in parallel...\n", len(jobs))

    results := scanMultiple(jobs)

    totalFindings := 0
    for _, r := range results {
        if r.Error != nil {
            fmt.Printf("\n[%s] ERROR: %v\n", r.Job.Name, r.Error)
            continue
        }

        findings := len(r.Result.Findings)
        totalFindings += findings

        fmt.Printf("\n[%s]\n", r.Job.Name)
        fmt.Printf("  Files: %d\n", r.Result.Manifest.TotalFiles)
        fmt.Printf("  Findings: %d\n", findings)
        fmt.Printf("  MSSS Score: %.1f (Level %d)\n",
            r.Result.MSSSScore.Total, r.Result.MSSSScore.Level)
    }

    fmt.Printf("\n=== Total: %d findings in %d projects ===\n",
        totalFindings, len(jobs))
}

Limit Concurrency

package main

import (
    "context"
    "fmt"
    "sync"

    "github.com/mcphub/mcp-scan/pkg/scanner"
)

func scanWithSemaphore(paths []string, maxConcurrent int) map[string]*scanner.Result {
    cfg := scanner.DefaultConfig()
    results := make(map[string]*scanner.Result)
    var mu sync.Mutex

    sem := make(chan struct{}, maxConcurrent)
    var wg sync.WaitGroup

    for _, path := range paths {
        wg.Add(1)
        go func(p string) {
            defer wg.Done()

            // Acquire slot
            sem <- struct{}{}
            defer func() { <-sem }()

            s := scanner.New(cfg)
            result, err := s.Scan(context.Background(), p)
            if err != nil {
                fmt.Printf("Error in %s: %v\n", p, err)
                return
            }

            mu.Lock()
            results[p] = result
            mu.Unlock()
        }(path)
    }

    wg.Wait()
    return results
}

func main() {
    paths := []string{
        "./project-1",
        "./project-2",
        "./project-3",
        "./project-4",
        "./project-5",
    }

    // Maximum 3 simultaneous scans
    results := scanWithSemaphore(paths, 3)

    for path, result := range results {
        fmt.Printf("%s: %d findings\n", path, len(result.Findings))
    }
}