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
- Custom Configuration
- Using Baselines
- Deep Mode
- Processing Findings
- Generating SARIF Reports
- MSSS Compliance Verification
- MCP Surface Analysis
- Error Handling
- Concurrent Scanning
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))
}
}