Application Architecture¶
This document describes the core architectural decisions that shape how cov-loupe operates: its dual-mode design and context-aware error handling strategy.
Table of Contents¶
Dual-Mode Operation (CLI and MCP Server)¶
Status¶
Accepted
Context¶
cov-loupe needed to serve two distinct use cases:
- Human users wanting a command-line tool to inspect coverage reports in their terminal
- AI agents and MCP clients needing programmatic access to coverage data via the Model Context Protocol (MCP) over JSON-RPC
We considered three approaches:
- Separate binaries/gems: Create
simplecov-cliandcov-loupeas separate projects - Single binary with explicit mode flags: Require users to pass
--mode mcpto run as MCP server - Automatic mode detection: Single binary that automatically detects the operating mode based on input (TTY status, stdin)
Key Constraints¶
- MCP servers communicate via JSON-RPC over stdin/stdout, so any human-readable output would corrupt the protocol
- CLI users expect immediate, readable output without ceremony
- The gem should be simple to install and use for both audiences
- Mode selection must be reliable and unambiguous
Decision (v4.0.0+)¶
We implemented explicit mode selection via the -m/--mode flag. The default mode is cli, and MCP users must pass -m mcp or --mode mcp to run the server.
Mode Selection Logic¶
The mode is determined by parsing the -m/--mode flag from argv (including environment variables via COV_LOUPE_OPTS):
- Default: CLI mode (when
-m/--modeis not specified) - MCP mode: Must explicitly pass
-m mcpor--mode mcp
The implementation parses the configuration from the command-line arguments and routes to either CoverageCLI or MCPServer based on the mode setting.
Why This Works¶
- MCP clients are configured once with
-m mcpor--mode mcpin their server config → always routes to MCP server - CLI users don't need to specify anything → defaults to CLI mode
- No ambiguity: Mode is explicit and deterministic based on the
-m/--modeflag
Historical Note¶
Prior to v4.0.0, cov-loupe used automatic mode detection based on TTY status and presence of subcommands. This was removed because: - Automatic detection caused issues with piped input (cov-loupe --format json > output.json would hang in MCP mode) - CI environments and non-TTY contexts were unpredictable - CLI-only flags without subcommands (--format, --sort-order) couldn't be reliably detected - Explicit mode selection is more predictable and follows standard practice for language servers
Consequences¶
Positive¶
- User convenience: Single gem to install (
gem install cov-loupe), single executable (cov-loupe) - Predictable behavior: Mode is explicit and deterministic - no surprises based on environment
- Simpler implementation: No complex mode detection logic to maintain
- Clear separation: CLI and MCP server implementations remain completely separate after routing
- Follows conventions: Matches standard practice for language servers (e.g.,
typescript-language-server --stdio)
Negative¶
- Breaking change: Users upgrading from v3.x must update MCP server configuration to include
-m mcpor--mode mcp - Slight verbosity: MCP users must include
-m mcpor--mode mcpin their server config (but this is one-time setup) - Shared dependencies: Some components (error handling, coverage model) must work correctly in both modes
Trade-offs¶
- Versus automatic detection: More explicit, but eliminates ambiguity and edge cases
- Versus separate gems: Single installation is simpler, but requires mode flag for MCP
Future Constraints¶
- Shared components (like
CoverageModel) must never output to stdout/stderr in ways that differ by mode - Default mode must remain
clifor backward compatibility with existing CLI users
References¶
- Implementation:
lib/cov_loupe.rb(CovLoupe.run) - Configuration:
lib/cov_loupe/app_config.rb - CLI implementation:
lib/cov_loupe/cli.rb - MCP server implementation:
lib/cov_loupe/mcp_server.rb - Related section: Context-Aware Error Handling
Context-Aware Error Handling¶
Status¶
Accepted
Context¶
cov-loupe operates in three distinct contexts, each with different error handling requirements:
- CLI mode: Human users expect friendly error messages, exit codes, and optional debug traces
- MCP server mode: AI agents/clients need structured error responses that don't crash the server
- Library mode: Embedding applications need exceptions they can catch and handle programmatically
Initially, we considered uniform error handling across all modes, but this created poor user experiences:
- CLI users saw raw exceptions with stack traces (scary and unhelpful)
- MCP servers crashed on errors instead of returning error responses
- Library users got friendly messages logged to stderr (unwanted side effects in their applications)
Key Requirements¶
- CLI: User-friendly messages, meaningful exit codes, optional stack traces for debugging
- MCP Server: Logged errors (to file, not stdout), structured JSON-RPC error responses, no server crashes
- Library: Raise custom exceptions with no logging, allowing consumers to handle errors as needed
- Consistency: Same underlying error types, but different presentation strategies
Decision¶
We implemented a context-aware error handling strategy using three components:
1. Custom Exception Hierarchy¶
All errors inherit from CovLoupe::Error (lib/cov_loupe/errors.rb) with a user_friendly_message method:
class Error < StandardError
def user_friendly_message
message # Can be overridden in subclasses
end
end
class FileNotFoundError < FileError; end
class CoverageDataError < Error; end
class ResultsetNotFoundError < CoverageDataError; end
# ... etc
This provides a unified interface for presenting errors to users while preserving exception types for programmatic handling.
2. ErrorHandler Class¶
The ErrorHandler class (see lib/cov_loupe/error_handler.rb) provides configurable error handling behavior:
class ErrorHandler
attr_accessor :error_mode, :logger
VALID_ERROR_MODES = [:off, :log, :debug].freeze
def initialize(error_mode: :log, logger: nil)
@error_mode = error_mode
@logger = logger
end
def handle_error(error, context: nil, reraise: true)
log_error(error, context)
if reraise
raise error.is_a?(CovLoupe::Error) ? error : convert_standard_error(error)
end
end
end
The convert_standard_error method transforms Ruby's standard errors into user-friendly custom exceptions:
Errno::ENOENT→FileNotFoundErrorJSON::ParserError→CoverageDataErrorErrno::EACCES→FilePermissionError
3. ErrorHandlerFactory¶
The ErrorHandlerFactory (defined in lib/cov_loupe/error_handler_factory.rb) creates mode-specific handlers:
module ErrorHandlerFactory
def self.for_cli(error_mode: :log)
ErrorHandler.new(error_mode: error_mode)
end
def self.for_library(error_mode: :off)
ErrorHandler.new(error_mode: :off) # No logging
end
def self.for_mcp_server(error_mode: :log)
ErrorHandler.new(error_mode: :log) # Logs to file
end
end
Error Flow by Mode¶
CLI Mode (lib/cov_loupe/cli.rb): 1. Catches all exceptions in the main run loop 2. Uses for_cli handler to log errors if debug mode is enabled 3. Displays user_friendly_message to the user 4. Exits with appropriate code (1 for errors, 2 for usage errors)
MCP Server Mode (lib/cov_loupe/base_tool.rb): 1. Each tool wraps execution in a rescue block 2. Uses for_mcp_server handler to log errors to ~/cov_loupe.log 3. Returns structured JSON-RPC error response 4. Server continues running (no crashes)
Library Mode (lib/cov_loupe.rb): 1. Uses for_library handler with error_mode: :off (no logging) 2. Raises custom exceptions directly 3. Consumers catch and handle CovLoupe::Error subclasses
Consequences¶
Positive¶
- Excellent UX: Each context gets appropriate error handling behavior
- Robustness: MCP server never crashes on tool errors
- Debuggability: CLI users can enable stack traces with error modes, MCP errors are logged
- Clean library API: No unwanted side effects (logging, stderr output) when used as a library
- Type safety: Custom exceptions allow programmatic error handling by type
Negative¶
- Complexity: Three error handling paths to maintain and test
- Coordination required: All error types must implement
user_friendly_messageconsistently - Error conversion overhead: Standard errors must be converted to custom exceptions
Trade-offs¶
- Versus uniform error handling: More code complexity, but dramatically better UX in each context
- Versus separate error classes per mode: Single error hierarchy is simpler, factory pattern adds mode-specific behavior
Implementation Notes¶
The ErrorHandler.convert_standard_error method uses pattern matching on exception types and error messages to provide helpful, context-aware error messages. This includes:
- Extracting filenames from system error messages
- Detecting SimpleCov-specific error patterns
- Providing actionable suggestions ("please run your tests first")
References¶
- Custom exceptions:
lib/cov_loupe/errors.rb - ErrorHandler implementation:
lib/cov_loupe/error_handler.rb - ErrorHandlerFactory:
lib/cov_loupe/error_handler_factory.rb - CLI error handling:
lib/cov_loupe/cli.rb(rescue block inCoverageCLI#run) - MCP tool error handling:
lib/cov_loupe/base_tool.rb(BaseTool#call) - Library mode:
lib/cov_loupe.rb(error handling withinCovLoupe.run) - Related section: Dual-Mode Operation