simplecov-mcp 1.0.1 → 2.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.
Files changed (164) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +98 -50
  3. data/docs/{ARCHITECTURE.md → dev/ARCHITECTURE.md} +11 -10
  4. data/docs/dev/BRANCH_ONLY_COVERAGE.md +158 -0
  5. data/docs/{DEVELOPMENT.md → dev/DEVELOPMENT.md} +2 -1
  6. data/docs/dev/README.md +10 -0
  7. data/docs/dev/RELEASING.md +146 -0
  8. data/docs/{arch-decisions → dev/arch-decisions}/001-x-arch-decision.md +3 -1
  9. data/docs/{arch-decisions → dev/arch-decisions}/002-x-arch-decision.md +7 -5
  10. data/docs/{arch-decisions → dev/arch-decisions}/003-x-arch-decision.md +2 -0
  11. data/docs/{arch-decisions → dev/arch-decisions}/004-x-arch-decision.md +6 -2
  12. data/docs/{arch-decisions → dev/arch-decisions}/005-x-arch-decision.md +4 -2
  13. data/docs/{arch-decisions → dev/arch-decisions}/README.md +3 -3
  14. data/docs/{presentations → dev/presentations}/simplecov-mcp-presentation.md +28 -22
  15. data/docs/fixtures/demo_project/README.md +9 -0
  16. data/docs/{ADVANCED_USAGE.md → user/ADVANCED_USAGE.md} +129 -319
  17. data/docs/user/CLI_FALLBACK_FOR_LLMS.md +34 -0
  18. data/docs/user/CLI_USAGE.md +750 -0
  19. data/docs/{ERROR_HANDLING.md → user/ERROR_HANDLING.md} +12 -12
  20. data/docs/user/EXAMPLES.md +588 -0
  21. data/docs/user/INSTALLATION.md +130 -0
  22. data/docs/{LIBRARY_API.md → user/LIBRARY_API.md} +90 -32
  23. data/docs/{MCP_INTEGRATION.md → user/MCP_INTEGRATION.md} +36 -34
  24. data/docs/user/README.md +14 -0
  25. data/docs/{TROUBLESHOOTING.md → user/TROUBLESHOOTING.md} +21 -100
  26. data/docs/user/V2-BREAKING-CHANGES.md +472 -0
  27. data/exe/simplecov-mcp +1 -1
  28. data/lib/simplecov_mcp/{cli_config.rb → app_config.rb} +12 -12
  29. data/lib/simplecov_mcp/app_context.rb +1 -1
  30. data/lib/simplecov_mcp/base_tool.rb +66 -38
  31. data/lib/simplecov_mcp/cli.rb +67 -123
  32. data/lib/simplecov_mcp/commands/base_command.rb +16 -27
  33. data/lib/simplecov_mcp/commands/command_factory.rb +8 -2
  34. data/lib/simplecov_mcp/commands/detailed_command.rb +16 -2
  35. data/lib/simplecov_mcp/commands/list_command.rb +1 -1
  36. data/lib/simplecov_mcp/commands/raw_command.rb +18 -2
  37. data/lib/simplecov_mcp/commands/summary_command.rb +20 -3
  38. data/lib/simplecov_mcp/commands/totals_command.rb +53 -0
  39. data/lib/simplecov_mcp/commands/uncovered_command.rb +24 -5
  40. data/lib/simplecov_mcp/commands/validate_command.rb +60 -0
  41. data/lib/simplecov_mcp/commands/version_command.rb +19 -4
  42. data/lib/simplecov_mcp/config_parser.rb +32 -0
  43. data/lib/simplecov_mcp/constants.rb +3 -3
  44. data/lib/simplecov_mcp/coverage_reporter.rb +31 -0
  45. data/lib/simplecov_mcp/error_handler.rb +81 -40
  46. data/lib/simplecov_mcp/error_handler_factory.rb +2 -2
  47. data/lib/simplecov_mcp/errors.rb +12 -19
  48. data/lib/simplecov_mcp/formatters/source_formatter.rb +23 -19
  49. data/lib/simplecov_mcp/formatters.rb +51 -0
  50. data/lib/simplecov_mcp/mcp_server.rb +9 -7
  51. data/lib/simplecov_mcp/mode_detector.rb +6 -5
  52. data/lib/simplecov_mcp/model.rb +122 -88
  53. data/lib/simplecov_mcp/option_normalizers.rb +39 -18
  54. data/lib/simplecov_mcp/option_parser_builder.rb +82 -65
  55. data/lib/simplecov_mcp/option_parsers/env_options_parser.rb +3 -5
  56. data/lib/simplecov_mcp/option_parsers/error_helper.rb +18 -17
  57. data/lib/simplecov_mcp/path_relativizer.rb +17 -14
  58. data/lib/simplecov_mcp/predicate_evaluator.rb +72 -0
  59. data/lib/simplecov_mcp/presenters/base_coverage_presenter.rb +1 -3
  60. data/lib/simplecov_mcp/presenters/coverage_detailed_presenter.rb +1 -3
  61. data/lib/simplecov_mcp/presenters/coverage_raw_presenter.rb +1 -3
  62. data/lib/simplecov_mcp/presenters/coverage_summary_presenter.rb +1 -3
  63. data/lib/simplecov_mcp/presenters/coverage_uncovered_presenter.rb +1 -3
  64. data/lib/simplecov_mcp/presenters/project_coverage_presenter.rb +1 -3
  65. data/lib/simplecov_mcp/presenters/project_totals_presenter.rb +27 -0
  66. data/lib/simplecov_mcp/resolvers/coverage_line_resolver.rb +14 -18
  67. data/lib/simplecov_mcp/resolvers/resultset_path_resolver.rb +7 -9
  68. data/lib/simplecov_mcp/resultset_loader.rb +20 -25
  69. data/lib/simplecov_mcp/staleness_checker.rb +50 -46
  70. data/lib/simplecov_mcp/table_formatter.rb +64 -0
  71. data/lib/simplecov_mcp/tools/all_files_coverage_tool.rb +20 -50
  72. data/lib/simplecov_mcp/tools/coverage_detailed_tool.rb +13 -7
  73. data/lib/simplecov_mcp/tools/coverage_raw_tool.rb +12 -7
  74. data/lib/simplecov_mcp/tools/coverage_summary_tool.rb +13 -8
  75. data/lib/simplecov_mcp/tools/coverage_table_tool.rb +20 -60
  76. data/lib/simplecov_mcp/tools/coverage_totals_tool.rb +44 -0
  77. data/lib/simplecov_mcp/tools/help_tool.rb +38 -66
  78. data/lib/simplecov_mcp/tools/uncovered_lines_tool.rb +13 -8
  79. data/lib/simplecov_mcp/tools/validate_tool.rb +72 -0
  80. data/lib/simplecov_mcp/tools/version_tool.rb +7 -14
  81. data/lib/simplecov_mcp/util.rb +18 -12
  82. data/lib/simplecov_mcp/version.rb +1 -1
  83. data/lib/simplecov_mcp.rb +23 -29
  84. data/spec/all_files_coverage_tool_spec.rb +4 -3
  85. data/spec/{cli_config_spec.rb → app_config_spec.rb} +31 -26
  86. data/spec/base_tool_spec.rb +17 -14
  87. data/spec/cli/show_default_report_spec.rb +2 -2
  88. data/spec/cli_enumerated_options_spec.rb +31 -9
  89. data/spec/cli_error_spec.rb +46 -23
  90. data/spec/cli_format_spec.rb +123 -0
  91. data/spec/cli_json_options_spec.rb +50 -0
  92. data/spec/cli_source_spec.rb +11 -63
  93. data/spec/cli_spec.rb +82 -97
  94. data/spec/cli_usage_spec.rb +15 -15
  95. data/spec/commands/base_command_spec.rb +12 -92
  96. data/spec/commands/command_factory_spec.rb +7 -3
  97. data/spec/commands/detailed_command_spec.rb +10 -24
  98. data/spec/commands/list_command_spec.rb +28 -0
  99. data/spec/commands/raw_command_spec.rb +43 -20
  100. data/spec/commands/summary_command_spec.rb +10 -23
  101. data/spec/commands/totals_command_spec.rb +34 -0
  102. data/spec/commands/uncovered_command_spec.rb +29 -23
  103. data/spec/commands/validate_command_spec.rb +213 -0
  104. data/spec/commands/version_command_spec.rb +38 -0
  105. data/spec/constants_spec.rb +3 -3
  106. data/spec/coverage_reporter_spec.rb +102 -0
  107. data/spec/coverage_table_tool_spec.rb +21 -10
  108. data/spec/coverage_totals_tool_spec.rb +37 -0
  109. data/spec/error_handler_spec.rb +120 -4
  110. data/spec/error_mode_spec.rb +18 -22
  111. data/spec/errors_edge_cases_spec.rb +101 -28
  112. data/spec/errors_stale_spec.rb +34 -0
  113. data/spec/file_based_mcp_tools_spec.rb +6 -6
  114. data/spec/fixtures/project1/lib/bar.rb +2 -0
  115. data/spec/fixtures/project1/lib/foo.rb +2 -0
  116. data/spec/help_tool_spec.rb +2 -18
  117. data/spec/integration_spec.rb +103 -161
  118. data/spec/logging_fallback_spec.rb +3 -3
  119. data/spec/mcp_server_integration_spec.rb +1 -1
  120. data/spec/mcp_server_spec.rb +70 -53
  121. data/spec/mode_detector_spec.rb +46 -41
  122. data/spec/model_error_handling_spec.rb +139 -78
  123. data/spec/model_staleness_spec.rb +13 -13
  124. data/spec/option_normalizers_spec.rb +111 -112
  125. data/spec/option_parsers/env_options_parser_spec.rb +25 -37
  126. data/spec/option_parsers/error_helper_spec.rb +56 -56
  127. data/spec/path_relativizer_spec.rb +15 -0
  128. data/spec/presenters/coverage_detailed_presenter_spec.rb +1 -1
  129. data/spec/presenters/coverage_summary_presenter_spec.rb +1 -1
  130. data/spec/presenters/coverage_uncovered_presenter_spec.rb +1 -1
  131. data/spec/presenters/project_coverage_presenter_spec.rb +9 -8
  132. data/spec/presenters/project_totals_presenter_spec.rb +144 -0
  133. data/spec/resolvers/coverage_line_resolver_spec.rb +261 -36
  134. data/spec/resolvers/resultset_path_resolver_spec.rb +13 -8
  135. data/spec/shared_examples/file_based_mcp_tools.rb +23 -18
  136. data/spec/shared_examples/formatted_command_examples.rb +64 -0
  137. data/spec/simple_cov_mcp_module_spec.rb +24 -3
  138. data/spec/simplecov_mcp/formatters/source_formatter_spec.rb +267 -0
  139. data/spec/simplecov_mcp/formatters_spec.rb +76 -0
  140. data/spec/simplecov_mcp/presenters/base_coverage_presenter_spec.rb +79 -0
  141. data/spec/simplecov_mcp_model_spec.rb +97 -47
  142. data/spec/simplecov_mcp_opts_spec.rb +42 -39
  143. data/spec/spec_helper.rb +27 -92
  144. data/spec/staleness_checker_spec.rb +10 -9
  145. data/spec/staleness_more_spec.rb +4 -4
  146. data/spec/support/cli_helpers.rb +22 -0
  147. data/spec/support/control_flow_helpers.rb +20 -0
  148. data/spec/support/fake_mcp.rb +40 -0
  149. data/spec/support/io_helpers.rb +29 -0
  150. data/spec/support/mcp_helpers.rb +35 -0
  151. data/spec/support/mcp_runner.rb +10 -8
  152. data/spec/support/mocking_helpers.rb +30 -0
  153. data/spec/table_format_spec.rb +70 -0
  154. data/spec/tools/validate_tool_spec.rb +132 -0
  155. data/spec/tools_error_handling_spec.rb +34 -48
  156. data/spec/util_spec.rb +5 -4
  157. data/spec/version_spec.rb +7 -7
  158. data/spec/version_tool_spec.rb +20 -22
  159. metadata +90 -23
  160. data/docs/BRANCH_ONLY_COVERAGE.md +0 -81
  161. data/docs/CLI_USAGE.md +0 -637
  162. data/docs/EXAMPLES.md +0 -430
  163. data/docs/INSTALLATION.md +0 -352
  164. data/spec/cli_success_predicate_spec.rb +0 -141
@@ -10,18 +10,23 @@ module SimpleCovMcp
10
10
  description <<~DESC
11
11
  Use this when the user asks for the covered/total line counts and percentage for a specific file.
12
12
  Do not use this for multi-file reports; coverage.all_files or coverage.table handle those.
13
- Inputs: file path (required) plus optional root/resultset/stale mode inherited from BaseTool.
14
- Output: JSON object {"file": String, "summary": {"covered": Integer, "total": Integer, "pct": Float}, "stale": String|False}.
13
+ Inputs: file path (required) plus optional root/resultset/staleness mode inherited from BaseTool.
14
+ Output: JSON object {"file": String, "summary": {"covered": Integer, "total": Integer, "percentage": Float}, "stale": String|False}.
15
15
  Examples: "What is the coverage for lib/simple_cov_mcp/tools/all_files_coverage_tool.rb?".
16
16
  DESC
17
17
  input_schema(**input_schema_def)
18
18
  class << self
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
- rescue => e
24
- handle_mcp_error(e, 'CoverageSummaryTool', error_mode: error_mode)
19
+ def call(path:, root: '.', resultset: nil, staleness: :off, error_mode: 'log',
20
+ server_context:)
21
+ with_error_handling('CoverageSummaryTool', error_mode: error_mode) do
22
+ model = CoverageModel.new(
23
+ root: root,
24
+ resultset: resultset,
25
+ staleness: staleness.to_sym
26
+ )
27
+ presenter = Presenters::CoverageSummaryPresenter.new(model: model, path: path)
28
+ respond_json(presenter.relativized_payload, name: 'coverage_summary.json', pretty: true)
29
+ end
25
30
  end
26
31
  end
27
32
  end
@@ -14,75 +14,35 @@ module SimpleCovMcp
14
14
  Output: text block containing the formatted coverage table with headers and percentages.
15
15
  Example: "Show me the CLI coverage table sorted descending".
16
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
- },
17
+ input_schema(**coverage_schema(
18
+ additional_properties: {
30
19
  sort_order: {
31
20
  type: 'string',
32
21
  description: 'Sort order for the printed coverage table (ascending or descending).',
33
22
  default: 'ascending',
34
23
  enum: ['ascending', 'descending']
35
24
  },
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
- }
25
+ tracked_globs: TRACKED_GLOBS_PROPERTY
56
26
  }
57
- )
58
-
27
+ ))
59
28
  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)
29
+ def call(root: '.', resultset: nil, sort_order: 'ascending', staleness: :off,
30
+ tracked_globs: nil, error_mode: 'log', server_context:)
31
+ with_error_handling('CoverageTableTool', error_mode: error_mode) do
32
+ # Convert string inputs from MCP to symbols for internal use
33
+ sort_order_sym = sort_order.to_sym
34
+ staleness_sym = staleness.to_sym
67
35
 
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)
36
+ model = CoverageModel.new(root: root, resultset: resultset, staleness: staleness_sym,
37
+ tracked_globs: tracked_globs)
38
+ table = model.format_table(
39
+ sort_order: sort_order_sym,
40
+ check_stale: (staleness_sym == :error),
41
+ tracked_globs: tracked_globs
42
+ )
43
+ # Return text response
44
+ ::MCP::Tool::Response.new([{ 'type' => 'text', 'text' => table }])
45
+ end
86
46
  end
87
47
  end
88
48
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../model'
4
+ require_relative '../base_tool'
5
+ require_relative '../presenters/project_totals_presenter'
6
+
7
+ module SimpleCovMcp
8
+ module Tools
9
+ class CoverageTotalsTool < BaseTool
10
+ description <<~DESC
11
+ Use this when you want aggregated coverage counts for the entire project.
12
+ It reports covered/total lines, uncovered line counts, and the overall average percentage.
13
+ Inputs: optional project root, alternate .resultset path, staleness mode, tracked_globs, and error mode.
14
+ Output: JSON {"lines":{"total","covered","uncovered"},"percentage":Float,"files":{"total","ok","stale"}}.
15
+ Example: "Give me total/covered/uncovered line counts and the overall coverage percent."
16
+ DESC
17
+
18
+ input_schema(**coverage_schema(
19
+ additional_properties: {
20
+ tracked_globs: TRACKED_GLOBS_PROPERTY
21
+ }
22
+ ))
23
+
24
+ class << self
25
+ def call(root: '.', resultset: nil, staleness: :off, tracked_globs: nil,
26
+ error_mode: 'log', server_context:)
27
+ with_error_handling('CoverageTotalsTool', error_mode: error_mode) do
28
+ # Convert string inputs from MCP to symbols for internal use
29
+ staleness_sym = staleness.to_sym
30
+
31
+ model = CoverageModel.new(root: root, resultset: resultset, staleness: staleness_sym,
32
+ tracked_globs: tracked_globs)
33
+ presenter = Presenters::ProjectTotalsPresenter.new(
34
+ model: model,
35
+ check_stale: (staleness_sym == :error),
36
+ tracked_globs: tracked_globs
37
+ )
38
+ respond_json(presenter.relativized_payload, name: 'coverage_totals.json', pretty: true)
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -6,30 +6,14 @@ module SimpleCovMcp
6
6
  module Tools
7
7
  class HelpTool < BaseTool
8
8
  description <<~DESC
9
- Use this when you are unsure which simplecov-mcp tool fits the user’s coverage request.
10
- Do not use this once you know the correct tool; call that tool directly.
11
- Inputs: optional query string to filter the list of tools.
12
- Output: JSON {"tools": [...]} with per-tool "use_when", "avoid_when", "inputs",#{' '}
13
- and "example" guidance.
14
- Example: "Which tool shows uncovered lines?".
9
+ Returns help containing descriptions of all tools, including: use_when, avoid_when, inputs.
15
10
  DESC
16
11
 
17
12
  input_schema(
18
13
  type: 'object',
19
14
  additionalProperties: false,
20
15
  properties: {
21
- query: {
22
- type: 'string',
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'
32
- }
16
+ error_mode: ERROR_MODE_PROPERTY
33
17
  }
34
18
  )
35
19
 
@@ -39,104 +23,92 @@ module SimpleCovMcp
39
23
  label: 'Single-file coverage summary',
40
24
  use_when: 'User wants covered/total line counts or percentage for one file.',
41
25
  avoid_when: 'User needs repo-wide stats or specific uncovered lines.',
42
- inputs: ['path (required)', 'root/resultset/stale (optional)'],
43
- example: 'What is the coverage for lib/simple_cov_mcp/model.rb?'
26
+ inputs: ['path (required)', 'root/resultset/staleness (optional)']
44
27
  },
45
28
  {
46
29
  tool: UncoveredLinesTool,
47
30
  label: 'Uncovered line numbers',
48
31
  use_when: 'User asks which lines in a file still lack tests.',
49
32
  avoid_when: 'User only wants overall percentages or detailed per-line hit data.',
50
- inputs: ['path (required)', 'root/resultset/stale (optional)'],
51
- example: 'List uncovered lines for lib/simple_cov_mcp/tools/coverage_summary_tool.rb.'
33
+ inputs: ['path (required)', 'root/resultset/staleness (optional)']
52
34
  },
53
35
  {
54
36
  tool: CoverageDetailedTool,
55
37
  label: 'Per-line coverage details',
56
38
  use_when: 'User needs per-line hit counts for a file.',
57
39
  avoid_when: 'User only wants totals or uncovered line numbers.',
58
- inputs: ['path (required)', 'root/resultset/stale (optional)'],
59
- example: 'Show detailed coverage for lib/simple_cov_mcp/util.rb.'
40
+ inputs: ['path (required)', 'root/resultset/staleness (optional)']
60
41
  },
61
42
  {
62
43
  tool: CoverageRawTool,
63
44
  label: 'Raw SimpleCov lines array',
64
45
  use_when: 'User needs the raw SimpleCov `lines` array for a file.',
65
46
  avoid_when: 'User expects human-friendly summaries or explanations.',
66
- inputs: ['path (required)', 'root/resultset/stale (optional)'],
67
- example: 'Fetch the raw coverage array for spec/support/helpers.rb.'
47
+ inputs: ['path (required)', 'root/resultset/staleness (optional)']
68
48
  },
69
49
  {
70
50
  tool: AllFilesCoverageTool,
71
51
  label: 'Repo-wide file coverage',
72
52
  use_when: 'User wants coverage percentages for every tracked file.',
73
53
  avoid_when: 'User asks about a single file.',
74
- inputs: ['root/resultset (optional)', 'sort_order', 'stale', 'tracked_globs'],
75
- example: 'List files with the lowest coverage.'
54
+ inputs: ['root/resultset (optional)', 'sort_order', 'staleness', 'tracked_globs']
55
+ },
56
+ {
57
+ tool: CoverageTotalsTool,
58
+ label: 'Project coverage totals',
59
+ use_when: 'User wants total/covered/uncovered line counts or the average percent.',
60
+ avoid_when: 'User needs per-file breakdowns.',
61
+ inputs: ['root/resultset (optional)', 'staleness', 'tracked_globs']
76
62
  },
77
63
  {
78
64
  tool: CoverageTableTool,
79
65
  label: 'Formatted coverage table',
80
66
  use_when: 'User wants the plain-text table produced by the CLI.',
81
67
  avoid_when: 'User needs JSON data for automation.',
82
- inputs: ['root/resultset (optional)', 'sort_order', 'stale'],
83
- example: 'Show me the coverage table sorted descending.'
68
+ inputs: ['root/resultset (optional)', 'sort_order', 'staleness']
69
+ },
70
+ {
71
+ tool: ValidateTool,
72
+ label: 'Validate coverage policy',
73
+ use_when: 'User needs to enforce coverage rules (e.g., minimum percentage) in CI.',
74
+ avoid_when: 'User just wants to view coverage data.',
75
+ inputs: ['path (required)', 'root/resultset (optional)']
76
+ },
77
+ {
78
+ tool: ValidateTool,
79
+ label: 'Validate coverage policy',
80
+ use_when: 'User needs to enforce coverage rules (e.g., minimum percentage) in CI.',
81
+ avoid_when: 'User just wants to view coverage data.',
82
+ inputs: ['path (required)', 'root/resultset (optional)']
84
83
  },
85
84
  {
86
85
  tool: VersionTool,
87
86
  label: 'simplecov-mcp version',
88
87
  use_when: 'User needs to confirm the running gem version.',
89
88
  avoid_when: 'User is asking for coverage information.',
90
- inputs: ['(no arguments)'],
91
- example: 'What version of simplecov-mcp is installed?'
89
+ inputs: ['(no arguments)']
92
90
  }
93
91
  ].freeze
94
92
 
95
93
  class << self
96
- def call(query: nil, error_mode: 'on', server_context:, **_unused)
97
- entries = TOOL_GUIDE.map { |guide| format_entry(guide) }
98
- entries = filter_entries(entries, query) if query && !query.strip.empty?
94
+ def call(error_mode: 'log', server_context:, **_unused)
95
+ with_error_handling('HelpTool', error_mode: error_mode) do
96
+ entries = TOOL_GUIDE.map { |guide| format_entry(guide) }
99
97
 
100
- data = { query: query, tools: entries }
101
- respond_json(data, name: 'tools_help.json')
102
- rescue => e
103
- handle_mcp_error(e, 'HelpTool', error_mode: error_mode)
98
+ data = { tools: entries }
99
+ respond_json(data, name: 'tools_help.json')
100
+ end
104
101
  end
105
102
 
106
- private
107
-
108
- def format_entry(guide)
103
+ private def format_entry(guide)
109
104
  {
110
105
  'tool' => guide[:tool].tool_name,
111
106
  'label' => guide[:label],
112
107
  'use_when' => guide[:use_when],
113
108
  'avoid_when' => guide[:avoid_when],
114
- 'inputs' => guide[:inputs],
115
- 'example' => guide[:example]
109
+ 'inputs' => guide[:inputs]
116
110
  }
117
111
  end
118
-
119
- def filter_entries(entries, query)
120
- tokens = query.downcase.scan(/\w+/)
121
- return entries if tokens.empty?
122
-
123
- entries.select do |entry|
124
- tokens.all? do |token|
125
- entry.any? { |_, value| value_matches?(value, token) }
126
- end
127
- end
128
- end
129
-
130
- def value_matches?(value, token)
131
- case value
132
- when String
133
- value.downcase.include?(token)
134
- when Array
135
- value.any? { |element| element.downcase.include?(token) }
136
- else
137
- false
138
- end
139
- end
140
112
  end
141
113
  end
142
114
  end
@@ -10,18 +10,23 @@ module SimpleCovMcp
10
10
  description <<~DESC
11
11
  Use this when the user wants to know which lines in a file still lack coverage.
12
12
  Do not use this for overall percentages; coverage.summary is faster when counts are enough.
13
- Inputs: file path (required) plus optional root/resultset/stale mode inherited from BaseTool.
14
- Output: JSON object with keys "file", "uncovered" (array of integers), "summary" {"covered","total","pct"}, and "stale" status.
13
+ Inputs: file path (required) plus optional root/resultset/staleness mode inherited from BaseTool.
14
+ Output: JSON object with keys "file", "uncovered" (array of integers), "summary" {"covered","total","percentage"}, and "stale" status.
15
15
  Example: "List uncovered lines for lib/simple_cov_mcp/tools/coverage_summary_tool.rb".
16
16
  DESC
17
17
  input_schema(**input_schema_def)
18
18
  class << self
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
- rescue => e
24
- handle_mcp_error(e, 'UncoveredLinesTool', error_mode: error_mode)
19
+ def call(path:, root: '.', resultset: nil, staleness: :off, error_mode: 'log',
20
+ server_context:)
21
+ with_error_handling('UncoveredLinesTool', error_mode: error_mode) do
22
+ model = CoverageModel.new(
23
+ root: root,
24
+ resultset: resultset,
25
+ staleness: staleness.to_sym
26
+ )
27
+ presenter = Presenters::CoverageUncoveredPresenter.new(model: model, path: path)
28
+ respond_json(presenter.relativized_payload, name: 'uncovered_lines.json', pretty: true)
29
+ end
25
30
  end
26
31
  end
27
32
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../base_tool'
4
+ require_relative '../model'
5
+ require_relative '../predicate_evaluator'
6
+
7
+ module SimpleCovMcp
8
+ module Tools
9
+ class ValidateTool < BaseTool
10
+ description <<~DESC
11
+ Validates coverage data against a predicate (Ruby code that evaluates to true/false).
12
+ Use this to enforce coverage policies programmatically.
13
+ Inputs: Either 'code' (Ruby string) OR 'file' (path to Ruby file), plus optional root/resultset/staleness/error_mode.
14
+ Output: JSON object {"result": Boolean} where true means policy passed, false means failed.
15
+ On error (syntax error, file not found, etc.), returns an MCP error response.
16
+ Security Warning: Predicates execute as arbitrary Ruby code with full system privileges.
17
+ Examples:
18
+ - "Check if all files have at least 80% coverage" → {"code": "->(m) { m.all_files.all? { |f| f['percentage'] >= 80 } }"}
19
+ - "Run coverage policy from file" → {"file": "coverage_policy.rb"}
20
+ DESC
21
+
22
+ input_schema(**coverage_schema(
23
+ additional_properties: {
24
+ code: {
25
+ type: 'string',
26
+ description: 'Ruby code string that returns a callable predicate. ' \
27
+ 'Must evaluate to a lambda, proc, or object with #call method.'
28
+ },
29
+ file: {
30
+ type: 'string',
31
+ description:
32
+ 'Path to Ruby file containing predicate code (absolute or relative to root).'
33
+ }
34
+ }
35
+ ))
36
+ class << self
37
+ def call(code: nil, file: nil, root: '.', resultset: nil, staleness: :off,
38
+ error_mode: 'log', server_context:)
39
+ with_error_handling('ValidateTool', error_mode: error_mode) do
40
+ # Re-use logic from ValidateCommand, but adapt for MCP return format
41
+ require_relative '../cli'
42
+
43
+ # Create a minimal CLI shim to reuse command logic
44
+ cli = CoverageCLI.new
45
+ cli.config.root = root
46
+ cli.config.resultset = resultset
47
+ cli.config.staleness = staleness.to_sym
48
+ cli.config.error_mode = error_mode.to_sym
49
+
50
+ # We need to capture the boolean result instead of letting it exit
51
+ # Commands::ValidateCommand is designed to exit, so we'll use the model and evaluator directly
52
+ # This duplicates some logic from ValidateCommand#execute but avoids the exit(status) call
53
+
54
+ model = CoverageModel.new(**cli.config.model_options)
55
+
56
+ result = if code
57
+ PredicateEvaluator.evaluate_code(code, model)
58
+ elsif file
59
+ # Resolve file path relative to root if needed
60
+ predicate_path = File.expand_path(file, root)
61
+ PredicateEvaluator.evaluate_file(predicate_path, model)
62
+ else
63
+ raise UsageError, "Either 'code' or 'file' must be provided"
64
+ end
65
+
66
+ respond_json({ result: result }, name: 'validate_result.json', pretty: true)
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -15,23 +15,16 @@ module SimpleCovMcp
15
15
  type: 'object',
16
16
  additionalProperties: false,
17
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
- }
18
+ error_mode: ERROR_MODE_PROPERTY
25
19
  }
26
20
  )
27
-
28
21
  class << self
29
- def call(error_mode: 'on', server_context: nil, **_args)
30
- ::MCP::Tool::Response.new([
31
- { type: 'text', text: "SimpleCovMcp version: #{SimpleCovMcp::VERSION}" }
32
- ])
33
- rescue => error
34
- handle_mcp_error(error, 'version_tool', error_mode: error_mode)
22
+ def call(error_mode: 'log', server_context: nil, **_args)
23
+ with_error_handling('VersionTool', error_mode: error_mode) do
24
+ ::MCP::Tool::Response.new([
25
+ { 'type' => 'text', 'text' => "SimpleCovMcp version: #{SimpleCovMcp::VERSION}" }
26
+ ])
27
+ end
35
28
  end
36
29
  end
37
30
  end
@@ -12,9 +12,7 @@ module SimpleCovMcp
12
12
  DEFAULT_LOG_FILESPEC = './simplecov_mcp.log'
13
13
 
14
14
  module CovUtil
15
- module_function
16
-
17
- def log(msg)
15
+ module_function def log(msg)
18
16
  log_file = SimpleCovMcp.active_log_file
19
17
 
20
18
  case log_file
@@ -27,39 +25,47 @@ module SimpleCovMcp
27
25
  path_to_log = log_file || DEFAULT_LOG_FILESPEC
28
26
  File.open(File.expand_path(path_to_log), 'a') { |f| f.puts "[#{Time.now.iso8601}] #{msg}" }
29
27
  end
30
- rescue StandardError => e
28
+ rescue => e
31
29
  # Fallback to stderr if file logging fails, but suppress in MCP mode
32
30
  # to avoid interfering with JSON-RPC protocol
33
31
  unless SimpleCovMcp.context.mcp_mode?
34
32
  begin
35
33
  $stderr.puts "[#{Time.now.iso8601}] LOGGING ERROR: #{e.message}"
36
34
  $stderr.puts "[#{Time.now.iso8601}] #{msg}"
37
- rescue StandardError
35
+ rescue
38
36
  # Silently ignore only stderr fallback failures
39
37
  end
40
38
  end
41
39
  end
42
40
 
43
- def find_resultset(root, resultset: nil)
41
+ # Safe logging that never raises - use when logging should not interrupt execution.
42
+ # Unlike `log`, this method guarantees it will never propagate exceptions.
43
+ module_function def safe_log(msg)
44
+ log(msg)
45
+ rescue
46
+ # Silently ignore all logging failures
47
+ end
48
+
49
+ module_function def find_resultset(root, resultset: nil)
44
50
  Resolvers::ResolverFactory.find_resultset(root, resultset: resultset)
45
51
  end
46
52
 
47
- def lookup_lines(cov, file_abs)
53
+ module_function def lookup_lines(cov, file_abs)
48
54
  Resolvers::ResolverFactory.lookup_lines(cov, file_abs)
49
55
  end
50
56
 
51
- def summary(arr)
57
+ module_function def summary(arr)
52
58
  total = 0
53
59
  covered = 0
54
60
  arr.compact.each do |hits|
55
61
  total += 1
56
62
  covered += 1 if hits.to_i > 0
57
63
  end
58
- pct = total.zero? ? 100.0 : ((covered.to_f * 100.0 / total) * 100).round / 100.0
59
- { 'covered' => covered, 'total' => total, 'pct' => pct }
64
+ percentage = total.zero? ? 100.0 : ((covered.to_f * 100.0 / total) * 100).round / 100.0
65
+ { 'covered' => covered, 'total' => total, 'percentage' => percentage }
60
66
  end
61
67
 
62
- def uncovered(arr)
68
+ module_function def uncovered(arr)
63
69
  out = []
64
70
 
65
71
  arr.each_with_index do |hits, i|
@@ -70,7 +76,7 @@ module SimpleCovMcp
70
76
  out
71
77
  end
72
78
 
73
- def detailed(arr)
79
+ module_function def detailed(arr)
74
80
  rows = []
75
81
  arr.each_with_index do |hits, i|
76
82
  h = hits&.to_i
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SimpleCovMcp
4
- VERSION = '1.0.1'
4
+ VERSION = '2.0.0'
5
5
  end