simplecov-mcp 0.1.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 +7 -0
- data/README.md +97 -0
- data/exe/simplecov-mcp +6 -0
- data/lib/simplecov/mcp/base_tool.rb +18 -0
- data/lib/simplecov/mcp/cli.rb +98 -0
- data/lib/simplecov/mcp/model.rb +59 -0
- data/lib/simplecov/mcp/tools/all_files_coverage.rb +28 -0
- data/lib/simplecov/mcp/tools/coverage_detailed.rb +22 -0
- data/lib/simplecov/mcp/tools/coverage_raw.rb +22 -0
- data/lib/simplecov/mcp/tools/coverage_summary.rb +22 -0
- data/lib/simplecov/mcp/tools/uncovered_lines.rb +22 -0
- data/lib/simplecov/mcp/util.rb +94 -0
- data/lib/simplecov/mcp/version.rb +8 -0
- data/lib/simplecov/mcp.rb +28 -0
- data/lib/simplecov_mcp.rb +5 -0
- data/spec/simplecov_mcp_model_spec.rb +54 -0
- data/spec/spec_helper.rb +19 -0
- metadata +106 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f2a8ecf4bfeeedc4b7377baaa3fcabf0b196ad9c17f2ccd29e5532b7a6104751
|
4
|
+
data.tar.gz: 8b52e3920dd3734add19862838f03b967bb73d17dd769539a66160ef9950e38f
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: bbcec76c297750544657a18cec0366148f1cff30ebff2c9b5bbc593f2d9492fe32554d2b4859f1e89f7efd19f35d35e7345f8ecdfa05d1c32ffea86f42c68ed6
|
7
|
+
data.tar.gz: 2266b137c11b92fcaedc7337010c7a5b8c6814d09b4a652bc642abad2082e3f8cd20e8d33930808257176e020602249408be71c206d518adba3cc577dab7aa41
|
data/README.md
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
# simplecov-mcp
|
2
|
+
|
3
|
+
MCP server + CLI for inspecting SimpleCov coverage data.
|
4
|
+
|
5
|
+
This gem provides:
|
6
|
+
|
7
|
+
- An MCP (Model Context Protocol) server exposing tools to query coverage for files.
|
8
|
+
- A human-friendly CLI that prints a sorted table of file coverage.
|
9
|
+
|
10
|
+
## Installation
|
11
|
+
|
12
|
+
Add to your Gemfile or install directly:
|
13
|
+
|
14
|
+
```
|
15
|
+
gem install simplecov-mcp
|
16
|
+
```
|
17
|
+
|
18
|
+
Require path is `simplecov/mcp` (also `simplecov_mcp`). Executable is `simplecov-mcp`.
|
19
|
+
|
20
|
+
## Usage
|
21
|
+
|
22
|
+
Environment variable:
|
23
|
+
|
24
|
+
- `SIMPLECOV_RESULTSET` — optional explicit path to `.resultset.json`.
|
25
|
+
|
26
|
+
Search order for resultset:
|
27
|
+
|
28
|
+
1. `.resultset.json`
|
29
|
+
2. `coverage/.resultset.json`
|
30
|
+
3. `tmp/.resultset.json`
|
31
|
+
|
32
|
+
### CLI Mode
|
33
|
+
|
34
|
+
Run in a project directory with a SimpleCov resultset:
|
35
|
+
|
36
|
+
```
|
37
|
+
simplecov-mcp
|
38
|
+
```
|
39
|
+
|
40
|
+
Forces CLI mode:
|
41
|
+
|
42
|
+
```
|
43
|
+
simplecov-mcp --cli
|
44
|
+
# or
|
45
|
+
COVERAGE_MCP_CLI=1 simplecov-mcp
|
46
|
+
``;
|
47
|
+
|
48
|
+
Example output:
|
49
|
+
|
50
|
+
```
|
51
|
+
┌───────────────────────────┬──────────┬──────────┬────────┐
|
52
|
+
│ File │ % │ Covered │ Total │
|
53
|
+
├───────────────────────────┼──────────┼──────────┼────────┤
|
54
|
+
│ lib/models/user.rb │ 92.31 │ 12 │ 13 │
|
55
|
+
│ lib/services/auth.rb │ 100.00 │ 8 │ 8 │
|
56
|
+
│ spec/user_spec.rb │ 85.71 │ 12 │ 14 │
|
57
|
+
└───────────────────────────┴──────────┴──────────┴────────┘
|
58
|
+
```
|
59
|
+
|
60
|
+
Files are sorted by percentage (ascending), then by path.
|
61
|
+
|
62
|
+
### MCP Server Mode
|
63
|
+
|
64
|
+
When stdin has data (e.g., from an MCP client), the program runs as an MCP server over stdio.
|
65
|
+
|
66
|
+
Available tools:
|
67
|
+
|
68
|
+
- `coverage_raw(path, root=".")`
|
69
|
+
- `coverage_summary(path, root=".")`
|
70
|
+
- `uncovered_lines(path, root=".")`
|
71
|
+
- `coverage_detailed(path, root=".")`
|
72
|
+
- `all_files_coverage(root=".")`
|
73
|
+
|
74
|
+
Example (manual):
|
75
|
+
|
76
|
+
```
|
77
|
+
echo '{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"coverage_summary","arguments":{"path":"lib/foo.rb"}}}' | simplecov-mcp
|
78
|
+
```
|
79
|
+
|
80
|
+
### Notes
|
81
|
+
|
82
|
+
- Library entrypoint: `require "simplecov/mcp"` or `require "simplecov_mcp"`
|
83
|
+
- Programmatic run: `Simplecov::Mcp.run(ARGV)`
|
84
|
+
- Logs basic diagnostics to `~/coverage_mcp.log`.
|
85
|
+
|
86
|
+
## Development
|
87
|
+
|
88
|
+
Standard Ruby gem structure. After cloning:
|
89
|
+
|
90
|
+
```
|
91
|
+
bundle install
|
92
|
+
ruby -Ilib exe/simplecov-mcp --cli
|
93
|
+
```
|
94
|
+
|
95
|
+
## License
|
96
|
+
|
97
|
+
MIT
|
data/exe/simplecov-mcp
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simplecov
|
4
|
+
module Mcp
|
5
|
+
class BaseTool < ::MCP::Tool
|
6
|
+
INPUT_SCHEMA = {
|
7
|
+
type: "object",
|
8
|
+
properties: {
|
9
|
+
path: { type: "string", description: "Absolute or project-relative file path" },
|
10
|
+
root: { type: "string", description: "Project root for resolution", default: "." }
|
11
|
+
},
|
12
|
+
required: ["path"]
|
13
|
+
}
|
14
|
+
def self.input_schema_def = INPUT_SCHEMA
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simplecov
|
4
|
+
module Mcp
|
5
|
+
class CoverageCLI
|
6
|
+
def initialize
|
7
|
+
@root = "."
|
8
|
+
end
|
9
|
+
|
10
|
+
def run(argv)
|
11
|
+
if force_cli?(argv)
|
12
|
+
show_default_report
|
13
|
+
else
|
14
|
+
run_mcp_server
|
15
|
+
end
|
16
|
+
rescue => e
|
17
|
+
CovUtil.log("CLI fatal error: #{e.class}: #{e.message}\n#{e.backtrace&.join("\n")}")
|
18
|
+
raise
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def force_cli?(argv)
|
24
|
+
return true if ENV["COVERAGE_MCP_CLI"] == "1"
|
25
|
+
return true if argv.include?("--cli") || argv.include?("--report")
|
26
|
+
# If interactive TTY, prefer CLI; else (e.g., pipes), run MCP.
|
27
|
+
return STDIN.tty?
|
28
|
+
end
|
29
|
+
|
30
|
+
def show_default_report
|
31
|
+
model = CoverageModel.new(root: @root)
|
32
|
+
file_summaries = model.all_files(sort_order: :ascending).map do |row|
|
33
|
+
row.dup.tap do |h|
|
34
|
+
h[:file] = Pathname.new(h[:file]).relative_path_from(Pathname.new(Dir.pwd)).to_s
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# Format as table with box-style borders
|
39
|
+
max_file_length = file_summaries.map { |f| f[:file].length }.max.to_i
|
40
|
+
max_file_length = [max_file_length, "File".length].max
|
41
|
+
|
42
|
+
# Calculate maximum numeric values for proper column widths
|
43
|
+
max_covered = file_summaries.map { |f| f[:covered].to_s.length }.max
|
44
|
+
max_total = file_summaries.map { |f| f[:total].to_s.length }.max
|
45
|
+
|
46
|
+
# Define column widths
|
47
|
+
file_width = max_file_length + 2 # Extra padding
|
48
|
+
pct_width = 8
|
49
|
+
covered_width = [max_covered, "Covered".length].max + 2
|
50
|
+
total_width = [max_total, "Total".length].max + 2
|
51
|
+
|
52
|
+
# Horizontal line for each column span
|
53
|
+
h_line = ->(col_width) { '─' * (col_width + 2) }
|
54
|
+
|
55
|
+
# Border line lambda
|
56
|
+
border_line = ->(left, middle, right) {
|
57
|
+
left + h_line.(file_width) +
|
58
|
+
middle + h_line.(pct_width) +
|
59
|
+
middle + h_line.(covered_width) +
|
60
|
+
middle + h_line.(total_width) +
|
61
|
+
right
|
62
|
+
}
|
63
|
+
|
64
|
+
# Top border
|
65
|
+
puts border_line.call("┌", "┬", "┐")
|
66
|
+
|
67
|
+
# Header row
|
68
|
+
printf "│ %-#{file_width}s │ %#{pct_width}s │ %#{covered_width}s │ %#{total_width}s │\n",
|
69
|
+
"File", " %", "Covered", "Total"
|
70
|
+
|
71
|
+
# Header separator
|
72
|
+
puts border_line.call("├", "┼", "┤")
|
73
|
+
|
74
|
+
# Data rows
|
75
|
+
file_summaries.each do |file_data|
|
76
|
+
printf "│ %-#{file_width}s │ %#{pct_width - 1}.2f%% │ %#{covered_width}d │ %#{total_width}d │\n",
|
77
|
+
file_data[:file],
|
78
|
+
file_data[:percentage],
|
79
|
+
file_data[:covered],
|
80
|
+
file_data[:total]
|
81
|
+
end
|
82
|
+
|
83
|
+
# Bottom border
|
84
|
+
puts border_line.call("└", "┴", "┘")
|
85
|
+
end
|
86
|
+
|
87
|
+
def run_mcp_server
|
88
|
+
server = ::MCP::Server.new(
|
89
|
+
name: "ruby_coverage_server",
|
90
|
+
version: Simplecov::Mcp::VERSION,
|
91
|
+
tools: [CoverageRaw, CoverageSummary, UncoveredLines, CoverageDetailed, AllFilesCoverage]
|
92
|
+
)
|
93
|
+
::MCP::Server::Transports::StdioTransport.new(server).open
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simplecov
|
4
|
+
module Mcp
|
5
|
+
class CoverageModel
|
6
|
+
def initialize(root: ".")
|
7
|
+
@root = File.absolute_path(root || ".")
|
8
|
+
@cov = CovUtil.load_latest_coverage(@root)
|
9
|
+
end
|
10
|
+
|
11
|
+
# Returns { file: <abs>, lines: [hits|nil,...] }
|
12
|
+
def raw_for(path)
|
13
|
+
abs, arr = resolve(path)
|
14
|
+
{ file: abs, lines: arr }
|
15
|
+
end
|
16
|
+
|
17
|
+
# Returns { file: <abs>, summary: {"covered"=>, "total"=>, "pct"=>} }
|
18
|
+
def summary_for(path)
|
19
|
+
abs, arr = resolve(path)
|
20
|
+
{ file: abs, summary: CovUtil.summary(arr) }
|
21
|
+
end
|
22
|
+
|
23
|
+
# Returns { file: <abs>, uncovered: [line,...], summary: {...} }
|
24
|
+
def uncovered_for(path)
|
25
|
+
abs, arr = resolve(path)
|
26
|
+
{ file: abs, uncovered: CovUtil.uncovered(arr), summary: CovUtil.summary(arr) }
|
27
|
+
end
|
28
|
+
|
29
|
+
# Returns { file: <abs>, lines: [{line:,hits:,covered:},...], summary: {...} }
|
30
|
+
def detailed_for(path)
|
31
|
+
abs, arr = resolve(path)
|
32
|
+
{ file: abs, lines: CovUtil.detailed(arr), summary: CovUtil.summary(arr) }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Returns [ { file:, covered:, total:, percentage: }, ... ]
|
36
|
+
def all_files(sort_order: :ascending)
|
37
|
+
rows = @cov.map do |abs_path, data|
|
38
|
+
next unless data["lines"].is_a?(Array)
|
39
|
+
s = CovUtil.summary(data["lines"])
|
40
|
+
{ file: abs_path, covered: s["covered"], total: s["total"], percentage: s["pct"] }
|
41
|
+
end.compact
|
42
|
+
|
43
|
+
rows.sort! do |a, b|
|
44
|
+
pct_cmp = (sort_order.to_s == "descending") ? (b[:percentage] <=> a[:percentage]) : (a[:percentage] <=> b[:percentage])
|
45
|
+
pct_cmp == 0 ? (a[:file] <=> b[:file]) : pct_cmp
|
46
|
+
end
|
47
|
+
rows
|
48
|
+
end
|
49
|
+
|
50
|
+
private
|
51
|
+
|
52
|
+
def resolve(path)
|
53
|
+
abs = File.absolute_path(path, @root)
|
54
|
+
[abs, CovUtil.lookup_lines(@cov, abs)]
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simplecov
|
4
|
+
module Mcp
|
5
|
+
class AllFilesCoverage < ::MCP::Tool
|
6
|
+
description "Return coverage percentage for all files in the project"
|
7
|
+
input_schema(
|
8
|
+
type: "object",
|
9
|
+
properties: {
|
10
|
+
root: { type: "string", description: "Project root for resolution", default: "." },
|
11
|
+
sort_order: { type: "string", description: "Sort order for coverage percentage: ascending or descending", default: "ascending", enum: ["ascending", "descending"] }
|
12
|
+
}
|
13
|
+
)
|
14
|
+
class << self
|
15
|
+
def call(root: ".", sort_order: "ascending", server_context:)
|
16
|
+
model = CoverageModel.new(root: root)
|
17
|
+
files = model.all_files(sort_order: sort_order)
|
18
|
+
::MCP::Tool::Response.new([{ type: "json", json: { files: files } }],
|
19
|
+
meta: { mimeType: "application/json" })
|
20
|
+
rescue => e
|
21
|
+
CovUtil.log("AllFilesCoverage error: #{e.class}: #{e.message}")
|
22
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.class}: #{e.message}" }])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simplecov
|
4
|
+
module Mcp
|
5
|
+
class CoverageDetailed < BaseTool
|
6
|
+
description "Verbose per-line objects [{line,hits,covered}] (token-heavy)"
|
7
|
+
input_schema(**input_schema_def)
|
8
|
+
class << self
|
9
|
+
def call(path:, root: ".", server_context:)
|
10
|
+
model = CoverageModel.new(root: root)
|
11
|
+
data = model.detailed_for(path)
|
12
|
+
::MCP::Tool::Response.new([{ type: "json", json: data }],
|
13
|
+
meta: { mimeType: "application/json" })
|
14
|
+
rescue => e
|
15
|
+
CovUtil.log("CoverageDetailed error: #{e.class}: #{e.message}")
|
16
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.class}: #{e.message}" }])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simplecov
|
4
|
+
module Mcp
|
5
|
+
class CoverageRaw < BaseTool
|
6
|
+
description "Return the original SimpleCov 'lines' array for a file"
|
7
|
+
input_schema(**input_schema_def)
|
8
|
+
class << self
|
9
|
+
def call(path:, root: ".", server_context:)
|
10
|
+
model = CoverageModel.new(root: root)
|
11
|
+
data = model.raw_for(path)
|
12
|
+
::MCP::Tool::Response.new([{ type: "json", json: data }],
|
13
|
+
meta: { mimeType: "application/json" })
|
14
|
+
rescue => e
|
15
|
+
CovUtil.log("CoverageRaw error: #{e.class}: #{e.message}")
|
16
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.class}: #{e.message}" }])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simplecov
|
4
|
+
module Mcp
|
5
|
+
class CoverageSummary < BaseTool
|
6
|
+
description "Return {covered,total,pct} for a file"
|
7
|
+
input_schema(**input_schema_def)
|
8
|
+
class << self
|
9
|
+
def call(path:, root: ".", server_context:)
|
10
|
+
model = CoverageModel.new(root: root)
|
11
|
+
data = model.summary_for(path)
|
12
|
+
::MCP::Tool::Response.new([{ type: "json", json: data }],
|
13
|
+
meta: { mimeType: "application/json" })
|
14
|
+
rescue => e
|
15
|
+
CovUtil.log("CoverageSummary error: #{e.class}: #{e.message}")
|
16
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.class}: #{e.message}" }])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simplecov
|
4
|
+
module Mcp
|
5
|
+
class UncoveredLines < BaseTool
|
6
|
+
description "Return only uncovered executable line numbers plus a summary"
|
7
|
+
input_schema(**input_schema_def)
|
8
|
+
class << self
|
9
|
+
def call(path:, root: ".", server_context:)
|
10
|
+
model = CoverageModel.new(root: root)
|
11
|
+
data = model.uncovered_for(path)
|
12
|
+
::MCP::Tool::Response.new([{ type: "json", json: data }],
|
13
|
+
meta: { mimeType: "application/json" })
|
14
|
+
rescue => e
|
15
|
+
CovUtil.log("UncoveredLines error: #{e.class}: #{e.message}")
|
16
|
+
::MCP::Tool::Response.new([{ type: "text", text: "Error: #{e.class}: #{e.message}" }])
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
@@ -0,0 +1,94 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Simplecov
|
4
|
+
module Mcp
|
5
|
+
RESULTSET_CANDIDATES = [
|
6
|
+
".resultset.json",
|
7
|
+
"coverage/.resultset.json",
|
8
|
+
"tmp/.resultset.json"
|
9
|
+
].freeze
|
10
|
+
|
11
|
+
module CovUtil
|
12
|
+
module_function
|
13
|
+
|
14
|
+
def log(msg)
|
15
|
+
path = File.expand_path("~/coverage_mcp.log")
|
16
|
+
File.open(path, "a") { |f| f.puts "[#{Time.now.iso8601}] #{msg}" }
|
17
|
+
rescue StandardError
|
18
|
+
# ignore logging failures
|
19
|
+
end
|
20
|
+
|
21
|
+
def find_resultset(root)
|
22
|
+
if (env = ENV["SIMPLECOV_RESULTSET"]) && !env.empty?
|
23
|
+
path = File.absolute_path(env, root)
|
24
|
+
return path if File.file?(path)
|
25
|
+
end
|
26
|
+
RESULTSET_CANDIDATES
|
27
|
+
.map { |p| File.absolute_path(p, root) }
|
28
|
+
.find { |p| File.file?(p) } or
|
29
|
+
raise "Could not find .resultset.json under #{root.inspect}; run tests or set SIMPLECOV_RESULTSET"
|
30
|
+
end
|
31
|
+
|
32
|
+
# returns { abs_path => {"lines" => [hits|nil,...]} }
|
33
|
+
def load_latest_coverage(root)
|
34
|
+
rs = find_resultset(root)
|
35
|
+
raw = JSON.parse(File.read(rs))
|
36
|
+
_suite, data = raw.max_by { |_k, v| (v["timestamp"] || v["created_at"] || 0).to_i }
|
37
|
+
cov = data["coverage"] or raise "No 'coverage' key in .resultset.json"
|
38
|
+
cov.transform_keys { |k| File.absolute_path(k, root) }
|
39
|
+
end
|
40
|
+
|
41
|
+
def lookup_lines(cov, file_abs)
|
42
|
+
if (h = cov[file_abs]) && h["lines"].is_a?(Array)
|
43
|
+
return h["lines"]
|
44
|
+
end
|
45
|
+
|
46
|
+
# try without current working directory prefix
|
47
|
+
cwd = Dir.pwd
|
48
|
+
without = file_abs.sub(/\A#{Regexp.escape(cwd)}\//, "")
|
49
|
+
if (h = cov[without]) && h["lines"].is_a?(Array)
|
50
|
+
return h["lines"]
|
51
|
+
end
|
52
|
+
|
53
|
+
# fallback: basename match
|
54
|
+
base = File.basename(file_abs)
|
55
|
+
kv = cov.find { |k, v| File.basename(k) == base && v["lines"].is_a?(Array) }
|
56
|
+
kv and return kv[1]["lines"]
|
57
|
+
|
58
|
+
raise "No coverage entry found for #{file_abs}"
|
59
|
+
end
|
60
|
+
|
61
|
+
def summary(arr)
|
62
|
+
total = 0
|
63
|
+
covered = 0
|
64
|
+
arr.each do |hits|
|
65
|
+
next if hits.nil?
|
66
|
+
total += 1
|
67
|
+
covered += 1 if hits.to_i > 0
|
68
|
+
end
|
69
|
+
pct = total.zero? ? 100.0 : ((covered.to_f * 100.0 / total) * 100).round / 100.0
|
70
|
+
{ "covered" => covered, "total" => total, "pct" => pct }
|
71
|
+
end
|
72
|
+
|
73
|
+
def uncovered(arr)
|
74
|
+
out = []
|
75
|
+
arr.each_with_index do |hits, i|
|
76
|
+
next if hits.nil?
|
77
|
+
out << (i + 1) if hits.to_i.zero?
|
78
|
+
end
|
79
|
+
out
|
80
|
+
end
|
81
|
+
|
82
|
+
def detailed(arr)
|
83
|
+
rows = []
|
84
|
+
arr.each_with_index do |hits, i|
|
85
|
+
next if hits.nil?
|
86
|
+
h = hits.to_i
|
87
|
+
rows << { line: i + 1, hits: h, covered: h.positive? }
|
88
|
+
end
|
89
|
+
rows
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "json"
|
4
|
+
require "time"
|
5
|
+
require "pathname"
|
6
|
+
require "mcp"
|
7
|
+
require "mcp/server/transports/stdio_transport"
|
8
|
+
require "awesome_print"
|
9
|
+
|
10
|
+
require_relative "mcp/version"
|
11
|
+
require_relative "mcp/util"
|
12
|
+
require_relative "mcp/model"
|
13
|
+
require_relative "mcp/base_tool"
|
14
|
+
require_relative "mcp/tools/coverage_raw"
|
15
|
+
require_relative "mcp/tools/coverage_summary"
|
16
|
+
require_relative "mcp/tools/uncovered_lines"
|
17
|
+
require_relative "mcp/tools/coverage_detailed"
|
18
|
+
require_relative "mcp/tools/all_files_coverage"
|
19
|
+
require_relative "mcp/cli"
|
20
|
+
|
21
|
+
module Simplecov
|
22
|
+
module Mcp
|
23
|
+
def self.run(argv)
|
24
|
+
CoverageCLI.new.run(argv)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
@@ -0,0 +1,54 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
RSpec.describe Simplecov::Mcp::CoverageModel do
|
6
|
+
let(:root) { (FIXTURES / "project1").to_s }
|
7
|
+
subject(:model) { described_class.new(root: root) }
|
8
|
+
|
9
|
+
describe "raw_for" do
|
10
|
+
it "returns absolute file and lines array" do
|
11
|
+
data = model.raw_for("lib/foo.rb")
|
12
|
+
expect(data[:file]).to eq(File.expand_path("lib/foo.rb", root))
|
13
|
+
expect(data[:lines]).to eq([1, 0, nil, 2])
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
describe "summary_for" do
|
18
|
+
it "computes covered/total/pct" do
|
19
|
+
data = model.summary_for("lib/foo.rb")
|
20
|
+
expect(data[:summary]["total"]).to eq(3)
|
21
|
+
expect(data[:summary]["covered"]).to eq(2)
|
22
|
+
expect(data[:summary]["pct"]).to be_within(0.01).of(66.67)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe "uncovered_for" do
|
27
|
+
it "lists uncovered executable line numbers" do
|
28
|
+
data = model.uncovered_for("lib/foo.rb")
|
29
|
+
expect(data[:uncovered]).to eq([2])
|
30
|
+
expect(data[:summary]["total"]).to eq(3)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "detailed_for" do
|
35
|
+
it "returns per-line details for non-nil lines" do
|
36
|
+
data = model.detailed_for("lib/foo.rb")
|
37
|
+
expect(data[:lines]).to eq([
|
38
|
+
{ line: 1, hits: 1, covered: true },
|
39
|
+
{ line: 2, hits: 0, covered: false },
|
40
|
+
{ line: 4, hits: 2, covered: true }
|
41
|
+
])
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
describe "all_files" do
|
46
|
+
it "sorts ascending by percentage then by file path" do
|
47
|
+
files = model.all_files(sort_order: :ascending)
|
48
|
+
expect(files.first[:file]).to eq(File.expand_path("lib/bar.rb", root))
|
49
|
+
expect(files.first[:percentage]).to be_within(0.01).of(33.33)
|
50
|
+
expect(files.last[:file]).to eq(File.expand_path("lib/foo.rb", root))
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ENV.delete("SIMPLECOV_RESULTSET")
|
4
|
+
|
5
|
+
require "rspec"
|
6
|
+
require "pathname"
|
7
|
+
require "json"
|
8
|
+
|
9
|
+
require "simplecov/mcp"
|
10
|
+
|
11
|
+
FIXTURES = Pathname.new(File.expand_path("fixtures", __dir__))
|
12
|
+
|
13
|
+
RSpec.configure do |config|
|
14
|
+
config.example_status_persistence_file_path = ".rspec_status"
|
15
|
+
config.disable_monkey_patching!
|
16
|
+
config.order = :random
|
17
|
+
Kernel.srand config.seed
|
18
|
+
end
|
19
|
+
|
metadata
ADDED
@@ -0,0 +1,106 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: simplecov-mcp
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Keith R. Bennett
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: mcp
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '0.2'
|
19
|
+
type: :runtime
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '0.2'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: awesome_print
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: 1.9.2
|
33
|
+
- - "<"
|
34
|
+
- !ruby/object:Gem::Version
|
35
|
+
version: '2'
|
36
|
+
type: :runtime
|
37
|
+
prerelease: false
|
38
|
+
version_requirements: !ruby/object:Gem::Requirement
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
version: 1.9.2
|
43
|
+
- - "<"
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: '2'
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: rspec
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
requirements:
|
50
|
+
- - "~>"
|
51
|
+
- !ruby/object:Gem::Version
|
52
|
+
version: '3.0'
|
53
|
+
type: :development
|
54
|
+
prerelease: false
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - "~>"
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '3.0'
|
60
|
+
description: Provides an MCP (Model Context Protocol) server and a CLI to inspect
|
61
|
+
SimpleCov coverage, including per-file summaries and uncovered lines.
|
62
|
+
email:
|
63
|
+
- keithrbennett@gmail.com
|
64
|
+
executables:
|
65
|
+
- simplecov-mcp
|
66
|
+
extensions: []
|
67
|
+
extra_rdoc_files: []
|
68
|
+
files:
|
69
|
+
- README.md
|
70
|
+
- exe/simplecov-mcp
|
71
|
+
- lib/simplecov/mcp.rb
|
72
|
+
- lib/simplecov/mcp/base_tool.rb
|
73
|
+
- lib/simplecov/mcp/cli.rb
|
74
|
+
- lib/simplecov/mcp/model.rb
|
75
|
+
- lib/simplecov/mcp/tools/all_files_coverage.rb
|
76
|
+
- lib/simplecov/mcp/tools/coverage_detailed.rb
|
77
|
+
- lib/simplecov/mcp/tools/coverage_raw.rb
|
78
|
+
- lib/simplecov/mcp/tools/coverage_summary.rb
|
79
|
+
- lib/simplecov/mcp/tools/uncovered_lines.rb
|
80
|
+
- lib/simplecov/mcp/util.rb
|
81
|
+
- lib/simplecov/mcp/version.rb
|
82
|
+
- lib/simplecov_mcp.rb
|
83
|
+
- spec/simplecov_mcp_model_spec.rb
|
84
|
+
- spec/spec_helper.rb
|
85
|
+
homepage: https://github.com/keithrbennett/simplecov-mcp
|
86
|
+
licenses:
|
87
|
+
- MIT
|
88
|
+
metadata: {}
|
89
|
+
rdoc_options: []
|
90
|
+
require_paths:
|
91
|
+
- lib
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - ">="
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '3.2'
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
98
|
+
requirements:
|
99
|
+
- - ">="
|
100
|
+
- !ruby/object:Gem::Version
|
101
|
+
version: '0'
|
102
|
+
requirements: []
|
103
|
+
rubygems_version: 3.7.1
|
104
|
+
specification_version: 4
|
105
|
+
summary: MCP server + CLI for SimpleCov coverage data
|
106
|
+
test_files: []
|