simplecov-mcp 0.3.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/LICENSE +21 -0
- data/README.md +173 -356
- data/docs/ADVANCED_USAGE.md +967 -0
- data/docs/ARCHITECTURE.md +79 -0
- data/docs/BRANCH_ONLY_COVERAGE.md +81 -0
- data/docs/CLI_USAGE.md +637 -0
- data/docs/DEVELOPMENT.md +82 -0
- data/docs/ERROR_HANDLING.md +93 -0
- data/docs/EXAMPLES.md +430 -0
- data/docs/INSTALLATION.md +352 -0
- data/docs/LIBRARY_API.md +635 -0
- data/docs/MCP_INTEGRATION.md +488 -0
- data/docs/TROUBLESHOOTING.md +276 -0
- data/docs/arch-decisions/001-x-arch-decision.md +93 -0
- data/docs/arch-decisions/002-x-arch-decision.md +157 -0
- data/docs/arch-decisions/003-x-arch-decision.md +163 -0
- data/docs/arch-decisions/004-x-arch-decision.md +199 -0
- data/docs/arch-decisions/005-x-arch-decision.md +187 -0
- data/docs/arch-decisions/README.md +60 -0
- data/docs/presentations/simplecov-mcp-presentation.md +249 -0
- data/exe/simplecov-mcp +4 -4
- data/lib/simplecov_mcp/app_context.rb +26 -0
- data/lib/simplecov_mcp/base_tool.rb +74 -0
- data/lib/simplecov_mcp/cli.rb +234 -0
- data/lib/simplecov_mcp/cli_config.rb +56 -0
- data/lib/simplecov_mcp/commands/base_command.rb +78 -0
- data/lib/simplecov_mcp/commands/command_factory.rb +39 -0
- data/lib/simplecov_mcp/commands/detailed_command.rb +24 -0
- data/lib/simplecov_mcp/commands/list_command.rb +13 -0
- data/lib/simplecov_mcp/commands/raw_command.rb +22 -0
- data/lib/simplecov_mcp/commands/summary_command.rb +24 -0
- data/lib/simplecov_mcp/commands/uncovered_command.rb +26 -0
- data/lib/simplecov_mcp/commands/version_command.rb +18 -0
- data/lib/simplecov_mcp/constants.rb +22 -0
- data/lib/simplecov_mcp/error_handler.rb +124 -0
- data/lib/simplecov_mcp/error_handler_factory.rb +31 -0
- data/lib/simplecov_mcp/errors.rb +179 -0
- data/lib/simplecov_mcp/formatters/source_formatter.rb +148 -0
- data/lib/simplecov_mcp/mcp_server.rb +40 -0
- data/lib/simplecov_mcp/mode_detector.rb +55 -0
- data/lib/simplecov_mcp/model.rb +300 -0
- data/lib/simplecov_mcp/option_normalizers.rb +92 -0
- data/lib/simplecov_mcp/option_parser_builder.rb +134 -0
- data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +50 -0
- data/lib/simplecov_mcp/option_parsers/error_helper.rb +109 -0
- data/lib/simplecov_mcp/path_relativizer.rb +61 -0
- data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +44 -0
- data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +16 -0
- data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +52 -0
- data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +126 -0
- data/lib/simplecov_mcp/resolvers/resolver_factory.rb +28 -0
- data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +78 -0
- data/lib/simplecov_mcp/resultset_loader.rb +136 -0
- data/lib/simplecov_mcp/staleness_checker.rb +243 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/all_files_coverage_tool.rb +31 -13
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_detailed_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_raw_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/coverage_summary_tool.rb +7 -7
- data/lib/simplecov_mcp/tools/coverage_table_tool.rb +90 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/help_tool.rb +13 -4
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/uncovered_lines_tool.rb +7 -7
- data/lib/{simple_cov_mcp → simplecov_mcp}/tools/version_tool.rb +11 -3
- data/lib/simplecov_mcp/util.rb +82 -0
- data/lib/{simple_cov_mcp → simplecov_mcp}/version.rb +1 -1
- data/lib/simplecov_mcp.rb +144 -2
- data/spec/MCP_INTEGRATION_TESTS_README.md +111 -0
- data/spec/TIMESTAMPS.md +48 -0
- data/spec/all_files_coverage_tool_spec.rb +29 -25
- data/spec/base_tool_spec.rb +11 -10
- data/spec/cli/show_default_report_spec.rb +33 -0
- data/spec/cli_config_spec.rb +137 -0
- data/spec/cli_enumerated_options_spec.rb +68 -0
- data/spec/cli_error_spec.rb +105 -47
- data/spec/cli_source_spec.rb +82 -23
- data/spec/cli_spec.rb +140 -5
- data/spec/cli_success_predicate_spec.rb +141 -0
- data/spec/cli_table_spec.rb +1 -1
- data/spec/cli_usage_spec.rb +10 -26
- data/spec/commands/base_command_spec.rb +187 -0
- data/spec/commands/command_factory_spec.rb +72 -0
- data/spec/commands/detailed_command_spec.rb +48 -0
- data/spec/commands/raw_command_spec.rb +46 -0
- data/spec/commands/summary_command_spec.rb +47 -0
- data/spec/commands/uncovered_command_spec.rb +49 -0
- data/spec/constants_spec.rb +61 -0
- data/spec/coverage_table_tool_spec.rb +17 -33
- data/spec/error_handler_spec.rb +22 -13
- data/spec/error_mode_spec.rb +143 -0
- data/spec/errors_edge_cases_spec.rb +239 -0
- data/spec/errors_stale_spec.rb +2 -2
- data/spec/file_based_mcp_tools_spec.rb +99 -0
- data/spec/fixtures/project1/lib/bar.rb +0 -1
- data/spec/fixtures/project1/lib/foo.rb +0 -1
- data/spec/help_tool_spec.rb +11 -17
- data/spec/integration_spec.rb +845 -0
- data/spec/logging_fallback_spec.rb +128 -0
- data/spec/mcp_logging_spec.rb +44 -0
- data/spec/mcp_server_integration_spec.rb +23 -0
- data/spec/mcp_server_spec.rb +15 -4
- data/spec/mode_detector_spec.rb +148 -0
- data/spec/model_error_handling_spec.rb +210 -0
- data/spec/model_staleness_spec.rb +40 -10
- data/spec/option_normalizers_spec.rb +204 -0
- data/spec/option_parsers/env_options_parser_spec.rb +233 -0
- data/spec/option_parsers/error_helper_spec.rb +222 -0
- data/spec/path_relativizer_spec.rb +83 -0
- data/spec/presenters/coverage_detailed_presenter_spec.rb +19 -0
- data/spec/presenters/coverage_raw_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_summary_presenter_spec.rb +15 -0
- data/spec/presenters/coverage_uncovered_presenter_spec.rb +16 -0
- data/spec/presenters/project_coverage_presenter_spec.rb +86 -0
- data/spec/resolvers/coverage_line_resolver_spec.rb +57 -0
- data/spec/resolvers/resolver_factory_spec.rb +61 -0
- data/spec/resolvers/resultset_path_resolver_spec.rb +55 -0
- data/spec/resultset_loader_spec.rb +167 -0
- data/spec/shared_examples/README.md +115 -0
- data/spec/shared_examples/coverage_presenter_examples.rb +66 -0
- data/spec/shared_examples/file_based_mcp_tools.rb +174 -0
- data/spec/shared_examples/mcp_tool_text_json_response.rb +16 -0
- data/spec/simple_cov_mcp_module_spec.rb +16 -0
- data/spec/simplecov_mcp_model_spec.rb +340 -9
- data/spec/simplecov_mcp_opts_spec.rb +182 -0
- data/spec/spec_helper.rb +147 -4
- data/spec/staleness_checker_spec.rb +373 -0
- data/spec/staleness_more_spec.rb +16 -13
- data/spec/support/mcp_runner.rb +64 -0
- data/spec/tools_error_handling_spec.rb +144 -0
- data/spec/util_spec.rb +109 -34
- data/spec/version_spec.rb +117 -9
- data/spec/version_tool_spec.rb +131 -10
- metadata +120 -63
- data/lib/simple_cov/mcp.rb +0 -9
- data/lib/simple_cov_mcp/base_tool.rb +0 -70
- data/lib/simple_cov_mcp/cli.rb +0 -390
- data/lib/simple_cov_mcp/error_handler.rb +0 -131
- data/lib/simple_cov_mcp/error_handler_factory.rb +0 -38
- data/lib/simple_cov_mcp/errors.rb +0 -176
- data/lib/simple_cov_mcp/mcp_server.rb +0 -30
- data/lib/simple_cov_mcp/model.rb +0 -104
- data/lib/simple_cov_mcp/staleness_checker.rb +0 -125
- data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +0 -61
- data/lib/simple_cov_mcp/util.rb +0 -122
- data/lib/simple_cov_mcp.rb +0 -102
- data/spec/coverage_detailed_tool_spec.rb +0 -36
- data/spec/coverage_raw_tool_spec.rb +0 -32
- data/spec/coverage_summary_tool_spec.rb +0 -39
- data/spec/legacy_shim_spec.rb +0 -13
- data/spec/uncovered_lines_tool_spec.rb +0 -33
@@ -0,0 +1,243 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'time'
|
4
|
+
require 'json'
|
5
|
+
require 'pathname'
|
6
|
+
require 'set'
|
7
|
+
require_relative 'errors'
|
8
|
+
require_relative 'util'
|
9
|
+
|
10
|
+
module SimpleCovMcp
|
11
|
+
# Lightweight service object to check staleness of coverage vs. sources
|
12
|
+
class StalenessChecker
|
13
|
+
MODES = [:off, :error].freeze
|
14
|
+
|
15
|
+
def initialize(root:, resultset:, mode: :off, tracked_globs: nil, timestamp: nil)
|
16
|
+
@root = File.absolute_path(root || '.')
|
17
|
+
@resultset = resultset
|
18
|
+
@mode = (mode || :off).to_sym
|
19
|
+
@tracked_globs = tracked_globs
|
20
|
+
@cov_timestamp = timestamp
|
21
|
+
@resultset_path = nil
|
22
|
+
end
|
23
|
+
|
24
|
+
def off?
|
25
|
+
@mode == :off
|
26
|
+
end
|
27
|
+
|
28
|
+
# Raise CoverageDataStaleError if stale (only in error mode)
|
29
|
+
def check_file!(file_abs, coverage_lines)
|
30
|
+
return if off?
|
31
|
+
|
32
|
+
d = compute_file_staleness_details(file_abs, coverage_lines)
|
33
|
+
# For single-file checks, missing files with recorded coverage count as stale
|
34
|
+
# via length mismatch; project-level checks also handle deleted files explicitly.
|
35
|
+
if d[:newer] || d[:len_mismatch]
|
36
|
+
raise CoverageDataStaleError.new(
|
37
|
+
nil,
|
38
|
+
nil,
|
39
|
+
file_path: rel(file_abs),
|
40
|
+
file_mtime: d[:file_mtime],
|
41
|
+
cov_timestamp: d[:coverage_timestamp],
|
42
|
+
src_len: d[:src_len],
|
43
|
+
cov_len: d[:cov_len],
|
44
|
+
resultset_path: resultset_path
|
45
|
+
)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Compute whether a specific file appears stale relative to coverage.
|
50
|
+
# Ignores mode and never raises; returns true when:
|
51
|
+
# - the file is missing/deleted, or
|
52
|
+
# - the file mtime is newer than the coverage timestamp, or
|
53
|
+
# - the source line count differs from the coverage lines array length (when present).
|
54
|
+
def stale_for_file?(file_abs, coverage_lines)
|
55
|
+
d = compute_file_staleness_details(file_abs, coverage_lines)
|
56
|
+
return 'M' unless d[:exists]
|
57
|
+
return 'T' if d[:newer]
|
58
|
+
return 'L' if d[:len_mismatch]
|
59
|
+
|
60
|
+
false
|
61
|
+
end
|
62
|
+
|
63
|
+
# Raise CoverageDataProjectStaleError if any covered file is newer or if
|
64
|
+
# tracked files are missing from coverage, or coverage includes deleted files.
|
65
|
+
def check_project!(coverage_map)
|
66
|
+
return if off?
|
67
|
+
|
68
|
+
ts = coverage_timestamp
|
69
|
+
newer = []
|
70
|
+
deleted = []
|
71
|
+
coverage_files = coverage_map.keys
|
72
|
+
coverage_files.each do |abs|
|
73
|
+
if File.file?(abs)
|
74
|
+
newer << rel(abs) if File.mtime(abs).to_i > ts.to_i
|
75
|
+
else
|
76
|
+
deleted << rel(abs)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
missing = []
|
81
|
+
if @tracked_globs && !Array(@tracked_globs).empty?
|
82
|
+
patterns = Array(@tracked_globs).map { |g| File.absolute_path(g, @root) }
|
83
|
+
tracked = patterns.flat_map { |p| Dir.glob(p, File::FNM_EXTGLOB | File::FNM_PATHNAME) }
|
84
|
+
.select { |p| File.file?(p) }
|
85
|
+
covered_set = coverage_files.to_set rescue coverage_files
|
86
|
+
tracked.each do |abs|
|
87
|
+
missing << rel(abs) unless covered_set.include?(abs)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
if !newer.empty? || !missing.empty? || !deleted.empty?
|
92
|
+
raise CoverageDataProjectStaleError.new(
|
93
|
+
nil,
|
94
|
+
nil,
|
95
|
+
cov_timestamp: ts,
|
96
|
+
newer_files: newer,
|
97
|
+
missing_files: missing,
|
98
|
+
deleted_files: deleted,
|
99
|
+
resultset_path: resultset_path
|
100
|
+
)
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
private
|
105
|
+
|
106
|
+
def coverage_timestamp
|
107
|
+
@cov_timestamp || 0
|
108
|
+
end
|
109
|
+
|
110
|
+
def resultset_path
|
111
|
+
@resultset_path ||= CovUtil.find_resultset(@root, resultset: @resultset)
|
112
|
+
rescue StandardError
|
113
|
+
nil
|
114
|
+
end
|
115
|
+
|
116
|
+
def safe_count_lines(path)
|
117
|
+
return 0 unless File.file?(path)
|
118
|
+
|
119
|
+
File.foreach(path).count
|
120
|
+
rescue StandardError
|
121
|
+
0
|
122
|
+
end
|
123
|
+
|
124
|
+
def missing_trailing_newline?(path)
|
125
|
+
return false unless File.file?(path)
|
126
|
+
|
127
|
+
File.open(path, 'rb') do |f|
|
128
|
+
size = f.size
|
129
|
+
return false if size.zero?
|
130
|
+
|
131
|
+
f.seek(-1, IO::SEEK_END)
|
132
|
+
f.getbyte != 0x0A
|
133
|
+
end
|
134
|
+
rescue StandardError
|
135
|
+
false
|
136
|
+
end
|
137
|
+
|
138
|
+
def rel(path)
|
139
|
+
# Handle relative vs absolute path mismatches that cause ArgumentError
|
140
|
+
Pathname.new(path).relative_path_from(Pathname.new(@root)).to_s
|
141
|
+
rescue ArgumentError
|
142
|
+
# Path is outside the project root or has a different prefix type, fall back to absolute path
|
143
|
+
path.to_s
|
144
|
+
end
|
145
|
+
|
146
|
+
# Centralized computation of staleness-related details for a single file.
|
147
|
+
# Returns a Hash with keys:
|
148
|
+
# :exists, :file_mtime, :coverage_timestamp, :cov_len, :src_len, :newer, :len_mismatch
|
149
|
+
def compute_file_staleness_details(file_abs, coverage_lines)
|
150
|
+
coverage_ts = coverage_timestamp
|
151
|
+
|
152
|
+
exists = File.file?(file_abs)
|
153
|
+
file_mtime = exists ? File.mtime(file_abs) : nil
|
154
|
+
|
155
|
+
cov_len = coverage_lines.respond_to?(:length) ? coverage_lines.length : 0
|
156
|
+
src_len = exists ? safe_count_lines(file_abs) : 0
|
157
|
+
|
158
|
+
# Adjust source line count to handle edge cases with missing trailing newlines
|
159
|
+
adjusted_src_len = adjust_line_count_for_missing_newline(
|
160
|
+
file_abs: file_abs,
|
161
|
+
exists: exists,
|
162
|
+
cov_len: cov_len,
|
163
|
+
src_len: src_len
|
164
|
+
)
|
165
|
+
|
166
|
+
# Check if the source file has been modified since coverage was generated
|
167
|
+
len_mismatch = check_length_mismatch(cov_len, adjusted_src_len)
|
168
|
+
newer = check_file_newer_than_coverage(file_mtime, coverage_ts, len_mismatch)
|
169
|
+
|
170
|
+
{
|
171
|
+
exists: exists,
|
172
|
+
file_mtime: file_mtime,
|
173
|
+
coverage_timestamp: coverage_ts,
|
174
|
+
cov_len: cov_len,
|
175
|
+
src_len: src_len,
|
176
|
+
newer: newer,
|
177
|
+
len_mismatch: len_mismatch
|
178
|
+
}
|
179
|
+
end
|
180
|
+
|
181
|
+
# Adjusts the source line count to account for files missing trailing newlines.
|
182
|
+
#
|
183
|
+
# Why this edge case exists:
|
184
|
+
# - File.foreach counts lines by separator (typically \n)
|
185
|
+
# - For a file with no trailing newline, File.foreach still counts all lines correctly
|
186
|
+
# - However, some editors or file operations may report one extra line when checking
|
187
|
+
# if the file doesn't end with a newline
|
188
|
+
# - SimpleCov's coverage array length matches the logical line count (excluding trailing newline)
|
189
|
+
# - If src_len is exactly one more than cov_len AND the file is missing a trailing newline,
|
190
|
+
# we adjust src_len down by 1 to match SimpleCov's convention
|
191
|
+
#
|
192
|
+
# Example: A file with "line1\nline2\nline3" (no final \n)
|
193
|
+
# - File.foreach counts: 3 lines
|
194
|
+
# - SimpleCov coverage array length: 3
|
195
|
+
# - No adjustment needed
|
196
|
+
#
|
197
|
+
# However, in certain edge cases where the file system or parsing reports an extra line:
|
198
|
+
# - Reported line count: 4
|
199
|
+
# - SimpleCov coverage array length: 3
|
200
|
+
# - Missing trailing newline: true
|
201
|
+
# - Adjustment: 4 - 1 = 3 (now matches)
|
202
|
+
def adjust_line_count_for_missing_newline(file_abs:, exists:, cov_len:, src_len:)
|
203
|
+
# Only adjust if:
|
204
|
+
# 1. File exists (can't check newlines for missing files)
|
205
|
+
# 2. Coverage data is present (cov_len > 0)
|
206
|
+
# 3. Source has exactly one more line than coverage
|
207
|
+
# 4. File is missing a trailing newline
|
208
|
+
needs_adjusting =
|
209
|
+
exists && cov_len.positive? && src_len == cov_len + 1 && missing_trailing_newline?(file_abs)
|
210
|
+
needs_adjusting ? src_len - 1 : src_len
|
211
|
+
end
|
212
|
+
|
213
|
+
# Checks if the source line count differs from the coverage line count.
|
214
|
+
#
|
215
|
+
# Why this check exists:
|
216
|
+
# - When a file is modified after coverage is generated, the line count often changes
|
217
|
+
# - A mismatch indicates the coverage data is stale and no longer represents the current file
|
218
|
+
# - We only flag as mismatch when coverage data exists (cov_len > 0)
|
219
|
+
#
|
220
|
+
# Note: Empty coverage (cov_len == 0) is not considered a mismatch, as it may represent
|
221
|
+
# files that were never executed or files that are legitimately empty.
|
222
|
+
def check_length_mismatch(cov_len, adjusted_src_len)
|
223
|
+
cov_len.positive? && adjusted_src_len != cov_len
|
224
|
+
end
|
225
|
+
|
226
|
+
# Determines if a file has been modified more recently than the coverage timestamp.
|
227
|
+
#
|
228
|
+
# Why this check exists:
|
229
|
+
# - Files modified after coverage generation may have behavioral changes not captured
|
230
|
+
# - However, if there's already a length mismatch, we prioritize that as the staleness indicator
|
231
|
+
# - This prevents double-flagging: if lines changed, the file is already stale (length mismatch)
|
232
|
+
#
|
233
|
+
# The logic: newer &&= !len_mismatch means:
|
234
|
+
# - If len_mismatch is true, set newer to false (length mismatch takes precedence)
|
235
|
+
# - This way, staleness is categorized as either 'T' (time-based) OR 'L' (length-based), not both
|
236
|
+
def check_file_newer_than_coverage(file_mtime, coverage_ts, len_mismatch)
|
237
|
+
newer = !!(file_mtime && file_mtime.to_i > coverage_ts.to_i)
|
238
|
+
# If there's a length mismatch, don't also flag as "newer" - the mismatch is more specific
|
239
|
+
newer &&= !len_mismatch
|
240
|
+
newer
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative '../model'
|
4
4
|
require_relative '../base_tool'
|
5
|
+
require_relative '../presenters/project_coverage_presenter'
|
5
6
|
|
6
7
|
module SimpleCovMcp
|
7
8
|
module Tools
|
@@ -10,7 +11,7 @@ module SimpleCovMcp
|
|
10
11
|
Use this when the user wants coverage percentages for every tracked file in the project.
|
11
12
|
Do not use this for single-file stats; prefer coverage.summary or coverage.uncovered_lines for that.
|
12
13
|
Inputs: optional project root, alternate .resultset path, sort order, staleness mode, and tracked_globs to alert on new files.
|
13
|
-
Output: JSON {"files": [{"file","covered","total","percentage","stale"}, ...], "counts": {"total", "ok", "stale"}} sorted as requested. "stale" is a
|
14
|
+
Output: JSON {"files": [{"file","covered","total","percentage","stale"}, ...], "counts": {"total", "ok", "stale"}} sorted as requested. "stale" is a string ('M', 'T', 'L') or false.
|
14
15
|
Examples: "List files with the lowest coverage"; "Show repo coverage sorted descending".
|
15
16
|
DESC
|
16
17
|
input_schema(
|
@@ -28,34 +29,51 @@ module SimpleCovMcp
|
|
28
29
|
},
|
29
30
|
sort_order: {
|
30
31
|
type: 'string',
|
31
|
-
description:
|
32
|
+
description: 'Sort order for coverage percentages.' \
|
33
|
+
"'ascending' highlights the riskiest files first.",
|
32
34
|
default: 'ascending',
|
33
35
|
enum: ['ascending', 'descending']
|
34
36
|
},
|
35
37
|
stale: {
|
36
38
|
type: 'string',
|
37
|
-
description:
|
39
|
+
description:
|
40
|
+
"How to handle missing/outdated coverage data. 'off' skips checks; 'error' raises.",
|
38
41
|
enum: ['off', 'error'],
|
39
42
|
default: 'off'
|
40
43
|
},
|
41
44
|
tracked_globs: {
|
42
45
|
type: 'array',
|
43
|
-
description: 'Glob patterns for files that should exist in the coverage report
|
46
|
+
description: 'Glob patterns for files that should exist in the coverage report' \
|
47
|
+
'(helps flag new files).',
|
44
48
|
items: { type: 'string' }
|
49
|
+
},
|
50
|
+
error_mode: {
|
51
|
+
type: 'string',
|
52
|
+
description:
|
53
|
+
"Error handling mode: 'off' (silent), 'on' (log errors), 'trace' (verbose).",
|
54
|
+
enum: ['off', 'on', 'trace'],
|
55
|
+
default: 'on'
|
45
56
|
}
|
46
57
|
}
|
47
58
|
)
|
48
59
|
class << self
|
49
|
-
def call(root: '.', resultset: nil, sort_order: 'ascending', stale: 'off',
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
60
|
+
def call(root: '.', resultset: nil, sort_order: 'ascending', stale: 'off',
|
61
|
+
tracked_globs: nil, error_mode: 'on', server_context:)
|
62
|
+
# Convert string inputs from MCP to symbols for internal use
|
63
|
+
sort_order_sym = sort_order.to_sym
|
64
|
+
stale_sym = stale.to_sym
|
65
|
+
|
66
|
+
model = CoverageModel.new(root: root, resultset: resultset, staleness: stale_sym,
|
67
|
+
tracked_globs: tracked_globs)
|
68
|
+
presenter = Presenters::ProjectCoveragePresenter.new(
|
69
|
+
model: model,
|
70
|
+
sort_order: sort_order_sym,
|
71
|
+
check_stale: (stale_sym == :error),
|
72
|
+
tracked_globs: tracked_globs
|
73
|
+
)
|
74
|
+
respond_json(presenter.relativized_payload, name: 'all_files_coverage.json')
|
57
75
|
rescue => e
|
58
|
-
handle_mcp_error(e, 'AllFilesCoverageTool')
|
76
|
+
handle_mcp_error(e, 'AllFilesCoverageTool', error_mode: error_mode)
|
59
77
|
end
|
60
78
|
end
|
61
79
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative '../base_tool'
|
4
4
|
require_relative '../model'
|
5
|
+
require_relative '../presenters/coverage_detailed_presenter'
|
5
6
|
|
6
7
|
module SimpleCovMcp
|
7
8
|
module Tools
|
@@ -10,18 +11,17 @@ module SimpleCovMcp
|
|
10
11
|
Use this when the user needs per-line coverage data for a single file.
|
11
12
|
Do not use this for high-level counts; coverage.summary is cheaper for aggregate numbers.
|
12
13
|
Inputs: file path (required) plus optional root/resultset/stale mode inherited from BaseTool.
|
13
|
-
Output: JSON object with "file", "lines" => [{"line": 12, "hits": 0, "covered": false}], plus "summary" with totals.
|
14
|
+
Output: JSON object with "file", "lines" => [{"line": 12, "hits": 0, "covered": false}], plus "summary" with totals and "stale" status.
|
14
15
|
Example: "Show detailed coverage for lib/simple_cov_mcp/model.rb".
|
15
16
|
DESC
|
16
17
|
input_schema(**input_schema_def)
|
17
18
|
class << self
|
18
|
-
def call(path:, root: '.', resultset: nil, stale: 'off', server_context:)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
respond_json(data, name: 'coverage_detailed.json', pretty: true)
|
19
|
+
def call(path:, root: '.', resultset: nil, stale: 'off', error_mode: 'on', server_context:)
|
20
|
+
model = CoverageModel.new(root: root, resultset: resultset, staleness: stale)
|
21
|
+
presenter = Presenters::CoverageDetailedPresenter.new(model: model, path: path)
|
22
|
+
respond_json(presenter.relativized_payload, name: 'coverage_detailed.json', pretty: true)
|
23
23
|
rescue => e
|
24
|
-
handle_mcp_error(e, 'CoverageDetailedTool')
|
24
|
+
handle_mcp_error(e, 'CoverageDetailedTool', error_mode: error_mode)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative '../base_tool'
|
4
4
|
require_relative '../model'
|
5
|
+
require_relative '../presenters/coverage_raw_presenter'
|
5
6
|
|
6
7
|
module SimpleCovMcp
|
7
8
|
module Tools
|
@@ -10,18 +11,17 @@ module SimpleCovMcp
|
|
10
11
|
Use this when you need the raw SimpleCov `lines` array for a file exactly as stored on disk.
|
11
12
|
Do not use this for human-friendly explanations; choose coverage.detailed or coverage.summary instead.
|
12
13
|
Inputs: file path (required) plus optional root/resultset/stale mode inherited from BaseTool.
|
13
|
-
Output: JSON object with "file" and "lines" (array of integers/nulls) mirroring SimpleCov's native structure.
|
14
|
+
Output: JSON object with "file" and "lines" (array of integers/nulls) mirroring SimpleCov's native structure, plus "stale" status.
|
14
15
|
Example: "Fetch the raw coverage array for spec/support/foo_helper.rb".
|
15
16
|
DESC
|
16
17
|
input_schema(**input_schema_def)
|
17
18
|
class << self
|
18
|
-
def call(path:, root: '.', resultset: nil, stale: 'off', server_context:)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
respond_json(data, name: 'coverage_raw.json', pretty: true)
|
19
|
+
def call(path:, root: '.', resultset: nil, stale: 'off', error_mode: 'on', server_context:)
|
20
|
+
model = CoverageModel.new(root: root, resultset: resultset, staleness: stale)
|
21
|
+
presenter = Presenters::CoverageRawPresenter.new(model: model, path: path)
|
22
|
+
respond_json(presenter.relativized_payload, name: 'coverage_raw.json', pretty: true)
|
23
23
|
rescue => e
|
24
|
-
handle_mcp_error(e, 'CoverageRawTool')
|
24
|
+
handle_mcp_error(e, 'CoverageRawTool', error_mode: error_mode)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative '../base_tool'
|
4
4
|
require_relative '../model'
|
5
|
+
require_relative '../presenters/coverage_summary_presenter'
|
5
6
|
|
6
7
|
module SimpleCovMcp
|
7
8
|
module Tools
|
@@ -10,18 +11,17 @@ module SimpleCovMcp
|
|
10
11
|
Use this when the user asks for the covered/total line counts and percentage for a specific file.
|
11
12
|
Do not use this for multi-file reports; coverage.all_files or coverage.table handle those.
|
12
13
|
Inputs: file path (required) plus optional root/resultset/stale mode inherited from BaseTool.
|
13
|
-
Output: JSON object {"file": String, "summary": {"covered": Integer, "total": Integer, "pct": Float}}.
|
14
|
+
Output: JSON object {"file": String, "summary": {"covered": Integer, "total": Integer, "pct": Float}, "stale": String|False}.
|
14
15
|
Examples: "What is the coverage for lib/simple_cov_mcp/tools/all_files_coverage_tool.rb?".
|
15
16
|
DESC
|
16
17
|
input_schema(**input_schema_def)
|
17
18
|
class << self
|
18
|
-
def call(path:, root: '.', resultset: nil, stale: 'off', server_context:)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
respond_json(data, name: 'coverage_summary.json', pretty: true)
|
19
|
+
def call(path:, root: '.', resultset: nil, stale: 'off', error_mode: 'on', server_context:)
|
20
|
+
model = CoverageModel.new(root: root, resultset: resultset, staleness: stale)
|
21
|
+
presenter = Presenters::CoverageSummaryPresenter.new(model: model, path: path)
|
22
|
+
respond_json(presenter.relativized_payload, name: 'coverage_summary.json', pretty: true)
|
23
23
|
rescue => e
|
24
|
-
handle_mcp_error(e, 'CoverageSummaryTool')
|
24
|
+
handle_mcp_error(e, 'CoverageSummaryTool', error_mode: error_mode)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
@@ -0,0 +1,90 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
|
4
|
+
require_relative '../base_tool'
|
5
|
+
require_relative '../presenters/project_coverage_presenter'
|
6
|
+
|
7
|
+
module SimpleCovMcp
|
8
|
+
module Tools
|
9
|
+
class CoverageTableTool < BaseTool
|
10
|
+
description <<~DESC
|
11
|
+
Use this when a user wants the plain text coverage table exactly like `simplecov-mcp --table` would print (no ANSI colors).
|
12
|
+
Do not use this for machine-readable data; coverage.all_files returns structured JSON.
|
13
|
+
Inputs: optional project root/resultset path/sort order/staleness mode matching the CLI flags.
|
14
|
+
Output: text block containing the formatted coverage table with headers and percentages.
|
15
|
+
Example: "Show me the CLI coverage table sorted descending".
|
16
|
+
DESC
|
17
|
+
input_schema(
|
18
|
+
type: 'object',
|
19
|
+
additionalProperties: false,
|
20
|
+
properties: {
|
21
|
+
root: {
|
22
|
+
type: 'string',
|
23
|
+
description: 'Project root used to resolve relative inputs.',
|
24
|
+
default: '.'
|
25
|
+
},
|
26
|
+
resultset: {
|
27
|
+
type: 'string',
|
28
|
+
description: 'Path to the SimpleCov .resultset.json file.'
|
29
|
+
},
|
30
|
+
sort_order: {
|
31
|
+
type: 'string',
|
32
|
+
description: 'Sort order for the printed coverage table (ascending or descending).',
|
33
|
+
default: 'ascending',
|
34
|
+
enum: ['ascending', 'descending']
|
35
|
+
},
|
36
|
+
stale: {
|
37
|
+
type: 'string',
|
38
|
+
description: 'How to handle missing/outdated coverage data. ' \
|
39
|
+
"'off' skips checks; 'error' raises.",
|
40
|
+
enum: ['off', 'error'],
|
41
|
+
default: 'off'
|
42
|
+
},
|
43
|
+
tracked_globs: {
|
44
|
+
type: 'array',
|
45
|
+
description: 'Glob patterns for files that should exist in the coverage report ' \
|
46
|
+
'(helps flag new files).',
|
47
|
+
items: { type: 'string' }
|
48
|
+
},
|
49
|
+
error_mode: {
|
50
|
+
type: 'string',
|
51
|
+
description:
|
52
|
+
"Error handling mode: 'off' (silent), 'on' (log errors), 'trace' (verbose).",
|
53
|
+
enum: ['off', 'on', 'trace'],
|
54
|
+
default: 'on'
|
55
|
+
}
|
56
|
+
}
|
57
|
+
)
|
58
|
+
|
59
|
+
class << self
|
60
|
+
def call(root: '.', resultset: nil, sort_order: 'ascending', stale: 'off',
|
61
|
+
tracked_globs: nil, error_mode: 'on', server_context:)
|
62
|
+
# Capture the output of the CLI's table report while honoring CLI options
|
63
|
+
# Convert string inputs from MCP to symbols for internal use
|
64
|
+
sort_order_sym = sort_order.to_sym
|
65
|
+
stale_sym = stale.to_sym
|
66
|
+
check_stale = (stale_sym == :error)
|
67
|
+
|
68
|
+
model = CoverageModel.new(root: root, resultset: resultset, staleness: stale_sym,
|
69
|
+
tracked_globs: tracked_globs)
|
70
|
+
presenter = Presenters::ProjectCoveragePresenter.new(
|
71
|
+
model: model,
|
72
|
+
sort_order: sort_order_sym,
|
73
|
+
check_stale: check_stale,
|
74
|
+
tracked_globs: tracked_globs
|
75
|
+
)
|
76
|
+
relativized = presenter.relative_files
|
77
|
+
table = model.format_table(
|
78
|
+
relativized,
|
79
|
+
sort_order: sort_order_sym,
|
80
|
+
check_stale: check_stale,
|
81
|
+
tracked_globs: nil # rows already filtered via all_files
|
82
|
+
)
|
83
|
+
::MCP::Tool::Response.new([{ type: 'text', text: table }])
|
84
|
+
rescue => e
|
85
|
+
handle_mcp_error(e, 'CoverageTableTool', error_mode: error_mode)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
@@ -9,7 +9,8 @@ module SimpleCovMcp
|
|
9
9
|
Use this when you are unsure which simplecov-mcp tool fits the user’s coverage request.
|
10
10
|
Do not use this once you know the correct tool; call that tool directly.
|
11
11
|
Inputs: optional query string to filter the list of tools.
|
12
|
-
Output: JSON {"tools": [...]} with per-tool "use_when", "avoid_when", "inputs"
|
12
|
+
Output: JSON {"tools": [...]} with per-tool "use_when", "avoid_when", "inputs",#{' '}
|
13
|
+
and "example" guidance.
|
13
14
|
Example: "Which tool shows uncovered lines?".
|
14
15
|
DESC
|
15
16
|
|
@@ -19,7 +20,15 @@ module SimpleCovMcp
|
|
19
20
|
properties: {
|
20
21
|
query: {
|
21
22
|
type: 'string',
|
22
|
-
description:
|
23
|
+
description:
|
24
|
+
'Optional keywords to filter the help entries (e.g., "uncovered", "summary").'
|
25
|
+
},
|
26
|
+
error_mode: {
|
27
|
+
type: 'string',
|
28
|
+
description:
|
29
|
+
"Error handling mode: 'off' (silent), 'on' (log errors), 'trace' (verbose).",
|
30
|
+
enum: ['off', 'on', 'trace'],
|
31
|
+
default: 'on'
|
23
32
|
}
|
24
33
|
}
|
25
34
|
)
|
@@ -84,14 +93,14 @@ module SimpleCovMcp
|
|
84
93
|
].freeze
|
85
94
|
|
86
95
|
class << self
|
87
|
-
def call(query: nil, server_context:, **_unused)
|
96
|
+
def call(query: nil, error_mode: 'on', server_context:, **_unused)
|
88
97
|
entries = TOOL_GUIDE.map { |guide| format_entry(guide) }
|
89
98
|
entries = filter_entries(entries, query) if query && !query.strip.empty?
|
90
99
|
|
91
100
|
data = { query: query, tools: entries }
|
92
101
|
respond_json(data, name: 'tools_help.json')
|
93
102
|
rescue => e
|
94
|
-
handle_mcp_error(e, 'HelpTool')
|
103
|
+
handle_mcp_error(e, 'HelpTool', error_mode: error_mode)
|
95
104
|
end
|
96
105
|
|
97
106
|
private
|
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative '../base_tool'
|
4
4
|
require_relative '../model'
|
5
|
+
require_relative '../presenters/coverage_uncovered_presenter'
|
5
6
|
|
6
7
|
module SimpleCovMcp
|
7
8
|
module Tools
|
@@ -10,18 +11,17 @@ module SimpleCovMcp
|
|
10
11
|
Use this when the user wants to know which lines in a file still lack coverage.
|
11
12
|
Do not use this for overall percentages; coverage.summary is faster when counts are enough.
|
12
13
|
Inputs: file path (required) plus optional root/resultset/stale mode inherited from BaseTool.
|
13
|
-
Output: JSON object with keys "file", "uncovered" (array of integers),
|
14
|
+
Output: JSON object with keys "file", "uncovered" (array of integers), "summary" {"covered","total","pct"}, and "stale" status.
|
14
15
|
Example: "List uncovered lines for lib/simple_cov_mcp/tools/coverage_summary_tool.rb".
|
15
16
|
DESC
|
16
17
|
input_schema(**input_schema_def)
|
17
18
|
class << self
|
18
|
-
def call(path:, root: '.', resultset: nil, stale: 'off', server_context:)
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
respond_json(data, name: 'uncovered_lines.json', pretty: true)
|
19
|
+
def call(path:, root: '.', resultset: nil, stale: 'off', error_mode: 'on', server_context:)
|
20
|
+
model = CoverageModel.new(root: root, resultset: resultset, staleness: stale)
|
21
|
+
presenter = Presenters::CoverageUncoveredPresenter.new(model: model, path: path)
|
22
|
+
respond_json(presenter.relativized_payload, name: 'uncovered_lines.json', pretty: true)
|
23
23
|
rescue => e
|
24
|
-
handle_mcp_error(e, 'UncoveredLinesTool')
|
24
|
+
handle_mcp_error(e, 'UncoveredLinesTool', error_mode: error_mode)
|
25
25
|
end
|
26
26
|
end
|
27
27
|
end
|
@@ -14,16 +14,24 @@ module SimpleCovMcp
|
|
14
14
|
input_schema(
|
15
15
|
type: 'object',
|
16
16
|
additionalProperties: false,
|
17
|
-
properties: {
|
17
|
+
properties: {
|
18
|
+
error_mode: {
|
19
|
+
type: 'string',
|
20
|
+
description:
|
21
|
+
"Error handling mode: 'off' (silent), 'on' (log errors), 'trace' (verbose).",
|
22
|
+
enum: ['off', 'on', 'trace'],
|
23
|
+
default: 'on'
|
24
|
+
}
|
25
|
+
}
|
18
26
|
)
|
19
27
|
|
20
28
|
class << self
|
21
|
-
def call(server_context: nil, **_args)
|
29
|
+
def call(error_mode: 'on', server_context: nil, **_args)
|
22
30
|
::MCP::Tool::Response.new([
|
23
31
|
{ type: 'text', text: "SimpleCovMcp version: #{SimpleCovMcp::VERSION}" }
|
24
32
|
])
|
25
33
|
rescue => error
|
26
|
-
handle_mcp_error(error, 'version_tool')
|
34
|
+
handle_mcp_error(error, 'version_tool', error_mode: error_mode)
|
27
35
|
end
|
28
36
|
end
|
29
37
|
end
|