rails-ai-context 0.15.10 → 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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/CLAUDE.md +3 -2
- data/CONTRIBUTING.md +1 -1
- data/README.md +10 -5
- data/SECURITY.md +1 -0
- data/docs/GUIDE.md +36 -2
- data/lib/rails_ai_context/configuration.rb +4 -0
- data/lib/rails_ai_context/server.rb +3 -2
- data/lib/rails_ai_context/tools/analyze_feature.rb +109 -0
- data/lib/rails_ai_context/tools/base_tool.rb +27 -0
- data/lib/rails_ai_context/tools/get_config.rb +3 -1
- data/lib/rails_ai_context/tools/get_controllers.rb +6 -4
- data/lib/rails_ai_context/tools/get_conventions.rb +3 -1
- data/lib/rails_ai_context/tools/get_edit_context.rb +3 -1
- data/lib/rails_ai_context/tools/get_gems.rb +3 -1
- data/lib/rails_ai_context/tools/get_model_details.rb +8 -3
- data/lib/rails_ai_context/tools/get_routes.rb +3 -1
- data/lib/rails_ai_context/tools/get_schema.rb +15 -4
- data/lib/rails_ai_context/tools/get_stimulus.rb +9 -3
- data/lib/rails_ai_context/tools/get_test_info.rb +3 -1
- data/lib/rails_ai_context/tools/get_view.rb +3 -1
- data/lib/rails_ai_context/tools/search_code.rb +3 -1
- data/lib/rails_ai_context/tools/validate.rb +3 -5
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +3 -3
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 436104e565f41ae5d82ff35810543c142c82174f277eef8a7f6b046de6437bc4
|
|
4
|
+
data.tar.gz: d3951676a8aef432734bda1d6fb97004e89c009d11bc1fe5c3b35d6a6b85a7e3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 58982aca74fc74ebbb5cfd410a3b680396fad54f9552931258644ee8d98f27f64fa53e0246692ffb4a4aeaa98b6f8e055195fa3d94d009cc0b48834f81eaee5c
|
|
7
|
+
data.tar.gz: 0d9220605f100fb14c045cb294071009116ad018ca4221a599e4805d93ab6e6b0817181e2138253349e57da02257da7566e1843d79b6460757bad238ad030caf
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,19 @@ 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
|
+
## [1.0.0] - 2026-03-23
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **New composite tool: `rails_analyze_feature`** — one call returns schema + models + controllers + routes for a feature area (e.g., `rails_analyze_feature(feature:"authentication")`). Total MCP tools: 14.
|
|
13
|
+
- **Custom tool registration API** — `config.custom_tools << MyCompany::PolicyCheckTool` lets teams extend the MCP server with their own tools.
|
|
14
|
+
- **Structured error responses with fuzzy suggestions** — `not_found_response` helper in BaseTool with "Did you mean?" fuzzy matching (substring + prefix) and `recovery_action` hints. Applied to schema, models, controllers, and stimulus lookups. AI agents self-correct on first retry.
|
|
15
|
+
- **Cache keys on paginated responses** — every paginated response includes `cache_key` from fingerprint so agents detect stale data between page fetches. Applied to schema, models, controllers, and stimulus pagination.
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
|
|
19
|
+
- **LLM-optimized tool descriptions (all 14 tools)** — every description now follows "what it does / Use when: / key params" format so AI agents pick the right tool on first try.
|
|
20
|
+
|
|
8
21
|
## [0.15.10] - 2026-03-23
|
|
9
22
|
|
|
10
23
|
### Changed
|
data/CLAUDE.md
CHANGED
|
@@ -9,7 +9,7 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
9
9
|
- `lib/rails_ai_context/configuration.rb` — User-facing config with presets (:standard, :full)
|
|
10
10
|
- `lib/rails_ai_context/introspector.rb` — Orchestrates sub-introspectors
|
|
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
|
-
- `lib/rails_ai_context/tools/` —
|
|
12
|
+
- `lib/rails_ai_context/tools/` — 14 MCP tools using the official mcp SDK
|
|
13
13
|
- `lib/rails_ai_context/serializers/` — Output formatters (claude, claude_rules, opencode, opencode_rules, cursor_rules, windsurf, windsurf_rules, copilot, copilot_instructions, rules, markdown, JSON, context_file_serializer, test_command_detection)
|
|
14
14
|
- `lib/rails_ai_context/resources.rb` — MCP resources (static data AI clients read directly)
|
|
15
15
|
- `lib/rails_ai_context/server.rb` — MCP server configuration (stdio + HTTP transports)
|
|
@@ -39,11 +39,12 @@ structure to AI assistants via the Model Context Protocol (MCP).
|
|
|
39
39
|
13. **Per-tool split rules** — `.claude/rules/`, `.cursor/rules/`, `.windsurf/rules/`, `.github/instructions/`
|
|
40
40
|
14. **Section markers** — root file content wrapped in `<!-- BEGIN/END rails-ai-context -->` to preserve user content
|
|
41
41
|
15. **generate_root_files toggle** — when false, skip root files (CLAUDE.md, etc.), only generate split rules
|
|
42
|
+
16. **custom_tools API** — `config.custom_tools` array lets users register additional MCP::Tool subclasses alongside the 14 built-in tools
|
|
42
43
|
|
|
43
44
|
## Testing
|
|
44
45
|
|
|
45
46
|
```bash
|
|
46
|
-
bundle exec rspec # Run specs (
|
|
47
|
+
bundle exec rspec # Run specs (520 examples)
|
|
47
48
|
bundle exec rubocop # Lint
|
|
48
49
|
```
|
|
49
50
|
|
data/CONTRIBUTING.md
CHANGED
|
@@ -19,7 +19,7 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
|
|
|
19
19
|
```
|
|
20
20
|
lib/rails_ai_context/
|
|
21
21
|
├── introspectors/ # 29 introspectors (schema, models, routes, etc.)
|
|
22
|
-
├── tools/ #
|
|
22
|
+
├── tools/ # 14 MCP tools with detail levels and pagination
|
|
23
23
|
├── serializers/ # Per-assistant formatters (claude, opencode, cursor, windsurf, copilot, JSON)
|
|
24
24
|
├── server.rb # MCP server setup (stdio + HTTP)
|
|
25
25
|
├── live_reload.rb # MCP live reload (file watcher + cache invalidation)
|
data/README.md
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
+
> Built by a Rails developer with 10 years of production experience. Yes, AI helped write this gem — the same way AI helps me ship features at work. I designed the architecture, made every decision, reviewed every line, and wrote 520 tests. The gem exists because I understand Rails deeply enough to know what AI agents get wrong and what context they need to get it right.
|
|
11
|
+
|
|
10
12
|
---
|
|
11
13
|
|
|
12
14
|
## The Problem
|
|
@@ -60,7 +62,7 @@ Agent: rails_validate(files:["app/models/cook.rb"], level:"rails") → catches c
|
|
|
60
62
|
|-------|-----------------|---------------|------------|
|
|
61
63
|
| **Static files** (CLAUDE.md, .cursorrules, etc.) | App overview: stack, models, gems, architecture, UI patterns, MCP tool reference | Automatically at session start | ~150 lines, zero tool calls |
|
|
62
64
|
| **Split rules** (.claude/rules/, .cursor/rules/) | Deep reference: full schema with column types, all model associations/scopes, controller listings | Conditionally — only when editing relevant files | Zero when not needed |
|
|
63
|
-
| **Live MCP tools** (
|
|
65
|
+
| **Live MCP tools** (14 tools) | Real-time queries: drill into any table, model, controller action, or view on demand. Semantic validation. | On-demand via agent tool calls | ~25-100 lines per call |
|
|
64
66
|
|
|
65
67
|
**Progressive disclosure:** the agent gets the map for free, reference guides when relevant, and live GPS when building.
|
|
66
68
|
|
|
@@ -70,7 +72,7 @@ Agent: rails_validate(files:["app/models/cook.rb"], level:"rails") → catches c
|
|
|
70
72
|
|
|
71
73
|
| Setup | Tokens | What it knows |
|
|
72
74
|
|-------|--------|---------------|
|
|
73
|
-
| **rails-ai-context (full)** | **28,834** |
|
|
75
|
+
| **rails-ai-context (full)** | **28,834** | 14 MCP tools + generated docs + split rules |
|
|
74
76
|
| rails-ai-context CLAUDE.md only | 33,106 | Generated docs + rules, no MCP tools |
|
|
75
77
|
| Normal Claude `/init` | 40,700 | Generic CLAUDE.md only |
|
|
76
78
|
| No rails-ai-context | 45,477 | Nothing — discovers everything from scratch |
|
|
@@ -95,9 +97,9 @@ But token savings is the side effect. The real value:
|
|
|
95
97
|
|
|
96
98
|
---
|
|
97
99
|
|
|
98
|
-
##
|
|
100
|
+
## 14 Live MCP Tools
|
|
99
101
|
|
|
100
|
-
The gem exposes **
|
|
102
|
+
The gem exposes **14 read-only tools** via MCP that AI clients call on-demand:
|
|
101
103
|
|
|
102
104
|
| Tool | What it returns |
|
|
103
105
|
|------|----------------|
|
|
@@ -114,6 +116,7 @@ The gem exposes **13 read-only tools** via MCP that AI clients call on-demand:
|
|
|
114
116
|
| `rails_get_stimulus` | Stimulus controllers — targets, values, actions, outlets |
|
|
115
117
|
| `rails_get_edit_context` | Surgical edit helper — returns code around a match with line numbers |
|
|
116
118
|
| `rails_validate` | Batch syntax validation for Ruby, ERB, and JavaScript files. `level:"rails"` adds semantic checks (partials, route helpers, columns, strong params, callbacks, FK indexes, Stimulus) |
|
|
119
|
+
| `rails_analyze_feature` | End-to-end feature analysis — finds matching models, controllers, routes, and views in one call |
|
|
117
120
|
|
|
118
121
|
### Smart Detail Levels
|
|
119
122
|
|
|
@@ -334,6 +337,8 @@ end
|
|
|
334
337
|
| **Search & Discovery** | | |
|
|
335
338
|
| `search_extensions` | `rb js erb yml yaml json ts tsx vue svelte haml slim` | File extensions for Ruby fallback search |
|
|
336
339
|
| `concern_paths` | `app/models/concerns app/controllers/concerns` | Where to look for concern source files |
|
|
340
|
+
| **Extensibility** | | |
|
|
341
|
+
| `custom_tools` | `[]` | Additional MCP tool classes to register alongside built-in tools |
|
|
337
342
|
</details>
|
|
338
343
|
|
|
339
344
|
---
|
|
@@ -422,7 +427,7 @@ The gem parses `db/schema.rb` as text when no database is connected. Works in CI
|
|
|
422
427
|
```bash
|
|
423
428
|
git clone https://github.com/crisnahine/rails-ai-context.git
|
|
424
429
|
cd rails-ai-context && bundle install
|
|
425
|
-
bundle exec rspec #
|
|
430
|
+
bundle exec rspec # 520 examples
|
|
426
431
|
bundle exec rubocop # Lint
|
|
427
432
|
```
|
|
428
433
|
|
data/SECURITY.md
CHANGED
data/docs/GUIDE.md
CHANGED
|
@@ -252,7 +252,7 @@ rails ai:context:claude # Use this instead (no quoting needed)
|
|
|
252
252
|
|
|
253
253
|
## MCP Tools — Full Reference
|
|
254
254
|
|
|
255
|
-
All
|
|
255
|
+
All 14 tools are **read-only** and **idempotent** — they never modify your application or database.
|
|
256
256
|
|
|
257
257
|
### rails_get_schema
|
|
258
258
|
|
|
@@ -588,6 +588,36 @@ rails_search_code(pattern: "validates", context_lines: 2)
|
|
|
588
588
|
|
|
589
589
|
**Security:** Uses `Open3.capture2` with array arguments (no shell injection). Validates file_type. Blocks path traversal. Respects `excluded_paths` and `sensitive_patterns` config.
|
|
590
590
|
|
|
591
|
+
### rails_analyze_feature
|
|
592
|
+
|
|
593
|
+
Analyzes a feature end-to-end: finds matching models, controllers, routes, and views in one call.
|
|
594
|
+
|
|
595
|
+
**Parameters:**
|
|
596
|
+
|
|
597
|
+
| Param | Type | Description |
|
|
598
|
+
|-------|------|-------------|
|
|
599
|
+
| `feature` | string | **Required.** Feature keyword to search for (e.g. `authentication`, `User`, `payments`, `orders`). Case-insensitive partial match across models, controllers, and routes. |
|
|
600
|
+
|
|
601
|
+
**Examples:**
|
|
602
|
+
|
|
603
|
+
```
|
|
604
|
+
rails_analyze_feature(feature: "authentication")
|
|
605
|
+
→ Models, controllers, and routes matching "authentication"
|
|
606
|
+
|
|
607
|
+
rails_analyze_feature(feature: "User")
|
|
608
|
+
→ User model with schema columns, associations, validations, scopes;
|
|
609
|
+
UsersController with actions and filters;
|
|
610
|
+
all user routes with verbs and paths
|
|
611
|
+
|
|
612
|
+
rails_analyze_feature(feature: "payments")
|
|
613
|
+
→ Cross-cutting view: Payment model + PaymentsController + payment routes
|
|
614
|
+
|
|
615
|
+
rails_analyze_feature(feature: "orders")
|
|
616
|
+
→ Everything related to orders across all layers
|
|
617
|
+
```
|
|
618
|
+
|
|
619
|
+
**Returns:** Markdown with sections for Models (with table, columns, indexes, FKs, associations, validations, scopes), Controllers (with actions and filters), and Routes (with verbs, paths, and route names). Each section shows match counts.
|
|
620
|
+
|
|
591
621
|
### Detail Level Summary
|
|
592
622
|
|
|
593
623
|
All tools that support `detail` use these three levels. Default limits vary by tool — schema defaults shown below:
|
|
@@ -716,7 +746,7 @@ RailsAiContext.configure do |config|
|
|
|
716
746
|
end
|
|
717
747
|
```
|
|
718
748
|
|
|
719
|
-
Both transports are **read-only** — they expose the same
|
|
749
|
+
Both transports are **read-only** — they expose the same 14 tools and never modify your app.
|
|
720
750
|
|
|
721
751
|
---
|
|
722
752
|
|
|
@@ -752,6 +782,9 @@ RailsAiContext.configure do |config|
|
|
|
752
782
|
# Cache TTL for introspection results (seconds)
|
|
753
783
|
config.cache_ttl = 30
|
|
754
784
|
|
|
785
|
+
# Additional MCP tool classes to register alongside built-in tools
|
|
786
|
+
# config.custom_tools = [MyApp::Tools::CustomTool]
|
|
787
|
+
|
|
755
788
|
# --- Exclusions ---
|
|
756
789
|
|
|
757
790
|
# Models to skip during introspection
|
|
@@ -838,6 +871,7 @@ end
|
|
|
838
871
|
| `claude_max_lines` | Integer | `150` | Max lines for CLAUDE.md in compact mode |
|
|
839
872
|
| `max_tool_response_chars` | Integer | `120_000` | Safety cap for MCP tool responses |
|
|
840
873
|
| `cache_ttl` | Integer | `30` | Cache TTL in seconds for introspection results |
|
|
874
|
+
| `custom_tools` | Array | `[]` | Additional MCP tool classes to register alongside built-in tools |
|
|
841
875
|
| `excluded_models` | Array | internal Rails models | Models to skip |
|
|
842
876
|
| `excluded_paths` | Array | `node_modules tmp log vendor .git` | Paths excluded from code search |
|
|
843
877
|
| `sensitive_patterns` | Array | `.env`, `.key`, `.pem`, credentials | File patterns blocked from search and read tools |
|
|
@@ -69,6 +69,9 @@ module RailsAiContext
|
|
|
69
69
|
attr_accessor :max_search_results # Max search results per call (default: 100)
|
|
70
70
|
attr_accessor :max_validate_files # Max files per validate call (default: 20)
|
|
71
71
|
|
|
72
|
+
# Additional MCP tool classes to register alongside built-in tools
|
|
73
|
+
attr_accessor :custom_tools
|
|
74
|
+
|
|
72
75
|
# Filtering — customize what's hidden from AI output
|
|
73
76
|
attr_accessor :excluded_controllers # Controller classes hidden from listings (e.g. DeviseController)
|
|
74
77
|
attr_accessor :excluded_route_prefixes # Route controller prefixes hidden with app_only (e.g. action_mailbox/)
|
|
@@ -140,6 +143,7 @@ module RailsAiContext
|
|
|
140
143
|
ActiveRecord::Migration::CheckPending ActionDispatch::HostAuthorization
|
|
141
144
|
Rack::MethodOverride ActionDispatch::Session::AbstractSecureStore
|
|
142
145
|
]
|
|
146
|
+
@custom_tools = []
|
|
143
147
|
@search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
|
|
144
148
|
@concern_paths = %w[app/models/concerns app/controllers/concerns]
|
|
145
149
|
end
|
|
@@ -21,7 +21,8 @@ module RailsAiContext
|
|
|
21
21
|
Tools::GetView,
|
|
22
22
|
Tools::GetStimulus,
|
|
23
23
|
Tools::GetEditContext,
|
|
24
|
-
Tools::Validate
|
|
24
|
+
Tools::Validate,
|
|
25
|
+
Tools::AnalyzeFeature
|
|
25
26
|
].freeze
|
|
26
27
|
|
|
27
28
|
def initialize(app, transport: :stdio)
|
|
@@ -36,7 +37,7 @@ module RailsAiContext
|
|
|
36
37
|
server = MCP::Server.new(
|
|
37
38
|
name: config.server_name,
|
|
38
39
|
version: config.server_version,
|
|
39
|
-
tools: TOOLS
|
|
40
|
+
tools: TOOLS + config.custom_tools
|
|
40
41
|
)
|
|
41
42
|
|
|
42
43
|
Resources.register(server)
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailsAiContext
|
|
4
|
+
module Tools
|
|
5
|
+
class AnalyzeFeature < BaseTool
|
|
6
|
+
tool_name "rails_analyze_feature"
|
|
7
|
+
description "Analyze a feature end-to-end: finds matching models, controllers, routes, and views in one call. " \
|
|
8
|
+
"Use when: exploring an unfamiliar feature, onboarding to a codebase area, or tracing a feature across layers. " \
|
|
9
|
+
"Pass feature:\"authentication\" or feature:\"User\" for broad cross-cutting discovery."
|
|
10
|
+
|
|
11
|
+
input_schema(
|
|
12
|
+
properties: {
|
|
13
|
+
feature: {
|
|
14
|
+
type: "string",
|
|
15
|
+
description: "Feature keyword to search for (e.g. 'authentication', 'User', 'payments', 'orders'). Case-insensitive partial match across models, controllers, and routes."
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
required: [ "feature" ]
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
22
|
+
|
|
23
|
+
def self.call(feature:, server_context: nil)
|
|
24
|
+
ctx = cached_context
|
|
25
|
+
pattern = feature.downcase
|
|
26
|
+
lines = [ "# Feature Analysis: #{feature}", "" ]
|
|
27
|
+
|
|
28
|
+
# --- Models ---
|
|
29
|
+
models = ctx[:models] || {}
|
|
30
|
+
matched_models = models.select { |name, data| !data[:error] && name.downcase.include?(pattern) }
|
|
31
|
+
|
|
32
|
+
if matched_models.any?
|
|
33
|
+
lines << "## Models (#{matched_models.size} matched)"
|
|
34
|
+
matched_models.sort.each do |name, data|
|
|
35
|
+
lines << ""
|
|
36
|
+
lines << "### #{name}"
|
|
37
|
+
lines << "**Table:** `#{data[:table_name]}`" if data[:table_name]
|
|
38
|
+
|
|
39
|
+
# Schema columns from schema introspection
|
|
40
|
+
table_name = data[:table_name]
|
|
41
|
+
if table_name && (schema = ctx[:schema]) && (tables = schema[:tables])
|
|
42
|
+
table_data = tables[table_name]
|
|
43
|
+
if table_data && table_data[:columns]&.any?
|
|
44
|
+
cols = table_data[:columns].reject { |c| %w[id created_at updated_at].include?(c[:name]) }
|
|
45
|
+
lines << "**Columns:** #{cols.map { |c| "#{c[:name]}:#{c[:type]}" }.join(', ')}" if cols.any?
|
|
46
|
+
if table_data[:indexes]&.any?
|
|
47
|
+
lines << "**Indexes:** #{table_data[:indexes].map { |i| "#{i[:columns].join(',')}#{i[:unique] ? ' (unique)' : ''}" }.join('; ')}"
|
|
48
|
+
end
|
|
49
|
+
if table_data[:foreign_keys]&.any?
|
|
50
|
+
lines << "**FKs:** #{table_data[:foreign_keys].map { |fk| "#{fk[:column]} -> #{fk[:to_table]}" }.join(', ')}"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if data[:associations]&.any?
|
|
56
|
+
lines << "**Associations:** #{data[:associations].map { |a| "#{a[:type]} :#{a[:name]}" }.join(', ')}"
|
|
57
|
+
end
|
|
58
|
+
if data[:validations]&.any?
|
|
59
|
+
lines << "**Validations:** #{data[:validations].map { |v| "#{v[:kind]} on #{v[:attributes].join(', ')}" }.uniq.join('; ')}"
|
|
60
|
+
end
|
|
61
|
+
if data[:scopes]&.any?
|
|
62
|
+
lines << "**Scopes:** #{data[:scopes].join(', ')}"
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
else
|
|
66
|
+
lines << "## Models" << "_No models matching '#{feature}'._"
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# --- Controllers ---
|
|
70
|
+
controllers = (ctx.dig(:controllers, :controllers) || {})
|
|
71
|
+
matched_controllers = controllers.select { |name, data| !data[:error] && name.downcase.include?(pattern) }
|
|
72
|
+
|
|
73
|
+
lines << ""
|
|
74
|
+
if matched_controllers.any?
|
|
75
|
+
lines << "## Controllers (#{matched_controllers.size} matched)"
|
|
76
|
+
matched_controllers.sort.each do |name, info|
|
|
77
|
+
actions = info[:actions]&.join(", ") || "none"
|
|
78
|
+
filters = (info[:filters] || []).map { |f| "#{f[:kind]} #{f[:name]}" }.join(", ")
|
|
79
|
+
lines << "" << "### #{name}"
|
|
80
|
+
lines << "- **Actions:** #{actions}"
|
|
81
|
+
lines << "- **Filters:** #{filters}" unless filters.empty?
|
|
82
|
+
end
|
|
83
|
+
else
|
|
84
|
+
lines << "## Controllers" << "_No controllers matching '#{feature}'._"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# --- Routes ---
|
|
88
|
+
by_controller = (ctx.dig(:routes, :by_controller) || {})
|
|
89
|
+
matched_routes = by_controller.select { |ctrl, _| ctrl.downcase.include?(pattern) }
|
|
90
|
+
|
|
91
|
+
lines << ""
|
|
92
|
+
if matched_routes.any?
|
|
93
|
+
route_count = matched_routes.values.sum(&:size)
|
|
94
|
+
lines << "## Routes (#{route_count} matched)"
|
|
95
|
+
matched_routes.sort.each do |ctrl, actions|
|
|
96
|
+
actions.each do |r|
|
|
97
|
+
name_part = r[:name] ? " `#{r[:name]}`" : ""
|
|
98
|
+
lines << "- `#{r[:verb]}` `#{r[:path]}` -> #{ctrl}##{r[:action]}#{name_part}"
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
else
|
|
102
|
+
lines << "## Routes" << "_No routes matching '#{feature}'._"
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
text_response(lines.join("\n"))
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -54,6 +54,33 @@ module RailsAiContext
|
|
|
54
54
|
reset_cache!
|
|
55
55
|
end
|
|
56
56
|
|
|
57
|
+
# Structured not-found error with fuzzy suggestion and recovery hint.
|
|
58
|
+
# Helps AI agents self-correct without retrying blind.
|
|
59
|
+
def not_found_response(type, name, available, recovery_tool: nil)
|
|
60
|
+
suggestion = find_closest_match(name, available)
|
|
61
|
+
lines = [ "#{type} '#{name}' not found." ]
|
|
62
|
+
lines << "Did you mean '#{suggestion}'?" if suggestion
|
|
63
|
+
lines << "Available: #{available.first(20).join(', ')}#{"..." if available.size > 20}"
|
|
64
|
+
lines << "_Recovery: #{recovery_tool}_" if recovery_tool
|
|
65
|
+
text_response(lines.join("\n"))
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Simple fuzzy match: find the closest available name by substring or edit distance
|
|
69
|
+
def find_closest_match(input, available)
|
|
70
|
+
return nil if available.empty?
|
|
71
|
+
downcased = input.downcase
|
|
72
|
+
# Exact substring match first
|
|
73
|
+
exact = available.find { |a| a.downcase.include?(downcased) || downcased.include?(a.downcase) }
|
|
74
|
+
return exact if exact
|
|
75
|
+
# Prefix match
|
|
76
|
+
available.find { |a| a.downcase.start_with?(downcased[0..2]) }
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Cache key for paginated responses — lets agents detect stale data between pages
|
|
80
|
+
def cache_key
|
|
81
|
+
SHARED_CACHE[:fingerprint] || "none"
|
|
82
|
+
end
|
|
83
|
+
|
|
57
84
|
# Helper: wrap text in an MCP::Tool::Response with safety-net truncation
|
|
58
85
|
def text_response(text)
|
|
59
86
|
max = RailsAiContext.configuration.max_tool_response_chars
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetConfig < BaseTool
|
|
6
6
|
tool_name "rails_get_config"
|
|
7
|
-
description "Get Rails
|
|
7
|
+
description "Get Rails app configuration: cache store, session store, timezone, queue adapter, custom middleware, initializers. " \
|
|
8
|
+
"Use when: configuring caching, checking session/queue setup, or seeing what initializers exist. " \
|
|
9
|
+
"No parameters needed. Returns only non-default middleware and notable initializers."
|
|
8
10
|
|
|
9
11
|
input_schema(properties: {})
|
|
10
12
|
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetControllers < BaseTool
|
|
6
6
|
tool_name "rails_get_controllers"
|
|
7
|
-
description "Get controller
|
|
7
|
+
description "Get controller details: actions, before_action filters, strong params, and parent class. " \
|
|
8
|
+
"Use when: adding/modifying controller actions, checking what filters apply, or reading action source code. " \
|
|
9
|
+
"Filter with controller:\"PostsController\", drill into action:\"create\" for source code with line numbers."
|
|
8
10
|
|
|
9
11
|
input_schema(
|
|
10
12
|
properties: {
|
|
@@ -60,8 +62,8 @@ module RailsAiContext
|
|
|
60
62
|
} || controller
|
|
61
63
|
info = controllers[key]
|
|
62
64
|
unless info
|
|
63
|
-
|
|
64
|
-
|
|
65
|
+
return not_found_response("Controller", controller, app_controller_names,
|
|
66
|
+
recovery_tool: "Call rails_get_controllers(detail:\"summary\") to see all controllers")
|
|
65
67
|
end
|
|
66
68
|
return text_response("Error inspecting #{key}: #{info[:error]}") if info[:error]
|
|
67
69
|
|
|
@@ -86,7 +88,7 @@ module RailsAiContext
|
|
|
86
88
|
return text_response("No controllers at offset #{offset}. Total: #{total}. Use `offset:0` to start over.")
|
|
87
89
|
end
|
|
88
90
|
|
|
89
|
-
pagination_hint = offset + limit < total ? "\n_Showing #{paginated_names.size} of #{total}. Use `offset:#{offset + limit}` for more._" : ""
|
|
91
|
+
pagination_hint = offset + limit < total ? "\n_Showing #{paginated_names.size} of #{total}. Use `offset:#{offset + limit}` for more. cache_key: #{cache_key}_" : ""
|
|
90
92
|
|
|
91
93
|
# Listing mode
|
|
92
94
|
case detail
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetConventions < BaseTool
|
|
6
6
|
tool_name "rails_get_conventions"
|
|
7
|
-
description "Detect
|
|
7
|
+
description "Detect app architecture and conventions: API-only vs Hotwire, design patterns, directory layout. " \
|
|
8
|
+
"Use when: starting work on an unfamiliar codebase, choosing implementation patterns, or checking what frameworks are in use. " \
|
|
9
|
+
"No parameters needed. Returns architecture style, detected patterns (STI, service objects), and notable config files."
|
|
8
10
|
|
|
9
11
|
input_schema(properties: {})
|
|
10
12
|
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetEditContext < BaseTool
|
|
6
6
|
tool_name "rails_get_edit_context"
|
|
7
|
-
description "Get
|
|
7
|
+
description "Get targeted code context for surgical edits: returns matching lines with surrounding code and line numbers. " \
|
|
8
|
+
"Use when: you need to edit a specific method or section without reading the entire file. " \
|
|
9
|
+
"Requires file:\"app/models/user.rb\" and near:\"def activate\" to locate the code region."
|
|
8
10
|
|
|
9
11
|
def self.max_file_size
|
|
10
12
|
RailsAiContext.configuration.max_file_size
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetGems < BaseTool
|
|
6
6
|
tool_name "rails_get_gems"
|
|
7
|
-
description "
|
|
7
|
+
description "Get notable gems from Gemfile.lock grouped by category: auth, jobs, frontend, API, database, testing, deploy. " \
|
|
8
|
+
"Use when: checking what libraries are available before adding a dependency, or understanding the tech stack. " \
|
|
9
|
+
"Filter with category:\"auth\" or category:\"database\". Omit for all categories."
|
|
8
10
|
|
|
9
11
|
input_schema(
|
|
10
12
|
properties: {
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetModelDetails < BaseTool
|
|
6
6
|
tool_name "rails_get_model_details"
|
|
7
|
-
description "Get
|
|
7
|
+
description "Get ActiveRecord model details: associations, validations, scopes, enums, callbacks, concerns. " \
|
|
8
|
+
"Use when: understanding model relationships, adding validations, checking existing scopes/callbacks. " \
|
|
9
|
+
"Specify model:\"User\" for full detail, or omit for a list. detail:\"full\" shows association lists."
|
|
8
10
|
|
|
9
11
|
input_schema(
|
|
10
12
|
properties: {
|
|
@@ -39,7 +41,10 @@ module RailsAiContext
|
|
|
39
41
|
if model
|
|
40
42
|
key = models.keys.find { |k| k.downcase == model.downcase } || model
|
|
41
43
|
data = models[key]
|
|
42
|
-
|
|
44
|
+
unless data
|
|
45
|
+
return not_found_response("Model", model, models.keys.sort,
|
|
46
|
+
recovery_tool: "Call rails_get_model_details(detail:\"summary\") to see all models")
|
|
47
|
+
end
|
|
43
48
|
return text_response("Error inspecting #{key}: #{data[:error]}") if data[:error]
|
|
44
49
|
return text_response(format_model(key, data))
|
|
45
50
|
end
|
|
@@ -55,7 +60,7 @@ module RailsAiContext
|
|
|
55
60
|
return text_response("No models at offset #{offset}. Total: #{total}. Use `offset:0` to start over.")
|
|
56
61
|
end
|
|
57
62
|
|
|
58
|
-
pagination_hint = offset + limit < total ? "\n_Showing #{paginated.size} of #{total}. Use `offset:#{offset + limit}` for more._" : ""
|
|
63
|
+
pagination_hint = offset + limit < total ? "\n_Showing #{paginated.size} of #{total}. Use `offset:#{offset + limit}` for more. cache_key: #{cache_key}_" : ""
|
|
59
64
|
|
|
60
65
|
# Listing mode
|
|
61
66
|
case detail
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetRoutes < BaseTool
|
|
6
6
|
tool_name "rails_get_routes"
|
|
7
|
-
description "Get
|
|
7
|
+
description "Get routing table: HTTP verbs, paths, controller#action, route names. " \
|
|
8
|
+
"Use when: building links/redirects, checking available endpoints, verifying route helpers exist. " \
|
|
9
|
+
"Filter with controller:\"users\", use detail:\"summary\" for counts or detail:\"full\" for route names."
|
|
8
10
|
|
|
9
11
|
input_schema(
|
|
10
12
|
properties: {
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetSchema < BaseTool
|
|
6
6
|
tool_name "rails_get_schema"
|
|
7
|
-
description "Get
|
|
7
|
+
description "Get database schema: tables, columns, types, indexes, foreign keys. " \
|
|
8
|
+
"Use when: writing migrations, checking column types/constraints, understanding table relationships. " \
|
|
9
|
+
"Filter to one table with table:\"users\", control detail with detail:\"summary\"|\"standard\"|\"full\"."
|
|
8
10
|
|
|
9
11
|
input_schema(
|
|
10
12
|
properties: {
|
|
@@ -57,7 +59,10 @@ module RailsAiContext
|
|
|
57
59
|
if table
|
|
58
60
|
table_key = tables.keys.find { |k| k.downcase == table.downcase } || table
|
|
59
61
|
table_data = tables[table_key]
|
|
60
|
-
|
|
62
|
+
unless table_data
|
|
63
|
+
return not_found_response("Table", table, tables.keys.sort,
|
|
64
|
+
recovery_tool: "Call rails_get_schema(detail:\"summary\") to see all tables")
|
|
65
|
+
end
|
|
61
66
|
output = format == "json" ? table_data.to_json : format_table_markdown(table_key, table_data)
|
|
62
67
|
return text_response(output)
|
|
63
68
|
end
|
|
@@ -77,7 +82,10 @@ module RailsAiContext
|
|
|
77
82
|
idx_count = data[:indexes]&.size || 0
|
|
78
83
|
lines << "- **#{name}** — #{col_count} columns, #{idx_count} indexes"
|
|
79
84
|
end
|
|
80
|
-
|
|
85
|
+
if offset + limit < total
|
|
86
|
+
lines << "" << "_Showing #{paginated.size} of #{total}. Use `offset:#{offset + limit}` for more, or `table:\"name\"` for full detail._"
|
|
87
|
+
lines << "_cache_key: #{cache_key}_"
|
|
88
|
+
end
|
|
81
89
|
text_response(lines.join("\n"))
|
|
82
90
|
|
|
83
91
|
when "standard"
|
|
@@ -113,7 +121,10 @@ module RailsAiContext
|
|
|
113
121
|
lines << format_table_markdown(name, tables[name])
|
|
114
122
|
lines << ""
|
|
115
123
|
end
|
|
116
|
-
|
|
124
|
+
if offset + limit < total
|
|
125
|
+
lines << "_Showing #{paginated.size} of #{total}. Use `offset:#{offset + limit}` for more._"
|
|
126
|
+
lines << "_cache_key: #{cache_key}_"
|
|
127
|
+
end
|
|
117
128
|
text_response(lines.join("\n"))
|
|
118
129
|
else
|
|
119
130
|
# Fallback to full dump (backward compat)
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetStimulus < BaseTool
|
|
6
6
|
tool_name "rails_get_stimulus"
|
|
7
|
-
description "Get Stimulus
|
|
7
|
+
description "Get Stimulus controllers: targets, values, actions, outlets, classes. " \
|
|
8
|
+
"Use when: wiring up data-controller attributes in views, adding targets/values, or checking existing Stimulus behavior. " \
|
|
9
|
+
"Filter with controller:\"filter-form\" for one controller's full API, or list all with detail:\"summary\"."
|
|
8
10
|
|
|
9
11
|
input_schema(
|
|
10
12
|
properties: {
|
|
@@ -43,7 +45,11 @@ module RailsAiContext
|
|
|
43
45
|
if controller
|
|
44
46
|
normalized = controller.downcase.tr("-", "_")
|
|
45
47
|
ctrl = all_controllers.find { |c| c[:name]&.downcase&.tr("-", "_") == normalized }
|
|
46
|
-
|
|
48
|
+
unless ctrl
|
|
49
|
+
names = all_controllers.map { |c| c[:name] }.sort
|
|
50
|
+
return not_found_response("Stimulus controller", controller, names,
|
|
51
|
+
recovery_tool: "Call rails_get_stimulus(detail:\"summary\") to see all controllers. Note: use dashes in HTML, underscores for lookup.")
|
|
52
|
+
end
|
|
47
53
|
return text_response(format_controller_full(ctrl))
|
|
48
54
|
end
|
|
49
55
|
|
|
@@ -58,7 +64,7 @@ module RailsAiContext
|
|
|
58
64
|
return text_response("No controllers at offset #{offset_val}. Total: #{total}. Use `offset:0` to start over.")
|
|
59
65
|
end
|
|
60
66
|
|
|
61
|
-
pagination_hint = offset_val + limit_val < total ? "\n_Showing #{controllers.size} of #{total}. Use `offset:#{offset_val + limit_val}` for more._" : ""
|
|
67
|
+
pagination_hint = offset_val + limit_val < total ? "\n_Showing #{controllers.size} of #{total}. Use `offset:#{offset_val + limit_val}` for more. cache_key: #{cache_key}_" : ""
|
|
62
68
|
|
|
63
69
|
case detail
|
|
64
70
|
when "summary"
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetTestInfo < BaseTool
|
|
6
6
|
tool_name "rails_get_test_info"
|
|
7
|
-
description "Get test infrastructure: framework, factories
|
|
7
|
+
description "Get test infrastructure and existing test files: framework, factories, fixtures, CI config, coverage setup. " \
|
|
8
|
+
"Use when: writing new tests, checking what factories/fixtures exist, or finding the test file for a model/controller. " \
|
|
9
|
+
"Use model:\"User\" or controller:\"Cooks\" to see existing tests. detail:\"full\" lists factory and fixture names."
|
|
8
10
|
|
|
9
11
|
input_schema(
|
|
10
12
|
properties: {
|
|
@@ -4,7 +4,9 @@ module RailsAiContext
|
|
|
4
4
|
module Tools
|
|
5
5
|
class GetView < BaseTool
|
|
6
6
|
tool_name "rails_get_view"
|
|
7
|
-
description "Get view
|
|
7
|
+
description "Get view templates, partials, and their Stimulus/partial references. " \
|
|
8
|
+
"Use when: editing ERB views, checking which partials a page renders, or finding Stimulus controller usage. " \
|
|
9
|
+
"Filter with controller:\"cooks\" for all views, or path:\"cooks/index.html.erb\" for one file's content."
|
|
8
10
|
|
|
9
11
|
input_schema(
|
|
10
12
|
properties: {
|
|
@@ -6,7 +6,9 @@ module RailsAiContext
|
|
|
6
6
|
module Tools
|
|
7
7
|
class SearchCode < BaseTool
|
|
8
8
|
tool_name "rails_search_code"
|
|
9
|
-
description "Search the Rails codebase
|
|
9
|
+
description "Search the Rails codebase by regex pattern, returning matching lines with file paths and line numbers. " \
|
|
10
|
+
"Use when: finding where a method is called, locating class definitions, or tracing how a feature is implemented. " \
|
|
11
|
+
"Requires pattern:\"def activate\". Narrow with path:\"app/models\" and file_type:\"rb\"."
|
|
10
12
|
|
|
11
13
|
def self.max_results_cap
|
|
12
14
|
RailsAiContext.configuration.max_search_results
|
|
@@ -8,11 +8,9 @@ module RailsAiContext
|
|
|
8
8
|
module Tools
|
|
9
9
|
class Validate < BaseTool
|
|
10
10
|
tool_name "rails_validate"
|
|
11
|
-
description "Validate syntax
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"column references, strong params vs schema, callback method existence, " \
|
|
15
|
-
"route-action consistency, has_many dependent, FK indexes, Stimulus controllers."
|
|
11
|
+
description "Validate syntax and semantics of Ruby, ERB, and JavaScript files in a single call. " \
|
|
12
|
+
"Use when: after editing files, before committing, to catch syntax errors and Rails-specific issues. " \
|
|
13
|
+
"Pass files:[\"app/models/user.rb\"], use level:\"rails\" for semantic checks (missing partials, bad column refs, orphaned routes)."
|
|
16
14
|
|
|
17
15
|
def self.max_files
|
|
18
16
|
RailsAiContext.configuration.max_validate_files
|
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,
|
|
5
|
+
"description": "Auto-expose Rails app structure to AI via MCP. Zero config, 14 read-only tools.",
|
|
6
6
|
"repository": {
|
|
7
7
|
"url": "https://github.com/crisnahine/rails-ai-context",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.
|
|
10
|
+
"version": "1.0.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "mcpb",
|
|
14
|
-
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/
|
|
14
|
+
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v1.0.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: 0.
|
|
4
|
+
version: 1.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- crisnahine
|
|
@@ -245,6 +245,7 @@ files:
|
|
|
245
245
|
- lib/rails_ai_context/serializers/windsurf_serializer.rb
|
|
246
246
|
- lib/rails_ai_context/server.rb
|
|
247
247
|
- lib/rails_ai_context/tasks/rails_ai_context.rake
|
|
248
|
+
- lib/rails_ai_context/tools/analyze_feature.rb
|
|
248
249
|
- lib/rails_ai_context/tools/base_tool.rb
|
|
249
250
|
- lib/rails_ai_context/tools/get_config.rb
|
|
250
251
|
- lib/rails_ai_context/tools/get_controllers.rb
|