woods 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (185) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +89 -0
  3. data/CODE_OF_CONDUCT.md +83 -0
  4. data/CONTRIBUTING.md +65 -0
  5. data/LICENSE.txt +21 -0
  6. data/README.md +406 -0
  7. data/exe/woods-console +59 -0
  8. data/exe/woods-console-mcp +22 -0
  9. data/exe/woods-mcp +34 -0
  10. data/exe/woods-mcp-http +37 -0
  11. data/exe/woods-mcp-start +58 -0
  12. data/lib/generators/woods/install_generator.rb +32 -0
  13. data/lib/generators/woods/pgvector_generator.rb +37 -0
  14. data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +15 -0
  15. data/lib/generators/woods/templates/create_woods_tables.rb.erb +43 -0
  16. data/lib/tasks/woods.rake +621 -0
  17. data/lib/tasks/woods_evaluation.rake +115 -0
  18. data/lib/woods/ast/call_site_extractor.rb +106 -0
  19. data/lib/woods/ast/method_extractor.rb +71 -0
  20. data/lib/woods/ast/node.rb +116 -0
  21. data/lib/woods/ast/parser.rb +614 -0
  22. data/lib/woods/ast.rb +6 -0
  23. data/lib/woods/builder.rb +200 -0
  24. data/lib/woods/cache/cache_middleware.rb +199 -0
  25. data/lib/woods/cache/cache_store.rb +264 -0
  26. data/lib/woods/cache/redis_cache_store.rb +116 -0
  27. data/lib/woods/cache/solid_cache_store.rb +111 -0
  28. data/lib/woods/chunking/chunk.rb +84 -0
  29. data/lib/woods/chunking/semantic_chunker.rb +295 -0
  30. data/lib/woods/console/adapters/cache_adapter.rb +58 -0
  31. data/lib/woods/console/adapters/good_job_adapter.rb +33 -0
  32. data/lib/woods/console/adapters/job_adapter.rb +68 -0
  33. data/lib/woods/console/adapters/sidekiq_adapter.rb +33 -0
  34. data/lib/woods/console/adapters/solid_queue_adapter.rb +33 -0
  35. data/lib/woods/console/audit_logger.rb +75 -0
  36. data/lib/woods/console/bridge.rb +177 -0
  37. data/lib/woods/console/confirmation.rb +90 -0
  38. data/lib/woods/console/connection_manager.rb +173 -0
  39. data/lib/woods/console/console_response_renderer.rb +74 -0
  40. data/lib/woods/console/embedded_executor.rb +373 -0
  41. data/lib/woods/console/model_validator.rb +81 -0
  42. data/lib/woods/console/rack_middleware.rb +87 -0
  43. data/lib/woods/console/safe_context.rb +82 -0
  44. data/lib/woods/console/server.rb +612 -0
  45. data/lib/woods/console/sql_validator.rb +172 -0
  46. data/lib/woods/console/tools/tier1.rb +118 -0
  47. data/lib/woods/console/tools/tier2.rb +117 -0
  48. data/lib/woods/console/tools/tier3.rb +110 -0
  49. data/lib/woods/console/tools/tier4.rb +79 -0
  50. data/lib/woods/coordination/pipeline_lock.rb +109 -0
  51. data/lib/woods/cost_model/embedding_cost.rb +88 -0
  52. data/lib/woods/cost_model/estimator.rb +128 -0
  53. data/lib/woods/cost_model/provider_pricing.rb +67 -0
  54. data/lib/woods/cost_model/storage_cost.rb +52 -0
  55. data/lib/woods/cost_model.rb +22 -0
  56. data/lib/woods/db/migrations/001_create_units.rb +38 -0
  57. data/lib/woods/db/migrations/002_create_edges.rb +35 -0
  58. data/lib/woods/db/migrations/003_create_embeddings.rb +37 -0
  59. data/lib/woods/db/migrations/004_create_snapshots.rb +45 -0
  60. data/lib/woods/db/migrations/005_create_snapshot_units.rb +40 -0
  61. data/lib/woods/db/migrations/006_rename_tables.rb +34 -0
  62. data/lib/woods/db/migrator.rb +73 -0
  63. data/lib/woods/db/schema_version.rb +73 -0
  64. data/lib/woods/dependency_graph.rb +236 -0
  65. data/lib/woods/embedding/indexer.rb +140 -0
  66. data/lib/woods/embedding/openai.rb +126 -0
  67. data/lib/woods/embedding/provider.rb +162 -0
  68. data/lib/woods/embedding/text_preparer.rb +112 -0
  69. data/lib/woods/evaluation/baseline_runner.rb +115 -0
  70. data/lib/woods/evaluation/evaluator.rb +139 -0
  71. data/lib/woods/evaluation/metrics.rb +79 -0
  72. data/lib/woods/evaluation/query_set.rb +148 -0
  73. data/lib/woods/evaluation/report_generator.rb +90 -0
  74. data/lib/woods/extracted_unit.rb +145 -0
  75. data/lib/woods/extractor.rb +1028 -0
  76. data/lib/woods/extractors/action_cable_extractor.rb +201 -0
  77. data/lib/woods/extractors/ast_source_extraction.rb +46 -0
  78. data/lib/woods/extractors/behavioral_profile.rb +309 -0
  79. data/lib/woods/extractors/caching_extractor.rb +261 -0
  80. data/lib/woods/extractors/callback_analyzer.rb +246 -0
  81. data/lib/woods/extractors/concern_extractor.rb +292 -0
  82. data/lib/woods/extractors/configuration_extractor.rb +219 -0
  83. data/lib/woods/extractors/controller_extractor.rb +404 -0
  84. data/lib/woods/extractors/database_view_extractor.rb +278 -0
  85. data/lib/woods/extractors/decorator_extractor.rb +253 -0
  86. data/lib/woods/extractors/engine_extractor.rb +223 -0
  87. data/lib/woods/extractors/event_extractor.rb +211 -0
  88. data/lib/woods/extractors/factory_extractor.rb +289 -0
  89. data/lib/woods/extractors/graphql_extractor.rb +892 -0
  90. data/lib/woods/extractors/i18n_extractor.rb +117 -0
  91. data/lib/woods/extractors/job_extractor.rb +374 -0
  92. data/lib/woods/extractors/lib_extractor.rb +218 -0
  93. data/lib/woods/extractors/mailer_extractor.rb +269 -0
  94. data/lib/woods/extractors/manager_extractor.rb +188 -0
  95. data/lib/woods/extractors/middleware_extractor.rb +133 -0
  96. data/lib/woods/extractors/migration_extractor.rb +469 -0
  97. data/lib/woods/extractors/model_extractor.rb +988 -0
  98. data/lib/woods/extractors/phlex_extractor.rb +252 -0
  99. data/lib/woods/extractors/policy_extractor.rb +191 -0
  100. data/lib/woods/extractors/poro_extractor.rb +229 -0
  101. data/lib/woods/extractors/pundit_extractor.rb +223 -0
  102. data/lib/woods/extractors/rails_source_extractor.rb +473 -0
  103. data/lib/woods/extractors/rake_task_extractor.rb +343 -0
  104. data/lib/woods/extractors/route_extractor.rb +181 -0
  105. data/lib/woods/extractors/scheduled_job_extractor.rb +331 -0
  106. data/lib/woods/extractors/serializer_extractor.rb +339 -0
  107. data/lib/woods/extractors/service_extractor.rb +217 -0
  108. data/lib/woods/extractors/shared_dependency_scanner.rb +91 -0
  109. data/lib/woods/extractors/shared_utility_methods.rb +281 -0
  110. data/lib/woods/extractors/state_machine_extractor.rb +398 -0
  111. data/lib/woods/extractors/test_mapping_extractor.rb +225 -0
  112. data/lib/woods/extractors/validator_extractor.rb +211 -0
  113. data/lib/woods/extractors/view_component_extractor.rb +311 -0
  114. data/lib/woods/extractors/view_template_extractor.rb +261 -0
  115. data/lib/woods/feedback/gap_detector.rb +89 -0
  116. data/lib/woods/feedback/store.rb +119 -0
  117. data/lib/woods/filename_utils.rb +32 -0
  118. data/lib/woods/flow_analysis/operation_extractor.rb +206 -0
  119. data/lib/woods/flow_analysis/response_code_mapper.rb +154 -0
  120. data/lib/woods/flow_assembler.rb +290 -0
  121. data/lib/woods/flow_document.rb +191 -0
  122. data/lib/woods/flow_precomputer.rb +102 -0
  123. data/lib/woods/formatting/base.rb +30 -0
  124. data/lib/woods/formatting/claude_adapter.rb +98 -0
  125. data/lib/woods/formatting/generic_adapter.rb +56 -0
  126. data/lib/woods/formatting/gpt_adapter.rb +64 -0
  127. data/lib/woods/formatting/human_adapter.rb +78 -0
  128. data/lib/woods/graph_analyzer.rb +374 -0
  129. data/lib/woods/mcp/bootstrapper.rb +96 -0
  130. data/lib/woods/mcp/index_reader.rb +394 -0
  131. data/lib/woods/mcp/renderers/claude_renderer.rb +81 -0
  132. data/lib/woods/mcp/renderers/json_renderer.rb +17 -0
  133. data/lib/woods/mcp/renderers/markdown_renderer.rb +353 -0
  134. data/lib/woods/mcp/renderers/plain_renderer.rb +240 -0
  135. data/lib/woods/mcp/server.rb +962 -0
  136. data/lib/woods/mcp/tool_response_renderer.rb +85 -0
  137. data/lib/woods/model_name_cache.rb +51 -0
  138. data/lib/woods/notion/client.rb +217 -0
  139. data/lib/woods/notion/exporter.rb +219 -0
  140. data/lib/woods/notion/mapper.rb +40 -0
  141. data/lib/woods/notion/mappers/column_mapper.rb +57 -0
  142. data/lib/woods/notion/mappers/migration_mapper.rb +39 -0
  143. data/lib/woods/notion/mappers/model_mapper.rb +161 -0
  144. data/lib/woods/notion/mappers/shared.rb +22 -0
  145. data/lib/woods/notion/rate_limiter.rb +68 -0
  146. data/lib/woods/observability/health_check.rb +79 -0
  147. data/lib/woods/observability/instrumentation.rb +34 -0
  148. data/lib/woods/observability/structured_logger.rb +57 -0
  149. data/lib/woods/operator/error_escalator.rb +81 -0
  150. data/lib/woods/operator/pipeline_guard.rb +92 -0
  151. data/lib/woods/operator/status_reporter.rb +80 -0
  152. data/lib/woods/railtie.rb +38 -0
  153. data/lib/woods/resilience/circuit_breaker.rb +99 -0
  154. data/lib/woods/resilience/index_validator.rb +167 -0
  155. data/lib/woods/resilience/retryable_provider.rb +108 -0
  156. data/lib/woods/retrieval/context_assembler.rb +261 -0
  157. data/lib/woods/retrieval/query_classifier.rb +133 -0
  158. data/lib/woods/retrieval/ranker.rb +277 -0
  159. data/lib/woods/retrieval/search_executor.rb +316 -0
  160. data/lib/woods/retriever.rb +152 -0
  161. data/lib/woods/ruby_analyzer/class_analyzer.rb +170 -0
  162. data/lib/woods/ruby_analyzer/dataflow_analyzer.rb +77 -0
  163. data/lib/woods/ruby_analyzer/fqn_builder.rb +18 -0
  164. data/lib/woods/ruby_analyzer/mermaid_renderer.rb +280 -0
  165. data/lib/woods/ruby_analyzer/method_analyzer.rb +143 -0
  166. data/lib/woods/ruby_analyzer/trace_enricher.rb +143 -0
  167. data/lib/woods/ruby_analyzer.rb +87 -0
  168. data/lib/woods/session_tracer/file_store.rb +104 -0
  169. data/lib/woods/session_tracer/middleware.rb +143 -0
  170. data/lib/woods/session_tracer/redis_store.rb +106 -0
  171. data/lib/woods/session_tracer/session_flow_assembler.rb +254 -0
  172. data/lib/woods/session_tracer/session_flow_document.rb +223 -0
  173. data/lib/woods/session_tracer/solid_cache_store.rb +139 -0
  174. data/lib/woods/session_tracer/store.rb +81 -0
  175. data/lib/woods/storage/graph_store.rb +120 -0
  176. data/lib/woods/storage/metadata_store.rb +196 -0
  177. data/lib/woods/storage/pgvector.rb +195 -0
  178. data/lib/woods/storage/qdrant.rb +205 -0
  179. data/lib/woods/storage/vector_store.rb +167 -0
  180. data/lib/woods/temporal/json_snapshot_store.rb +245 -0
  181. data/lib/woods/temporal/snapshot_store.rb +345 -0
  182. data/lib/woods/token_utils.rb +19 -0
  183. data/lib/woods/version.rb +5 -0
  184. data/lib/woods.rb +246 -0
  185. metadata +270 -0
@@ -0,0 +1,612 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mcp'
4
+ require_relative 'connection_manager'
5
+ require_relative 'model_validator'
6
+ require_relative 'safe_context'
7
+ require_relative 'tools/tier1'
8
+ require_relative 'tools/tier2'
9
+ require_relative 'tools/tier3'
10
+ require_relative 'tools/tier4'
11
+ require_relative 'sql_validator'
12
+ require_relative 'audit_logger'
13
+ require_relative 'confirmation'
14
+ require_relative 'console_response_renderer'
15
+
16
+ module Woods
17
+ module Console
18
+ # Console MCP Server — queries live Rails application state.
19
+ #
20
+ # Communicates with a bridge process running inside the Rails environment
21
+ # via JSON-lines over stdio. Exposes Tier 1-4 tools (read-only, domain, analytics, guarded) through MCP.
22
+ #
23
+ # @example
24
+ # server = Woods::Console::Server.build(config: config)
25
+ # transport = MCP::Server::Transports::StdioTransport.new(server)
26
+ # transport.open
27
+ #
28
+ module Server # rubocop:disable Metrics/ModuleLength
29
+ TIER1_TOOLS = %w[count sample find pluck aggregate association_count schema recent status].freeze
30
+ TIER2_TOOLS = %w[diagnose_model data_snapshot validate_record check_setting update_setting
31
+ check_policy validate_with check_eligibility decorate].freeze
32
+ TIER3_TOOLS = %w[slow_endpoints error_rates throughput job_queues job_failures job_find
33
+ job_schedule redis_info cache_stats channel_status].freeze
34
+ TIER4_TOOLS = %w[eval sql query].freeze
35
+
36
+ class << self # rubocop:disable Metrics/ClassLength
37
+ # Build a configured MCP::Server with console tools using the bridge protocol.
38
+ #
39
+ # @param config [Hash] Configuration hash (from YAML or env)
40
+ # @return [MCP::Server] Configured server ready for transport
41
+ def build(config:)
42
+ connection_config = config['console'] || config
43
+ conn_mgr = ConnectionManager.new(config: connection_config)
44
+ redacted_columns = Array(config['redacted_columns'] || connection_config['redacted_columns'])
45
+ safe_ctx = redacted_columns.any? ? SafeContext.new(connection: nil, redacted_columns: redacted_columns) : nil
46
+
47
+ build_server(conn_mgr, safe_ctx)
48
+ end
49
+
50
+ # Build a configured MCP::Server using embedded ActiveRecord execution.
51
+ #
52
+ # No bridge process needed — queries run directly via ActiveRecord.
53
+ # Pass the returned server to StdioTransport or StreamableHTTPTransport.
54
+ #
55
+ # @param model_validator [ModelValidator] Validates model/column names
56
+ # @param safe_context [SafeContext] Wraps queries in rolled-back transactions
57
+ # @param redacted_columns [Array<String>] Column names to redact from output
58
+ # @param connection [Object, nil] Database connection for adapter detection
59
+ # @param read_tools_enabled [Boolean] Enable sql/query tools in embedded mode (default: false)
60
+ # @return [MCP::Server] Configured server ready for transport
61
+ def build_embedded(model_validator:, safe_context:, redacted_columns: [], connection: nil,
62
+ read_tools_enabled: false)
63
+ require_relative 'embedded_executor'
64
+
65
+ executor = EmbeddedExecutor.new(
66
+ model_validator: model_validator, safe_context: safe_context,
67
+ connection: connection, read_tools_enabled: read_tools_enabled
68
+ )
69
+ redact_ctx = if redacted_columns.any?
70
+ SafeContext.new(connection: nil,
71
+ redacted_columns: redacted_columns)
72
+ end
73
+
74
+ build_server(executor, redact_ctx)
75
+ end
76
+
77
+ # Register Tier 1 read-only tools on the server.
78
+ #
79
+ # @param server [MCP::Server] The MCP server instance
80
+ # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
81
+ # @param safe_ctx [SafeContext, nil] Optional context for column redaction
82
+ # @return [void]
83
+ def register_tier1_tools(server, conn_mgr, safe_ctx = nil, renderer: nil)
84
+ TIER1_TOOLS.each { |tool| send(:"define_#{tool}", server, conn_mgr, safe_ctx, renderer: renderer) }
85
+ end
86
+
87
+ # Register Tier 2 domain-aware tools on the server.
88
+ #
89
+ # @param server [MCP::Server] The MCP server instance
90
+ # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
91
+ # @param safe_ctx [SafeContext, nil] Optional context for column redaction
92
+ # @return [void]
93
+ def register_tier2_tools(server, conn_mgr, safe_ctx = nil, renderer: nil)
94
+ TIER2_TOOLS.each { |tool| send(:"define_#{tool}", server, conn_mgr, safe_ctx, renderer: renderer) }
95
+ end
96
+
97
+ # Register Tier 3 analytics tools on the server.
98
+ #
99
+ # @param server [MCP::Server] The MCP server instance
100
+ # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
101
+ # @param safe_ctx [SafeContext, nil] Optional context for column redaction
102
+ # @return [void]
103
+ def register_tier3_tools(server, conn_mgr, safe_ctx = nil, renderer: nil)
104
+ TIER3_TOOLS.each { |tool| send(:"define_#{tool}", server, conn_mgr, safe_ctx, renderer: renderer) }
105
+ end
106
+
107
+ # Register Tier 4 guarded tools on the server.
108
+ #
109
+ # @param server [MCP::Server] The MCP server instance
110
+ # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Request executor
111
+ # @param safe_ctx [SafeContext, nil] Optional context for column redaction
112
+ # @return [void]
113
+ def register_tier4_tools(server, conn_mgr, safe_ctx = nil, renderer: nil)
114
+ TIER4_TOOLS.each { |tool| send(:"define_#{tool}", server, conn_mgr, safe_ctx, renderer: renderer) }
115
+ end
116
+
117
+ private
118
+
119
+ # Shared server construction used by both build() and build_embedded().
120
+ #
121
+ # @param conn_mgr [ConnectionManager, EmbeddedExecutor] Any object with send_request(Hash) -> Hash
122
+ # @param safe_ctx [SafeContext, nil] Optional context for column redaction
123
+ # @return [MCP::Server]
124
+ def build_server(conn_mgr, safe_ctx)
125
+ server = ::MCP::Server.new(
126
+ name: 'woods-console',
127
+ version: defined?(Woods::VERSION) ? Woods::VERSION : '0.1.0'
128
+ )
129
+
130
+ renderer = build_console_renderer
131
+
132
+ register_tier1_tools(server, conn_mgr, safe_ctx, renderer: renderer)
133
+ register_tier2_tools(server, conn_mgr, safe_ctx, renderer: renderer)
134
+ register_tier3_tools(server, conn_mgr, safe_ctx, renderer: renderer)
135
+ register_tier4_tools(server, conn_mgr, safe_ctx, renderer: renderer)
136
+ server
137
+ end
138
+
139
+ def respond(text)
140
+ ::MCP::Tool::Response.new([{ type: 'text', text: text }])
141
+ end
142
+
143
+ def send_to_bridge(conn_mgr, request, safe_ctx = nil, renderer: nil)
144
+ response = conn_mgr.send_request(request)
145
+ if response['ok']
146
+ result = response['result']
147
+ result = apply_redaction(result, safe_ctx) if safe_ctx
148
+ text = renderer ? renderer.render_default(result) : JSON.pretty_generate(result)
149
+ respond(text)
150
+ else
151
+ error_text = "#{response['error_type']}: #{response['error']}"
152
+ ::MCP::Tool::Response.new(
153
+ [{ type: 'text', text: error_text }],
154
+ error: error_text
155
+ )
156
+ end
157
+ rescue ConnectionError => e
158
+ ::MCP::Tool::Response.new([{ type: 'text', text: "Connection error: #{e.message}" }], error: e.message)
159
+ end
160
+
161
+ # Apply SafeContext column redaction to a result value.
162
+ #
163
+ # Handles Hash (single record) and Array<Hash> (multiple records).
164
+ # Non-Hash values are returned unchanged.
165
+ #
166
+ # @param result [Object] The result from the bridge
167
+ # @param safe_ctx [SafeContext] The context with redacted_columns configured
168
+ # @return [Object] Redacted result
169
+ def apply_redaction(result, safe_ctx)
170
+ case result
171
+ when Array
172
+ result.map { |item| item.is_a?(Hash) ? safe_ctx.redact(item) : item }
173
+ when Hash
174
+ safe_ctx.redact(result)
175
+ else
176
+ result
177
+ end
178
+ end
179
+
180
+ def build_console_renderer
181
+ format = if Woods.respond_to?(:configuration)
182
+ Woods.configuration&.context_format || :markdown
183
+ else
184
+ :markdown
185
+ end
186
+ format == :json ? JsonConsoleRenderer.new : ConsoleResponseRenderer.new
187
+ end
188
+
189
+ def define_count(server, conn_mgr, safe_ctx = nil, renderer: nil)
190
+ define_console_tool(server, conn_mgr, 'console_count', 'Count records matching scope conditions',
191
+ properties: { model: str_prop('Model name'), scope: obj_prop('Filter conditions') },
192
+ required: ['model'], safe_ctx: safe_ctx, renderer: renderer) do |args|
193
+ Tools::Tier1.console_count(model: args[:model], scope: args[:scope])
194
+ end
195
+ end
196
+
197
+ def define_sample(server, conn_mgr, safe_ctx = nil, renderer: nil)
198
+ define_console_tool(server, conn_mgr, 'console_sample', 'Random sample of records',
199
+ properties: {
200
+ model: str_prop('Model name'), limit: int_prop('Max records (default 5, max 25)'),
201
+ columns: arr_prop('Columns to include'), scope: obj_prop('Filter conditions')
202
+ }, required: ['model'], safe_ctx: safe_ctx, renderer: renderer) do |args|
203
+ Tools::Tier1.console_sample(
204
+ model: args[:model], scope: args[:scope], limit: args[:limit] || 5, columns: args[:columns]
205
+ )
206
+ end
207
+ end
208
+
209
+ def define_find(server, conn_mgr, safe_ctx = nil, renderer: nil)
210
+ define_console_tool(server, conn_mgr, 'console_find',
211
+ 'Find a single record by primary key or unique column',
212
+ properties: {
213
+ model: str_prop('Model name'), id: int_prop('Primary key value'),
214
+ by: obj_prop('Unique column lookup'),
215
+ columns: arr_prop('Columns to include')
216
+ }, required: ['model'], safe_ctx: safe_ctx, renderer: renderer) do |args|
217
+ Tools::Tier1.console_find(model: args[:model], id: args[:id], by: args[:by], columns: args[:columns])
218
+ end
219
+ end
220
+
221
+ def define_pluck(server, conn_mgr, safe_ctx = nil, renderer: nil)
222
+ define_console_tool(server, conn_mgr, 'console_pluck', 'Extract column values from records',
223
+ properties: {
224
+ model: str_prop('Model name'), columns: arr_prop('Column names to pluck'),
225
+ scope: obj_prop('Filter conditions'),
226
+ limit: int_prop('Max records (default 100, max 1000)'),
227
+ distinct: bool_prop('Return unique values only')
228
+ }, required: %w[model columns], safe_ctx: safe_ctx, renderer: renderer) do |args|
229
+ Tools::Tier1.console_pluck(
230
+ model: args[:model], columns: args[:columns], scope: args[:scope],
231
+ limit: args[:limit] || 100, distinct: args[:distinct] || false
232
+ )
233
+ end
234
+ end
235
+
236
+ def define_aggregate(server, conn_mgr, safe_ctx = nil, renderer: nil)
237
+ define_console_tool(server, conn_mgr, 'console_aggregate',
238
+ 'Run aggregate function (sum/avg/min/max) on a column',
239
+ properties: {
240
+ model: str_prop('Model name'),
241
+ function: str_prop('Aggregate function: sum, avg, minimum, maximum'),
242
+ column: str_prop('Column to aggregate'), scope: obj_prop('Filter conditions')
243
+ }, required: %w[model function column], safe_ctx: safe_ctx, renderer: renderer) do |args|
244
+ Tools::Tier1.console_aggregate(
245
+ model: args[:model], function: args[:function], column: args[:column], scope: args[:scope]
246
+ )
247
+ end
248
+ end
249
+
250
+ def define_association_count(server, conn_mgr, safe_ctx = nil, renderer: nil)
251
+ define_console_tool(server, conn_mgr, 'console_association_count',
252
+ 'Count associated records for a specific record',
253
+ properties: {
254
+ model: str_prop('Model name'), id: int_prop('Record primary key'),
255
+ association: str_prop('Association name'),
256
+ scope: obj_prop('Filter on association')
257
+ }, required: %w[model id association], safe_ctx: safe_ctx, renderer: renderer) do |args|
258
+ Tools::Tier1.console_association_count(
259
+ model: args[:model], id: args[:id], association: args[:association], scope: args[:scope]
260
+ )
261
+ end
262
+ end
263
+
264
+ def define_schema(server, conn_mgr, safe_ctx = nil, renderer: nil)
265
+ define_console_tool(server, conn_mgr, 'console_schema', 'Get database schema for a model',
266
+ properties: {
267
+ model: str_prop('Model name'),
268
+ include_indexes: bool_prop('Include index information')
269
+ }, required: ['model'], safe_ctx: safe_ctx, renderer: renderer) do |args|
270
+ Tools::Tier1.console_schema(model: args[:model], include_indexes: args[:include_indexes] || false)
271
+ end
272
+ end
273
+
274
+ def define_recent(server, conn_mgr, safe_ctx = nil, renderer: nil)
275
+ define_console_tool(server, conn_mgr, 'console_recent', 'Recently created/updated records',
276
+ properties: {
277
+ model: str_prop('Model name'),
278
+ order_by: str_prop('Column to sort by (default: created_at)'),
279
+ direction: str_prop('Sort direction: asc or desc (default: desc)'),
280
+ limit: int_prop('Max records (default 10, max 50)'),
281
+ scope: obj_prop('Filter conditions'), columns: arr_prop('Columns to include')
282
+ }, required: ['model'], safe_ctx: safe_ctx, renderer: renderer) do |args|
283
+ Tools::Tier1.console_recent(
284
+ model: args[:model], order_by: args[:order_by] || 'created_at',
285
+ direction: args[:direction] || 'desc', limit: args[:limit] || 10,
286
+ scope: args[:scope], columns: args[:columns]
287
+ )
288
+ end
289
+ end
290
+
291
+ def define_status(server, conn_mgr, safe_ctx = nil, renderer: nil)
292
+ define_console_tool(server, conn_mgr, 'console_status',
293
+ 'System health check - list models and connection status',
294
+ properties: {}, safe_ctx: safe_ctx, renderer: renderer) do |_args|
295
+ Tools::Tier1.console_status
296
+ end
297
+ end
298
+
299
+ # ── Tier 2 tool definitions ──────────────────────────────────────────
300
+
301
+ def define_diagnose_model(server, conn_mgr, safe_ctx = nil, renderer: nil)
302
+ define_console_tool(server, conn_mgr, 'console_diagnose_model',
303
+ 'Diagnose a model: count, recent records, aggregates',
304
+ properties: {
305
+ model: str_prop('Model name'), scope: obj_prop('Filter conditions'),
306
+ sample_size: int_prop('Sample records (default 5, max 25)')
307
+ }, required: ['model'], safe_ctx: safe_ctx, renderer: renderer) do |args|
308
+ Tools::Tier2.console_diagnose_model(
309
+ model: args[:model], scope: args[:scope], sample_size: args[:sample_size] || 5
310
+ )
311
+ end
312
+ end
313
+
314
+ def define_data_snapshot(server, conn_mgr, safe_ctx = nil, renderer: nil)
315
+ define_console_tool(server, conn_mgr, 'console_data_snapshot',
316
+ 'Snapshot a record with associations for debugging',
317
+ properties: {
318
+ model: str_prop('Model name'), id: int_prop('Record primary key'),
319
+ associations: arr_prop('Association names to include'),
320
+ depth: int_prop('Association depth (default 1, max 3)')
321
+ }, required: %w[model id], safe_ctx: safe_ctx, renderer: renderer) do |args|
322
+ Tools::Tier2.console_data_snapshot(
323
+ model: args[:model], id: args[:id],
324
+ associations: args[:associations], depth: args[:depth] || 1
325
+ )
326
+ end
327
+ end
328
+
329
+ def define_validate_record(server, conn_mgr, safe_ctx = nil, renderer: nil)
330
+ define_console_tool(server, conn_mgr, 'console_validate_record',
331
+ 'Run validations on an existing record',
332
+ properties: {
333
+ model: str_prop('Model name'), id: int_prop('Record primary key'),
334
+ attributes: obj_prop('Attributes to set before validating')
335
+ }, required: %w[model id], safe_ctx: safe_ctx, renderer: renderer) do |args|
336
+ Tools::Tier2.console_validate_record(
337
+ model: args[:model], id: args[:id], attributes: args[:attributes]
338
+ )
339
+ end
340
+ end
341
+
342
+ def define_check_setting(server, conn_mgr, safe_ctx = nil, renderer: nil)
343
+ define_console_tool(server, conn_mgr, 'console_check_setting',
344
+ 'Check a configuration setting value',
345
+ properties: {
346
+ key: str_prop('Setting key'), namespace: str_prop('Setting namespace')
347
+ }, required: ['key'], safe_ctx: safe_ctx, renderer: renderer) do |args|
348
+ Tools::Tier2.console_check_setting(key: args[:key], namespace: args[:namespace])
349
+ end
350
+ end
351
+
352
+ def define_update_setting(server, conn_mgr, safe_ctx = nil, renderer: nil)
353
+ define_console_tool(server, conn_mgr, 'console_update_setting',
354
+ 'Update a configuration setting (requires confirmation)',
355
+ properties: {
356
+ key: str_prop('Setting key'), value: str_prop('New value'),
357
+ namespace: str_prop('Setting namespace')
358
+ }, required: %w[key value], safe_ctx: safe_ctx, renderer: renderer) do |args|
359
+ Tools::Tier2.console_update_setting(
360
+ key: args[:key], value: args[:value], namespace: args[:namespace]
361
+ )
362
+ end
363
+ end
364
+
365
+ def define_check_policy(server, conn_mgr, safe_ctx = nil, renderer: nil)
366
+ define_console_tool(server, conn_mgr, 'console_check_policy',
367
+ 'Check authorization policy for a record and user',
368
+ properties: {
369
+ model: str_prop('Model name'), id: int_prop('Record primary key'),
370
+ user_id: int_prop('User to check'), action: str_prop('Policy action')
371
+ }, required: %w[model id user_id action],
372
+ safe_ctx: safe_ctx, renderer: renderer) do |args|
373
+ Tools::Tier2.console_check_policy(
374
+ model: args[:model], id: args[:id], user_id: args[:user_id], action: args[:action]
375
+ )
376
+ end
377
+ end
378
+
379
+ def define_validate_with(server, conn_mgr, safe_ctx = nil, renderer: nil)
380
+ define_console_tool(server, conn_mgr, 'console_validate_with',
381
+ 'Validate attributes against a model without persisting',
382
+ properties: {
383
+ model: str_prop('Model name'), attributes: obj_prop('Attributes to validate'),
384
+ context: str_prop('Validation context')
385
+ }, required: %w[model attributes], safe_ctx: safe_ctx, renderer: renderer) do |args|
386
+ Tools::Tier2.console_validate_with(
387
+ model: args[:model], attributes: args[:attributes], context: args[:context]
388
+ )
389
+ end
390
+ end
391
+
392
+ def define_check_eligibility(server, conn_mgr, safe_ctx = nil, renderer: nil)
393
+ define_console_tool(server, conn_mgr, 'console_check_eligibility',
394
+ 'Check feature eligibility for a record',
395
+ properties: {
396
+ model: str_prop('Model name'), id: int_prop('Record primary key'),
397
+ feature: str_prop('Feature name')
398
+ }, required: %w[model id feature], safe_ctx: safe_ctx, renderer: renderer) do |args|
399
+ Tools::Tier2.console_check_eligibility(
400
+ model: args[:model], id: args[:id], feature: args[:feature]
401
+ )
402
+ end
403
+ end
404
+
405
+ def define_decorate(server, conn_mgr, safe_ctx = nil, renderer: nil)
406
+ define_console_tool(server, conn_mgr, 'console_decorate',
407
+ 'Invoke a decorator on a record and return computed attributes',
408
+ properties: {
409
+ model: str_prop('Model name'), id: int_prop('Record primary key'),
410
+ methods: arr_prop('Decorator methods to call')
411
+ }, required: %w[model id], safe_ctx: safe_ctx, renderer: renderer) do |args|
412
+ Tools::Tier2.console_decorate(model: args[:model], id: args[:id], methods: args[:methods])
413
+ end
414
+ end
415
+
416
+ # ── Tier 3 tool definitions ──────────────────────────────────────────
417
+
418
+ def define_slow_endpoints(server, conn_mgr, safe_ctx = nil, renderer: nil)
419
+ define_console_tool(server, conn_mgr, 'console_slow_endpoints',
420
+ 'List slowest endpoints by response time',
421
+ properties: {
422
+ limit: int_prop('Max endpoints (default 10, max 100)'),
423
+ period: str_prop('Time period (default: 1h)')
424
+ }, safe_ctx: safe_ctx, renderer: renderer) do |args|
425
+ Tools::Tier3.console_slow_endpoints(limit: args[:limit] || 10, period: args[:period] || '1h')
426
+ end
427
+ end
428
+
429
+ def define_error_rates(server, conn_mgr, safe_ctx = nil, renderer: nil)
430
+ define_console_tool(server, conn_mgr, 'console_error_rates',
431
+ 'Get error rates by controller or overall',
432
+ properties: {
433
+ period: str_prop('Time period (default: 1h)'),
434
+ controller: str_prop('Filter by controller')
435
+ }, safe_ctx: safe_ctx, renderer: renderer) do |args|
436
+ Tools::Tier3.console_error_rates(period: args[:period] || '1h', controller: args[:controller])
437
+ end
438
+ end
439
+
440
+ def define_throughput(server, conn_mgr, safe_ctx = nil, renderer: nil)
441
+ define_console_tool(server, conn_mgr, 'console_throughput',
442
+ 'Get request throughput over time',
443
+ properties: {
444
+ period: str_prop('Time period (default: 1h)'),
445
+ interval: str_prop('Aggregation interval (default: 5m)')
446
+ }, safe_ctx: safe_ctx, renderer: renderer) do |args|
447
+ Tools::Tier3.console_throughput(
448
+ period: args[:period] || '1h', interval: args[:interval] || '5m'
449
+ )
450
+ end
451
+ end
452
+
453
+ def define_job_queues(server, conn_mgr, safe_ctx = nil, renderer: nil)
454
+ define_console_tool(server, conn_mgr, 'console_job_queues',
455
+ 'Get job queue statistics',
456
+ properties: {
457
+ queue: str_prop('Filter by queue name')
458
+ }, safe_ctx: safe_ctx, renderer: renderer) do |args|
459
+ Tools::Tier3.console_job_queues(queue: args[:queue])
460
+ end
461
+ end
462
+
463
+ def define_job_failures(server, conn_mgr, safe_ctx = nil, renderer: nil)
464
+ define_console_tool(server, conn_mgr, 'console_job_failures',
465
+ 'List recent job failures',
466
+ properties: {
467
+ limit: int_prop('Max failures (default 10, max 100)'),
468
+ queue: str_prop('Filter by queue name')
469
+ }, safe_ctx: safe_ctx, renderer: renderer) do |args|
470
+ Tools::Tier3.console_job_failures(limit: args[:limit] || 10, queue: args[:queue])
471
+ end
472
+ end
473
+
474
+ def define_job_find(server, conn_mgr, safe_ctx = nil, renderer: nil)
475
+ define_console_tool(server, conn_mgr, 'console_job_find',
476
+ 'Find a job by ID, optionally retry it (requires confirmation)',
477
+ properties: {
478
+ job_id: str_prop('Job identifier'),
479
+ retry: bool_prop('Retry the job (requires confirmation)')
480
+ }, required: ['job_id'], safe_ctx: safe_ctx, renderer: renderer) do |args|
481
+ Tools::Tier3.console_job_find(job_id: args[:job_id], retry_job: args[:retry])
482
+ end
483
+ end
484
+
485
+ def define_job_schedule(server, conn_mgr, safe_ctx = nil, renderer: nil)
486
+ define_console_tool(server, conn_mgr, 'console_job_schedule',
487
+ 'List scheduled/upcoming jobs',
488
+ properties: {
489
+ limit: int_prop('Max jobs (default 20, max 100)')
490
+ }, safe_ctx: safe_ctx, renderer: renderer) do |args|
491
+ Tools::Tier3.console_job_schedule(limit: args[:limit] || 20)
492
+ end
493
+ end
494
+
495
+ def define_redis_info(server, conn_mgr, safe_ctx = nil, renderer: nil)
496
+ define_console_tool(server, conn_mgr, 'console_redis_info',
497
+ 'Get Redis server information',
498
+ properties: {
499
+ section: str_prop('INFO section (e.g., memory, stats)')
500
+ }, safe_ctx: safe_ctx, renderer: renderer) do |args|
501
+ Tools::Tier3.console_redis_info(section: args[:section])
502
+ end
503
+ end
504
+
505
+ def define_cache_stats(server, conn_mgr, safe_ctx = nil, renderer: nil)
506
+ define_console_tool(server, conn_mgr, 'console_cache_stats',
507
+ 'Get cache store statistics',
508
+ properties: {
509
+ namespace: str_prop('Cache namespace filter')
510
+ }, safe_ctx: safe_ctx, renderer: renderer) do |args|
511
+ Tools::Tier3.console_cache_stats(namespace: args[:namespace])
512
+ end
513
+ end
514
+
515
+ def define_channel_status(server, conn_mgr, safe_ctx = nil, renderer: nil)
516
+ define_console_tool(server, conn_mgr, 'console_channel_status',
517
+ 'Get ActionCable channel status',
518
+ properties: {
519
+ channel: str_prop('Filter by channel name')
520
+ }, safe_ctx: safe_ctx, renderer: renderer) do |args|
521
+ Tools::Tier3.console_channel_status(channel: args[:channel])
522
+ end
523
+ end
524
+
525
+ # ── Tier 4 tool definitions ──────────────────────────────────────────
526
+
527
+ def define_eval(server, conn_mgr, safe_ctx = nil, renderer: nil)
528
+ define_console_tool(server, conn_mgr, 'console_eval',
529
+ 'Execute arbitrary Ruby code (requires confirmation)',
530
+ properties: {
531
+ code: str_prop('Ruby code to execute'),
532
+ timeout: int_prop('Timeout in seconds (default 10, max 30)')
533
+ }, required: ['code'], safe_ctx: safe_ctx, renderer: renderer) do |args|
534
+ Tools::Tier4.console_eval(code: args[:code], timeout: args[:timeout] || 10)
535
+ end
536
+ end
537
+
538
+ def define_sql(server, conn_mgr, safe_ctx = nil, renderer: nil)
539
+ validator = SqlValidator.new
540
+ define_console_tool(server, conn_mgr, 'console_sql',
541
+ 'Execute read-only SQL (SELECT/WITH...SELECT only)',
542
+ properties: {
543
+ sql: str_prop('SQL query (SELECT or WITH...SELECT only)'),
544
+ limit: int_prop('Max rows returned (default unlimited, max 10000)')
545
+ }, required: ['sql'], safe_ctx: safe_ctx, renderer: renderer) do |args|
546
+ Tools::Tier4.console_sql(sql: args[:sql], validator: validator, limit: args[:limit])
547
+ end
548
+ end
549
+
550
+ def define_query(server, conn_mgr, safe_ctx = nil, renderer: nil)
551
+ props = {
552
+ model: str_prop('Model name'), select: arr_prop('Columns to select'),
553
+ joins: arr_prop('Associations to join'), group_by: arr_prop('Columns to group by'),
554
+ having: str_prop('HAVING clause'), order: obj_prop('Order specification'),
555
+ scope: obj_prop('Filter conditions'), limit: int_prop('Max rows (max 10000)')
556
+ }
557
+ define_console_tool(server, conn_mgr, 'console_query',
558
+ 'Enhanced query builder with joins and grouping',
559
+ properties: props, required: %w[model select],
560
+ safe_ctx: safe_ctx, renderer: renderer) do |args|
561
+ Tools::Tier4.console_query(
562
+ model: args[:model], select: args[:select], joins: args[:joins],
563
+ group_by: args[:group_by], having: args[:having],
564
+ order: args[:order], scope: args[:scope], limit: args[:limit]
565
+ )
566
+ end
567
+ end
568
+
569
+ # Shared tool definition helper that wires block -> bridge -> response.
570
+ # rubocop:disable Metrics/ParameterLists
571
+ def define_console_tool(server, conn_mgr, name, description, properties:, required: nil,
572
+ safe_ctx: nil, renderer: nil, &tool_block)
573
+ bridge_method = method(:send_to_bridge)
574
+ coerce_method = method(:coerce_integer_args!)
575
+ integer_keys = integer_property_keys(properties)
576
+ schema = { properties: properties }
577
+ schema[:required] = required if required&.any?
578
+ server.define_tool(name: name, description: description, input_schema: schema) do |server_context:, **args|
579
+ coerce_method.call(args, integer_keys)
580
+ request = tool_block.call(args)
581
+ bridge_method.call(conn_mgr, request.transform_keys(&:to_s), safe_ctx, renderer: renderer)
582
+ end
583
+ end
584
+ # rubocop:enable Metrics/ParameterLists
585
+
586
+ # Pre-compute property keys declared as integer in a schema.
587
+ #
588
+ # @param properties [Hash] Tool schema properties
589
+ # @return [Array<Symbol>]
590
+ def integer_property_keys(properties)
591
+ properties.select { |_k, v| v[:type] == 'integer' }.keys.map(&:to_sym)
592
+ end
593
+
594
+ # Coerce string values to integers for known integer keys.
595
+ #
596
+ # @param args [Hash] Tool arguments (mutated in place)
597
+ # @param keys [Array<Symbol>] Keys that should be integers
598
+ # @return [void]
599
+ def coerce_integer_args!(args, keys)
600
+ keys.each { |k| args[k] = args[k].to_i if args[k].is_a?(String) }
601
+ end
602
+
603
+ # Schema property helpers for concise tool definitions.
604
+ def str_prop(desc) = { type: 'string', description: desc }
605
+ def int_prop(desc) = { type: 'integer', description: desc }
606
+ def obj_prop(desc) = { type: 'object', description: desc }
607
+ def bool_prop(desc) = { type: 'boolean', description: desc }
608
+ def arr_prop(desc) = { type: 'array', items: { type: 'string' }, description: desc }
609
+ end
610
+ end
611
+ end
612
+ end