simplecov-mcp 0.2.1 → 0.3.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 41a4d8cde53da2d974436b735011f381362fa7e17e53d3ff587b41a9e66c285a
4
- data.tar.gz: d22bf6d5265373c0430499c7fbac6ed249d1786e63bac0b7562964b9d9ae8958
3
+ metadata.gz: '08e9a29ca4d2e2daafd4620690021de136eaa2cfc2ba652340f73d8b69547d8d'
4
+ data.tar.gz: 784e8ef1f21e6ed4e37783a840935e909d8a6d1f6ae4c941da34d0e6635f9f95
5
5
  SHA512:
6
- metadata.gz: 63aceca2e283855ee5ab6aaf16007b70baa4042e5085e976a9eecdc43f1054925d7b7749bc5ac9f4f64bed12392672f3c3f9c6006b3c844bbb5c0bcd9efa4fda
7
- data.tar.gz: 599201a717dc571240c0f10b0492e4938428f1b898745dafbfbb6988bbbfb4f5505c7632e91a4d7afe16bd4b1f44876280f635c3323cf23a847441ae3c7049e1
6
+ metadata.gz: 52e2a95fda335f611f38cd0b97164a62e381532ceb71b330c32023be10eeb8d97e999bdc32f0dafac168f0470cac481151e78f223623e90a377fe79ef807705b
7
+ data.tar.gz: 42ce8d9f01b069f2e3dd0d0e817cd135298fa14d06553ef5b0771d37baf9f5bd4c194d9a9296a823966da21ec505e69b41fd489dfcfa1b0e0b74bb63949b7a21
data/README.md CHANGED
@@ -92,6 +92,12 @@ echo '{"jsonrpc":"2.0","id":2,"method":"tools/call","params":{"name":"all_files_
92
92
 
93
93
  Tip: In an MCP-capable editor/agent, configure `simplecov-mcp` as a stdio server and call the same tool names with the `arguments` shown above.
94
94
 
95
+ Response content types (MCP):
96
+
97
+ - JSON data returns as a single `type: "resource"` item with `resource.mimeType: "application/json"` and the JSON string in `resource.text` (e.g., `name: "coverage_summary.json"`).
98
+ - Human-readable strings (e.g., the table and version) return as `type: "text"`.
99
+ - Errors return as `type: "text"` with a friendly message.
100
+
95
101
  Resultset resolution:
96
102
 
97
103
  - If `resultset:` is provided, it may be:
@@ -120,7 +126,7 @@ Using simplecov-mcp, show me a table of all files and their coverages.
120
126
 
121
127
  ----
122
128
 
123
- Using simplecov-mcp, find the uncovered code lines and report to me:
129
+ Using simplecov-mcp, find the uncovered code lines and report to me, in a markdown file:
124
130
 
125
131
  * the most important coverage omissions to address
126
132
  * the simplest coverage omissions to address
@@ -348,6 +354,11 @@ Available tools:
348
354
  - Returns `{ files: [{"file","covered","total","percentage","stale"}, ...] }` where `stale` is a boolean.
349
355
  - `version_tool()` — returns version information
350
356
 
357
+ Response shape and content types:
358
+
359
+ - JSON tools above return a single content item `{ "type": "resource", "resource": { "mimeType": "application/json", "text": "{...}", "name": "<tool>.json" } }`.
360
+ - `coverage_table_tool` and `version_tool` return `{ "type": "text", "text": "..." }`.
361
+
351
362
  Notes:
352
363
 
353
364
  - `resultset` lets clients pass a nonstandard path to `.resultset.json` directly (absolute or relative to `root`). It may be:
@@ -44,6 +44,22 @@ module SimpleCovMcp
44
44
  ::MCP::Tool::Response.new([{ type: 'text', text: "Error: #{normalized.user_friendly_message}" }])
45
45
  end
46
46
 
47
+ # Respond with JSON as a resource to avoid clients mutating content types.
48
+ # The resource embeds the JSON string with a clear MIME type.
49
+ def self.respond_json(payload, name: 'data.json', pretty: false)
50
+ json = pretty ? JSON.pretty_generate(payload) : JSON.generate(payload)
51
+ ::MCP::Tool::Response.new([
52
+ {
53
+ 'type' => 'resource',
54
+ 'resource' => {
55
+ 'mimeType' => 'application/json',
56
+ 'text' => json,
57
+ 'name' => name
58
+ }
59
+ }
60
+ ])
61
+ end
62
+
47
63
  private
48
64
 
49
65
  def self.log_mcp_error(error, tool_name)
@@ -53,9 +53,7 @@ module SimpleCovMcp
53
53
  stale_count = files.count { |f| f['stale'] }
54
54
  ok_count = total - stale_count
55
55
  payload = { files: files, counts: { total: total, ok: ok_count, stale: stale_count } }
56
- ::MCP::Tool::Response.new([
57
- { 'type' => 'text', 'text' => JSON.generate(payload) }
58
- ])
56
+ respond_json(payload, name: 'all_files_coverage.json')
59
57
  rescue => e
60
58
  handle_mcp_error(e, 'AllFilesCoverageTool')
61
59
  end
@@ -19,7 +19,7 @@ module SimpleCovMcp
19
19
  mode = stale
20
20
  model = CoverageModel.new(root: root, resultset: resultset, staleness: mode)
21
21
  data = model.detailed_for(path)
22
- ::MCP::Tool::Response.new([{ type: 'text', text: JSON.pretty_generate(data) }])
22
+ respond_json(data, name: 'coverage_detailed.json', pretty: true)
23
23
  rescue => e
24
24
  handle_mcp_error(e, 'CoverageDetailedTool')
25
25
  end
@@ -19,7 +19,7 @@ module SimpleCovMcp
19
19
  mode = stale
20
20
  model = CoverageModel.new(root: root, resultset: resultset, staleness: mode)
21
21
  data = model.raw_for(path)
22
- ::MCP::Tool::Response.new([{ type: 'text', text: JSON.pretty_generate(data) }])
22
+ respond_json(data, name: 'coverage_raw.json', pretty: true)
23
23
  rescue => e
24
24
  handle_mcp_error(e, 'CoverageRawTool')
25
25
  end
@@ -19,7 +19,7 @@ module SimpleCovMcp
19
19
  mode = stale
20
20
  model = CoverageModel.new(root: root, resultset: resultset, staleness: mode)
21
21
  data = model.summary_for(path)
22
- ::MCP::Tool::Response.new([{ type: 'text', text: JSON.pretty_generate(data) }])
22
+ respond_json(data, name: 'coverage_summary.json', pretty: true)
23
23
  rescue => e
24
24
  handle_mcp_error(e, 'CoverageSummaryTool')
25
25
  end
@@ -89,9 +89,7 @@ module SimpleCovMcp
89
89
  entries = filter_entries(entries, query) if query && !query.strip.empty?
90
90
 
91
91
  data = { query: query, tools: entries }
92
- ::MCP::Tool::Response.new([
93
- { 'type' => 'text', 'text' => JSON.generate(data) }
94
- ])
92
+ respond_json(data, name: 'tools_help.json')
95
93
  rescue => e
96
94
  handle_mcp_error(e, 'HelpTool')
97
95
  end
@@ -19,7 +19,7 @@ module SimpleCovMcp
19
19
  mode = stale
20
20
  model = CoverageModel.new(root: root, resultset: resultset, staleness: mode)
21
21
  data = model.uncovered_for(path)
22
- ::MCP::Tool::Response.new([{ type: 'text', text: JSON.pretty_generate(data) }])
22
+ respond_json(data, name: 'uncovered_lines.json', pretty: true)
23
23
  rescue => e
24
24
  handle_mcp_error(e, 'UncoveredLinesTool')
25
25
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCovMcp
4
- VERSION = '0.2.1'
4
+ VERSION = '0.3.0'
5
5
  end
@@ -28,7 +28,9 @@ RSpec.describe SimpleCovMcp::Tools::AllFilesCoverageTool do
28
28
 
29
29
  response = described_class.call(root: root, server_context: server_context)
30
30
  entry = response.payload.first
31
- json = JSON.parse(entry['text'])
31
+ expect(entry['type']).to eq('resource')
32
+ expect(entry['resource']).to include('mimeType' => 'application/json')
33
+ json = JSON.parse(entry['resource']['text'])
32
34
 
33
35
  expect(json).to have_key('files')
34
36
  files = json['files']
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'simple_cov_mcp/tools/coverage_detailed_tool'
5
+
6
+ RSpec.describe SimpleCovMcp::Tools::CoverageDetailedTool do
7
+ let(:server_context) { instance_double('ServerContext').as_null_object }
8
+
9
+ before do
10
+ stub_const('MCP::Tool::Response', Struct.new(:payload))
11
+ end
12
+
13
+ it 'returns JSON as an application/json resource' do
14
+ model = instance_double(SimpleCovMcp::CoverageModel)
15
+ allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
16
+ allow(model).to receive(:detailed_for).with('lib/foo.rb').and_return(
17
+ {
18
+ 'file' => '/abs/path/lib/foo.rb',
19
+ 'lines' => [
20
+ { 'line' => 1, 'hits' => 1, 'covered' => true },
21
+ { 'line' => 2, 'hits' => 0, 'covered' => false }
22
+ ],
23
+ 'summary' => { 'covered' => 1, 'total' => 2, 'pct' => 50.0 }
24
+ }
25
+ )
26
+
27
+ response = described_class.call(path: 'lib/foo.rb', server_context: server_context)
28
+ item = response.payload.first
29
+ expect(item['type']).to eq('resource')
30
+ expect(item['resource']).to include('mimeType' => 'application/json', 'name' => 'coverage_detailed.json')
31
+ data = JSON.parse(item['resource']['text'])
32
+ expect(data).to include('file', 'lines', 'summary')
33
+ expect(data['lines']).to be_an(Array)
34
+ end
35
+ end
36
+
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'simple_cov_mcp/tools/coverage_raw_tool'
5
+
6
+ RSpec.describe SimpleCovMcp::Tools::CoverageRawTool do
7
+ let(:server_context) { instance_double('ServerContext').as_null_object }
8
+
9
+ before do
10
+ stub_const('MCP::Tool::Response', Struct.new(:payload))
11
+ end
12
+
13
+ it 'returns JSON as an application/json resource' do
14
+ model = instance_double(SimpleCovMcp::CoverageModel)
15
+ allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
16
+ allow(model).to receive(:raw_for).with('lib/foo.rb').and_return(
17
+ {
18
+ 'file' => '/abs/path/lib/foo.rb',
19
+ 'lines' => [nil, 1, 0]
20
+ }
21
+ )
22
+
23
+ response = described_class.call(path: 'lib/foo.rb', server_context: server_context)
24
+ item = response.payload.first
25
+ expect(item['type']).to eq('resource')
26
+ expect(item['resource']).to include('mimeType' => 'application/json', 'name' => 'coverage_raw.json')
27
+ data = JSON.parse(item['resource']['text'])
28
+ expect(data).to include('file', 'lines')
29
+ expect(data['lines']).to be_an(Array)
30
+ end
31
+ end
32
+
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'simple_cov_mcp/tools/coverage_summary_tool'
5
+
6
+ RSpec.describe SimpleCovMcp::Tools::CoverageSummaryTool do
7
+ let(:server_context) { instance_double('ServerContext').as_null_object }
8
+
9
+ before do
10
+ response_class = Class.new do
11
+ attr_reader :payload, :meta
12
+ def initialize(payload, meta: nil)
13
+ @payload = payload
14
+ @meta = meta
15
+ end
16
+ end
17
+ stub_const('MCP::Tool::Response', response_class)
18
+ end
19
+
20
+ it 'returns JSON as an application/json resource' do
21
+ model = instance_double(SimpleCovMcp::CoverageModel)
22
+ allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
23
+ allow(model).to receive(:summary_for).with('lib/foo.rb').and_return(
24
+ {
25
+ 'file' => '/abs/path/lib/foo.rb',
26
+ 'summary' => { 'covered' => 10, 'total' => 12, 'pct' => 83.33 }
27
+ }
28
+ )
29
+
30
+ response = described_class.call(path: 'lib/foo.rb', server_context: server_context)
31
+ item = response.payload.first
32
+ expect(item['type']).to eq('resource')
33
+ expect(item['resource']).to include('mimeType' => 'application/json', 'name' => 'coverage_summary.json')
34
+ data = JSON.parse(item['resource']['text'])
35
+ expect(data).to include('file', 'summary')
36
+ expect(data['summary']).to include('covered', 'total', 'pct')
37
+ end
38
+ end
39
+
@@ -23,9 +23,9 @@ RSpec.describe SimpleCovMcp::Tools::HelpTool do
23
23
  expect(response.meta).to be_nil
24
24
 
25
25
  payload = response.payload.first
26
- expect(payload['type']).to eq('text')
27
-
28
- data = JSON.parse(payload['text'])
26
+ expect(payload['type']).to eq('resource')
27
+ expect(payload['resource']).to include('mimeType' => 'application/json')
28
+ data = JSON.parse(payload['resource']['text'])
29
29
  tool_names = data['tools'].map { |entry| entry['tool'] }
30
30
 
31
31
  expect(tool_names).to include('coverage_summary_tool', 'uncovered_lines_tool', 'all_files_coverage_tool', 'coverage_table_tool', 'version_tool')
@@ -35,7 +35,8 @@ RSpec.describe SimpleCovMcp::Tools::HelpTool do
35
35
  it 'filters entries when a query is provided' do
36
36
  response = described_class.call(query: 'uncovered', server_context: server_context)
37
37
  payload = response.payload.first
38
- data = JSON.parse(payload['text'])
38
+ expect(payload['type']).to eq('resource')
39
+ data = JSON.parse(payload['resource']['text'])
39
40
 
40
41
  expect(data['tools']).not_to be_empty
41
42
  expect(data['tools']).to all(satisfy do |entry|
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'simple_cov_mcp/tools/uncovered_lines_tool'
5
+
6
+ RSpec.describe SimpleCovMcp::Tools::UncoveredLinesTool do
7
+ let(:server_context) { instance_double('ServerContext').as_null_object }
8
+
9
+ before do
10
+ stub_const('MCP::Tool::Response', Struct.new(:payload))
11
+ end
12
+
13
+ it 'returns JSON as an application/json resource' do
14
+ model = instance_double(SimpleCovMcp::CoverageModel)
15
+ allow(SimpleCovMcp::CoverageModel).to receive(:new).and_return(model)
16
+ allow(model).to receive(:uncovered_for).with('lib/foo.rb').and_return(
17
+ {
18
+ 'file' => '/abs/path/lib/foo.rb',
19
+ 'uncovered' => [5, 9, 12],
20
+ 'summary' => { 'covered' => 10, 'total' => 12, 'pct' => 83.33 }
21
+ }
22
+ )
23
+
24
+ response = described_class.call(path: 'lib/foo.rb', server_context: server_context)
25
+ item = response.payload.first
26
+ expect(item['type']).to eq('resource')
27
+ expect(item['resource']).to include('mimeType' => 'application/json', 'name' => 'uncovered_lines.json')
28
+ data = JSON.parse(item['resource']['text'])
29
+ expect(data).to include('file', 'uncovered', 'summary')
30
+ expect(data['uncovered']).to eq([5, 9, 12])
31
+ end
32
+ end
33
+
data/spec/version_spec.rb CHANGED
@@ -5,6 +5,11 @@ require 'spec_helper'
5
5
  RSpec.describe 'Version constant' do
6
6
  it 'exposes a semver-like version string' do
7
7
  expect(SimpleCovMcp::VERSION).to be_a(String)
8
- expect(SimpleCovMcp::VERSION).to match(/\A\d+\.\d+\.\d+(?:[.-][0-9A-Za-z]+)?\z/)
8
+ # Named fragments for readability (simplified SemVer)
9
+ CORE = /\d+\.\d+\.\d+/
10
+ ID = /[[:alnum:].-]+/ # ASCII alnum plus dot/hyphen
11
+ SEMVER = /\A#{CORE.source}(?:-#{ID.source})?(?:\+#{ID.source})?\z/
12
+
13
+ expect(SimpleCovMcp::VERSION).to match(SEMVER)
9
14
  end
10
15
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+ require 'simple_cov_mcp/tools/version_tool'
5
+
6
+ RSpec.describe SimpleCovMcp::Tools::VersionTool do
7
+ let(:server_context) { instance_double('ServerContext').as_null_object }
8
+
9
+ before do
10
+ stub_const('MCP::Tool::Response', Struct.new(:payload))
11
+ end
12
+
13
+ it 'returns a text payload with the version string' do
14
+ response = described_class.call(server_context: server_context)
15
+ item = response.payload.first
16
+ expect(item[:type] || item['type']).to eq('text')
17
+ text = item[:text] || item['text']
18
+ expect(text).to include('SimpleCovMcp version:')
19
+ expect(text).to include(SimpleCovMcp::VERSION)
20
+ end
21
+ end
22
+
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.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Keith R. Bennett
@@ -110,6 +110,9 @@ files:
110
110
  - spec/cli_spec.rb
111
111
  - spec/cli_table_spec.rb
112
112
  - spec/cli_usage_spec.rb
113
+ - spec/coverage_detailed_tool_spec.rb
114
+ - spec/coverage_raw_tool_spec.rb
115
+ - spec/coverage_summary_tool_spec.rb
113
116
  - spec/coverage_table_tool_spec.rb
114
117
  - spec/error_handler_spec.rb
115
118
  - spec/errors_stale_spec.rb
@@ -122,8 +125,10 @@ files:
122
125
  - spec/simplecov_mcp_model_spec.rb
123
126
  - spec/spec_helper.rb
124
127
  - spec/staleness_more_spec.rb
128
+ - spec/uncovered_lines_tool_spec.rb
125
129
  - spec/util_spec.rb
126
130
  - spec/version_spec.rb
131
+ - spec/version_tool_spec.rb
127
132
  homepage: https://github.com/keithrbennett/simplecov-mcp
128
133
  licenses:
129
134
  - MIT