rails-ai-context 0.15.10 → 1.0.0

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