rails-ai-context 5.16.1 → 5.17.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/.rubocop.yml +3 -10
- data/AGENTS.md +15 -0
- data/CHANGELOG.md +128 -0
- data/CONTRIBUTING.md +1 -1
- data/README.md +3 -3
- data/app/controllers/rails_ai_context/mcp_controller.rb +44 -2
- data/docs/ARCHITECTURE.md +9 -9
- data/docs/CLI.md +2 -2
- data/docs/CONFIGURATION.md +15 -4
- data/docs/CUSTOM_TOOLS.md +2 -2
- data/docs/FAQ.md +3 -3
- data/docs/GUIDE.md +4 -4
- data/docs/INTROSPECTORS.md +4 -3
- data/docs/QUICKSTART.md +1 -1
- data/docs/SECURITY.md +29 -18
- data/docs/SETUP.md +3 -1
- data/docs/STANDALONE.md +2 -2
- data/docs/TOOLS.md +2 -2
- data/docs/TROUBLESHOOTING.md +2 -2
- data/docs/agents/domain.md +38 -0
- data/docs/agents/issue-tracker.md +45 -0
- data/docs/agents/triage-labels.md +15 -0
- data/docs/index.md +1 -1
- data/exe/rails-ai-context +29 -5
- data/lib/rails_ai_context/configuration.rb +3 -1
- data/lib/rails_ai_context/detail_level.rb +40 -0
- data/lib/rails_ai_context/introspectors/convention_introspector.rb +14 -11
- data/lib/rails_ai_context/introspectors/engine_introspector.rb +1 -1
- data/lib/rails_ai_context/introspectors/gem_introspector.rb +0 -2
- data/lib/rails_ai_context/introspectors/listeners/schema_dsl_listener.rb +11 -0
- data/lib/rails_ai_context/introspectors/performance_introspector.rb +5 -40
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +85 -182
- data/lib/rails_ai_context/introspectors/schema_reader.rb +166 -0
- data/lib/rails_ai_context/introspectors/source_introspector.rb +2 -4
- data/lib/rails_ai_context/mcp_config_generator.rb +5 -12
- data/lib/rails_ai_context/resources.rb +11 -0
- data/lib/rails_ai_context/safe_file.rb +14 -0
- data/lib/rails_ai_context/serializers/claude_serializer.rb +7 -9
- data/lib/rails_ai_context/serializers/context_file_serializer.rb +1 -8
- data/lib/rails_ai_context/serializers/context_mode_dispatch.rb +25 -0
- data/lib/rails_ai_context/serializers/copilot_serializer.rb +4 -13
- data/lib/rails_ai_context/serializers/markdown_serializer.rb +1 -1
- data/lib/rails_ai_context/serializers/opencode_serializer.rb +4 -13
- data/lib/rails_ai_context/serializers/section_marker_writer.rb +1 -6
- data/lib/rails_ai_context/serializers/tool_guide_helper.rb +1 -1
- data/lib/rails_ai_context/tasks/rails_ai_context.rake +10 -7
- data/lib/rails_ai_context/tools/analyze_feature.rb +1 -1
- data/lib/rails_ai_context/tools/diagnose.rb +2 -2
- data/lib/rails_ai_context/tools/generate_test.rb +1 -1
- data/lib/rails_ai_context/tools/get_callbacks.rb +2 -2
- data/lib/rails_ai_context/tools/get_concern.rb +2 -2
- data/lib/rails_ai_context/tools/get_conventions.rb +18 -1
- data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -1
- data/lib/rails_ai_context/tools/get_helper_methods.rb +20 -5
- data/lib/rails_ai_context/tools/get_schema.rb +1 -1
- data/lib/rails_ai_context/tools/get_test_info.rb +1 -1
- data/lib/rails_ai_context/tools/get_view.rb +1 -1
- data/lib/rails_ai_context/tools/onboard.rb +3 -3
- data/lib/rails_ai_context/tools/performance_check.rb +3 -3
- data/lib/rails_ai_context/tools/query.rb +10 -7
- data/lib/rails_ai_context/tools/review_changes.rb +18 -2
- data/lib/rails_ai_context/tools/runtime_info.rb +4 -4
- data/lib/rails_ai_context/tools/search_code.rb +2 -2
- data/lib/rails_ai_context/tools/validate.rb +5 -853
- data/lib/rails_ai_context/tools/validate_semantics.rb +879 -0
- data/lib/rails_ai_context/version.rb +1 -1
- data/lib/rails_ai_context/vfs.rb +26 -2
- metadata +9 -2
- data/docs/superpowers/plans/2026-05-24-prism-ast-migration.md +0 -1218
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebb8d4e0ebed93183c0c019071ee63d097fb8c7495f551393991000aa5d06e7b
|
|
4
|
+
data.tar.gz: 2141cf0a281a173da2f0fa34927be843f93600ced5cdd67ccd6eb4aeef261ce0
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e1bdda115ee2c82a43af9f6389ad18204629539e5867db0ae36d8c4fe86f50547084315470ed5842adccbced095026e2a220f32626d1c1e43600b595d7224afb
|
|
7
|
+
data.tar.gz: 197fe45172ce6888fd89ee63ecc505f5e014f1b3c759a918a7244025958ce9d17801f4bf1a0ac3ce401a93063b0f3d75998b4ec485f6bdbe4df9449be333920d
|
data/.rubocop.yml
CHANGED
|
@@ -11,13 +11,6 @@ AllCops:
|
|
|
11
11
|
Style/Documentation:
|
|
12
12
|
Enabled: false
|
|
13
13
|
|
|
14
|
-
Metrics
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
Metrics/ClassLength:
|
|
18
|
-
Max: 200
|
|
19
|
-
|
|
20
|
-
Metrics/BlockLength:
|
|
21
|
-
Exclude:
|
|
22
|
-
- "spec/**/*"
|
|
23
|
-
- "*.gemspec"
|
|
14
|
+
# rubocop-rails-omakase disables the whole Metrics department. Setting Max on a
|
|
15
|
+
# disabled cop does not re-enable it, so size limits here would enforce nothing.
|
|
16
|
+
# Turning it on would need ~40 existing files brought under the limit first.
|
data/AGENTS.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# rails-ai-context
|
|
2
|
+
|
|
3
|
+
## Agent skills
|
|
4
|
+
|
|
5
|
+
### Issue tracker
|
|
6
|
+
|
|
7
|
+
Issues live as GitHub issues on `crisnahine/rails-ai-context`, driven with the `gh` CLI. See `docs/agents/issue-tracker.md`.
|
|
8
|
+
|
|
9
|
+
### Triage labels
|
|
10
|
+
|
|
11
|
+
The five canonical roles, each label string equal to its name (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`.
|
|
12
|
+
|
|
13
|
+
### Domain docs
|
|
14
|
+
|
|
15
|
+
Single-context: `CONTEXT.md` and `docs/adr/` at the repo root. See `docs/agents/domain.md`.
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,134 @@ 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.17.0] - 2026-08-09
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **`config.query_allowed_columns`** exempts a column name from the built-in
|
|
13
|
+
sensitive list used by `rails_query`. The rejection message told you to
|
|
14
|
+
subtract from `query_redacted_columns`, which could never work: that list is
|
|
15
|
+
unioned with a frozen suffix list, so an app with its own
|
|
16
|
+
`oauth_applications.secret` had no way to query it.
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
|
|
20
|
+
- **Documented security behaviour now matches the code.** The ReDoS timeout on
|
|
21
|
+
user-supplied regexes needs `Regexp.timeout`, which Ruby 3.1 does not have,
|
|
22
|
+
so `SECURITY.md` no longer promises it unconditionally on a version the gem
|
|
23
|
+
still supports. Layer 4 was documented as post-execution redaction; it has
|
|
24
|
+
rejected the query outright since 5.8.1, which is what the doc now says.
|
|
25
|
+
- **`config.introspectors = %i[source]` no longer appears to be supported.**
|
|
26
|
+
`SourceIntrospector` was listed in the introspector table but is
|
|
27
|
+
infrastructure, not a registered introspector, so configuring it raised
|
|
28
|
+
`ConfigurationError`.
|
|
29
|
+
- **Corrected documented defaults**, which had drifted: `sensitive_patterns` is
|
|
30
|
+
27 patterns not 8, `excluded_middleware` 25 not 24, `excluded_filters` 5 not
|
|
31
|
+
3, and the listener count is 21. `extra_app_paths` and
|
|
32
|
+
`instrumentation_include_arguments` were undocumented.
|
|
33
|
+
- **Secondary database dumps report their own generated columns.** Parsing
|
|
34
|
+
`db/queue_schema.rb` read generated columns from the primary `db/schema.rb`
|
|
35
|
+
instead of the dump being parsed.
|
|
36
|
+
- **Polymorphic foreign keys no longer always report a missing index.** The
|
|
37
|
+
compound check compared one joined string against two column names, so it
|
|
38
|
+
could never match an existing index.
|
|
39
|
+
- **A column is no longer treated as indexed because a wider column name is.**
|
|
40
|
+
Index matching compared substrings, so an index on `user_id` counted as
|
|
41
|
+
covering `id`.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- **schema.rb is read through one AST-backed reader.** `SchemaIntrospector`,
|
|
46
|
+
`PerformanceIntrospector` and `ConventionIntrospector` each parsed the dump
|
|
47
|
+
line by line with their own `create_table` regex; they now share
|
|
48
|
+
`SchemaReader`, built on the existing `SchemaDslListener`, as do the static
|
|
49
|
+
tier's table parse and the check-constraint, enum and generated-column
|
|
50
|
+
reads that each walked the dump separately.
|
|
51
|
+
- **The static schema tier reports four things differently.** Where no live
|
|
52
|
+
connection is available and `db/schema.rb` is parsed instead: a `t.references`
|
|
53
|
+
or `t.belongs_to` column is reported by its foreign key name (`author_id`,
|
|
54
|
+
not `author`), which is what the live tier has always reported; a column
|
|
55
|
+
default written as a proc reports its source rather than being omitted; a
|
|
56
|
+
default split across lines is picked up; and an expression index declared at
|
|
57
|
+
the top level is flagged `expression: true`, as an in-table one already was.
|
|
58
|
+
- **`rails_validate`'s Rails-aware rules moved to `ValidateSemantics`.** The
|
|
59
|
+
tool entry point kept syntax validation and orchestration; the 15 lint rules
|
|
60
|
+
now live in their own class. Behaviour is unchanged.
|
|
61
|
+
- **`detail` has a type.** `DetailLevel` defines the three levels, their
|
|
62
|
+
ordering and normalization. An unrecognised value now reads as `standard`
|
|
63
|
+
rather than silently selecting whichever branch happened to be last.
|
|
64
|
+
- **Soft-delete detection prefers the schema.** A `deleted_at` column in the
|
|
65
|
+
dump now drives the `soft_delete` convention, instead of matching the word
|
|
66
|
+
anywhere in model source. Apps dumping `structure.sql` keep the old source
|
|
67
|
+
match, and the `acts_as_paranoid` / `discard` macro check is unchanged.
|
|
68
|
+
|
|
69
|
+
## [5.16.2] - 2026-07-17
|
|
70
|
+
|
|
71
|
+
### Fixed
|
|
72
|
+
|
|
73
|
+
- **Standalone static tier works when installed via `gem install`.**
|
|
74
|
+
`tool <name> --no-boot`, `tool <name> --help`, and `tool --list` outside
|
|
75
|
+
an app all died with `cannot load such file -- active_support/...`. The
|
|
76
|
+
RubyGems binstub eagerly activates the whole dependency tree, so the
|
|
77
|
+
exe's framework-path strip left every rails-family spec still flagged as
|
|
78
|
+
activated and `Gem.try_activate` refused to re-add the stripped paths.
|
|
79
|
+
The exe now stashes the stripped specs and splices their load paths back
|
|
80
|
+
at every app-less require site (Bundler-pinned paths still win after a
|
|
81
|
+
boot). Verified through the real binstub in an empty directory, in an
|
|
82
|
+
app whose Gemfile does not include the gem, and on the boot-failed
|
|
83
|
+
static fallback.
|
|
84
|
+
- **Engine-mounted MCP survives mcp SDK 0.24.** The SDK's SSE writer now
|
|
85
|
+
calls `stream.flush` after every event, and
|
|
86
|
+
`ActionController::Live::Buffer` defines no `flush` - every SSE-mode
|
|
87
|
+
`tools/call` through `mount RailsAiContext::Engine` 500'd on all
|
|
88
|
+
supported Rails versions (the payload still arrived, then the request
|
|
89
|
+
died). The controller now hands the transport a flush-capable stream.
|
|
90
|
+
Verified with live curl sessions on booted Rails 7.1 and 8.1 apps.
|
|
91
|
+
- **Engine-mounted GET streams actually stay open.** The transport
|
|
92
|
+
registers the server-push stream and returns; Live then closed the
|
|
93
|
+
response as soon as the action returned, so the SSE channel died
|
|
94
|
+
instantly, `notifications/tools/list_changed` could never be delivered,
|
|
95
|
+
and (once held open) clients would have waited up to 30s for response
|
|
96
|
+
headers. The action now commits headers immediately with an SSE comment
|
|
97
|
+
and holds the thread until the transport or the client closes the
|
|
98
|
+
stream. SETUP.md documents the one-thread-per-connected-client cost.
|
|
99
|
+
- **Helpers keep their namespaces.** `app/helpers/admin/dashboard_helper.rb`
|
|
100
|
+
was reported as `DashboardHelper`, and looking it up by its real constant
|
|
101
|
+
name `Admin::DashboardHelper` failed. Module names now derive from the
|
|
102
|
+
path under `app/helpers` (`app/helpers/concerns` stays its own root,
|
|
103
|
+
matching the railties autoload glob), and exact path matches win before
|
|
104
|
+
basename fallbacks so a top-level helper is not shadowed by a namespaced
|
|
105
|
+
one.
|
|
106
|
+
- **`rails_validate` checks qualified render targets in Ruby files.**
|
|
107
|
+
`render partial: "posts/missing"` in a controller or service passed
|
|
108
|
+
`level:"rails"` silently (the partial check only ran for ERB). Bare
|
|
109
|
+
`render "posts/show"` keeps template semantics in Ruby files, so the
|
|
110
|
+
controller template-render idiom is not false-flagged. Tool descriptions
|
|
111
|
+
now state exactly which column references are checked
|
|
112
|
+
(validates/permit/callbacks).
|
|
113
|
+
- **`rails_review_changes` describes untracked files.** New files rendered
|
|
114
|
+
as empty headings (a fresh app right after `rails new` plus install
|
|
115
|
+
showed 20 of them); they now read `_new file, N lines_`, and only the
|
|
116
|
+
HEAD flow makes that claim (a committed-then-reverted file against an
|
|
117
|
+
older ref reads `_no diff available_`).
|
|
118
|
+
- **Views resource resolves extension-less paths.** Reading
|
|
119
|
+
`rails-ai-context://views/posts/index` now serves
|
|
120
|
+
`posts/index.html.erb`; a same-named directory no longer defeats the
|
|
121
|
+
lookup, and sensitive-file candidates are rejected so the
|
|
122
|
+
not-found/not-allowed message split cannot act as an existence oracle
|
|
123
|
+
for secrets placed under `app/views`.
|
|
124
|
+
- **Resource templates accept both URI schemes.** `rails://` and
|
|
125
|
+
`rails-ai-context://` now both resolve for the controllers, views, and
|
|
126
|
+
routes templates, and contents echo the URI the client requested.
|
|
127
|
+
- **`preset` output no longer scrambles under pipes** in either CLI:
|
|
128
|
+
all framing (banner, separators, "Running:" labels) goes to stderr and
|
|
129
|
+
stdout carries pure tool output.
|
|
130
|
+
- **Context files stop leaking internal tokens.** Architecture sections
|
|
131
|
+
showed raw keys (`concerns_models`, `pwa`, `solid_queue`) next to
|
|
132
|
+
humanized labels; every token the convention introspector emits now has
|
|
133
|
+
a label. Stale doc counts corrected across README and docs (39 tools,
|
|
134
|
+
25 doctor checks).
|
|
135
|
+
|
|
8
136
|
## [5.16.1] - 2026-07-12
|
|
9
137
|
|
|
10
138
|
### Fixed
|
data/CONTRIBUTING.md
CHANGED
|
@@ -20,7 +20,7 @@ The test suite uses [Combustion](https://github.com/pat/combustion) to boot a mi
|
|
|
20
20
|
lib/rails_ai_context/
|
|
21
21
|
├── cli/ # CLI tool runner (tool_runner.rb) - executes MCP tools from rake/Thor
|
|
22
22
|
├── introspectors/ # 39 introspectors (schema, models, routes, etc.)
|
|
23
|
-
├── tools/ #
|
|
23
|
+
├── tools/ # 39 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
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
<br>
|
|
22
22
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
23
23
|
[](https://github.com/crisnahine/rails-ai-context)
|
|
24
|
-
[](https://github.com/crisnahine/rails-ai-context/actions)
|
|
25
25
|
[](LICENSE)
|
|
26
26
|
|
|
27
27
|
</div>
|
|
@@ -245,7 +245,7 @@ rails 'ai:tool[stimulus]' controller=chart
|
|
|
245
245
|
|
|
246
246
|
<br>
|
|
247
247
|
|
|
248
|
-
##
|
|
248
|
+
## 39 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
|
|
|
@@ -509,7 +509,7 @@ empty listings.
|
|
|
509
509
|
|
|
510
510
|
## Build your own tools
|
|
511
511
|
|
|
512
|
-
Register custom MCP tools alongside the
|
|
512
|
+
Register custom MCP tools alongside the 39 built-in ones:
|
|
513
513
|
|
|
514
514
|
```ruby
|
|
515
515
|
# app/mcp_tools/rails_get_business_metrics.rb
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "delegate"
|
|
4
|
+
|
|
3
5
|
module RailsAiContext
|
|
4
6
|
# Rails controller for serving MCP over Streamable HTTP.
|
|
5
7
|
# Alternative to the Rack middleware - integrates with Rails routing,
|
|
@@ -16,10 +18,25 @@ module RailsAiContext
|
|
|
16
18
|
# supported Rails versions.
|
|
17
19
|
include ActionController::Live
|
|
18
20
|
|
|
21
|
+
# The MCP SDK's SSE writer calls stream.flush after every event, but
|
|
22
|
+
# Live's buffer writes straight through to the client and defines no
|
|
23
|
+
# flush - the NoMethodError would 500 the request after the payload was
|
|
24
|
+
# already delivered and tear down long-lived GET streams.
|
|
25
|
+
class FlushableStream < SimpleDelegator
|
|
26
|
+
def flush; end
|
|
27
|
+
end
|
|
28
|
+
|
|
19
29
|
def handle
|
|
20
30
|
status_code, rack_headers, body = self.class.mcp_transport.handle_request(request)
|
|
21
31
|
self.status = status_code
|
|
22
|
-
rack_headers.each
|
|
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
|
|
23
40
|
|
|
24
41
|
if body.respond_to?(:each)
|
|
25
42
|
# Plain enumerable body (initialize, errors, JSON mode): join to a
|
|
@@ -29,8 +46,25 @@ module RailsAiContext
|
|
|
29
46
|
body.close if body.respond_to?(:close)
|
|
30
47
|
self.response_body = chunks.join
|
|
31
48
|
elsif body.respond_to?(:call)
|
|
49
|
+
stream = response.stream
|
|
50
|
+
stream = FlushableStream.new(stream) unless stream.respond_to?(:flush)
|
|
32
51
|
begin
|
|
33
|
-
body.call(
|
|
52
|
+
body.call(stream)
|
|
53
|
+
# A GET opens the server-push channel: the transport registers the
|
|
54
|
+
# stream and returns, expecting it to outlive this call. Live closes
|
|
55
|
+
# the response when the action returns, so hold the thread until the
|
|
56
|
+
# transport's keepalive (or the client) closes the stream. Live also
|
|
57
|
+
# sends headers only on the first write - the transport writes
|
|
58
|
+
# nothing until its first keepalive ping, so commit with an SSE
|
|
59
|
+
# comment up front or clients sit waiting on headers.
|
|
60
|
+
if request.get?
|
|
61
|
+
begin
|
|
62
|
+
stream.write(": connected\n\n")
|
|
63
|
+
rescue IOError
|
|
64
|
+
nil
|
|
65
|
+
end
|
|
66
|
+
wait_for_stream_close
|
|
67
|
+
end
|
|
34
68
|
ensure
|
|
35
69
|
begin
|
|
36
70
|
response.stream.close
|
|
@@ -43,6 +77,14 @@ module RailsAiContext
|
|
|
43
77
|
end
|
|
44
78
|
end
|
|
45
79
|
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
def wait_for_stream_close
|
|
83
|
+
sleep 0.5 until response.stream.closed?
|
|
84
|
+
rescue IOError
|
|
85
|
+
nil
|
|
86
|
+
end
|
|
87
|
+
|
|
46
88
|
class << self
|
|
47
89
|
# Class-level memoization - transport persists across requests.
|
|
48
90
|
# Thread-safe: MCP::Server and transport are stateless for reads.
|
data/docs/ARCHITECTURE.md
CHANGED
|
@@ -25,12 +25,12 @@ graph TD
|
|
|
25
25
|
|
|
26
26
|
subgraph engine["Introspection Engine"]
|
|
27
27
|
direction LR
|
|
28
|
-
I["Introspectors\
|
|
29
|
-
AST["AST Engine\nPrism\
|
|
28
|
+
I["Introspectors\n39 modules\nPresets\nCached"]
|
|
29
|
+
AST["AST Engine\nPrism\n21 listeners\nConfidence tags"]
|
|
30
30
|
H["Hydration Layer\nSchema hints\ninjected into\ntool responses"]
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
engine --> R["Tool Registry\
|
|
33
|
+
engine --> R["Tool Registry\n39 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
|
|
43
|
+
CLI["CLI Runner\nRake / Thor\nSame 39 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 (
|
|
76
|
+
Cache->>App: introspect (39 modules)
|
|
77
77
|
App-->>Cache: structured data
|
|
78
78
|
Cache-->>TR: fresh context
|
|
79
79
|
end
|
|
@@ -115,8 +115,8 @@ The `Introspector` orchestrator runs configured introspectors and merges results
|
|
|
115
115
|
**Prism AST parsing** replaced all regex-based Ruby source parsing in v5.2.0.
|
|
116
116
|
|
|
117
117
|
- **AstCache** - Thread-safe parse cache (`Concurrent::Map`), keyed by path + SHA256 + mtime
|
|
118
|
-
- **SourceIntrospector** - Single-pass Prism Dispatcher walks the AST once,
|
|
119
|
-
- **
|
|
118
|
+
- **SourceIntrospector** - Single-pass Prism Dispatcher walks the AST once, feeding every registered listener simultaneously
|
|
119
|
+
- **21 Listeners** - Associations, Validations, Scopes, Enums, Callbacks, Macros and Methods are the default map for model analysis; the rest are used through targeted walks over schema dumps, migrations, Gemfiles, rake tasks and initializers
|
|
120
120
|
- **Confidence** - Every result carries `[VERIFIED]` (static literals) or `[INFERRED]` (dynamic expressions)
|
|
121
121
|
|
|
122
122
|
### Tool Registry (`lib/rails_ai_context/tools/base_tool.rb`)
|
|
@@ -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
|
|
195
|
+
- Same 39 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
|
|
218
|
+
2. **Read-only tools** - All 39 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
|
@@ -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
|
|
43
|
+
Run any of the 39 MCP tools from the terminal.
|
|
44
44
|
|
|
45
45
|
```bash
|
|
46
46
|
# Rake syntax
|
|
@@ -93,7 +93,7 @@ rails-ai-context context --format claude # Specific format
|
|
|
93
93
|
|
|
94
94
|
### `doctor`
|
|
95
95
|
|
|
96
|
-
Run
|
|
96
|
+
Run the diagnostic checks and report an AI readiness score.
|
|
97
97
|
|
|
98
98
|
```bash
|
|
99
99
|
rails ai:doctor
|
data/docs/CONFIGURATION.md
CHANGED
|
@@ -91,8 +91,8 @@ preset: full
|
|
|
91
91
|
| `excluded_models` | Array | 8 framework models | Models to skip during introspection |
|
|
92
92
|
| `excluded_controllers` | Array | 2 framework controllers | Controllers to skip |
|
|
93
93
|
| `excluded_route_prefixes` | Array | 6 framework prefixes | Route prefixes to skip |
|
|
94
|
-
| `excluded_filters` | Array |
|
|
95
|
-
| `excluded_middleware` | Array |
|
|
94
|
+
| `excluded_filters` | Array | 5 framework filters | Controller filters to skip |
|
|
95
|
+
| `excluded_middleware` | Array | 25 framework middleware | Middleware to skip in listing |
|
|
96
96
|
| `excluded_paths` | Array | `["node_modules", "tmp", "log", "vendor", ".git", "doc", "docs"]` | Paths excluded from search |
|
|
97
97
|
| `excluded_association_names` | Array | 7 framework associations | Association names to hide from model output |
|
|
98
98
|
| `excluded_concerns` | Array of Regex | Framework concerns | Concerns to skip (supports regex) |
|
|
@@ -116,6 +116,16 @@ preset: full
|
|
|
116
116
|
| `search_extensions` | Array | `["rb", "js", "erb", "yml", "yaml", "json", "ts", "tsx", "vue", "svelte", "haml", "slim"]` | File extensions to search |
|
|
117
117
|
| `concern_paths` | Array | `["app/models/concerns", "app/controllers/concerns"]` | Paths to scan for concerns |
|
|
118
118
|
| `frontend_paths` | Array | `nil` (auto-detect) | Override frontend file paths |
|
|
119
|
+
| `extra_app_paths` | Array | `[]` | Extra directories under the app root to treat as application code |
|
|
120
|
+
|
|
121
|
+
### Instrumentation
|
|
122
|
+
|
|
123
|
+
| Option | Type | Default | Description |
|
|
124
|
+
|:-------|:-----|:--------|:------------|
|
|
125
|
+
| `instrumentation_include_arguments` | Boolean | `false` | Include tool arguments in instrumentation events |
|
|
126
|
+
|
|
127
|
+
> [!WARNING]
|
|
128
|
+
> `instrumentation_include_arguments` forwards raw tool arguments to your subscribers, which for `rails_query` means the SQL text and for other tools can mean environment variable names. Leave it off unless your subscriber is as trusted as your logs.
|
|
119
129
|
|
|
120
130
|
### Database Query Safety
|
|
121
131
|
|
|
@@ -123,7 +133,8 @@ preset: full
|
|
|
123
133
|
|:-------|:-----|:--------|:-----------|:------------|
|
|
124
134
|
| `query_timeout` | Integer | `5` | - | SQL query timeout in seconds |
|
|
125
135
|
| `query_row_limit` | Integer | `100` | 1–1000 | Maximum rows returned |
|
|
126
|
-
| `query_redacted_columns` | Array |
|
|
136
|
+
| `query_redacted_columns` | Array | 14 patterns | - | Column names that cause a query to be rejected |
|
|
137
|
+
| `query_allowed_columns` | Array | `[]` | - | Column names exempted from the built-in sensitive list |
|
|
127
138
|
| `allow_query_in_production` | Boolean | `false` | - | Allow `rails_query` tool in production |
|
|
128
139
|
|
|
129
140
|
### Logs
|
|
@@ -136,7 +147,7 @@ preset: full
|
|
|
136
147
|
|
|
137
148
|
| Option | Type | Default | Description |
|
|
138
149
|
|:-------|:-----|:--------|:------------|
|
|
139
|
-
| `sensitive_patterns` | Array |
|
|
150
|
+
| `sensitive_patterns` | Array | 27 patterns | File patterns blocked from search/read (`.env*`, `*.key`, `*.pem`, `config/credentials.yml.enc`, `.ssh/*`, etc.) |
|
|
140
151
|
|
|
141
152
|
### Extensibility
|
|
142
153
|
|
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
|
|
5
|
+
**Build your own MCP tools that run alongside the 39 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
|
|
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.
|
|
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
|
|
28
|
+
3. **CLI** - Same 39 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
|
|
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.
|
|
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
|
|
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.
|
|
73
73
|
|
|
74
74
|
### Can I remove built-in tools?
|
|
75
75
|
|
data/docs/GUIDE.md
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
| Guide | Description |
|
|
19
19
|
|:------|:------------|
|
|
20
20
|
| [Quickstart](QUICKSTART.md) | Get running in 5 minutes |
|
|
21
|
-
| [Tools Reference](TOOLS.md) | All
|
|
21
|
+
| [Tools Reference](TOOLS.md) | All 39 MCP tools with parameters |
|
|
22
22
|
| [Recipes](RECIPES.md) | Real-world workflows and examples |
|
|
23
23
|
| [Custom Tools](CUSTOM_TOOLS.md) | Build your own MCP tools |
|
|
24
24
|
| [Configuration](CONFIGURATION.md) | Every config option |
|
|
@@ -283,7 +283,7 @@ rails ai:context:claude # Use this instead (no quoting needed)
|
|
|
283
283
|
|
|
284
284
|
## CLI Tools
|
|
285
285
|
|
|
286
|
-
All
|
|
286
|
+
All 39 MCP tools can be run directly from the terminal - no MCP server or AI client needed.
|
|
287
287
|
|
|
288
288
|
### Rake
|
|
289
289
|
|
|
@@ -349,7 +349,7 @@ The `tool_mode` is selected during `rails generate rails_ai_context:install`.
|
|
|
349
349
|
|
|
350
350
|
## MCP Tools - Full Reference
|
|
351
351
|
|
|
352
|
-
All
|
|
352
|
+
All 39 tools are **read-only** and **idempotent** - they never modify your application or database.
|
|
353
353
|
|
|
354
354
|
### rails_get_schema
|
|
355
355
|
|
|
@@ -1169,7 +1169,7 @@ if defined?(RailsAiContext)
|
|
|
1169
1169
|
end
|
|
1170
1170
|
```
|
|
1171
1171
|
|
|
1172
|
-
Both transports are **read-only** - they expose the same
|
|
1172
|
+
Both transports are **read-only** - they expose the same 39 tools and never modify your app.
|
|
1173
1173
|
|
|
1174
1174
|
### Controller Transport (Alternative)
|
|
1175
1175
|
|
data/docs/INTROSPECTORS.md
CHANGED
|
@@ -94,7 +94,6 @@ end
|
|
|
94
94
|
|
|
95
95
|
| Introspector | Key | What it extracts |
|
|
96
96
|
|:-------------|:----|:-----------------|
|
|
97
|
-
| SourceIntrospector | `:source` | Prism AST: associations, validations, scopes, enums, callbacks, macros, methods |
|
|
98
97
|
| MigrationIntrospector | `:migrations` | Migration files, versions, reversibility |
|
|
99
98
|
| SeedsIntrospector | `:seeds` | Seed file analysis |
|
|
100
99
|
| DatabaseStatsIntrospector | `:database_stats` | Table sizes, row counts, index stats |
|
|
@@ -170,9 +169,11 @@ These introspectors map directly onto [`RAILS_NERVOUS_SYSTEM.md`](../RAILS_NERVO
|
|
|
170
169
|
|
|
171
170
|
## AST-based introspection
|
|
172
171
|
|
|
173
|
-
The **SourceIntrospector** uses Prism AST parsing for model analysis. It
|
|
172
|
+
The **SourceIntrospector** uses Prism AST parsing for model analysis. It is infrastructure shared by the introspectors above rather than an introspector you can enable: there is no `:source` key for `config.introspectors`.
|
|
174
173
|
|
|
175
|
-
|
|
174
|
+
It runs a single-pass Dispatcher that walks the AST once and feeds events to all registered listeners simultaneously. Model analysis uses the seven below by default; the other fourteen listener classes are used through targeted walks (schema dumps, migrations, Gemfiles, rake tasks, initializers, and so on), for 21 in total.
|
|
175
|
+
|
|
176
|
+
### The 7 default Prism listeners
|
|
176
177
|
|
|
177
178
|
| Listener | What it detects |
|
|
178
179
|
|:---------|:---------------|
|
data/docs/QUICKSTART.md
CHANGED
data/docs/SECURITY.md
CHANGED
|
@@ -11,11 +11,11 @@
|
|
|
11
11
|
---
|
|
12
12
|
|
|
13
13
|
> [!CAUTION]
|
|
14
|
-
> This gem is designed for **development environments**. The query tool is disabled in production by default. Sensitive files are blocked. All
|
|
14
|
+
> This gem is designed for **development environments**. The query tool is disabled in production by default. Sensitive files are blocked. All 39 tools are read-only.
|
|
15
15
|
|
|
16
16
|
## Design principles
|
|
17
17
|
|
|
18
|
-
1. **Read-only by design** - All
|
|
18
|
+
1. **Read-only by design** - All 39 tools are annotated as non-destructive in the MCP protocol
|
|
19
19
|
2. **Defense in depth** - Multiple security layers, not single points of failure
|
|
20
20
|
3. **Sensitive data blocking** - Configurable patterns prevent access to secrets
|
|
21
21
|
4. **Offline by default** - No network calls except optional `rails_search_docs` with `fetch: true`
|
|
@@ -33,8 +33,9 @@ flowchart LR
|
|
|
33
33
|
L1 -->|"Blocked:\nINSERT, DROP,\nUNION SELECT..."| R1[Rejected]
|
|
34
34
|
L1 -->|SELECT only| L2{Layer 2\nDatabase Read-Only}
|
|
35
35
|
L2 -->|"SET TRANSACTION\nREAD ONLY\n+ timeout"| L3{Layer 3\nRow Limit}
|
|
36
|
-
L3 -->|"Cap: 1000 rows\nDefault: 100"| L4{Layer 4\
|
|
37
|
-
L4 -->|"password_digest
|
|
36
|
+
L3 -->|"Cap: 1000 rows\nDefault: 100"| L4{Layer 4\nSensitive Columns}
|
|
37
|
+
L4 -->|"names password_digest,\napi_key, ..."| R1
|
|
38
|
+
L4 -->|"no sensitive column"| OK[Safe Result]
|
|
38
39
|
|
|
39
40
|
style R1 fill:#e74c3c,stroke:#c0392b,color:#fff
|
|
40
41
|
style OK fill:#27ae60,stroke:#1e8449,color:#fff
|
|
@@ -75,13 +76,19 @@ All queries execute inside a transaction, then rollback (even if they could writ
|
|
|
75
76
|
- Configurable: `config.query_row_limit` (hard cap: 1000)
|
|
76
77
|
- Applied as `LIMIT` clause appended to query
|
|
77
78
|
|
|
78
|
-
### Layer 4 -
|
|
79
|
+
### Layer 4 - Sensitive column rejection
|
|
79
80
|
|
|
80
|
-
|
|
81
|
+
A query that names a sensitive column is **rejected before execution**, not redacted after it:
|
|
81
82
|
|
|
82
83
|
**Default redacted patterns:** `password_digest`, `encrypted_password`, `password_hash`, `reset_password_token`, `confirmation_token`, `unlock_token`, `otp_secret`, `session_data`, `secret_key`, `api_key`, `api_secret`, `access_token`, `refresh_token`, `jti`
|
|
83
84
|
|
|
84
|
-
|
|
85
|
+
Matching is by name, case-insensitive and word-bounded, against both the defaults above and `config.query_redacted_columns`. `SELECT password_digest AS pd FROM users` is blocked outright: post-execution redaction reads the column names the database returns, which the caller controls through aliases and expressions, so it cannot be relied on.
|
|
86
|
+
|
|
87
|
+
If one of your own columns merely looks sensitive (an `oauth_applications.secret`, say), exempt it by name:
|
|
88
|
+
|
|
89
|
+
```ruby
|
|
90
|
+
config.query_allowed_columns = %w[secret]
|
|
91
|
+
```
|
|
85
92
|
|
|
86
93
|
### Environment guard
|
|
87
94
|
|
|
@@ -96,15 +103,16 @@ The `rails_search_code` and file-reading tools block access to sensitive files:
|
|
|
96
103
|
|
|
97
104
|
### Default patterns
|
|
98
105
|
|
|
99
|
-
```
|
|
100
|
-
.env
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
106
|
+
```text
|
|
107
|
+
.env .env.*
|
|
108
|
+
config/master.key
|
|
109
|
+
config/credentials.yml.enc config/credentials/*.yml.enc
|
|
110
|
+
config/database.yml config/secrets.yml
|
|
111
|
+
config/cable.yml config/storage.yml
|
|
112
|
+
config/mongoid.yml config/redis.yml
|
|
113
|
+
*.pem *.key *.p12 *.pfx *.jks *.keystore
|
|
114
|
+
**/id_rsa **/id_ed25519 **/id_ecdsa **/id_dsa
|
|
115
|
+
.ssh/* .aws/credentials .aws/config .netrc .pgpass .my.cnf
|
|
108
116
|
```
|
|
109
117
|
|
|
110
118
|
### AI context file exclusions
|
|
@@ -158,7 +166,7 @@ File type parameters accept only alphanumeric characters.
|
|
|
158
166
|
|
|
159
167
|
## Regex injection prevention
|
|
160
168
|
|
|
161
|
-
|
|
169
|
+
On Ruby 3.2 and newer, user-supplied regex patterns have a 1-second timeout:
|
|
162
170
|
|
|
163
171
|
```ruby
|
|
164
172
|
Regexp.new(pattern, timeout: 1)
|
|
@@ -166,6 +174,9 @@ Regexp.new(pattern, timeout: 1)
|
|
|
166
174
|
|
|
167
175
|
Complex patterns that would cause catastrophic backtracking raise `RegexpError` instead of hanging.
|
|
168
176
|
|
|
177
|
+
> [!WARNING]
|
|
178
|
+
> `Regexp.timeout` does not exist on Ruby 3.1, which this gem still supports. There the timeout is skipped, and a pattern crafted to backtrack catastrophically can hang the process serving the tool. If you expose `rails_search_code` to input you do not control, run it on Ruby 3.2 or newer.
|
|
179
|
+
|
|
169
180
|
---
|
|
170
181
|
|
|
171
182
|
## Safe file reading
|
|
@@ -207,7 +218,7 @@ When using HTTP transport (Rack middleware or McpController):
|
|
|
207
218
|
|
|
208
219
|
- **Default bind**: `127.0.0.1` (localhost only - not exposed to network)
|
|
209
220
|
- **`auto_mount` is `false` by default** - must be explicitly enabled
|
|
210
|
-
- **Doctor checks**
|
|
221
|
+
- **Doctor checks** fail if `auto_mount` is true in production, and report it as enabled elsewhere
|
|
211
222
|
|
|
212
223
|
The McpController uses thread-safe transport initialization with mutex synchronization.
|
|
213
224
|
|