rails-ai-context 5.15.0 → 5.16.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 +4 -4
- data/CHANGELOG.md +222 -0
- data/README.md +3 -2
- data/config/routes.rb +3 -1
- data/docs/CUSTOM_TOOLS.md +17 -3
- data/docs/TROUBLESHOOTING.md +17 -0
- data/exe/rails-ai-context +50 -7
- data/lib/generators/rails_ai_context/install/install_generator.rb +1 -1
- data/lib/rails_ai_context/app_kind.rb +12 -3
- data/lib/rails_ai_context/cli/tool_runner.rb +1 -1
- data/lib/rails_ai_context/doctor.rb +121 -3
- data/lib/rails_ai_context/introspector.rb +4 -1
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +16 -1
- data/lib/rails_ai_context/introspectors/job_introspector.rb +19 -1
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +3 -0
- data/lib/rails_ai_context/introspectors/listeners/scopes_listener.rb +21 -4
- data/lib/rails_ai_context/introspectors/model_introspector.rb +28 -4
- data/lib/rails_ai_context/introspectors/route_introspector.rb +22 -0
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +158 -5
- data/lib/rails_ai_context/introspectors/turbo_introspector.rb +15 -2
- data/lib/rails_ai_context/path_resolver.rb +6 -0
- data/lib/rails_ai_context/safe_file.rb +5 -1
- data/lib/rails_ai_context/schema_version.rb +9 -4
- data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -2
- data/lib/rails_ai_context/serializers/compact_serializer_helper.rb +20 -9
- data/lib/rails_ai_context/serializers/copilot_instructions_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +13 -2
- data/lib/rails_ai_context/serializers/cursor_rules_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/section_guard.rb +16 -0
- data/lib/rails_ai_context/server.rb +49 -7
- data/lib/rails_ai_context/source_line.rb +208 -0
- data/lib/rails_ai_context/test_helper.rb +6 -1
- data/lib/rails_ai_context/tools/analyze_feature.rb +11 -4
- data/lib/rails_ai_context/tools/base_tool.rb +16 -1
- data/lib/rails_ai_context/tools/diagnose.rb +7 -3
- data/lib/rails_ai_context/tools/generate_test.rb +12 -0
- data/lib/rails_ai_context/tools/get_config.rb +6 -0
- data/lib/rails_ai_context/tools/get_conventions.rb +28 -8
- data/lib/rails_ai_context/tools/get_edit_context.rb +1 -1
- data/lib/rails_ai_context/tools/get_env.rb +3 -0
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +4 -4
- data/lib/rails_ai_context/tools/get_job_pattern.rb +19 -6
- data/lib/rails_ai_context/tools/get_model_details.rb +15 -3
- data/lib/rails_ai_context/tools/get_routes.rb +8 -0
- data/lib/rails_ai_context/tools/get_schema.rb +24 -1
- data/lib/rails_ai_context/tools/get_service_pattern.rb +9 -1
- data/lib/rails_ai_context/tools/get_turbo_map.rb +27 -8
- data/lib/rails_ai_context/tools/query.rb +4 -0
- data/lib/rails_ai_context/tools/runtime_info.rb +4 -0
- data/lib/rails_ai_context/tools/search_code.rb +1 -1
- data/lib/rails_ai_context/tools/search_docs.rb +2 -2
- data/lib/rails_ai_context/version.rb +1 -1
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 59c335a48ab2976318ed2fc58d1cce4e2153c0db0144b61ca8d902f160bbeb76
|
|
4
|
+
data.tar.gz: 334cc340093515797006c8326b47fa97721c9e66f07f940f3ec0864db2ae241a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f0e5532e52f873a3cb659dbf90fde582974f4840609f0c8365318f5b862659475484232e9ead3fd0074ec1a079db565cd8046a157ceb4f1bc1099b08877feded
|
|
7
|
+
data.tar.gz: e6bd4fe02b30344a494a8af944ab6ca94e07f944d143d99bd4f116ca3f15099a5fbbe840b38c1b186398c43c3b23825889fd4b852593fc09342edc50910cd4e9
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,228 @@ 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.16.0] - 2026-07-12
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- **HTTP engine mount works again.** `mount RailsAiContext::Engine, at: "/mcp"`
|
|
13
|
+
500'd with `uninitialized constant McpController`: the engine is not
|
|
14
|
+
namespace-isolated, so its route needed the fully-qualified
|
|
15
|
+
`rails_ai_context/mcp#handle` controller path. Verified with a real curl
|
|
16
|
+
MCP session (initialize + session-id + tools/call) against a booted app.
|
|
17
|
+
- **`config.custom_tools` is usable as documented.** Referencing an
|
|
18
|
+
`app/mcp_tools/` class constant in the initializer aborted boot with
|
|
19
|
+
`NameError` (app/ constants are not autoloadable while initializers run),
|
|
20
|
+
and a class-name string crashed every CLI tool invocation with
|
|
21
|
+
`undefined method 'tool_name' for an instance of String`. Entries may now
|
|
22
|
+
be classes or class-name strings; strings resolve lazily after boot, and
|
|
23
|
+
invalid entries are warn-skipped everywhere (server, CLI runner,
|
|
24
|
+
TestHelper) instead of taking down unrelated tools. Docs and the
|
|
25
|
+
initializer template now show the string form.
|
|
26
|
+
- **Mailers are no longer invisible in development.** `extract_mailers` read
|
|
27
|
+
`ActionMailer::Base.descendants` without the eager-load fallback that
|
|
28
|
+
channels already had, so every dev-mode run reported zero mailers
|
|
29
|
+
(`ai:inspect`, context files, onboard).
|
|
30
|
+
- **TestHelper is self-sufficient.** `require "rails_ai_context/test_helper"`
|
|
31
|
+
alone raised `uninitialized constant RailsAiContext::Tools` - the common
|
|
32
|
+
case, since a `group: :development` install is never Bundler-required in
|
|
33
|
+
the test environment. The helper now requires the gem core itself; docs
|
|
34
|
+
explain custom-tool name lookup in the test environment.
|
|
35
|
+
- **`claude_max_lines` counts physical lines.** Dual MCP/CLI tool examples
|
|
36
|
+
embed newlines inside single entries, so a default-capped CLAUDE.md could
|
|
37
|
+
ship 161 real lines with no trim marker. The cap (and the trim) now apply
|
|
38
|
+
to physical lines of the managed block.
|
|
39
|
+
- **turbo_map no longer flags correct wiring.** `broadcast_append_to post`
|
|
40
|
+
against `turbo_stream_from @post` warned "no matching turbo_stream_from";
|
|
41
|
+
stream matching now strips the ivar sigil before comparing.
|
|
42
|
+
- **frontend_stack's Turbo wiring line can actually render.** It read
|
|
43
|
+
`turbo[:broadcasts]`/`[:frames]` - keys the introspector never emits
|
|
44
|
+
(`model_broadcasts`/`turbo_frames`) - so the line was dead code for every
|
|
45
|
+
app; callback-style `broadcast_*_to` calls are now also detected.
|
|
46
|
+
- **generate_test emits runnable tests for arg-taking scopes.** A scope like
|
|
47
|
+
`scope :by_status, ->(s) { ... }` produced `Post.by_status` (ArgumentError
|
|
48
|
+
at runtime); scope lambdas' required params are now captured through the
|
|
49
|
+
AST layer and such scopes get an `assert_respond_to` test with a
|
|
50
|
+
fill-in-the-args call site. Verified by running the generated file for real.
|
|
51
|
+
- **job_pattern keeps `wait:` in retry configuration.** One ordered regex
|
|
52
|
+
dropped whichever of `wait:`/`attempts:` came first in `retry_on`; the two
|
|
53
|
+
options are now scanned independently.
|
|
54
|
+
- **get_env skips comment lines.** `# ENV["WEB_CONCURRENCY"]` in the stock
|
|
55
|
+
puma.rb comment was reported as a real environment variable.
|
|
56
|
+
- **diagnose omits an empty "Next Steps" section** instead of rendering a
|
|
57
|
+
bare header when no file/method context was parsed.
|
|
58
|
+
- **analyze_feature lists jbuilder templates and record renders**
|
|
59
|
+
(`render @post` / `render post`), matching what get_view reports.
|
|
60
|
+
- **service_pattern surfaces mailer dependencies** (`PostMailer.published_email`
|
|
61
|
+
style calls) and recognizes `perform_now`.
|
|
62
|
+
- **README observability example reads `event.payload[:duration]`** - the
|
|
63
|
+
bridge emits a zero-width event, so `event.duration` is always ~0ms.
|
|
64
|
+
- Singular/plural agreement in tool output ("1 result", "1 site",
|
|
65
|
+
"1 broadcast", "1 line", "1 table", "1 controller", "1 migration file").
|
|
66
|
+
- edit_context's footer no longer suggests pasting the line-numbered block
|
|
67
|
+
verbatim as an Edit old_string.
|
|
68
|
+
- **A syntax-broken model or controller file no longer kills the process in
|
|
69
|
+
the runtime tier.** Eager loading aborts at the first bad file with a
|
|
70
|
+
`SyntaxError` - a `ScriptError`, which sailed past every bare `rescue` and
|
|
71
|
+
took down the MCP server mid-session and every model-touching rake task.
|
|
72
|
+
Introspection now rescues `ScriptError` at the dispatch level, eager
|
|
73
|
+
loading falls back to per-constant loading, and `discover_models`
|
|
74
|
+
tolerates unloadable classes - one broken file costs exactly that file,
|
|
75
|
+
matching the static tier's contract.
|
|
76
|
+
- **Standalone CLI no longer mixes framework versions.** The RubyGems
|
|
77
|
+
binstub activates the newest installed railties before the app's
|
|
78
|
+
Gemfile.lock pins its own, leaving newer framework paths in $LOAD_PATH:
|
|
79
|
+
every generated fact reported the wrong Rails version (e.g. 8.1.3 for an
|
|
80
|
+
8.0.5 app) with "already initialized constant" warnings polluting all
|
|
81
|
+
output. The exe now strips Rails-family gem paths at startup; the app's
|
|
82
|
+
bundle is the only framework source.
|
|
83
|
+
- **`--app-path` and `--environment` work after the tool name** (`tool
|
|
84
|
+
schema --table notes --app-path /x`), matching the `--json`/`--no-boot`
|
|
85
|
+
passthrough handling. Previously they were silently discarded and the
|
|
86
|
+
command ran against the wrong directory.
|
|
87
|
+
- **structure.sql parsing: single-line CREATE TABLE statements** (the shape
|
|
88
|
+
sqlite's `.schema` emits) no longer lose every column but the first;
|
|
89
|
+
bodies are split on top-level commas before per-line parsing.
|
|
90
|
+
- **structure.sql parsing: foreign keys land on the right table.** The
|
|
91
|
+
`ALTER TABLE ... FOREIGN KEY` regex could span statement boundaries and
|
|
92
|
+
attribute a FK to whichever table had the first `ADD CONSTRAINT`.
|
|
93
|
+
- **structure.sql parsing: NOT NULL columns are no longer reported as
|
|
94
|
+
nullable** (nullability was simply not extracted).
|
|
95
|
+
- **Runtime-only tools refuse in the static tier.** After a mid-boot
|
|
96
|
+
failure, `runtime_info` and `query` ran against the half-initialized app
|
|
97
|
+
and returned live, unmarked data contradicting the static-tier banner in
|
|
98
|
+
the same response; both now answer `[UNAVAILABLE: static tier]` with the
|
|
99
|
+
boot-failure reason. This also removes a raw NameError leak in
|
|
100
|
+
runtime_info's Cache section and query's wrong `--skip-active-record`
|
|
101
|
+
blame under `--no-boot`.
|
|
102
|
+
- **The static-tier banner carries the literal `[STATIC]` tag** that README
|
|
103
|
+
and COMPATIBILITY.md promise.
|
|
104
|
+
- **doctor's "Secrets in .gitignore" check understands globs.** Rails 8.1
|
|
105
|
+
generates `/config/*.key`; the substring check false-failed every default
|
|
106
|
+
8.1 app and docked its readiness score.
|
|
107
|
+
- **model_details carries confidence tags**: `[VERIFIED]` on the runtime
|
|
108
|
+
model header, per-scope `[VERIFIED]`/`[INFERRED]` from the AST layer
|
|
109
|
+
(README promised tags this tool never rendered).
|
|
110
|
+
- **Route stats agree across generated context files.** CLAUDE.md counted
|
|
111
|
+
all routes over app controllers while copilot-instructions counted all
|
|
112
|
+
routes over all controllers ("35 across 1" vs "35 across 18"); every
|
|
113
|
+
serializer now reports app routes across app controllers with the
|
|
114
|
+
framework-inclusive total alongside.
|
|
115
|
+
- **get_routes counts engine mounts.** Verbless rack mounts (propshaft's
|
|
116
|
+
`/assets`) vanished from both the app list and the excluded-framework
|
|
117
|
+
count; the header now reports them explicitly.
|
|
118
|
+
- **Mongoid apps are detected from the Gemfile too** (previously only
|
|
119
|
+
Gemfile.lock or config/mongoid.yml), so a fresh checkout without a
|
|
120
|
+
lockfile gets honest Mongoid treatment instead of ActiveRecord answers.
|
|
121
|
+
- **`extra_app_paths` accepts entries ending in `/app`** (`custom/app`
|
|
122
|
+
previously globbed `custom/app/app/models` and silently missed).
|
|
123
|
+
- **analyze_feature no longer invents model names** ("user, payment, order")
|
|
124
|
+
on apps with zero models.
|
|
125
|
+
- **A missing schema is "unavailable", not "failed".** A greenfield app's
|
|
126
|
+
`context` output framed the absent db/schema.rb as "introspection failed";
|
|
127
|
+
absent data sources now use the `[UNAVAILABLE]` vocabulary.
|
|
128
|
+
- **conventions: `create!` flows are no longer attributed the save-and-branch
|
|
129
|
+
skeleton**, zero-signal test files are no longer credited as pattern
|
|
130
|
+
sources, and empty custom-directory descriptions drop the dangling dash.
|
|
131
|
+
- **get_config names the environment its values reflect** (cache store,
|
|
132
|
+
queue adapter, and Action Cable differ per environment).
|
|
133
|
+
- **get_env skips ERB-only false positives from comments** and standalone
|
|
134
|
+
`doctor` writes its report to stdout (`doctor > report.txt` works).
|
|
135
|
+
- docs: TROUBLESHOOTING covers RubyGems' "Resolving dependencies..." stdout
|
|
136
|
+
pollution of the MCP stdio stream and its fixes; a new doctor check ("MCP
|
|
137
|
+
stdio hygiene") detects the condition by re-running gem activation the way
|
|
138
|
+
an MCP client's binstub launch does.
|
|
139
|
+
- **Static schema answers include the implicit id primary key**, typed per
|
|
140
|
+
the adapter in config/database.yml (bigint everywhere, integer on SQLite);
|
|
141
|
+
composite-primary-key tables (`primary_key: [...]`) are left to their
|
|
142
|
+
explicitly dumped columns instead of gaining a phantom id.
|
|
143
|
+
- **Static schema answers name their source dialect and migration state**:
|
|
144
|
+
structure.sql parses report "Dialect", "Schema version" (from the
|
|
145
|
+
schema_migrations insert tail), and the exact pending migration files;
|
|
146
|
+
schema.rb parses report version plus files newer than it. Secondary
|
|
147
|
+
database dumps compare against their own db/<name>_migrate directories.
|
|
148
|
+
- Review-round regression fixes (found by adversarial review of this batch):
|
|
149
|
+
the missing-schema `{unavailable:}` result no longer leaks blank
|
|
150
|
+
"- Database:" lines into generated context files; the gitignore matcher
|
|
151
|
+
honors git's basename rule (`*.key`, bare `master.key`); multi-line
|
|
152
|
+
`retry_on` declarations keep their continuation-line options; a
|
|
153
|
+
syntax-broken `custom_tools` string entry warn-skips instead of crashing
|
|
154
|
+
the server and CLI; the Turbo broadcast scan ignores comments and method
|
|
155
|
+
definitions; redirect and lambda routes are not counted as engine mounts.
|
|
156
|
+
- Second review-round fixes: the gitignore security check honors `!`
|
|
157
|
+
negation patterns (a re-included secret now correctly fails the check);
|
|
158
|
+
`SafeFile.read` scrubs invalid UTF-8 so one bad byte in one file can no
|
|
159
|
+
longer raise out of any scan (previously it could erase every model's
|
|
160
|
+
Turbo broadcasts, or crash `get_turbo_map` outright); legacy zero-padded
|
|
161
|
+
migration versions (`001_` storing version "1") no longer read as
|
|
162
|
+
forever-pending; the synthesized primary-key type is resolved per
|
|
163
|
+
database from config/database.yml (a mixed postgres/sqlite multi-db app
|
|
164
|
+
types each dump correctly); endless-method broadcasts
|
|
165
|
+
(`def refresh = broadcast_replace_to(...)`) are detected while trailing
|
|
166
|
+
`#comments` (with or without spaces) are not, in both the introspector
|
|
167
|
+
and get_turbo_map's own scanners; `retry_on` options inside trailing
|
|
168
|
+
comments are not reported as real retry config; the doctor stdio-hygiene
|
|
169
|
+
probe launches from Bundler's original environment (a configured bundle
|
|
170
|
+
path no longer produces a spurious warning); a custom tool whose class
|
|
171
|
+
body raises during autoload is reported as broken rather than
|
|
172
|
+
"class not found"; the schema_migrations INSERT regex and the
|
|
173
|
+
section-usability predicate each have a single home.
|
|
174
|
+
- Third review-round fixes: source-line scans (Turbo broadcasts, retry_on
|
|
175
|
+
options) strip comments with a string-aware walker (`SourceLine`) instead
|
|
176
|
+
of a regex, so a `#` inside a string literal (`"#comments"`, `"##{id}"`)
|
|
177
|
+
no longer truncates the line and drops real calls, while comment text
|
|
178
|
+
still never reads as code - including comments after a continuation comma
|
|
179
|
+
in multi-line `retry_on`; `def self.broadcast_*_to` definitions are not
|
|
180
|
+
counted as broadcast calls; the doctor gitignore check now models git's
|
|
181
|
+
negation semantics (dir-only `!*/` entries, no re-inclusion under an
|
|
182
|
+
excluded parent, negations must match the file itself) - verified against
|
|
183
|
+
`git check-ignore` on a 16-case battery; the database.yml block lookup
|
|
184
|
+
no longer bleeds across whitespace-only lines into a sibling database's
|
|
185
|
+
adapter; and a custom tool whose body references a missing constant that
|
|
186
|
+
shares a name segment with the entry is no longer misreported as
|
|
187
|
+
"class not found".
|
|
188
|
+
- Fourth review-round fixes: the gitignore check now evaluates paths the
|
|
189
|
+
way git does (ancestor-directory recursion with last-match-wins instead
|
|
190
|
+
of descend globbing), closing a false-SAFE case where `config/` followed
|
|
191
|
+
by `!config/` read as still covering config/master.key - verified 25/25
|
|
192
|
+
against `git check-ignore`, including `**/`, whitelist preludes, and
|
|
193
|
+
re-included directories; it also honors case-insensitive filesystems the
|
|
194
|
+
way core.ignorecase does. `SourceLine.strip_comment` gained %-literal
|
|
195
|
+
support (`%w[cs#billing]` is content, `n % 2` is not a literal), an
|
|
196
|
+
O(n) walk (a 400k-char multibyte line dropped from 27s to 51ms), and a
|
|
197
|
+
no-`#` fast path. Broadcast def-blanking handles constant receivers
|
|
198
|
+
(`def User.broadcast_updates_to`) and multiple defs per line; multi-line
|
|
199
|
+
broadcast extraction reads comment-stripped context; `::`-prefixed
|
|
200
|
+
custom_tools entries classify correctly; config/database.yml is read
|
|
201
|
+
once, CRLF-normalized, per introspection run.
|
|
202
|
+
- Sixth-through-eighth review-round fixes (call-site scanning converged
|
|
203
|
+
after three more single-finding rounds): classic one-line methods
|
|
204
|
+
(`def x; broadcast_y_to(...); end`) contribute their body while
|
|
205
|
+
visibility-prefixed definitions (`private def`, `private_class_method
|
|
206
|
+
def`) contribute nothing; `;`/`=` inside string keyword defaults
|
|
207
|
+
(`sep: "; "`, `label: "a=b"`) and inside `#{...}` interpolation (with
|
|
208
|
+
nested quoted strings) no longer delimit a signature. A final review
|
|
209
|
+
round at a realistic-Rails bar returned zero findings.
|
|
210
|
+
- Fifth review-round fixes: comment stripping is now a whole-fragment
|
|
211
|
+
state machine (`SourceLine.strip_comments`) - literal state carries
|
|
212
|
+
across newlines, so a comment mid-way through a multi-line broadcast
|
|
213
|
+
call is removed while a string spanning lines keeps its tail; backtick
|
|
214
|
+
command literals (with interpolation) are modeled; singleton broadcast
|
|
215
|
+
definitions on any receiver (`def record.broadcast_status_to`,
|
|
216
|
+
`def self::...`) are excluded from broadcast detection; database.yml
|
|
217
|
+
without a final newline still resolves its last block's adapter; and
|
|
218
|
+
the case-insensitivity probe works for app roots with caseless names
|
|
219
|
+
(all-digit directories).
|
|
220
|
+
|
|
221
|
+
### Changed
|
|
222
|
+
|
|
223
|
+
- Standalone `doctor` writes its report to stdout (previously stderr), so
|
|
224
|
+
`doctor > report.txt` captures it; update scripts that redirected `2>`.
|
|
225
|
+
- Custom tools resolved through the CLI are now validated as `MCP::Tool`
|
|
226
|
+
subclasses, matching what the MCP server has always enforced; duck-typed
|
|
227
|
+
tool classes that only ever worked via `rails-ai-context tool` are
|
|
228
|
+
warn-skipped with the reason.
|
|
229
|
+
|
|
8
230
|
## [5.15.0] - 2026-07-11
|
|
9
231
|
|
|
10
232
|
### Added
|
data/README.md
CHANGED
|
@@ -523,7 +523,7 @@ class RailsGetBusinessMetrics < MCP::Tool
|
|
|
523
523
|
end
|
|
524
524
|
|
|
525
525
|
# config/initializers/rails_ai_context.rb
|
|
526
|
-
config.custom_tools = [RailsGetBusinessMetrics]
|
|
526
|
+
config.custom_tools = ["RailsGetBusinessMetrics"]
|
|
527
527
|
```
|
|
528
528
|
|
|
529
529
|
Test with the built-in `TestHelper` (works with RSpec and Minitest):
|
|
@@ -562,7 +562,8 @@ Every MCP tool call fires an `ActiveSupport::Notifications` event:
|
|
|
562
562
|
|
|
563
563
|
```ruby
|
|
564
564
|
ActiveSupport::Notifications.subscribe("rails_ai_context.tools.call") do |event|
|
|
565
|
-
|
|
565
|
+
ms = (event.payload[:duration].to_f * 1000).round
|
|
566
|
+
Rails.logger.info "[MCP] #{event.payload[:tool_name]} - #{ms}ms"
|
|
566
567
|
end
|
|
567
568
|
```
|
|
568
569
|
|
data/config/routes.rb
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
RailsAiContext::Engine.routes.draw do
|
|
4
|
-
|
|
4
|
+
# The engine is not namespace-isolated, so the controller must be
|
|
5
|
+
# referenced by its full path or Rails resolves a top-level McpController.
|
|
6
|
+
match "/", to: "rails_ai_context/mcp#handle", via: :all
|
|
5
7
|
end
|
data/docs/CUSTOM_TOOLS.md
CHANGED
|
@@ -75,13 +75,20 @@ Add your tool classes to the configuration:
|
|
|
75
75
|
# config/initializers/rails_ai_context.rb
|
|
76
76
|
if defined?(RailsAiContext)
|
|
77
77
|
RailsAiContext.configure do |config|
|
|
78
|
-
config.custom_tools = [RailsGetBusinessMetrics]
|
|
78
|
+
config.custom_tools = ["RailsGetBusinessMetrics"]
|
|
79
79
|
end
|
|
80
80
|
end
|
|
81
81
|
```
|
|
82
82
|
|
|
83
83
|
Custom tools appear alongside built-in tools in the MCP server and CLI.
|
|
84
84
|
|
|
85
|
+
Use the class *name* as a string: classes under `app/` (including
|
|
86
|
+
`app/mcp_tools/`) are not autoloadable while initializers run, so a bare
|
|
87
|
+
constant reference there aborts boot with a `NameError`. String entries are
|
|
88
|
+
resolved after the app finishes booting. A bare constant still works for
|
|
89
|
+
classes already loaded when the initializer runs (e.g. defined in `lib/`
|
|
90
|
+
and required manually).
|
|
91
|
+
|
|
85
92
|
## Using BaseTool features
|
|
86
93
|
|
|
87
94
|
For access to caching, pagination, and helper methods, you can use `RailsAiContext::Tools::BaseTool` utilities directly:
|
|
@@ -111,7 +118,14 @@ end
|
|
|
111
118
|
|
|
112
119
|
## Testing custom tools
|
|
113
120
|
|
|
114
|
-
Use the built-in `TestHelper` module
|
|
121
|
+
Use the built-in `TestHelper` module.
|
|
122
|
+
|
|
123
|
+
With the gem in `group: :development`, the initializer's guard makes
|
|
124
|
+
`config.custom_tools` a no-op in the test environment, so name-based lookup
|
|
125
|
+
(`execute_tool("rails_get_business_metrics")`) won't find custom tools
|
|
126
|
+
there. Either install the gem with `group: %i[development test]`, or pass
|
|
127
|
+
the class itself: `execute_tool(RailsGetBusinessMetrics)`. Built-in tools
|
|
128
|
+
resolve by name in any environment.
|
|
115
129
|
|
|
116
130
|
### With RSpec
|
|
117
131
|
|
|
@@ -191,7 +205,7 @@ If a custom tool replaces a built-in one, exclude the original:
|
|
|
191
205
|
|
|
192
206
|
```ruby
|
|
193
207
|
RailsAiContext.configure do |config|
|
|
194
|
-
config.custom_tools = [MyBetterSecurityScan]
|
|
208
|
+
config.custom_tools = ["MyBetterSecurityScan"]
|
|
195
209
|
config.skip_tools = %w[rails_security_scan]
|
|
196
210
|
end
|
|
197
211
|
```
|
data/docs/TROUBLESHOOTING.md
CHANGED
|
@@ -100,6 +100,23 @@ For Codex CLI specifically, the env section in `.codex/config.toml` must match y
|
|
|
100
100
|
2. Check schema exists: `ls db/schema.rb` or `ls db/structure.sql`
|
|
101
101
|
3. Run doctor: `rails ai:doctor`
|
|
102
102
|
|
|
103
|
+
### MCP client reports a JSON parse error on the first line ("Resolving dependencies...")
|
|
104
|
+
|
|
105
|
+
RubyGems can print `Resolving dependencies...` to stdout while activating the
|
|
106
|
+
standalone `rails-ai-context` binary - before any gem code runs - which breaks
|
|
107
|
+
the pure-JSON stdio framing. This happens when gem activation needs the full
|
|
108
|
+
dependency resolver: typically a `GEM_PATH` that is missing part of the
|
|
109
|
+
dependency graph, or launching from inside another Bundler project's
|
|
110
|
+
directory. Fixes:
|
|
111
|
+
|
|
112
|
+
1. Launch `rails-ai-context serve` with the app root as the working directory
|
|
113
|
+
(the generated MCP configs do this by default).
|
|
114
|
+
2. Make sure `GEM_PATH` covers the complete default gem path
|
|
115
|
+
(`gem env path`), not just `gem env gemdir` - Ruby's bundled gems
|
|
116
|
+
(racc, etc.) live in a separate directory.
|
|
117
|
+
3. In-Gemfile installs are immune: `bundle exec rails-ai-context serve`
|
|
118
|
+
activates through the lockfile.
|
|
119
|
+
|
|
103
120
|
### MCP server responds slowly
|
|
104
121
|
|
|
105
122
|
- Check `config.cache_ttl` - lower values mean more frequent re-introspection
|
data/exe/rails-ai-context
CHANGED
|
@@ -11,6 +11,24 @@
|
|
|
11
11
|
# correctly, and clearing specs breaks gems that look up their own path at runtime
|
|
12
12
|
# (e.g., MCP SDK reads Gem.loaded_specs["json-schema"].full_gem_path).
|
|
13
13
|
if defined?(Gem) && Gem.respond_to?(:loaded_specs) && !ENV["BUNDLE_BIN_PATH"]
|
|
14
|
+
# Also strip the Rails-family gems' lib paths. Leaving them in $LOAD_PATH
|
|
15
|
+
# lets the app's boot resolve framework files from the NEWEST installed
|
|
16
|
+
# versions instead of the app's Gemfile.lock pins, producing a mixed
|
|
17
|
+
# framework tree: wrong Rails.version in every generated fact and
|
|
18
|
+
# "already initialized constant" double-load warnings. Every Rails app's
|
|
19
|
+
# bundle provides these; in app-less modes a require re-activates them
|
|
20
|
+
# fresh. Non-framework tooling deps (mcp and friends) must keep their
|
|
21
|
+
# paths - Bundler.setup during app boot locks out late RubyGems
|
|
22
|
+
# activation, and restore_standalone_environment! splices them back.
|
|
23
|
+
rails_family = %w[
|
|
24
|
+
rails railties activesupport activemodel activerecord actionpack
|
|
25
|
+
actionview actionmailer activejob actioncable activestorage
|
|
26
|
+
actionmailbox actiontext
|
|
27
|
+
].freeze
|
|
28
|
+
Gem.loaded_specs.each_value do |spec|
|
|
29
|
+
next if spec.default_gem? || !rails_family.include?(spec.name)
|
|
30
|
+
spec.full_require_paths.each { |path| $LOAD_PATH.delete(path) }
|
|
31
|
+
end
|
|
14
32
|
Gem.loaded_specs.delete_if { |_, spec| !spec.default_gem? }
|
|
15
33
|
end
|
|
16
34
|
|
|
@@ -74,6 +92,12 @@ class RailsAiContextCLI < Thor
|
|
|
74
92
|
no_boot = options[:no_boot]
|
|
75
93
|
no_boot = true if args.delete("--no-boot")
|
|
76
94
|
|
|
95
|
+
# Same for the value-taking class options: after the tool name they
|
|
96
|
+
# arrive as plain args, and dropping them silently sends the command
|
|
97
|
+
# at the wrong app (or wrong RAILS_ENV).
|
|
98
|
+
@app_path_override = extract_value_flag!(args, "--app-path")
|
|
99
|
+
@environment_override = extract_value_flag!(args, "--environment")
|
|
100
|
+
|
|
77
101
|
boot_rails!(allow_static: true, no_boot: no_boot)
|
|
78
102
|
|
|
79
103
|
runner = ::RailsAiContext::CLI::ToolRunner.new(name, args, json_mode: json_mode)
|
|
@@ -173,17 +197,19 @@ class RailsAiContextCLI < Thor
|
|
|
173
197
|
def doctor
|
|
174
198
|
boot_rails!(context: "doctor")
|
|
175
199
|
result = ::RailsAiContext::Doctor.new.run
|
|
200
|
+
# The report is the command's product - it belongs on stdout so
|
|
201
|
+
# `doctor > report.txt` captures it. stderr stays for errors only.
|
|
176
202
|
result[:checks].each do |check|
|
|
177
203
|
icon = case check.status
|
|
178
204
|
when :pass then "PASS"
|
|
179
205
|
when :warn then "WARN"
|
|
180
206
|
when :fail then "FAIL"
|
|
181
207
|
end
|
|
182
|
-
|
|
183
|
-
|
|
208
|
+
puts " [#{icon}] #{check.name}: #{check.message}"
|
|
209
|
+
puts " Fix: #{check.fix}" if check.fix
|
|
184
210
|
end
|
|
185
|
-
|
|
186
|
-
|
|
211
|
+
puts ""
|
|
212
|
+
puts "AI Readiness Score: #{result[:score]}/100"
|
|
187
213
|
|
|
188
214
|
failed = result[:checks].count { |c| c.status == :fail }
|
|
189
215
|
if options[:strict] && failed > 0
|
|
@@ -590,12 +616,29 @@ class RailsAiContextCLI < Thor
|
|
|
590
616
|
# capture references BEFORE Rails boot runs and re-register them afterwards.
|
|
591
617
|
STANDALONE_REQUIRED_GEMS = %w[mcp json-schema addressable public_suffix].freeze
|
|
592
618
|
|
|
619
|
+
# Pull a value-taking flag (--flag VALUE or --flag=VALUE) out of the
|
|
620
|
+
# pass-through args that stop_on_unknown_option! leaves after the tool
|
|
621
|
+
# name. Returns the value, or nil when the flag isn't present.
|
|
622
|
+
def extract_value_flag!(args, flag)
|
|
623
|
+
if (idx = args.index(flag))
|
|
624
|
+
args.delete_at(idx)
|
|
625
|
+
return args.delete_at(idx)
|
|
626
|
+
end
|
|
627
|
+
if (pair = args.find { |a| a.start_with?("#{flag}=") })
|
|
628
|
+
args.delete(pair)
|
|
629
|
+
return pair.split("=", 2)[1]
|
|
630
|
+
end
|
|
631
|
+
nil
|
|
632
|
+
end
|
|
633
|
+
|
|
593
634
|
def boot_rails!(context: nil, allow_static: false, no_boot: false)
|
|
594
|
-
|
|
635
|
+
app_path_opt = @app_path_override || options[:app_path]
|
|
636
|
+
environment_opt = @environment_override || options[:environment]
|
|
637
|
+
app_root = File.expand_path(app_path_opt || Dir.pwd)
|
|
595
638
|
# Relative paths inside the app (log/, tmp/, sqlite files) assume the
|
|
596
639
|
# process cwd is the app root, so honor --app-path by moving there.
|
|
597
|
-
Dir.chdir(app_root) if
|
|
598
|
-
ENV["RAILS_ENV"] =
|
|
640
|
+
Dir.chdir(app_root) if app_path_opt
|
|
641
|
+
ENV["RAILS_ENV"] = environment_opt if environment_opt
|
|
599
642
|
|
|
600
643
|
if allow_static && (no_boot || options[:no_boot])
|
|
601
644
|
enter_static_tier!("static mode requested with --no-boot")
|
|
@@ -284,7 +284,7 @@ module RailsAiContext
|
|
|
284
284
|
"Extensibility" => <<~SECTION,
|
|
285
285
|
# ── Extensibility ─────────────────────────────────────────────────
|
|
286
286
|
# Register additional MCP tool classes alongside the #{RailsAiContext::Server.builtin_tools.size} built-in tools
|
|
287
|
-
# config.custom_tools = [MyApp::CustomTool]
|
|
287
|
+
# config.custom_tools = ["MyApp::CustomTool"] # class name as string - resolved after boot
|
|
288
288
|
|
|
289
289
|
# Exclude specific built-in tools by name
|
|
290
290
|
# config.skip_tools = %w[rails_security_scan]
|
|
@@ -13,10 +13,19 @@ module RailsAiContext
|
|
|
13
13
|
return true if File.exist?(File.join(root, "config", "mongoid.yml"))
|
|
14
14
|
|
|
15
15
|
lock = File.join(root, "Gemfile.lock")
|
|
16
|
-
|
|
16
|
+
if File.exist?(lock)
|
|
17
|
+
content = RailsAiContext::SafeFile.read(lock)
|
|
18
|
+
return !!content&.include?(" mongoid (")
|
|
19
|
+
end
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
21
|
+
# No lockfile yet (fresh checkout, bare directory): fall back to the
|
|
22
|
+
# Gemfile's own declaration so the app still gets Mongoid treatment
|
|
23
|
+
# instead of misleading ActiveRecord answers.
|
|
24
|
+
gemfile = File.join(root, "Gemfile")
|
|
25
|
+
return false unless File.exist?(gemfile)
|
|
26
|
+
|
|
27
|
+
content = RailsAiContext::SafeFile.read(gemfile)
|
|
28
|
+
!!content&.match?(/^\s*gem\s+["']mongoid["']/)
|
|
20
29
|
end
|
|
21
30
|
end
|
|
22
31
|
end
|
|
@@ -70,7 +70,7 @@ module RailsAiContext
|
|
|
70
70
|
def self.available_tools
|
|
71
71
|
skip = RailsAiContext.configuration.skip_tools
|
|
72
72
|
tools = Server.builtin_tools
|
|
73
|
-
tools +=
|
|
73
|
+
tools += Server.resolve_custom_tools
|
|
74
74
|
return tools if skip.empty?
|
|
75
75
|
tools.reject { |t| skip.include?(t.tool_name) }
|
|
76
76
|
end
|
|
@@ -27,6 +27,7 @@ module RailsAiContext
|
|
|
27
27
|
check_prism
|
|
28
28
|
check_brakeman
|
|
29
29
|
check_live_reload
|
|
30
|
+
check_stdio_activation_hygiene
|
|
30
31
|
check_security_gitignore
|
|
31
32
|
check_security_auto_mount
|
|
32
33
|
check_performance_schema_size
|
|
@@ -152,7 +153,7 @@ module RailsAiContext
|
|
|
152
153
|
migrate_dir = File.join(app.root, "db/migrate")
|
|
153
154
|
if Dir.exist?(migrate_dir) && Dir.glob(File.join(migrate_dir, "*.rb")).any?
|
|
154
155
|
count = Dir.glob(File.join(migrate_dir, "*.rb")).size
|
|
155
|
-
Check.new(name: "Migrations", status: :pass, message: "#{count} migration files", fix: nil)
|
|
156
|
+
Check.new(name: "Migrations", status: :pass, message: "#{count} migration #{count == 1 ? 'file' : 'files'}", fix: nil)
|
|
156
157
|
else
|
|
157
158
|
Check.new(name: "Migrations", status: :warn, message: "No migrations", fix: nil)
|
|
158
159
|
end
|
|
@@ -343,6 +344,46 @@ module RailsAiContext
|
|
|
343
344
|
fix: "Check mcp gem: `bundle info mcp`")
|
|
344
345
|
end
|
|
345
346
|
|
|
347
|
+
# RubyGems prints "Resolving dependencies..." to STDOUT when activating
|
|
348
|
+
# this gem needs the full resolver (split/incomplete GEM_PATH, conflicting
|
|
349
|
+
# candidate versions). That happens before any gem code runs and corrupts
|
|
350
|
+
# the MCP stdio stream's pure-JSON framing. Re-run the activation the way
|
|
351
|
+
# the standalone binstub does and flag anything that lands on stdout.
|
|
352
|
+
# In-Gemfile installs activate through the lockfile and are immune.
|
|
353
|
+
def check_stdio_activation_hygiene
|
|
354
|
+
return Check.new(name: "MCP stdio hygiene", status: :pass,
|
|
355
|
+
message: "in-Gemfile install activates via bundler (no resolver output)", fix: nil) unless InstallMode.standalone?
|
|
356
|
+
|
|
357
|
+
require "open3"
|
|
358
|
+
# An MCP client launches the binstub from a clean shell; simulate that
|
|
359
|
+
# exactly. The booted app's Bundler has mutated this process's env
|
|
360
|
+
# (RUBYOPT gets -rbundler/setup; with a configured bundle path even
|
|
361
|
+
# GEM_HOME/GEM_PATH point into vendor/bundle), so start from Bundler's
|
|
362
|
+
# snapshot of the pre-boot environment when it is available.
|
|
363
|
+
base_env = defined?(Bundler) && Bundler.respond_to?(:original_env) ? Bundler.original_env : ENV.to_h
|
|
364
|
+
%w[RUBYOPT RUBYLIB BUNDLE_GEMFILE BUNDLE_BIN_PATH BUNDLER_SETUP BUNDLE_PATH].each { |k| base_env.delete(k) }
|
|
365
|
+
stdout, _stderr, status = Open3.capture3(
|
|
366
|
+
base_env, Gem.ruby, "--disable-gems", "-e",
|
|
367
|
+
"require 'rubygems'; gem 'rails-ai-context'",
|
|
368
|
+
unsetenv_others: true
|
|
369
|
+
)
|
|
370
|
+
if status.success? && stdout.empty?
|
|
371
|
+
Check.new(name: "MCP stdio hygiene", status: :pass,
|
|
372
|
+
message: "gem activation is silent on stdout", fix: nil)
|
|
373
|
+
elsif stdout.empty?
|
|
374
|
+
Check.new(name: "MCP stdio hygiene", status: :warn,
|
|
375
|
+
message: "gem activation exited #{status.exitstatus} (stdout clean)",
|
|
376
|
+
fix: "Run `gem list rails-ai-context` and reinstall if missing")
|
|
377
|
+
else
|
|
378
|
+
Check.new(name: "MCP stdio hygiene", status: :fail,
|
|
379
|
+
message: "gem activation writes to stdout (#{stdout.lines.first.to_s.strip.truncate(60)}) - this corrupts the MCP stdio stream",
|
|
380
|
+
fix: "GEM_PATH is split or incomplete; use the full `gem env path` (see TROUBLESHOOTING: MCP client reports a JSON parse error)")
|
|
381
|
+
end
|
|
382
|
+
rescue => e
|
|
383
|
+
Check.new(name: "MCP stdio hygiene", status: :warn,
|
|
384
|
+
message: "could not verify activation hygiene: #{e.message.truncate(60)}", fix: nil)
|
|
385
|
+
end
|
|
386
|
+
|
|
346
387
|
# ── Introspector health ───────────────────────────────────────────
|
|
347
388
|
|
|
348
389
|
def check_introspector_health
|
|
@@ -467,8 +508,8 @@ module RailsAiContext
|
|
|
467
508
|
|
|
468
509
|
gitignore = File.read(gitignore_path)
|
|
469
510
|
issues = []
|
|
470
|
-
issues << ".env exists but not in .gitignore" if sensitive_files.include?(".env") && !
|
|
471
|
-
issues << "config/master.key not in .gitignore" if sensitive_files.include?("config/master.key") && !
|
|
511
|
+
issues << ".env exists but not in .gitignore" if sensitive_files.include?(".env") && !gitignore_covers?(gitignore, ".env")
|
|
512
|
+
issues << "config/master.key not in .gitignore" if sensitive_files.include?("config/master.key") && !gitignore_covers?(gitignore, "config/master.key")
|
|
472
513
|
|
|
473
514
|
if issues.empty?
|
|
474
515
|
Check.new(name: "Secrets in .gitignore", status: :pass, message: "Sensitive files properly gitignored", fix: nil)
|
|
@@ -479,6 +520,83 @@ module RailsAiContext
|
|
|
479
520
|
end
|
|
480
521
|
end
|
|
481
522
|
|
|
523
|
+
# Substring checks miss glob entries (Rails 8.1 generates `/config/*.key`
|
|
524
|
+
# rather than a literal master.key line), so match .gitignore patterns
|
|
525
|
+
# the way git does: anchored when the pattern contains a slash (* stops
|
|
526
|
+
# at separators), basename-at-any-depth when it doesn't, last-match-wins
|
|
527
|
+
# ordering, and git's negation rules - a `pattern/` entry only concerns
|
|
528
|
+
# directories, a negation must match the file itself (not a container),
|
|
529
|
+
# and a file cannot be re-included while a parent directory is excluded.
|
|
530
|
+
def gitignore_covers?(content, path)
|
|
531
|
+
gitignore_path_ignored?(parse_gitignore_rules(content), path, dir: false)
|
|
532
|
+
end
|
|
533
|
+
|
|
534
|
+
def parse_gitignore_rules(content)
|
|
535
|
+
content.each_line.filter_map do |line|
|
|
536
|
+
pattern = line.strip
|
|
537
|
+
next if pattern.empty? || pattern.start_with?("#")
|
|
538
|
+
|
|
539
|
+
negation = pattern.start_with?("!")
|
|
540
|
+
pattern = pattern.delete_prefix("!")
|
|
541
|
+
dir_only = pattern.end_with?("/")
|
|
542
|
+
anchored = pattern.chomp("/").include?("/")
|
|
543
|
+
pattern = pattern.delete_prefix("/").chomp("/")
|
|
544
|
+
next if pattern.empty?
|
|
545
|
+
|
|
546
|
+
{ negation: negation, dir_only: dir_only, anchored: anchored, pattern: pattern }
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Mirrors git's evaluation order: if any ancestor directory ends up
|
|
551
|
+
# ignored (itself evaluated with the same rules, negations included),
|
|
552
|
+
# everything beneath it is ignored and cannot be re-included. Otherwise
|
|
553
|
+
# the path is judged by the last rule matching the path ITSELF - there
|
|
554
|
+
# is no "descend" globbing, which is what made `config/` + `!config/`
|
|
555
|
+
# (a re-included directory) falsely read as still covering its files.
|
|
556
|
+
def gitignore_path_ignored?(rules, path, dir:)
|
|
557
|
+
return true if gitignore_parent_ignored?(rules, path)
|
|
558
|
+
|
|
559
|
+
ignored = false
|
|
560
|
+
rules.each do |rule|
|
|
561
|
+
# `pattern/` entries concern directories only, in both polarities.
|
|
562
|
+
next if rule[:dir_only] && !dir
|
|
563
|
+
next if rule[:negation] && !ignored
|
|
564
|
+
|
|
565
|
+
ignored = !rule[:negation] if gitignore_pattern_matches?(rule, path)
|
|
566
|
+
end
|
|
567
|
+
ignored
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def gitignore_parent_ignored?(rules, path)
|
|
571
|
+
parts = path.split("/")[0..-2]
|
|
572
|
+
parents = parts.each_index.map { |i| parts[0..i].join("/") }
|
|
573
|
+
parents.any? { |parent| gitignore_path_ignored?(rules, parent, dir: true) }
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def gitignore_pattern_matches?(rule, path)
|
|
577
|
+
flags = File::FNM_PATHNAME | File::FNM_DOTMATCH
|
|
578
|
+
flags |= File::FNM_CASEFOLD if gitignore_case_insensitive?
|
|
579
|
+
if rule[:anchored]
|
|
580
|
+
File.fnmatch?(rule[:pattern], path, flags) || path == rule[:pattern]
|
|
581
|
+
else
|
|
582
|
+
File.fnmatch?(rule[:pattern], File.basename(path), flags)
|
|
583
|
+
end
|
|
584
|
+
end
|
|
585
|
+
|
|
586
|
+
# git sets core.ignorecase from the filesystem; mirror it by probing
|
|
587
|
+
# whether the .gitignore file itself resolves case-insensitively
|
|
588
|
+
# (APFS/NTFS default). The .gitignore basename always carries cased
|
|
589
|
+
# letters, unlike the app root (which can be all digits).
|
|
590
|
+
def gitignore_case_insensitive?
|
|
591
|
+
return @gitignore_case_insensitive if defined?(@gitignore_case_insensitive)
|
|
592
|
+
|
|
593
|
+
gitignore = File.join(app.root.to_s, ".gitignore")
|
|
594
|
+
swapped = File.join(app.root.to_s, ".GITIGNORE")
|
|
595
|
+
@gitignore_case_insensitive = File.exist?(gitignore) && File.identical?(gitignore, swapped)
|
|
596
|
+
rescue StandardError
|
|
597
|
+
@gitignore_case_insensitive = false
|
|
598
|
+
end
|
|
599
|
+
|
|
482
600
|
def check_security_auto_mount
|
|
483
601
|
config = RailsAiContext.configuration
|
|
484
602
|
if config.auto_mount && defined?(Rails.env) && Rails.env.production?
|
|
@@ -29,7 +29,10 @@ module RailsAiContext
|
|
|
29
29
|
config.introspectors.each do |name|
|
|
30
30
|
introspector = resolve_introspector(name)
|
|
31
31
|
context[name] = run_introspector(introspector)
|
|
32
|
-
rescue => e
|
|
32
|
+
rescue StandardError, ScriptError => e
|
|
33
|
+
# ScriptError included: a syntax-broken app file surfacing through
|
|
34
|
+
# eager loading must cost one section, not the whole process (the
|
|
35
|
+
# MCP server would otherwise die mid-session).
|
|
33
36
|
context[name] = { error: e.message }
|
|
34
37
|
RailsAiContext.log_warn "[rails-ai-context] #{name} introspection failed: #{e.message}"
|
|
35
38
|
end
|