rails-ai-context 2.0.4 → 3.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.
@@ -9,6 +9,7 @@ module RailsAiContext
9
9
  include TestCommandDetection
10
10
  include StackOverviewHelper
11
11
  include DesignSystemHelper
12
+ include ToolGuideHelper
12
13
 
13
14
  attr_reader :context
14
15
 
@@ -91,50 +92,13 @@ module RailsAiContext
91
92
  # Design System
92
93
  lines.concat(render_design_system(context, max_lines: 35))
93
94
 
94
- # MCP tools
95
- lines << "## MCP Tool Reference"
96
- lines << ""
97
- lines << "This project has MCP tools for live introspection."
98
- lines << "**Always start with `detail:\"summary\"`, then drill into specifics.**"
99
- lines << ""
100
- lines << "### Detail levels (schema, routes, models, controllers)"
101
- lines << "- `summary` — names + counts (default limit: 50)"
102
- lines << "- `standard` — names + key details (default limit: 15, this is the default)"
103
- lines << "- `full` — everything including indexes, FKs (default limit: 5)"
104
- lines << ""
105
- lines << "### rails_get_schema"
106
- lines << "Params: `table`, `detail`, `limit`, `offset`, `format`"
107
- lines << "- `rails_get_schema(detail:\"summary\")` — all tables with column counts"
108
- lines << "- `rails_get_schema(table:\"users\")` — full detail for one table"
109
- lines << "- `rails_get_schema(detail:\"summary\", limit:20, offset:40)` — paginate"
110
- lines << ""
111
- lines << "### rails_get_model_details"
112
- lines << "Params: `model`, `detail`"
113
- lines << "- `rails_get_model_details(detail:\"summary\")` — list all model names"
114
- lines << "- `rails_get_model_details(model:\"User\")` — associations, validations, scopes, enums"
115
- lines << ""
116
- lines << "### rails_get_routes"
117
- lines << "Params: `controller`, `detail`, `limit`, `offset`"
118
- lines << "- `rails_get_routes(detail:\"summary\")` — route counts per controller"
119
- lines << "- `rails_get_routes(controller:\"users\")` — routes for one controller"
120
- lines << ""
121
- lines << "### rails_get_controllers"
122
- lines << "Params: `controller`, `detail`"
123
- lines << "- `rails_get_controllers(detail:\"summary\")` — names + action counts"
124
- lines << "- `rails_get_controllers(controller:\"UsersController\")` — actions, filters, params"
125
- lines << ""
126
- lines << "### Other tools"
127
- lines << "- `rails_get_config` — cache store, session, timezone, middleware"
128
- lines << "- `rails_get_test_info` — test framework, factories/fixtures, CI config"
129
- lines << "- `rails_get_gems` — notable gems categorized by function"
130
- lines << "- `rails_get_conventions` — architecture patterns, directory structure"
131
- lines << "- `rails_search_code(pattern:\"regex\", file_type:\"rb\", max_results:20)` — codebase search"
132
- lines << ""
95
+ # Tools reference (respects tool_mode)
96
+ lines.concat(render_tools_guide)
133
97
 
134
98
  # Conventions
135
99
  lines << "## Conventions"
136
100
  lines << "- Follow existing patterns and naming conventions"
137
- lines << "- Use MCP tools to check schema before writing migrations"
101
+ lines << "- Use the introspection tools to check schema before writing migrations"
138
102
  lines << "- Run `#{detect_test_command}` after changes"
139
103
  lines << ""
140
104
 
@@ -8,6 +8,7 @@ module RailsAiContext
8
8
  class CursorRulesSerializer
9
9
  include StackOverviewHelper
10
10
  include DesignSystemHelper
11
+ include ToolGuideHelper
11
12
 
12
13
  attr_reader :context
13
14
 
@@ -141,7 +142,6 @@ module RailsAiContext
141
142
 
142
143
  lines = [
143
144
  "---",
144
- "description: \"ActiveRecord models reference\"",
145
145
  "globs:",
146
146
  " - \"app/models/**/*.rb\"",
147
147
  "alwaysApply: false",
@@ -184,7 +184,6 @@ module RailsAiContext
184
184
 
185
185
  lines = [
186
186
  "---",
187
- "description: \"Controller reference\"",
188
187
  "globs:",
189
188
  " - \"app/controllers/**/*.rb\"",
190
189
  "alwaysApply: false",
@@ -215,7 +214,6 @@ module RailsAiContext
215
214
 
216
215
  lines = [
217
216
  "---",
218
- "description: \"Design system and UI patterns for this Rails app\"",
219
217
  "globs:",
220
218
  " - \"app/views/**/*.erb\"",
221
219
  "alwaysApply: false",
@@ -253,52 +251,17 @@ module RailsAiContext
253
251
  end
254
252
 
255
253
  # Always-on MCP tool reference — strongest enforcement point for Cursor
256
- def render_mcp_tools_rule # rubocop:disable Metrics/MethodLength
254
+ def render_mcp_tools_rule
257
255
  lines = [
258
256
  "---",
259
- "description: \"Rails MCP tools (25) — MANDATORY, use before reading any reference files\"",
257
+ "description: \"Rails tools (25) — MANDATORY, use before reading any reference files\"",
260
258
  "alwaysApply: true",
261
259
  "---",
262
- "",
263
- "# Rails MCP Tools (25) — MANDATORY, Use Before Read",
264
- "",
265
- "CRITICAL: This project has live MCP tools. You MUST use them for gathering context.",
266
- "Read files ONLY when you are about to edit them. Never read reference files directly.",
267
- "",
268
- "## Mandatory Workflow",
269
- "1. Gathering context → use MCP tools (NOT file reads, NOT grep)",
270
- "2. Reading files → ONLY files you will edit (Read is required before Edit)",
271
- "3. After editing → `rails_validate(files:[...])` every time, no exceptions",
272
- "",
273
- "## Do NOT Bypass — Anti-Patterns",
274
- "| Instead of... | Use this MCP tool |",
275
- "|---------------|-------------------|",
276
- "| Reading db/schema.rb | `rails_get_schema(table:\"name\")` |",
277
- "| Reading config/routes.rb | `rails_get_routes(controller:\"name\")` |",
278
- "| Reading model files for context | `rails_get_model_details(model:\"Name\")` |",
279
- "| Grep for code patterns | `rails_search_code(pattern:\"regex\")` |",
280
- "| Reading test files for patterns | `rails_get_test_info(model:\"Name\")` |",
281
- "| Reading controller for context | `rails_get_controllers(controller:\"Name\", action:\"x\")` |",
282
- "| Reading JS for Stimulus API | `rails_get_stimulus(controller:\"name\")` |",
283
- "| Multiple reads for a feature | `rails_analyze_feature(feature:\"keyword\")` |",
284
- "| ruby -c / erb / node -c | `rails_validate(files:[...])` |",
285
- "",
286
- "## All 25 Tools",
287
- "- `rails_get_schema` | `rails_get_model_details` | `rails_get_routes` | `rails_get_controllers`",
288
- "- `rails_get_view` | `rails_get_stimulus` | `rails_get_test_info` | `rails_analyze_feature`",
289
- "- `rails_get_design_system` | `rails_get_edit_context` | `rails_validate` | `rails_search_code`",
290
- "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_security_scan`",
291
- "- `rails_get_concern` | `rails_get_callbacks` | `rails_get_helper_methods` | `rails_get_service_pattern`",
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",
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"
260
+ ""
300
261
  ]
301
262
 
263
+ lines.concat(render_tools_guide)
264
+
302
265
  lines.join("\n")
303
266
  end
304
267
  end
@@ -3,7 +3,7 @@
3
3
  module RailsAiContext
4
4
  module Serializers
5
5
  # Generates AI-friendly markdown context files from introspection data.
6
- # Outputs: CLAUDE.md (for Claude Code), .windsurfrules, etc.
6
+ # Outputs: CLAUDE.md (for Claude Code), copilot-instructions.md, etc.
7
7
  class MarkdownSerializer # rubocop:disable Metrics/ClassLength
8
8
  include TestCommandDetection
9
9
 
@@ -9,6 +9,7 @@ module RailsAiContext
9
9
  include TestCommandDetection
10
10
  include StackOverviewHelper
11
11
  include DesignSystemHelper
12
+ include ToolGuideHelper
12
13
 
13
14
  attr_reader :context
14
15
 
@@ -163,44 +164,8 @@ module RailsAiContext
163
164
  render_design_system(context, max_lines: 30)
164
165
  end
165
166
 
166
- def render_mcp_guide # rubocop:disable Metrics/MethodLength
167
- [
168
- "## MCP Tools (25) — MANDATORY, Use Before Read",
169
- "",
170
- "CRITICAL: This project has live MCP tools. You MUST use them for gathering context.",
171
- "Read files ONLY when you are about to edit them. Never read reference files directly.",
172
- "Start with `detail:\"summary\"`, then drill into specifics.",
173
- "",
174
- "### Mandatory Workflow",
175
- "1. **Before exploring a feature**: `rails_analyze_feature(feature:\"...\")` — models + controllers (inherited filters) + routes (code-ready helpers) + services + jobs + views + tests + gaps",
176
- "2. **Before writing migrations**: `rails_get_schema(table:\"...\")` — NOT reading db/schema.rb",
177
- "3. **Before modifying a model**: `rails_get_model_details(model:\"...\")` — NOT reading the model file",
178
- "4. **Before adding routes**: `rails_get_routes(controller:\"...\")` — Read only when you will edit",
179
- "5. **Before creating views**: `rails_get_design_system` — match existing patterns",
180
- "6. **After editing ANY file**: `rails_validate(files:[...])` — no exceptions",
181
- "",
182
- "### Do NOT Bypass",
183
- "| Instead of... | Use this MCP tool |",
184
- "|---------------|-------------------|",
185
- "| Reading db/schema.rb | `rails_get_schema(table:\"x\")` — includes orphaned table warnings |",
186
- "| Reading model files | `rails_get_model_details(model:\"X\")` |",
187
- "| Reading routes.rb | `rails_get_routes(controller:\"x\")` |",
188
- "| Grep for code | `rails_search_code(pattern:\"x\", match_type:\"trace\")` |",
189
- "| Reading test files | `rails_get_test_info(model:\"X\")` |",
190
- "| Reading controller | `rails_get_controllers(controller:\"X\", action:\"y\")` |",
191
- "| ruby -c / erb / node | `rails_validate(files:[...])` |",
192
- "",
193
- "### All 25 Tools",
194
- "- `rails_get_schema` | `rails_get_model_details` | `rails_get_routes` | `rails_get_controllers`",
195
- "- `rails_get_view` | `rails_get_stimulus` | `rails_get_test_info` | `rails_analyze_feature`",
196
- "- `rails_get_design_system` | `rails_get_edit_context` | `rails_validate` | `rails_search_code`",
197
- "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_security_scan`",
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
- "- `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",
201
- "- `rails_get_context(model:\"X\")` — composite cross-layer context in one call",
202
- ""
203
- ]
167
+ def render_mcp_guide
168
+ render_tools_guide
204
169
  end
205
170
 
206
171
  def render_conventions
@@ -0,0 +1,214 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module Serializers
5
+ # Shared helper for rendering the tool reference section in context files.
6
+ # Reads config.tool_mode to generate MCP syntax, CLI syntax, or both.
7
+ module ToolGuideHelper
8
+ # Returns the tool invocation example for a given tool call.
9
+ # MCP: rails_analyze_feature(feature:"cook")
10
+ # CLI: rails 'ai:tool[analyze_feature]' feature=cook
11
+ def tool_call(mcp_call, cli_call)
12
+ case tool_mode
13
+ when :cli
14
+ "→ `#{cli_call}`"
15
+ when :mcp
16
+ "→ MCP: `#{mcp_call}`\n→ CLI: `#{cli_call}`"
17
+ else
18
+ "→ `#{mcp_call}`"
19
+ end
20
+ end
21
+
22
+ def tool_mode
23
+ RailsAiContext.configuration.tool_mode
24
+ end
25
+
26
+ def tools_header
27
+ "## Tools (25) — MANDATORY, Use Before Read"
28
+ end
29
+
30
+ def tools_intro
31
+ case tool_mode
32
+ when :cli
33
+ [
34
+ "This project has 25 introspection tools. **MANDATORY — use these instead of reading files.**",
35
+ "They return only relevant, structured data and save tokens. Read files ONLY when you are about to Edit them.",
36
+ ""
37
+ ]
38
+ else
39
+ [
40
+ "This project has 25 MCP tools via `rails ai:serve` (configured in `.mcp.json`).",
41
+ "**MANDATORY — use these instead of reading files.** They return structured data and save tokens.",
42
+ "Read files ONLY when you are about to Edit them.",
43
+ "If MCP tools are not connected, use CLI fallback: `#{cli_cmd("TOOL_NAME", "param=value")}`",
44
+ ""
45
+ ]
46
+ end
47
+ end
48
+
49
+ def tools_task_section # rubocop:disable Metrics/MethodLength
50
+ [
51
+ "### What Are You Trying to Do?",
52
+ "",
53
+ "**Understand a feature or area:**",
54
+ tool_call("rails_analyze_feature(feature:\"cook\")", cli_cmd("analyze_feature", "feature=cook")),
55
+ tool_call("rails_get_context(model:\"Cook\")", cli_cmd("context", "model=Cook")),
56
+ "",
57
+ "**Understand a method (who calls it, what it calls):**",
58
+ tool_call("rails_search_code(pattern:\"can_cook?\", match_type:\"trace\")", cli_cmd("search_code", "pattern=\"can_cook?\" match_type=trace")),
59
+ "",
60
+ "**Add a field or modify a model:**",
61
+ tool_call("rails_get_schema(table:\"cooks\")", cli_cmd("schema", "table=cooks")),
62
+ tool_call("rails_get_model_details(model:\"Cook\")", cli_cmd("model_details", "model=Cook")),
63
+ "",
64
+ "**Fix a controller bug:**",
65
+ tool_call("rails_get_controllers(controller:\"CooksController\", action:\"create\")", cli_cmd("controllers", "controller=CooksController action=create")),
66
+ "",
67
+ "**Build or modify a view:**",
68
+ tool_call("rails_get_design_system(detail:\"standard\")", cli_cmd("design_system", "detail=standard")),
69
+ tool_call("rails_get_view(controller:\"cooks\")", cli_cmd("view", "controller=cooks")),
70
+ tool_call("rails_get_partial_interface(partial:\"shared/status_badge\")", cli_cmd("partial_interface", "partial=shared/status_badge")),
71
+ "",
72
+ "**Write tests:**",
73
+ tool_call("rails_get_test_info(detail:\"standard\")", cli_cmd("test_info", "detail=standard")),
74
+ tool_call("rails_get_test_info(model:\"Cook\")", cli_cmd("test_info", "model=Cook")),
75
+ "",
76
+ "**Find code:**",
77
+ tool_call("rails_search_code(pattern:\"has_many\")", cli_cmd("search_code", "pattern=\"has_many\"")),
78
+ tool_call("rails_search_code(pattern:\"create\", match_type:\"definition\")", cli_cmd("search_code", "pattern=create match_type=definition")),
79
+ "",
80
+ "**After editing (EVERY time):**",
81
+ tool_call("rails_validate(files:[\"app/models/cook.rb\"], level:\"rails\")", cli_cmd("validate", "files=app/models/cook.rb level=rails")),
82
+ ""
83
+ ]
84
+ end
85
+
86
+ def tools_rules_section
87
+ case tool_mode
88
+ when :cli
89
+ [
90
+ "### Rules",
91
+ "",
92
+ "1. NEVER read db/schema.rb, config/routes.rb, model files, or test files for reference — use the CLI tools above",
93
+ "2. NEVER use Grep or search agents for code search — use `#{cli_cmd("search_code")}`",
94
+ "3. NEVER run `ruby -c`, `erb`, or `node -c` — use `#{cli_cmd("validate")}`",
95
+ "4. Read files ONLY when you are about to Edit them",
96
+ "5. Start with `detail=summary` to orient, then drill into specifics",
97
+ ""
98
+ ]
99
+ else
100
+ [
101
+ "### Rules",
102
+ "",
103
+ "1. NEVER read db/schema.rb, config/routes.rb, model files, or test files for reference — use the MCP tools above",
104
+ "2. NEVER use Grep or search agents for code search — use `rails_search_code`",
105
+ "3. NEVER run `ruby -c`, `erb`, or `node -c` — use `rails_validate`",
106
+ "4. Read files ONLY when you are about to Edit them",
107
+ "5. Start with `detail:\"summary\"` to orient, then drill into specifics",
108
+ "6. If MCP tools are not connected, use CLI: `#{cli_cmd("TOOL_NAME", "param=value")}`",
109
+ ""
110
+ ]
111
+ end
112
+ end
113
+
114
+ def tools_table # rubocop:disable Metrics/MethodLength
115
+ lines = [ "### All 25 Tools", "" ]
116
+
117
+ if tool_mode == :cli
118
+ lines.concat(tools_table_cli)
119
+ else
120
+ lines.concat(tools_table_mcp_and_cli)
121
+ end
122
+
123
+ lines
124
+ end
125
+
126
+ def tools_table_mcp_and_cli # rubocop:disable Metrics/MethodLength
127
+ [
128
+ "| MCP | CLI | What it does |",
129
+ "|-----|-----|-------------|",
130
+ "| `rails_analyze_feature(feature:\"X\")` | `#{cli_cmd("analyze_feature", "feature=X")}` | Full-stack: models + controllers + routes + services + jobs + views + tests |",
131
+ "| `rails_get_context(model:\"X\")` | `#{cli_cmd("context", "model=X")}` | Composite: schema + model + controller + routes + views in one call |",
132
+ "| `rails_search_code(pattern:\"X\", match_type:\"trace\")` | `#{cli_cmd("search_code", "pattern=X match_type=trace")}` | Trace: definition + source + siblings + callers + test coverage |",
133
+ "| `rails_get_controllers(controller:\"X\", action:\"Y\")` | `#{cli_cmd("controllers", "controller=X action=Y")}` | Action source + inherited filters + render map + private methods |",
134
+ "| `rails_validate(files:[...], level:\"rails\")` | `#{cli_cmd("validate", "files=a.rb,b.rb level=rails")}` | Syntax + semantic validation + Brakeman security |",
135
+ "| `rails_get_schema(table:\"X\")` | `#{cli_cmd("schema", "table=X")}` | Columns with [indexed]/[unique]/[encrypted]/[default] hints |",
136
+ "| `rails_get_model_details(model:\"X\")` | `#{cli_cmd("model_details", "model=X")}` | Associations, validations, scopes, enums, macros, delegations |",
137
+ "| `rails_get_routes(controller:\"X\")` | `#{cli_cmd("routes", "controller=X")}` | Routes with code-ready helpers and controller filters inline |",
138
+ "| `rails_get_view(controller:\"X\")` | `#{cli_cmd("view", "controller=X")}` | Templates with ivars, Turbo wiring, Stimulus refs, partial locals |",
139
+ "| `rails_get_design_system` | `#{cli_cmd("design_system")}` | Canonical HTML/ERB copy-paste patterns for buttons, inputs, cards |",
140
+ "| `rails_get_stimulus(controller:\"X\")` | `#{cli_cmd("stimulus", "controller=X")}` | Targets, values, actions + HTML data-attributes + view lookup |",
141
+ "| `rails_get_test_info(model:\"X\")` | `#{cli_cmd("test_info", "model=X")}` | Tests + fixture contents + test template |",
142
+ "| `rails_get_concern(name:\"X\", detail:\"full\")` | `#{cli_cmd("concern", "name=X detail=full")}` | Concern methods with source + which models include it |",
143
+ "| `rails_get_callbacks(model:\"X\")` | `#{cli_cmd("callbacks", "model=X")}` | Callbacks in Rails execution order with source |",
144
+ "| `rails_get_edit_context(file:\"X\", near:\"Y\")` | `#{cli_cmd("edit_context", "file=X near=Y")}` | Code around a match with class/method context |",
145
+ "| `rails_search_code(pattern:\"X\")` | `#{cli_cmd("search_code", "pattern=X")}` | Regex search + `exclude_tests` + `group_by_file` + pagination |",
146
+ "| `rails_get_service_pattern` | `#{cli_cmd("service_pattern")}` | Service objects: interface, dependencies, side effects, callers |",
147
+ "| `rails_get_job_pattern` | `#{cli_cmd("job_pattern")}` | Jobs: queue, retries, guard clauses, broadcasts, schedules |",
148
+ "| `rails_get_env` | `#{cli_cmd("env")}` | Environment variables + credentials keys (not values) |",
149
+ "| `rails_get_partial_interface(partial:\"X\")` | `#{cli_cmd("partial_interface", "partial=X")}` | Partial locals contract: what to pass + usage examples |",
150
+ "| `rails_get_turbo_map` | `#{cli_cmd("turbo_map")}` | Turbo Stream/Frame wiring + mismatch warnings |",
151
+ "| `rails_get_helper_methods` | `#{cli_cmd("helper_methods")}` | App + framework helpers with view cross-references |",
152
+ "| `rails_get_config` | `#{cli_cmd("config")}` | Database adapter, auth, assets, cache, queue, Action Cable |",
153
+ "| `rails_get_gems` | `#{cli_cmd("gems")}` | Notable gems with versions, categories, config file locations |",
154
+ "| `rails_get_conventions` | `#{cli_cmd("conventions")}` | App patterns: auth checks, flash messages, test patterns |",
155
+ "| `rails_security_scan` | `#{cli_cmd("security_scan")}` | Brakeman static analysis: SQL injection, XSS, mass assignment |"
156
+ ]
157
+ end
158
+
159
+ def tools_table_cli # rubocop:disable Metrics/MethodLength
160
+ [
161
+ "| CLI | What it does |",
162
+ "|-----|-------------|",
163
+ "| `#{cli_cmd("analyze_feature", "feature=X")}` | Full-stack: models + controllers + routes + services + jobs + views + tests |",
164
+ "| `#{cli_cmd("context", "model=X")}` | Composite: schema + model + controller + routes + views in one call |",
165
+ "| `#{cli_cmd("search_code", "pattern=X match_type=trace")}` | Trace: definition + source + siblings + callers + test coverage |",
166
+ "| `#{cli_cmd("controllers", "controller=X action=Y")}` | Action source + inherited filters + render map + private methods |",
167
+ "| `#{cli_cmd("validate", "files=a.rb,b.rb level=rails")}` | Syntax + semantic validation + Brakeman security |",
168
+ "| `#{cli_cmd("schema", "table=X")}` | Columns with [indexed]/[unique]/[encrypted]/[default] hints |",
169
+ "| `#{cli_cmd("model_details", "model=X")}` | Associations, validations, scopes, enums, macros, delegations |",
170
+ "| `#{cli_cmd("routes", "controller=X")}` | Routes with code-ready helpers and controller filters inline |",
171
+ "| `#{cli_cmd("view", "controller=X")}` | Templates with ivars, Turbo wiring, Stimulus refs, partial locals |",
172
+ "| `#{cli_cmd("design_system")}` | Canonical HTML/ERB copy-paste patterns for buttons, inputs, cards |",
173
+ "| `#{cli_cmd("stimulus", "controller=X")}` | Targets, values, actions + HTML data-attributes + view lookup |",
174
+ "| `#{cli_cmd("test_info", "model=X")}` | Tests + fixture contents + test template |",
175
+ "| `#{cli_cmd("concern", "name=X detail=full")}` | Concern methods with source + which models include it |",
176
+ "| `#{cli_cmd("callbacks", "model=X")}` | Callbacks in Rails execution order with source |",
177
+ "| `#{cli_cmd("edit_context", "file=X near=Y")}` | Code around a match with class/method context |",
178
+ "| `#{cli_cmd("search_code", "pattern=X")}` | Regex search + `exclude_tests` + `group_by_file` + pagination |",
179
+ "| `#{cli_cmd("service_pattern")}` | Service objects: interface, dependencies, side effects, callers |",
180
+ "| `#{cli_cmd("job_pattern")}` | Jobs: queue, retries, guard clauses, broadcasts, schedules |",
181
+ "| `#{cli_cmd("env")}` | Environment variables + credentials keys (not values) |",
182
+ "| `#{cli_cmd("partial_interface", "partial=X")}` | Partial locals contract: what to pass + usage examples |",
183
+ "| `#{cli_cmd("turbo_map")}` | Turbo Stream/Frame wiring + mismatch warnings |",
184
+ "| `#{cli_cmd("helper_methods")}` | App + framework helpers with view cross-references |",
185
+ "| `#{cli_cmd("config")}` | Database adapter, auth, assets, cache, queue, Action Cable |",
186
+ "| `#{cli_cmd("gems")}` | Notable gems with versions, categories, config file locations |",
187
+ "| `#{cli_cmd("conventions")}` | App patterns: auth checks, flash messages, test patterns |",
188
+ "| `#{cli_cmd("security_scan")}` | Brakeman static analysis: SQL injection, XSS, mass assignment |"
189
+ ]
190
+ end
191
+
192
+ # Full tool guide section — used by all serializers.
193
+ def render_tools_guide
194
+ lines = []
195
+ lines << tools_header
196
+ lines << ""
197
+ lines.concat(tools_intro)
198
+ lines.concat(tools_task_section)
199
+ lines.concat(tools_rules_section)
200
+ lines.concat(tools_table)
201
+ lines
202
+ end
203
+
204
+ private
205
+
206
+ # Generate zsh-safe CLI command: rails 'ai:tool[name]' params
207
+ def cli_cmd(tool_name, params = nil)
208
+ cmd = "rails 'ai:tool[#{tool_name}]'"
209
+ cmd += " #{params}" if params
210
+ cmd
211
+ end
212
+ end
213
+ end
214
+ end
@@ -6,7 +6,6 @@ ASSISTANT_TABLE = <<~TABLE unless defined?(ASSISTANT_TABLE)
6
6
  Claude Code CLAUDE.md + .claude/rules/ rails ai:context:claude
7
7
  OpenCode AGENTS.md rails ai:context:opencode
8
8
  Cursor .cursor/rules/ rails ai:context:cursor
9
- Windsurf .windsurfrules + .windsurf/rules/ rails ai:context:windsurf
10
9
  GitHub Copilot .github/copilot-instructions.md rails ai:context:copilot
11
10
  JSON (generic) .ai-context.json rails ai:context:json
12
11
  TABLE
@@ -28,8 +27,7 @@ AI_TOOL_OPTIONS = {
28
27
  "1" => { key: :claude, name: "Claude Code" },
29
28
  "2" => { key: :cursor, name: "Cursor" },
30
29
  "3" => { key: :copilot, name: "GitHub Copilot" },
31
- "4" => { key: :windsurf, name: "Windsurf" },
32
- "5" => { key: :opencode, name: "OpenCode" }
30
+ "4" => { key: :opencode, name: "OpenCode" }
33
31
  }.freeze unless defined?(AI_TOOL_OPTIONS)
34
32
 
35
33
  def prompt_ai_tools
@@ -58,6 +56,67 @@ def prompt_ai_tools
58
56
  selected
59
57
  end unless defined?(prompt_ai_tools)
60
58
 
59
+ def prompt_tool_mode
60
+ puts ""
61
+ puts "Do you also want MCP server support?"
62
+ puts ""
63
+ puts " 1. Yes — MCP primary + CLI fallback (generates .mcp.json)"
64
+ puts " 2. No — CLI only (no server needed)"
65
+ puts ""
66
+ print "Enter number (default: 1): "
67
+ input = $stdin.gets&.strip || "1"
68
+
69
+ mode = input == "2" ? :cli : :mcp
70
+ label = mode == :mcp ? "MCP + CLI fallback" : "CLI only"
71
+ puts "Selected: #{label}"
72
+ mode
73
+ end unless defined?(prompt_tool_mode)
74
+
75
+ def save_tool_mode_to_initializer(mode)
76
+ init_path = Rails.root.join("config/initializers/rails_ai_context.rb")
77
+ return unless File.exist?(init_path)
78
+
79
+ content = File.read(init_path)
80
+ mode_line = " config.tool_mode = :#{mode}"
81
+
82
+ if content.include?("config.tool_mode")
83
+ content.sub!(/^.*config\.tool_mode.*$/, mode_line)
84
+ elsif content.include?("config.ai_tools")
85
+ # Insert after ai_tools line
86
+ content.sub!(/^(.*config\.ai_tools.*)$/, "\\1\n#{mode_line}")
87
+ elsif content.include?("RailsAiContext.configure")
88
+ content.sub!(/RailsAiContext\.configure do \|config\|\n/, "RailsAiContext.configure do |config|\n#{mode_line}\n")
89
+ else
90
+ return
91
+ end
92
+
93
+ File.write(init_path, content)
94
+ rescue
95
+ nil
96
+ end unless defined?(save_tool_mode_to_initializer)
97
+
98
+ def ensure_mcp_json
99
+ mcp_path = Rails.root.join(".mcp.json")
100
+ return if File.exist?(mcp_path)
101
+
102
+ server_entry = { "command" => "bundle", "args" => [ "exec", "rails", "ai:serve" ] }
103
+ content = JSON.pretty_generate({ mcpServers: { "rails-ai-context" => server_entry } }) + "\n"
104
+ File.write(mcp_path, content)
105
+ puts "✅ Created .mcp.json (MCP auto-discovery for Claude Code, Cursor, etc.)"
106
+ rescue => e
107
+ puts "⚠️ Could not create .mcp.json: #{e.message}"
108
+ end unless defined?(ensure_mcp_json)
109
+
110
+ def tool_mode_configured?
111
+ init_path = Rails.root.join("config/initializers/rails_ai_context.rb")
112
+ return false unless File.exist?(init_path)
113
+ content = File.read(init_path)
114
+ # Check for uncommented tool_mode line (not just a comment)
115
+ content.match?(/^\s*config\.tool_mode\s*=/)
116
+ rescue
117
+ false
118
+ end unless defined?(tool_mode_configured?)
119
+
61
120
  def save_ai_tools_to_initializer(tools)
62
121
  init_path = Rails.root.join("config/initializers/rails_ai_context.rb")
63
122
  return unless File.exist?(init_path)
@@ -82,6 +141,48 @@ rescue
82
141
  end unless defined?(save_ai_tools_to_initializer)
83
142
 
84
143
  namespace :ai do
144
+ desc "Run an MCP tool from the CLI: rails 'ai:tool[schema]' table=users detail=full"
145
+ task :tool, [ :name ] => :environment do |_t, args|
146
+ require "rails_ai_context"
147
+
148
+ name = args[:name]
149
+
150
+ unless name
151
+ puts RailsAiContext::CLI::ToolRunner.tool_list
152
+ next
153
+ end
154
+
155
+ # Parse key=value pairs from ARGV (skip rake-internal args)
156
+ params = {}
157
+ ARGV.each do |arg|
158
+ next if arg.start_with?("-") || arg.include?("[") || arg == "ai:tool"
159
+ if arg.include?("=")
160
+ key, value = arg.split("=", 2)
161
+ params[key.to_sym] = value
162
+ end
163
+ end
164
+
165
+ json_mode = ENV["JSON"] == "1"
166
+
167
+ if params.delete(:help) || ARGV.include?("--help")
168
+ runner = RailsAiContext::CLI::ToolRunner.new(name, {})
169
+ puts RailsAiContext::CLI::ToolRunner.tool_help(runner.tool_class)
170
+ next
171
+ end
172
+
173
+ runner = RailsAiContext::CLI::ToolRunner.new(name, params, json_mode: json_mode)
174
+ puts runner.run
175
+ rescue RailsAiContext::CLI::ToolRunner::ToolNotFoundError => e
176
+ $stderr.puts "Error: #{e.message}"
177
+ exit 1
178
+ rescue RailsAiContext::CLI::ToolRunner::InvalidArgumentError => e
179
+ $stderr.puts "Error: #{e.message}"
180
+ exit 3
181
+ rescue => e
182
+ $stderr.puts "Error: #{e.message}"
183
+ exit 2
184
+ end
185
+
85
186
  desc "Generate AI context files for configured AI tools (prompts on first run)"
86
187
  task context: :environment do
87
188
  require "rails_ai_context"
@@ -96,6 +197,16 @@ namespace :ai do
96
197
  save_ai_tools_to_initializer(ai_tools) if ai_tools
97
198
  end
98
199
 
200
+ # Prompt for tool_mode if not yet configured in initializer
201
+ unless tool_mode_configured?
202
+ tool_mode = prompt_tool_mode
203
+ RailsAiContext.configuration.tool_mode = tool_mode
204
+ save_tool_mode_to_initializer(tool_mode)
205
+ end
206
+
207
+ # Auto-create .mcp.json when tool_mode is :mcp and it doesn't exist
208
+ ensure_mcp_json if RailsAiContext.configuration.tool_mode == :mcp
209
+
99
210
  puts "🔍 Introspecting #{Rails.application.class.module_parent_name}..."
100
211
 
101
212
  if ai_tools.nil? || ai_tools.empty?
@@ -115,7 +226,7 @@ namespace :ai do
115
226
  puts "Change AI tools: config/initializers/rails_ai_context.rb (config.ai_tools)"
116
227
  end
117
228
 
118
- desc "Generate AI context in a specific format (claude, cursor, windsurf, copilot, json)"
229
+ desc "Generate AI context in a specific format (claude, cursor, copilot, json)"
119
230
  task :context_for, [ :format ] => :environment do |_t, args|
120
231
  require "rails_ai_context"
121
232
 
@@ -131,7 +242,7 @@ namespace :ai do
131
242
  end
132
243
 
133
244
  namespace :context do
134
- { claude: "CLAUDE.md", opencode: "AGENTS.md", cursor: ".cursor/rules/", windsurf: ".windsurfrules",
245
+ { claude: "CLAUDE.md", opencode: "AGENTS.md", cursor: ".cursor/rules/",
135
246
  copilot: ".github/copilot-instructions.md", json: ".ai-context.json" }.each do |fmt, file|
136
247
  desc "Generate #{file} context file"
137
248
  task fmt => :environment do
@@ -386,7 +386,14 @@ module RailsAiContext
386
386
  # Extract method source from raw source string using indentation-based matching
387
387
  private_class_method def self.extract_method_source(source, method_name)
388
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/) }
389
+ escaped = Regexp.escape(method_name.to_s)
390
+ # Don't use \b after ? or ! — they ARE word boundaries
391
+ pattern = if method_name.to_s.end_with?("?") || method_name.to_s.end_with?("!")
392
+ /\A\s*def\s+#{escaped}/
393
+ else
394
+ /\A\s*def\s+#{escaped}\b/
395
+ end
396
+ start_idx = source_lines.index { |l| l.match?(pattern) }
390
397
  return nil unless start_idx
391
398
 
392
399
  def_indent = source_lines[start_idx][/\A\s*/].length
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RailsAiContext
4
- VERSION = "2.0.4"
4
+ VERSION = "3.0.0"
5
5
  end
@@ -3,7 +3,7 @@
3
3
  require "zeitwerk"
4
4
 
5
5
  loader = Zeitwerk::Loader.for_gem(warn_on_extra_files: false)
6
- loader.inflector.inflect("devops_introspector" => "DevOpsIntrospector")
6
+ loader.inflector.inflect("devops_introspector" => "DevOpsIntrospector", "cli" => "CLI")
7
7
  loader.ignore("#{__dir__}/generators")
8
8
  loader.ignore("#{__dir__}/rails-ai-context.rb")
9
9
  loader.setup
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.4",
10
+ "version": "3.0.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "mcpb",
14
- "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v2.0.4/rails-ai-context-mcp.mcpb",
14
+ "identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v3.0.0/rails-ai-context-mcp.mcpb",
15
15
  "fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
16
16
  "transport": {
17
17
  "type": "stdio"