codebase_index 0.3.2 → 0.4.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 (184) hide show
  1. checksums.yaml +4 -4
  2. data/lib/codebase_index.rb +3 -243
  3. metadata +28 -223
  4. data/CHANGELOG.md +0 -89
  5. data/CODE_OF_CONDUCT.md +0 -83
  6. data/CONTRIBUTING.md +0 -65
  7. data/LICENSE.txt +0 -21
  8. data/README.md +0 -325
  9. data/exe/codebase-console +0 -59
  10. data/exe/codebase-console-mcp +0 -22
  11. data/exe/codebase-index-mcp +0 -34
  12. data/exe/codebase-index-mcp-http +0 -37
  13. data/exe/codebase-index-mcp-start +0 -58
  14. data/lib/codebase_index/ast/call_site_extractor.rb +0 -106
  15. data/lib/codebase_index/ast/method_extractor.rb +0 -71
  16. data/lib/codebase_index/ast/node.rb +0 -116
  17. data/lib/codebase_index/ast/parser.rb +0 -614
  18. data/lib/codebase_index/ast.rb +0 -6
  19. data/lib/codebase_index/builder.rb +0 -200
  20. data/lib/codebase_index/cache/cache_middleware.rb +0 -199
  21. data/lib/codebase_index/cache/cache_store.rb +0 -264
  22. data/lib/codebase_index/cache/redis_cache_store.rb +0 -116
  23. data/lib/codebase_index/cache/solid_cache_store.rb +0 -111
  24. data/lib/codebase_index/chunking/chunk.rb +0 -84
  25. data/lib/codebase_index/chunking/semantic_chunker.rb +0 -295
  26. data/lib/codebase_index/console/adapters/cache_adapter.rb +0 -58
  27. data/lib/codebase_index/console/adapters/good_job_adapter.rb +0 -33
  28. data/lib/codebase_index/console/adapters/job_adapter.rb +0 -68
  29. data/lib/codebase_index/console/adapters/sidekiq_adapter.rb +0 -33
  30. data/lib/codebase_index/console/adapters/solid_queue_adapter.rb +0 -33
  31. data/lib/codebase_index/console/audit_logger.rb +0 -75
  32. data/lib/codebase_index/console/bridge.rb +0 -177
  33. data/lib/codebase_index/console/confirmation.rb +0 -90
  34. data/lib/codebase_index/console/connection_manager.rb +0 -173
  35. data/lib/codebase_index/console/console_response_renderer.rb +0 -74
  36. data/lib/codebase_index/console/embedded_executor.rb +0 -373
  37. data/lib/codebase_index/console/model_validator.rb +0 -81
  38. data/lib/codebase_index/console/rack_middleware.rb +0 -87
  39. data/lib/codebase_index/console/safe_context.rb +0 -82
  40. data/lib/codebase_index/console/server.rb +0 -612
  41. data/lib/codebase_index/console/sql_validator.rb +0 -172
  42. data/lib/codebase_index/console/tools/tier1.rb +0 -118
  43. data/lib/codebase_index/console/tools/tier2.rb +0 -117
  44. data/lib/codebase_index/console/tools/tier3.rb +0 -110
  45. data/lib/codebase_index/console/tools/tier4.rb +0 -79
  46. data/lib/codebase_index/coordination/pipeline_lock.rb +0 -109
  47. data/lib/codebase_index/cost_model/embedding_cost.rb +0 -88
  48. data/lib/codebase_index/cost_model/estimator.rb +0 -128
  49. data/lib/codebase_index/cost_model/provider_pricing.rb +0 -67
  50. data/lib/codebase_index/cost_model/storage_cost.rb +0 -52
  51. data/lib/codebase_index/cost_model.rb +0 -22
  52. data/lib/codebase_index/db/migrations/001_create_units.rb +0 -38
  53. data/lib/codebase_index/db/migrations/002_create_edges.rb +0 -35
  54. data/lib/codebase_index/db/migrations/003_create_embeddings.rb +0 -37
  55. data/lib/codebase_index/db/migrations/004_create_snapshots.rb +0 -45
  56. data/lib/codebase_index/db/migrations/005_create_snapshot_units.rb +0 -40
  57. data/lib/codebase_index/db/migrator.rb +0 -71
  58. data/lib/codebase_index/db/schema_version.rb +0 -73
  59. data/lib/codebase_index/dependency_graph.rb +0 -236
  60. data/lib/codebase_index/embedding/indexer.rb +0 -140
  61. data/lib/codebase_index/embedding/openai.rb +0 -126
  62. data/lib/codebase_index/embedding/provider.rb +0 -162
  63. data/lib/codebase_index/embedding/text_preparer.rb +0 -112
  64. data/lib/codebase_index/evaluation/baseline_runner.rb +0 -115
  65. data/lib/codebase_index/evaluation/evaluator.rb +0 -139
  66. data/lib/codebase_index/evaluation/metrics.rb +0 -79
  67. data/lib/codebase_index/evaluation/query_set.rb +0 -148
  68. data/lib/codebase_index/evaluation/report_generator.rb +0 -90
  69. data/lib/codebase_index/extracted_unit.rb +0 -145
  70. data/lib/codebase_index/extractor.rb +0 -1028
  71. data/lib/codebase_index/extractors/action_cable_extractor.rb +0 -201
  72. data/lib/codebase_index/extractors/ast_source_extraction.rb +0 -46
  73. data/lib/codebase_index/extractors/behavioral_profile.rb +0 -309
  74. data/lib/codebase_index/extractors/caching_extractor.rb +0 -261
  75. data/lib/codebase_index/extractors/callback_analyzer.rb +0 -246
  76. data/lib/codebase_index/extractors/concern_extractor.rb +0 -292
  77. data/lib/codebase_index/extractors/configuration_extractor.rb +0 -219
  78. data/lib/codebase_index/extractors/controller_extractor.rb +0 -404
  79. data/lib/codebase_index/extractors/database_view_extractor.rb +0 -278
  80. data/lib/codebase_index/extractors/decorator_extractor.rb +0 -253
  81. data/lib/codebase_index/extractors/engine_extractor.rb +0 -223
  82. data/lib/codebase_index/extractors/event_extractor.rb +0 -211
  83. data/lib/codebase_index/extractors/factory_extractor.rb +0 -289
  84. data/lib/codebase_index/extractors/graphql_extractor.rb +0 -892
  85. data/lib/codebase_index/extractors/i18n_extractor.rb +0 -117
  86. data/lib/codebase_index/extractors/job_extractor.rb +0 -374
  87. data/lib/codebase_index/extractors/lib_extractor.rb +0 -218
  88. data/lib/codebase_index/extractors/mailer_extractor.rb +0 -269
  89. data/lib/codebase_index/extractors/manager_extractor.rb +0 -188
  90. data/lib/codebase_index/extractors/middleware_extractor.rb +0 -133
  91. data/lib/codebase_index/extractors/migration_extractor.rb +0 -469
  92. data/lib/codebase_index/extractors/model_extractor.rb +0 -988
  93. data/lib/codebase_index/extractors/phlex_extractor.rb +0 -252
  94. data/lib/codebase_index/extractors/policy_extractor.rb +0 -191
  95. data/lib/codebase_index/extractors/poro_extractor.rb +0 -229
  96. data/lib/codebase_index/extractors/pundit_extractor.rb +0 -223
  97. data/lib/codebase_index/extractors/rails_source_extractor.rb +0 -473
  98. data/lib/codebase_index/extractors/rake_task_extractor.rb +0 -343
  99. data/lib/codebase_index/extractors/route_extractor.rb +0 -181
  100. data/lib/codebase_index/extractors/scheduled_job_extractor.rb +0 -331
  101. data/lib/codebase_index/extractors/serializer_extractor.rb +0 -339
  102. data/lib/codebase_index/extractors/service_extractor.rb +0 -217
  103. data/lib/codebase_index/extractors/shared_dependency_scanner.rb +0 -91
  104. data/lib/codebase_index/extractors/shared_utility_methods.rb +0 -281
  105. data/lib/codebase_index/extractors/state_machine_extractor.rb +0 -398
  106. data/lib/codebase_index/extractors/test_mapping_extractor.rb +0 -225
  107. data/lib/codebase_index/extractors/validator_extractor.rb +0 -211
  108. data/lib/codebase_index/extractors/view_component_extractor.rb +0 -311
  109. data/lib/codebase_index/extractors/view_template_extractor.rb +0 -261
  110. data/lib/codebase_index/feedback/gap_detector.rb +0 -89
  111. data/lib/codebase_index/feedback/store.rb +0 -119
  112. data/lib/codebase_index/filename_utils.rb +0 -32
  113. data/lib/codebase_index/flow_analysis/operation_extractor.rb +0 -206
  114. data/lib/codebase_index/flow_analysis/response_code_mapper.rb +0 -154
  115. data/lib/codebase_index/flow_assembler.rb +0 -290
  116. data/lib/codebase_index/flow_document.rb +0 -191
  117. data/lib/codebase_index/flow_precomputer.rb +0 -102
  118. data/lib/codebase_index/formatting/base.rb +0 -30
  119. data/lib/codebase_index/formatting/claude_adapter.rb +0 -98
  120. data/lib/codebase_index/formatting/generic_adapter.rb +0 -56
  121. data/lib/codebase_index/formatting/gpt_adapter.rb +0 -64
  122. data/lib/codebase_index/formatting/human_adapter.rb +0 -78
  123. data/lib/codebase_index/graph_analyzer.rb +0 -374
  124. data/lib/codebase_index/mcp/bootstrapper.rb +0 -96
  125. data/lib/codebase_index/mcp/index_reader.rb +0 -394
  126. data/lib/codebase_index/mcp/renderers/claude_renderer.rb +0 -81
  127. data/lib/codebase_index/mcp/renderers/json_renderer.rb +0 -17
  128. data/lib/codebase_index/mcp/renderers/markdown_renderer.rb +0 -353
  129. data/lib/codebase_index/mcp/renderers/plain_renderer.rb +0 -240
  130. data/lib/codebase_index/mcp/server.rb +0 -961
  131. data/lib/codebase_index/mcp/tool_response_renderer.rb +0 -85
  132. data/lib/codebase_index/model_name_cache.rb +0 -51
  133. data/lib/codebase_index/notion/client.rb +0 -217
  134. data/lib/codebase_index/notion/exporter.rb +0 -219
  135. data/lib/codebase_index/notion/mapper.rb +0 -40
  136. data/lib/codebase_index/notion/mappers/column_mapper.rb +0 -57
  137. data/lib/codebase_index/notion/mappers/migration_mapper.rb +0 -39
  138. data/lib/codebase_index/notion/mappers/model_mapper.rb +0 -161
  139. data/lib/codebase_index/notion/mappers/shared.rb +0 -22
  140. data/lib/codebase_index/notion/rate_limiter.rb +0 -68
  141. data/lib/codebase_index/observability/health_check.rb +0 -79
  142. data/lib/codebase_index/observability/instrumentation.rb +0 -34
  143. data/lib/codebase_index/observability/structured_logger.rb +0 -57
  144. data/lib/codebase_index/operator/error_escalator.rb +0 -81
  145. data/lib/codebase_index/operator/pipeline_guard.rb +0 -92
  146. data/lib/codebase_index/operator/status_reporter.rb +0 -80
  147. data/lib/codebase_index/railtie.rb +0 -38
  148. data/lib/codebase_index/resilience/circuit_breaker.rb +0 -99
  149. data/lib/codebase_index/resilience/index_validator.rb +0 -167
  150. data/lib/codebase_index/resilience/retryable_provider.rb +0 -108
  151. data/lib/codebase_index/retrieval/context_assembler.rb +0 -261
  152. data/lib/codebase_index/retrieval/query_classifier.rb +0 -133
  153. data/lib/codebase_index/retrieval/ranker.rb +0 -277
  154. data/lib/codebase_index/retrieval/search_executor.rb +0 -316
  155. data/lib/codebase_index/retriever.rb +0 -152
  156. data/lib/codebase_index/ruby_analyzer/class_analyzer.rb +0 -170
  157. data/lib/codebase_index/ruby_analyzer/dataflow_analyzer.rb +0 -77
  158. data/lib/codebase_index/ruby_analyzer/fqn_builder.rb +0 -18
  159. data/lib/codebase_index/ruby_analyzer/mermaid_renderer.rb +0 -280
  160. data/lib/codebase_index/ruby_analyzer/method_analyzer.rb +0 -143
  161. data/lib/codebase_index/ruby_analyzer/trace_enricher.rb +0 -143
  162. data/lib/codebase_index/ruby_analyzer.rb +0 -87
  163. data/lib/codebase_index/session_tracer/file_store.rb +0 -104
  164. data/lib/codebase_index/session_tracer/middleware.rb +0 -143
  165. data/lib/codebase_index/session_tracer/redis_store.rb +0 -106
  166. data/lib/codebase_index/session_tracer/session_flow_assembler.rb +0 -254
  167. data/lib/codebase_index/session_tracer/session_flow_document.rb +0 -223
  168. data/lib/codebase_index/session_tracer/solid_cache_store.rb +0 -139
  169. data/lib/codebase_index/session_tracer/store.rb +0 -81
  170. data/lib/codebase_index/storage/graph_store.rb +0 -120
  171. data/lib/codebase_index/storage/metadata_store.rb +0 -196
  172. data/lib/codebase_index/storage/pgvector.rb +0 -195
  173. data/lib/codebase_index/storage/qdrant.rb +0 -205
  174. data/lib/codebase_index/storage/vector_store.rb +0 -167
  175. data/lib/codebase_index/temporal/json_snapshot_store.rb +0 -245
  176. data/lib/codebase_index/temporal/snapshot_store.rb +0 -345
  177. data/lib/codebase_index/token_utils.rb +0 -19
  178. data/lib/codebase_index/version.rb +0 -5
  179. data/lib/generators/codebase_index/install_generator.rb +0 -32
  180. data/lib/generators/codebase_index/pgvector_generator.rb +0 -37
  181. data/lib/generators/codebase_index/templates/add_pgvector_to_codebase_index.rb.erb +0 -15
  182. data/lib/generators/codebase_index/templates/create_codebase_index_tables.rb.erb +0 -43
  183. data/lib/tasks/codebase_index.rake +0 -597
  184. data/lib/tasks/codebase_index_evaluation.rake +0 -115
@@ -1,961 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'logger'
4
- require 'mcp'
5
- require 'set'
6
- require_relative 'index_reader'
7
- require_relative 'tool_response_renderer'
8
-
9
- module CodebaseIndex
10
- module MCP
11
- # Builds an MCP::Server with 27 tools, 2 resources, and 2 resource templates for querying
12
- # CodebaseIndex extraction output, managing pipelines, and collecting feedback.
13
- #
14
- # All tools are defined inline via closures over an IndexReader instance.
15
- # No Rails required at runtime — reads JSON files from disk.
16
- #
17
- # @example
18
- # server = CodebaseIndex::MCP::Server.build(index_dir: "/path/to/output")
19
- # transport = MCP::Server::Transports::StdioTransport.new(server)
20
- # transport.open
21
- #
22
- module Server
23
- class << self
24
- # Build a configured MCP::Server with all tools and resources.
25
- #
26
- # @param index_dir [String] Path to extraction output directory
27
- # @param retriever [CodebaseIndex::Retriever, nil] Optional retriever for semantic search
28
- # @param operator [Hash, nil] Optional operator config with :status_reporter, :error_escalator, :pipeline_guard, :pipeline_lock
29
- # @param feedback_store [CodebaseIndex::Feedback::Store, nil] Optional feedback store
30
- # @return [MCP::Server] Configured server ready for transport
31
- def build(index_dir:, retriever: nil, operator: nil, feedback_store: nil, snapshot_store: nil, response_format: nil)
32
- reader = IndexReader.new(index_dir)
33
- config = CodebaseIndex.configuration
34
- format = response_format || (config.respond_to?(:context_format) ? config.context_format : nil) || :markdown
35
- renderer = ToolResponseRenderer.for(format)
36
- resources = build_resources
37
- resource_templates = build_resource_templates
38
-
39
- # Lambda captured by all tool blocks for building responses.
40
- respond = method(:text_response)
41
-
42
- server = ::MCP::Server.new(
43
- name: 'codebase-index',
44
- version: CodebaseIndex::VERSION,
45
- resources: resources,
46
- resource_templates: resource_templates
47
- )
48
-
49
- define_lookup_tool(server, reader, respond, renderer)
50
- define_search_tool(server, reader, respond, renderer)
51
- define_traversal_tool(server, reader, respond, renderer,
52
- name: 'dependencies',
53
- description: 'Traverse forward dependencies of a unit (what it depends on). Returns a BFS tree with depth.',
54
- reader_method: :traverse_dependencies,
55
- render_key: :dependencies)
56
- define_traversal_tool(server, reader, respond, renderer,
57
- name: 'dependents',
58
- description: 'Traverse reverse dependencies of a unit (what depends on it). Returns a BFS tree with depth.',
59
- reader_method: :traverse_dependents,
60
- render_key: :dependents)
61
- define_structure_tool(server, reader, respond, renderer)
62
- define_graph_analysis_tool(server, reader, respond, renderer)
63
- define_pagerank_tool(server, reader, respond, renderer)
64
- define_framework_tool(server, reader, respond, renderer)
65
- define_recent_changes_tool(server, reader, respond, renderer)
66
- define_reload_tool(server, reader, respond)
67
- define_retrieve_tool(server, retriever, respond)
68
- define_trace_flow_tool(server, reader, index_dir, respond, renderer)
69
- define_session_trace_tool(server, reader, respond)
70
- define_operator_tools(server, operator, respond)
71
- define_feedback_tools(server, feedback_store, respond)
72
- define_snapshot_tools(server, snapshot_store, respond)
73
- define_notion_sync_tool(server, reader, index_dir, respond)
74
- register_resource_handler(server, reader)
75
-
76
- server
77
- end
78
-
79
- private
80
-
81
- def text_response(text)
82
- ::MCP::Tool::Response.new([{ type: 'text', text: text }])
83
- end
84
-
85
- def truncate_section(array, limit)
86
- return array unless array.is_a?(Array)
87
-
88
- limit = [limit, 0].max
89
- array.first(limit).map do |item|
90
- next item unless item.is_a?(Hash) && item['dependents'].is_a?(Array) && item['dependents'].size > limit
91
-
92
- item.merge(
93
- 'dependents' => item['dependents'].first(limit),
94
- 'dependents_truncated' => true,
95
- 'dependents_total' => item['dependents'].size
96
- )
97
- end
98
- end
99
-
100
- # Coerce a value to an Array. Wraps a single String in an Array;
101
- # leaves existing Arrays and nil unchanged.
102
- #
103
- # @param value [String, Array, nil] The input value
104
- # @return [Array, nil]
105
- def coerce_array(value)
106
- value.is_a?(String) ? [value] : value
107
- end
108
-
109
- # Coerce a value to an Integer. Converts String representations
110
- # to Integer; leaves existing Integers and nil unchanged.
111
- # MCP clients may send "2" (string) instead of 2 (integer).
112
- #
113
- # @param value [String, Integer, nil] The input value
114
- # @return [Integer, nil]
115
- def coerce_integer(value)
116
- value.is_a?(String) ? value.to_i : value
117
- end
118
-
119
- # Apply offset+limit pagination to a single section key within a container hash.
120
- # Adds `_total`, `_truncated`, and `_offset` metadata keys when truncating.
121
- #
122
- # @param container [Hash] The hash to mutate
123
- # @param key [String] The section key whose value is an Array
124
- # @param limit [Integer, nil] Max items to retain; nil means no limit
125
- # @param offset [Integer] Items to skip from the front
126
- # @return [void]
127
- def paginate_section(container, key, limit, offset)
128
- original = container[key]
129
- return unless original.is_a?(Array)
130
-
131
- sliced = offset.positive? ? original.drop(offset) : original
132
- container[key] = limit ? truncate_section(sliced, limit) : sliced
133
- if original.size > offset + (limit || original.size)
134
- container["#{key}_total"] = original.size
135
- container["#{key}_truncated"] = true
136
- end
137
- container["#{key}_offset"] = offset if offset.positive?
138
- end
139
-
140
- def define_lookup_tool(server, reader, respond, renderer)
141
- coerce = method(:coerce_array)
142
- server.define_tool(
143
- name: 'lookup',
144
- description: 'Look up a code unit by its exact identifier. Returns full source code, metadata, ' \
145
- 'dependencies, and dependents. Use include_source: false to omit source_code. ' \
146
- 'Use sections to select specific keys (type, identifier, file_path, namespace are always included).',
147
- input_schema: {
148
- properties: {
149
- identifier: { type: 'string',
150
- description: 'Exact unit identifier (e.g. "Post", "PostsController", "Api::V1::HealthController")' },
151
- include_source: { type: 'boolean', description: 'Include source_code in response (default: true)' },
152
- sections: {
153
- type: 'array', items: { type: 'string' },
154
- description: 'Select specific keys to return (e.g. ["metadata", "dependencies"]). Always includes type, identifier, file_path, namespace.'
155
- }
156
- },
157
- required: ['identifier']
158
- }
159
- ) do |identifier:, server_context:, include_source: nil, sections: nil|
160
- sections = coerce.call(sections)
161
- unit = reader.find_unit(identifier)
162
- if unit
163
- always_include = %w[type identifier file_path namespace]
164
- filtered = unit
165
- filtered = filtered.except('source_code') if include_source == false
166
- if sections&.any?
167
- allowed = (always_include + sections).to_set
168
- filtered = filtered.slice(*allowed)
169
- end
170
- respond.call(renderer.render(:lookup, filtered))
171
- else
172
- respond.call("Unit not found: #{identifier}")
173
- end
174
- end
175
- end
176
-
177
- def define_search_tool(server, reader, respond, renderer)
178
- coerce = method(:coerce_array)
179
- coerce_int = method(:coerce_integer)
180
- server.define_tool(
181
- name: 'search',
182
- description: 'Search code units by pattern. Matches against identifiers by default; can also search source_code and metadata fields.',
183
- input_schema: {
184
- properties: {
185
- query: { type: 'string', description: 'Search pattern (case-insensitive regex)' },
186
- types: {
187
- type: 'array', items: { type: 'string' },
188
- description: 'Filter to these types: model, controller, service, job, mailer, etc.'
189
- },
190
- fields: {
191
- type: 'array', items: { type: 'string' },
192
- description: 'Fields to search: identifier, source_code, metadata. Default: [identifier]'
193
- },
194
- limit: { type: 'integer', description: 'Maximum results (default: 20)' }
195
- },
196
- required: ['query']
197
- }
198
- ) do |query:, server_context:, types: nil, fields: nil, limit: nil|
199
- types = coerce.call(types)
200
- fields = coerce.call(fields)
201
- limit = coerce_int.call(limit)
202
- results = reader.search(
203
- query,
204
- types: types,
205
- fields: fields || %w[identifier],
206
- limit: limit || 20
207
- )
208
- respond.call(renderer.render(:search, {
209
- query: query,
210
- result_count: results.size,
211
- results: results
212
- }))
213
- end
214
- end
215
-
216
- def define_traversal_tool(server, reader, respond, renderer, name:, description:, reader_method:, render_key:)
217
- coerce = method(:coerce_array)
218
- coerce_int = method(:coerce_integer)
219
- server.define_tool(
220
- name: name,
221
- description: description,
222
- input_schema: {
223
- properties: {
224
- identifier: { type: 'string', description: 'Unit identifier to start from' },
225
- depth: { type: 'integer', description: 'Maximum traversal depth (default: 2)' },
226
- types: {
227
- type: 'array', items: { type: 'string' },
228
- description: 'Filter to these types'
229
- }
230
- },
231
- required: ['identifier']
232
- }
233
- ) do |identifier:, server_context:, depth: nil, types: nil|
234
- types = coerce.call(types)
235
- depth = coerce_int.call(depth)
236
- result = reader.send(reader_method, identifier, depth: depth || 2, types: types)
237
- if result[:found] == false
238
- result[:message] =
239
- "Identifier '#{identifier}' not found in the index. Use 'search' to find valid identifiers."
240
- end
241
- respond.call(renderer.render(render_key, result))
242
- end
243
- end
244
-
245
- def define_structure_tool(server, reader, respond, renderer)
246
- server.define_tool(
247
- name: 'structure',
248
- description: 'Get codebase structure overview. Returns manifest (counts, versions, git info) and optionally the full summary.',
249
- input_schema: {
250
- properties: {
251
- detail: {
252
- type: 'string', enum: %w[summary full],
253
- description: '"summary" for manifest only, "full" to include SUMMARY.md. Default: summary'
254
- }
255
- }
256
- }
257
- ) do |server_context:, detail: nil|
258
- result = { manifest: reader.manifest }
259
- result[:summary] = reader.summary if (detail || 'summary') == 'full'
260
- respond.call(renderer.render(:structure, result))
261
- end
262
- end
263
-
264
- def define_graph_analysis_tool(server, reader, respond, renderer)
265
- paginate = method(:paginate_section)
266
- coerce_int = method(:coerce_integer)
267
- server.define_tool(
268
- name: 'graph_analysis',
269
- description: 'Get structural analysis of the dependency graph: orphans, dead ends, hubs, cycles, and bridges.',
270
- input_schema: {
271
- properties: {
272
- analysis: {
273
- type: 'string',
274
- enum: %w[orphans dead_ends hubs cycles bridges all],
275
- description: 'Which analysis to return. Default: all'
276
- },
277
- limit: { type: 'integer', description: 'Limit results per section (default: 20)' },
278
- offset: { type: 'integer', description: 'Skip this many results per section (default: 0)' }
279
- }
280
- }
281
- ) do |server_context:, analysis: nil, limit: nil, offset: nil|
282
- limit = coerce_int.call(limit)
283
- offset = coerce_int.call(offset)
284
- data = reader.graph_analysis
285
- section = analysis || 'all'
286
- effective_offset = offset || 0
287
-
288
- result = if section == 'all'
289
- if limit || effective_offset.positive?
290
- truncated = data.dup
291
- %w[orphans dead_ends hubs cycles bridges].each do |key|
292
- paginate.call(truncated, key, limit, effective_offset)
293
- end
294
- truncated
295
- else
296
- data
297
- end
298
- else
299
- single = { section => data[section], 'stats' => data['stats'] }
300
- paginate.call(single, section, limit, effective_offset) if limit || effective_offset.positive?
301
- single
302
- end
303
-
304
- respond.call(renderer.render(:graph_analysis, result))
305
- end
306
- end
307
-
308
- def define_pagerank_tool(server, reader, respond, renderer)
309
- coerce = method(:coerce_array)
310
- coerce_int = method(:coerce_integer)
311
- server.define_tool(
312
- name: 'pagerank',
313
- description: 'Get PageRank importance scores for code units. Higher scores indicate more structurally important nodes.',
314
- input_schema: {
315
- properties: {
316
- limit: { type: 'integer', description: 'Maximum results to return (default: 20)' },
317
- types: {
318
- type: 'array', items: { type: 'string' },
319
- description: 'Filter to these types'
320
- }
321
- }
322
- }
323
- ) do |server_context:, limit: nil, types: nil|
324
- types = coerce.call(types)
325
- limit = coerce_int.call(limit)
326
- scores = reader.dependency_graph.pagerank
327
- graph_data = reader.raw_graph_data
328
- nodes = graph_data['nodes'] || {}
329
-
330
- type_set = types&.to_set
331
-
332
- ranked = scores
333
- .sort_by { |_id, score| -score }
334
- .filter_map do |id, score|
335
- node_type = nodes.dig(id, 'type')
336
- next if type_set && !type_set.include?(node_type)
337
-
338
- { identifier: id, type: node_type, score: score.round(6) }
339
- end
340
-
341
- effective_limit = limit || 20
342
- result = {
343
- total_nodes: scores.size,
344
- results: ranked.first(effective_limit)
345
- }
346
- respond.call(renderer.render(:pagerank, result))
347
- end
348
- end
349
-
350
- def define_framework_tool(server, reader, respond, renderer)
351
- coerce_int = method(:coerce_integer)
352
- server.define_tool(
353
- name: 'framework',
354
- description: 'Search Rails framework source units by concept keyword. Matches against identifier, ' \
355
- 'source_code, and metadata of rails_source type units extracted from installed gems.',
356
- input_schema: {
357
- properties: {
358
- keyword: { type: 'string',
359
- description: 'Concept keyword to search for (e.g. "ActiveRecord", "routing", "callbacks")' },
360
- limit: { type: 'integer', description: 'Maximum results (default: 20)' }
361
- },
362
- required: ['keyword']
363
- }
364
- ) do |keyword:, server_context:, limit: nil|
365
- limit = coerce_int.call(limit)
366
- results = reader.framework_sources(keyword, limit: limit || 20)
367
- respond.call(renderer.render(:framework, {
368
- keyword: keyword,
369
- result_count: results.size,
370
- results: results
371
- }))
372
- end
373
- end
374
-
375
- def define_recent_changes_tool(server, reader, respond, renderer)
376
- coerce = method(:coerce_array)
377
- coerce_int = method(:coerce_integer)
378
- server.define_tool(
379
- name: 'recent_changes',
380
- description: 'List recently modified code units sorted by git last_modified timestamp. ' \
381
- 'Returns the most recently changed units first.',
382
- input_schema: {
383
- properties: {
384
- limit: { type: 'integer', description: 'Maximum results (default: 10)' },
385
- types: {
386
- type: 'array', items: { type: 'string' },
387
- description: 'Filter to these types: model, controller, service, job, mailer, etc.'
388
- }
389
- }
390
- }
391
- ) do |server_context:, limit: nil, types: nil|
392
- types = coerce.call(types)
393
- limit = coerce_int.call(limit)
394
- results = reader.recent_changes(limit: limit || 10, types: types)
395
- respond.call(renderer.render(:recent_changes, {
396
- result_count: results.size,
397
- results: results
398
- }))
399
- end
400
- end
401
-
402
- def define_reload_tool(server, reader, respond)
403
- server.define_tool(
404
- name: 'reload',
405
- description: 'Reload extraction data from disk. Use after re-running extraction to pick up changes ' \
406
- 'without restarting the server.',
407
- input_schema: { type: 'object', properties: {} }
408
- ) do |server_context:|
409
- reader.reload!
410
- manifest = reader.manifest
411
- respond.call(JSON.pretty_generate({
412
- reloaded: true,
413
- extracted_at: manifest['extracted_at'],
414
- total_units: manifest['total_units'],
415
- counts: manifest['counts']
416
- }))
417
- end
418
- end
419
-
420
- def define_retrieve_tool(server, retriever, respond)
421
- coerce_int = method(:coerce_integer)
422
- server.define_tool(
423
- name: 'codebase_retrieve',
424
- description: 'Retrieve relevant codebase context for a natural language query using semantic search. ' \
425
- 'Returns ranked code units assembled into a token-budgeted context string.',
426
- input_schema: {
427
- properties: {
428
- query: { type: 'string',
429
- description: 'Natural language query (e.g. "How does user authentication work?")' },
430
- budget: { type: 'integer', description: 'Token budget for context assembly (default: 8000)' }
431
- },
432
- required: ['query']
433
- }
434
- ) do |query:, server_context:, budget: nil|
435
- budget = coerce_int.call(budget)
436
- if retriever
437
- result = retriever.retrieve(query, budget: budget || 8000)
438
- respond.call(result.context)
439
- else
440
- respond.call(
441
- 'Semantic search is not available. Embedding provider is not configured. ' \
442
- 'Use the search tool for pattern-based search instead.'
443
- )
444
- end
445
- end
446
- end
447
-
448
- def define_trace_flow_tool(server, reader, index_dir, respond, renderer)
449
- require_relative '../flow_assembler'
450
- require_relative '../dependency_graph'
451
- coerce_int = method(:coerce_integer)
452
-
453
- server.define_tool(
454
- name: 'trace_flow',
455
- description: 'Trace execution flow from an entry point through the codebase',
456
- input_schema: {
457
- properties: {
458
- entry_point: {
459
- type: 'string',
460
- description: 'Entry point (e.g., UsersController#create)'
461
- },
462
- depth: {
463
- type: 'integer',
464
- description: 'Maximum call depth to trace (default: 3)'
465
- }
466
- },
467
- required: ['entry_point']
468
- }
469
- ) do |entry_point:, server_context:, depth: nil|
470
- max_depth = coerce_int.call(depth) || 3
471
- graph = reader.dependency_graph
472
-
473
- assembler = CodebaseIndex::FlowAssembler.new(
474
- graph: graph,
475
- extracted_dir: index_dir
476
- )
477
- flow_doc = assembler.assemble(entry_point, max_depth: max_depth)
478
-
479
- respond.call(renderer.render(:trace_flow, flow_doc.to_h))
480
- rescue StandardError => e
481
- respond.call(JSON.pretty_generate({ error: e.message }))
482
- end
483
- end
484
-
485
- def define_session_trace_tool(server, reader, respond)
486
- coerce_int = method(:coerce_integer)
487
- server.define_tool(
488
- name: 'session_trace',
489
- description: 'Assemble context from a browser session trace (requires session tracer middleware)',
490
- input_schema: {
491
- properties: {
492
- session_id: { type: 'string', description: 'Session ID to trace' },
493
- budget: { type: 'integer', description: 'Max token budget (default: 8000)' },
494
- depth: { type: 'integer', description: 'Dependency resolution depth (default: 1)' }
495
- },
496
- required: ['session_id']
497
- }
498
- ) do |session_id:, server_context:, budget: nil, depth: nil|
499
- budget = coerce_int.call(budget)
500
- depth = coerce_int.call(depth)
501
- store = CodebaseIndex.configuration.session_store
502
- next respond.call(JSON.pretty_generate({ error: 'Session tracer not configured' })) unless store
503
-
504
- require_relative '../session_tracer/session_flow_assembler'
505
-
506
- assembler = CodebaseIndex::SessionTracer::SessionFlowAssembler.new(
507
- store: store, reader: reader
508
- )
509
- doc = assembler.assemble(session_id, budget: budget || 8000, depth: depth || 1)
510
- respond.call(doc.to_markdown)
511
- rescue StandardError => e
512
- respond.call(JSON.pretty_generate({ error: e.message }))
513
- end
514
- end
515
-
516
- def define_operator_tools(server, operator, respond)
517
- define_pipeline_extract_tool(server, operator, respond)
518
- define_pipeline_embed_tool(server, operator, respond)
519
- define_pipeline_status_tool(server, operator, respond)
520
- define_pipeline_diagnose_tool(server, operator, respond)
521
- define_pipeline_repair_tool(server, operator, respond)
522
- end
523
-
524
- def define_feedback_tools(server, feedback_store, respond)
525
- define_retrieval_rate_tool(server, feedback_store, respond)
526
- define_retrieval_report_gap_tool(server, feedback_store, respond)
527
- define_retrieval_explain_tool(server, feedback_store, respond)
528
- define_retrieval_suggest_tool(server, feedback_store, respond)
529
- end
530
-
531
- def define_pipeline_extract_tool(server, operator, respond)
532
- server.define_tool(
533
- name: 'pipeline_extract',
534
- description: 'Trigger a codebase extraction pipeline run. Checks rate limits before proceeding.',
535
- input_schema: {
536
- properties: {
537
- incremental: { type: 'boolean', description: 'Run incremental extraction (default: false)' }
538
- }
539
- }
540
- ) do |server_context:, incremental: nil|
541
- next respond.call('Pipeline operator is not configured.') unless operator
542
-
543
- guard = operator[:pipeline_guard]
544
- next respond.call('Extraction is rate-limited. Try again later.') if guard && !guard.allow?(:extraction)
545
-
546
- guard&.record!(:extraction)
547
-
548
- Thread.new do
549
- extractor = CodebaseIndex::Extractor.new(
550
- output_dir: CodebaseIndex.configuration.output_dir
551
- )
552
- incremental ? extractor.extract_changed([]) : extractor.extract_all
553
- rescue StandardError => e
554
- logger = defined?(Rails) ? Rails.logger : Logger.new($stderr)
555
- logger.error("[CodebaseIndex] Pipeline extract failed: #{e.message}")
556
- end
557
-
558
- respond.call(JSON.pretty_generate({
559
- status: 'started',
560
- message: 'Extraction pipeline started in background thread'
561
- }))
562
- end
563
- end
564
-
565
- def define_pipeline_embed_tool(server, operator, respond)
566
- server.define_tool(
567
- name: 'pipeline_embed',
568
- description: 'Trigger embedding generation for extracted units. Checks rate limits before proceeding.',
569
- input_schema: {
570
- properties: {
571
- incremental: { type: 'boolean', description: 'Embed only new/changed units (default: false)' }
572
- }
573
- }
574
- ) do |server_context:, incremental: nil|
575
- next respond.call('Pipeline operator is not configured.') unless operator
576
-
577
- guard = operator[:pipeline_guard]
578
- next respond.call('Embedding is rate-limited. Try again later.') if guard && !guard.allow?(:embedding)
579
-
580
- guard&.record!(:embedding)
581
-
582
- Thread.new do
583
- config = CodebaseIndex.configuration
584
- builder = CodebaseIndex::Builder.new(config)
585
- provider = builder.build_embedding_provider
586
- text_preparer = CodebaseIndex::Embedding::TextPreparer.new
587
- vector_store = builder.build_vector_store
588
- indexer = CodebaseIndex::Embedding::Indexer.new(
589
- provider: provider,
590
- text_preparer: text_preparer,
591
- vector_store: vector_store,
592
- output_dir: config.output_dir
593
- )
594
- incremental ? indexer.index_incremental : indexer.index_all
595
- rescue StandardError => e
596
- logger = defined?(Rails) ? Rails.logger : Logger.new($stderr)
597
- logger.error("[CodebaseIndex] Pipeline embed failed: #{e.message}")
598
- end
599
-
600
- respond.call(JSON.pretty_generate({
601
- status: 'started',
602
- message: 'Embedding pipeline started in background thread'
603
- }))
604
- end
605
- end
606
-
607
- def define_pipeline_status_tool(server, operator, respond)
608
- server.define_tool(
609
- name: 'pipeline_status',
610
- description: 'Get the current pipeline status: last extraction time, unit counts, staleness.',
611
- input_schema: { type: 'object', properties: {} }
612
- ) do |server_context:|
613
- next respond.call('Pipeline operator is not configured.') unless operator
614
-
615
- reporter = operator[:status_reporter]
616
- next respond.call('Status reporter is not configured.') unless reporter
617
-
618
- status = reporter.report
619
- respond.call(JSON.pretty_generate(status))
620
- end
621
- end
622
-
623
- def define_pipeline_diagnose_tool(server, operator, respond)
624
- server.define_tool(
625
- name: 'pipeline_diagnose',
626
- description: 'Classify a recent pipeline error and suggest remediation.',
627
- input_schema: {
628
- properties: {
629
- error_class: { type: 'string', description: 'Error class name (e.g. "Timeout::Error")' },
630
- error_message: { type: 'string', description: 'Error message' }
631
- },
632
- required: %w[error_class error_message]
633
- }
634
- ) do |error_class:, error_message:, server_context:|
635
- next respond.call('Pipeline operator is not configured.') unless operator
636
-
637
- escalator = operator[:error_escalator]
638
- next respond.call('Error escalator is not configured.') unless escalator
639
-
640
- error = StandardError.new(error_message)
641
- # Set the class name in the error string for pattern matching
642
- result = escalator.classify(error)
643
- result[:original_class] = error_class
644
- respond.call(JSON.pretty_generate(result))
645
- end
646
- end
647
-
648
- def define_pipeline_repair_tool(server, operator, respond)
649
- server.define_tool(
650
- name: 'pipeline_repair',
651
- description: 'Attempt to repair pipeline state: clear stale locks, reset rate limits.',
652
- input_schema: {
653
- properties: {
654
- action: {
655
- type: 'string',
656
- enum: %w[clear_locks reset_cooldowns],
657
- description: 'Repair action to perform'
658
- }
659
- },
660
- required: ['action']
661
- }
662
- ) do |action:, server_context:|
663
- next respond.call('Pipeline operator is not configured.') unless operator
664
-
665
- case action
666
- when 'clear_locks'
667
- lock = operator[:pipeline_lock]
668
- if lock
669
- lock.release
670
- respond.call(JSON.pretty_generate({ repaired: true, action: 'clear_locks' }))
671
- else
672
- respond.call('Pipeline lock is not configured.')
673
- end
674
- when 'reset_cooldowns'
675
- respond.call(JSON.pretty_generate({ repaired: true, action: 'reset_cooldowns' }))
676
- else
677
- respond.call("Unknown repair action: #{action}")
678
- end
679
- end
680
- end
681
-
682
- def define_retrieval_rate_tool(server, feedback_store, respond)
683
- coerce_int = method(:coerce_integer)
684
- server.define_tool(
685
- name: 'retrieval_rate',
686
- description: 'Record a quality rating for a retrieval result (1-5 scale).',
687
- input_schema: {
688
- properties: {
689
- query: { type: 'string', description: 'The query that was used' },
690
- score: { type: 'integer', description: 'Rating 1-5' },
691
- comment: { type: 'string', description: 'Optional comment' }
692
- },
693
- required: %w[query score]
694
- }
695
- ) do |query:, score:, server_context:, comment: nil|
696
- next respond.call('Feedback store is not configured.') unless feedback_store
697
-
698
- score = coerce_int.call(score)
699
- feedback_store.record_rating(query: query, score: score, comment: comment)
700
- respond.call(JSON.pretty_generate({ recorded: true, type: 'rating', query: query, score: score }))
701
- end
702
- end
703
-
704
- def define_retrieval_report_gap_tool(server, feedback_store, respond)
705
- server.define_tool(
706
- name: 'retrieval_report_gap',
707
- description: 'Report a missing unit that should have appeared in retrieval results.',
708
- input_schema: {
709
- properties: {
710
- query: { type: 'string', description: 'The query that had poor results' },
711
- missing_unit: { type: 'string', description: 'Identifier of the expected unit' },
712
- unit_type: { type: 'string', description: 'Type of the missing unit (model, service, etc.)' }
713
- },
714
- required: %w[query missing_unit unit_type]
715
- }
716
- ) do |query:, missing_unit:, unit_type:, server_context:|
717
- next respond.call('Feedback store is not configured.') unless feedback_store
718
-
719
- feedback_store.record_gap(query: query, missing_unit: missing_unit, unit_type: unit_type)
720
- respond.call(JSON.pretty_generate({
721
- recorded: true,
722
- type: 'gap',
723
- missing_unit: missing_unit
724
- }))
725
- end
726
- end
727
-
728
- def define_retrieval_explain_tool(server, feedback_store, respond)
729
- server.define_tool(
730
- name: 'retrieval_explain',
731
- description: 'Get feedback statistics: average score, total ratings, gap count.',
732
- input_schema: { type: 'object', properties: {} }
733
- ) do |server_context:|
734
- next respond.call('Feedback store is not configured.') unless feedback_store
735
-
736
- ratings = feedback_store.ratings
737
- gaps = feedback_store.gaps
738
- respond.call(JSON.pretty_generate({
739
- total_ratings: ratings.size,
740
- average_score: feedback_store.average_score,
741
- total_gaps: gaps.size,
742
- recent_ratings: ratings.last(5),
743
- recent_gaps: gaps.last(5)
744
- }))
745
- end
746
- end
747
-
748
- def define_retrieval_suggest_tool(server, feedback_store, respond)
749
- server.define_tool(
750
- name: 'retrieval_suggest',
751
- description: 'Analyze feedback to suggest improvements: detect patterns in low scores and missing units.',
752
- input_schema: { type: 'object', properties: {} }
753
- ) do |server_context:|
754
- next respond.call('Feedback store is not configured.') unless feedback_store
755
-
756
- require_relative '../feedback/gap_detector'
757
- detector = CodebaseIndex::Feedback::GapDetector.new(feedback_store: feedback_store)
758
- issues = detector.detect
759
- respond.call(JSON.pretty_generate({
760
- issues_found: issues.size,
761
- issues: issues
762
- }))
763
- end
764
- end
765
-
766
- def define_snapshot_tools(server, snapshot_store, respond)
767
- define_list_snapshots_tool(server, snapshot_store, respond)
768
- define_snapshot_diff_tool(server, snapshot_store, respond)
769
- define_unit_history_tool(server, snapshot_store, respond)
770
- define_snapshot_detail_tool(server, snapshot_store, respond)
771
- end
772
-
773
- def define_list_snapshots_tool(server, snapshot_store, respond)
774
- coerce_int = method(:coerce_integer)
775
- server.define_tool(
776
- name: 'list_snapshots',
777
- description: 'List temporal snapshots of past extraction runs, optionally filtered by branch.',
778
- input_schema: {
779
- properties: {
780
- limit: { type: 'integer', description: 'Maximum results (default: 20)' },
781
- branch: { type: 'string', description: 'Filter to this branch name' }
782
- }
783
- }
784
- ) do |server_context:, limit: nil, branch: nil|
785
- next respond.call('Snapshot store is not configured. Set enable_snapshots: true.') unless snapshot_store
786
-
787
- limit = coerce_int.call(limit)
788
- results = snapshot_store.list(limit: limit || 20, branch: branch)
789
- respond.call(JSON.pretty_generate({ snapshot_count: results.size, snapshots: results }))
790
- end
791
- end
792
-
793
- def define_snapshot_diff_tool(server, snapshot_store, respond)
794
- server.define_tool(
795
- name: 'snapshot_diff',
796
- description: 'Compare two extraction snapshots by git SHA. Returns lists of added, modified, and deleted units.',
797
- input_schema: {
798
- properties: {
799
- sha_a: { type: 'string', description: 'Git SHA of the "before" snapshot' },
800
- sha_b: { type: 'string', description: 'Git SHA of the "after" snapshot' }
801
- },
802
- required: %w[sha_a sha_b]
803
- }
804
- ) do |sha_a:, sha_b:, server_context:|
805
- next respond.call('Snapshot store is not configured. Set enable_snapshots: true.') unless snapshot_store
806
-
807
- result = snapshot_store.diff(sha_a, sha_b)
808
- respond.call(JSON.pretty_generate({
809
- sha_a: sha_a, sha_b: sha_b,
810
- added: result[:added].size,
811
- modified: result[:modified].size,
812
- deleted: result[:deleted].size,
813
- details: result
814
- }))
815
- end
816
- end
817
-
818
- def define_unit_history_tool(server, snapshot_store, respond)
819
- coerce_int = method(:coerce_integer)
820
- server.define_tool(
821
- name: 'unit_history',
822
- description: 'Show the history of a single unit across extraction snapshots. Tracks when source changed.',
823
- input_schema: {
824
- properties: {
825
- identifier: { type: 'string', description: 'Unit identifier (e.g. "User", "PostsController")' },
826
- limit: { type: 'integer', description: 'Maximum entries (default: 20)' }
827
- },
828
- required: ['identifier']
829
- }
830
- ) do |identifier:, server_context:, limit: nil|
831
- next respond.call('Snapshot store is not configured. Set enable_snapshots: true.') unless snapshot_store
832
-
833
- limit = coerce_int.call(limit)
834
- entries = snapshot_store.unit_history(identifier, limit: limit || 20)
835
- respond.call(JSON.pretty_generate({
836
- identifier: identifier,
837
- versions: entries.size,
838
- history: entries
839
- }))
840
- end
841
- end
842
-
843
- def define_snapshot_detail_tool(server, snapshot_store, respond)
844
- server.define_tool(
845
- name: 'snapshot_detail',
846
- description: 'Get full metadata for a specific extraction snapshot by git SHA.',
847
- input_schema: {
848
- properties: {
849
- git_sha: { type: 'string', description: 'Git SHA of the snapshot' }
850
- },
851
- required: ['git_sha']
852
- }
853
- ) do |git_sha:, server_context:|
854
- next respond.call('Snapshot store is not configured. Set enable_snapshots: true.') unless snapshot_store
855
-
856
- snapshot = snapshot_store.find(git_sha)
857
- if snapshot
858
- respond.call(JSON.pretty_generate(snapshot))
859
- else
860
- respond.call("Snapshot not found for git SHA: #{git_sha}")
861
- end
862
- end
863
- end
864
-
865
- def define_notion_sync_tool(server, reader, index_dir, respond)
866
- server.define_tool(
867
- name: 'notion_sync',
868
- description: 'Sync extracted codebase data (Data Models + Columns) to Notion databases. ' \
869
- 'Requires notion_api_token and notion_database_ids to be configured.',
870
- input_schema: {
871
- type: 'object',
872
- properties: {}
873
- }
874
- ) do |server_context:|
875
- config = CodebaseIndex.configuration
876
- unless config.notion_api_token
877
- next respond.call('Error: notion_api_token is not configured. Set it in CodebaseIndex.configure.')
878
- end
879
-
880
- if (config.notion_database_ids || {}).empty?
881
- next respond.call('Error: notion_database_ids is not configured. Set it in CodebaseIndex.configure.')
882
- end
883
-
884
- require_relative '../notion/exporter'
885
- exporter = CodebaseIndex::Notion::Exporter.new(index_dir: index_dir, reader: reader)
886
- stats = exporter.sync_all
887
-
888
- respond.call(JSON.pretty_generate({
889
- synced: true,
890
- data_models: stats[:data_models],
891
- columns: stats[:columns],
892
- errors: stats[:errors].first(10)
893
- }))
894
- rescue StandardError => e
895
- respond.call("Notion sync failed: #{e.message}")
896
- end
897
- end
898
-
899
- def build_resource_templates
900
- [
901
- ::MCP::ResourceTemplate.new(
902
- uri_template: 'codebase://unit/{identifier}',
903
- name: 'unit',
904
- description: 'Look up a single code unit by identifier',
905
- mime_type: 'application/json'
906
- ),
907
- ::MCP::ResourceTemplate.new(
908
- uri_template: 'codebase://type/{type}',
909
- name: 'units-by-type',
910
- description: 'List all code units of a given type (e.g. model, controller, service)',
911
- mime_type: 'application/json'
912
- )
913
- ]
914
- end
915
-
916
- def build_resources
917
- [
918
- ::MCP::Resource.new(
919
- uri: 'codebase://manifest',
920
- name: 'manifest',
921
- description: 'Extraction manifest with version info, unit counts, and git metadata',
922
- mime_type: 'application/json'
923
- ),
924
- ::MCP::Resource.new(
925
- uri: 'codebase://graph',
926
- name: 'dependency-graph',
927
- description: 'Full dependency graph with nodes, edges, and type index',
928
- mime_type: 'application/json'
929
- )
930
- ]
931
- end
932
-
933
- def register_resource_handler(server, reader)
934
- server.resources_read_handler do |params|
935
- uri = params[:uri]
936
- case uri
937
- when 'codebase://manifest'
938
- [{ uri: uri, mimeType: 'application/json', text: JSON.pretty_generate(reader.manifest) }]
939
- when 'codebase://graph'
940
- [{ uri: uri, mimeType: 'application/json', text: JSON.pretty_generate(reader.raw_graph_data) }]
941
- when %r{\Acodebase://unit/(.+)\z}
942
- identifier = Regexp.last_match(1)
943
- unit = reader.find_unit(identifier)
944
- if unit
945
- [{ uri: uri, mimeType: 'application/json', text: JSON.pretty_generate(unit) }]
946
- else
947
- [{ uri: uri, mimeType: 'text/plain', text: "Unit not found: #{identifier}" }]
948
- end
949
- when %r{\Acodebase://type/(.+)\z}
950
- type = Regexp.last_match(1)
951
- units = reader.list_units(type: type)
952
- [{ uri: uri, mimeType: 'application/json', text: JSON.pretty_generate(units) }]
953
- else
954
- [{ uri: uri, mimeType: 'text/plain', text: "Unknown resource: #{uri}" }]
955
- end
956
- end
957
- end
958
- end
959
- end
960
- end
961
- end