simplecov-mcp 0.1.0 → 0.2.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +379 -32
  3. data/exe/simplecov-mcp +19 -2
  4. data/lib/simple_cov/mcp.rb +9 -0
  5. data/lib/simple_cov_mcp/base_tool.rb +54 -0
  6. data/lib/simple_cov_mcp/cli.rb +390 -0
  7. data/lib/simple_cov_mcp/error_handler.rb +131 -0
  8. data/lib/simple_cov_mcp/error_handler_factory.rb +38 -0
  9. data/lib/simple_cov_mcp/errors.rb +176 -0
  10. data/lib/simple_cov_mcp/mcp_server.rb +30 -0
  11. data/lib/simple_cov_mcp/model.rb +104 -0
  12. data/lib/simple_cov_mcp/staleness_checker.rb +125 -0
  13. data/lib/simple_cov_mcp/tools/all_files_coverage_tool.rb +63 -0
  14. data/lib/simple_cov_mcp/tools/coverage_detailed_tool.rb +29 -0
  15. data/lib/simple_cov_mcp/tools/coverage_raw_tool.rb +29 -0
  16. data/lib/simple_cov_mcp/tools/coverage_summary_tool.rb +29 -0
  17. data/lib/simple_cov_mcp/tools/coverage_table_tool.rb +61 -0
  18. data/lib/simple_cov_mcp/tools/help_tool.rb +133 -0
  19. data/lib/simple_cov_mcp/tools/uncovered_lines_tool.rb +29 -0
  20. data/lib/simple_cov_mcp/tools/version_tool.rb +31 -0
  21. data/lib/simple_cov_mcp/util.rb +122 -0
  22. data/lib/simple_cov_mcp/version.rb +5 -0
  23. data/lib/simple_cov_mcp.rb +102 -0
  24. data/lib/simplecov_mcp.rb +2 -3
  25. data/spec/all_files_coverage_tool_spec.rb +46 -0
  26. data/spec/base_tool_spec.rb +58 -0
  27. data/spec/cli_error_spec.rb +103 -0
  28. data/spec/cli_json_source_spec.rb +92 -0
  29. data/spec/cli_source_spec.rb +37 -0
  30. data/spec/cli_spec.rb +72 -0
  31. data/spec/cli_table_spec.rb +28 -0
  32. data/spec/cli_usage_spec.rb +58 -0
  33. data/spec/coverage_table_tool_spec.rb +64 -0
  34. data/spec/error_handler_spec.rb +72 -0
  35. data/spec/errors_stale_spec.rb +49 -0
  36. data/spec/fixtures/project1/lib/bar.rb +4 -0
  37. data/spec/fixtures/project1/lib/foo.rb +5 -0
  38. data/spec/help_tool_spec.rb +47 -0
  39. data/spec/legacy_shim_spec.rb +13 -0
  40. data/spec/mcp_server_spec.rb +78 -0
  41. data/spec/model_staleness_spec.rb +49 -0
  42. data/spec/simplecov_mcp_model_spec.rb +51 -32
  43. data/spec/spec_helper.rb +37 -7
  44. data/spec/staleness_more_spec.rb +39 -0
  45. data/spec/util_spec.rb +78 -0
  46. data/spec/version_spec.rb +10 -0
  47. metadata +56 -13
  48. data/lib/simplecov/mcp/base_tool.rb +0 -18
  49. data/lib/simplecov/mcp/cli.rb +0 -98
  50. data/lib/simplecov/mcp/model.rb +0 -59
  51. data/lib/simplecov/mcp/tools/all_files_coverage.rb +0 -28
  52. data/lib/simplecov/mcp/tools/coverage_detailed.rb +0 -22
  53. data/lib/simplecov/mcp/tools/coverage_raw.rb +0 -22
  54. data/lib/simplecov/mcp/tools/coverage_summary.rb +0 -22
  55. data/lib/simplecov/mcp/tools/uncovered_lines.rb +0 -22
  56. data/lib/simplecov/mcp/util.rb +0 -94
  57. data/lib/simplecov/mcp/version.rb +0 -8
  58. data/lib/simplecov/mcp.rb +0 -28
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SimpleCovMcp::CoverageModel do
6
+ let(:root) { (FIXTURES / 'project1').to_s }
7
+
8
+ def with_stubbed_coverage_timestamp(ts) = begin
9
+ allow(SimpleCovMcp::CovUtil).to receive(:latest_timestamp).and_return(ts)
10
+ yield
11
+ end
12
+
13
+ it "raises stale error when staleness mode is 'error' and file is newer" do
14
+ with_stubbed_coverage_timestamp(0) do
15
+ model = described_class.new(root: root, staleness: 'error')
16
+ expect {
17
+ model.summary_for('lib/foo.rb')
18
+ }.to raise_error(SimpleCovMcp::CoverageDataStaleError, /stale/i)
19
+ end
20
+ end
21
+
22
+ it "does not check staleness when mode is 'off'" do
23
+ with_stubbed_coverage_timestamp(0) do
24
+ model = described_class.new(root: root, staleness: 'off')
25
+ expect { model.summary_for('lib/foo.rb') }.not_to raise_error
26
+ end
27
+ end
28
+ it 'all_files raises project-level stale when any source file is newer than coverage' do
29
+ with_stubbed_coverage_timestamp(0) do
30
+ model = described_class.new(root: root, staleness: 'error')
31
+ expect { model.all_files }.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
32
+ end
33
+ end
34
+
35
+ it 'all_files detects new files via tracked_globs' do
36
+ with_stubbed_coverage_timestamp(Time.now.to_i) do
37
+ tmp = File.join(root, 'lib', 'brand_new_file.rb')
38
+ begin
39
+ File.write(tmp, "# new file\n")
40
+ model = described_class.new(root: root, staleness: 'error')
41
+ expect {
42
+ model.all_files(tracked_globs: ['lib/**/*.rb'])
43
+ }.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
44
+ ensure
45
+ File.delete(tmp) if File.exist?(tmp)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -1,54 +1,73 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "spec_helper"
3
+ require 'spec_helper'
4
4
 
5
- RSpec.describe Simplecov::Mcp::CoverageModel do
6
- let(:root) { (FIXTURES / "project1").to_s }
5
+ RSpec.describe SimpleCovMcp::CoverageModel do
6
+ let(:root) { (FIXTURES / 'project1').to_s }
7
7
  subject(:model) { described_class.new(root: root) }
8
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])
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
14
  end
15
15
  end
16
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)
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
23
  end
24
24
  end
25
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)
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
31
  end
32
32
  end
33
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 }
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
41
  ])
42
42
  end
43
43
  end
44
44
 
45
- describe "all_files" do
46
- it "sorts ascending by percentage then by file path" do
45
+ describe 'all_files' do
46
+ it 'sorts ascending by percentage then by file path' do
47
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))
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
51
  end
52
52
  end
53
- end
54
53
 
54
+ describe 'resultset directory handling' do
55
+ it 'accepts a directory containing .resultset.json' do
56
+ model = described_class.new(root: root, resultset: 'coverage')
57
+ data = model.summary_for('lib/foo.rb')
58
+ expect(data['summary']['total']).to eq(3)
59
+ expect(data['summary']['covered']).to eq(2)
60
+ end
61
+
62
+ it 'uses SIMPLECOV_RESULTSET when it is a directory' do
63
+ begin
64
+ ENV['SIMPLECOV_RESULTSET'] = 'coverage'
65
+ model = described_class.new(root: root)
66
+ data = model.summary_for('lib/foo.rb')
67
+ expect(data['summary']['covered']).to eq(2)
68
+ ensure
69
+ ENV.delete('SIMPLECOV_RESULTSET')
70
+ end
71
+ end
72
+ end
73
+ end
data/spec/spec_helper.rb CHANGED
@@ -1,19 +1,49 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ENV.delete("SIMPLECOV_RESULTSET")
3
+ # Enable SimpleCov for this project (coverage output in ./coverage)
4
+ begin
5
+ require 'simplecov'
6
+ SimpleCov.start do
7
+ enable_coverage :branch if SimpleCov.respond_to?(:enable_coverage)
8
+ add_filter %r{^/spec/}
9
+ track_files 'lib/**/*.rb'
10
+ end
11
+ rescue LoadError
12
+ warn 'SimpleCov not available; skipping coverage'
13
+ end
14
+
15
+ ENV.delete('SIMPLECOV_RESULTSET')
4
16
 
5
- require "rspec"
6
- require "pathname"
7
- require "json"
17
+ require 'rspec'
18
+ require 'pathname'
19
+ require 'json'
8
20
 
9
- require "simplecov/mcp"
21
+ require 'simple_cov_mcp'
10
22
 
11
- FIXTURES = Pathname.new(File.expand_path("fixtures", __dir__))
23
+ FIXTURES = Pathname.new(File.expand_path('fixtures', __dir__))
12
24
 
13
25
  RSpec.configure do |config|
14
- config.example_status_persistence_file_path = ".rspec_status"
26
+ config.example_status_persistence_file_path = '.rspec_status'
15
27
  config.disable_monkey_patching!
16
28
  config.order = :random
17
29
  Kernel.srand config.seed
18
30
  end
19
31
 
32
+ # Shared test helpers
33
+ module TestIOHelpers
34
+ # Suppress stdout/stderr within the given block, yielding the StringIOs
35
+ def silence_output
36
+ original_stdout = $stdout
37
+ original_stderr = $stderr
38
+ $stdout = StringIO.new
39
+ $stderr = StringIO.new
40
+ yield $stdout, $stderr
41
+ ensure
42
+ $stdout = original_stdout
43
+ $stderr = original_stderr
44
+ end
45
+ end
46
+
47
+ RSpec.configure do |config|
48
+ config.include TestIOHelpers
49
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Additional staleness cases' do
6
+ let(:root) { (FIXTURES / 'project1').to_s }
7
+
8
+ describe SimpleCovMcp::CoverageModel do
9
+ it 'raises file-level stale when source and coverage lengths differ' do
10
+ # Ensure time is not the triggering factor
11
+ allow(SimpleCovMcp::CovUtil).to receive(:latest_timestamp).and_return(Time.now.to_i)
12
+ model = SimpleCovMcp::CoverageModel.new(root: root, resultset: 'coverage', staleness: 'error')
13
+ # bar.rb has 3 coverage entries but 4 source lines in fixtures
14
+ expect {
15
+ model.summary_for('lib/bar.rb')
16
+ }.to raise_error(SimpleCovMcp::CoverageDataStaleError, /stale/i)
17
+ end
18
+ end
19
+
20
+ describe SimpleCovMcp::StalenessChecker do
21
+ it 'flags deleted files present only in coverage' do
22
+ checker = described_class.new(root: root, resultset: 'coverage', mode: 'error')
23
+ coverage_map = {
24
+ File.join(root, 'lib', 'does_not_exist_anymore.rb') => { 'lines' => [1] }
25
+ }
26
+ expect {
27
+ checker.check_project!(coverage_map)
28
+ }.to raise_error(SimpleCovMcp::CoverageDataProjectStaleError)
29
+ end
30
+
31
+ it 'does not raise for empty tracked_globs when nothing else is stale' do
32
+ allow(SimpleCovMcp::CovUtil).to receive(:latest_timestamp).and_return(Time.now.to_i)
33
+ checker = described_class.new(root: root, resultset: 'coverage', mode: 'error', tracked_globs: [])
34
+ expect {
35
+ checker.check_project!({})
36
+ }.not_to raise_error
37
+ end
38
+ end
39
+ end
data/spec/util_spec.rb ADDED
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe SimpleCovMcp::CovUtil do
6
+ let(:root) { (FIXTURES / 'project1').to_s }
7
+ let(:resultset_file) { File.join(root, 'coverage', '.resultset.json') }
8
+
9
+ it 'latest_timestamp returns integer from fixture' do
10
+ ts = described_class.latest_timestamp(root, resultset: 'coverage')
11
+ expect(ts).to be_a(Integer)
12
+ expect(ts).to eq(1_720_000_000)
13
+ end
14
+
15
+ it 'find_resultset honors SIMPLECOV_RESULTSET file path' do
16
+ begin
17
+ ENV['SIMPLECOV_RESULTSET'] = resultset_file
18
+ path = described_class.find_resultset(root)
19
+ expect(path).to eq(File.absolute_path(resultset_file, root))
20
+ ensure
21
+ ENV.delete('SIMPLECOV_RESULTSET')
22
+ end
23
+ end
24
+
25
+ it 'lookup_lines supports cwd-stripping and basename fallbacks' do
26
+ lines = [1, 0]
27
+
28
+ # Exact key
29
+ cov = { '/abs/path/foo.rb' => { 'lines' => lines } }
30
+ expect(described_class.lookup_lines(cov, '/abs/path/foo.rb')).to eq(lines)
31
+
32
+ # CWD strip fallback
33
+ begin
34
+ allow(Dir).to receive(:pwd).and_return('/cwd')
35
+ cov = { 'sub/foo.rb' => { 'lines' => lines } }
36
+ expect(described_class.lookup_lines(cov, '/cwd/sub/foo.rb')).to eq(lines)
37
+ ensure
38
+ # no-op
39
+ end
40
+
41
+ # Basename fallback
42
+ cov = { '/some/where/else/foo.rb' => { 'lines' => lines } }
43
+ expect(described_class.lookup_lines(cov, '/another/place/foo.rb')).to eq(lines)
44
+
45
+ # Missing raises a helpful string error
46
+ cov = {}
47
+ expect {
48
+ described_class.lookup_lines(cov, '/nowhere/foo.rb')
49
+ }.to raise_error(RuntimeError, /No coverage entry found/)
50
+ end
51
+
52
+ it 'summary handles edge cases and coercion' do
53
+ expect(described_class.summary([])).to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
54
+ expect(described_class.summary([nil, nil])).to include('pct' => 100.0, 'total' => 0, 'covered' => 0)
55
+ expect(described_class.summary(['1', '0', nil])).to include('pct' => 50.0, 'total' => 2, 'covered' => 1)
56
+ end
57
+
58
+ it 'uncovered and detailed ignore nils' do
59
+ arr = [1, 0, nil, 2]
60
+ expect(described_class.uncovered(arr)).to eq([2])
61
+ expect(described_class.detailed(arr)).to eq([
62
+ { 'line' => 1, 'hits' => 1, 'covered' => true },
63
+ { 'line' => 2, 'hits' => 0, 'covered' => false },
64
+ { 'line' => 4, 'hits' => 2, 'covered' => true }
65
+ ])
66
+ end
67
+
68
+ it 'load_latest_coverage raises CoverageDataError on invalid JSON via model' do
69
+ Dir.mktmpdir do |dir|
70
+ bad = File.join(dir, '.resultset.json')
71
+ File.write(bad, '{not-json')
72
+ expect {
73
+ SimpleCovMcp::CoverageModel.new(root: root, resultset: dir)
74
+ }.to raise_error(SimpleCovMcp::CoverageDataError, /Invalid coverage data format/)
75
+ end
76
+ end
77
+ end
78
+
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ RSpec.describe 'Version constant' do
6
+ it 'exposes a semver-like version string' do
7
+ expect(SimpleCovMcp::VERSION).to be_a(String)
8
+ expect(SimpleCovMcp::VERSION).to match(/\A\d+\.\d+\.\d+(?:[.-][0-9A-Za-z]+)?\z/)
9
+ end
10
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: simplecov-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith R. Bennett
@@ -57,6 +57,20 @@ dependencies:
57
57
  - - "~>"
58
58
  - !ruby/object:Gem::Version
59
59
  version: '3.0'
60
+ - !ruby/object:Gem::Dependency
61
+ name: simplecov
62
+ requirement: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0.21'
67
+ type: :development
68
+ prerelease: false
69
+ version_requirements: !ruby/object:Gem::Requirement
70
+ requirements:
71
+ - - ">="
72
+ - !ruby/object:Gem::Version
73
+ version: '0.21'
60
74
  description: Provides an MCP (Model Context Protocol) server and a CLI to inspect
61
75
  SimpleCov coverage, including per-file summaries and uncovered lines.
62
76
  email:
@@ -68,20 +82,49 @@ extra_rdoc_files: []
68
82
  files:
69
83
  - README.md
70
84
  - 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
85
+ - lib/simple_cov/mcp.rb
86
+ - lib/simple_cov_mcp.rb
87
+ - lib/simple_cov_mcp/base_tool.rb
88
+ - lib/simple_cov_mcp/cli.rb
89
+ - lib/simple_cov_mcp/error_handler.rb
90
+ - lib/simple_cov_mcp/error_handler_factory.rb
91
+ - lib/simple_cov_mcp/errors.rb
92
+ - lib/simple_cov_mcp/mcp_server.rb
93
+ - lib/simple_cov_mcp/model.rb
94
+ - lib/simple_cov_mcp/staleness_checker.rb
95
+ - lib/simple_cov_mcp/tools/all_files_coverage_tool.rb
96
+ - lib/simple_cov_mcp/tools/coverage_detailed_tool.rb
97
+ - lib/simple_cov_mcp/tools/coverage_raw_tool.rb
98
+ - lib/simple_cov_mcp/tools/coverage_summary_tool.rb
99
+ - lib/simple_cov_mcp/tools/coverage_table_tool.rb
100
+ - lib/simple_cov_mcp/tools/help_tool.rb
101
+ - lib/simple_cov_mcp/tools/uncovered_lines_tool.rb
102
+ - lib/simple_cov_mcp/tools/version_tool.rb
103
+ - lib/simple_cov_mcp/util.rb
104
+ - lib/simple_cov_mcp/version.rb
82
105
  - lib/simplecov_mcp.rb
106
+ - spec/all_files_coverage_tool_spec.rb
107
+ - spec/base_tool_spec.rb
108
+ - spec/cli_error_spec.rb
109
+ - spec/cli_json_source_spec.rb
110
+ - spec/cli_source_spec.rb
111
+ - spec/cli_spec.rb
112
+ - spec/cli_table_spec.rb
113
+ - spec/cli_usage_spec.rb
114
+ - spec/coverage_table_tool_spec.rb
115
+ - spec/error_handler_spec.rb
116
+ - spec/errors_stale_spec.rb
117
+ - spec/fixtures/project1/lib/bar.rb
118
+ - spec/fixtures/project1/lib/foo.rb
119
+ - spec/help_tool_spec.rb
120
+ - spec/legacy_shim_spec.rb
121
+ - spec/mcp_server_spec.rb
122
+ - spec/model_staleness_spec.rb
83
123
  - spec/simplecov_mcp_model_spec.rb
84
124
  - spec/spec_helper.rb
125
+ - spec/staleness_more_spec.rb
126
+ - spec/util_spec.rb
127
+ - spec/version_spec.rb
85
128
  homepage: https://github.com/keithrbennett/simplecov-mcp
86
129
  licenses:
87
130
  - MIT
@@ -100,7 +143,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
100
143
  - !ruby/object:Gem::Version
101
144
  version: '0'
102
145
  requirements: []
103
- rubygems_version: 3.7.1
146
+ rubygems_version: 3.7.2
104
147
  specification_version: 4
105
148
  summary: MCP server + CLI for SimpleCov coverage data
106
149
  test_files: []
@@ -1,18 +0,0 @@
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
-
@@ -1,98 +0,0 @@
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
-
@@ -1,59 +0,0 @@
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
-
@@ -1,28 +0,0 @@
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
-
@@ -1,22 +0,0 @@
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
-