Skip to content

Back to main README


marp: true theme: default class: lead paginate: true backgroundColor: #fff color: #333


SimpleCovMCP

MCP Server, CLI, and Library for SimpleCov Ruby Test Coverage

  • Keith Bennett
  • First presented to PhRUG (Philippines Ruby User Group), 2025-10-01

What is cov-loupe?

A three-in-one gem that makes SimpleCov coverage data accessible to:

  • πŸ€– AI agents via Model Context Protocol (MCP)
  • πŸ’» Command line via its command line interface
  • πŸ“š Ruby scripts and applications as a library

Lazy dependency on SimpleCov - single-suite resultsets avoid loading it; multi-suite files trigger a merge via SimpleCov’s combine helpers.

What is it not? It is not a replacement for SimpleCov's generated web presentation of the coverage data.

This code base requires a Ruby version >= 3.2.0, because this is required by the mcp gem it uses.


High Level Objectives

  • Query coverage programmatically
  • Integrate with AI tools
  • Automate coverage analysis
  • Focus on specific files/patterns

Key Features

  • Lazy SimpleCov dependency - only loaded when multi-suite resultsets need merging
  • Flexible resultset location - via CLI flags, passed parameter, or env var
  • Staleness detection - warns or optionally errors when files newer than coverage
  • JSON output - perfect for jq, scripts, CI/CD
  • Source code integration - show uncovered lines with or without context
  • Colored output - readable terminal display

Demo 1: MCP Server Mode

AI Coverage Assistant

# Test the MCP server manually
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"coverage_summary_tool","arguments":{"path":"lib/cov_loupe/model.rb"}}}' | cov-loupe

What AI agents can do: - Analyze coverage gaps - Suggest testing priorities
- Generate ad-hoc coverage reports


MCP Tools (Functions) Available

Tool Purpose Example CLI Command
list_coverage_tool Project-wide coverage data cov-loupe list
coverage_detailed_tool Per-line hit counts cov-loupe detailed lib/cov_loupe/model.rb
coverage_raw_tool Raw SimpleCov lines array cov-loupe raw lib/cov_loupe/model.rb
coverage_summary_tool Get coverage % for a file cov-loupe summary lib/cov_loupe/model.rb
coverage_table_tool Formatted coverage table cov-loupe list
coverage_totals_tool Aggregated line totals cov-loupe totals
help_tool Tool usage guidance cov-loupe --help
uncovered_lines_tool Find missing test coverage cov-loupe uncovered lib/cov_loupe/cli.rb
version_tool Display version info cov-loupe version

Demo 2: CLI Tool

# Show all files, worst coverage first
cov-loupe

# Focus on a specific file
cov-loupe summary lib/cov_loupe/cli.rb

# Find untested lines with source context
cov-loupe uncovered lib/cov_loupe/cli.rb --source=uncovered --source-context 3

# JSON for scripts
cov-loupe -fJ | jq '.files[] | select(.percentage < 80)'

# Ruby alternative:
cov-loupe -fJ | ruby -r json -e '
  JSON.parse($stdin.read)["files"].select { |f| f["percentage"] < 80 }.each do |f|
    puts JSON.pretty_generate(f)
  end
'

# Rexe alternative:
cov-loupe -fJ | rexe -ij -mb -oJ 'self["files"].select { |f| f["percentage"] < 80 }'

Demo 2: CLI Tool (cont'd.)

# Custom resultset location
cov-loupe --resultset coverage-all/

# Sort by highest coverage
cov-loupe --sort-order d

# Staleness checking (file newer than coverage?)
cov-loupe --staleness error

# Track new files missing from coverage
cov-loupe --tracked-globs "lib/**/tools/*.rb"

Demo 3: Ruby Library

Programmatic Integration

require 'cov_loupe'

model = CovLoupe::CoverageModel.new

# Get project overview
files = model.list
puts "Lowest coverage: #{files.first['percentage']}%"

# Focus on specific concerns
uncovered = model.uncovered_for("lib/wifi-wand/models/ubuntu_model.rb")
puts "Uncovered hash's keys: #{uncovered.keys.inspect}" 
puts "Missing lines: #{uncovered['uncovered'].inspect}"

# Output:
# Lowest coverage: 17.0%
# Uncovered hash's keys: ["file", "uncovered", "summary"]
# Missing lines: [13, 17, 21,...200, 203]

Custom Threshold Git Pre-Commit Hook

require 'cov_loupe'

files = CovLoupe::CoverageModel.new.list
critical, other = files.partition { |f| f['file'].include?('/lib/critical/') }

fails = critical.select { |f| f['percentage'] < 100.0 } + 
        other.select { |f| f['percentage'] < 90.0 }

if fails.any?
  puts "❌ Coverage failures:"
  fails.each { |f| puts "  #{f['file']}: #{f['percentage']}%" }
  exit 1
else
  puts "βœ… All thresholds met!"
end

Architecture Overview

lib/cov_loupe
β”œβ”€β”€ base_tool.rb
β”œβ”€β”€ cli.rb
β”œβ”€β”€ error_handler_factory.rb
β”œβ”€β”€ error_handler.rb
β”œβ”€β”€ errors.rb
β”œβ”€β”€ mcp_server.rb
β”œβ”€β”€ model.rb
β”œβ”€β”€ path_relativizer.rb
β”œβ”€β”€ staleness_checker.rb
β”œβ”€β”€ tools
β”‚ β”œβ”€β”€ list_coverage_tool.rb
β”‚ β”œβ”€β”€ coverage_detailed_tool.rb
β”‚ β”œβ”€β”€ coverage_raw_tool.rb
β”‚ β”œβ”€β”€ coverage_summary_tool.rb
β”‚ β”œβ”€β”€ coverage_table_tool.rb
β”‚ β”œβ”€β”€ coverage_totals_tool.rb
β”‚ β”œβ”€β”€ help_tool.rb
β”‚ β”œβ”€β”€ uncovered_lines_tool.rb
β”‚ └── version_tool.rb
β”œβ”€β”€ util.rb
└── version.rb

Clean separation: CLI ↔ Model ↔ MCP Tools


MCP Plumbing - the MCP Gem

BaseTool subclasses the mcp gem's Tool class and defines a schema (see base_tool.rb):

class BaseTool < ::MCP::Tool
  # ...
end

The MCP tools available to the model subclass BaseTool and implement their respective tasks.

mcp_server.rb creates an instance of the mcp gem's Server class and runs it:

server = ::MCP::Server.new(
  name:    'cov-loupe',
  version: CovLoupe::VERSION,
  tools:   tools
)
::MCP::Server::Transports::StdioTransport.new(server).open

Questions?

Demo requests: - Specific MCP tool usage? - CLI workflow examples?
- Library integration patterns? - AI assistant setup?

Contact: - GitHub issues for bugs/features - Ruby community discussions

Thank you! πŸ™

Making test coverage accessible to humans and AI alike