rails-ai-context 5.32.2 → 5.34.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.
Files changed (203) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1491 -0
  3. data/CONTEXT.md +27 -3
  4. data/README.md +16 -12
  5. data/Rakefile +6 -0
  6. data/app/controllers/rails_ai_context/mcp_controller.rb +112 -84
  7. data/config/routes.rb +2 -3
  8. data/docs/ARCHITECTURE.md +7 -5
  9. data/docs/CLI.md +79 -6
  10. data/docs/COMPATIBILITY.md +75 -1
  11. data/docs/CONFIGURATION.md +30 -11
  12. data/docs/CUSTOM_TOOLS.md +2 -1
  13. data/docs/FAQ.md +16 -6
  14. data/docs/GUIDE.md +117 -71
  15. data/docs/INTROSPECTORS.md +10 -8
  16. data/docs/QUICKSTART.md +3 -3
  17. data/docs/RECIPES.md +1 -1
  18. data/docs/SECURITY.md +109 -25
  19. data/docs/SETUP.md +30 -6
  20. data/docs/STANDALONE.md +10 -3
  21. data/docs/TOOLS.md +152 -34
  22. data/docs/TROUBLESHOOTING.md +90 -12
  23. data/docs/adr/0003-shared-tool-cache-semantics.md +8 -0
  24. data/docs/adr/0005-workspace-mcp-entries.md +39 -0
  25. data/exe/rails-ai-context +733 -104
  26. data/lib/generators/rails_ai_context/install/install_generator.rb +341 -125
  27. data/lib/rails_ai_context/app_kind.rb +18 -5
  28. data/lib/rails_ai_context/ast_cache.rb +7 -0
  29. data/lib/rails_ai_context/base_mixins.rb +8 -8
  30. data/lib/rails_ai_context/boot_manager.rb +22 -3
  31. data/lib/rails_ai_context/change_watch.rb +14 -11
  32. data/lib/rails_ai_context/cli/app_root.rb +401 -0
  33. data/lib/rails_ai_context/cli/entry_boot.rb +58 -22
  34. data/lib/rails_ai_context/cli/tool_runner.rb +47 -21
  35. data/lib/rails_ai_context/code_reloader.rb +115 -1
  36. data/lib/rails_ai_context/concern_membership.rb +3 -1
  37. data/lib/rails_ai_context/concern_paths.rb +8 -3
  38. data/lib/rails_ai_context/configuration.rb +41 -9
  39. data/lib/rails_ai_context/context_file_report.rb +19 -2
  40. data/lib/rails_ai_context/database_file.rb +26 -0
  41. data/lib/rails_ai_context/doctor.rb +1122 -184
  42. data/lib/rails_ai_context/engine.rb +18 -1
  43. data/lib/rails_ai_context/erb_source.rb +19 -0
  44. data/lib/rails_ai_context/executable.rb +21 -0
  45. data/lib/rails_ai_context/facts_formatter.rb +5 -2
  46. data/lib/rails_ai_context/file_walk.rb +61 -10
  47. data/lib/rails_ai_context/fingerprinter/snapshot.rb +234 -0
  48. data/lib/rails_ai_context/fingerprinter.rb +40 -12
  49. data/lib/rails_ai_context/gem_lock.rb +186 -27
  50. data/lib/rails_ai_context/git_ignore.rb +22 -0
  51. data/lib/rails_ai_context/install/ai_tool.rb +31 -5
  52. data/lib/rails_ai_context/install/cleanup.rb +21 -0
  53. data/lib/rails_ai_context/install/program.rb +209 -35
  54. data/lib/rails_ai_context/install/selection_record.rb +258 -97
  55. data/lib/rails_ai_context/install/surface.rb +6 -1
  56. data/lib/rails_ai_context/install/validation_hook.rb +282 -0
  57. data/lib/rails_ai_context/install/workspace.rb +166 -0
  58. data/lib/rails_ai_context/install_mode.rb +71 -13
  59. data/lib/rails_ai_context/introspectors/action_mailbox_introspector.rb +1 -1
  60. data/lib/rails_ai_context/introspectors/action_presence.rb +11 -6
  61. data/lib/rails_ai_context/introspectors/action_source.rb +182 -0
  62. data/lib/rails_ai_context/introspectors/action_text_introspector.rb +1 -2
  63. data/lib/rails_ai_context/introspectors/active_record_settings.rb +1 -1
  64. data/lib/rails_ai_context/introspectors/active_storage_introspector.rb +1 -3
  65. data/lib/rails_ai_context/introspectors/active_support_introspector.rb +3 -3
  66. data/lib/rails_ai_context/introspectors/admin_resources.rb +1 -1
  67. data/lib/rails_ai_context/introspectors/anyway_configs.rb +1 -1
  68. data/lib/rails_ai_context/introspectors/api_introspector.rb +3 -3
  69. data/lib/rails_ai_context/introspectors/auth_introspector.rb +15 -10
  70. data/lib/rails_ai_context/introspectors/call_site_expansion.rb +24 -1
  71. data/lib/rails_ai_context/introspectors/component_introspector.rb +3 -2
  72. data/lib/rails_ai_context/introspectors/controller_introspector.rb +387 -60
  73. data/lib/rails_ai_context/introspectors/convention_introspector.rb +1 -1
  74. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +15 -3
  75. data/lib/rails_ai_context/introspectors/devops_introspector.rb +3 -1
  76. data/lib/rails_ai_context/introspectors/eager_load.rb +201 -9
  77. data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
  78. data/lib/rails_ai_context/introspectors/env_config_introspector.rb +1 -1
  79. data/lib/rails_ai_context/introspectors/env_references.rb +10 -1
  80. data/lib/rails_ai_context/introspectors/frontend_framework_introspector.rb +1 -1
  81. data/lib/rails_ai_context/introspectors/gem_introspector.rb +23 -10
  82. data/lib/rails_ai_context/introspectors/grape_endpoints.rb +1 -1
  83. data/lib/rails_ai_context/introspectors/habtm_join_tables.rb +1 -1
  84. data/lib/rails_ai_context/introspectors/helper_names.rb +1 -1
  85. data/lib/rails_ai_context/introspectors/i18n_introspector.rb +3 -3
  86. data/lib/rails_ai_context/introspectors/job_introspector.rb +8 -6
  87. data/lib/rails_ai_context/introspectors/listeners/base_listener.rb +12 -8
  88. data/lib/rails_ai_context/introspectors/listeners/callbacks_listener.rb +78 -4
  89. data/lib/rails_ai_context/introspectors/listeners/config_assignment_listener.rb +1 -0
  90. data/lib/rails_ai_context/introspectors/listeners/env_access_listener.rb +10 -1
  91. data/lib/rails_ai_context/introspectors/listeners/foreign_evals_listener.rb +61 -0
  92. data/lib/rails_ai_context/introspectors/listeners/macros_listener.rb +13 -4
  93. data/lib/rails_ai_context/introspectors/listeners/migration_dsl_listener.rb +2 -1
  94. data/lib/rails_ai_context/introspectors/listeners/migration_replay_listener.rb +3 -2
  95. data/lib/rails_ai_context/introspectors/listeners/routes_dsl_listener.rb +29 -8
  96. data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +2 -2
  97. data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +88 -12
  98. data/lib/rails_ai_context/introspectors/middleware_introspector.rb +2 -2
  99. data/lib/rails_ai_context/introspectors/migration_introspector.rb +27 -0
  100. data/lib/rails_ai_context/introspectors/migration_replay/helpers.rb +1 -1
  101. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +83 -27
  102. data/lib/rails_ai_context/introspectors/migration_replay.rb +15 -14
  103. data/lib/rails_ai_context/introspectors/model_introspector.rb +231 -23
  104. data/lib/rails_ai_context/introspectors/n_plus_one_scan.rb +620 -0
  105. data/lib/rails_ai_context/introspectors/performance_introspector.rb +11 -174
  106. data/lib/rails_ai_context/introspectors/route_introspector.rb +10 -1
  107. data/lib/rails_ai_context/introspectors/schema_conventions.rb +23 -3
  108. data/lib/rails_ai_context/introspectors/schema_introspector.rb +66 -16
  109. data/lib/rails_ai_context/introspectors/schema_reader.rb +4 -3
  110. data/lib/rails_ai_context/introspectors/seeds_introspector.rb +1 -1
  111. data/lib/rails_ai_context/introspectors/source_scan.rb +39 -9
  112. data/lib/rails_ai_context/introspectors/stimulus_introspector.rb +15 -6
  113. data/lib/rails_ai_context/introspectors/test_introspector.rb +1 -1
  114. data/lib/rails_ai_context/introspectors/turbo_introspector.rb +131 -12
  115. data/lib/rails_ai_context/introspectors/view_introspector.rb +4 -4
  116. data/lib/rails_ai_context/introspectors/view_template_introspector.rb +15 -1
  117. data/lib/rails_ai_context/legacy_cleanup.rb +17 -4
  118. data/lib/rails_ai_context/live_reload.rb +20 -20
  119. data/lib/rails_ai_context/mcp_config_generator.rb +916 -109
  120. data/lib/rails_ai_context/mcp_edge.rb +182 -32
  121. data/lib/rails_ai_context/middleware.rb +5 -1
  122. data/lib/rails_ai_context/migration_status.rb +88 -25
  123. data/lib/rails_ai_context/output_guard.rb +48 -2
  124. data/lib/rails_ai_context/package_json.rb +1 -1
  125. data/lib/rails_ai_context/path_resolver.rb +46 -13
  126. data/lib/rails_ai_context/payload.rb +92 -8
  127. data/lib/rails_ai_context/pending_migrations.rb +5 -1
  128. data/lib/rails_ai_context/portable_path.rb +3 -2
  129. data/lib/rails_ai_context/presets.rb +15 -11
  130. data/lib/rails_ai_context/redaction.rb +262 -51
  131. data/lib/rails_ai_context/resources.rb +55 -20
  132. data/lib/rails_ai_context/route_coverage.rb +8 -0
  133. data/lib/rails_ai_context/safe_file.rb +14 -1
  134. data/lib/rails_ai_context/safe_path.rb +101 -4
  135. data/lib/rails_ai_context/schema_adapter.rb +8 -0
  136. data/lib/rails_ai_context/schema_version.rb +9 -0
  137. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +88 -33
  138. data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +36 -34
  139. data/lib/rails_ai_context/serializers/context_file_serializer.rb +21 -29
  140. data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +8 -6
  141. data/lib/rails_ai_context/serializers/copilot_serializer.rb +2 -7
  142. data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +19 -16
  143. data/lib/rails_ai_context/serializers/guide_examples.rb +142 -0
  144. data/lib/rails_ai_context/serializers/markdown_serializer.rb +146 -30
  145. data/lib/rails_ai_context/serializers/opencode_rules_serializer.rb +38 -15
  146. data/lib/rails_ai_context/serializers/section_facts.rb +44 -1
  147. data/lib/rails_ai_context/serializers/section_marker_writer.rb +94 -12
  148. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +24 -12
  149. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +244 -129
  150. data/lib/rails_ai_context/server.rb +304 -71
  151. data/lib/rails_ai_context/tasks/rails_ai_context.rake +447 -372
  152. data/lib/rails_ai_context/tools/analyze_feature.rb +72 -18
  153. data/lib/rails_ai_context/tools/argument_schema.rb +49 -0
  154. data/lib/rails_ai_context/tools/base_tool.rb +367 -46
  155. data/lib/rails_ai_context/tools/brakeman_guard.rb +24 -0
  156. data/lib/rails_ai_context/tools/dependency_graph.rb +63 -28
  157. data/lib/rails_ai_context/tools/diagnose.rb +218 -11
  158. data/lib/rails_ai_context/tools/generate_test.rb +339 -39
  159. data/lib/rails_ai_context/tools/get_callbacks.rb +24 -13
  160. data/lib/rails_ai_context/tools/get_component_catalog.rb +1 -1
  161. data/lib/rails_ai_context/tools/get_concern.rb +91 -27
  162. data/lib/rails_ai_context/tools/get_config.rb +13 -4
  163. data/lib/rails_ai_context/tools/get_context.rb +71 -14
  164. data/lib/rails_ai_context/tools/get_controllers.rb +179 -44
  165. data/lib/rails_ai_context/tools/get_conventions.rb +28 -6
  166. data/lib/rails_ai_context/tools/get_edit_context.rb +27 -14
  167. data/lib/rails_ai_context/tools/get_env.rb +18 -9
  168. data/lib/rails_ai_context/tools/get_frontend_stack.rb +3 -1
  169. data/lib/rails_ai_context/tools/get_gems.rb +1 -1
  170. data/lib/rails_ai_context/tools/get_helper_methods.rb +10 -6
  171. data/lib/rails_ai_context/tools/get_i18n.rb +1 -1
  172. data/lib/rails_ai_context/tools/get_job_pattern.rb +3 -2
  173. data/lib/rails_ai_context/tools/get_mailers.rb +3 -3
  174. data/lib/rails_ai_context/tools/get_model_details.rb +30 -10
  175. data/lib/rails_ai_context/tools/get_partial_interface.rb +242 -23
  176. data/lib/rails_ai_context/tools/get_routes.rb +29 -8
  177. data/lib/rails_ai_context/tools/get_schema.rb +52 -19
  178. data/lib/rails_ai_context/tools/get_service_pattern.rb +24 -10
  179. data/lib/rails_ai_context/tools/get_stimulus.rb +37 -1
  180. data/lib/rails_ai_context/tools/get_test_info.rb +18 -4
  181. data/lib/rails_ai_context/tools/get_turbo_map.rb +221 -58
  182. data/lib/rails_ai_context/tools/get_view.rb +14 -14
  183. data/lib/rails_ai_context/tools/migration_advisor.rb +355 -48
  184. data/lib/rails_ai_context/tools/onboard.rb +52 -8
  185. data/lib/rails_ai_context/tools/performance_check.rb +5 -3
  186. data/lib/rails_ai_context/tools/query.rb +914 -86
  187. data/lib/rails_ai_context/tools/read_logs.rb +69 -15
  188. data/lib/rails_ai_context/tools/review_changes.rb +215 -53
  189. data/lib/rails_ai_context/tools/runtime_info.rb +51 -1
  190. data/lib/rails_ai_context/tools/safe_call.rb +35 -14
  191. data/lib/rails_ai_context/tools/search_code.rb +243 -67
  192. data/lib/rails_ai_context/tools/search_docs.rb +4 -4
  193. data/lib/rails_ai_context/tools/security_scan.rb +143 -33
  194. data/lib/rails_ai_context/tools/session_context.rb +39 -5
  195. data/lib/rails_ai_context/tools/validate.rb +259 -40
  196. data/lib/rails_ai_context/tools/validate_semantics.rb +130 -56
  197. data/lib/rails_ai_context/version.rb +1 -1
  198. data/lib/rails_ai_context/vfs.rb +67 -40
  199. data/lib/rails_ai_context/view_file.rb +41 -3
  200. data/lib/rails_ai_context/watcher.rb +51 -13
  201. data/lib/rails_ai_context.rb +21 -0
  202. data/server.json +1 -1
  203. metadata +14 -1
@@ -11,7 +11,7 @@
11
11
  ---
12
12
 
13
13
  > [!TIP]
14
- > Always start with `rails ai:doctor`. It runs 25 checks and catches most issues automatically.
14
+ > Always start with `rails ai:doctor`. It checks the app, its databases, the MCP configs and the context files, and catches most issues automatically.
15
15
 
16
16
  ## Diagnostics first
17
17
 
@@ -20,7 +20,7 @@ rails ai:doctor # In-Gemfile
20
20
  rails-ai-context doctor # Standalone
21
21
  ```
22
22
 
23
- This runs 25 checks and returns an AI readiness score (0-100). Each failed check includes a fix suggestion.
23
+ This runs every check that applies to the app ([the list](GUIDE.md#diagnostics)) and returns an AI readiness score (0-100). Each check that fails or warns includes a fix suggestion.
24
24
 
25
25
  ---
26
26
 
@@ -70,9 +70,10 @@ ls -la .mcp.json .cursor/ .vscode/ .github/
70
70
  - OpenCode: `opencode.json`
71
71
  - Codex: `.codex/config.toml`
72
72
 
73
- 2. Verify the command works:
73
+ 2. Verify the command the config holds works:
74
74
  ```bash
75
- bundle exec rails ai:serve
75
+ bundle exec rails-ai-context serve # in-Gemfile
76
+ rails-ai-context serve # standalone
76
77
  # Should output nothing (waiting for stdio input)
77
78
  # Ctrl+C to stop
78
79
  ```
@@ -82,18 +83,66 @@ ls -la .mcp.json .cursor/ .vscode/ .github/
82
83
  rails generate rails_ai_context:install
83
84
  ```
84
85
 
86
+ ### "Could not write .vscode/mcp.json - that tool will not auto-discover the MCP server"
87
+
88
+ The install never replaces a config it cannot write back faithfully: one that does not parse as JSON, one holding comments that writing it back would drop, or one that is not UTF-8. Trailing commas, which VS Code and OpenCode accept, are read the way they read them under json 2.9 or later; an older json refuses them, as JSON does. The line after the warning says which, and ends with the exact entry to add, as `Add {"servers":{...}} to it by hand`. Add that entry yourself, or remove the comments (and, under an older json, the trailing commas) and run the install again. `doctor` names the same entry for a file whose comments keep the install out, rather than sending you back to the install, and asks for one that is not UTF-8 to be saved as UTF-8 first.
89
+
90
+ Claude Code reads `.mcp.json` with a plain JSON parser, and loads no server from a file holding a comment or a trailing comma ("MCP config is not a valid JSON"); Cursor's `.cursor/mcp.json` is held to plain JSON too. Either one is reported as `Claude Code cannot read it: it holds a trailing comma, which JSON does not allow`, by the install and by `doctor`, however current the entry in it. Make the file plain JSON and run the install again. `doctor` reports one that does not parse, or holds no object to merge into, a workspace's above the app included. Dropping an AI tool leaves such a file alone the same way and names the entries to remove.
91
+
92
+ ### Codex: "TOML parse error ... duplicate key"
93
+
94
+ Codex refuses a `config.toml` that declares a server twice. An earlier install could write a second `[mcp_servers.rails-ai-context]` table beside the server spelled another way: a quoted name (`[mcp_servers."rails-ai-context"]`), an inline table under `[mcp_servers]`, or dotted keys. `doctor` names such a file. Two tables are merged into one when the install runs again; for an inline table or dotted keys, delete one of the two by hand.
95
+
96
+ The install now finds the server however its name is quoted and replaces its table where it stands. An entry of the gem's written as an inline table or with dotted keys is left as it is, with a warning, as is a file whose `mcp_servers` is one inline table: write it as a `[mcp_servers.rails-ai-context]` table, or delete it, and the next run keeps it current. `doctor` gives that same edit for any problem in such an entry that only a rewrite fixes, a stale env snapshot included.
97
+
98
+ ### Codex: "stream did not contain valid UTF-8"
99
+
100
+ Codex reads `config.toml` as TOML, which is UTF-8, and loads nothing from a file holding a byte that is not - one in a comment included. The install still writes its table into such a file, keeping every byte around it, and says `Codex CLI cannot read it: it is not UTF-8, which TOML is`; `doctor` fails it in the same words. Save the file as UTF-8.
101
+
85
102
  ### "MCP server fails to start"
86
103
 
104
+ `rails ai:doctor` (or `rails-ai-context doctor`) checks the command each config holds and says which one cannot start and why. The usual causes:
105
+
106
+ - `rails-ai-context is not currently included in the bundle`: the config runs `bundle exec rails-ai-context serve`, but the gem left the app's Gemfile. A standalone install runs `rails-ai-context serve`: run `rails-ai-context init` to rewrite the configs.
107
+ - The config runs `rails-ai-context serve` while the app's Gemfile.lock carries the gem: it needs a copy installed outside the bundle as well, which hands the run to the bundle's. Run `rails generate rails_ai_context:install` to write `bundle exec rails-ai-context serve`.
108
+ - `can't find executable rails-ai-context for gem rails-ai-context`: the bundle's copy of the gem lists no executable, as a `path:` copy of 5.32.2 or earlier outside a git checkout does. Point the Gemfile at a git checkout or a release.
109
+ - On Windows, a config that runs `bundle` or `rails-ai-context` itself rather than through `cmd /c`: those are batch files there, which Claude Code, Codex and the other clients that start a server without a shell do not run (`doctor` warns about it). Run the install again; on Windows it writes `cmd /c bundle exec rails-ai-context serve`.
110
+ - On macOS or Linux, a config that runs `cmd /c ...`: it was written on Windows, and there is no `cmd` here (`doctor` fails it). Run the install again on this machine, and keep the MCP configs out of git if the team works on both.
111
+
87
112
  Check for Bundler/Ruby path issues:
88
113
 
89
114
  ```bash
90
115
  which bundle
91
- which rails
116
+ which rails-ai-context
92
117
  ruby -v
93
118
  ```
94
119
 
95
120
  For Codex CLI specifically, the env section in `.codex/config.toml` must match your current Ruby environment. Re-run install if you changed Ruby versions.
96
121
 
122
+ ### Answers say "Boot did not finish within 20s"
123
+
124
+ The app's boot hung, usually an initializer waiting on a service that is
125
+ down (Redis, Elasticsearch, a secrets store). A stdio server gives the boot 20
126
+ seconds, because its client stops waiting for the first answer after about 30
127
+ (Claude Code, the MCP Inspector), and then serves the static tier; every other
128
+ command gives it 60. Start the service, or, for an app that is healthy but
129
+ slow to boot, raise both limits in the server's MCP entry: the boot's with
130
+ `RAILS_AI_CONTEXT_BOOT_TIMEOUT` (seconds) in its `env`, and the client's with
131
+ its own setting (Claude Code reads `MCP_TIMEOUT`, in milliseconds). Raising
132
+ only the boot's leaves a client that gives up first. `rails ai:serve` has no
133
+ static tier, so it exits with the same message instead.
134
+
135
+ ### A server set up for a folder of apps fails to start
136
+
137
+ `rails-ai-context init` run in a folder of apps writes entries that name each app relative to that folder (`--app-path shop`, and for an in-Gemfile app `BUNDLE_GEMFILE=shop/Gemfile`, or the monorepo's shared Gemfile its `config/boot.rb` names). Claude Code, Codex and OpenCode start a server in the folder they were launched in, so:
138
+
139
+ 1. Start the AI tool in the folder of apps, not inside one of them. Launched inside an app, the tool still reads the folder's config above it, and the server says `--app-path shop is read from ...; it names an app from ...`. An in-Gemfile entry fails earlier, in Bundler, with a Gemfile it cannot find. To work in one app on its own, run `rails-ai-context init` inside it too.
140
+ 2. Check the Ruby. Every server in the folder runs under the Ruby your version manager picks in the folder, and `init` warns about an app that declares another. `Your Ruby version is ..., but your Gemfile specified ...` from an in-Gemfile entry is that case: install the app's bundle for that Ruby, or give the app its own config with `init` inside it.
141
+
142
+ ### "No Rails app found in ..., and 2 below it"
143
+
144
+ You ran a command in a folder that holds several apps. The error lists the exact command for each, with `--app-path`. To set them all up at once, run `rails-ai-context init` in that folder.
145
+
97
146
  ### "Tools return empty results"
98
147
 
99
148
  1. Check that your Rails app boots: `rails runner "puts 'ok'"`
@@ -110,7 +159,8 @@ dependency graph, or launching from inside another Bundler project's
110
159
  directory. Fixes:
111
160
 
112
161
  1. Launch `rails-ai-context serve` with the app root as the working directory
113
- (the generated MCP configs do this by default).
162
+ (the generated MCP configs do this by default; a folder of apps' configs
163
+ start in that folder and name the app with `--app-path`).
114
164
  2. Make sure `GEM_PATH` covers the complete default gem path
115
165
  (`gem env path`), not just `gem env gemdir` - Ruby's bundled gems
116
166
  (racc, etc.) live in a separate directory.
@@ -120,10 +170,29 @@ directory. Fixes:
120
170
  ### MCP server responds slowly
121
171
 
122
172
  - Check `config.cache_ttl` - lower values mean more frequent re-introspection
173
+ - Without the `listen` gem, each call first checks the app's files for edits, about 100 ms at 10,000 files; adding `listen` to the development group replaces that check with live reload
123
174
  - Check `config.preset` - `:standard` is faster than `:full`
124
175
  - Large schema files (>10MB) slow down schema introspection
125
176
  - Run `rails ai:doctor` - it checks schema size and view count
126
177
 
178
+ ### The stdio server stops with "MCP stdio connection closed: stdio frame exceeds 4194304 bytes"
179
+
180
+ The MCP SDK reads one JSON-RPC message per line and refuses a line of 4 MiB or more. The server answers the client with a JSON-RPC `-32600` error, prints the reason on stderr and exits 1. The message is too large for any client to need; look for a tool argument the client filled with a whole file.
181
+
182
+ ### HTTP answers an HTML error page while a migration is pending
183
+
184
+ In development Rails answers every request with its pending-migration page, and the mounted engine is a route like any other. Run the migration, or use `auto_mount`, stdio or the standalone server, which all answer ahead of that check; `rails_runtime_info` lists the pending migrations.
185
+
186
+ ### HTTP answers 403 "Forbidden: Invalid Host header"
187
+
188
+ The standalone server (`serve --transport http`, `rails ai:serve_http`) answers only requests whose `Host` header is `127.0.0.1`, `::1` or `localhost`, as the MCP SDK does, to stop DNS rebinding. A client that reaches it by the machine's IP or hostname is refused, whatever `http_bind` says. Connect to `localhost`: through a published container port, or an SSH tunnel (`ssh -L 6029:localhost:6029 your-host`).
189
+
190
+ The mounted engine and `auto_mount` answer the hosts the app's `config.hosts` allows, as the rest of the app does. Add the name the client uses to `config.hosts`, the way you would for any other route.
191
+
192
+ ### HTTP answers 403 "does not serve MCP over HTTP in production"
193
+
194
+ The mounted engine and `auto_mount` refuse every request outside development and test (production, staging) unless the app sets `config.allow_http_in_production = true`. Set it only with the endpoint behind your app's authentication; see [Security](SECURITY.md#mcp-http-transport).
195
+
127
196
  ---
128
197
 
129
198
  ## Context file issues
@@ -151,7 +220,11 @@ rails ai:context
151
220
 
152
221
  ### "My custom content in CLAUDE.md was overwritten"
153
222
 
154
- The gem uses section markers (`<!-- BEGIN/END rails-ai-context -->`) to preserve user content. Content outside these markers is preserved. If markers were removed, the gem may overwrite.
223
+ The gem writes its section between `<!-- BEGIN rails-ai-context -->` and `<!-- END rails-ai-context -->` and replaces only what lies between them, so text above and below the markers survives every run; text inside them does not. A file with no markers at all gets the section added at the top, and its own text kept below.
224
+
225
+ The per-directory `app/models/AGENTS.md` and `app/controllers/AGENTS.md` that OpenCode and Codex read follow the same rule. One the gem generated whole carries `> Auto-generated by rails-ai-context. Do not edit manually.` and is rewritten whole; one you wrote yourself keeps its text, and the listing goes between the markers above it.
226
+
227
+ If one marker goes missing, or the file holds a second pair, nothing says where the gem's section ends. The run then leaves the file as it is and reports it as `unpaired rails-ai-context marker, left as it is`. Put the lost marker back where the gem's section ends, or delete the leftover section with its markers, and the next run updates the file again.
155
228
 
156
229
  ### "Generated files are too large"
157
230
 
@@ -218,6 +291,9 @@ brew install ripgrep
218
291
 
219
292
  # Ubuntu
220
293
  apt install ripgrep
294
+
295
+ # Windows
296
+ winget install BurntSushi.ripgrep.MSVC
221
297
  ```
222
298
 
223
299
  The gem falls back to Ruby regex if ripgrep isn't available. Search still works, just slower on large codebases.
@@ -238,7 +314,7 @@ Standalone mode boots the app with a small shim, then restores the `$LOAD_PATH`
238
314
 
239
315
  The file is read from the app root, once, at boot, and an initializer does not replace it: a `configure` block wins only the keys it assigns, key by key ([Precedence](CONFIGURATION.md#precedence)). In standalone mode the initializer contributes nothing at all, since the gem is not loaded while `config/initializers` runs. Check:
240
316
 
241
- 1. You are in the app root, or passed `--app-path`.
317
+ 1. You are inside the app, or passed `--app-path`. A command run from a subdirectory reads the config of the app root it walks up to, and names it on stderr.
242
318
  2. The key is one the gem knows. An unknown key warns on stderr and is ignored.
243
319
  3. The file is readable and parses. Corrupted YAML degrades gracefully with a warning.
244
320
 
@@ -262,10 +338,12 @@ Without it, the tool reports "not installed" but the gem works fine otherwise.
262
338
 
263
339
  The scan runs under the app's own bundle, so a brakeman installed globally is
264
340
  not on its load path. The tool falls back on its own: it runs the installed
265
- brakeman as a separate process outside the bundle and says so under the
266
- results. This message means that fallback produced no report either - the gem
267
- is there and the run failed. Run `brakeman` in the app directory to see what
268
- it hit, or add it to the Gemfile so the scan runs in-process:
341
+ gem's `bin/brakeman` with the app's Ruby, as a separate process outside the
342
+ bundle, and says so under the results. This message means that fallback
343
+ produced no report either - the gem is there and the run failed. `rails
344
+ ai:doctor` runs the same script's `--version` and names what it printed. Run
345
+ `brakeman` in the app directory to see what it hit, or add it to the Gemfile
346
+ so the scan runs in-process:
269
347
 
270
348
  ```bash
271
349
  bundle add brakeman --group development
@@ -8,10 +8,16 @@ Status: accepted
8
8
 
9
9
  **One client's session record was served to another.** `SESSION_CONTEXT[:queries]` was a single flat hash. Over stdio that is right - one process is one conversation - but the middleware, the engine controller and the standalone HTTP server each serve every client from one process, so `rails_session_context` listed calls the asking client never made. The record is now bucketed per conversation: all three HTTP entry points wrap each request in `BaseTool.with_session(<Mcp-Session-Id>)`, and stdio falls through to a single default bucket, unchanged.
10
10
 
11
+ **The scope ended before the tool ran.** The SDK answers a request inside a session with a streaming body, and the tool runs when the web server calls that body - after the entry point has returned, so the scope around `handle_request` was already gone and the middleware and the standalone server recorded every client in the default bucket again. `McpEdge.serve` hands the web server a body that re-enters the client's session. The engine controller calls the body itself, inside its own scope.
12
+
13
+ **One client's reset emptied every client's record.** `action: "reset"` cleared the whole hash. It clears the calling conversation's bucket now (`current_session_reset!`). `session_reset!` still clears them all, for a change that makes every earlier answer stale: live reload.
14
+
11
15
  **`session_queries` handed out live entries.** It returned `values.dup`, a shallow copy, so the entry hashes stayed live inside the record and kept being mutated by later calls - a caller's snapshot changed under it. It now copies each entry.
12
16
 
13
17
  **The record grew without bound, keyed by a client-controlled header.** Bucketing per conversation introduced a hash whose keys come from `Mcp-Session-Id`, in a process that stays up. Three things were wrong at once: the hash had a default block, so merely *reading* a session's history created a bucket; nothing capped the id's length; and nothing evicted. Reading no longer writes, ids are truncated to `MAX_SESSION_ID_LENGTH`, and the number of remembered conversations is capped at `MAX_SESSIONS`.
14
18
 
19
+ **A composite tool's inner calls were recorded as the client's.** `rails_get_context`, `rails_diagnose` and `rails_review_changes` answer by calling other tools, and every call recorded itself, so one `rails_get_context` read as four calls the client never made, and the first inner call took the outer call's params with it. SafeCall now marks the outermost call on the thread, and only that call is recorded, once, with its own params.
20
+
15
21
  Eviction is least-recently-used, not oldest-created. Recording re-inserts the session so hash order tracks use. Ordering by creation instead would evict the conversation that has run for hours ahead of a hundred idle newcomers - backwards, and worst on exactly the long-lived transports that made bucketing necessary.
16
22
 
17
23
  ## Intended semantics, not defects
@@ -21,3 +27,5 @@ Eviction is least-recently-used, not oldest-created. Recording re-inserts the se
21
27
  **`deep_dup` also happens under the mutex.** Every caller gets an independent copy, so a tool that mutates its context cannot corrupt the next tool's view. Copying inside the lock adds to hold time, and moving it outside would hand out the cached object itself between the read and the copy. Isolation is worth more than the contention; the copy is cheap next to the introspection it protects.
22
28
 
23
29
  Both are pinned by `spec/lib/rails_ai_context/tools/shared_cache_concurrency_spec.rb`, which also drives the middleware and the engine controller concurrently against both caches.
30
+
31
+ **Every server checks the files outside the mutex, at the start of each call.** Within the TTL the fast path trusts the cache, so something has to drop it on an edit before a call reads it. Live reload cannot: `listen` delivers a change after its debounce, and with no `listen` in the bundle nothing delivers it at all. So `refresh_if_files_changed!` checks the files itself (`Fingerprinter::Snapshot`, a stat of each watched directory and file), and on a change reloads the app's code and resets the caches. That check cannot share the mutex: the code reload waits for running calls to finish, and those may be waiting on the mutex. Only a check that began after a call arrived can stand in for the call's own - an earlier one may have passed a file before it was edited - so calls made at once wait for one check together, and calls one after another each check. Sharing any check made within ten walks' time, as the first version did, answered a call made right after an edit from before it.
@@ -0,0 +1,39 @@
1
+ # A workspace's MCP entries name their app by a relative path
2
+
3
+ Status: accepted
4
+
5
+ `rails-ai-context init` run in a folder of apps (a workspace, see `CONTEXT.md`) writes one server per app into the folder's own MCP configs (#429). Those files may be committed, so each entry has to find its app on any machine, and an AI tool gives a stdio server nothing to find it with but the command line, the environment and the directory it starts the server in.
6
+
7
+ ## What the tools do
8
+
9
+ Checked against each tool's documentation and source in October 2026:
10
+
11
+ | Tool | Starts the server in | Names the config's folder |
12
+ |------|----------------------|---------------------------|
13
+ | Claude Code (`.mcp.json`) | the folder it was launched in; it also reads a `.mcp.json` above that folder | no; no `cwd` key either |
14
+ | Codex CLI (`.codex/config.toml`) | its session directory | no; a `cwd` key, itself relative to the session directory |
15
+ | OpenCode (`opencode.json`) | the folder it was opened in | no; a `cwd` key only from v1.17.4, and older versions reject unknown keys |
16
+ | VS Code (`.vscode/mcp.json`) | the workspace folder | `${workspaceFolder}` |
17
+ | Cursor (`.cursor/mcp.json`) | undocumented; users report `/` or `~` | `${workspaceFolder}`, the folder holding `.cursor/` |
18
+
19
+ ## Decision
20
+
21
+ - `--app-path` and an in-Gemfile app's `BUNDLE_GEMFILE` are written relative to the workspace. Where the tool names its config's folder (Cursor, VS Code: `AiTool`'s `folder_variable`), they start from that name; elsewhere they are bare relative paths, which those tools resolve against the folder they were started in. An absolute path would hold only on the machine that wrote it.
22
+ - Each app keeps its own install form: the bare binary for a standalone app, `bundle exec` for an in-Gemfile one, decided from that app's own Gemfile.lock (or its Gemfile, before the bundle is installed) rather than the process's bundle.
23
+ - A re-run writes the current set: an entry under a name the gem gives an app in the folder (its folder name or its path, never a numbered one) is dropped when that app's folder is gone, or when a twin's arrival renamed it. A folder this machine lacks reads as gone: the gem cannot tell an app removed from one never checked out. An entry counts as the gem's by its name and by running `rails-ai-context serve`, or, under the bare name every version has written, by running a command at all; an entry with no command (an HTTP one), a second entry named by hand for an app, a numbered one (as likely made by hand), one naming its app through a variable the tool expands, and one naming an app outside the folder are left as somebody's own.
24
+ - An in-Gemfile app's `BUNDLE_GEMFILE` names the Gemfile its bundle reads: its own, by the name it has in the app even when it links elsewhere, since Bundler names the lockfile after the path it is given; else the one its `config/boot.rb` names, which is how a monorepo's apps share one bundle; else the one `bundle exec` run inside the app would find. With none of these the entry names none.
25
+ - A config the gem cannot write back faithfully - one JSON does not parse, which VS Code and OpenCode accept with trailing commas, one whose comments a rewrite would drop, or one that is not UTF-8 - is left as it is, with the entry to add by hand, said where the install speaks. A folder's config is the likelier to be kept by hand.
26
+ - Each entry is `rails-ai-context-<app folder>` and announces `<app folder>-rails-ai-context`, through the `RAILS_AI_CONTEXT_SERVER_NAME` environment variable rather than a flag: an app whose bundle pins an older gem ignores an unknown variable and starts, where an unknown flag would stop it. The variable only takes the place of the default name, so a `server_name` the app configured wins. VS Code names every tool after the announced name, sanitised and cut to 13 characters, so a name that starts with the gem's would give every app's tools the same `mcp_rails-ai-cont_` prefix and leave the model unable to tell them apart. Names are capped at 30 characters (a hash keeps a longer one unique): Cursor drops a tool whose server and tool names together pass 60, Codex up to v0.100 capped `mcp__<server>__<tool>` at 64, and the longest built-in tool name is 27.
27
+
28
+ ## Consequences
29
+
30
+ The entries work when the tool is started in the workspace folder, which is the setup they are written for. A tool started inside one of the apps that still reads the workspace's config (Claude Code, Codex, OpenCode) starts each server there, where a relative path misses. The binary then names the folder the path was written for instead of guessing a different app; an in-Gemfile entry fails earlier, in Bundler, before the gem runs. An app opened on its own gets its own config from `init` run inside it.
31
+
32
+ Every server in a workspace runs under the Ruby the tool resolves in the workspace folder, which a version manager picks by that folder, not by the app. `init` warns when an app declares another; a standalone app that cannot boot under it answers from the static tier, and an in-Gemfile entry may not start.
33
+
34
+ ## Alternatives not taken
35
+
36
+ - **Absolute paths**: correct on one machine, wrong in every clone.
37
+ - **A `cwd` key per entry**: Claude Code has none, OpenCode's breaks its older versions, and Codex's is relative to the same directory a bare path already is.
38
+ - **Walking up for a relative `--app-path` that misses**: it would make the binary's `--app-path` mean different things in different directories, and would still leave `bundle exec` entries failing before the gem runs.
39
+ - **Re-launching an in-Gemfile app's server from inside the app** (the global binary resolving the app, then `exec`ing `bundle exec` there): it fixes the Ruby choice, but needs the binary installed outside every bundle, cannot `exec` in place on Windows, and fights Codex's environment snapshot, which pins the Ruby `init` ran under.