rails-ai-context 3.0.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +30 -7
- data/CLAUDE.md +2 -2
- data/README.md +5 -1
- data/lib/rails_ai_context/cli/tool_runner.rb +20 -10
- data/lib/rails_ai_context/tools/base_tool.rb +14 -3
- data/lib/rails_ai_context/tools/get_edit_context.rb +5 -1
- data/lib/rails_ai_context/tools/get_model_details.rb +2 -1
- data/lib/rails_ai_context/tools/get_partial_interface.rb +16 -1
- data/lib/rails_ai_context/tools/get_routes.rb +4 -3
- data/lib/rails_ai_context/tools/get_schema.rb +7 -2
- data/lib/rails_ai_context/tools/get_stimulus.rb +6 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +13 -2
- data/lib/rails_ai_context/tools/get_view.rb +17 -4
- data/lib/rails_ai_context/tools/validate.rb +22 -2
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +3 -3
- metadata +11 -9
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: a6bdb0dcbde3c7abfab69bee14e661afc8a260fe3d00faa99ecc4becbc58a19b
|
|
4
|
+
data.tar.gz: deacfa10c6c188d0482fc82199f085b4f13cbf24b0cf06e0977074ecf67864d8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: bf049844c52cd8b88371a352907b08c9863668424ae37f9393d72c7210501c21fcc2ea3df797e59184b603b98e723f0bd07ebb4e36503852344c27d96970a308
|
|
7
|
+
data.tar.gz: ab5cf1f7f4031d605db8f076da5544ede9c1aa48d426dc57482a45551d46741c687caaaf0e8722932d8d982c6a9af5c86f425e117a80bcb88b36d2d1d761040f
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [3.1.0] - 2026-03-26
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **Consistent input normalization across all tools** — AI agents and humans can now use any casing or format and tools resolve correctly:
|
|
13
|
+
- `model=brand_profile` (snake_case) now resolves to `BrandProfile` via `.underscore` comparison in `get_model_details`.
|
|
14
|
+
- `table=Cook` (model name) now resolves to `cooks` table via `.underscore.pluralize` normalization in `get_schema`.
|
|
15
|
+
- `controller=CooksController` now works in `get_view` and `get_routes` — both strip `Controller`/`_controller` suffix consistently, matching `get_controllers` behavior.
|
|
16
|
+
- `controller=cooks_controller` no longer leaves a trailing underscore in route matching.
|
|
17
|
+
- `stimulus=CookStatus` (PascalCase) now resolves to `cook_status` via `.underscore` conversion in `get_stimulus`.
|
|
18
|
+
- `partial=_status_badge` (underscore-prefixed, no directory) now searches recursively across all view directories in `get_partial_interface`.
|
|
19
|
+
- `model=cooks` (plural) now tries `.singularize` for test file lookup in `get_test_info`.
|
|
20
|
+
- **Smarter fuzzy matching** — `BaseTool.find_closest_match` now prefers shortest substring match (so `Cook` suggests `cooks`, not `cook_comments`) and supports underscore/classify variant matching.
|
|
21
|
+
- **File path suggestions in validate** — `files=["cook.rb"]` now suggests `app/models/cook.rb` when the file isn't found at the given path.
|
|
22
|
+
- **Empty parameter validation** — `edit_context` now returns friendly messages for empty `file` or `near` parameters instead of hard errors.
|
|
23
|
+
|
|
8
24
|
## [3.0.0] - 2026-03-26
|
|
9
25
|
|
|
10
26
|
### Removed
|
|
@@ -13,17 +29,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
13
29
|
|
|
14
30
|
### Added
|
|
15
31
|
|
|
16
|
-
- **CLI tool support** — all 25 MCP tools can now be run from the terminal
|
|
17
|
-
- **`tool_mode` config** —
|
|
18
|
-
- **
|
|
19
|
-
- **
|
|
32
|
+
- **CLI tool support** — all 25 MCP tools can now be run from the terminal: `rails 'ai:tool[schema]' table=users detail=full`. Also via Thor CLI: `rails-ai-context tool schema --table users`. `rails ai:tool` lists all tools. `--help` shows per-tool help auto-generated from input_schema. `--json` / `JSON=1` for JSON envelope. Tool name resolution: `schema` → `get_schema` → `rails_get_schema`.
|
|
33
|
+
- **`tool_mode` config** — `:mcp` (default, MCP primary + CLI fallback) or `:cli` (CLI only, no MCP server needed). Selected during install and first `rails ai:context` run.
|
|
34
|
+
- **ToolRunner** — `lib/rails_ai_context/cli/tool_runner.rb` handles CLI tool execution: arg parsing, type coercion from input_schema, required param validation, enum checking, fuzzy tool name suggestions on typos.
|
|
35
|
+
- **ToolGuideHelper** — shared serializer module renders tool reference sections with MCP or CLI syntax based on `tool_mode`, with MANDATORY enforcement + CLI escape hatch. 3-column tool table (MCP | CLI | description).
|
|
36
|
+
- **Copilot `excludeAgent`** — MCP tools instruction file uses `excludeAgent: "code-review"` (code review can't invoke MCP tools, saves 4K char budget).
|
|
37
|
+
- **`.mcp.json` auto-create** — `rails ai:context` automatically creates `.mcp.json` when `tool_mode` is `:mcp` and the file doesn't exist. Existing apps upgrading to v3.0.0 get it without re-running the install generator.
|
|
38
|
+
- **Full config initializer** — generated initializer documents every configuration option organized by section (AI Tools, Introspection, Models & Filtering, MCP Server, File Size Limits, Extensibility, Security, Search).
|
|
39
|
+
- **Cursor MDC compliance spec** — 26 tests validating MDC format: frontmatter fields, rule types, glob syntax, line limits.
|
|
40
|
+
- **Copilot compliance spec** — 25 tests validating instruction format: applyTo, excludeAgent, file naming, content quality.
|
|
20
41
|
|
|
21
42
|
### Changed
|
|
22
43
|
|
|
23
44
|
- Serializer count reduced from 6 to 5 (Claude, Cursor, Copilot, OpenCode, JSON).
|
|
24
|
-
- Install generator
|
|
25
|
-
-
|
|
26
|
-
-
|
|
45
|
+
- Install generator renumbered (4 AI tool options instead of 5) + MCP opt-in step.
|
|
46
|
+
- Cursor glob-based rules no longer combine `globs` + `description` (pure Type 2 auto-attach per Cursor best practices).
|
|
47
|
+
- MCP tool instructions use MANDATORY enforcement with CLI escape hatch — AI agents use tools when available, fall back to CLI or file reading when not.
|
|
48
|
+
- All CLI examples use zsh-safe quoting: `rails 'ai:tool[X]'` (brackets are glob patterns in zsh).
|
|
49
|
+
- README rewritten with real-world workflow examples, categorized tool table, MCP vs CLI showcase.
|
|
27
50
|
|
|
28
51
|
## [2.0.5] - 2026-03-25
|
|
29
52
|
|
data/CLAUDE.md
CHANGED
|
@@ -11,7 +11,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
11
11
|
- `lib/rails_ai_context/introspectors/` — 29 introspectors (schema, models, routes, jobs, gems, conventions, stimulus, database_stats, controllers, views, view_templates, design_tokens, turbo, i18n, config, active_storage, action_text, auth, api, tests, rake_tasks, assets, devops, action_mailbox, migrations, seeds, middleware, engines, multi_database)
|
|
12
12
|
- `lib/rails_ai_context/tools/` — 25 MCP tools using the official mcp SDK
|
|
13
13
|
- `lib/rails_ai_context/cli/` — CLI tool runner (`tool_runner.rb`) — executes MCP tools from rake/Thor
|
|
14
|
-
- `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, copilot, copilot_instructions, rules, markdown, JSON, context_file_serializer, test_command_detection, tool_guide_helper)
|
|
14
|
+
- `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, copilot, copilot_instructions, rules, markdown, JSON, context_file_serializer, test_command_detection, tool_guide_helper, design_system_helper, stack_overview_helper)
|
|
15
15
|
- `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
|
|
16
16
|
- `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
|
|
17
17
|
- `lib/rails_ai_context/middleware.rb` — Rack middleware for auto-mounting MCP HTTP endpoint
|
|
@@ -51,7 +51,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
51
51
|
## Testing
|
|
52
52
|
|
|
53
53
|
```bash
|
|
54
|
-
bundle exec rspec # Run specs (
|
|
54
|
+
bundle exec rspec # Run specs (681 examples)
|
|
55
55
|
bundle exec rubocop # Lint
|
|
56
56
|
```
|
|
57
57
|
|
data/README.md
CHANGED
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
|
|
10
10
|
**Works with:** Claude Code • Cursor • GitHub Copilot • OpenCode • Any terminal
|
|
11
11
|
|
|
12
|
+
> Built by a Rails developer with 10+ years of production experience. AI assisted — the same way it assists me shipping features at work. I designed the architecture, made every decision, reviewed every line, and wrote 681 tests. This gem exists because I understand Rails deeply enough to know exactly what AI agents get wrong and what context they need to get it right.
|
|
13
|
+
|
|
12
14
|
```bash
|
|
13
15
|
gem "rails-ai-context", group: :development
|
|
14
16
|
rails generate rails_ai_context:install
|
|
@@ -16,6 +18,8 @@ rails generate rails_ai_context:install
|
|
|
16
18
|
|
|
17
19
|
That's it. Your AI now has 25 tools that understand your entire Rails app — via MCP server or CLI. Zero config.
|
|
18
20
|
|
|
21
|
+
> **[Full Guide →](docs/GUIDE.md)** — every command, every parameter, every configuration option.
|
|
22
|
+
|
|
19
23
|
---
|
|
20
24
|
|
|
21
25
|
## Two ways to use it
|
|
@@ -336,7 +340,7 @@ end
|
|
|
336
340
|
```bash
|
|
337
341
|
git clone https://github.com/crisnahine/rails-ai-context.git
|
|
338
342
|
cd rails-ai-context && bundle install
|
|
339
|
-
bundle exec rspec #
|
|
343
|
+
bundle exec rspec # 681 examples
|
|
340
344
|
bundle exec rubocop # Lint
|
|
341
345
|
```
|
|
342
346
|
|
|
@@ -219,27 +219,37 @@ module RailsAiContext
|
|
|
219
219
|
end
|
|
220
220
|
|
|
221
221
|
# Validate kwargs against the tool's input_schema.
|
|
222
|
+
# For missing required params: strip empty values so the tool's own guards
|
|
223
|
+
# can return a friendly response (matching MCP behavior).
|
|
224
|
+
# For invalid enums: strip the bad value and let the tool use its default.
|
|
222
225
|
def validate_kwargs!(kwargs, schema)
|
|
223
226
|
properties = schema[:properties] || {}
|
|
224
227
|
required = (schema[:required] || []).map(&:to_s)
|
|
225
228
|
|
|
226
|
-
#
|
|
229
|
+
# For required params with empty-string values, keep the key but set to nil
|
|
230
|
+
# so the tool's own guards can return friendly "parameter is required" messages
|
|
231
|
+
# (matching MCP behavior). We keep the key to avoid Ruby ArgumentError on
|
|
232
|
+
# required keyword arguments.
|
|
227
233
|
required.each do |param|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
234
|
+
if kwargs.key?(param.to_sym) && kwargs[param.to_sym].to_s.strip == ""
|
|
235
|
+
kwargs[param.to_sym] = nil
|
|
236
|
+
elsif !kwargs.key?(param.to_sym)
|
|
237
|
+
kwargs[param.to_sym] = nil
|
|
232
238
|
end
|
|
233
239
|
end
|
|
234
240
|
|
|
235
|
-
# Check enum constraints
|
|
241
|
+
# Check enum constraints — downcase before comparing for case-insensitive match.
|
|
242
|
+
# If invalid, strip the param so the tool uses its default behavior.
|
|
236
243
|
kwargs.each do |key, value|
|
|
237
244
|
prop = properties[key]
|
|
238
245
|
next unless prop&.dig(:enum)
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
246
|
+
|
|
247
|
+
# Try case-insensitive match first
|
|
248
|
+
matched = prop[:enum].find { |e| e.to_s.downcase == value.to_s.downcase }
|
|
249
|
+
if matched
|
|
250
|
+
kwargs[key] = matched
|
|
251
|
+
else
|
|
252
|
+
kwargs.delete(key)
|
|
243
253
|
end
|
|
244
254
|
end
|
|
245
255
|
end
|
|
@@ -65,13 +65,24 @@ module RailsAiContext
|
|
|
65
65
|
text_response(lines.join("\n"))
|
|
66
66
|
end
|
|
67
67
|
|
|
68
|
-
#
|
|
68
|
+
# Fuzzy match: find the closest available name by exact, underscore, substring, or prefix
|
|
69
69
|
def find_closest_match(input, available)
|
|
70
70
|
return nil if available.empty?
|
|
71
71
|
downcased = input.downcase
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
underscored = input.underscore.downcase
|
|
73
|
+
|
|
74
|
+
# Exact case-insensitive match (including underscore/classify variants)
|
|
75
|
+
exact = available.find do |a|
|
|
76
|
+
a_down = a.downcase
|
|
77
|
+
a_under = a.underscore.downcase
|
|
78
|
+
a_down == downcased || a_under == underscored || a_down == underscored || a_under == downcased
|
|
79
|
+
end
|
|
74
80
|
return exact if exact
|
|
81
|
+
|
|
82
|
+
# Substring match — prefer shortest (most specific) to avoid cook → cook_comments
|
|
83
|
+
substring_matches = available.select { |a| a.downcase.include?(downcased) || downcased.include?(a.downcase) }
|
|
84
|
+
return substring_matches.min_by(&:length) if substring_matches.any?
|
|
85
|
+
|
|
75
86
|
# Prefix match
|
|
76
87
|
available.find { |a| a.downcase.start_with?(downcased[0..2]) }
|
|
77
88
|
end
|
|
@@ -36,7 +36,11 @@ module RailsAiContext
|
|
|
36
36
|
SENSITIVE_PATTERNS = nil # uses configuration.sensitive_patterns
|
|
37
37
|
|
|
38
38
|
def self.call(file:, near:, context_lines: 5, server_context: nil)
|
|
39
|
-
# Reject empty
|
|
39
|
+
# Reject empty parameters
|
|
40
|
+
if file.nil? || file.strip.empty?
|
|
41
|
+
return text_response("The `file` parameter is required. Provide a path relative to Rails root (e.g. 'app/models/cook.rb').")
|
|
42
|
+
end
|
|
43
|
+
|
|
40
44
|
if near.nil? || near.strip.empty?
|
|
41
45
|
return text_response("The `near` parameter is required. Provide a method name, keyword, or string to find.")
|
|
42
46
|
end
|
|
@@ -40,7 +40,8 @@ module RailsAiContext
|
|
|
40
40
|
# Specific model — always full detail (strip whitespace for fuzzy input)
|
|
41
41
|
if model
|
|
42
42
|
model = model.strip
|
|
43
|
-
|
|
43
|
+
model_under = model.underscore
|
|
44
|
+
key = models.keys.find { |k| k.downcase == model.downcase || k.underscore == model_under } || model
|
|
44
45
|
data = models[key]
|
|
45
46
|
unless data
|
|
46
47
|
return not_found_response("Model", model, models.keys.sort,
|
|
@@ -201,6 +201,7 @@ module RailsAiContext
|
|
|
201
201
|
|
|
202
202
|
# Resolve a partial reference to an actual file path on disk.
|
|
203
203
|
# Handles both underscore-prefixed filenames and non-prefixed input.
|
|
204
|
+
# Falls back to recursive search when no directory is specified.
|
|
204
205
|
private_class_method def self.resolve_partial_path(views_dir, partial)
|
|
205
206
|
# Normalize: strip leading underscore from basename if provided
|
|
206
207
|
parts = partial.split("/")
|
|
@@ -211,10 +212,11 @@ module RailsAiContext
|
|
|
211
212
|
prefixed_basename = basename.start_with?("_") ? basename : "_#{basename}"
|
|
212
213
|
unprefixed_basename = basename.delete_prefix("_")
|
|
213
214
|
|
|
215
|
+
extensions = %w[.html.erb .erb .html.haml .haml .html.slim .slim]
|
|
214
216
|
candidates = []
|
|
215
217
|
|
|
216
218
|
# Try prefixed name with various extensions
|
|
217
|
-
|
|
219
|
+
extensions.each do |ext|
|
|
218
220
|
candidates << File.join(views_dir, *dir_parts, "#{prefixed_basename}#{ext}")
|
|
219
221
|
candidates << File.join(views_dir, *dir_parts, "#{unprefixed_basename}#{ext}")
|
|
220
222
|
end
|
|
@@ -223,6 +225,19 @@ module RailsAiContext
|
|
|
223
225
|
candidates << File.join(views_dir, partial)
|
|
224
226
|
|
|
225
227
|
found = candidates.find { |c| File.exist?(c) }
|
|
228
|
+
|
|
229
|
+
# Fallback: if no directory was specified and direct lookup failed,
|
|
230
|
+
# search recursively for the partial across all view directories
|
|
231
|
+
if found.nil? && dir_parts.empty?
|
|
232
|
+
extensions.each do |ext|
|
|
233
|
+
matches = Dir.glob(File.join(views_dir, "**", "#{prefixed_basename}#{ext}"))
|
|
234
|
+
if matches.any?
|
|
235
|
+
found = matches.first
|
|
236
|
+
break
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
226
241
|
return nil unless found
|
|
227
242
|
|
|
228
243
|
# Path traversal protection
|
|
@@ -53,10 +53,11 @@ module RailsAiContext
|
|
|
53
53
|
by_controller = by_controller.reject { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
|
|
54
54
|
end
|
|
55
55
|
|
|
56
|
-
# Filter by controller — accepts
|
|
56
|
+
# Filter by controller — accepts "cooks", "CooksController", "cooks_controller", "Api::V1::Posts"
|
|
57
57
|
if controller
|
|
58
|
-
normalized = controller.
|
|
59
|
-
|
|
58
|
+
normalized = controller.underscore.delete_suffix("_controller")
|
|
59
|
+
normalized_alt = controller.downcase.delete_suffix("_controller").delete_suffix("controller")
|
|
60
|
+
filtered = by_controller.select { |k, _| k.downcase.include?(normalized) || k.downcase.include?(normalized_alt) }
|
|
60
61
|
return text_response("No routes for '#{controller}'. Controllers: #{by_controller.keys.sort.join(', ')}") if filtered.empty?
|
|
61
62
|
by_controller = filtered
|
|
62
63
|
end
|
|
@@ -56,9 +56,14 @@ module RailsAiContext
|
|
|
56
56
|
offset = [ offset.to_i, 0 ].max
|
|
57
57
|
limit = [ limit.to_i, 0 ].max if limit && limit.to_i < 0
|
|
58
58
|
|
|
59
|
-
# Single table — case-insensitive lookup
|
|
59
|
+
# Single table — case-insensitive lookup with model name normalization
|
|
60
|
+
# Accepts: "users", "Users", "User" (model name → pluralized+underscored table)
|
|
60
61
|
if table
|
|
61
|
-
|
|
62
|
+
table_down = table.downcase
|
|
63
|
+
table_as_table = table.underscore.pluralize # Cook → cooks, BrandProfile → brand_profiles
|
|
64
|
+
table_key = tables.keys.find { |k|
|
|
65
|
+
k.downcase == table_down || k == table_as_table || k == table.underscore
|
|
66
|
+
} || table
|
|
62
67
|
table_data = tables[table_key]
|
|
63
68
|
unless table_data
|
|
64
69
|
return not_found_response("Table", table, tables.keys.sort,
|
|
@@ -44,7 +44,12 @@ module RailsAiContext
|
|
|
44
44
|
# (HTML uses data-controller="weekly-chart", file is weekly_chart_controller.js)
|
|
45
45
|
if controller
|
|
46
46
|
normalized = controller.downcase.tr("-", "_")
|
|
47
|
-
|
|
47
|
+
# Also handle PascalCase: CookStatus → cook_status
|
|
48
|
+
underscored = controller.underscore.downcase.tr("-", "_")
|
|
49
|
+
ctrl = all_controllers.find { |c|
|
|
50
|
+
name_norm = c[:name]&.downcase&.tr("-", "_")
|
|
51
|
+
name_norm == normalized || name_norm == underscored
|
|
52
|
+
}
|
|
48
53
|
unless ctrl
|
|
49
54
|
names = all_controllers.map { |c| c[:name] }.sort
|
|
50
55
|
return not_found_response("Stimulus controller", controller, names,
|
|
@@ -157,16 +157,27 @@ module RailsAiContext
|
|
|
157
157
|
end
|
|
158
158
|
|
|
159
159
|
private_class_method def self.find_test_file(name, type, detail = "full")
|
|
160
|
-
# Normalize: accept "Bonus::CrisesController", "bonus/crises", "Crises"
|
|
160
|
+
# Normalize: accept "Bonus::CrisesController", "bonus/crises", "Crises", "cooks" (plural)
|
|
161
161
|
snake = name.to_s.tr("/", "::").underscore.sub(/_controller$/, "")
|
|
162
|
+
# For models, also try singular form (cooks → cook)
|
|
163
|
+
snake_singular = snake.singularize
|
|
162
164
|
candidates = case type
|
|
163
165
|
when :model
|
|
164
|
-
[
|
|
166
|
+
base = [
|
|
165
167
|
"spec/models/#{snake}_spec.rb",
|
|
166
168
|
"test/models/#{snake}_test.rb",
|
|
167
169
|
"spec/models/concerns/#{snake}_spec.rb",
|
|
168
170
|
"test/models/concerns/#{snake}_test.rb"
|
|
169
171
|
]
|
|
172
|
+
if snake != snake_singular
|
|
173
|
+
base += [
|
|
174
|
+
"spec/models/#{snake_singular}_spec.rb",
|
|
175
|
+
"test/models/#{snake_singular}_test.rb",
|
|
176
|
+
"spec/models/concerns/#{snake_singular}_spec.rb",
|
|
177
|
+
"test/models/concerns/#{snake_singular}_test.rb"
|
|
178
|
+
]
|
|
179
|
+
end
|
|
180
|
+
base
|
|
170
181
|
when :controller
|
|
171
182
|
[
|
|
172
183
|
"spec/controllers/#{snake}_controller_spec.rb",
|
|
@@ -51,9 +51,17 @@ module RailsAiContext
|
|
|
51
51
|
end
|
|
52
52
|
|
|
53
53
|
if controller
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
# Normalize: accept "CooksController", "cooks", "cooks_controller", "Bonus::CooksController"
|
|
55
|
+
ctrl_lower = controller.underscore.delete_suffix("_controller")
|
|
56
|
+
ctrl_lower_alt = controller.downcase.delete_suffix("controller")
|
|
57
|
+
filtered_templates = templates.select { |k, _|
|
|
58
|
+
k_down = k.downcase
|
|
59
|
+
k_down.start_with?(ctrl_lower + "/") || k_down.start_with?(ctrl_lower_alt + "/")
|
|
60
|
+
}
|
|
61
|
+
filtered_partials = partials.select { |k, _|
|
|
62
|
+
k_down = k.downcase
|
|
63
|
+
k_down.start_with?(ctrl_lower + "/") || k_down.start_with?(ctrl_lower_alt + "/")
|
|
64
|
+
}
|
|
57
65
|
|
|
58
66
|
if filtered_templates.empty? && filtered_partials.empty?
|
|
59
67
|
all_dirs = (templates.keys + partials.keys).map { |k| k.split("/").first }.uniq.sort
|
|
@@ -303,7 +311,12 @@ module RailsAiContext
|
|
|
303
311
|
.sort
|
|
304
312
|
|
|
305
313
|
if controller
|
|
306
|
-
|
|
314
|
+
ctrl_lower = controller.underscore.delete_suffix("_controller")
|
|
315
|
+
ctrl_lower_alt = controller.downcase.delete_suffix("controller")
|
|
316
|
+
templates = templates.select { |t|
|
|
317
|
+
t_down = t.downcase
|
|
318
|
+
t_down.start_with?(ctrl_lower + "/") || t_down.start_with?(ctrl_lower_alt + "/")
|
|
319
|
+
}
|
|
307
320
|
end
|
|
308
321
|
|
|
309
322
|
lines = [ "# Views (#{templates.size} templates)", "" ]
|
|
@@ -37,7 +37,7 @@ module RailsAiContext
|
|
|
37
37
|
# ── Main entry point ─────────────────────────────────────────────
|
|
38
38
|
|
|
39
39
|
def self.call(files:, level: "syntax", server_context: nil)
|
|
40
|
-
return text_response("No files provided.") if files.empty?
|
|
40
|
+
return text_response("No files provided. Pass file paths relative to Rails root (e.g. files:[\"app/models/cook.rb\"]).") if files.nil? || files.empty?
|
|
41
41
|
return text_response("Too many files (#{files.size}). Maximum is #{max_files} per call.") if files.size > max_files
|
|
42
42
|
|
|
43
43
|
results = []
|
|
@@ -48,7 +48,9 @@ module RailsAiContext
|
|
|
48
48
|
full_path = Rails.root.join(file)
|
|
49
49
|
|
|
50
50
|
unless File.exist?(full_path)
|
|
51
|
-
|
|
51
|
+
suggestion = find_file_suggestion(file)
|
|
52
|
+
hint = suggestion ? " Did you mean '#{suggestion}'?" : ""
|
|
53
|
+
results << "\u2717 #{file} \u2014 file not found.#{hint}"
|
|
52
54
|
total += 1
|
|
53
55
|
next
|
|
54
56
|
end
|
|
@@ -122,6 +124,24 @@ module RailsAiContext
|
|
|
122
124
|
|
|
123
125
|
# ── Ruby validation ──────────────────────────────────────────────
|
|
124
126
|
|
|
127
|
+
# Search common Rails directories for a file by basename and suggest the full path
|
|
128
|
+
private_class_method def self.find_file_suggestion(file)
|
|
129
|
+
basename = File.basename(file)
|
|
130
|
+
%w[app/models app/controllers app/views app/helpers app/jobs app/mailers
|
|
131
|
+
app/services app/channels lib config].each do |dir|
|
|
132
|
+
candidate = File.join(dir, basename)
|
|
133
|
+
return candidate if File.exist?(Rails.root.join(candidate))
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Broader recursive search
|
|
137
|
+
matches = Dir.glob(File.join(Rails.root, "app", "**", basename)).first(1)
|
|
138
|
+
return matches.first.sub("#{Rails.root}/", "") if matches.any?
|
|
139
|
+
|
|
140
|
+
nil
|
|
141
|
+
rescue
|
|
142
|
+
nil
|
|
143
|
+
end
|
|
144
|
+
|
|
125
145
|
private_class_method def self.validate_ruby(full_path)
|
|
126
146
|
prism_available? ? validate_ruby_prism(full_path) : validate_ruby_subprocess(full_path)
|
|
127
147
|
end
|
data/server.json
CHANGED
|
@@ -2,16 +2,16 @@
|
|
|
2
2
|
"$schema": "https://static.modelcontextprotocol.io/schemas/2025-12-11/server.schema.json",
|
|
3
3
|
"name": "io.github.crisnahine/rails-ai-context",
|
|
4
4
|
"title": "Rails AI Context",
|
|
5
|
-
"description": "Auto-expose Rails app structure to AI via MCP. Zero config, 25 read-only tools.",
|
|
5
|
+
"description": "Auto-expose Rails app structure to AI via MCP or CLI. Zero config, 25 read-only tools.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/crisnahine/rails-ai-context",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "3.
|
|
10
|
+
"version": "3.1.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "mcpb",
|
|
14
|
-
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v3.
|
|
14
|
+
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v3.1.0/rails-ai-context-mcp.mcpb",
|
|
15
15
|
"fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: rails-ai-context
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.
|
|
4
|
+
version: 3.1.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -164,10 +164,11 @@ dependencies:
|
|
|
164
164
|
description: |
|
|
165
165
|
rails-ai-context gives AI coding agents a complete mental model of your Rails
|
|
166
166
|
app — not just files, but how schema, models, routes, controllers, views, and
|
|
167
|
-
conventions connect. 25 live MCP
|
|
168
|
-
semantic validation that catches cross-file errors
|
|
169
|
-
partials, broken routes) before code runs.
|
|
170
|
-
Claude Code, Cursor, GitHub Copilot, and
|
|
167
|
+
conventions connect. 25 live tools (via MCP server or CLI) let agents query
|
|
168
|
+
structure on demand with semantic validation that catches cross-file errors
|
|
169
|
+
(wrong columns, missing partials, broken routes) before code runs.
|
|
170
|
+
Auto-generates context files for Claude Code, Cursor, GitHub Copilot, and
|
|
171
|
+
OpenCode. Zero config.
|
|
171
172
|
email:
|
|
172
173
|
- crisjosephnahine@gmail.com
|
|
173
174
|
executables:
|
|
@@ -290,9 +291,9 @@ metadata:
|
|
|
290
291
|
post_install_message: |
|
|
291
292
|
rails-ai-context installed! Quick start:
|
|
292
293
|
rails generate rails_ai_context:install
|
|
293
|
-
rails ai:context
|
|
294
|
-
rails ai:
|
|
295
|
-
rails ai:serve
|
|
294
|
+
rails ai:context # generate context files
|
|
295
|
+
rails 'ai:tool[schema]' # run any of the 25 tools from CLI
|
|
296
|
+
rails ai:serve # start MCP server (optional)
|
|
296
297
|
rdoc_options: []
|
|
297
298
|
require_paths:
|
|
298
299
|
- lib
|
|
@@ -309,5 +310,6 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
309
310
|
requirements: []
|
|
310
311
|
rubygems_version: 3.6.9
|
|
311
312
|
specification_version: 4
|
|
312
|
-
summary: Give AI agents a complete mental model of your Rails app
|
|
313
|
+
summary: Give AI agents a complete mental model of your Rails app — 25 tools via MCP
|
|
314
|
+
or CLI. Zero config.
|
|
313
315
|
test_files: []
|