rails-ai-context 2.0.2 → 2.0.4
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 +18 -0
- data/README.md +139 -327
- data/docs/GUIDE.md +3 -2
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +8 -4
- data/lib/rails_ai_context/serializers/claude_serializer.rb +5 -4
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +7 -1
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +7 -1
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +5 -4
- data/lib/rails_ai_context/serializers/windsurf_rules_serializer.rb +7 -1
- data/lib/rails_ai_context/tools/analyze_feature.rb +49 -3
- data/lib/rails_ai_context/tools/get_concern.rb +62 -5
- data/lib/rails_ai_context/tools/get_schema.rb +8 -0
- data/lib/rails_ai_context/tools/search_code.rb +109 -22
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 70a0eb825c34329a1f0bc9d387a4a2bfd2a42b56717305dcaa5df4c0712264f7
|
|
4
|
+
data.tar.gz: 5a2e229f4bbcaf39d9eebe3e3851444ceed0860b79ad4bcccf11fdba9487ac0a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: eca131443eb443b24a7681233f5e5adcef9a2c2fb3f4ee6d4e665c795470bed1e0bfdfa92f4f4861136f3e7c2b34210972273b2d0b976f3ffb7fa88834ed31b7
|
|
7
|
+
data.tar.gz: b2e96f95e25b8e9cf9fb140446969fc7a027ff22e210321c3ade42da59aa7c6ed702ceabc0c8be62e8a56c2fa25230dcb460bfd3182716c3a1ef1cc300b836bb
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,24 @@ 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
|
+
## [2.0.4] - 2026-03-25
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **Orphaned table detection** — `get_schema` standard mode flags tables with no ActiveRecord model: "⚠ Orphaned tables: content_calendars, cook_comments"
|
|
13
|
+
- **Concern method source code** — `get_concern(name:"X", detail:"full")` shows method bodies inline, same pattern as callbacks tool.
|
|
14
|
+
- **analyze_feature: inherited filters** — shows `authenticate_user! (from ApplicationController)` in controller section.
|
|
15
|
+
- **analyze_feature: code-ready route helpers** — `cook_path(@record)`, `cooks_path` inline with routes.
|
|
16
|
+
- **analyze_feature: service test gaps** — checks services for missing test files, not just models/controllers/jobs.
|
|
17
|
+
- **All 6 serializers updated** — Claude, Cursor, Copilot, Windsurf, OpenCode all document trace mode, concern source, orphaned tables, inherited filters.
|
|
18
|
+
|
|
19
|
+
## [2.0.3] - 2026-03-25
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- **Trace mode 100%** — `match_type:"trace"` now shows 7 sections: definition with class/module context, source code, internal calls, sibling methods (same file), app callers with route chain hints, and test coverage (separated from app code). Zero follow-up calls needed.
|
|
24
|
+
- **README rewrite** — neuro marketing techniques: loss aversion hook, measured token savings table, trace output inline, architecture diagram. 456→261 lines.
|
|
25
|
+
|
|
8
26
|
## [2.0.2] - 2026-03-25
|
|
9
27
|
|
|
10
28
|
### Added
|
data/README.md
CHANGED
|
@@ -1,273 +1,182 @@
|
|
|
1
1
|
# rails-ai-context
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
### Your AI is guessing. This gem makes it know.
|
|
4
4
|
|
|
5
5
|
[](https://rubygems.org/gems/rails-ai-context)
|
|
6
6
|
[](https://registry.modelcontextprotocol.io)
|
|
7
7
|
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
8
8
|
[](LICENSE)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
**Works with:** Claude Code • Cursor • GitHub Copilot • Windsurf • OpenCode
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
## The Problem
|
|
15
|
-
|
|
16
|
-
AI agents working on Rails apps operate blind. They read files one at a time but never see the full picture — how your schema connects to your models, which callbacks fire on save, what filters apply to a controller action, which Stimulus controllers exist, or what your UI conventions are.
|
|
17
|
-
|
|
18
|
-
The result: **guess-and-check coding.** The agent writes code, it breaks, it reads more files, fixes it, breaks again. Each iteration wastes tokens and erodes trust.
|
|
19
|
-
|
|
20
|
-
## The Solution
|
|
21
|
-
|
|
22
|
-
**rails-ai-context** gives your AI agent what a senior Rails developer has naturally: a structured mental model of the entire application.
|
|
12
|
+
> 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 575 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.
|
|
23
13
|
|
|
24
14
|
```bash
|
|
25
|
-
|
|
15
|
+
gem "rails-ai-context", group: :development
|
|
26
16
|
rails generate rails_ai_context:install
|
|
27
|
-
rails ai:context
|
|
28
17
|
```
|
|
29
18
|
|
|
30
|
-
|
|
19
|
+
That's it. Your AI now has 25 live MCP tools that understand your entire Rails app. Zero config.
|
|
31
20
|
|
|
32
|
-
|
|
21
|
+
---
|
|
33
22
|
|
|
34
|
-
|
|
23
|
+
## What AI gets wrong without this
|
|
35
24
|
|
|
36
|
-
|
|
25
|
+
Right now, your AI agent:
|
|
37
26
|
|
|
38
|
-
|
|
27
|
+
- **Reads all 2,000 lines of schema.rb** to find one column type
|
|
28
|
+
- **Misses encrypted columns** — doesn't know `gemini_api_key` is encrypted
|
|
29
|
+
- **Shows 25 Devise methods** as if they're your code
|
|
30
|
+
- **Doesn't see inherited filters** — misses `authenticate_user!` from ApplicationController
|
|
31
|
+
- **Uses underscores in Stimulus HTML** — `data-cook_status` instead of `data-cook-status`
|
|
32
|
+
- **Breaks Turbo Stream wiring** — broadcasts to channels nobody subscribes to
|
|
33
|
+
- **Permits wrong params** — doesn't cross-check against your schema
|
|
34
|
+
- **Guesses your UI patterns** — invents new button styles instead of matching yours
|
|
39
35
|
|
|
40
|
-
|
|
36
|
+
**Every wrong guess = a wasted iteration.** You fix it, re-run, it breaks something else.
|
|
41
37
|
|
|
42
|
-
|
|
38
|
+
---
|
|
43
39
|
|
|
44
|
-
|
|
40
|
+
## What AI knows with this
|
|
45
41
|
|
|
46
|
-
|
|
42
|
+
One call. Full picture.
|
|
47
43
|
|
|
48
44
|
```
|
|
49
|
-
|
|
50
|
-
Agent: rails_get_model_details(model:"Cook") → associations, validations, scopes, callbacks, concern methods
|
|
51
|
-
Agent: rails_get_controllers(controller:"cooks", action:"create") → source code + applicable filters + strong params body
|
|
52
|
-
Agent: rails_validate(files:["app/models/cook.rb"], level:"rails") → catches column/route/partial errors before execution
|
|
45
|
+
rails_search_code(pattern: "can_cook?", match_type: "trace")
|
|
53
46
|
```
|
|
54
47
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
48
|
+
```
|
|
49
|
+
# Trace: can_cook?
|
|
50
|
+
|
|
51
|
+
## Definition
|
|
52
|
+
app/models/concerns/plan_limitable.rb:8
|
|
53
|
+
def can_cook?
|
|
54
|
+
p = effective_plan
|
|
55
|
+
return true if p.unlimited_cooks?
|
|
56
|
+
(cooks_this_month || 0) < p.cooks_per_month
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
## Calls internally
|
|
60
|
+
- unlimited_cooks?
|
|
61
|
+
|
|
62
|
+
## Called from (8 sites)
|
|
63
|
+
### app/controllers/cooks_controller.rb (Controller)
|
|
64
|
+
24: unless current_user.can_cook?
|
|
65
|
+
### app/views/cooks/new.html.erb (View)
|
|
66
|
+
7: <% unless current_user.can_cook? %>
|
|
67
|
+
### test/models/concerns/plan_limitable_test.rb (Test)
|
|
68
|
+
24: assert @user.can_cook?
|
|
69
|
+
29: assert_not @user.can_cook?
|
|
70
|
+
```
|
|
66
71
|
|
|
67
|
-
|
|
72
|
+
Definition + source code + every caller grouped by type + what it calls internally. **One tool call replaces 6 file reads.**
|
|
68
73
|
|
|
69
74
|
---
|
|
70
75
|
|
|
71
|
-
##
|
|
76
|
+
## Measured token savings
|
|
72
77
|
|
|
73
|
-
|
|
74
|
-
|-------|--------|---------------|
|
|
75
|
-
| **rails-ai-context (full)** | **28,834** | 25 MCP tools + generated docs + split rules |
|
|
76
|
-
| rails-ai-context CLAUDE.md only | 33,106 | Generated docs + rules, no MCP tools |
|
|
77
|
-
| Normal Claude `/init` | 40,700 | Generic CLAUDE.md only |
|
|
78
|
-
| No rails-ai-context | 45,477 | Nothing — discovers everything from scratch |
|
|
78
|
+
Tested on a real Rails 8 app (5 models, 19 controllers, 95 routes):
|
|
79
79
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
```
|
|
80
|
+
| Task | Without gem | With gem | Saved |
|
|
81
|
+
|------|-------------|----------|-------|
|
|
82
|
+
| Trace a method across the codebase | ~9,080 tokens (read 5 files) | ~198 tokens (1 MCP call) | **98%** |
|
|
83
|
+
| Understand a feature (schema + model + controller) | ~5,200 tokens (read 3 files) | ~1,500 tokens (2 MCP calls) | **71%** |
|
|
84
|
+
| Check all table columns | ~2,573 tokens (read schema.rb) | ~908 tokens (1 MCP call) | **65%** |
|
|
86
85
|
|
|
87
|
-
|
|
86
|
+
"Without" = AI reads files it would realistically need. "With" = MCP tools return only what's relevant.
|
|
88
87
|
|
|
89
|
-
>
|
|
88
|
+
> Savings scale with app size. A 50-model app reads more files per task — MCP calls stay the same size.
|
|
90
89
|
|
|
91
|
-
But
|
|
90
|
+
But tokens are the side effect. The real value:
|
|
92
91
|
|
|
93
|
-
- **
|
|
94
|
-
- **Cross-file
|
|
95
|
-
- **
|
|
96
|
-
- **No stale context** — live reload invalidates caches when files change mid-session
|
|
92
|
+
- **First attempt is correct** — AI understands associations, callbacks, and constraints before writing code
|
|
93
|
+
- **Cross-file validation** — catches wrong columns, missing partials, broken routes in one call
|
|
94
|
+
- **Matches your patterns** — your button classes, your test style, your flash messages
|
|
97
95
|
|
|
98
96
|
---
|
|
99
97
|
|
|
100
98
|
## 25 Live MCP Tools
|
|
101
99
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
| Tool |
|
|
105
|
-
|
|
106
|
-
| `
|
|
107
|
-
| `
|
|
108
|
-
| `
|
|
109
|
-
| `
|
|
110
|
-
| `
|
|
111
|
-
| `
|
|
112
|
-
| `
|
|
113
|
-
| `
|
|
114
|
-
| `
|
|
115
|
-
| `
|
|
116
|
-
| `
|
|
117
|
-
| `
|
|
118
|
-
| `
|
|
119
|
-
| `
|
|
120
|
-
| `
|
|
121
|
-
| `
|
|
122
|
-
| `
|
|
123
|
-
| `
|
|
124
|
-
| `
|
|
125
|
-
| `rails_get_service_pattern` | Service objects — interface, dependencies, side effects, error handling, calling convention |
|
|
126
|
-
| `rails_get_job_pattern` | Background jobs — queue, retries, guard clauses, service calls, Turbo broadcasts, schedules |
|
|
100
|
+
Every tool is **read-only** and returns structured, token-efficient data. Start with `detail:"summary"`, drill into specifics.
|
|
101
|
+
|
|
102
|
+
| Tool | One-liner |
|
|
103
|
+
|------|-----------|
|
|
104
|
+
| `rails_search_code` | **Trace mode**: definition + class context + source + internal calls + sibling methods + callers with route chain + test coverage. Also: `"definition"`, `"call"`, `"class"` filters, smart pagination |
|
|
105
|
+
| `rails_get_context` | **Composite**: schema + model + controller + routes + views in one call |
|
|
106
|
+
| `rails_analyze_feature` | **Full-stack**: everything about a feature — models, controllers, routes, services, jobs, views, Stimulus, tests |
|
|
107
|
+
| `rails_validate` | **Syntax + semantic + security** in one call. Catches wrong columns, missing partials, broken routes, Brakeman vulnerabilities |
|
|
108
|
+
| `rails_get_controllers` | Action source code + inherited filters + render map + side effects + private methods inline |
|
|
109
|
+
| `rails_get_schema` | Columns with `[indexed]`, `[unique]`, `[encrypted]`, `[default: value]` hints + model name inline |
|
|
110
|
+
| `rails_get_model_details` | Associations, validations, scopes with lambda body, enum backing types, macros, delegations, constants |
|
|
111
|
+
| `rails_get_routes` | Code-ready helpers (`cook_path(@record)`), controller filters inline, required params |
|
|
112
|
+
| `rails_get_view` | Templates with ivars, Turbo wiring, Stimulus refs, partial locals — pipe-separated, scannable |
|
|
113
|
+
| `rails_get_stimulus` | Copy-paste HTML data-attributes (dashes, not underscores) + reverse view lookup |
|
|
114
|
+
| `rails_get_design_system` | Canonical HTML/ERB copy-paste patterns for buttons, inputs, cards, modals |
|
|
115
|
+
| `rails_get_test_info` | Fixture contents with relationships + test template matching your app's patterns |
|
|
116
|
+
| `rails_get_conventions` | Your app's actual patterns — auth checks, flash messages, create action template, test patterns |
|
|
117
|
+
| `rails_get_turbo_map` | Broadcast → subscription wiring with mismatch warnings |
|
|
118
|
+
| `rails_get_partial_interface` | Partial locals contract — what to pass, what methods are called on each local |
|
|
119
|
+
| `rails_get_concern` | Public methods, signatures, which models include it |
|
|
120
|
+
| `rails_get_callbacks` | Callbacks in Rails execution order with source code |
|
|
121
|
+
| `rails_get_service_pattern` | Interface, dependencies, side effects, error handling, who calls it |
|
|
122
|
+
| `rails_get_job_pattern` | Queue, retries, guard clauses, Turbo broadcasts, schedules |
|
|
127
123
|
| `rails_get_env` | Environment variables, credentials keys (not values), external service dependencies |
|
|
128
|
-
| `
|
|
129
|
-
| `
|
|
130
|
-
| `
|
|
124
|
+
| `rails_get_helper_methods` | App + framework helpers with view usage cross-references |
|
|
125
|
+
| `rails_get_config` | Database adapter, auth framework, assets stack, Action Cable, middleware |
|
|
126
|
+
| `rails_get_gems` | Notable gems with versions, categories, and config file locations |
|
|
127
|
+
| `rails_get_edit_context` | Method-aware code extraction with class/method context header |
|
|
128
|
+
| `rails_security_scan` | Brakeman static analysis — SQL injection, XSS, mass assignment |
|
|
131
129
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
Schema, routes, models, and controllers tools support a `detail` parameter — critical for large apps:
|
|
135
|
-
|
|
136
|
-
| Level | Returns | Default limit |
|
|
137
|
-
|-------|---------|---------------|
|
|
138
|
-
| `summary` | Names + counts | 50 |
|
|
139
|
-
| `standard` | Names + key details *(default)* | 25 |
|
|
140
|
-
| `full` | Everything (indexes, FKs, constraints) | 10 |
|
|
141
|
-
|
|
142
|
-
```ruby
|
|
143
|
-
rails_get_schema(detail: "summary") # → all tables with column counts
|
|
144
|
-
rails_get_schema(table: "users") # → full detail for one table
|
|
145
|
-
rails_get_routes(controller: "users") # → routes for one controller
|
|
146
|
-
rails_get_model_details(model: "User") # → associations, validations, scopes
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
A safety net (`max_tool_response_chars`, default 200K) truncates oversized responses with hints to use filters.
|
|
130
|
+
> **[Full parameter documentation →](docs/GUIDE.md)**
|
|
150
131
|
|
|
151
132
|
---
|
|
152
133
|
|
|
153
|
-
##
|
|
154
|
-
|
|
155
|
-
`rails ai:context` generates context files tailored to each AI assistant:
|
|
134
|
+
## How It Works
|
|
156
135
|
|
|
157
136
|
```
|
|
158
|
-
|
|
159
|
-
│
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
│
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
│
|
|
166
|
-
│
|
|
167
|
-
|
|
168
|
-
│
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
│
|
|
172
|
-
│
|
|
173
|
-
│
|
|
174
|
-
│
|
|
175
|
-
│
|
|
176
|
-
|
|
177
|
-
├── ⚡ OpenCode
|
|
178
|
-
│ ├── AGENTS.md native OpenCode context
|
|
179
|
-
│ ├── app/models/AGENTS.md auto-loaded when editing models
|
|
180
|
-
│ └── app/controllers/AGENTS.md auto-loaded when editing controllers
|
|
181
|
-
│
|
|
182
|
-
├── 🔵 Windsurf
|
|
183
|
-
│ ├── .windsurfrules ≤5,800 chars (6K limit)
|
|
184
|
-
│ └── .windsurf/rules/
|
|
185
|
-
│ ├── rails-context.md project overview
|
|
186
|
-
│ ├── rails-ui-patterns.md CSS component patterns
|
|
187
|
-
│ └── rails-mcp-tools.md tool reference
|
|
188
|
-
│
|
|
189
|
-
├── 🟠 GitHub Copilot
|
|
190
|
-
│ ├── .github/copilot-instructions.md ≤500 lines (compact)
|
|
191
|
-
│ └── .github/instructions/
|
|
192
|
-
│ ├── rails-context.instructions.md applyTo: **/*
|
|
193
|
-
│ ├── rails-models.instructions.md applyTo: app/models/**
|
|
194
|
-
│ ├── rails-controllers.instructions.md applyTo: app/controllers/**
|
|
195
|
-
│ ├── rails-ui-patterns.instructions.md applyTo: app/views/**
|
|
196
|
-
│ └── rails-mcp-tools.instructions.md applyTo: **/*
|
|
197
|
-
│
|
|
198
|
-
├── 📋 .ai-context.json full JSON (programmatic)
|
|
199
|
-
└── .mcp.json MCP auto-discovery
|
|
137
|
+
┌─────────────────────────────────────────────────────────┐
|
|
138
|
+
│ Your Rails App │
|
|
139
|
+
│ models + schema + routes + controllers + views + jobs │
|
|
140
|
+
└────────────────────────┬────────────────────────────────┘
|
|
141
|
+
│ introspects
|
|
142
|
+
▼
|
|
143
|
+
┌─────────────────────────────────────────────────────────┐
|
|
144
|
+
│ rails-ai-context (29 introspectors) │
|
|
145
|
+
│ Parses everything. Caches results. Zero config. │
|
|
146
|
+
└────────┬─────────────────────────────┬──────────────────┘
|
|
147
|
+
│ │
|
|
148
|
+
▼ ▼
|
|
149
|
+
┌────────────────────┐ ┌───────────────────────────────┐
|
|
150
|
+
│ Static Files │ │ Live MCP Server (25 tools) │
|
|
151
|
+
│ CLAUDE.md │ │ Real-time queries on demand │
|
|
152
|
+
│ .cursor/rules/ │ │ Schema, models, routes, etc. │
|
|
153
|
+
│ .github/instr... │ │ Trace, validate, analyze │
|
|
154
|
+
│ .windsurfrules │ │ Auto-discovered via .mcp.json │
|
|
155
|
+
└────────────────────┘ └───────────────────────────────┘
|
|
200
156
|
```
|
|
201
157
|
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
---
|
|
205
|
-
|
|
206
|
-
## What Your AI Learns
|
|
207
|
-
|
|
208
|
-
| Category | What's introspected |
|
|
209
|
-
|----------|-------------------|
|
|
210
|
-
| **Database** | Every table, column, index, foreign key, and migration |
|
|
211
|
-
| **Models** | Associations, validations, scopes, enums, callbacks, concerns, macros |
|
|
212
|
-
| **Routing** | Every route with HTTP verbs, paths, controller actions, API namespaces |
|
|
213
|
-
| **Controllers** | Actions, filters, strong params, concerns, API controllers |
|
|
214
|
-
| **Views** | Layouts, templates, partials, helpers, template engines, view components |
|
|
215
|
-
| **Frontend** | Stimulus controllers (targets, values, actions, outlets), Turbo Frames/Streams |
|
|
216
|
-
| **Background** | ActiveJob classes, mailers, Action Cable channels |
|
|
217
|
-
| **Gems** | 70+ notable gems categorized (Devise = auth, Sidekiq = jobs, Pundit = authorization) |
|
|
218
|
-
| **Auth** | Devise modules, Pundit policies, CanCanCan, has_secure_password, CORS, CSP |
|
|
219
|
-
| **API** | Serializers, GraphQL, versioning, rate limiting, API-only mode |
|
|
220
|
-
| **Testing** | Framework, factories/fixtures, CI config, coverage, system tests |
|
|
221
|
-
| **Config** | Cache store, session store, middleware, initializers, timezone |
|
|
222
|
-
| **DevOps** | Puma, Procfile, Docker, deployment tools, asset pipeline |
|
|
223
|
-
| **Architecture** | Service objects, STI, polymorphism, state machines, multi-tenancy, engines |
|
|
224
|
-
|
|
225
|
-
29 introspectors total. The `:full` preset runs 28 by default; use `:standard` for 13 core only (`database_stats` is opt-in, PostgreSQL only).
|
|
158
|
+
The install generator asks which AI tools you use and only generates files for those.
|
|
226
159
|
|
|
227
160
|
---
|
|
228
161
|
|
|
229
|
-
##
|
|
230
|
-
|
|
231
|
-
The install generator creates `.mcp.json` — **Claude Code and Cursor auto-detect it**. No manual config needed.
|
|
162
|
+
## Install
|
|
232
163
|
|
|
233
|
-
|
|
164
|
+
```bash
|
|
165
|
+
# Add to Gemfile
|
|
166
|
+
gem "rails-ai-context", group: :development
|
|
234
167
|
|
|
235
|
-
|
|
168
|
+
# Install (picks your AI tools, generates context)
|
|
169
|
+
rails generate rails_ai_context:install
|
|
236
170
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
Add to `~/Library/Application Support/Claude/claude_desktop_config.json` (macOS):
|
|
241
|
-
|
|
242
|
-
```json
|
|
243
|
-
{
|
|
244
|
-
"mcpServers": {
|
|
245
|
-
"rails-ai-context": {
|
|
246
|
-
"command": "bundle",
|
|
247
|
-
"args": ["exec", "rails", "ai:serve"],
|
|
248
|
-
"cwd": "/path/to/your/rails/app"
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
171
|
+
# Or generate context directly
|
|
172
|
+
rails ai:context
|
|
252
173
|
```
|
|
253
|
-
</details>
|
|
254
|
-
|
|
255
|
-
<details>
|
|
256
|
-
<summary><strong>HTTP transport (for remote clients)</strong></summary>
|
|
257
174
|
|
|
258
|
-
|
|
259
|
-
rails ai:serve_http # Starts at http://127.0.0.1:6029/mcp
|
|
260
|
-
```
|
|
175
|
+
Both commands ask which AI tools you use (Claude, Cursor, Copilot, Windsurf, OpenCode) and only generate what you need.
|
|
261
176
|
|
|
262
|
-
|
|
177
|
+
MCP auto-discovery: `.mcp.json` is detected automatically by Claude Code and Cursor. No manual config.
|
|
263
178
|
|
|
264
|
-
|
|
265
|
-
RailsAiContext.configure do |config|
|
|
266
|
-
config.auto_mount = true
|
|
267
|
-
config.http_path = "/mcp"
|
|
268
|
-
end
|
|
269
|
-
```
|
|
270
|
-
</details>
|
|
179
|
+
> **[Full Guide →](docs/GUIDE.md)** — every command, parameter, and configuration option.
|
|
271
180
|
|
|
272
181
|
---
|
|
273
182
|
|
|
@@ -276,31 +185,17 @@ end
|
|
|
276
185
|
```ruby
|
|
277
186
|
# config/initializers/rails_ai_context.rb
|
|
278
187
|
RailsAiContext.configure do |config|
|
|
279
|
-
#
|
|
280
|
-
config.
|
|
281
|
-
|
|
282
|
-
# Cherry-pick on top of a preset
|
|
283
|
-
# config.introspectors += %i[views turbo auth api]
|
|
188
|
+
# Which AI tools to generate context for (selected during install)
|
|
189
|
+
# config.ai_tools = %i[claude cursor]
|
|
284
190
|
|
|
285
|
-
#
|
|
286
|
-
# config.
|
|
191
|
+
# Presets: :full (28 introspectors, default) or :standard (13 core)
|
|
192
|
+
# config.preset = :full
|
|
287
193
|
|
|
288
194
|
# Exclude models from introspection
|
|
289
|
-
config.excluded_models += %w[AdminUser
|
|
290
|
-
|
|
291
|
-
# Exclude paths from code search
|
|
292
|
-
config.excluded_paths += %w[vendor/bundle]
|
|
293
|
-
|
|
294
|
-
# Cache TTL for MCP tool responses (seconds)
|
|
295
|
-
config.cache_ttl = 60
|
|
195
|
+
# config.excluded_models += %w[AdminUser]
|
|
296
196
|
|
|
297
|
-
#
|
|
298
|
-
#
|
|
299
|
-
# config.live_reload = :auto
|
|
300
|
-
|
|
301
|
-
# Skip root files (CLAUDE.md, .windsurfrules, etc.) — only generate split rules
|
|
302
|
-
# Lets you manage root files yourself while still getting .claude/rules/, .cursor/rules/, etc.
|
|
303
|
-
# config.generate_root_files = false
|
|
197
|
+
# Skip specific MCP tools
|
|
198
|
+
# config.skip_tools = %w[rails_security_scan]
|
|
304
199
|
end
|
|
305
200
|
```
|
|
306
201
|
|
|
@@ -316,122 +211,39 @@ end
|
|
|
316
211
|
| `context_mode` | `:compact` | `:compact` (≤150 lines) or `:full` (dump everything) |
|
|
317
212
|
| `claude_max_lines` | `150` | Max lines for CLAUDE.md in compact mode |
|
|
318
213
|
| `generate_root_files` | `true` | Generate root files (CLAUDE.md, etc.) — set `false` for split rules only |
|
|
319
|
-
| `
|
|
214
|
+
| `ai_tools` | `nil` (all) | AI tools to generate context for: `%i[claude cursor copilot windsurf opencode]` |
|
|
320
215
|
| **MCP Server** | | |
|
|
321
|
-
| `server_name` | `"rails-ai-context"` | MCP server name |
|
|
322
|
-
| `server_version` | gem version | MCP server version |
|
|
323
|
-
| `auto_mount` | `false` | Auto-mount HTTP MCP endpoint |
|
|
324
|
-
| `http_path` | `"/mcp"` | HTTP endpoint path |
|
|
325
|
-
| `http_port` | `6029` | HTTP server port |
|
|
326
|
-
| `http_bind` | `"127.0.0.1"` | HTTP server bind address |
|
|
327
216
|
| `cache_ttl` | `60` | Cache TTL in seconds |
|
|
328
217
|
| `max_tool_response_chars` | `200_000` | Safety cap for MCP tool responses |
|
|
329
218
|
| `live_reload` | `:auto` | `:auto`, `true`, or `false` — MCP live reload |
|
|
330
|
-
| `
|
|
331
|
-
| **Filtering & Exclusions** | | |
|
|
332
|
-
| `excluded_models` | internal Rails models | Models to skip during introspection |
|
|
333
|
-
| `excluded_paths` | `node_modules tmp log vendor .git` | Paths excluded from code search |
|
|
334
|
-
| `sensitive_patterns` | `.env .env.* config/master.key config/credentials.yml.enc config/credentials/*.yml.enc *.pem *.key` | File patterns blocked from search and read tools |
|
|
335
|
-
| `excluded_controllers` | `DeviseController` etc. | Controller classes hidden from listings |
|
|
336
|
-
| `excluded_route_prefixes` | `action_mailbox/ active_storage/ rails/` etc. | Route controller prefixes hidden with app_only |
|
|
337
|
-
| `excluded_concerns` | Rails/Devise/framework patterns | Regex patterns for concerns to hide |
|
|
338
|
-
| `excluded_filters` | `verify_authenticity_token` etc. | Framework filter names hidden from controller output |
|
|
339
|
-
| `excluded_middleware` | standard Rack/Rails middleware | Default middleware hidden from config output |
|
|
219
|
+
| `auto_mount` | `false` | Auto-mount HTTP MCP endpoint |
|
|
340
220
|
| **File Size Limits** | | |
|
|
341
|
-
| `max_file_size` | `5_000_000` | Per-file read limit
|
|
342
|
-
| `max_test_file_size` | `1_000_000` | Test file read limit (bytes) |
|
|
343
|
-
| `max_schema_file_size` | `10_000_000` | schema.rb / structure.sql parse limit (bytes) |
|
|
344
|
-
| `max_view_total_size` | `10_000_000` | Total aggregated view content for UI patterns (bytes) |
|
|
345
|
-
| `max_view_file_size` | `1_000_000` | Per-view file during aggregation (bytes) |
|
|
221
|
+
| `max_file_size` | `5_000_000` | Per-file read limit (bytes) |
|
|
346
222
|
| `max_search_results` | `200` | Max search results per call |
|
|
347
223
|
| `max_validate_files` | `50` | Max files per validate call |
|
|
348
|
-
| **Search & Discovery** | | |
|
|
349
|
-
| `search_extensions` | `rb js erb yml yaml json ts tsx vue svelte haml slim` | File extensions for Ruby fallback search |
|
|
350
|
-
| `concern_paths` | `app/models/concerns app/controllers/concerns` | Where to look for concern source files |
|
|
351
224
|
| **Extensibility** | | |
|
|
352
|
-
| `custom_tools` | `[]` | Additional MCP tool classes
|
|
353
|
-
| `skip_tools` | `[]` | Built-in tool names to exclude
|
|
354
|
-
| `ai_tools` | `nil` (all) | AI tools to generate context for: `%i[claude cursor copilot windsurf opencode]` |
|
|
225
|
+
| `custom_tools` | `[]` | Additional MCP tool classes |
|
|
226
|
+
| `skip_tools` | `[]` | Built-in tool names to exclude |
|
|
355
227
|
</details>
|
|
356
228
|
|
|
357
229
|
---
|
|
358
230
|
|
|
359
231
|
## Commands
|
|
360
232
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
| Command | Description |
|
|
233
|
+
| Command | What it does |
|
|
364
234
|
|---------|-------------|
|
|
365
|
-
| `rails ai:context` | Generate
|
|
366
|
-
| `rails ai:context:full` | Generate all files in full mode (dumps everything) |
|
|
367
|
-
| `rails ai:context:claude` | Generate Claude Code files only |
|
|
368
|
-
| `rails ai:context:opencode` | Generate OpenCode files only |
|
|
369
|
-
| `rails ai:context:cursor` | Generate Cursor files only |
|
|
370
|
-
| `rails ai:context:windsurf` | Generate Windsurf files only |
|
|
371
|
-
| `rails ai:context:copilot` | Generate Copilot files only |
|
|
372
|
-
| `rails ai:context:json` | Generate JSON context file only |
|
|
235
|
+
| `rails ai:context` | Generate context files for your AI tools |
|
|
373
236
|
| `rails ai:serve` | Start MCP server (stdio) |
|
|
374
|
-
| `rails ai:
|
|
375
|
-
| `rails ai:
|
|
376
|
-
| `rails ai:
|
|
377
|
-
| `rails ai:inspect` | Print introspection summary to stdout |
|
|
378
|
-
|
|
379
|
-
### Standalone CLI
|
|
380
|
-
|
|
381
|
-
The gem also ships a `rails-ai-context` executable — an alternative to rake tasks.
|
|
382
|
-
|
|
383
|
-
| Command | Equivalent rake task |
|
|
384
|
-
|---------|---------------------|
|
|
385
|
-
| `rails-ai-context serve` | `rails ai:serve` |
|
|
386
|
-
| `rails-ai-context serve --transport http` | `rails ai:serve_http` |
|
|
387
|
-
| `rails-ai-context context` | `rails ai:context` |
|
|
388
|
-
| `rails-ai-context context --format claude` | `rails ai:context:claude` |
|
|
389
|
-
| `rails-ai-context doctor` | `rails ai:doctor` |
|
|
390
|
-
| `rails-ai-context watch` | `rails ai:watch` |
|
|
391
|
-
| `rails-ai-context inspect` | `rails ai:inspect` |
|
|
392
|
-
| `rails-ai-context version` | — |
|
|
393
|
-
|
|
394
|
-
Run from your Rails app root. Use `rails-ai-context help` for all options.
|
|
395
|
-
|
|
396
|
-
---
|
|
397
|
-
|
|
398
|
-
## Stack Compatibility
|
|
399
|
-
|
|
400
|
-
Works with every Rails architecture — auto-detects what's relevant:
|
|
401
|
-
|
|
402
|
-
| Setup | Coverage | Notes |
|
|
403
|
-
|-------|----------|-------|
|
|
404
|
-
| Rails full-stack (ERB + Hotwire) | 29/29 | All introspectors relevant |
|
|
405
|
-
| Rails + Inertia.js (React/Vue) | ~22/29 | Views/Turbo partially useful, backend fully covered |
|
|
406
|
-
| Rails API + React/Next.js SPA | ~20/29 | Schema, models, routes, API, auth, jobs — all covered |
|
|
407
|
-
| Rails API + mobile app | ~20/29 | Same as SPA — backend introspection is identical |
|
|
408
|
-
| Rails engine (mountable gem) | ~15/29 | Core introspectors (schema, models, routes, gems) work |
|
|
409
|
-
|
|
410
|
-
Frontend introspectors (views, Turbo, Stimulus, assets) degrade gracefully — they report nothing when those features aren't present.
|
|
411
|
-
|
|
412
|
-
---
|
|
413
|
-
|
|
414
|
-
## Works Without a Database
|
|
415
|
-
|
|
416
|
-
The gem parses `db/schema.rb` as text when no database is connected. Works in CI, Docker build stages, and Claude Code sessions without a running DB.
|
|
237
|
+
| `rails ai:doctor` | Diagnostics + AI readiness score |
|
|
238
|
+
| `rails ai:watch` | Auto-regenerate on file changes |
|
|
239
|
+
| `rails ai:inspect` | Print introspection summary |
|
|
417
240
|
|
|
418
241
|
---
|
|
419
242
|
|
|
420
243
|
## Requirements
|
|
421
244
|
|
|
422
245
|
- Ruby >= 3.2, Rails >= 7.1
|
|
423
|
-
-
|
|
424
|
-
- Optional: `listen` gem for watch mode, `ripgrep` for fast code search
|
|
425
|
-
|
|
426
|
-
---
|
|
427
|
-
|
|
428
|
-
## vs. Other Ruby MCP Projects
|
|
429
|
-
|
|
430
|
-
| Project | Approach | rails-ai-context |
|
|
431
|
-
|---------|----------|-----------------|
|
|
432
|
-
| [Official Ruby SDK](https://github.com/modelcontextprotocol/ruby-sdk) | Low-level protocol library | We **use** this as our foundation |
|
|
433
|
-
| [fast-mcp](https://github.com/yjacquin/fast-mcp) | Generic MCP framework | We're a **product** — zero-config Rails introspection |
|
|
434
|
-
| [rails-mcp-server](https://github.com/maquina-app/rails-mcp-server) | Manual config (`projects.yml`) | We auto-discover everything |
|
|
246
|
+
- Optional: `brakeman` for security scanning, `listen` for watch mode, `ripgrep` for fast search
|
|
435
247
|
|
|
436
248
|
---
|
|
437
249
|
|
|
@@ -440,7 +252,7 @@ The gem parses `db/schema.rb` as text when no database is connected. Works in CI
|
|
|
440
252
|
```bash
|
|
441
253
|
git clone https://github.com/crisnahine/rails-ai-context.git
|
|
442
254
|
cd rails-ai-context && bundle install
|
|
443
|
-
bundle exec rspec #
|
|
255
|
+
bundle exec rspec # 575 examples
|
|
444
256
|
bundle exec rubocop # Lint
|
|
445
257
|
```
|
|
446
258
|
|
|
@@ -448,7 +260,7 @@ Bug reports and pull requests welcome at [github.com/crisnahine/rails-ai-context
|
|
|
448
260
|
|
|
449
261
|
## Sponsorship
|
|
450
262
|
|
|
451
|
-
If rails-ai-context
|
|
263
|
+
If rails-ai-context saves you time, consider [becoming a sponsor](https://github.com/sponsors/crisnahine).
|
|
452
264
|
|
|
453
265
|
## License
|
|
454
266
|
|
data/docs/GUIDE.md
CHANGED
|
@@ -564,7 +564,7 @@ Ripgrep-powered regex search across the codebase.
|
|
|
564
564
|
| `pattern` | string | **Required.** Regex pattern or method name to search for. |
|
|
565
565
|
| `path` | string | Subdirectory to search in (e.g. `app/models`, `config`). Default: entire app. |
|
|
566
566
|
| `file_type` | string | Filter by file extension (e.g. `rb`, `erb`, `js`). Alphanumeric only. |
|
|
567
|
-
| `match_type` | string | `any` (default), `definition` (def lines), `class` (class/module lines), `call` (call sites only), `trace` (**full picture** — definition + source + callers +
|
|
567
|
+
| `match_type` | string | `any` (default), `definition` (def lines), `class` (class/module lines), `call` (call sites only), `trace` (**full picture** — definition with class context + source code + internal calls + sibling methods + callers with route chain + test coverage separated). |
|
|
568
568
|
| `exact_match` | boolean | Match whole words only (wraps pattern in `\b` boundaries). Default: false. |
|
|
569
569
|
| `exclude_tests` | boolean | Exclude test/spec/features directories. Default: false. |
|
|
570
570
|
| `group_by_file` | boolean | Group results by file with match counts. Default: false. |
|
|
@@ -577,7 +577,8 @@ Smart result limiting: <10 results shows all, 10-100 shows half, >100 caps at 10
|
|
|
577
577
|
|
|
578
578
|
```
|
|
579
579
|
rails_search_code(pattern: "can_cook?", match_type: "trace")
|
|
580
|
-
→ FULL PICTURE: definition
|
|
580
|
+
→ FULL PICTURE: definition with class context + source code + internal calls
|
|
581
|
+
+ sibling methods + app callers with route chain + test coverage (separated)
|
|
581
582
|
|
|
582
583
|
rails_search_code(pattern: "create", match_type: "definition")
|
|
583
584
|
→ Only `def create` / `def self.create` lines
|
|
@@ -344,8 +344,8 @@ module RailsAiContext
|
|
|
344
344
|
"",
|
|
345
345
|
"## Tools (25)",
|
|
346
346
|
"",
|
|
347
|
-
"**rails_get_schema** — database tables, columns, indexes, foreign keys",
|
|
348
|
-
"- `rails_get_schema(detail:\"summary\")` — all tables with column counts",
|
|
347
|
+
"**rails_get_schema** — database tables, columns, indexes, foreign keys, orphaned table detection",
|
|
348
|
+
"- `rails_get_schema(detail:\"summary\")` — all tables with column counts + orphaned tables (tables with no ActiveRecord model)",
|
|
349
349
|
"- `rails_get_schema(table:\"users\")` — full detail for one table",
|
|
350
350
|
"",
|
|
351
351
|
"**rails_get_model_details** — associations, validations, scopes, enums, callbacks",
|
|
@@ -381,7 +381,7 @@ module RailsAiContext
|
|
|
381
381
|
"- `rails_validate(files:[\"app/models/cook.rb\"])` — checks Ruby, ERB, JS syntax in one call",
|
|
382
382
|
"",
|
|
383
383
|
"**rails_analyze_feature** — combined schema + models + controllers + routes for a feature area",
|
|
384
|
-
"- `rails_analyze_feature(feature:\"authentication\")` — one call gets everything related to a feature",
|
|
384
|
+
"- `rails_analyze_feature(feature:\"authentication\")` — one call gets everything related to a feature, including inherited filters from parent controllers and code-ready route helpers",
|
|
385
385
|
"",
|
|
386
386
|
"**rails_get_design_system** — color palette, component patterns, canonical page examples",
|
|
387
387
|
"- `rails_get_design_system(detail:\"standard\")` — colors + components + real HTML examples + design rules",
|
|
@@ -390,13 +390,17 @@ module RailsAiContext
|
|
|
390
390
|
"**rails_get_config** — cache store, session, timezone, middleware, initializers",
|
|
391
391
|
"**rails_get_gems** — notable gems categorized by function",
|
|
392
392
|
"**rails_get_conventions** — architecture patterns, directory structure",
|
|
393
|
-
"**rails_search_code** — regex search
|
|
393
|
+
"**rails_search_code** — regex search with trace mode for deep call-chain analysis",
|
|
394
|
+
"- `rails_search_code(pattern:\"regex\", file_type:\"rb\")` — basic regex search across files",
|
|
395
|
+
"- `rails_search_code(pattern:\"can_cook?\", match_type:\"trace\")` — trace mode: definition + class context + source + siblings + callers with route chain + test coverage",
|
|
396
|
+
"- `rails_search_code(pattern:\"cook\", exclude_tests: true)` — search app code only, skip test files",
|
|
394
397
|
"",
|
|
395
398
|
"**rails_security_scan** — Brakeman security analysis",
|
|
396
399
|
"- `rails_security_scan` — run security scan, returns warnings by confidence",
|
|
397
400
|
"",
|
|
398
401
|
"**rails_get_concern** — concern methods and includers",
|
|
399
402
|
"- `rails_get_concern(name:\"Searchable\")` — methods, included modules, includers",
|
|
403
|
+
"- `rails_get_concern(name:\"Searchable\", detail:\"full\")` — + method source code",
|
|
400
404
|
"",
|
|
401
405
|
"**rails_get_callbacks** — model callbacks in execution order",
|
|
402
406
|
"- `rails_get_callbacks(model:\"User\")` — before/after/around callbacks by type",
|
|
@@ -214,11 +214,11 @@ module RailsAiContext
|
|
|
214
214
|
"### Quick Reference",
|
|
215
215
|
"| Need | Use this MCP tool | Do NOT use |",
|
|
216
216
|
"|------|-------------------|------------|",
|
|
217
|
-
"| Column types | `rails_get_schema(table:\"x\")` | Read db/schema.rb |",
|
|
217
|
+
"| Column types | `rails_get_schema(table:\"x\")` — includes orphaned table warnings | Read db/schema.rb |",
|
|
218
218
|
"| Model associations | `rails_get_model_details(model:\"X\")` | Read app/models/x.rb |",
|
|
219
219
|
"| Route paths | `rails_get_routes(controller:\"x\")` | Read config/routes.rb |",
|
|
220
|
-
"| Feature overview | `rails_analyze_feature(feature:\"x\")` | Explore agent / Grep |",
|
|
221
|
-
"| Find code | `rails_search_code(pattern:\"x\")` | Grep tool |",
|
|
220
|
+
"| Feature overview | `rails_analyze_feature(feature:\"x\")` — models + controllers (inherited filters) + routes (code-ready helpers) + services + jobs + views + tests + gaps | Explore agent / Grep |",
|
|
221
|
+
"| Find code | `rails_search_code(pattern:\"x\", match_type:\"trace\")` | Grep tool |",
|
|
222
222
|
"| Validate edits | `rails_validate(files:[...])` | ruby -c / erb / node |",
|
|
223
223
|
"| Controller logic | `rails_get_controllers(controller:\"X\", action:\"y\")` | Read controller file |",
|
|
224
224
|
"| UI patterns | `rails_get_design_system` | Read view files |",
|
|
@@ -227,10 +227,11 @@ module RailsAiContext
|
|
|
227
227
|
"| Full context | `rails_get_context(model:\"X\")` | Multiple Read calls |",
|
|
228
228
|
"",
|
|
229
229
|
"### More Tools",
|
|
230
|
-
"- `rails_get_view(controller:\"x\")` | `rails_get_concern(name:\"X\")` | `rails_get_callbacks(model:\"X\")`",
|
|
230
|
+
"- `rails_get_view(controller:\"x\")` | `rails_get_concern(name:\"X\", detail:\"full\")` — concern methods with source code | `rails_get_callbacks(model:\"X\")`",
|
|
231
231
|
"- `rails_get_helper_methods` | `rails_get_service_pattern` | `rails_get_job_pattern`",
|
|
232
232
|
"- `rails_get_env` | `rails_get_partial_interface(path:\"shared/_form\")` | `rails_get_turbo_map`",
|
|
233
233
|
"- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_security_scan`",
|
|
234
|
+
"- `rails_search_code(pattern:\"method\", match_type:\"trace\")` — **trace mode**: definition + class context + source + siblings + callers + test coverage in one call",
|
|
234
235
|
"- `rails_get_edit_context(file:\"path\", near:\"keyword\")` — surgical edit helper with line numbers",
|
|
235
236
|
""
|
|
236
237
|
]
|
|
@@ -250,7 +250,13 @@ module RailsAiContext
|
|
|
250
250
|
"- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_security_scan`",
|
|
251
251
|
"- `rails_get_concern` | `rails_get_callbacks` | `rails_get_helper_methods` | `rails_get_service_pattern`",
|
|
252
252
|
"- `rails_get_job_pattern` | `rails_get_env` | `rails_get_partial_interface` | `rails_get_turbo_map`",
|
|
253
|
-
"- `rails_get_context(model:\"X\")` — composite cross-layer context in one call"
|
|
253
|
+
"- `rails_get_context(model:\"X\")` — composite cross-layer context in one call",
|
|
254
|
+
"",
|
|
255
|
+
"## Power Features",
|
|
256
|
+
"- `rails_search_code(pattern:\"method\", match_type:\"trace\")` — trace: definition + source + siblings + callers + tests",
|
|
257
|
+
"- `rails_get_concern(name:\"X\", detail:\"full\")` — concern methods with source code",
|
|
258
|
+
"- `rails_analyze_feature` — full-stack with inherited filters, route helpers, test gaps",
|
|
259
|
+
"- `rails_get_schema` — columns, indexes, defaults, encrypted hints, orphaned table warnings"
|
|
254
260
|
]
|
|
255
261
|
|
|
256
262
|
lines.join("\n")
|
|
@@ -290,7 +290,13 @@ module RailsAiContext
|
|
|
290
290
|
"- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_security_scan`",
|
|
291
291
|
"- `rails_get_concern` | `rails_get_callbacks` | `rails_get_helper_methods` | `rails_get_service_pattern`",
|
|
292
292
|
"- `rails_get_job_pattern` | `rails_get_env` | `rails_get_partial_interface` | `rails_get_turbo_map`",
|
|
293
|
-
"- `rails_get_context(model:\"X\")` — composite cross-layer context in one call"
|
|
293
|
+
"- `rails_get_context(model:\"X\")` — composite cross-layer context in one call",
|
|
294
|
+
"",
|
|
295
|
+
"## Power Features",
|
|
296
|
+
"- `rails_search_code(pattern:\"method\", match_type:\"trace\")` — trace: definition + source + siblings + callers + tests",
|
|
297
|
+
"- `rails_get_concern(name:\"X\", detail:\"full\")` — concern methods with source code",
|
|
298
|
+
"- `rails_analyze_feature` — full-stack with inherited filters, route helpers, test gaps",
|
|
299
|
+
"- `rails_get_schema` — columns, indexes, defaults, encrypted hints, orphaned table warnings"
|
|
294
300
|
]
|
|
295
301
|
|
|
296
302
|
lines.join("\n")
|
|
@@ -172,7 +172,7 @@ module RailsAiContext
|
|
|
172
172
|
"Start with `detail:\"summary\"`, then drill into specifics.",
|
|
173
173
|
"",
|
|
174
174
|
"### Mandatory Workflow",
|
|
175
|
-
"1. **Before exploring a feature**: `rails_analyze_feature(feature:\"...\")` —
|
|
175
|
+
"1. **Before exploring a feature**: `rails_analyze_feature(feature:\"...\")` — models + controllers (inherited filters) + routes (code-ready helpers) + services + jobs + views + tests + gaps",
|
|
176
176
|
"2. **Before writing migrations**: `rails_get_schema(table:\"...\")` — NOT reading db/schema.rb",
|
|
177
177
|
"3. **Before modifying a model**: `rails_get_model_details(model:\"...\")` — NOT reading the model file",
|
|
178
178
|
"4. **Before adding routes**: `rails_get_routes(controller:\"...\")` — Read only when you will edit",
|
|
@@ -182,10 +182,10 @@ module RailsAiContext
|
|
|
182
182
|
"### Do NOT Bypass",
|
|
183
183
|
"| Instead of... | Use this MCP tool |",
|
|
184
184
|
"|---------------|-------------------|",
|
|
185
|
-
"| Reading db/schema.rb | `rails_get_schema(table:\"x\")` |",
|
|
185
|
+
"| Reading db/schema.rb | `rails_get_schema(table:\"x\")` — includes orphaned table warnings |",
|
|
186
186
|
"| Reading model files | `rails_get_model_details(model:\"X\")` |",
|
|
187
187
|
"| Reading routes.rb | `rails_get_routes(controller:\"x\")` |",
|
|
188
|
-
"| Grep for code | `rails_search_code(pattern:\"x\")` |",
|
|
188
|
+
"| Grep for code | `rails_search_code(pattern:\"x\", match_type:\"trace\")` |",
|
|
189
189
|
"| Reading test files | `rails_get_test_info(model:\"X\")` |",
|
|
190
190
|
"| Reading controller | `rails_get_controllers(controller:\"X\", action:\"y\")` |",
|
|
191
191
|
"| ruby -c / erb / node | `rails_validate(files:[...])` |",
|
|
@@ -195,8 +195,9 @@ module RailsAiContext
|
|
|
195
195
|
"- `rails_get_view` | `rails_get_stimulus` | `rails_get_test_info` | `rails_analyze_feature`",
|
|
196
196
|
"- `rails_get_design_system` | `rails_get_edit_context` | `rails_validate` | `rails_search_code`",
|
|
197
197
|
"- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_security_scan`",
|
|
198
|
-
"- `rails_get_concern` | `rails_get_callbacks` | `rails_get_helper_methods` | `rails_get_service_pattern`",
|
|
198
|
+
"- `rails_get_concern(name:\"X\", detail:\"full\")` — concern methods with source code | `rails_get_callbacks` | `rails_get_helper_methods` | `rails_get_service_pattern`",
|
|
199
199
|
"- `rails_get_job_pattern` | `rails_get_env` | `rails_get_partial_interface` | `rails_get_turbo_map`",
|
|
200
|
+
"- `rails_search_code(pattern:\"method\", match_type:\"trace\")` — **trace mode**: definition + class context + source + siblings + callers + test coverage in one call",
|
|
200
201
|
"- `rails_get_context(model:\"X\")` — composite cross-layer context in one call",
|
|
201
202
|
""
|
|
202
203
|
]
|
|
@@ -95,7 +95,13 @@ module RailsAiContext
|
|
|
95
95
|
"- rails_get_config | rails_get_gems | rails_get_conventions | rails_security_scan",
|
|
96
96
|
"- rails_get_concern | rails_get_callbacks | rails_get_helper_methods | rails_get_service_pattern",
|
|
97
97
|
"- rails_get_job_pattern | rails_get_env | rails_get_partial_interface | rails_get_turbo_map",
|
|
98
|
-
"- rails_get_context(model:\"X\") — composite cross-layer context in one call"
|
|
98
|
+
"- rails_get_context(model:\"X\") — composite cross-layer context in one call",
|
|
99
|
+
"",
|
|
100
|
+
"Power Features:",
|
|
101
|
+
"- rails_search_code(pattern:\"method\", match_type:\"trace\") — trace: definition + source + siblings + callers + tests",
|
|
102
|
+
"- rails_get_concern(name:\"X\", detail:\"full\") — concern methods with source code",
|
|
103
|
+
"- rails_analyze_feature — full-stack with inherited filters, route helpers, test gaps",
|
|
104
|
+
"- rails_get_schema — columns, indexes, defaults, encrypted hints, orphaned table warnings"
|
|
99
105
|
]
|
|
100
106
|
|
|
101
107
|
lines.join("\n")
|
|
@@ -114,11 +114,17 @@ module RailsAiContext
|
|
|
114
114
|
actions = info[:actions]&.join(", ") || "none"
|
|
115
115
|
lines << "" << "### #{name}"
|
|
116
116
|
lines << "- **Actions:** #{actions}"
|
|
117
|
+
|
|
118
|
+
# Inherited filters from parent controller
|
|
119
|
+
parent_filters = detect_parent_filters_for_analyze(info[:parent_class], controllers)
|
|
120
|
+
if parent_filters.any?
|
|
121
|
+
lines << "- **Inherited filters:** #{parent_filters.map { |f| "#{f[:name]} _(from #{info[:parent_class]})_" }.join(', ')}"
|
|
122
|
+
end
|
|
123
|
+
|
|
117
124
|
filters = (info[:filters] || []).select { |f| f.is_a?(Hash) }.map do |f|
|
|
118
125
|
label = "#{f[:kind]} #{f[:name]}"
|
|
119
126
|
label += " only: #{Array(f[:only]).join(', ')}" if f[:only]&.any?
|
|
120
127
|
label += " except: #{Array(f[:except]).join(', ')}" if f[:except]&.any?
|
|
121
|
-
label += " unless: #{f[:unless]}" if f[:unless]
|
|
122
128
|
label
|
|
123
129
|
end
|
|
124
130
|
lines << "- **Filters:** #{filters.join('; ')}" if filters.any?
|
|
@@ -139,8 +145,14 @@ module RailsAiContext
|
|
|
139
145
|
lines << "## Routes (#{route_count})"
|
|
140
146
|
matched.sort.each do |ctrl, actions|
|
|
141
147
|
actions.each do |r|
|
|
142
|
-
|
|
143
|
-
|
|
148
|
+
params = r[:path].scan(/:(\w+)/).flatten
|
|
149
|
+
helper = if r[:name]
|
|
150
|
+
args = params.any? ? "(#{params.map { |p| p == "id" ? "@record" : ":#{p}" }.join(', ')})" : ""
|
|
151
|
+
" `#{r[:name]}_path#{args}`"
|
|
152
|
+
else
|
|
153
|
+
""
|
|
154
|
+
end
|
|
155
|
+
lines << "- `#{r[:verb]}` `#{r[:path]}` → #{ctrl}##{r[:action]}#{helper}"
|
|
144
156
|
end
|
|
145
157
|
end
|
|
146
158
|
else
|
|
@@ -321,6 +333,18 @@ module RailsAiContext
|
|
|
321
333
|
end
|
|
322
334
|
end
|
|
323
335
|
|
|
336
|
+
# Check services
|
|
337
|
+
service_dir = File.join(root, "app", "services")
|
|
338
|
+
if Dir.exist?(service_dir)
|
|
339
|
+
Dir.glob(File.join(service_dir, "**", "*.rb")).each do |path|
|
|
340
|
+
next unless File.basename(path, ".rb").include?(pattern)
|
|
341
|
+
snake = File.basename(path, ".rb")
|
|
342
|
+
unless test_basenames.any? { |t| t.include?(snake) }
|
|
343
|
+
gaps << "Service `#{snake}` — no test file found"
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
324
348
|
return if gaps.empty?
|
|
325
349
|
|
|
326
350
|
lines << "## Test Coverage Gaps"
|
|
@@ -330,6 +354,28 @@ module RailsAiContext
|
|
|
330
354
|
nil
|
|
331
355
|
end
|
|
332
356
|
|
|
357
|
+
# Detect inherited filters from parent controller
|
|
358
|
+
def detect_parent_filters_for_analyze(parent_class, all_controllers)
|
|
359
|
+
return [] unless parent_class
|
|
360
|
+
parent_data = all_controllers[parent_class]
|
|
361
|
+
if parent_data
|
|
362
|
+
return (parent_data[:filters] || []).select { |f| f.is_a?(Hash) && f[:kind] == "before" && !f[:only]&.any? }
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# Fallback: read source file
|
|
366
|
+
path = Rails.root.join("app", "controllers", "#{parent_class.underscore}.rb")
|
|
367
|
+
return [] unless File.exist?(path)
|
|
368
|
+
source = File.read(path, encoding: "UTF-8") rescue nil
|
|
369
|
+
return [] unless source
|
|
370
|
+
|
|
371
|
+
source.each_line.filter_map do |line|
|
|
372
|
+
next if line.include?("only:") || line.include?("except:")
|
|
373
|
+
{ name: $1 } if line.match(/\A\s*before_action\s+:(\w+)/)
|
|
374
|
+
end
|
|
375
|
+
rescue
|
|
376
|
+
[]
|
|
377
|
+
end
|
|
378
|
+
|
|
333
379
|
# --- AF6: Related Models via Associations ---
|
|
334
380
|
def discover_related_models(ctx, matched_models, lines)
|
|
335
381
|
return if matched_models.empty?
|
|
@@ -18,13 +18,18 @@ module RailsAiContext
|
|
|
18
18
|
type: "string",
|
|
19
19
|
enum: %w[model controller all],
|
|
20
20
|
description: "Filter by concern type. model: app/models/concerns/. controller: app/controllers/concerns/. all: both (default)."
|
|
21
|
+
},
|
|
22
|
+
detail: {
|
|
23
|
+
type: "string",
|
|
24
|
+
enum: %w[summary standard full],
|
|
25
|
+
description: "Detail level. summary: concern names only. standard: names + method signatures (default). full: method signatures with source code."
|
|
21
26
|
}
|
|
22
27
|
}
|
|
23
28
|
)
|
|
24
29
|
|
|
25
30
|
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
26
31
|
|
|
27
|
-
def self.call(name: nil, type: "all", server_context: nil)
|
|
32
|
+
def self.call(name: nil, type: "all", detail: "standard", server_context: nil)
|
|
28
33
|
root = rails_app.root.to_s
|
|
29
34
|
max_size = RailsAiContext.configuration.max_file_size
|
|
30
35
|
|
|
@@ -36,7 +41,7 @@ module RailsAiContext
|
|
|
36
41
|
|
|
37
42
|
# Specific concern — full detail
|
|
38
43
|
if name
|
|
39
|
-
return show_concern(name, concern_dirs, root, max_size)
|
|
44
|
+
return show_concern(name, concern_dirs, root, max_size, detail)
|
|
40
45
|
end
|
|
41
46
|
|
|
42
47
|
# List all concerns
|
|
@@ -67,7 +72,7 @@ module RailsAiContext
|
|
|
67
72
|
end
|
|
68
73
|
end
|
|
69
74
|
|
|
70
|
-
private_class_method def self.show_concern(name, concern_dirs, root, max_size)
|
|
75
|
+
private_class_method def self.show_concern(name, concern_dirs, root, max_size, detail = "standard")
|
|
71
76
|
# Find the concern file — try underscore variants and nested paths
|
|
72
77
|
underscore = name.underscore
|
|
73
78
|
file_path = nil
|
|
@@ -121,14 +126,47 @@ module RailsAiContext
|
|
|
121
126
|
public_methods = parse_public_methods(source)
|
|
122
127
|
if public_methods.any?
|
|
123
128
|
lines << "" << "## Public Methods"
|
|
124
|
-
|
|
129
|
+
if detail == "full"
|
|
130
|
+
public_methods.each do |m|
|
|
131
|
+
method_name = m.to_s.split("(").first
|
|
132
|
+
method_source = extract_method_source(source, method_name)
|
|
133
|
+
if method_source
|
|
134
|
+
lines << "### #{m}"
|
|
135
|
+
lines << "```ruby"
|
|
136
|
+
lines << method_source
|
|
137
|
+
lines << "```"
|
|
138
|
+
lines << ""
|
|
139
|
+
else
|
|
140
|
+
lines << "- `#{m}`"
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
else
|
|
144
|
+
public_methods.each { |m| lines << "- `#{m}`" }
|
|
145
|
+
end
|
|
125
146
|
end
|
|
126
147
|
|
|
127
148
|
# Parse class methods (inside class_methods block or def self.)
|
|
128
149
|
class_methods = parse_class_methods(source)
|
|
129
150
|
if class_methods.any?
|
|
130
151
|
lines << "" << "## Class Methods"
|
|
131
|
-
|
|
152
|
+
if detail == "full"
|
|
153
|
+
class_methods.each do |m|
|
|
154
|
+
method_name = m.to_s.split("(").first
|
|
155
|
+
# Try both `def method_name` and `def self.method_name`
|
|
156
|
+
method_source = extract_method_source(source, method_name) || extract_method_source(source, "self.#{method_name}")
|
|
157
|
+
if method_source
|
|
158
|
+
lines << "### #{m}"
|
|
159
|
+
lines << "```ruby"
|
|
160
|
+
lines << method_source
|
|
161
|
+
lines << "```"
|
|
162
|
+
lines << ""
|
|
163
|
+
else
|
|
164
|
+
lines << "- `#{m}`"
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
else
|
|
168
|
+
class_methods.each { |m| lines << "- `#{m}`" }
|
|
169
|
+
end
|
|
132
170
|
end
|
|
133
171
|
|
|
134
172
|
# Parse callbacks defined in the concern
|
|
@@ -345,6 +383,25 @@ module RailsAiContext
|
|
|
345
383
|
[]
|
|
346
384
|
end
|
|
347
385
|
|
|
386
|
+
# Extract method source from raw source string using indentation-based matching
|
|
387
|
+
private_class_method def self.extract_method_source(source, method_name)
|
|
388
|
+
source_lines = source.lines
|
|
389
|
+
start_idx = source_lines.index { |l| l.match?(/\A\s*def\s+#{Regexp.escape(method_name.to_s)}\b/) }
|
|
390
|
+
return nil unless start_idx
|
|
391
|
+
|
|
392
|
+
def_indent = source_lines[start_idx][/\A\s*/].length
|
|
393
|
+
result = []
|
|
394
|
+
|
|
395
|
+
source_lines[start_idx..].each_with_index do |line, i|
|
|
396
|
+
result << line.rstrip
|
|
397
|
+
break if i > 0 && line.match?(/\A\s{#{def_indent}}end\b/)
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
result.join("\n")
|
|
401
|
+
rescue
|
|
402
|
+
nil
|
|
403
|
+
end
|
|
404
|
+
|
|
348
405
|
private_class_method def self.find_includers(concern_name, root, concern_type)
|
|
349
406
|
includers = []
|
|
350
407
|
search_dirs = []
|
|
@@ -160,6 +160,14 @@ module RailsAiContext
|
|
|
160
160
|
lines << cols
|
|
161
161
|
lines << ""
|
|
162
162
|
end
|
|
163
|
+
|
|
164
|
+
# Detect orphaned tables (no ActiveRecord model maps to them)
|
|
165
|
+
orphaned = paginated.select { |name| models_for_table(name).empty? }
|
|
166
|
+
if orphaned.any?
|
|
167
|
+
lines << "\u26A0 **Orphaned tables** (no ActiveRecord model): #{orphaned.join(', ')}"
|
|
168
|
+
lines << ""
|
|
169
|
+
end
|
|
170
|
+
|
|
163
171
|
lines << "_Use `detail:\"summary\"` for all #{total} tables, `detail:\"full\"` for indexes/FKs, or `table:\"name\"` for one table._" if total > limit
|
|
164
172
|
text_response(lines.join("\n"))
|
|
165
173
|
|
|
@@ -306,15 +306,18 @@ module RailsAiContext
|
|
|
306
306
|
if def_results.any?
|
|
307
307
|
lines << "## Definition"
|
|
308
308
|
def_results.each do |r|
|
|
309
|
-
|
|
310
|
-
|
|
309
|
+
# Class/module context
|
|
310
|
+
class_context = extract_class_context(File.join(root, r[:file]), r[:line_number])
|
|
311
|
+
lines << "**#{r[:file]}:#{r[:line_number]}**#{class_context ? " in `#{class_context}`" : ""}"
|
|
312
|
+
|
|
313
|
+
# Full method body
|
|
311
314
|
body = extract_method_body(File.join(root, r[:file]), r[:line_number])
|
|
312
315
|
if body
|
|
313
316
|
lines << "```ruby"
|
|
314
317
|
lines << body
|
|
315
318
|
lines << "```"
|
|
316
319
|
|
|
317
|
-
#
|
|
320
|
+
# What does this method call?
|
|
318
321
|
internal_calls = body.scan(/\b([a-z_]\w*[!?]?)(?:\s*[\(])/).flatten.uniq
|
|
319
322
|
internal_calls += body.scan(/\b([A-Z]\w+(?:::\w+)*)\.(new|call|perform_later|perform_async|find|where|create)/).map { |c| "#{c[0]}.#{c[1]}" }
|
|
320
323
|
internal_calls.reject! { |c| %w[if else elsif unless return end def class module do begin rescue ensure raise puts print].include?(c) }
|
|
@@ -325,6 +328,14 @@ module RailsAiContext
|
|
|
325
328
|
internal_calls.first(15).each { |c| lines << "- `#{c}`" }
|
|
326
329
|
end
|
|
327
330
|
end
|
|
331
|
+
|
|
332
|
+
# Sibling methods in the same file
|
|
333
|
+
siblings = extract_sibling_methods(File.join(root, r[:file]), r[:line_number], cleaned)
|
|
334
|
+
if siblings.any?
|
|
335
|
+
lines << "" << "## Sibling methods (same file)"
|
|
336
|
+
siblings.first(10).each { |s| lines << "- `#{s}`" }
|
|
337
|
+
end
|
|
338
|
+
|
|
328
339
|
lines << ""
|
|
329
340
|
end
|
|
330
341
|
else
|
|
@@ -346,28 +357,46 @@ module RailsAiContext
|
|
|
346
357
|
callers.reject! { |r| def_locations.include?("#{r[:file]}:#{r[:line_number]}") }
|
|
347
358
|
|
|
348
359
|
if callers.any?
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
360
|
+
# Separate app code from tests
|
|
361
|
+
app_callers = callers.reject { |r| r[:file].match?(/\A(test|spec)\//) }
|
|
362
|
+
test_callers = callers.select { |r| r[:file].match?(/\A(test|spec)\//) }
|
|
363
|
+
|
|
364
|
+
if app_callers.any?
|
|
365
|
+
lines << "## Called from (#{app_callers.size} sites)"
|
|
366
|
+
grouped = app_callers.group_by { |r| r[:file] }
|
|
367
|
+
grouped.each do |file, matches|
|
|
368
|
+
category = case file
|
|
369
|
+
when /controller/i then "Controller"
|
|
370
|
+
when /model/i then "Model"
|
|
371
|
+
when /view|\.erb/i then "View"
|
|
372
|
+
when /job/i then "Job"
|
|
373
|
+
when /service/i then "Service"
|
|
374
|
+
when /\.js$|\.ts$/i then "JavaScript"
|
|
375
|
+
else "Other"
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
# Route chain for controller callers
|
|
379
|
+
route_hint = ""
|
|
380
|
+
if category == "Controller" && file.match?(/app\/controllers\/(.+)_controller\.rb/)
|
|
381
|
+
ctrl_path = $1
|
|
382
|
+
route_actions = extract_controller_actions_from_matches(matches)
|
|
383
|
+
routes = find_routes_for_controller(ctrl_path, route_actions, root)
|
|
384
|
+
route_hint = " → #{routes}" if routes
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
lines << "### #{file} (#{category})#{route_hint}"
|
|
388
|
+
matches.first(5).each do |r|
|
|
389
|
+
lines << " #{r[:line_number]}: #{r[:content].strip}"
|
|
390
|
+
end
|
|
391
|
+
lines << " _(#{matches.size - 5} more)_" if matches.size > 5
|
|
364
392
|
end
|
|
393
|
+
end
|
|
365
394
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
395
|
+
if test_callers.any?
|
|
396
|
+
lines << "" << "## Tested by (#{test_callers.size} references)"
|
|
397
|
+
test_callers.group_by { |r| r[:file] }.each do |file, matches|
|
|
398
|
+
lines << "- `#{file}` (#{matches.size} references)"
|
|
369
399
|
end
|
|
370
|
-
lines << " _(#{matches.size - 5} more)_" if matches.size > 5
|
|
371
400
|
end
|
|
372
401
|
else
|
|
373
402
|
lines << "## Called from"
|
|
@@ -388,6 +417,64 @@ module RailsAiContext
|
|
|
388
417
|
end
|
|
389
418
|
end
|
|
390
419
|
|
|
420
|
+
# Extract class/module context for a line
|
|
421
|
+
private_class_method def self.extract_class_context(file_path, line_num)
|
|
422
|
+
return nil unless File.exist?(file_path)
|
|
423
|
+
lines = File.readlines(file_path)
|
|
424
|
+
# Walk backwards from the method to find the enclosing class/module
|
|
425
|
+
(line_num - 2).downto(0) do |i|
|
|
426
|
+
if lines[i]&.match?(/\A\s*(class|module)\s+(\S+)/)
|
|
427
|
+
return lines[i].strip.sub(/\s*<.*/, "")
|
|
428
|
+
end
|
|
429
|
+
end
|
|
430
|
+
nil
|
|
431
|
+
rescue
|
|
432
|
+
nil
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
# Extract sibling methods in the same file (other public methods)
|
|
436
|
+
private_class_method def self.extract_sibling_methods(file_path, def_line, exclude_method)
|
|
437
|
+
return [] unless File.exist?(file_path)
|
|
438
|
+
return [] if File.size(file_path) > RailsAiContext.configuration.max_file_size
|
|
439
|
+
source = File.read(file_path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
440
|
+
methods = []
|
|
441
|
+
in_private = false
|
|
442
|
+
source.each_line do |line|
|
|
443
|
+
in_private = true if line.match?(/\A\s*private\s*$/)
|
|
444
|
+
next if in_private
|
|
445
|
+
if (m = line.match(/\A\s*def\s+((?:self\.)?\w+[?!]?)/))
|
|
446
|
+
name = m[1]
|
|
447
|
+
methods << name unless name == exclude_method || name.start_with?("initialize")
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
methods
|
|
451
|
+
rescue
|
|
452
|
+
[]
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
# Extract which action a controller caller is in
|
|
456
|
+
private_class_method def self.extract_controller_actions_from_matches(matches)
|
|
457
|
+
actions = []
|
|
458
|
+
matches.each do |m|
|
|
459
|
+
# Look for the method name from indentation context
|
|
460
|
+
actions << $1 if m[:content].match?(/\b(create|index|show|new|edit|update|destroy|[a-z_]+)\b/)
|
|
461
|
+
end
|
|
462
|
+
actions.uniq.first(3)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
# Find routes for a controller
|
|
466
|
+
private_class_method def self.find_routes_for_controller(ctrl_path, _actions, _root)
|
|
467
|
+
routes = cached_context[:routes]
|
|
468
|
+
return nil unless routes
|
|
469
|
+
by_controller = routes[:by_controller] || {}
|
|
470
|
+
ctrl_routes = by_controller[ctrl_path]
|
|
471
|
+
return nil unless ctrl_routes&.any?
|
|
472
|
+
# Show the first 2 routes as hints
|
|
473
|
+
ctrl_routes.first(2).map { |r| "`#{r[:verb]} #{r[:path]}`" }.join(", ")
|
|
474
|
+
rescue
|
|
475
|
+
nil
|
|
476
|
+
end
|
|
477
|
+
|
|
391
478
|
# Extract a method body from a file given the def line number
|
|
392
479
|
private_class_method def self.extract_method_body(file_path, def_line)
|
|
393
480
|
return nil unless File.exist?(file_path)
|
data/server.json
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"url": "https://github.com/crisnahine/rails-ai-context",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "2.0.
|
|
10
|
+
"version": "2.0.4",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "mcpb",
|
|
14
|
-
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v2.0.
|
|
14
|
+
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v2.0.4/rails-ai-context-mcp.mcpb",
|
|
15
15
|
"fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|