rails-ai-context 5.18.0 → 5.19.1

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.
Files changed (86) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +103 -0
  3. data/CONTEXT.md +17 -0
  4. data/CONTRIBUTING.md +2 -2
  5. data/README.md +19 -13
  6. data/SECURITY.md +1 -1
  7. data/app/controllers/rails_ai_context/mcp_controller.rb +52 -10
  8. data/docs/ARCHITECTURE.md +8 -8
  9. data/docs/CLI.md +2 -2
  10. data/docs/COMPATIBILITY.md +3 -2
  11. data/docs/CONFIGURATION.md +3 -3
  12. data/docs/CUSTOM_TOOLS.md +2 -2
  13. data/docs/FAQ.md +4 -4
  14. data/docs/GUIDE.md +19 -16
  15. data/docs/INTROSPECTORS.md +8 -7
  16. data/docs/SECURITY.md +2 -2
  17. data/docs/SETUP.md +2 -2
  18. data/docs/STANDALONE.md +2 -2
  19. data/docs/TOOLS.md +50 -2
  20. data/docs/TROUBLESHOOTING.md +1 -1
  21. data/docs/_config.yml +1 -1
  22. data/docs/index.md +3 -3
  23. data/exe/rails-ai-context +6 -8
  24. data/lib/generators/rails_ai_context/install/install_generator.rb +6 -6
  25. data/lib/rails_ai_context/configuration.rb +1 -1
  26. data/lib/rails_ai_context/count_phrase.rb +24 -0
  27. data/lib/rails_ai_context/doctor.rb +21 -17
  28. data/lib/rails_ai_context/facts_formatter.rb +5 -3
  29. data/lib/rails_ai_context/introspector.rb +1 -0
  30. data/lib/rails_ai_context/introspectors/autoload_introspector.rb +3 -2
  31. data/lib/rails_ai_context/introspectors/env_config_introspector.rb +127 -0
  32. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +38 -21
  33. data/lib/rails_ai_context/introspectors/schema_introspector.rb +1 -1
  34. data/lib/rails_ai_context/introspectors/source_introspector.rb +1 -0
  35. data/lib/rails_ai_context/json_budget.rb +301 -0
  36. data/lib/rails_ai_context/live_reload.rb +6 -4
  37. data/lib/rails_ai_context/resources.rb +2 -2
  38. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +4 -4
  39. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +8 -6
  40. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +3 -3
  41. data/lib/rails_ai_context/serializers/copilot_serializer.rb +3 -3
  42. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +3 -3
  43. data/lib/rails_ai_context/serializers/markdown_serializer.rb +4 -4
  44. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +12 -10
  45. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +12 -4
  46. data/lib/rails_ai_context/server.rb +13 -0
  47. data/lib/rails_ai_context/tasks/rails_ai_context.rake +2 -2
  48. data/lib/rails_ai_context/tools/analyze_feature.rb +5 -5
  49. data/lib/rails_ai_context/tools/base_tool.rb +3 -1
  50. data/lib/rails_ai_context/tools/get_active_support.rb +107 -0
  51. data/lib/rails_ai_context/tools/get_api.rb +3 -3
  52. data/lib/rails_ai_context/tools/get_autoload.rb +71 -0
  53. data/lib/rails_ai_context/tools/get_callbacks.rb +2 -2
  54. data/lib/rails_ai_context/tools/get_component_catalog.rb +2 -2
  55. data/lib/rails_ai_context/tools/get_concern.rb +3 -3
  56. data/lib/rails_ai_context/tools/get_controllers.rb +1 -1
  57. data/lib/rails_ai_context/tools/get_conventions.rb +1 -1
  58. data/lib/rails_ai_context/tools/get_edit_context.rb +1 -1
  59. data/lib/rails_ai_context/tools/get_engines.rb +56 -0
  60. data/lib/rails_ai_context/tools/get_env_config.rb +91 -0
  61. data/lib/rails_ai_context/tools/get_frontend_stack.rb +6 -6
  62. data/lib/rails_ai_context/tools/get_helper_methods.rb +2 -2
  63. data/lib/rails_ai_context/tools/get_i18n.rb +142 -0
  64. data/lib/rails_ai_context/tools/get_job_pattern.rb +3 -3
  65. data/lib/rails_ai_context/tools/get_mailers.rb +65 -0
  66. data/lib/rails_ai_context/tools/get_model_details.rb +2 -2
  67. data/lib/rails_ai_context/tools/get_partial_interface.rb +3 -3
  68. data/lib/rails_ai_context/tools/get_routes.rb +5 -5
  69. data/lib/rails_ai_context/tools/get_schema.rb +6 -6
  70. data/lib/rails_ai_context/tools/get_service_pattern.rb +3 -3
  71. data/lib/rails_ai_context/tools/get_stimulus.rb +5 -5
  72. data/lib/rails_ai_context/tools/get_test_info.rb +8 -8
  73. data/lib/rails_ai_context/tools/get_view.rb +11 -11
  74. data/lib/rails_ai_context/tools/onboard.rb +5 -10
  75. data/lib/rails_ai_context/tools/query.rb +1 -1
  76. data/lib/rails_ai_context/tools/read_logs.rb +1 -1
  77. data/lib/rails_ai_context/tools/review_changes.rb +3 -3
  78. data/lib/rails_ai_context/tools/runtime_info.rb +3 -3
  79. data/lib/rails_ai_context/tools/search_code.rb +5 -5
  80. data/lib/rails_ai_context/tools/search_docs.rb +2 -2
  81. data/lib/rails_ai_context/tools/security_scan.rb +9 -4
  82. data/lib/rails_ai_context/tools/session_context.rb +2 -2
  83. data/lib/rails_ai_context/version.rb +1 -1
  84. data/lib/rails_ai_context/vfs.rb +4 -4
  85. data/server.json +4 -4
  86. metadata +13 -3
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d4a8615479dc45f376da0873dbd18511feda7dafddad0e4c4c14039d26f8a4bf
4
- data.tar.gz: 9fa06c53d871f05d30c284964777ebd15b26d44e87284e9232591e288ab47d29
3
+ metadata.gz: 175ccbf5bac4703a8f9699708a02435a9b80546c09129deae79ffb5f182e14e6
4
+ data.tar.gz: 0adbdf439e6ac853606fc786d6f92041a70aec4e2379ebfe98132eb14352b219
5
5
  SHA512:
6
- metadata.gz: 38cc97cb5ec47c4ecd680ebb588169372d4d5a41c53dd0c59fde9260dc2bffad4f9830ffea5bc84cbef0079b167d522bccf77903104e5f6e5a03bd93aa85445d
7
- data.tar.gz: 9987140ad897944e8def9318c6eedad861228f345465937062dba5e6ea36a2c47246d5c0697acce041afe7966b866d40bd289e67eeb1d582a6b220cc5c6dc147
6
+ metadata.gz: 9bb08e8b5d06124360be6870c7f667f008a666d07bea55a33b89662049ea1ee00b3ab88b5c5060ee70c47b8d1b77c422cfd8434f3327094427e2ac321c8a9201
7
+ data.tar.gz: 160c8b647c1a6be4bec7290be0308a98318edd92c2525235410bd1fed94459ddd107538f5ced4c8fced29745a9de00aa8dfc37f0bdc9274afa358c0aa2f82e3f
data/CHANGELOG.md CHANGED
@@ -5,6 +5,109 @@ 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
+ ## [5.19.1] - 2026-08-10
9
+
10
+ ### Fixed
11
+
12
+ - **Static-tier routes no longer inherit a closed `namespace` block.**
13
+ `SourceIntrospector` registered its listeners for every Prism event except
14
+ `:on_call_node_leave`, so the two listeners that pop a scope stack never
15
+ received it. With `--no-boot`, every route declared after a
16
+ `namespace :admin do ... end` block was reported under that namespace, so an
17
+ app with `/comments` and `/health` served `/admin/comments` and
18
+ `/admin/health` tagged `[VERIFIED]`. The same cause put a gem declared after
19
+ a `group :development do ... end` block into that group in
20
+ `rails_get_gems`' group listing. Booted mode was never affected.
21
+ - **`rails_get_i18n` coverage is measured against the default locale's keys.**
22
+ It compared raw key counts, so a locale defining five keys against a
23
+ one-key default reported `500.0%`. Coverage is now the share of the default
24
+ locale's keys the other locale also defines, and each locale reports how
25
+ many keys are missing and how many it adds beyond the default.
26
+ - **`rails_get_autoload` reports each path once.** Rails lists a path once per
27
+ railtie that contributed it, so an app using `config.autoload_lib` showed
28
+ `lib` twice under a count of two.
29
+ - **Counts of one read as singular.** Tools, generated context files, `doctor`,
30
+ `facts`, the rake tasks and the standalone CLI wrote `1 keys`,
31
+ `1 associations`, `1 pending migration(s)` and the like. A locale file with
32
+ one key now reads `1 key`, a model with one association reads
33
+ `1 association`, and `facts` reads `1 index, 1 FK`. Counts render through
34
+ one shared helper, replacing the four spellings that had grown up across the
35
+ render code: raw interpolation, a `== 1 ?` ternary, an `#{"s" unless n == 1}`
36
+ suffix, and the `(s)` hedge. A handful of sites keep raw interpolation on
37
+ purpose: ratios (`1/1 files passed`), diff stats, and byte or row counts
38
+ whose branch only runs past a limit.
39
+ - **A locale file with a symbol root key counts toward coverage.** Both `en:`
40
+ and `:en:` load as valid YAML, but only the string form had its root
41
+ stripped, so a symbol-rooted locale compared its `es.`-prefixed paths
42
+ against nothing and scored 0%.
43
+
44
+ ## [5.19.0] - 2026-08-09
45
+
46
+ ### Added - 6 new tools surfacing previously unserved introspection (45 tools total)
47
+
48
+ An audit found five introspectors whose data never reached the tool
49
+ surface (three only served context files; `:autoload` and
50
+ `:active_support` were unreachable entirely), plus one nervous-system
51
+ gap nothing introspected. All six are now first-class tools, registered
52
+ automatically in both MCP and CLI:
53
+
54
+ - **`rails_get_i18n`** - default/available locales, backend, locale files with
55
+ key counts, per-locale coverage vs the default locale, and fallbacks
56
+ (data: `:i18n` introspector, previously serializer-only).
57
+ - **`rails_get_mailers`** - every ActionMailer class with its delivery actions
58
+ and delivery method (data: `:jobs` introspector's mailer extraction,
59
+ previously serializer-only). Filter with `mailer:"UserMailer"`.
60
+ - **`rails_get_engines`** - engines mounted in `config/routes.rb` with
61
+ known-engine descriptions, plus loaded engine classes with route/model
62
+ counts (data: `:engines` introspector, previously resource-only).
63
+ - **`rails_get_autoload`** - Zeitwerk vs Classic mode, autoloaders with
64
+ collapsed/ignored dirs, autoload/eager-load paths, and custom inflections
65
+ (data: `:autoload` introspector, previously unreachable).
66
+ - **`rails_get_active_support`** - concerns registry, deprecators,
67
+ MessageVerifier/MessageEncryptor usage, tagged logging, subscribed
68
+ `on_load` hooks, and cache store (data: `:active_support` introspector,
69
+ previously unreachable).
70
+ - **`rails_get_env_config`** - per-environment configuration from
71
+ `config/environments/*.rb`: notable toggles (`force_ssl`, `eager_load`,
72
+ caching, log level, queue adapter, mailer delivery) and every config key
73
+ each environment sets. Backed by the new **EnvConfigIntrospector**
74
+ (40 introspectors total, wired into `PRESETS[:full]`; file-based, so it
75
+ also works in the static tier). Config keys and values are read with
76
+ `ConfigAssignmentListener`, so a multi-line value, an assignment nested in
77
+ a conditional, and the `Rails.application.config.x = y` form all read
78
+ correctly. The key list pages with `offset`/`limit`.
79
+
80
+ ### Fixed
81
+
82
+ - **Engine-mounted MCP returns JSON-RPC errors instead of Rails 500s.**
83
+ `McpController#handle` had no rescue around `handle_request` - a
84
+ transport-level exception escaped into a generic Rails HTML 500, breaking
85
+ the client's JSON-RPC loop. It now answers 500 with a JSON-RPC `-32603`
86
+ body, mirroring `RailsAiContext::Middleware`.
87
+ - **Standalone HTTP transport survives transport exceptions.** The Rack
88
+ lambda behind `rails-ai-context serve --transport http` let a
89
+ `handle_request` exception propagate to rackup (dropped connection). It
90
+ now returns the same JSON-RPC `-32603` body.
91
+ - **MCP resources honor `max_tool_response_chars` without breaking the JSON
92
+ contract.** Static resource, model, and VFS routes payloads were emitted
93
+ unbounded (a huge schema or routes table rode a single JSON-RPC frame).
94
+ They now fit the cap by dropping whole elements from the data rather than
95
+ slicing the serialized string, so a capped payload still parses as the
96
+ `application/json` it is labeled. What was dropped is reported under a
97
+ `_truncated` key, which also counts any over-long string value that had to
98
+ be cut. New `RailsAiContext::JsonBudget` owns the reduction.
99
+ - **A committed SSE stream is no longer overwritten by the error handler.**
100
+ `McpController#handle`'s rescue set a status, headers, and a JSON body on
101
+ responses that were already on the wire - closing a stream commits it, so
102
+ every streaming failure reached the rescue committed. Assigning a body
103
+ there swapped the stream out from under the thread draining it, turning a
104
+ truncated SSE response into a garbled one. Committed failures now re-raise
105
+ to `ActionController::Live`, which logs them with a backtrace and closes
106
+ the connection; uncommitted failures still get the JSON-RPC `-32603` body.
107
+ - **`server.json` tool count** said 38 while the gem served 39; now tracks
108
+ the real count (45). Broken `RAILS_NERVOUS_SYSTEM.md` link in
109
+ `docs/INTROSPECTORS.md` replaced with a plain reference.
110
+
8
111
  ## [5.18.0] - 2026-08-09
9
112
 
10
113
  ### Added
data/CONTEXT.md ADDED
@@ -0,0 +1,17 @@
1
+ # Context
2
+
3
+ Terms this project uses in a narrower sense than everyday English. One entry per term that has caused a naming collision or an ambiguous read.
4
+
5
+ ## Environment
6
+
7
+ Overloaded. Always qualify it; never use "environment" bare in a tool name, an introspector name, or a config key.
8
+
9
+ **Process environment** - the environment variables a running app sees, plus the places they are declared (`ENV[]` call sites, `.env.example`, Dockerfile, credentials keys). Served by `rails_get_env`, read by `EnvIntrospector`.
10
+
11
+ **Environment config** - what `config/environments/*.rb` declares per environment: the assigned `config.*` keys and the values of the notable toggles. Served by `rails_get_env_config`, read by `EnvConfigIntrospector`.
12
+
13
+ **Environment** (unqualified, as data) - a single named Rails environment: development, production, staging. This is the only sense in which the bare word is allowed, and only as a value, never as a name. The `:environments` payload key means "the list of these", which is why it kept its name when the introspector was renamed.
14
+
15
+ ## Static tier
16
+
17
+ The mode where the app did not boot, or `--no-boot` was passed. An introspector answers in this tier only if it defines `static_call`. Two shapes count as defining it: reading a different source (parsing `db/schema.rb` instead of querying the connection), and reading the same source a booted app would (a file-based introspector, where `static_call` is the same work under another name).
data/CONTRIBUTING.md CHANGED
@@ -19,8 +19,8 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
19
19
  ```
20
20
  lib/rails_ai_context/
21
21
  ├── cli/ # CLI tool runner (tool_runner.rb) - executes MCP tools from rake/Thor
22
- ├── introspectors/ # 39 introspectors (schema, models, routes, etc.)
23
- ├── tools/ # 39 MCP tools with detail levels and pagination
22
+ ├── introspectors/ # 40 introspectors (schema, models, routes, etc.)
23
+ ├── tools/ # 45 MCP tools with detail levels and pagination
24
24
  ├── serializers/ # Per-assistant formatters + shared ToolGuideHelper
25
25
  ├── server.rb # MCP server setup (stdio + HTTP)
26
26
  ├── live_reload.rb # MCP live reload (file watcher + cache invalidation)
data/README.md CHANGED
@@ -68,7 +68,7 @@ rails-ai-context serve # start MCP server
68
68
 
69
69
  </div>
70
70
 
71
- Now your AI doesn't guess - it **asks your app directly.** 39 tools and 5 resource templates that query your schema, models, routes, controllers, views, and conventions on demand. Model introspection uses Prism AST parsing - every result carries a `[VERIFIED]` or `[INFERRED]` confidence tag so AI knows what's ground truth and what needs runtime checking.
71
+ Now your AI doesn't guess - it **asks your app directly.** 45 tools and 5 resource templates that query your schema, models, routes, controllers, views, and conventions on demand. Model introspection uses Prism AST parsing - every result carries a `[VERIFIED]` or `[INFERRED]` confidence tag so AI knows what's ground truth and what needs runtime checking.
72
72
 
73
73
  <br>
74
74
 
@@ -163,7 +163,7 @@ Native Rails controller transport. No separate process needed.
163
163
 
164
164
  ### CLI
165
165
 
166
- Same 39 tools, no server needed. Works in any terminal, any AI tool.
166
+ Same 45 tools, no server needed. Works in any terminal, any AI tool.
167
167
 
168
168
  ```bash
169
169
  rails 'ai:tool[search_code]' pattern="publishable?" match_type=trace
@@ -245,7 +245,7 @@ rails 'ai:tool[stimulus]' controller=chart
245
245
 
246
246
  <br>
247
247
 
248
- ## 39 Tools
248
+ ## 45 Tools
249
249
 
250
250
  Every tool is **read-only** and returns data verified against your actual app - not guesses, not training data.
251
251
 
@@ -331,6 +331,12 @@ Every tool is **read-only** and returns data verified against your actual app -
331
331
  | `get_service_pattern` | Interface, dependencies, side effects, callers |
332
332
  | `get_job_pattern` | Queue, retries, guard clauses, broadcasts, schedules |
333
333
  | `get_component_catalog` | ViewComponent/Phlex: props, slots, previews, sidecar assets |
334
+ | `get_i18n` | Locales, translation files with key counts, per-locale coverage, fallbacks |
335
+ | `get_mailers` | Mailer classes with delivery actions and delivery method |
336
+ | `get_engines` | Mounted engines (with known-engine descriptions) + loaded engine classes |
337
+ | `get_autoload` | Zeitwerk mode, autoload/eager-load paths, collapsed dirs, custom inflections |
338
+ | `get_active_support` | Concerns registry, deprecators, MessageVerifier usage, on_load hooks, cache store |
339
+ | `get_env_config` | Per-environment config: notable toggles + every config key each env sets |
334
340
 
335
341
  </details>
336
342
 
@@ -351,7 +357,7 @@ Every tool is **read-only** and returns data verified against your actual app -
351
357
 
352
358
  </details>
353
359
 
354
- > **[All 39 tools with parameters →](docs/TOOLS.md)** &nbsp;|&nbsp; **[Real-world recipes →](docs/RECIPES.md)**
360
+ > **[All 45 tools with parameters →](docs/TOOLS.md)** &nbsp;|&nbsp; **[Real-world recipes →](docs/RECIPES.md)**
355
361
 
356
362
  <br>
357
363
 
@@ -397,12 +403,12 @@ Enabled by default. Disable with `config.anti_hallucination_rules = false` if yo
397
403
 
398
404
  ```mermaid
399
405
  graph TD
400
- A["Your Rails App\nmodels + schema + routes + controllers + views + jobs"] -->|"39 introspectors"| B
406
+ A["Your Rails App\nmodels + schema + routes + controllers + views + jobs"] -->|"40 introspectors"| B
401
407
 
402
408
  B["rails-ai-context\nPrism AST parsing · Cached · Confidence-tagged\nVFS: rails-ai-context:// URIs introspected fresh"]
403
409
 
404
- B --> C["MCP Server\nstdio / HTTP\n39 tools · 5 templates"]
405
- B --> D["CLI Tools\nRake / Thor\nSame 39 tools"]
410
+ B --> C["MCP Server\nstdio / HTTP\n45 tools · 5 templates"]
411
+ B --> D["CLI Tools\nRake / Thor\nSame 45 tools"]
406
412
  B --> E["Static Files\nCLAUDE.md · .cursor/rules/ · .cursorrules\n.github/instructions/"]
407
413
 
408
414
  style A fill:#4a9eff,stroke:#2d7ad4,color:#fff
@@ -440,7 +446,7 @@ Both paths ask which AI tools you use (Claude Code, Cursor, GitHub Copilot, Open
440
446
  | In-Gemfile | Standalone | What it does |
441
447
  |:-----------|:-----------|:------------|
442
448
  | `rails ai:context` | `rails-ai-context context` | Generate context files |
443
- | `rails 'ai:tool[NAME]'` | `rails-ai-context tool NAME` | Run any of the 39 tools |
449
+ | `rails 'ai:tool[NAME]'` | `rails-ai-context tool NAME` | Run any of the 45 tools |
444
450
  | `rails ai:tool` | `rails-ai-context tool --list` | List all available tools |
445
451
  | `rails ai:serve` | `rails-ai-context serve` | Start MCP server (stdio) |
446
452
  | `rails ai:doctor` | `rails-ai-context doctor` | Diagnostics + AI readiness score |
@@ -491,13 +497,13 @@ empty listings.
491
497
  | | |
492
498
  |:------|:------------|
493
499
  | **[Quickstart](docs/QUICKSTART.md)** | 5-minute getting started |
494
- | **[Tools Reference](docs/TOOLS.md)** | All 39 tools with every parameter |
500
+ | **[Tools Reference](docs/TOOLS.md)** | All 45 tools with every parameter |
495
501
  | **[Recipes](docs/RECIPES.md)** | Real-world workflows and examples |
496
502
  | **[Custom Tools](docs/CUSTOM_TOOLS.md)** | Build and test your own MCP tools |
497
503
  | **[Configuration](docs/CONFIGURATION.md)** | 40+ config options with defaults |
498
504
  | **[AI Tool Setup](docs/SETUP.md)** | Claude, Cursor, Copilot, OpenCode, Codex |
499
505
  | **[Architecture](docs/ARCHITECTURE.md)** | System design and internals |
500
- | **[Introspectors](docs/INTROSPECTORS.md)** | All 39 introspectors and AST engine |
506
+ | **[Introspectors](docs/INTROSPECTORS.md)** | All 40 introspectors and AST engine |
501
507
  | **[Security](docs/SECURITY.md)** | 4-layer SQL safety and file blocking |
502
508
  | **[CLI Reference](docs/CLI.md)** | Commands and argument syntax |
503
509
  | **[Standalone](docs/STANDALONE.md)** | Use without Gemfile entry |
@@ -509,7 +515,7 @@ empty listings.
509
515
 
510
516
  ## Build your own tools
511
517
 
512
- Register custom MCP tools alongside the 39 built-in ones:
518
+ Register custom MCP tools alongside the 45 built-in ones:
513
519
 
514
520
  ```ruby
515
521
  # app/mcp_tools/rails_get_business_metrics.rb
@@ -547,7 +553,7 @@ if defined?(RailsAiContext)
547
553
  RailsAiContext.configure do |config|
548
554
  config.ai_tools = %i[claude cursor] # Which AI tools to generate for
549
555
  config.tool_mode = :mcp # :mcp (default) or :cli
550
- config.preset = :full # :full (39 introspectors) or :standard (17)
556
+ config.preset = :full # :full (40 introspectors) or :standard (17)
551
557
  end
552
558
  end
553
559
  ```
@@ -583,7 +589,7 @@ end
583
589
  ## About
584
590
 
585
591
  Built by a Rails developer with 10+ years of production experience.<br>
586
- 2154 tests + 100-example e2e harness. 39 tools. 5 resource templates. 39 introspectors. Standalone or in-Gemfile.<br>
592
+ 2727 tests + 158-example e2e harness. 45 tools. 5 resource templates. 40 introspectors. Standalone or in-Gemfile.<br>
587
593
  MIT licensed. [Contributions welcome.](CONTRIBUTING.md)
588
594
 
589
595
  <br>
data/SECURITY.md CHANGED
@@ -39,7 +39,7 @@ If you discover a security vulnerability in rails-ai-context, please report it r
39
39
 
40
40
  ## Security Design
41
41
 
42
- - All 38 MCP tools are **read-only** and never modify your application or database.
42
+ - All 45 MCP tools are **read-only** and never modify your application or database.
43
43
  - **Sensitive file blocking** - configurable `sensitive_patterns` blocks access to `.env`, `*.key`, `*.pem`, `credentials.yml.enc` across all search and read tools. Patterns are checked in `rails_search_code`, `rails_get_edit_context`, and all new tools.
44
44
  - **Path traversal protection** - all file-reading tools validate paths with `File.realpath()` against `Rails.root` to prevent directory escape.
45
45
  - **Command injection prevention** - code search uses `Open3.capture2` with array arguments (never shell strings). The `--` flag separator prevents pattern injection.
@@ -29,15 +29,7 @@ module RailsAiContext
29
29
  def handle
30
30
  status_code, rack_headers, body = self.class.mcp_transport.handle_request(request)
31
31
  self.status = status_code
32
- rack_headers.each do |k, v|
33
- # mcp >= 1.0 returns Rack 3-style lowercase header keys. Rails 7.0's
34
- # response header hash is case-sensitive (Rack 2), so a lowercase
35
- # "content-type" never registers and Rails falls back to text/html on
36
- # an otherwise valid JSON body. Write it with the canonical case.
37
- key = k.casecmp("content-type").zero? ? "Content-Type" : k
38
- response.headers[key] = v
39
- end
40
-
32
+ apply_transport_headers(rack_headers)
41
33
  if body.respond_to?(:each)
42
34
  # Plain enumerable body (initialize, errors, JSON mode): join to a
43
35
  # string so Content-Length/ETag semantics stay conventional.
@@ -75,16 +67,66 @@ module RailsAiContext
75
67
  else
76
68
  self.response_body = body
77
69
  end
70
+ rescue => e
71
+ # Once the response is committed the status and headers are already on
72
+ # the wire, so a JSON-RPC frame written here cannot reach the client.
73
+ # Worse, assigning a body swaps the stream out from under the thread
74
+ # still draining the old one, turning a truncated SSE stream into a
75
+ # garbled one. The streaming branch's ensure always closes the stream,
76
+ # and closing commits, so every streaming failure lands here committed.
77
+ # Hand those to Live, which logs them with a backtrace and tears the
78
+ # connection down.
79
+ raise if response.committed?
80
+
81
+ # Mirror Middleware#json_rpc_error_response: a transport failure must
82
+ # still answer in JSON-RPC shape. Without this the exception escapes
83
+ # into a generic Rails 500 (HTML), breaking the client's JSON-RPC loop.
84
+ RailsAiContext.log_warn "[rails-ai-context] MCP request failed: #{e.class}: #{e.message}"
85
+ self.status = 500
86
+ response.headers["Content-Type"] = "application/json"
87
+ self.response_body = {
88
+ jsonrpc: "2.0",
89
+ error: { code: -32603, message: "Internal error: #{e.message}" },
90
+ id: nil
91
+ }.to_json
78
92
  end
79
93
 
80
94
  private
81
95
 
96
+ # Rack 3 transports name their headers in lowercase, and the MCP SDK
97
+ # switched to that in 1.0. Rails 7.0 keeps response headers in a
98
+ # case-sensitive Hash, so a lowercase "content-type" is invisible to the
99
+ # canonical lookup Rails makes when it commits, and it labels the response
100
+ # with its own text/html default - a correct JSON-RPC body under a type no
101
+ # client will parse. Rails 7.1 moved to case-insensitive Rack::Headers and
102
+ # does not have the problem. Only the type needs the canonical spelling,
103
+ # because Rails is the only reader that looks a header up by name; the
104
+ # value is passed through so nothing gains a charset it did not have.
105
+ def apply_transport_headers(rack_headers)
106
+ rack_headers.each do |name, value|
107
+ key = name.to_s.casecmp?("content-type") ? "Content-Type" : name
108
+ response.headers[key] = value
109
+ end
110
+ end
111
+
82
112
  def wait_for_stream_close
83
- sleep 0.5 until response.stream.closed?
113
+ sleep 0.5 until stream_finished?
84
114
  rescue IOError
85
115
  nil
86
116
  end
87
117
 
118
+ # A client hangup aborts the buffer instead of closing it, so `closed?`
119
+ # alone leaves this thread parked until the transport's next keepalive
120
+ # write notices the hangup - up to the keepalive interval per dropped
121
+ # client. Live's buffer reports the hangup through `connected?`; the plain
122
+ # buffer used off the streaming path does not define it, so ask first.
123
+ def stream_finished?
124
+ stream = response.stream
125
+ return true if stream.closed?
126
+
127
+ stream.respond_to?(:connected?) && !stream.connected?
128
+ end
129
+
88
130
  class << self
89
131
  # Class-level memoization - transport persists across requests.
90
132
  # Thread-safe: MCP::Server and transport are stateless for reads.
data/docs/ARCHITECTURE.md CHANGED
@@ -18,19 +18,19 @@ graph TD
18
18
  A["models + schema + routes + controllers + views + jobs + config"]
19
19
  end
20
20
 
21
- A -->|"39 introspectors"| gem
21
+ A -->|"40 introspectors"| gem
22
22
 
23
23
  subgraph gem["rails-ai-context"]
24
24
  direction TB
25
25
 
26
26
  subgraph engine["Introspection Engine"]
27
27
  direction LR
28
- I["Introspectors\n39 modules\nPresets\nCached"]
28
+ I["Introspectors\n40 modules\nPresets\nCached"]
29
29
  AST["AST Engine\nPrism\n24 listeners\nConfidence tags"]
30
30
  H["Hydration Layer\nSchema hints\ninjected into\ntool responses"]
31
31
  end
32
32
 
33
- engine --> R["Tool Registry\n39 tools auto-discovered via inherited\n+ custom_tools - skip_tools = active tools"]
33
+ engine --> R["Tool Registry\n45 tools auto-discovered via inherited\n+ custom_tools - skip_tools = active tools"]
34
34
  end
35
35
 
36
36
  R --> MCP
@@ -40,7 +40,7 @@ graph TD
40
40
  subgraph outputs["Output"]
41
41
  direction LR
42
42
  MCP["MCP Server\nstdio / HTTP\nResources\nVFS URIs"]
43
- CLI["CLI Runner\nRake / Thor\nSame 39 tools\nNo server needed"]
43
+ CLI["CLI Runner\nRake / Thor\nSame 45 tools\nNo server needed"]
44
44
  S["Serializers\n14 modules\nStatic files\nPer-AI-tool"]
45
45
  end
46
46
 
@@ -73,7 +73,7 @@ sequenceDiagram
73
73
  alt cache hit (TTL + fingerprint valid)
74
74
  Cache-->>TR: cached context
75
75
  else cache miss
76
- Cache->>App: introspect (39 modules)
76
+ Cache->>App: introspect (40 modules)
77
77
  App-->>Cache: structured data
78
78
  Cache-->>TR: fresh context
79
79
  end
@@ -101,7 +101,7 @@ flowchart LR
101
101
 
102
102
  ### Introspectors (`lib/rails_ai_context/introspectors/`)
103
103
 
104
- 39 modules that extract structured data from your Rails app. Each introspector:
104
+ 40 modules that extract structured data from your Rails app. Each introspector:
105
105
 
106
106
  - Returns a Hash (never raises - wraps errors in `{ error: msg }`)
107
107
  - Is registered in `INTROSPECTOR_MAP` with a symbol key
@@ -192,7 +192,7 @@ Thor-based CLI that works standalone (no Gemfile entry):
192
192
 
193
193
  - `ToolRunner` - Parses CLI args, resolves tool names, executes tools, formats output
194
194
  - Supports `--json` mode for machine-readable output
195
- - Same 39 tools available as MCP and CLI
195
+ - Same 45 tools available as MCP and CLI
196
196
 
197
197
  ### Caching
198
198
 
@@ -215,7 +215,7 @@ SHA256-based change detection:
215
215
  ## Key design decisions
216
216
 
217
217
  1. **Official MCP SDK** - Not a custom protocol. Uses `mcp` gem's `MCP::Tool`, `MCP::Server`, transports.
218
- 2. **Read-only tools** - All 39 tools annotated as non-destructive. Defense-in-depth for query tool.
218
+ 2. **Read-only tools** - All 45 tools annotated as non-destructive. Defense-in-depth for query tool.
219
219
  3. **Graceful degradation** - Works without database (parses schema.rb as text), without Brakeman, without ripgrep, without listen gem.
220
220
  4. **Zeitwerk autoloading** - Files loaded on-demand. No `require_relative` in the gem.
221
221
  5. **Diff-aware generation** - Context file regeneration skips unchanged files using fingerprinting.
data/docs/CLI.md CHANGED
@@ -17,7 +17,7 @@
17
17
  | In-Gemfile (Rake) | `rails ai:` | `rails 'ai:tool[schema]' table=users` |
18
18
  | Standalone (Thor) | `rails-ai-context` | `rails-ai-context tool schema --table users` |
19
19
 
20
- Both provide the same 39 tools and functionality.
20
+ Both provide the same 45 tools and functionality.
21
21
 
22
22
  ---
23
23
 
@@ -40,7 +40,7 @@ rails-ai-context serve --transport http --port 6029 # HTTP transport
40
40
 
41
41
  ### `tool`
42
42
 
43
- Run any of the 39 MCP tools from the terminal.
43
+ Run any of the 45 MCP tools from the terminal.
44
44
 
45
45
  ```bash
46
46
  # Rake syntax
@@ -80,6 +80,7 @@ that define a `static_call` path can answer without a booted app:
80
80
  | `routes` | `config/routes.rb` parsed with a dedicated Prism listener |
81
81
  | `models` | `app/models/**/*.rb` (plus packs/engines/extra paths) parsed, not constantized |
82
82
  | `controllers` | `app/controllers/**/*.rb` (plus packs/engines/extra paths) parsed, not constantized |
83
+ | `env_config` | `config/environments/*.rb` read from disk - file-based, so the static tier serves the same data as a booted app |
83
84
 
84
85
  The other 34 introspectors (views, jobs, gems, turbo, i18n, active_storage,
85
86
  auth, api, and the rest) have no static path and report `{ unavailable: reason
@@ -167,11 +168,11 @@ Proof sources:
167
168
  prints to stdout, writes via the `STDOUT` constant, hangs past the
168
169
  timeout) plus `spec/e2e/static_tier_spec.rb` ("broken-boot app over the
169
170
  CLI", "broken-boot app over MCP stdio", "syntax error in one model file").
170
- 8. No introspector outside the five in the operating-tiers table defines
171
+ 8. No introspector outside the six in the operating-tiers table defines
171
172
  `static_call` (`lib/rails_ai_context/introspectors/view_introspector.rb`
172
173
  has none); `Introspector#run_introspector` reports `{ unavailable: reason
173
174
  }` for every such section regardless of shape.
174
- 9. `spec/e2e/empty_app_spec.rb` - all 39 built-in tools swept against an app
175
+ 9. `spec/e2e/empty_app_spec.rb` - all 45 built-in tools swept against an app
175
176
  with no scaffold, no models, no controllers beyond
176
177
  `ApplicationController`, no routes beyond root.
177
178
  10. `spec/e2e/massive_app_spec.rb` - `schema`, `model_details`, and `routes`
@@ -56,7 +56,7 @@ preset: full
56
56
 
57
57
  | Option | Type | Default | Description |
58
58
  |:-------|:-----|:--------|:------------|
59
- | `preset` | Symbol | `:full` | `:full` (39 introspectors) or `:standard` (17 introspectors) |
59
+ | `preset` | Symbol | `:full` | `:full` (40 introspectors) or `:standard` (17 introspectors) |
60
60
  | `context_mode` | Symbol | `:compact` | `:compact` (context files capped at ~150 lines) or `:full` (no line cap) |
61
61
  | `introspectors` | Array of symbols | (from preset) | Override the introspector list directly |
62
62
  | `generate_root_files` | Boolean | `true` | Set `false` to generate split rules only, no root CLAUDE.md/AGENTS.md |
@@ -69,7 +69,7 @@ preset: full
69
69
  |:-------|:-----|:--------|:-----------|:------------|
70
70
  | `server_name` | String | `"rails-ai-context"` | - | MCP server name |
71
71
  | `cache_ttl` | Integer | `60` | Must be positive | Cache time-to-live in seconds |
72
- | `max_tool_response_chars` | Integer | `200_000` | Must be positive | Safety cap for tool response length |
72
+ | `max_tool_response_chars` | Integer | `200_000` | Must be positive | Safety cap for tool responses and MCP resource payloads. An over-cap resource keeps its JSON shape: whole elements are dropped and reported under a `_truncated` key |
73
73
  | `live_reload` | Symbol/Boolean | `:auto` | - | `:auto` (uses `listen` gem if available), `true`, or `false` |
74
74
  | `live_reload_debounce` | Float | `1.5` | - | Seconds to wait before processing file changes |
75
75
  | `auto_mount` | Boolean | `false` | - | Auto-mount Rack middleware for HTTP transport |
@@ -166,7 +166,7 @@ preset: full
166
166
 
167
167
  ## Presets
168
168
 
169
- ### `:full` (default) - 39 introspectors
169
+ ### `:full` (default) - 40 introspectors
170
170
 
171
171
  All available introspectors. Maximum context.
172
172
 
data/docs/CUSTOM_TOOLS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Custom Tools
4
4
 
5
- **Build your own MCP tools that run alongside the 39 built-in ones.**
5
+ **Build your own MCP tools that run alongside the 45 built-in ones.**
6
6
 
7
7
  [Tools Reference](TOOLS.md) · [Configuration](CONFIGURATION.md) · [Architecture](ARCHITECTURE.md) · [FAQ](FAQ.md)
8
8
 
@@ -11,7 +11,7 @@
11
11
  ---
12
12
 
13
13
  > [!NOTE]
14
- > Custom tools have full access to your Rails environment - ActiveRecord, services, mailers, everything. They appear alongside the 39 built-in tools in both MCP and CLI.
14
+ > Custom tools have full access to your Rails environment - ActiveRecord, services, mailers, everything. They appear alongside the 45 built-in tools in both MCP and CLI.
15
15
 
16
16
  ## Creating a custom tool
17
17
 
data/docs/FAQ.md CHANGED
@@ -25,7 +25,7 @@ Claude Code, Cursor, GitHub Copilot, OpenCode, and Codex CLI. Each gets tailored
25
25
  No. The gem works three ways:
26
26
  1. **MCP server** - AI calls tools via the protocol (best experience)
27
27
  2. **Static files** - Generated context files (CLAUDE.md, .cursor/rules/, etc.)
28
- 3. **CLI** - Same 39 tools from the terminal, no server needed
28
+ 3. **CLI** - Same 45 tools from the terminal, no server needed
29
29
 
30
30
  ### Is this safe for production?
31
31
 
@@ -47,7 +47,7 @@ Yes. The gem gracefully degrades - it parses `db/schema.rb` as text when no data
47
47
 
48
48
  ### Can I switch between Gemfile and standalone?
49
49
 
50
- Yes, freely. Both generate identical context files and provide the same 39 tools. Just re-run the install/init to update MCP config files.
50
+ Yes, freely. Both generate identical context files and provide the same 45 tools. Just re-run the install/init to update MCP config files.
51
51
 
52
52
  ### Do I need to commit the generated files?
53
53
 
@@ -69,7 +69,7 @@ Start with `rails_onboard` for an app overview, `rails_analyze_feature` for feat
69
69
 
70
70
  ### Can I add my own tools?
71
71
 
72
- Yes. See [Custom Tools](CUSTOM_TOOLS.md). Create an `MCP::Tool` subclass, register it via `config.custom_tools`, and it appears alongside the 39 built-in tools.
72
+ Yes. See [Custom Tools](CUSTOM_TOOLS.md). Create an `MCP::Tool` subclass, register it via `config.custom_tools`, and it appears alongside the 45 built-in tools.
73
73
 
74
74
  ### Can I remove built-in tools?
75
75
 
@@ -99,7 +99,7 @@ PostgreSQL, MySQL, and SQLite. Each gets database-specific safety mechanisms (re
99
99
 
100
100
  ### What's the difference between `:full` and `:standard` preset?
101
101
 
102
- - **`:full`** (default) - 39 introspectors. Full context for every aspect of your app.
102
+ - **`:full`** (default) - 40 introspectors. Full context for every aspect of your app.
103
103
  - **`:standard`** - 17 introspectors. Faster, covers the essentials (schema, models, routes, controllers, tests, etc.).
104
104
 
105
105
  ### What's `:compact` vs `:full` context mode?