Class: CovLoupe::BaseTool
- Inherits:
-
MCP::Tool
- Object
- MCP::Tool
- CovLoupe::BaseTool
- Defined in:
- lib/cov_loupe/base_tool.rb
Overview
Base class for all MCP tool implementations.
Provides shared infrastructure for:
- Schema definition (COMMON_PROPERTIES, coverage_schema, PATH_PROPERTY)
- Model creation with config merging (server context config → tool params → defaults)
- Error handling (with_error_handling wraps all tool calls)
- JSON response formatting (respond_json with ASCII mode support)
- Output character resolution (resolve_output_chars from tool param or server context)
Tool dispatch pattern:
1. Tool class receives call() with JSON params from MCP client
2. Config is merged: server_context.app_config < explicit tool params
3. CoverageModel is created for the call and reuses shared cached data
4. Presenter computes payload (absolute paths, then relativized)
5. respond_json formats the output
File-scope tools (summary, raw, detailed, uncovered) use call_with_file_payload which infers the model method and JSON name from the tool class name.
Direct Known Subclasses
Tools::FileCoverageDetailedTool, Tools::FileCoverageRawTool, Tools::FileCoverageSummaryTool, Tools::FileUncoveredLinesTool, Tools::HelpTool, Tools::ProjectCoverageTool, Tools::ProjectCoverageTotalsTool, Tools::ProjectValidateTool, Tools::VersionTool
Constant Summary collapse
- COMMON_PROPERTIES =
{ root: { type: 'string', description: 'Project root used to resolve relative paths ' \ '(defaults to current workspace).', default: '.', }, resultset: { type: 'string', description: 'Path to the SimpleCov .resultset.json file (absolute or relative to root).', }, raise_on_stale: { type: 'boolean', description: 'If true, raise error if coverage data is stale (missing files, ' \ 'timestamp mismatch). Defaults to false.', default: false, }, error_mode: { type: 'string', description: "Error handling mode: 'off' (silent), 'log' (log errors), " \ "'debug' (verbose with backtraces).", enum: %w[off log debug], default: 'log', }, output_chars: { type: 'string', description: "Output character mode: 'default' (UTF-8 encoding uses fancy, else ascii), " \ "'fancy' (Unicode box-drawing and symbols), 'ascii' (ASCII-only 0x00-0x7F). " \ 'Accepts: d[efault], f[ancy], a[scii].', enum: %w[default fancy ascii d f a], default: 'default', }, }.freeze
- ERROR_MODE_PROPERTY =
COMMON_PROPERTIES[:error_mode].freeze
- TRACKED_GLOBS_PROPERTY =
{ type: 'array', description: 'Glob patterns for files that should exist in the coverage report' \ '(helps flag new files).', items: { type: 'string' }, }.freeze
- DEFAULT_SORT_ORDER =
CoverageModel::DEFAULT_SORT_ORDER.to_s
- SORT_ORDER_PROPERTY =
{ type: 'string', description: 'Sort order for coverage percentages. ' \ "'#{DEFAULT_SORT_ORDER}' (default) lists highest coverage first. " \ 'Accepts: a[scending], d[escending].', default: DEFAULT_SORT_ORDER, enum: %w[ascending descending a d], }.freeze
- PATH_PROPERTY =
{ type: 'string', description: 'Repo-relative or absolute path to the file whose coverage data you need.', examples: ['lib/cov_loupe/model.rb'], }.freeze
- FILE_INPUT_SCHEMA =
coverage_schema( additional_properties: { path: PATH_PROPERTY }, required: ['path'] )
Class Method Summary collapse
-
.call_with_file_payload(path:, error_mode:, server_context:, output_chars: nil, **model_option_overrides) ⇒ MCP::Tool::Response
Runs a file-based tool request by deriving payload method and JSON name from the tool class.
- .coverage_schema(additional_properties: {}, required: []) ⇒ Object
-
.create_configured_model(server_context:, **model_option_overrides) ⇒ Array<CoverageModel, Hash>
Creates and configures a CoverageModel instance, returning both the model and the configuration.
-
.create_model(server_context:, **model_option_overrides) ⇒ CoverageModel
Creates and configures a CoverageModel instance.
-
.default_model_options ⇒ Object
Default configuration when no context or explicit params are provided.
-
.handle_mcp_error(error, tool_name, error_mode: :log, output_chars: :default) ⇒ MCP::Tool::Response
Handle errors consistently across all MCP tools.
- .input_schema_def ⇒ Object
-
.model_config_for(server_context:, **model_option_overrides) ⇒ Hash
Merges configuration from server context (CLI flags) with explicit tool parameters (JSON).
-
.resolve_output_chars(output_chars, server_context) ⇒ Symbol
Resolves output_chars from tool parameter or server context.
-
.respond_json(payload, name: 'data.json', pretty: false, output_chars: :default) ⇒ MCP::Tool::Response
Respond with JSON as a resource to avoid clients mutating content types.
-
.with_error_handling(tool_name, error_mode:, output_chars: :default) ⇒ Object
Wrap tool execution with consistent error handling.
Class Method Details
.call_with_file_payload(path:, error_mode:, server_context:, output_chars: nil, **model_option_overrides) ⇒ MCP::Tool::Response
Runs a file-based tool request by deriving payload method and JSON name from the tool class.
296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 |
# File 'lib/cov_loupe/base_tool.rb', line 296 def self.call_with_file_payload(path:, error_mode:, server_context:, output_chars: nil, **model_option_overrides) tool_name = name.split('::').last output_chars_sym = resolve_output_chars(output_chars, server_context) with_error_handling(tool_name, error_mode: error_mode, output_chars: output_chars_sym) do model, config = create_configured_model(server_context: server_context, **model_option_overrides) presenter = Presenters::CoveragePayloadPresenter.new( model: model, path: path, payload_method: payload_method_for(tool_name), raise_on_stale: config[:raise_on_stale] ) respond_json(presenter.relativized_payload, name: json_name_for(tool_name), pretty: true, output_chars: output_chars_sym) end end |
.coverage_schema(additional_properties: {}, required: []) ⇒ Object
92 93 94 95 96 97 98 99 100 |
# File 'lib/cov_loupe/base_tool.rb', line 92 def self.coverage_schema(additional_properties: {}, required: []) schema = { type: 'object', additionalProperties: false, properties: COMMON_PROPERTIES.merge(additional_properties), } schema[:required] = required unless required.empty? schema.freeze end |
.create_configured_model(server_context:, **model_option_overrides) ⇒ Array<CoverageModel, Hash>
Creates and configures a CoverageModel instance, returning both the model and the configuration. Useful when the tool needs access to the resolved configuration (e.g., root, raise_on_stale).
Models are now lightweight (data is loaded lazily via ModelDataCache), so we create a fresh instance on each call rather than caching at the model level.
239 240 241 242 243 |
# File 'lib/cov_loupe/base_tool.rb', line 239 def self.create_configured_model(server_context:, **model_option_overrides) config = model_config_for(server_context: server_context, **model_option_overrides) model = CoverageModel.new(**config) [model, config] end |
.create_model(server_context:, **model_option_overrides) ⇒ CoverageModel
Creates and configures a CoverageModel instance. Encapsulates the common pattern of merging config and initializing the model.
224 225 226 227 228 |
# File 'lib/cov_loupe/base_tool.rb', line 224 def self.create_model(server_context:, **model_option_overrides) model, _config = create_configured_model(server_context: server_context, **model_option_overrides) model end |
.default_model_options ⇒ Object
Default configuration when no context or explicit params are provided
246 247 248 |
# File 'lib/cov_loupe/base_tool.rb', line 246 def self. { root: '.', resultset: nil, raise_on_stale: false, tracked_globs: [] } end |
.handle_mcp_error(error, tool_name, error_mode: :log, output_chars: :default) ⇒ MCP::Tool::Response
Handle errors consistently across all MCP tools. Returns an MCP::Tool::Response marked as an error (isError: true) so MCP clients can programmatically detect the failure, with the user-friendly message carried in the response content.
131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/cov_loupe/base_tool.rb', line 131 def self.handle_mcp_error(error, tool_name, error_mode: :log, output_chars: :default) # Safely normalize error_mode to a symbol, defaulting to :log for invalid inputs # This prevents crashes when MCP clients send invalid types (null, numbers, objects, etc.) safe_mode = case error_mode when Symbol then error_mode when String then error_mode.to_sym else :log end # Validate against VALID_ERROR_MODES and fallback to :log if invalid # This prevents ArgumentError when handling errors with invalid error_mode values safe_mode = :log unless ErrorHandler::VALID_ERROR_MODES.include?(safe_mode) # Create error handler with the specified mode error_handler = ErrorHandlerFactory.for_mcp_server(error_mode: safe_mode) # Normalize to a CovLoupe::Error so we can handle/log uniformly normalized = error.is_a?(CovLoupe::Error) \ ? error : error_handler.convert_standard_error(error) log_mcp_error(normalized, tool_name, error_handler) # Convert error message to ASCII if needed = normalized. = OutputChars.convert(, output_chars || :default) # Flag the failure in the tool result via isError: true so MCP clients # can distinguish failed tool calls from successful ones, rather than # embedding error text in a normal success response. This is the # tool-result-level error signal; a JSON-RPC error is reserved for # protocol- or dispatch-level failures such as an unknown tool. ::MCP::Tool::Response.new( [{ 'type' => 'text', 'text' => "Error: #{}" }], error: true ) end |
.input_schema_def ⇒ Object
106 |
# File 'lib/cov_loupe/base_tool.rb', line 106 def self.input_schema_def = FILE_INPUT_SCHEMA |
.model_config_for(server_context:, **model_option_overrides) ⇒ Hash
Merges configuration from server context (CLI flags) with explicit tool parameters (JSON). Explicit parameters take precedence over context config, which takes precedence over defaults.
210 211 212 213 214 215 216 217 |
# File 'lib/cov_loupe/base_tool.rb', line 210 def self.model_config_for(server_context:, **model_option_overrides) # Start with config from context (CLI flags) or hardcoded defaults base = server_context.app_config&. || # Merge explicit params from JSON, removing nils # (nil means "not provided", so use base config) base.merge(model_option_overrides.compact) end |
.resolve_output_chars(output_chars, server_context) ⇒ Symbol
Resolves output_chars from tool parameter or server context. Tool parameter takes precedence over server context config. Uses strict validation for tool parameters to catch invalid values.
258 259 260 261 262 263 264 |
# File 'lib/cov_loupe/base_tool.rb', line 258 def self.resolve_output_chars(output_chars, server_context) # Use explicit parameter if provided return normalize_output_chars_strict(output_chars) if output_chars # Fall back to server context config server_context.app_config&.output_chars || :default end |
.respond_json(payload, name: 'data.json', pretty: false, output_chars: :default) ⇒ MCP::Tool::Response
Respond with JSON as a resource to avoid clients mutating content types. The resource embeds the JSON string with a clear MIME type.
174 175 176 177 178 179 180 181 182 |
# File 'lib/cov_loupe/base_tool.rb', line 174 def self.respond_json(payload, name: 'data.json', pretty: false, output_chars: :default) ascii_only = ascii_only?(output_chars) json = if pretty ascii_only ? JSON.pretty_generate(payload, ascii_only: true) : JSON.pretty_generate(payload) else ascii_only ? JSON.generate(payload, ascii_only: true) : JSON.generate(payload) end ::MCP::Tool::Response.new([{ 'type' => 'text', 'text' => json }]) end |
.with_error_handling(tool_name, error_mode:, output_chars: :default) ⇒ Object
Wrap tool execution with consistent error handling. Yields to the block and rescues any error, delegating to handle_mcp_error. This eliminates duplicate rescue blocks across all tools.
115 116 117 118 119 |
# File 'lib/cov_loupe/base_tool.rb', line 115 def self.with_error_handling(tool_name, error_mode:, output_chars: :default) yield rescue => e handle_mcp_error(e, tool_name, error_mode: error_mode, output_chars: output_chars) end |