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.
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiContext
4
+ module CLI
5
+ # Runs MCP tools from the command line without requiring an MCP client.
6
+ # Reads tool schemas at runtime — no hardcoded parameter lists.
7
+ #
8
+ # Usage:
9
+ # runner = ToolRunner.new("schema", ["--table", "users", "--detail", "full"])
10
+ # puts runner.run
11
+ #
12
+ # runner = ToolRunner.new("schema", { table: "users", detail: "full" })
13
+ # puts runner.run
14
+ class ToolRunner
15
+ class ToolNotFoundError < StandardError; end
16
+ class InvalidArgumentError < StandardError; end
17
+
18
+ attr_reader :tool_class, :raw_args, :json_mode
19
+
20
+ def initialize(tool_name, raw_args, json_mode: false)
21
+ @tool_class = resolve_tool(tool_name)
22
+ @raw_args = raw_args
23
+ @json_mode = json_mode
24
+ end
25
+
26
+ def run
27
+ kwargs = build_kwargs
28
+ schema = tool_schema
29
+ validate_kwargs!(kwargs, schema)
30
+ response = tool_class.call(**kwargs)
31
+ extract_output(response)
32
+ end
33
+
34
+ # List all available tools with short names and descriptions.
35
+ def self.tool_list
36
+ lines = [ "Available tools:", "" ]
37
+ available_tools.each do |tool|
38
+ short = short_name(tool.tool_name)
39
+ desc = tool.description_value.to_s[0..79]
40
+ lines << " #{short.ljust(24)} #{desc}"
41
+ end
42
+ lines << ""
43
+ lines << "Usage: rails 'ai:tool[NAME]' param=value"
44
+ lines << " rails-ai-context tool NAME --param value"
45
+ lines.join("\n")
46
+ end
47
+
48
+ # Filtered tool list respecting skip_tools config.
49
+ def self.available_tools
50
+ skip = RailsAiContext.configuration.skip_tools
51
+ tools = Server::TOOLS
52
+ tools += RailsAiContext.configuration.custom_tools
53
+ return tools if skip.empty?
54
+ tools.reject { |t| skip.include?(t.tool_name) }
55
+ end
56
+
57
+ # Generate help for a specific tool from its input_schema.
58
+ def self.tool_help(tool_class)
59
+ schema = tool_class.input_schema_value&.schema || {}
60
+ properties = schema[:properties] || {}
61
+ required = schema[:required] || []
62
+
63
+ lines = [
64
+ "#{tool_class.tool_name} — #{tool_class.description_value}",
65
+ "",
66
+ "Usage:",
67
+ " rails 'ai:tool[#{short_name(tool_class.tool_name)}]' #{properties.keys.map { |k| "#{k}=VALUE" }.join(' ')}",
68
+ " rails-ai-context tool #{short_name(tool_class.tool_name)} #{properties.keys.map { |k| "--#{k.to_s.tr('_', '-')} VALUE" }.join(' ')}",
69
+ ""
70
+ ]
71
+
72
+ if properties.any?
73
+ lines << "Options:"
74
+ properties.each do |name, prop|
75
+ flag = "--#{name.to_s.tr('_', '-')}"
76
+ type_hint = prop[:type] || "string"
77
+ type_hint = "#{type_hint} (#{prop[:enum].join('/')})" if prop[:enum]
78
+ req = required.include?(name.to_s) ? " [required]" : ""
79
+ desc = prop[:description] || ""
80
+ lines << " #{flag.ljust(24)} #{desc} (#{type_hint})#{req}"
81
+ end
82
+ else
83
+ lines << " No parameters."
84
+ end
85
+
86
+ lines.join("\n")
87
+ end
88
+
89
+ # Derive short name: rails_get_schema → schema, rails_analyze_feature → analyze_feature
90
+ def self.short_name(tool_name)
91
+ tool_name.sub(/\Arails_get_/, "").sub(/\Arails_/, "")
92
+ end
93
+
94
+ private
95
+
96
+ def tool_schema
97
+ tool_class.input_schema_value&.schema || {}
98
+ end
99
+
100
+ # Resolve tool name: tries short → medium → full form.
101
+ # "schema" → "rails_get_schema", "search_code" → "rails_search_code"
102
+ def resolve_tool(name)
103
+ tools = self.class.available_tools
104
+ tool_names = tools.map(&:tool_name)
105
+
106
+ # Try exact match
107
+ found = tools.find { |t| t.tool_name == name }
108
+ return found if found
109
+
110
+ # Try rails_ prefix
111
+ found = tools.find { |t| t.tool_name == "rails_#{name}" }
112
+ return found if found
113
+
114
+ # Try rails_get_ prefix
115
+ found = tools.find { |t| t.tool_name == "rails_get_#{name}" }
116
+ return found if found
117
+
118
+ # Try case-insensitive short name match
119
+ found = tools.find { |t| self.class.short_name(t.tool_name) == name }
120
+ return found if found
121
+
122
+ # Fuzzy suggestion
123
+ short_names = tools.map { |t| self.class.short_name(t.tool_name) }
124
+ suggestion = Tools::BaseTool.find_closest_match(name, short_names)
125
+ msg = "Unknown tool '#{name}'."
126
+ msg += " Did you mean '#{suggestion}'?" if suggestion
127
+ msg += "\n\nRun with --list to see all available tools."
128
+ raise ToolNotFoundError, msg
129
+ end
130
+
131
+ # Parse raw_args into keyword arguments hash.
132
+ # Supports both hash input (rake) and array input (CLI).
133
+ def build_kwargs
134
+ kwargs = case raw_args
135
+ when Hash
136
+ raw_args.transform_keys(&:to_sym).except(:server_context)
137
+ when Array
138
+ return parse_cli_args(raw_args).except(:server_context)
139
+ else
140
+ {}
141
+ end
142
+
143
+ properties = (tool_schema[:properties] || {})
144
+ kwargs.each do |key, value|
145
+ prop = properties[key]
146
+ next unless prop
147
+ kwargs[key] = coerce_value(value, prop)
148
+ end
149
+
150
+ kwargs
151
+ end
152
+
153
+ # Parse ["--table", "users", "--detail", "full", "--app-only"] into { table: "users", ... }
154
+ def parse_cli_args(args)
155
+ result = {}
156
+ i = 0
157
+ properties = (tool_schema[:properties] || {})
158
+
159
+ while i < args.size
160
+ arg = args[i]
161
+
162
+ if arg.start_with?("--no-")
163
+ key = arg.sub("--no-", "").tr("-", "_").to_sym
164
+ result[key] = false
165
+ i += 1
166
+ elsif arg.start_with?("--")
167
+ if arg.include?("=")
168
+ key, value = arg.sub("--", "").split("=", 2)
169
+ key = key.tr("-", "_").to_sym
170
+ result[key] = coerce_value(value, properties[key] || {})
171
+ else
172
+ key = arg.sub("--", "").tr("-", "_").to_sym
173
+ prop = properties[key] || {}
174
+
175
+ if prop[:type] == "boolean"
176
+ result[key] = true
177
+ i += 1
178
+ next
179
+ end
180
+
181
+ value = args[i + 1]
182
+ if value && !value.start_with?("--")
183
+ result[key] = coerce_value(value, prop)
184
+ i += 2
185
+ next
186
+ else
187
+ result[key] = true
188
+ i += 1
189
+ next
190
+ end
191
+ end
192
+ i += 1
193
+ elsif arg.include?("=")
194
+ # key=value style (rake)
195
+ key, value = arg.split("=", 2)
196
+ key = key.tr("-", "_").to_sym
197
+ result[key] = coerce_value(value, properties[key] || {})
198
+ i += 1
199
+ else
200
+ i += 1
201
+ end
202
+ end
203
+
204
+ result
205
+ end
206
+
207
+ # Coerce a string value to the type specified in the JSON Schema property.
208
+ def coerce_value(raw, property_schema)
209
+ case property_schema[:type]
210
+ when "integer"
211
+ raw.to_i
212
+ when "boolean"
213
+ %w[true 1 yes].include?(raw.to_s.downcase)
214
+ when "array"
215
+ raw.is_a?(Array) ? raw : raw.to_s.split(",").map(&:strip)
216
+ else
217
+ raw.to_s
218
+ end
219
+ end
220
+
221
+ # Validate kwargs against the tool's input_schema.
222
+ def validate_kwargs!(kwargs, schema)
223
+ properties = schema[:properties] || {}
224
+ required = (schema[:required] || []).map(&:to_s)
225
+
226
+ # Check required params
227
+ required.each do |param|
228
+ unless kwargs.key?(param.to_sym) && !kwargs[param.to_sym].nil? && kwargs[param.to_sym].to_s != ""
229
+ raise InvalidArgumentError,
230
+ "Missing required parameter '#{param}' for #{tool_class.tool_name}.\n" \
231
+ "Run: rails-ai-context tool #{self.class.short_name(tool_class.tool_name)} --help"
232
+ end
233
+ end
234
+
235
+ # Check enum constraints
236
+ kwargs.each do |key, value|
237
+ prop = properties[key]
238
+ next unless prop&.dig(:enum)
239
+ unless prop[:enum].include?(value.to_s)
240
+ raise InvalidArgumentError,
241
+ "Invalid value '#{value}' for --#{key.to_s.tr('_', '-')}. " \
242
+ "Must be one of: #{prop[:enum].join(', ')}"
243
+ end
244
+ end
245
+ end
246
+
247
+ # Extract text from MCP::Tool::Response.
248
+ def extract_output(response)
249
+ text = response.content.first&.dig(:text) || ""
250
+ if json_mode
251
+ require "json"
252
+ JSON.pretty_generate(tool: tool_class.tool_name, output: text)
253
+ else
254
+ text
255
+ end
256
+ end
257
+ end
258
+ end
259
+ end
@@ -56,7 +56,7 @@ module RailsAiContext
56
56
  # Debounce interval in seconds for live reload file watching
57
57
  attr_accessor :live_reload_debounce
58
58
 
59
- # Whether to generate root-level context files (CLAUDE.md, AGENTS.md, .windsurfrules, etc.)
59
+ # Whether to generate root-level context files (CLAUDE.md, AGENTS.md, etc.)
60
60
  # When false, only generates split rule files (.claude/rules/, .cursor/rules/, etc.)
61
61
  attr_accessor :generate_root_files
62
62
 
@@ -76,9 +76,12 @@ module RailsAiContext
76
76
  attr_accessor :skip_tools
77
77
 
78
78
  # Which AI tools to generate context for (selected during install)
79
- # nil = all formats, or %i[claude cursor copilot windsurf opencode]
79
+ # nil = all formats, or %i[claude cursor copilot opencode]
80
80
  attr_accessor :ai_tools
81
81
 
82
+ # Tool invocation mode: :mcp (MCP primary + CLI fallback) or :cli (CLI only)
83
+ attr_accessor :tool_mode
84
+
82
85
  # Filtering — customize what's hidden from AI output
83
86
  attr_accessor :excluded_controllers # Controller classes hidden from listings (e.g. DeviseController)
84
87
  attr_accessor :excluded_route_prefixes # Route controller prefixes hidden with app_only (e.g. action_mailbox/)
@@ -153,6 +156,7 @@ module RailsAiContext
153
156
  @custom_tools = []
154
157
  @skip_tools = []
155
158
  @ai_tools = nil
159
+ @tool_mode = :mcp
156
160
  @search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
157
161
  @concern_paths = %w[app/models/concerns app/controllers/concerns]
158
162
  end
@@ -7,6 +7,7 @@ module RailsAiContext
7
7
  class ClaudeRulesSerializer
8
8
  include StackOverviewHelper
9
9
  include DesignSystemHelper
10
+ include ToolGuideHelper
10
11
 
11
12
  attr_reader :context
12
13
 
@@ -293,141 +294,8 @@ module RailsAiContext
293
294
  lines.join("\n")
294
295
  end
295
296
 
296
- def render_mcp_tools_reference # rubocop:disable Metrics/MethodLength,Metrics/AbcSize
297
- lines = [
298
- "# Rails MCP Tools — MANDATORY, Use Before Read/Grep",
299
- "",
300
- "CRITICAL: This project has live MCP tools. You MUST use them for gathering context.",
301
- "MCP tools return parsed, up-to-date, token-efficient data with line numbers.",
302
- "Read files ONLY when you are about to Edit them. Never read reference files directly.",
303
- "",
304
- "## Mandatory Workflow — Follow This Order",
305
- "",
306
- "### 1. Gathering context (BEFORE writing any code)",
307
- "- Exploring a feature area → `rails_analyze_feature(feature:\"...\")` — NOT Explore agents, NOT Grep",
308
- "- Understanding a model → `rails_get_model_details(model:\"...\")` — NOT reading the model file",
309
- "- Checking database columns → `rails_get_schema(table:\"...\")` — NOT reading db/schema.rb",
310
- "- Checking routes → `rails_get_routes(controller:\"...\")` — NOT reading config/routes.rb",
311
- "- Understanding a controller → `rails_get_controllers(controller:\"...\", action:\"...\")` — NOT reading the file",
312
- "- Finding code patterns → `rails_search_code(pattern:\"...\")` — NOT using Grep tool",
313
- "- UI patterns → `rails_get_design_system` — NOT reading view files for patterns",
314
- "- Full cross-layer context → `rails_get_context(model:\"...\")` — replaces multiple Read calls",
315
- "",
316
- "### 2. Reading files (ONLY files you will edit)",
317
- "- Read a file ONLY when you are about to Edit it (Read is required before Edit)",
318
- "- For everything else — schema, routes, models, tests, patterns — use MCP tools above",
319
- "",
320
- "### 3. After editing (EVERY time, no exceptions)",
321
- "- `rails_validate(files:[\"path/to/file.rb\", \"path/to/view.erb\"])` — one call checks all",
322
- "- Do NOT run `ruby -c`, `erb` checks, or `node -c` separately — use rails_validate instead",
323
- "- Do NOT re-read files to verify edits. Trust your Edit and validate syntax only.",
324
- "",
325
- "## Do NOT Bypass — Anti-Patterns",
326
- "| Instead of... | Use this MCP tool |",
327
- "|---------------|-------------------|",
328
- "| Reading db/schema.rb | `rails_get_schema(table:\"name\")` |",
329
- "| Reading config/routes.rb for reference | `rails_get_routes(controller:\"name\")` |",
330
- "| Reading model files for associations | `rails_get_model_details(model:\"Name\")` |",
331
- "| Grep/Explore agent for code search | `rails_search_code(pattern:\"regex\")` |",
332
- "| Reading test files for patterns | `rails_get_test_info(model:\"Name\")` |",
333
- "| Reading controller files for context | `rails_get_controllers(controller:\"Name\")` |",
334
- "| Reading JS files for Stimulus API | `rails_get_stimulus(controller:\"name\")` |",
335
- "| Multiple reads to understand a feature | `rails_analyze_feature(feature:\"keyword\")` |",
336
- "| ruby -c / erb / node -c after edits | `rails_validate(files:[...])` |",
337
- "",
338
- "## Reference-only files — NEVER read these, use MCP",
339
- "- db/schema.rb → `rails_get_schema` (column types in rails-schema.md rules for quick checks)",
340
- "- config/routes.rb → `rails_get_routes` (Read ONLY when adding routes)",
341
- "- Model files → `rails_get_model_details` (Read ONLY when editing business logic)",
342
- "- app/javascript/controllers/index.js → Stimulus auto-registers. Never read this.",
343
- "- Test files → `rails_get_test_info`",
344
- "",
345
- "## Tools (25)",
346
- "",
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
- "- `rails_get_schema(table:\"users\")` — full detail for one table",
350
- "",
351
- "**rails_get_model_details** — associations, validations, scopes, enums, callbacks",
352
- "- `rails_get_model_details(detail:\"summary\")` — list all model names",
353
- "- `rails_get_model_details(model:\"User\")` — full detail for one model",
354
- "",
355
- "**rails_get_routes** — HTTP verbs, paths, controller actions",
356
- "- `rails_get_routes(detail:\"summary\")` — route counts per controller",
357
- "- `rails_get_routes(controller:\"users\")` — routes for one controller",
358
- "",
359
- "**rails_get_controllers** — actions, filters, strong params, action source code",
360
- "- `rails_get_controllers(detail:\"summary\")` — names + action counts",
361
- "- `rails_get_controllers(controller:\"CooksController\", action:\"index\")` — action source code + filters",
362
- "",
363
- "**rails_get_view** — view templates, partials, Stimulus references",
364
- "- `rails_get_view(controller:\"cooks\")` — list all views for a controller",
365
- "- `rails_get_view(path:\"cooks/index.html.erb\")` — full template content",
366
- "",
367
- "**rails_get_stimulus** — Stimulus controllers with targets, values, actions",
368
- "- `rails_get_stimulus(detail:\"summary\")` — all controllers with counts",
369
- "- `rails_get_stimulus(controller:\"filter-form\")` — full detail for one controller",
370
- "",
371
- "**rails_get_test_info** — test framework, fixtures, factories, helpers",
372
- "- `rails_get_test_info(detail:\"full\")` — fixture names, factory names, helper setup",
373
- "- `rails_get_test_info(model:\"Cook\")` — full test source for a model",
374
- "- `rails_get_test_info(model:\"Cook\", detail:\"summary\")` — test names only (saves tokens)",
375
- "- `rails_get_test_info(controller:\"Cooks\")` — existing controller tests",
376
- "",
377
- "**rails_get_edit_context** — surgical edit helper with line numbers",
378
- "- `rails_get_edit_context(file:\"app/models/cook.rb\", near:\"scope\")` — returns code around match with line numbers",
379
- "",
380
- "**rails_validate** — syntax checker for edited files",
381
- "- `rails_validate(files:[\"app/models/cook.rb\"])` — checks Ruby, ERB, JS syntax in one call",
382
- "",
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, including inherited filters from parent controllers and code-ready route helpers",
385
- "",
386
- "**rails_get_design_system** — color palette, component patterns, canonical page examples",
387
- "- `rails_get_design_system(detail:\"standard\")` — colors + components + real HTML examples + design rules",
388
- "- `rails_get_design_system(detail:\"full\")` — + typography, responsive, dark mode, layout, spacing",
389
- "",
390
- "**rails_get_config** — cache store, session, timezone, middleware, initializers",
391
- "**rails_get_gems** — notable gems categorized by function",
392
- "**rails_get_conventions** — architecture patterns, directory structure",
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",
397
- "",
398
- "**rails_security_scan** — Brakeman security analysis",
399
- "- `rails_security_scan` — run security scan, returns warnings by confidence",
400
- "",
401
- "**rails_get_concern** — concern methods and includers",
402
- "- `rails_get_concern(name:\"Searchable\")` — methods, included modules, includers",
403
- "- `rails_get_concern(name:\"Searchable\", detail:\"full\")` — + method source code",
404
- "",
405
- "**rails_get_callbacks** — model callbacks in execution order",
406
- "- `rails_get_callbacks(model:\"User\")` — before/after/around callbacks by type",
407
- "",
408
- "**rails_get_helper_methods** — app + framework helpers",
409
- "- `rails_get_helper_methods` — all helper methods across app and framework",
410
- "",
411
- "**rails_get_service_pattern** — service object patterns and interfaces",
412
- "- `rails_get_service_pattern` — service objects with call signatures and dependencies",
413
- "",
414
- "**rails_get_job_pattern** — background job patterns and schedules",
415
- "- `rails_get_job_pattern` — job classes, queues, retry policies, schedules",
416
- "",
417
- "**rails_get_env** — environment variables and credentials keys",
418
- "- `rails_get_env` — ENV vars referenced in code + credentials structure",
419
- "",
420
- "**rails_get_partial_interface** — partial locals contract",
421
- "- `rails_get_partial_interface(path:\"shared/_form\")` — required/optional locals for a partial",
422
- "",
423
- "**rails_get_turbo_map** — Turbo Streams/Frames wiring",
424
- "- `rails_get_turbo_map` — Turbo Frame IDs, Stream channels, broadcast sources",
425
- "",
426
- "**rails_get_context** — composite cross-layer context",
427
- "- `rails_get_context(model:\"User\")` — schema + model + controllers + views in one call"
428
- ]
429
-
430
- lines.join("\n")
297
+ def render_mcp_tools_reference
298
+ render_tools_guide.join("\n")
431
299
  end
432
300
  end
433
301
  end
@@ -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
 
@@ -196,45 +197,8 @@ module RailsAiContext
196
197
  render_design_system(context, max_lines: 30)
197
198
  end
198
199
 
199
- def render_mcp_guide # rubocop:disable Metrics/MethodLength
200
- [
201
- "## MCP Tools (25) — ALWAYS Use These First",
202
- "",
203
- "This project has live MCP tools. You MUST use them instead of reading reference files.",
204
- "Start with `detail:\"summary\"`, then drill into specifics.",
205
- "",
206
- "### Mandatory Workflow",
207
- "1. **Before exploring a feature**: `rails_analyze_feature(feature:\"...\")` — NOT Explore agents or Grep",
208
- "2. **Before writing migrations**: `rails_get_schema(table:\"...\")` — NOT reading db/schema.rb",
209
- "3. **Before modifying a model**: `rails_get_model_details(model:\"...\")` — NOT reading the model file",
210
- "4. **Before adding routes**: `rails_get_routes(controller:\"...\")` — Read routes.rb only when you will edit it",
211
- "5. **Before creating views**: `rails_get_design_system` — match existing patterns",
212
- "6. **After editing ANY file**: `rails_validate(files:[...])` — no exceptions, no ruby -c / erb / node -c",
213
- "",
214
- "### Quick Reference",
215
- "| Need | Use this MCP tool | Do NOT use |",
216
- "|------|-------------------|------------|",
217
- "| Column types | `rails_get_schema(table:\"x\")` — includes orphaned table warnings | Read db/schema.rb |",
218
- "| Model associations | `rails_get_model_details(model:\"X\")` | Read app/models/x.rb |",
219
- "| Route paths | `rails_get_routes(controller:\"x\")` | Read config/routes.rb |",
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
- "| Validate edits | `rails_validate(files:[...])` | ruby -c / erb / node |",
223
- "| Controller logic | `rails_get_controllers(controller:\"X\", action:\"y\")` | Read controller file |",
224
- "| UI patterns | `rails_get_design_system` | Read view files |",
225
- "| Stimulus API | `rails_get_stimulus(controller:\"x\")` | Read JS files |",
226
- "| Test patterns | `rails_get_test_info(model:\"X\")` | Read test files |",
227
- "| Full context | `rails_get_context(model:\"X\")` | Multiple Read calls |",
228
- "",
229
- "### More Tools",
230
- "- `rails_get_view(controller:\"x\")` | `rails_get_concern(name:\"X\", detail:\"full\")` — concern methods with source code | `rails_get_callbacks(model:\"X\")`",
231
- "- `rails_get_helper_methods` | `rails_get_service_pattern` | `rails_get_job_pattern`",
232
- "- `rails_get_env` | `rails_get_partial_interface(path:\"shared/_form\")` | `rails_get_turbo_map`",
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",
235
- "- `rails_get_edit_context(file:\"path\", near:\"keyword\")` — surgical edit helper with line numbers",
236
- ""
237
- ]
200
+ def render_mcp_guide
201
+ render_tools_guide
238
202
  end
239
203
 
240
204
  def render_conventions
@@ -267,18 +231,8 @@ module RailsAiContext
267
231
  "- Follow existing patterns and conventions",
268
232
  "- Match existing code style",
269
233
  "- Run tests after changes",
270
- "- After editing, ALWAYS use `rails_validate(files:[...])` — do NOT use separate ruby -c / erb / node -c calls",
271
234
  "- Do NOT re-read files to verify edits — trust your Edit, validate syntax only",
272
235
  "- Stimulus controllers auto-register — no manual import in controllers/index.js needed",
273
- "",
274
- "## MCP-First Rule — Do NOT Bypass",
275
- "- Do NOT read db/schema.rb — use `rails_get_schema`",
276
- "- Do NOT read config/routes.rb for reference — use `rails_get_routes`",
277
- "- Do NOT read model files for associations/scopes — use `rails_get_model_details`",
278
- "- Do NOT use Grep to search code — use `rails_search_code`",
279
- "- Do NOT launch Explore agents for context — use `rails_analyze_feature`",
280
- "- Do NOT read test files for patterns — use `rails_get_test_info`",
281
- "- Read files ONLY when you are about to Edit them",
282
236
  ""
283
237
  ]
284
238
  end
@@ -3,7 +3,7 @@
3
3
  module RailsAiContext
4
4
  module Serializers
5
5
  # Orchestrates writing context files to disk in various formats.
6
- # Supports: CLAUDE.md, AGENTS.md, .windsurfrules, .github/copilot-instructions.md, JSON
6
+ # Supports: CLAUDE.md, AGENTS.md, .github/copilot-instructions.md, JSON
7
7
  # Also generates split rule files for AI tools that support them.
8
8
  #
9
9
  # Root files (CLAUDE.md, etc.) are wrapped in section markers so user content
@@ -15,7 +15,6 @@ module RailsAiContext
15
15
  FORMAT_MAP = {
16
16
  claude: "CLAUDE.md",
17
17
  opencode: "AGENTS.md",
18
- windsurf: ".windsurfrules",
19
18
  copilot: ".github/copilot-instructions.md",
20
19
  json: ".ai-context.json"
21
20
  }.freeze
@@ -78,7 +77,6 @@ module RailsAiContext
78
77
  when :json then JsonSerializer.new(context).call
79
78
  when :claude then ClaudeSerializer.new(context).call
80
79
  when :opencode then OpencodeSerializer.new(context).call
81
- when :windsurf then WindsurfSerializer.new(context).call
82
80
  when :copilot then CopilotSerializer.new(context).call
83
81
  else MarkdownSerializer.new(context).call
84
82
  end
@@ -136,12 +134,6 @@ module RailsAiContext
136
134
  skipped.concat(result[:skipped])
137
135
  end
138
136
 
139
- if formats.include?(:windsurf)
140
- result = WindsurfRulesSerializer.new(context).call(output_dir)
141
- written.concat(result[:written])
142
- skipped.concat(result[:skipped])
143
- end
144
-
145
137
  if formats.include?(:opencode)
146
138
  result = OpencodeRulesSerializer.new(context).call(output_dir)
147
139
  written.concat(result[:written])
@@ -7,6 +7,7 @@ module RailsAiContext
7
7
  class CopilotInstructionsSerializer
8
8
  include StackOverviewHelper
9
9
  include DesignSystemHelper
10
+ include ToolGuideHelper
10
11
 
11
12
  attr_reader :context
12
13
 
@@ -213,52 +214,17 @@ module RailsAiContext
213
214
  lines.join("\n")
214
215
  end
215
216
 
216
- def render_mcp_tools_instructions # rubocop:disable Metrics/MethodLength
217
+ def render_mcp_tools_instructions
217
218
  lines = [
218
219
  "---",
219
220
  "applyTo: \"**/*\"",
221
+ "excludeAgent: \"code-review\"",
220
222
  "---",
221
- "",
222
- "# Rails MCP Tools (25) — MANDATORY, Use Before Read",
223
- "",
224
- "CRITICAL: This project has live MCP tools. You MUST use them for gathering context.",
225
- "Read files ONLY when you are about to edit them. Never read reference files directly.",
226
- "Start with `detail:\"summary\"`, then drill into specifics.",
227
- "",
228
- "## Mandatory Workflow",
229
- "1. Gathering context → use MCP tools (NOT file reads, NOT grep)",
230
- "2. Reading files → ONLY files you will edit",
231
- "3. After editing → `rails_validate(files:[...])` every time, no exceptions",
232
- "",
233
- "## Do NOT Bypass",
234
- "| Instead of... | Use this MCP tool |",
235
- "|---------------|-------------------|",
236
- "| Reading db/schema.rb | `rails_get_schema(table:\"name\")` |",
237
- "| Reading config/routes.rb | `rails_get_routes(controller:\"name\")` |",
238
- "| Reading model files for context | `rails_get_model_details(model:\"Name\")` |",
239
- "| Grep for code patterns | `rails_search_code(pattern:\"regex\")` |",
240
- "| Reading test files | `rails_get_test_info(model:\"Name\")` |",
241
- "| Reading controller for context | `rails_get_controllers(controller:\"Name\", action:\"x\")` |",
242
- "| Reading JS for Stimulus API | `rails_get_stimulus(controller:\"name\")` |",
243
- "| Multiple reads for a feature | `rails_analyze_feature(feature:\"keyword\")` |",
244
- "| ruby -c / erb / node -c | `rails_validate(files:[...])` |",
245
- "",
246
- "## All 25 Tools",
247
- "- `rails_get_schema` | `rails_get_model_details` | `rails_get_routes` | `rails_get_controllers`",
248
- "- `rails_get_view` | `rails_get_stimulus` | `rails_get_test_info` | `rails_analyze_feature`",
249
- "- `rails_get_design_system` | `rails_get_edit_context` | `rails_validate` | `rails_search_code`",
250
- "- `rails_get_config` | `rails_get_gems` | `rails_get_conventions` | `rails_security_scan`",
251
- "- `rails_get_concern` | `rails_get_callbacks` | `rails_get_helper_methods` | `rails_get_service_pattern`",
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",
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"
223
+ ""
260
224
  ]
261
225
 
226
+ lines.concat(render_tools_guide)
227
+
262
228
  lines.join("\n")
263
229
  end
264
230
  end