rails-ai-context 5.35.0 → 5.35.2

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 (123) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +1024 -0
  3. data/CONTEXT.md +37 -7
  4. data/CONTRIBUTING.md +30 -1
  5. data/README.md +9 -9
  6. data/SECURITY.md +2 -2
  7. data/docs/ARCHITECTURE.md +12 -4
  8. data/docs/CLI.md +44 -15
  9. data/docs/COMPATIBILITY.md +34 -8
  10. data/docs/CONFIGURATION.md +28 -5
  11. data/docs/CUSTOM_TOOLS.md +8 -2
  12. data/docs/FAQ.md +5 -5
  13. data/docs/GUIDE.md +33 -65
  14. data/docs/INTROSPECTORS.md +4 -4
  15. data/docs/QUICKSTART.md +1 -1
  16. data/docs/RECIPES.md +5 -5
  17. data/docs/RELEASING.md +3 -1
  18. data/docs/SECURITY.md +255 -74
  19. data/docs/SETUP.md +1 -1
  20. data/docs/STANDALONE.md +2 -2
  21. data/docs/TOOLS.md +151 -90
  22. data/docs/TROUBLESHOOTING.md +123 -8
  23. data/docs/_config.yml +1 -1
  24. data/docs/adr/0008-query-runs-only-where-it-can-guard.md +38 -0
  25. data/docs/adr/0009-dbhub-shapes-in-ruby.md +26 -0
  26. data/docs/adr/0010-one-config-tool-with-sections.md +32 -0
  27. data/docs/index.md +2 -2
  28. data/exe/rails-ai-context +24 -9
  29. data/lib/generators/rails_ai_context/install/install_generator.rb +2 -0
  30. data/lib/rails_ai_context/app_databases.rb +377 -0
  31. data/lib/rails_ai_context/app_kind.rb +2 -0
  32. data/lib/rails_ai_context/cli/app_root.rb +1 -0
  33. data/lib/rails_ai_context/cli/tool_runner.rb +110 -99
  34. data/lib/rails_ai_context/configuration.rb +15 -8
  35. data/lib/rails_ai_context/database_family/mysql_runtime.rb +170 -0
  36. data/lib/rails_ai_context/database_family/postgresql_plan.rb +124 -0
  37. data/lib/rails_ai_context/database_family/postgresql_runtime.rb +211 -0
  38. data/lib/rails_ai_context/database_family/sqlite_runtime.rb +391 -0
  39. data/lib/rails_ai_context/database_family.rb +282 -0
  40. data/lib/rails_ai_context/database_file.rb +61 -7
  41. data/lib/rails_ai_context/database_yml.rb +7 -29
  42. data/lib/rails_ai_context/doctor.rb +15 -16
  43. data/lib/rails_ai_context/instrumentation.rb +1 -1
  44. data/lib/rails_ai_context/introspectors/connection_pool_introspector.rb +8 -23
  45. data/lib/rails_ai_context/introspectors/database_stats_introspector.rb +6 -70
  46. data/lib/rails_ai_context/introspectors/declared_constant.rb +19 -7
  47. data/lib/rails_ai_context/introspectors/env_introspector.rb +1 -1
  48. data/lib/rails_ai_context/introspectors/env_references.rb +1 -1
  49. data/lib/rails_ai_context/introspectors/job_introspector.rb +7 -7
  50. data/lib/rails_ai_context/introspectors/migration_introspector.rb +8 -11
  51. data/lib/rails_ai_context/introspectors/migration_replay/statements.rb +9 -9
  52. data/lib/rails_ai_context/introspectors/model_introspector.rb +33 -39
  53. data/lib/rails_ai_context/introspectors/multi_database_introspector.rb +33 -30
  54. data/lib/rails_ai_context/introspectors/node_source.rb +2 -0
  55. data/lib/rails_ai_context/introspectors/performance_introspector.rb +80 -24
  56. data/lib/rails_ai_context/introspectors/pg_partitions.rb +1 -4
  57. data/lib/rails_ai_context/introspectors/schema_conventions.rb +6 -8
  58. data/lib/rails_ai_context/introspectors/schema_dump_path.rb +12 -14
  59. data/lib/rails_ai_context/introspectors/schema_introspector.rb +47 -27
  60. data/lib/rails_ai_context/introspectors/schema_reader.rb +1 -1
  61. data/lib/rails_ai_context/introspectors/source_scan.rb +39 -4
  62. data/lib/rails_ai_context/migration_status.rb +18 -46
  63. data/lib/rails_ai_context/payload.rb +48 -1
  64. data/lib/rails_ai_context/pending_migrations.rb +3 -4
  65. data/lib/rails_ai_context/run_cache.rb +5 -0
  66. data/lib/rails_ai_context/schema_adapter.rb +13 -18
  67. data/lib/rails_ai_context/secret_columns.rb +506 -0
  68. data/lib/rails_ai_context/serializers/claude_rules_serializer.rb +2 -2
  69. data/lib/rails_ai_context/serializers/markdown_serializer.rb +14 -10
  70. data/lib/rails_ai_context/serializers/section_facts.rb +12 -2
  71. data/lib/rails_ai_context/serializers/stack_overview_helper.rb +5 -2
  72. data/lib/rails_ai_context/serializers/tool_guide_helper.rb +35 -7
  73. data/lib/rails_ai_context/server.rb +143 -29
  74. data/lib/rails_ai_context/sql_gate.rb +1007 -0
  75. data/lib/rails_ai_context/tasks/rails_ai_context.rake +1 -2
  76. data/lib/rails_ai_context/test_helper.rb +3 -1
  77. data/lib/rails_ai_context/tools/analyze_feature.rb +11 -7
  78. data/lib/rails_ai_context/tools/base_tool.rb +71 -48
  79. data/lib/rails_ai_context/tools/dependency_graph.rb +1 -1
  80. data/lib/rails_ai_context/tools/diagnose.rb +105 -42
  81. data/lib/rails_ai_context/tools/generate_test.rb +4 -6
  82. data/lib/rails_ai_context/tools/get_active_support.rb +1 -6
  83. data/lib/rails_ai_context/tools/get_api.rb +1 -6
  84. data/lib/rails_ai_context/tools/get_autoload.rb +1 -6
  85. data/lib/rails_ai_context/tools/get_callbacks.rb +5 -5
  86. data/lib/rails_ai_context/tools/get_component_catalog.rb +6 -7
  87. data/lib/rails_ai_context/tools/get_concern.rb +6 -6
  88. data/lib/rails_ai_context/tools/get_config.rb +189 -12
  89. data/lib/rails_ai_context/tools/get_context.rb +111 -105
  90. data/lib/rails_ai_context/tools/get_controllers.rb +8 -5
  91. data/lib/rails_ai_context/tools/get_conventions.rb +1 -6
  92. data/lib/rails_ai_context/tools/get_engines.rb +1 -6
  93. data/lib/rails_ai_context/tools/get_env.rb +1 -6
  94. data/lib/rails_ai_context/tools/get_env_config.rb +2 -8
  95. data/lib/rails_ai_context/tools/get_frontend_stack.rb +1 -6
  96. data/lib/rails_ai_context/tools/get_gems.rb +1 -6
  97. data/lib/rails_ai_context/tools/get_helper_methods.rb +4 -5
  98. data/lib/rails_ai_context/tools/get_i18n.rb +1 -7
  99. data/lib/rails_ai_context/tools/get_job_pattern.rb +1 -1
  100. data/lib/rails_ai_context/tools/get_mailers.rb +1 -1
  101. data/lib/rails_ai_context/tools/get_model_details.rb +5 -8
  102. data/lib/rails_ai_context/tools/get_partial_interface.rb +3 -4
  103. data/lib/rails_ai_context/tools/get_schema.rb +10 -8
  104. data/lib/rails_ai_context/tools/get_service_pattern.rb +60 -24
  105. data/lib/rails_ai_context/tools/get_turbo_map.rb +1 -1
  106. data/lib/rails_ai_context/tools/migration_advisor.rb +47 -32
  107. data/lib/rails_ai_context/tools/onboard.rb +24 -24
  108. data/lib/rails_ai_context/tools/performance_check.rb +15 -2
  109. data/lib/rails_ai_context/tools/query.rb +142 -1356
  110. data/lib/rails_ai_context/tools/read_logs.rb +1 -1
  111. data/lib/rails_ai_context/tools/review_changes.rb +6 -7
  112. data/lib/rails_ai_context/tools/runtime_info.rb +125 -88
  113. data/lib/rails_ai_context/tools/safe_call.rb +70 -28
  114. data/lib/rails_ai_context/tools/search_code.rb +0 -4
  115. data/lib/rails_ai_context/tools/search_docs.rb +3 -1
  116. data/lib/rails_ai_context/tools/security_scan.rb +9 -5
  117. data/lib/rails_ai_context/tools/session_context.rb +18 -12
  118. data/lib/rails_ai_context/tools/validate.rb +1 -0
  119. data/lib/rails_ai_context/tools/validate_semantics.rb +5 -5
  120. data/lib/rails_ai_context/version.rb +1 -1
  121. data/lib/rails_ai_context.rb +4 -7
  122. data/server.json +2 -2
  123. metadata +14 -3
data/docs/TOOLS.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  # MCP Tools Reference
4
4
 
5
- **All 45 read-only tools, with every parameter.**
5
+ **All 35 read-only tools, with every parameter.**
6
6
 
7
7
  [Quickstart](QUICKSTART.md) · [Recipes](RECIPES.md) · [Custom Tools](CUSTOM_TOOLS.md) · [CLI Reference](CLI.md)
8
8
 
@@ -59,9 +59,9 @@ Most lookup tools accept a **`detail`** parameter: `summary` (compact), `standar
59
59
  | [Understand](#understand) | `analyze_feature`, `get_context`, `onboard` |
60
60
  | [Schema & Models](#schema--models) | `get_schema`, `get_model_details`, `get_callbacks`, `get_concern` |
61
61
  | [Controllers & Routes](#controllers--routes) | `get_controllers`, `get_routes` |
62
- | [Views & Frontend](#views--frontend) | `get_view`, `get_stimulus`, `get_partial_interface`, `get_turbo_map`, `get_frontend_stack` |
62
+ | [Views & Frontend](#views--frontend) | `get_view`, `get_stimulus`, `get_partial_interface`, `get_turbo_map` |
63
63
  | [Testing & Quality](#testing--quality) | `get_test_info`, `generate_test`, `validate`, `security_scan`, `performance_check` |
64
- | [App Config & Services](#app-config--services) | `get_api`, `get_conventions`, `get_config`, `get_gems`, `get_env`, `get_helper_methods`, `get_service_pattern`, `get_job_pattern`, `get_component_catalog`, `get_i18n`, `get_mailers`, `get_engines`, `get_autoload`, `get_active_support`, `get_env_config` |
64
+ | [App Config & Services](#app-config--services) | `get_config`, `get_helper_methods`, `get_service_pattern`, `get_job_pattern`, `get_component_catalog`, `get_mailers` |
65
65
  | [Data & Debugging](#data--debugging) | `dependency_graph`, `migration_advisor`, `search_docs`, `query`, `read_logs`, `diagnose`, `review_changes`, `runtime_info`, `session_context` |
66
66
 
67
67
  <p align="right"><a href="#table-of-contents">↑ back to top</a></p>
@@ -131,23 +131,33 @@ Composite context: schema + model + controller + routes + views for a resource.
131
131
  | `action` | string | - | Specific controller action; requires `controller` |
132
132
  | `model` | string | - | Model name (e.g., `Post`) |
133
133
  | `feature` | string | - | Feature keyword (e.g., `post`) |
134
- | `include` | array | - | Extra context to add: `stimulus`, `turbo`, `services`, `jobs`, `conventions`, `helpers`, `env`, `callbacks` |
134
+ | `include` | array | - | Extra context to add: `stimulus`, `turbo`, `services`, `jobs`, `conventions`, `helpers`, `env`, `callbacks`, `tests`, `config`, `gems`, `security`. A key is read in any capitalisation, and a key given twice runs once |
135
+
136
+ A model, controller or feature keyword the app does not have is answered as not found, with the closest name where there is one. An `action` the controller does not have is answered "Action 'nope' not found in PostsController. Available: create, destroy, ...", also when `skip_tools` lists `rails_get_controllers`. With `rails_analyze_feature` turned off, a `feature` keyword that only model names hold is answered with them: "The model names that hold 'comment': Comment." An include whose tool `skip_tools` turns off is answered by saying so, and that tool does not run.
137
+
138
+ An unexpected failure is an error response that starts "Tool rails_get_context failed:". A crash inside one section is an error response too: `isError` over MCP and exit 1 on the CLI. The text keeps every section, and the crashed one reads "Tool <name> failed: <Class>: <message>". A section that answers an error for another reason, such as a refusal or a failed Brakeman run, stays a section of an ordinary answer. `rails_diagnose` and `rails_review_changes` answer a crashed section the same way.
135
139
 
136
140
  ### `rails_onboard`
137
141
 
138
142
  Narrative app walkthrough for getting up to speed. The stack line counts the
139
143
  tables of every database the app's own schema dumps declare, as
140
144
  `rails_get_schema` reads them; the databases Rails 8 gives Solid Queue, Solid
141
- Cache and Solid Cable get one line, without their tables. It ends with the app's custom
142
- rake tasks (Rakefile, `lib/tasks`, `rakelib`), each with its arguments,
143
- description and file: the first 15 at `standard`, every one at `full`.
145
+ Cache and Solid Cable get one line, without their tables. A database whose
146
+ dump was not read is named with "(tables not read)" and left out of the table
147
+ count, and the summary says so: "5 tables in 2 databases, the warehouse
148
+ database not read". It ends with the app's custom rake tasks (Rakefile,
149
+ `lib/tasks`, `rakelib`), each with its arguments, description and file: the
150
+ first 15 at `standard`, every one at `full`. A known limit: on
151
+ `preset: standard` on the static tier the stack line reads the primary's
152
+ adapter as `unknown`, and a database's heading in the generated files can name
153
+ no adapter.
144
154
  Then the app's own generators under `lib/generators` (the `bin/rails generate`
145
155
  command and its USAGE line), the `lib/templates` files that replace a built-in
146
156
  generator's template, and the Railties under `lib/` with their initializers.
147
157
 
148
158
  | Parameter | Type | Default | Description |
149
159
  |:----------|:-----|:--------|:------------|
150
- | `detail` | enum | `standard` | `quick`, `standard`, `full` |
160
+ | `detail` | enum | `standard` | `summary` (one-paragraph overview), `standard`, `full` |
151
161
 
152
162
  <p align="right"><a href="#table-of-contents">↑ back to top</a></p>
153
163
 
@@ -162,6 +172,23 @@ a table `db/schema.rb` declares and the connected database does not have is
162
172
  named as a migration that has not run, and the listing header says when the
163
173
  two table counts disagree.
164
174
 
175
+ The other databases are listed after the primary's tables: every database any
176
+ environment in `config/database.yml` configures, so a Rails 8 app's queue,
177
+ cache and cable dumps are read even where only production configures them,
178
+ then any committed dump no environment names. A dump too large to read, or
179
+ holding no tables, is named with the reason. A secondary's dump is read from
180
+ its own path, else from the other format's file under its name
181
+ (`db/queue_schema.rb` or `db/queue_structure.sql`), whatever `database_tasks`
182
+ or `schema_format` says.
183
+
184
+ A database only another environment configures carries a mark, as in
185
+ `cache (production only)`, and a first database that is not named `primary`
186
+ is listed under its own name where another database is the one named
187
+ `primary`. Both are the same under every `preset`. With the
188
+ `multi_database` introspector off, as on `preset: standard`, `rails_get_schema`,
189
+ `rails_onboard`, `rails_performance_check` and the generated files read them
190
+ from the app databases list.
191
+
165
192
  Column hints follow the leftmost-prefix rule: `[indexed]` means some index leads
166
193
  with the column, so a lookup on it alone can use one; `[unique]` means a unique
167
194
  index covers the column alone; `[unique with x]` names the partners of a
@@ -363,14 +390,6 @@ Turbo Stream broadcast-to-subscription wiring with mismatch warnings. Streams ar
363
390
  | `stream` | string | - | Filter by stream or channel name (e.g., `notifications`); a name no broadcast or subscription carries says so and lists the app's streams |
364
391
  | `controller` | string | - | Filter by controller name (e.g., `messages`) |
365
392
 
366
- ### `rails_get_frontend_stack`
367
-
368
- Auto-detects React/Vue/Svelte/Angular, Hotwire, TypeScript, Vite/Shakapacker, package manager, monorepo layout.
369
-
370
- | Parameter | Type | Default | Description |
371
- |:----------|:-----|:--------|:------------|
372
- | `detail` | enum | `standard` | `summary`, `standard`, `full` |
373
-
374
393
  <p align="right"><a href="#table-of-contents">↑ back to top</a></p>
375
394
 
376
395
  ---
@@ -415,7 +434,7 @@ JavaScript (`.js`, `.mjs`, `.cjs`) is parsed by `node --check` as the module sys
415
434
 
416
435
  ### `rails_security_scan`
417
436
 
418
- Brakeman static analysis: SQL injection, XSS, mass assignment, command injection.
437
+ Brakeman static analysis: SQL injection, XSS, mass assignment, command injection. A scan that fails, on an unknown check name for one, answers "Brakeman scan failed: ..." as an error response: `isError` over MCP and exit 1 on the CLI. A brakeman run from outside the bundle that started and failed is an error response too. One that could not start is an ordinary answer.
419
438
 
420
439
  | Parameter | Type | Default | Description |
421
440
  |:----------|:-----|:--------|:------------|
@@ -429,7 +448,9 @@ Brakeman static analysis: SQL injection, XSS, mass assignment, command injection
429
448
  > bundle and says so: the installed gem's own `bin/brakeman`, run by the app's
430
449
  > Ruby, not whatever `brakeman` is on PATH. When no scan can run, the answer
431
450
  > says which case it is: brakeman is nowhere on the machine, or it is
432
- > installed and produced no report.
451
+ > installed and produced no report. For the second it quotes what brakeman
452
+ > said, or says "it wrote no report and printed nothing" or "its report could
453
+ > not be read".
433
454
 
434
455
  ### `rails_performance_check`
435
456
 
@@ -455,6 +476,16 @@ not a risk. The inverse Rails sets on each record of
455
476
  the render passes as a literal (`show_seller: false`) is not read. ERB and
456
477
  jbuilder are read; Haml and Slim templates are not, and neither are serializers.
457
478
 
479
+ Missing indexes and counter caches are checked in every database's dump, and a
480
+ finding outside the primary names its database. A dump on disk under a
481
+ database's name is read whatever `database_tasks` or `schema_dump` says. A
482
+ database whose tables could not be read is listed under **Not checked** with
483
+ the reason: its dump is not on disk or is over `max_schema_file_size`, or its
484
+ `config/database.yml` entry turns `database_tasks` or `schema_dump` off and no
485
+ dump is on disk, or "`schema_dump` in its database.yml entry names no file this
486
+ check reads". A database another environment configures carries its mark:
487
+ `**Not checked:** warehouse (production only, db/warehouse_schema.rb is not on disk)`.
488
+
458
489
  | Parameter | Type | Default | Description |
459
490
  |:----------|:-----|:--------|:------------|
460
491
  | `model` | string | - | Filter results to one model |
@@ -467,27 +498,33 @@ jbuilder are read; Haml and Slim templates are not, and neither are serializers.
467
498
 
468
499
  ## App Config & Services
469
500
 
470
- ### `rails_get_api`
501
+ ### `rails_get_config`
471
502
 
472
- API layer: api_only mode, serialization strategy (Jbuilder, serializers), GraphQL, versioning, rate limiting, OpenAPI specs, CORS, pagination.
503
+ The app's configuration, one section per call. `section` picks it; the default, `app`, is what the tool answered before it took sections. Ten tools that each answered one section were folded in, and their old names still work: the CLI resolves `gems`, `rails_get_gems` and the rest to their section, and a call to an old name is answered from its section with a note naming the new call and the arguments the section reads. Under `tool_mode: :cli` the note is the command. A value goes into the note's call when it is a number, a boolean, or at most 80 characters of letters, digits, `_`, `.` and `-`. Any other value is named after the call: "_rails_get_i18n is now `rails_get_config(section:"i18n")`, with the same `locale`; call that instead._" A parameter the asked section does not read is named in the answer as ignored.
473
504
 
474
505
  | Parameter | Type | Default | Description |
475
506
  |:----------|:-----|:--------|:------------|
476
- | `detail` | enum | `standard` | `summary` (one-liner), `standard` (per-area breakdown), `full` (adds OpenAPI spec paths) |
507
+ | `section` | enum | `app` | `app`, `gems`, `frontend`, `api`, `engines`, `conventions`, `autoload`, `active_support`, `env`, `env_config`, `i18n` |
508
+ | `detail` | enum | `standard` | `summary`, `standard`, `full`. Read by `frontend`, `api`, `env` |
509
+ | `category` | enum | `all` | Read by `gems`. See below |
510
+ | `environment` | string | - | Read by `env_config`. One environment, by exact name (e.g. `production`) |
511
+ | `locale` | string | - | Read by `i18n`. One locale's files and coverage, by exact name |
512
+ | `offset` | integer | `0` | Read by `gems`, `env_config`, `i18n`. Skip this many entries |
513
+ | `limit` | integer | `50` | Read by `gems`, `env_config`, `i18n`. Max entries to return |
477
514
 
478
- ### `rails_get_conventions`
515
+ `skip_tools` turns one section off by its old tool's name (`rails_get_env` turns `env` off); `rails_get_config` turns off `app` alone. The tool leaves `tools/list` only when every section is off.
479
516
 
480
- Auth checks, flash messages, create action template, test patterns, and the admin resources ActiveAdmin, Administrate, Avo, Madmin and Trestle register (with ActiveAdmin's permitted params).
517
+ With `app` off, `section` is required: the published schema marks it required, the description says so, and a call with no `section` is answered with the sections that are on.
481
518
 
482
- *No parameters.*
519
+ A `section` the tool does not have, or one `skip_tools` turned off, is refused with the sections that are on. A call to an old name that sends a parameter no section reads is refused with that section's own parameters, and a refused call to an old name carries no note about the new call.
483
520
 
484
- ### `rails_get_config`
521
+ A custom tool named `rails_get_config` takes the built-in's place when `skip_tools` lists that name. The ten sections go with the built-in, so a retired name such as `rails_get_gems` is then an unknown tool.
485
522
 
486
- Database config, auth framework, assets, cache, queue, Action Cable. The `use` and `map` calls in `config.ru` are listed apart from the stack, since they run before Rails.
523
+ #### `app` (default)
487
524
 
488
- *No parameters.*
525
+ Database config, auth framework, assets, cache, queue, Action Cable. The `use` and `map` calls in `config.ru` are listed apart from the stack, since they run before Rails.
489
526
 
490
- ### `rails_get_gems`
527
+ #### `gems` (was `rails_get_gems`)
491
528
 
492
529
  Notable gems with versions, categories, and config file locations. A config
493
530
  location is printed only when the app has that file, and an initializer is
@@ -498,19 +535,47 @@ another gem brings in, or, for a framework `rails` pulls in, when
498
535
  config/application.rb loads it; a gem the lockfile holds only for another
499
536
  gem's own use is left out.
500
537
 
501
- | Parameter | Type | Default | Description |
502
- |:----------|:-----|:--------|:------------|
503
- | `category` | enum | `all` | `auth`, `jobs`, `frontend`, `api`, `database`, `files`, `testing`, `deploy`, `monitoring`, `admin`, `pagination`, `search`, `forms`, `server`, `notifications`, `validation`, `utilities`, `services`, `payments`, `all` |
504
- | `offset` | integer | `0` | Skip this many gems |
505
- | `limit` | integer | `50` | Max gems to return |
538
+ `category` takes: `auth`, `jobs`, `frontend`, `api`, `database`, `files`, `testing`, `deploy`, `monitoring`, `admin`, `pagination`, `search`, `forms`, `server`, `notifications`, `validation`, `utilities`, `services`, `payments`, `all`.
539
+
540
+ #### `frontend` (was `rails_get_frontend_stack`)
541
+
542
+ Auto-detects React/Vue/Svelte/Angular, Hotwire, TypeScript, Vite/Shakapacker, package manager, monorepo layout.
506
543
 
507
- ### `rails_get_env`
544
+ #### `api` (was `rails_get_api`)
545
+
546
+ API layer: api_only mode, serialization strategy (Jbuilder, serializers), GraphQL, versioning, rate limiting, OpenAPI specs, CORS, pagination.
547
+
548
+ #### `engines` (was `rails_get_engines`)
549
+
550
+ What `config/routes.rb` mounts - engines and plain Rack apps alike, with known-engine descriptions, each with the path it answers on when the source spells one out - the app's own in-repo engines, plugins and modules read from the tree, each with the models the model scan filed under its path, and loaded engine classes with route/model counts.
551
+
552
+ #### `conventions` (was `rails_get_conventions`)
553
+
554
+ Auth checks, flash messages, create action template, test patterns, and the admin resources ActiveAdmin, Administrate, Avo, Madmin and Trestle register (with ActiveAdmin's permitted params).
555
+
556
+ #### `autoload` (was `rails_get_autoload`)
557
+
558
+ Autoloading setup: Zeitwerk vs Classic mode, autoloaders with collapsed/ignored dirs, autoload/eager-load paths, custom inflections.
559
+
560
+ #### `active_support` (was `rails_get_active_support`)
561
+
562
+ ActiveSupport surface: concerns registry, deprecators, MessageVerifier/MessageEncryptor usage, the `ActiveSupport::Notifications` events the app subscribes to (`subscribe`, `monotonic_subscribe`, and a Subscriber's `attach_to`) with file and line, read from source in both tiers, tagged logging, subscribed `on_load` hooks, cache store. Validator classes living among the concerns are counted and listed apart from them, as `rails_get_concern` does.
563
+
564
+ #### `env` (was `rails_get_env`)
508
565
 
509
566
  Environment variables + credentials keys (values are never exposed). Scans `.rb`, `.rake`, ERB views and config YAML under `app`, `config` and `lib`, plus `config.ru`, `db/seeds.rb`, `db/seeds/` and the Ruby scripts in `bin/`. A tag on a YAML comment line is not read: ERB runs it, but what it writes goes with the comment. Config YAML matching `sensitive_patterns` (`config/database.yml`, `config/cable.yml`, `config/storage.yml` and the rest) is read only for the ENV names in its ERB tags, so a default there is listed as present and not read; credentials and keys are never read, and the answer says so. A default is read from a second argument or from a block whose one statement is a literal (`ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" }`). The env Kamal's `config/deploy.yml` gives the app container is listed too: secret names only, and clear values, except one holding a URL or a key-like token, or under a secret-named variable, which shows as hidden, and one an ERB tag sets, which says so. So are the setting keys in the config gem's `config/settings.yml` and `config/settings/<env>.yml` when the bundle has the config gem, and the attributes of each `Anyway::Config` class in `config/configs` or `app/configs` with the env name it reads (`PAYMENT_API_KEY`), never their values. A variable whose call sites pass different defaults is labelled as such rather than with one site's default; `detail:"full"` names each site's. A default is redacted by the rule a source literal gets: a credential format under any name, a URL's password, and under a secret-named variable a value that is not clearly something else, so an address, URL or hostname default prints as written.
510
567
 
511
- | Parameter | Type | Default | Description |
512
- |:----------|:-----|:--------|:------------|
513
- | `detail` | enum | `standard` | `summary`, `standard`, `full` |
568
+ #### `env_config` (was `rails_get_env_config`)
569
+
570
+ Per-environment configuration from `config/environments/*.rb`: notable toggles (`force_ssl`, `eager_load`, caching, log level, queue adapter, mailer delivery) and every config key each environment sets. A key assigned in more than one branch reports every value with its condition; booted, the running environment reports the value the app resolved. An
571
+ "Every environment" block above them lists the keys `config/application.rb`
572
+ sets (`config.x` included) and, for each `config_for(:name)`, the keys
573
+ `config/name.yml` gives the running environment, `shared` merged in, names
574
+ only.
575
+
576
+ #### `i18n` (was `rails_get_i18n`)
577
+
578
+ I18n setup: default/available locales, backend, locale files with key counts, per-locale coverage, fallbacks.
514
579
 
515
580
  ### `rails_get_helper_methods`
516
581
 
@@ -534,7 +599,11 @@ nested inside a `hash` filter shown under it. Callers are read from every
534
599
  autoloads, and the page says when the twenty-caller display cap or the scan's
535
600
  own file ceiling left the list partial. A module under
536
601
  `app/services/concerns/` is a concern rather than a service object, so
537
- `rails_get_concern` lists it and this tool does not.
602
+ `rails_get_concern` lists it and this tool does not. The listing counts
603
+ services, and when the service directories hold more Ruby files than that,
604
+ one line under the heading says how many it left out and of which kind: base
605
+ classes, concerns, modules with no entry point that other code mixes in,
606
+ mailers, and files too large to read.
538
607
 
539
608
  | Parameter | Type | Default | Description |
540
609
  |:----------|:-----|:--------|:------------|
@@ -584,16 +653,6 @@ ViewComponent/Phlex components: props, slots, previews, sidecar assets, usage ex
584
653
  | `offset` | integer | `0` | Skip this many components |
585
654
  | `limit` | integer | `50` | Max components to return |
586
655
 
587
- ### `rails_get_i18n`
588
-
589
- I18n setup: default/available locales, backend, locale files with key counts, per-locale coverage, fallbacks.
590
-
591
- | Parameter | Type | Default | Description |
592
- |:----------|:-----|:--------|:------------|
593
- | `locale` | string | - | Show only this locale's files and coverage, by exact name |
594
- | `offset` | integer | `0` | Pagination offset over locale files |
595
- | `limit` | integer | `50` | Max locale files to return |
596
-
597
656
  ### `rails_get_mailers`
598
657
 
599
658
  ActionMailer mailers: every mailer class with its delivery actions and delivery method. A mailer that declares no action of its own - one taking them from a gem base or a mixin, or one called through class methods - is listed with where its actions come from rather than left out. A base other mailers inherit from is not one: it leaves the listing, and a line above names the ones left out. Asked for by name, a base answers with what every mailer below it inherits - its layout, helpers, defaults, callbacks and mixins as the app wrote them, the methods it defines - and which mailers inherit it.
@@ -617,38 +676,6 @@ defines it), and each mailbox's processing callbacks.
617
676
  | `offset` | integer | `0` | Pagination offset |
618
677
  | `limit` | integer | `50` | Max mailers to return |
619
678
 
620
- ### `rails_get_engines`
621
-
622
- What `config/routes.rb` mounts - engines and plain Rack apps alike, with known-engine descriptions, each with the path it answers on when the source spells one out - the app's own in-repo engines, plugins and modules read from the tree, each with the models the model scan filed under its path, and loaded engine classes with route/model counts.
623
-
624
- *No parameters.*
625
-
626
- ### `rails_get_autoload`
627
-
628
- Autoloading setup: Zeitwerk vs Classic mode, autoloaders with collapsed/ignored dirs, autoload/eager-load paths, custom inflections.
629
-
630
- *No parameters.*
631
-
632
- ### `rails_get_active_support`
633
-
634
- ActiveSupport surface: concerns registry, deprecators, MessageVerifier/MessageEncryptor usage, the `ActiveSupport::Notifications` events the app subscribes to (`subscribe`, `monotonic_subscribe`, and a Subscriber's `attach_to`) with file and line, read from source in both tiers, tagged logging, subscribed `on_load` hooks, cache store. Validator classes living among the concerns are counted and listed apart from them, as `rails_get_concern` does.
635
-
636
- *No parameters.*
637
-
638
- ### `rails_get_env_config`
639
-
640
- Per-environment configuration from `config/environments/*.rb`: notable toggles (`force_ssl`, `eager_load`, caching, log level, queue adapter, mailer delivery) and every config key each environment sets. A key assigned in more than one branch reports every value with its condition; booted, the running environment reports the value the app resolved. An
641
- "Every environment" block above them lists the keys `config/application.rb`
642
- sets (`config.x` included) and, for each `config_for(:name)`, the keys
643
- `config/name.yml` gives the running environment, `shared` merged in, names
644
- only.
645
-
646
- | Parameter | Type | Default | Description |
647
- |:----------|:-----|:--------|:------------|
648
- | `environment` | string | - | One environment, by exact name (e.g. `production`) |
649
- | `offset` | integer | `0` | Skip this many config keys per environment |
650
- | `limit` | integer | `50` | Max config keys listed per environment |
651
-
652
679
  <p align="right"><a href="#table-of-contents">↑ back to top</a></p>
653
680
 
654
681
  ---
@@ -680,12 +707,14 @@ file already named what the generator would name this one (`rails generate
680
707
  migration` refuses that name), and a migration not yet run that already adds
681
708
  the column or creates the table when it runs (a `def down`, a `dir.down` or a
682
709
  `revert` block only undoes, so what it adds does not count). It also warns
683
- about a `create_table` for a
684
- table that exists and a `null: false` column with no default on a table with
685
- rows (the row count is the database's estimate), and refuses a column type no
686
- adapter knows. For `remove_column`, `rename_column` and `change_type` it lists
687
- the lines in `app/`, `lib/` and `config/` that name the column, the code the
688
- change breaks.
710
+ about a `create_table` for a table that exists and a `null: false` column with
711
+ no default on a table with rows. The row count is the primary database's
712
+ estimate, so a table on another database has no count and the warning says
713
+ "if `<table>` holds any rows". It refuses a column type no adapter knows. For
714
+ `remove_column`, `rename_column` and `change_type` it lists the lines in
715
+ `app/`, `lib/` and `config/` that name the column, the code the change breaks.
716
+ Index and lock advice follows the database family of the database that holds
717
+ the table.
689
718
 
690
719
  It refuses a `table`, `type` or `new_name` longer than 256 characters, a
691
720
  `column` that long outside `create_table`, and `options` longer than 1,024
@@ -714,7 +743,17 @@ Bundled topic index with weighted keyword search. Optional on-demand GitHub fetc
714
743
 
715
744
  ### `rails_query`
716
745
 
717
- Safe read-only SQL with layered security: regex validation, a PostgreSQL plan check (whole-row leaks, expanded sensitive columns and VOLATILE admin functions refused before anything runs), `SET TRANSACTION READ ONLY`, timeout, and column redaction. [Learn about the security model →](SECURITY.md)
746
+ Safe read-only SQL with layered security: the SQL gate, a PostgreSQL plan check (whole-row leaks, expanded secret columns and VOLATILE admin functions refused before anything runs), `SET TRANSACTION READ ONLY`, timeout, and column redaction. [Learn about the security model →](SECURITY.md)
747
+
748
+ It runs on PostgreSQL, MySQL (with MariaDB) and SQLite, each with its own read-only transaction and statement timeout. On any other adapter it answers with that adapter's name and runs nothing. A statement that runs past `query_timeout` is answered as a timeout and any other database error as an SQL error. Both are error responses: `isError` over MCP and a non-zero exit on the CLI. So is a database no connection reaches, which the answer names with what to do about it. So are a locked SQLite database and the refusal in production.
749
+
750
+ On PostgreSQL, MySQL and MariaDB every statement a call sends has a time limit. The query has `query_timeout` to itself and the reads beside it share a second one, so the statements of a call take about twice `query_timeout` at most. On SQLite the query has `query_timeout`, and the reads beside it run on the app's own connection with no statement limit. A SQLite statement that names something the app's connection alone holds, such as a temp table or an attached database, runs on that connection, and its answer says it ran without a time limit. The [security model](SECURITY.md#layer-2---database-level-read-only) says what bounds each statement and lists the stated limits.
751
+
752
+ On MySQL, MariaDB and SQLite a statement that names a view whose definition names a secret column is refused. So is a `*` inside a subquery when the statement names a table that holds a secret column, and on SQLite `IN` before a table's name (`x IN t`). On PostgreSQL a materialized view whose definition names a secret column is refused. On SQLite a virtual table that holds a secret column, a shadow table and the page modules are refused, and only the pragma functions that describe the schema or the database file run.
753
+
754
+ The secret column rule reads the columns of every schema and database a statement reaches: the ones its text names, on PostgreSQL the ones its plan names, and the ones the views it names lead to, three reads deep. So a secret column of a table in another one is refused by name too.
755
+
756
+ On MySQL and MariaDB the text checks read a backslash in a string both ways, and a statement that either reading refuses is refused. There a `[` outside a string or a quoted name, a user variable assignment (`:=`) and `LAST_INSERT_ID` with an argument are refused too. On every family the text checks give up with a refusal when they have run longer than `query_timeout`.
718
757
 
719
758
  | Parameter | Type | Default | Description |
720
759
  |:----------|:-----|:--------|:------------|
@@ -722,10 +761,25 @@ Safe read-only SQL with layered security: regex validation, a PostgreSQL plan ch
722
761
  | `limit` | integer | `100` | Maximum rows to return (hard cap: 1000) |
723
762
  | `format` | enum | `table` | `table`, `csv` |
724
763
  | `explain` | boolean | `false` | Show query plan instead of results |
764
+ | `database` | string | the primary | The app database to query, by its `config/database.yml` name (`queue`, `cache`). The secret column rule and the database family are that database's |
765
+
766
+ `database` is checked when the tool runs, against the databases the app has then:
767
+
768
+ - A name the app does not have is answered with the closest one and every name it has.
769
+ - A database only another environment configures, a replica and one with `database_tasks` off included, is answered with the environments that do.
770
+ - A top-level entry of `config/database.yml` that code connects to by name (`establish_connection(:reporting)`) is an environment to Rails, so `database` does not take its name: the answer lists every database the app has, as in "No database named reporting. This app has: primary, cache, queue".
771
+ - One that only a committed dump names is answered as such.
772
+ - On the static tier the whole tool is unavailable, with or without `database`.
773
+ - A SQLite database whose file is not there is an error, with or without `database`. The message starts "The <name> database does not exist yet" and names `bin/rails db:prepare`, and nothing is created. A `file:` URI is checked at the path inside it.
774
+ - A SQLite database other than the primary that lives in memory (`:memory:`, or a `file:` URI with `mode=memory`) is refused by name. It exists only inside the process that created it, and a new connection would open an empty one.
775
+ - A database whose columns cannot be read is not queried. The answer is "The columns of the <name> database could not be read, so its secret columns cannot be told and no query runs on it."
776
+ - When a statement or an `explain: true` fails on a table this database does not have and another database's schema holds it, the answer names that database and the `database` value to use. The pointer is built from the schema dumps, so a table on a database with no dump gets none.
777
+
778
+ Each database other than the primary is reached through a connection handler of its own for the length of the call, so the app's own connection pools are left as they are and two calls at once never share a connection.
725
779
 
726
780
  > Disabled in production by default.
727
781
 
728
- > Name the columns you need. A query that references a sensitive column (directly, or through an alias or expression), serialises a whole row, carries a column-alias list that renames a wildcard (a `VALUES`-body list such as `(VALUES (1,'a')) AS v(id, name)` is allowed - it renames no table column), uses UNION, or calls a session-effecting function is refused with a message that says why. `SELECT *` is allowed and its sensitive columns come back `[FILTERED]`. `SHOW CREATE TABLE` / `SHOW CREATE VIEW` are allowed and their definition text is secret-redacted; other `SHOW CREATE` forms are refused. In the default format a definition is shown whole, as the DDL in a fenced code block with its line breaks kept (a view's character set and collation follow as `name: value` lines); `csv` returns it as one quoted field. When the row limit holds rows back - only when the query really returned more than the cap - the answer says so in every format.
782
+ > Name the columns you need. A query is refused, with a message that says why, when it does one of these: it references a secret column (directly, or through an alias or expression; an `encrypts` column also when `query_allowed_columns` lists it); it serialises a whole row; it carries a column-alias list that renames a wildcard (a `VALUES`-body list such as `(VALUES (1,'a')) AS v(id, name)` is allowed, since it renames no table column); it uses UNION; it calls a session-effecting function, a function that returns a whole table as one value (`table_to_xml`), or any function by a quoted name; it reads a statistics, session or credential relation (`pg_stats`, `pg_stat_activity`, `pg_settings`, `performance_schema`, the `mysql` schema) or a PostgreSQL setting that can hold a password (`primary_conninfo`); it is a `SHOW` with a `WHERE` clause, `SHOW ALL` on PostgreSQL or `EXPLAIN FOR CONNECTION` on MySQL and MariaDB; or it is over 1,000,000 bytes long. `SELECT *` is allowed and its secret columns come back `[FILTERED]`. `SHOW CREATE TABLE` / `SHOW CREATE VIEW` are allowed and their definition text is secret-redacted; other `SHOW CREATE` forms are refused. In the default format a definition is shown whole, as the DDL in a fenced code block with its line breaks kept (a view's character set and collation follow as `name: value` lines); `csv` returns it as one quoted field. When the row limit holds rows back - only when the query really returned more than the cap - the answer says so in every format. With `csv`, a query that returns no rows answers the header row alone.
729
783
 
730
784
  > **Cell rendering by format.** A `markdown` table cell shows `NULL` as `_NULL_`, escapes a pipe (`a\|b`) so it cannot end the cell, renders a line break as the two characters `\n` so it cannot end the row, and shortens a value over 100 characters to `…` (a `SHOW CREATE` definition is not a table cell, so it is never shortened). A `csv` cell is the value itself: `NULL` is an empty field, nothing is escaped beyond RFC 4180 quoting (a field holding a comma, a quote or a line break is wrapped in quotes with its quotes doubled), and nothing is truncated - so a line break stays a real line break inside the quoted field. Binary (non-text) data shows as `[BLOB]` in both.
731
785
 
@@ -776,12 +830,19 @@ Each diff passes through the redaction every tool's source does, read as the fil
776
830
 
777
831
  Live database pool stats, table sizes, pending migrations, cache stats, queue depth.
778
832
 
779
- The pool and the cache are read inside the MCP server process, and the answer says so: the pool's counts are that process's own connections, and a `MemoryStore`'s entries are its own, since each app server process holds its own. A store the processes share (Redis, Memcached, Solid Cache, a file store) is the app's. Another environment that configures a shared store is named, as where the app's cache lives there.
833
+ Table sizes come from PostgreSQL, MySQL (base tables only) and SQLite, and index usage from PostgreSQL. On MySQL, MariaDB and SQLite the full detail says "_Index usage is not available for the MySQL database family._", or SQLite. On an adapter with no database family the database section says table sizes and index usage are not available for that adapter. A read that failed or took longer than `query_timeout` is said in one line: "_Table sizes were not read: the read failed or took longer than query_timeout._", and at `detail: "full"` "_Index usage was not read: the read failed or took longer than query_timeout._"
834
+
835
+ The pool and the cache are read inside the MCP server process, and the answer says so: the pool's counts are that process's own connections, and a `MemoryStore`'s entries are its own, since each app server process holds its own. A store the processes share (Redis, Memcached, Solid Cache, a file store) is the app's. Another environment that configures a shared store is named, as where the app's cache lives there. Solid Cache's stats are read from its database, so when that is a SQLite file that is not there, the cache section says the database does not exist yet and creates nothing.
780
836
 
781
837
  | Parameter | Type | Default | Description |
782
838
  |:----------|:-----|:--------|:------------|
783
839
  | `detail` | enum | `standard` | `summary`, `standard`, `full` |
784
840
  | `section` | enum | - | One section only: `database`, `cache`, `jobs`, `connections` |
841
+ | `database` | string | the primary | The app database whose adapter, table sizes, index usage and pending migrations the database section shows, named and checked as `rails_query`'s `database` is, whatever the `section`. A name that cannot be reached is an error for every section, and a section that does not read `database` says so |
842
+
843
+ With no `database`, pending migrations are read for every database the running environment configures and migrates, each from its own `schema_migrations`. With more than one, the line names the databases that were read (`**Migrations:** all up to date in primary, queue`). A database that could not be read gets a line of its own with the reason, `_Pending migrations could not be read for <name>: <reason>_`, and the others are still reported. An in-memory database is left out. Under a named database the section shows that database's own. For a replica, or a database whose `config/database.yml` entry turns `database_tasks` off, it says Rails runs no migrations on it. The connections section is always about the app's own pools.
844
+
845
+ A named database is an error whatever the `section` when it is not configured here, lives in memory outside the primary, has a SQLite file that is not there, has an adapter whose gem did not load, or has a server that does not answer. The last two answers name the database, say what stopped the connection and say what to do. With no `database`, a primary whose SQLite file is not there or that cannot be reached is a note in the Database section, and the other sections still answer. Nothing is created either way. On the static tier the whole tool is unavailable, with or without `database`.
785
846
 
786
847
  ### `rails_session_context`
787
848
 
@@ -790,7 +851,7 @@ Session-aware context tracking across tool calls within a conversation. Pass `ac
790
851
  | Parameter | Type | Default | Description |
791
852
  |:----------|:-----|:--------|:------------|
792
853
  | `action` | enum | - | `status` (queried tools with timestamps), `summary` (short recap), `reset` (clear this conversation's record) |
793
- | `mark` | string | - | Record a tool and its params as already queried (e.g., `get_schema:users`). The tool must be one the server has; a short name such as `schema` is taken |
854
+ | `mark` | string | - | Record a tool and its params as already queried (e.g., `get_schema:users`). The tool must be one the server has; a short name such as `schema` is taken. A retired name such as `get_gems` is recorded as `rails_get_config` with its section |
794
855
 
795
856
  Each conversation has its own record: over HTTP, one server process serves every client, and each client's `Mcp-Session-Id` keeps its calls, and its `reset`, to itself. A tool that answers by calling others (`rails_get_context`, `rails_diagnose`, `rails_review_changes`) is recorded as the one call the client made.
796
857
 
@@ -266,7 +266,7 @@ config.generate_root_files = false
266
266
 
267
267
  ### "rails_query is disabled in production"
268
268
 
269
- By design. Override if needed:
269
+ By design. The answer is an error response: `isError` over MCP and exit 1 from the CLI. Override if needed:
270
270
 
271
271
  ```ruby
272
272
  config.allow_query_in_production = true
@@ -274,21 +274,135 @@ config.allow_query_in_production = true
274
274
 
275
275
  ### "Blocked: contains INSERT" and other "Blocked:" messages
276
276
 
277
- The 4-layer SQL validator blocks write operations and injection patterns, and the message names what it hit. Ensure you're only running SELECT queries.
277
+ The SQL gate refuses write statements, file and admin functions and secret column names, and the message names what it hit. Only read statements run: SELECT, WITH, SHOW, EXPLAIN and DESCRIBE.
278
278
 
279
- Common false positives:
280
- - A blocked keyword inside a string (`WHERE note = 'please insert coin'`) → the validator matches words, not SQL structure, so the query is refused
281
- - Hash characters → a `#` starts a comment on MySQL only, and never inside a quoted string
282
- - JSONB operators (`#>>`) → preserved correctly since v5.6.0
279
+ What a string literal holds decides some of these:
280
+ - A write keyword or a semicolon inside a string (`WHERE note = 'please insert coin'`, `'a; b'`) is data on every database, and the query runs
281
+ - A write after a `WITH` clause (`WITH ... INSERT INTO ...`) is named as that write: "Blocked: contains INSERT"
282
+ - A secret column's name, `FOR UPDATE` / `FOR SHARE` or `UNION` inside a string is data on PostgreSQL only. On MySQL, MariaDB and SQLite the raw text decides and the query is refused, since no later check there would catch a quote the masking misread
283
+ - `INTO OUTFILE` / `INTO DUMPFILE` is refused wherever it stands, inside a string too, on every database
284
+ - A file or network function (`pg_read_file`, `LOAD_FILE`), a refused `SHOW` form (`SHOW GRANTS`) and a relation that is not read (`pg_stats`, `slow_log`) are refused wherever they stand, inside a string too, each on the database that has it. `slow_log` is refused on MySQL and MariaDB, and `pg_stats` on PostgreSQL
285
+ - A session or administrative function (`pg_terminate_backend` on PostgreSQL, `GET_LOCK` on MySQL and MariaDB) is refused wherever its call stands, so `'pg_terminate_backend(1)'` inside a string is refused too. Its bare name inside a string is data
286
+ - A `#` starts a comment on MySQL only, and only outside a quoted string
287
+ - A JSONB operator (`#>>`) runs as written (since v5.6.0)
288
+
289
+ ### "Blocked: a `*` inside a subquery", "Blocked: `TABLE` inside a subquery" and "Blocked: the view ... is not queried"
290
+
291
+ MySQL, MariaDB and SQLite trace no result column back to its table column, so these shapes are refused there when a secret column is in reach:
292
+
293
+ - A `*` inside a subquery, a CTE body included, when the statement names a table that holds a secret column. The message names that table and the column. The rule tests the two facts together and does not tell which table the wildcard reads, so the statement is refused also when the wildcard is over another table. Name the columns in the subquery: `WITH x AS (SELECT id, email FROM users) SELECT * FROM x`. On MySQL and MariaDB `(TABLE t)` counts as a wildcard: select the columns you need. `EXISTS (SELECT * ...)` and `count(*)` run
294
+ - On SQLite, `IN` before a table's name (`WHERE id IN users`), when the statement names a table that holds a secret column. It is SQLite's shorthand for `IN (SELECT * FROM users)`. Write the subquery with its columns named: `WHERE id IN (SELECT id FROM users)`
295
+ - On SQLite, either of those two in a statement that holds a string equal to such a table's name (`WHERE role = 'users'`). SQLite reads a string as a table's name where a name can stand, and the rule cannot tell that use from plain data. The message says "in a statement that holds the string 'users'". Name the columns in the subquery, or compare with another value
296
+ - A view whose definition names a secret column, or has a wildcard and names a table that holds one. Query the tables the view reads. When the column holds no secret, exempt it by name as [Column values show [FILTERED]](#column-values-show-filtered) says
297
+
298
+ On PostgreSQL a plain view runs and its secret columns come back `[FILTERED]`. A materialized view whose definition names a secret column gets the view message, since its stored rows cannot be traced to a table column. Query the tables it was built from.
299
+
300
+ ### "Blocked: ... is not read: it is a shadow table", "it is a virtual table that holds the secret column" and "it returns the database file's raw pages"
301
+
302
+ On SQLite `rails_query` reads no table that holds another table's rows or words under other names:
303
+
304
+ - `sqlite_dbpage`, `sqlite_dbdata` and `sqlite_dbptr` return the database file's pages, with every stored value in them
305
+ - A shadow table (`docs_content`, `docs_data`) holds what a virtual table stores, under other column names. The message names the virtual table to query
306
+ - A virtual table that holds a secret column is refused whole, since a search or a function over it reads a column by its position. Keep the secret column out of the virtual table, or query the table it was copied from
307
+ - An `fts5vocab` or `fts4aux` table over such a table lists its words, so it is refused too
308
+ - A virtual table whose definition names a refused view, or a table that is not read, holds their rows or words. The message says "its definition names" and what it names
309
+
310
+ A view over one of these is refused with "its definition names `<table>`, which is not read". The name is refused inside a string too.
311
+
312
+ ### "Blocked: `pragma_foreign_keys` tells of the connection that runs it"
313
+
314
+ On SQLite `rails_query` runs a statement on a connection of its own, so a value that belongs to a connection would not be the app's. The pragma functions that describe the schema or the database file run: `pragma_table_info`, `pragma_index_list`, `pragma_foreign_key_list`, `pragma_integrity_check`, `pragma_page_count` and the others [SECURITY.md](SECURITY.md#layer-1---the-sql-gate-statement-checks) lists. Every other `pragma_*` function is refused, and so are `changes()`, `total_changes()` and `last_insert_rowid()`. Read those on the app's own connection, in `bin/rails runner` or the console. A `pragma_*` name is refused wherever it stands, so a column whose own name starts with `pragma_` is refused too: read it in the console as well.
315
+
316
+ ### "Blocked: the statement is ... bytes long"
317
+
318
+ `rails_query` checks a statement of 1,000,000 bytes at most. Send a shorter one: split a long `IN` list, or join against a table that holds the ids.
319
+
320
+ ### "Blocked: the checks on the statement's text ran longer than query_timeout"
321
+
322
+ The checks that read a statement's text have `query_timeout` to themselves and give up when they pass it. The statement was not sent to the database. The schema read that comes before the checks was. Send a shorter statement.
323
+
324
+ ### "Blocked: ... is not queried: its columns could not be read" and "leads through more databases than rails_query follows"
325
+
326
+ The secret column rule reads the columns of every schema and database a statement reaches. A relation whose columns it cannot read is not queried, so give the account read access to the catalogue of that schema, or query a relation it can read. A view the statement names is followed through the databases or schemas its definition names, three reads deep. A statement that names a view that leads deeper is not run, and the message names the view: query the tables it reads. A statement that does not name that view runs.
327
+
328
+ ### "Blocked: SHOW with a WHERE clause is not run"
329
+
330
+ MySQL's statement timeout does not hold a `SHOW` filter, so it is refused on every database. `SHOW ... LIKE` runs. For any other filter select from `information_schema`: `SELECT table_name FROM information_schema.tables WHERE table_schema = DATABASE()`.
331
+
332
+ ### "Blocked: `pg_stat_activity` is not read"
333
+
334
+ `rails_query` reads no relation that holds values sampled from other tables, other sessions' statements or stored credentials. On PostgreSQL those are `pg_stats`, `pg_stat_activity`, `pg_authid`, `pg_settings` and their like. On MySQL and MariaDB they are `information_schema.processlist`, `performance_schema`, `sys` and the whole `mysql` schema among others. On SQLite they are the `sqlite_stat` tables and `sqlite_stmt`. [SECURITY.md](SECURITY.md#relations-and-settings-that-are-never-read) lists every name. Use the database's own client for those. Each name is refused on the database that has it, so an app table named `slow_log` is refused on MySQL and MariaDB. For `pg_settings` the message ends "To read one setting, run SHOW with its name."
335
+
336
+ ### "A name before a dot is read as that schema, so an alias named `sys` needs another name"
337
+
338
+ On MySQL and MariaDB `mysql.` and `sys.` are read as those schemas wherever they stand outside a single-quoted string. An alias cannot be told from a schema, so `FROM users sys ... sys.id` is refused. Give the alias another name. The sentence ends every refusal over `mysql.` or `sys.` outside double-quoted text, so a real `FROM mysql.user` gets it too, with `mysql` as the name. A double-quoted string that holds `mysql.` or `sys.` is refused too, with a sentence of its own: MySQL reads double-quoted text as a name when the session's sql_mode holds `ANSI_QUOTES`. Write it in single quotes.
339
+
340
+ ### "That is how MySQL reads the statement when the session's sql_mode holds NO_BACKSLASH_ESCAPES"
341
+
342
+ On MySQL and MariaDB a backslash in a string is an escape, unless the session's sql_mode holds `NO_BACKSLASH_ESCAPES`. `rails_query` does not read the session's mode, so it checks the statement under both readings and refuses it when either one does. This message ends a refusal that the second reading alone gives: with `\'` read as a backslash and the end of the string, the text after it is code. Write a quote inside a string doubled (`'it''s'`), so that both readings agree.
343
+
344
+ ### "Blocked: a `[` outside a string or a quoted name is not run"
345
+
346
+ On MySQL and MariaDB a square bracket outside a string or a quoted name is refused. MariaDB reads `[name]` as a quoted name when the session's sql_mode holds `MSSQL`, and `rails_query` does not read the session's mode, so its checks could read such a statement another way than the server does. Quote a name in backticks: `` `name` ``. A `[` inside a string, such as a JSON path (`'$[0]'`), runs. One case apart: a string that holds a backslash before a quote (`'O\'Brien'`) is read a second way that ends it at that quote, and a `[` which that reading leaves outside a string is refused. Write the quote doubled (`'O''Brien'`).
347
+
348
+ ### "Blocked: a user variable assignment (`:=`) is not run"
349
+
350
+ On MySQL and MariaDB `SELECT @n := @n + 1` leaves `@n` set on the app's pooled connection after the query returns, and a read-only transaction does not stop that. To number rows, use a window function, which MySQL 8 and MariaDB 10.2 and later have: `SELECT ROW_NUMBER() OVER (ORDER BY id) AS n, title FROM posts`. A `:=` inside a string or a quoted name runs, unless a backslash before a quote in the statement leaves it outside a string in the second reading of a backslash: write that quote doubled.
351
+
352
+ ### "Blocked: LAST_INSERT_ID with an argument is not run"
353
+
354
+ On MySQL and MariaDB `LAST_INSERT_ID(<value>)` sets the value the next `LAST_INSERT_ID()` returns, and it stays on the app's pooled connection after the query returns. `LAST_INSERT_ID()` with no argument runs. The call is refused wherever it stands, inside a string too.
355
+
356
+ ### "Blocked: a column-alias list" on a statement that has none
357
+
358
+ One name in parentheses after `GLOB`, `MATCH` or `OVER` that follows a subquery has the shape of a column-alias list, so `WHERE (SELECT title FROM post_titles LIMIT 1) GLOB (title)` and `sum((SELECT 1)) OVER (w)` are refused. Write the name with no parentheses (`GLOB title`), or write the window inline (`OVER (ORDER BY id)`).
359
+
360
+ ### "Blocked: UNION is not run" on a statement with no UNION
361
+
362
+ A quoted name that spells the word is refused, as in `SELECT 1 AS "union"`. Give the alias another name. On PostgreSQL the word inside a string is data.
363
+
364
+ ### "Blocked: the setting ... is not read", "Blocked: SHOW ALL is not run" and "Blocked: SHOW takes a setting's plain name on PostgreSQL"
365
+
366
+ On PostgreSQL `rails_query` reads one setting at a time, by its plain name: `SHOW work_mem`. `SHOW ALL` and a quoted name (`SHOW "work_mem"`) are refused. Six settings can hold a password or a command line that carries one and are refused: `primary_conninfo`, `ssl_passphrase_command`, `archive_command`, `restore_command`, `archive_cleanup_command` and `recovery_end_command`. So is a setting whose own name reads as a secret, such as `app.jwt_secret`.
367
+
368
+ ### "Blocked: current_setting runs only with the setting's name written as one plain string"
369
+
370
+ Write the name as one string: `current_setting('work_mem')`. A name built by an expression, read from a column or written as an `E''` or dollar-quoted string is refused, since the gate could not read which setting comes back.
371
+
372
+ ### "Blocked: a quoted name is followed by a parenthesis"
373
+
374
+ A function called by a quoted name (`"upper"('x')`, `` `sleep`(0) ``) is refused on every database, since the checks read a function's name to decide whether it runs. Write the name without quotes. A function whose name needs quotes, such as a mixed-case one on PostgreSQL, cannot be called through `rails_query`.
375
+
376
+ ### "Blocked: EXPLAIN FOR CONNECTION is not run"
377
+
378
+ On MySQL and MariaDB `EXPLAIN FOR CONNECTION` shows the plan of the statement another session is running, so it is refused. `EXPLAIN` of a statement of your own runs.
379
+
380
+ ### "The columns of the <name> database could not be read"
381
+
382
+ The secret column rule reads the database's columns before any query runs. When that read fails and, for the primary, the cached schema holds no tables either, no query runs on that database. The read is a few statements whatever the number of tables, six at most, so an account that cannot read the catalogue cannot use `rails_query`.
383
+
384
+ ### "The database is locked by another connection, so the query did not run"
385
+
386
+ Another connection holds a lock on the SQLite database, such as a write transaction left open in a console. Nothing ran. End that transaction and run the query again. The query waits on a lock for 80% of `query_timeout` before it answers this. A lock met while the connection opens, as on the CLI or on a server's first call, gets the same answer. A query that holds its read lock and then runs past `query_timeout` answers the timeout, also when a writer asks for the lock while it runs.
387
+
388
+ ### "This SQLite query ran without a time limit"
389
+
390
+ On SQLite `rails_query` runs a statement in a process of its own, which it ends at `query_timeout`. That process cannot see what the app's own connection alone holds. So the statement runs on the app's connection, with this note, on an in-memory database, on a platform without `fork`, inside an open transaction, and when it needs a table, a function, a module, a collation or an encryption key only that connection has. While the connection holds a temp object or an attachment, it runs there too when it names a temp object, the `temp` schema, `sqlite_temp_master` or an attached database, or calls a pragma function (`pragma_table_info(...)`, `pragma_table_list` and the rest) or `dbstat`. A name is matched anywhere in the statement, in any letter case and inside a string too. A statement that returns rows slowly still ends a few rows after `query_timeout`. One long step cannot be ended there.
391
+
392
+ ### "The reads rails_query makes before a statement ... took the whole <N> second timeout"
393
+
394
+ Before a statement runs, `rails_query` reads the columns of the schema and, on PostgreSQL, the statement's plan. Those reads share one `query_timeout`, and here they used all of it, so the statement was not run. A table another session has locked is the usual cause. Run the query again when the lock is gone, or raise `config.query_timeout`. "The statement ran, and the read that traces its result columns ..." is the same limit met after the statement: its rows are not shown.
283
395
 
284
396
  ### "Column values show [FILTERED]"
285
397
 
286
- Columns whose names look sensitive are redacted, and a query that names one is rejected with "Blocked: query references sensitive column". Shortening `config.query_redacted_columns` does not lift either, because a built-in list applies as well. Exempt a column of your own by name:
398
+ Columns whose names look secret are redacted, and a query that names one is rejected with "Blocked: query references secret column `x`." followed by what to do. Shortening `config.query_redacted_columns` does not lift either, because a built-in list applies as well. Exempt a column of your own by name:
287
399
 
288
400
  ```ruby
289
401
  config.query_allowed_columns = %w[secret]
290
402
  ```
291
403
 
404
+ On a standalone install, where no initializer runs, put the name under `query_allowed_columns:` in `.rails-ai-context.yml`.
405
+
292
406
  ---
293
407
 
294
408
  ## Search issues
@@ -335,7 +449,8 @@ The file is read from the app root, once, at boot, and an initializer does not r
335
449
 
336
450
  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.
337
451
  2. The key is one the gem knows. An unknown key warns on stderr and is ignored.
338
- 3. The file is readable and parses. Corrupted YAML degrades gracefully with a warning.
452
+ 3. The file holds `key: value` settings. A file that holds a bare string, a list or a number is ignored with "WARNING: <path>: the file holds no `key: value` settings. Ignored."
453
+ 4. The file is readable and parses. Corrupted YAML degrades gracefully with a warning.
339
454
 
340
455
  ---
341
456
 
data/docs/_config.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  title: rails-ai-context
2
- description: 45 MCP tools that give AI agents live access to your Rails schema, models, routes & conventions.
2
+ description: 35 MCP tools that give AI agents live access to your Rails schema, models, routes & conventions.
3
3
  theme: jekyll-theme-minimal
4
4
  plugins:
5
5
  - jekyll-relative-links