rails-ai-bridge 1.1.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (101) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +8 -0
  3. data/CHANGELOG.md +35 -0
  4. data/Rakefile +3 -0
  5. data/SECURITY.md +13 -5
  6. data/UPGRADING.md +87 -0
  7. data/docs/roadmap-context-assistants.md +27 -0
  8. data/docs/roadmap-mcp-v2.md +45 -0
  9. data/docs/roadmaps.md +48 -0
  10. data/lib/generators/rails_ai_bridge/install/install_generator.rb +50 -22
  11. data/lib/rails_ai_bridge/assistant_formats_preference.rb +84 -0
  12. data/lib/rails_ai_bridge/config/auth.rb +35 -0
  13. data/lib/rails_ai_bridge/config/introspection.rb +85 -0
  14. data/lib/rails_ai_bridge/config/mcp.rb +111 -0
  15. data/lib/rails_ai_bridge/config/output.rb +47 -0
  16. data/lib/rails_ai_bridge/config/server.rb +43 -0
  17. data/lib/rails_ai_bridge/configuration.rb +91 -99
  18. data/lib/rails_ai_bridge/exclusion_helper.rb +23 -0
  19. data/lib/rails_ai_bridge/http_transport_app.rb +33 -3
  20. data/lib/rails_ai_bridge/introspector.rb +1 -1
  21. data/lib/rails_ai_bridge/introspectors/model_introspector.rb +12 -1
  22. data/lib/rails_ai_bridge/introspectors/schema/static_schema_parser.rb +131 -0
  23. data/lib/rails_ai_bridge/introspectors/schema_introspector.rb +31 -32
  24. data/lib/rails_ai_bridge/mcp/auth/base_strategy.rb +24 -0
  25. data/lib/rails_ai_bridge/mcp/auth/strategies/bearer_token.rb +104 -0
  26. data/lib/rails_ai_bridge/mcp/auth/strategies/jwt.rb +65 -0
  27. data/lib/rails_ai_bridge/mcp/auth_result.rb +39 -0
  28. data/lib/rails_ai_bridge/mcp/authenticator.rb +122 -0
  29. data/lib/rails_ai_bridge/mcp/http_rate_limiter.rb +40 -0
  30. data/lib/rails_ai_bridge/mcp/http_structured_log.rb +49 -0
  31. data/lib/rails_ai_bridge/resources.rb +21 -11
  32. data/lib/rails_ai_bridge/serializers/context_file_serializer.rb +10 -10
  33. data/lib/rails_ai_bridge/serializers/context_summary.rb +69 -0
  34. data/lib/rails_ai_bridge/serializers/formatters/action_mailbox_formatter.rb +22 -0
  35. data/lib/rails_ai_bridge/serializers/formatters/action_text_formatter.rb +22 -0
  36. data/lib/rails_ai_bridge/serializers/formatters/active_storage_formatter.rb +23 -0
  37. data/lib/rails_ai_bridge/serializers/formatters/api_formatter.rb +31 -0
  38. data/lib/rails_ai_bridge/serializers/formatters/app_overview_formatter.rb +22 -0
  39. data/lib/rails_ai_bridge/serializers/formatters/assets_formatter.rb +23 -0
  40. data/lib/rails_ai_bridge/serializers/formatters/auth_formatter.rb +34 -0
  41. data/lib/rails_ai_bridge/serializers/formatters/base.rb +22 -0
  42. data/lib/rails_ai_bridge/serializers/formatters/claude_footer_formatter.rb +31 -0
  43. data/lib/rails_ai_bridge/serializers/formatters/claude_header_formatter.rb +25 -0
  44. data/lib/rails_ai_bridge/serializers/formatters/codex_footer_formatter.rb +18 -0
  45. data/lib/rails_ai_bridge/serializers/formatters/codex_header_formatter.rb +23 -0
  46. data/lib/rails_ai_bridge/serializers/formatters/config_formatter.rb +23 -0
  47. data/lib/rails_ai_bridge/serializers/formatters/controllers_formatter.rb +48 -0
  48. data/lib/rails_ai_bridge/serializers/formatters/conventions_formatter.rb +24 -0
  49. data/lib/rails_ai_bridge/serializers/formatters/copilot_footer_formatter.rb +18 -0
  50. data/lib/rails_ai_bridge/serializers/formatters/copilot_header_formatter.rb +22 -0
  51. data/lib/rails_ai_bridge/serializers/formatters/devops_formatter.rb +28 -0
  52. data/lib/rails_ai_bridge/serializers/formatters/engines_formatter.rb +27 -0
  53. data/lib/rails_ai_bridge/serializers/formatters/footer_formatter.rb +18 -0
  54. data/lib/rails_ai_bridge/serializers/formatters/gems_formatter.rb +26 -0
  55. data/lib/rails_ai_bridge/serializers/formatters/header_formatter.rb +24 -0
  56. data/lib/rails_ai_bridge/serializers/formatters/i18n_formatter.rb +22 -0
  57. data/lib/rails_ai_bridge/serializers/formatters/jobs_formatter.rb +35 -0
  58. data/lib/rails_ai_bridge/serializers/formatters/mcp_guide_formatter.rb +59 -0
  59. data/lib/rails_ai_bridge/serializers/formatters/middleware_formatter.rb +28 -0
  60. data/lib/rails_ai_bridge/serializers/formatters/migrations_formatter.rb +35 -0
  61. data/lib/rails_ai_bridge/serializers/formatters/models_formatter.rb +31 -0
  62. data/lib/rails_ai_bridge/serializers/formatters/multi_database_formatter.rb +35 -0
  63. data/lib/rails_ai_bridge/serializers/formatters/rake_tasks_formatter.rb +25 -0
  64. data/lib/rails_ai_bridge/serializers/formatters/routes_formatter.rb +25 -0
  65. data/lib/rails_ai_bridge/serializers/formatters/rules_footer_formatter.rb +18 -0
  66. data/lib/rails_ai_bridge/serializers/formatters/rules_header_formatter.rb +19 -0
  67. data/lib/rails_ai_bridge/serializers/formatters/schema_formatter.rb +24 -0
  68. data/lib/rails_ai_bridge/serializers/formatters/section_formatter.rb +62 -0
  69. data/lib/rails_ai_bridge/serializers/formatters/seeds_formatter.rb +31 -0
  70. data/lib/rails_ai_bridge/serializers/formatters/tests_formatter.rb +25 -0
  71. data/lib/rails_ai_bridge/serializers/formatters/turbo_formatter.rb +33 -0
  72. data/lib/rails_ai_bridge/serializers/formatters/views_formatter.rb +35 -0
  73. data/lib/rails_ai_bridge/serializers/markdown_serializer.rb +43 -486
  74. data/lib/rails_ai_bridge/serializers/{claude_rules_serializer.rb → providers/claude_rules_serializer.rb} +5 -3
  75. data/lib/rails_ai_bridge/serializers/{claude_serializer.rb → providers/claude_serializer.rb} +31 -99
  76. data/lib/rails_ai_bridge/serializers/{codex_serializer.rb → providers/codex_serializer.rb} +29 -40
  77. data/lib/rails_ai_bridge/serializers/{codex_support_serializer.rb → providers/codex_support_serializer.rb} +8 -6
  78. data/lib/rails_ai_bridge/serializers/{copilot_instructions_serializer.rb → providers/copilot_instructions_serializer.rb} +5 -3
  79. data/lib/rails_ai_bridge/serializers/{copilot_serializer.rb → providers/copilot_serializer.rb} +25 -34
  80. data/lib/rails_ai_bridge/serializers/{cursor_rules_serializer.rb → providers/cursor_rules_serializer.rb} +46 -19
  81. data/lib/rails_ai_bridge/serializers/{rules_serializer.rb → providers/rules_serializer.rb} +13 -27
  82. data/lib/rails_ai_bridge/serializers/{windsurf_rules_serializer.rb → providers/windsurf_rules_serializer.rb} +5 -3
  83. data/lib/rails_ai_bridge/serializers/{windsurf_serializer.rb → providers/windsurf_serializer.rb} +22 -6
  84. data/lib/rails_ai_bridge/server.rb +3 -1
  85. data/lib/rails_ai_bridge/tools/get_model_details.rb +7 -103
  86. data/lib/rails_ai_bridge/tools/get_schema.rb +7 -91
  87. data/lib/rails_ai_bridge/tools/model_details/full_formatter.rb +34 -0
  88. data/lib/rails_ai_bridge/tools/model_details/single_model_formatter.rb +75 -0
  89. data/lib/rails_ai_bridge/tools/model_details/standard_formatter.rb +34 -0
  90. data/lib/rails_ai_bridge/tools/model_details/summary_formatter.rb +21 -0
  91. data/lib/rails_ai_bridge/tools/schema/full_formatter.rb +38 -0
  92. data/lib/rails_ai_bridge/tools/schema/standard_formatter.rb +42 -0
  93. data/lib/rails_ai_bridge/tools/schema/summary_formatter.rb +41 -0
  94. data/lib/rails_ai_bridge/tools/schema/table_formatter.rb +46 -0
  95. data/lib/rails_ai_bridge/version.rb +1 -1
  96. data/lib/rails_ai_bridge.rb +11 -7
  97. data/rails-ai-bridge.gemspec +7 -6
  98. data/tasks/prd-v2-architecture-and-quality.md +222 -0
  99. data/tasks/tasks-v2-architecture-and-quality.md +258 -0
  100. metadata +104 -36
  101. data/lib/rails_ai_bridge/mcp_http_auth.rb +0 -69
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 94459120e6365ac608ed9a403e16a622e7ef664b377f8585980827c28dd911f0
4
- data.tar.gz: e3561760ceb930242a6d344ae3938dfdca49879639e7776a19cc2fa08adff4eb
3
+ metadata.gz: f8392971b0bc2f8bf98ebe626062b79e7ff271e8aa009ace229702948862eace
4
+ data.tar.gz: d3e358ef11eef6c9050289d73ab69ec5ad09684d7d8e7d8b25b5da4350306c9e
5
5
  SHA512:
6
- metadata.gz: c595b9b20dc56cef6adf370d931f06083137691cd5323dd8fe43615eba819eb7d725d5ee62160c0305fb311f3d2592ad767cd2eb8b69bfc3f075aae380b9209e
7
- data.tar.gz: d6ee1293103eccc654bbed37f67b9deb022ecede51ee8bc4f4714db86f7f18d73a3ebe0054d86c6c8350107dd1bf6239520ff137c5b828faeb75152095c158bd
6
+ metadata.gz: d174446e69dddabd9f99457af197d607793f26b09ef346f2c27f124be15700fb1b7961e4ba76dd43959d32bfcfe108787c611505043ecf12e61ba7fd1735b7e9
7
+ data.tar.gz: 6bb2d7c5290441e76359de520d3e7e737890d3d6456b6e269bfba579c79540a900cccdd872ec580dc5d392c09c6f053e2f0516842aa8e222f0d994111de2b5de
data/.yardopts ADDED
@@ -0,0 +1,8 @@
1
+ --markup markdown
2
+ --output-dir doc/yard
3
+ --protected
4
+ --no-private
5
+ lib/**/*.rb
6
+ -
7
+ README.md
8
+ CHANGELOG.md
data/CHANGELOG.md CHANGED
@@ -7,6 +7,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [2.0.0] - 2026-03-31
11
+
10
12
  ### Added
11
13
 
12
14
  - **Shared runtime context provider** — MCP tools and `rails://...` resources now read through `RailsAiBridge::ContextProvider`, keeping cache invalidation and snapshot semantics aligned across both entry points.
@@ -15,15 +17,48 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
15
17
  - **Section-level context reads** — `ContextProvider.fetch_section` and `BaseTool.cached_section` let single-section tools avoid rebuilding or materializing the full snapshot path when unnecessary.
16
18
  - **Folder-level contributor docs** — key runtime folders now include local `README.md` guides for structure, boundaries, and extension points.
17
19
  - **Extensibility integration coverage** — specs now prove that a custom introspector, tool, and resource can be registered and used together from the host app configuration surface.
20
+ - **Serializer formatter objects** — `MarkdownSerializer` is now a thin orchestrator delegating to 37 single-responsibility `Formatters::*` classes; each formatter is independently testable and injectable.
21
+ - **Tool response formatters** — `GetSchema` and `GetModelDetails` delegate all rendering to `Tools::Schema::*` and `Tools::ModelDetails::*` formatter classes; tool `call` methods are ≤20 lines each.
22
+ - **`Config::Auth`, `Config::Server`, `Config::Introspection`, `Config::Output`** — `Configuration` is now a `Forwardable` facade over four focused sub-objects; each is independently readable and injectable.
23
+ - **`Mcp::Authenticator`** — consolidates strategy resolution, static-token lookup, and configuration predicates into a single entry point, replacing the previous split between `McpHttpAuth` and `Mcp::HttpAuth`.
24
+ - **`Mcp::HttpRateLimiter`** — optional in-process sliding-window rate limiter per client IP; configured via `config.mcp.rate_limit_max_requests` and `config.mcp.rate_limit_window_seconds`. Returns 429 with `Retry-After` header when exceeded.
25
+ - **`Mcp::HttpStructuredLog`** — optional one-JSON-line-per-request logger for the MCP HTTP path; enabled via `config.mcp.http_log_json = true`. Logs `event`, `http_status`, `path`, `client_ip`, and `request_id`; never logs tokens or full Rack env.
26
+ - **`Config::Mcp`** — new `config.mcp` sub-object (5th façade sub-config) for MCP HTTP operational settings: `mode`, `security_profile`, `rate_limit_max_requests`, `rate_limit_window_seconds`, `http_log_json`, `authorize`, `require_auth_in_production`.
27
+ - **`config.mcp.authorize`** — optional post-auth lambda `(context, request) { truthy }`; returning falsey yields HTTP 403 on the MCP path.
28
+ - **`config.mcp.require_auth_in_production`** — when `true`, boot fails in production unless an auth mechanism is configured.
29
+ - **`HttpTransportApp`** updated — request pipeline is now: path check → auth → authorize → rate limit → structured log → transport.
30
+ - **`SectionFormatter` template method base** — 22 of 37 formatters now inherit from `SectionFormatter`, which handles the nil/error guard in one place; each formatter only implements `render(data)`.
31
+ - **`Serializers::Providers` namespace** — 10 LLM provider serializers extracted into `lib/rails_ai_bridge/serializers/providers/`, separating provider concerns from domain infrastructure (`MarkdownSerializer`, `JsonSerializer`, formatters).
32
+ - **`UPGRADING.md`** — new upgrade guide documenting `config.mcp` settings, rate limit semantics, structured logging, `authorize` behaviour, and the `require_auth_in_production` flag.
33
+ - **Contributor roadmaps** — `docs/roadmaps.md`, `docs/roadmap-mcp-v2.md`, `docs/roadmap-context-assistants.md` added.
18
34
 
19
35
  ### Changed
20
36
 
21
37
  - **Install generator messages** — the install flow now reports created vs unchanged files correctly and the generated initializer comments reflect the current preset sizes.
22
38
  - **Fingerprint reuse on invalidation** — context refresh reuses a single fingerprint snapshot per fetch cycle instead of scanning twice when cached context becomes stale.
39
+ - **`FullClaudeSerializer`, `FullRulesSerializer`, `FullCopilotSerializer`, `FullCodexSerializer` removed** — full-mode rendering is now handled by injecting header/footer formatter classes into `MarkdownSerializer` via constructor arguments; no subclassing needed.
40
+ - **Test suite expanded to 841 examples at ≥87% line coverage.**
23
41
 
24
42
  ### Fixed
25
43
 
26
44
  - **Install generator output bug** — `generate_context` results are no longer iterated as raw hash pairs during install-time file generation.
45
+ - **`StandardFormatter` pagination hint** — navigation hint now correctly uses `offset + limit < total` (consistent with `SummaryFormatter` and `FullFormatter`), preventing a spurious hint on the last page.
46
+
47
+ ### Upgrading from 1.x
48
+
49
+ **No configuration changes required.** Every `config.*` attribute from 1.x is still available unchanged — `Configuration` now delegates to focused sub-objects (`Config::Auth`, `Config::Server`, `Config::Introspection`, `Config::Output`, `Config::Mcp`) but exposes the same flat DSL.
50
+
51
+ The following internal classes were removed; they were never part of the documented public API:
52
+
53
+ | Removed | Replacement |
54
+ |---------|-------------|
55
+ | `Mcp::HttpAuth` / `McpHttpAuth` | `Mcp::Authenticator` (same behaviour, single entry point) |
56
+ | `FullClaudeSerializer` | Pass `header_class: Formatters::ClaudeHeaderFormatter` to `MarkdownSerializer` |
57
+ | `FullCopilotSerializer` | Pass `header_class: Formatters::CopilotHeaderFormatter` to `MarkdownSerializer` |
58
+ | `FullCodexSerializer` | Pass `header_class: Formatters::CodexHeaderFormatter` to `MarkdownSerializer` |
59
+ | `FullRulesSerializer` | Pass `header_class: Formatters::RulesHeaderFormatter` to `MarkdownSerializer` |
60
+
61
+ If you were only using the gem through its initializer, rake tasks, or MCP server — no action needed.
27
62
 
28
63
  ## [1.1.0] - 2026-03-20
29
64
 
data/Rakefile CHANGED
@@ -5,4 +5,7 @@ require "rspec/core/rake_task"
5
5
 
6
6
  RSpec::Core::RakeTask.new(:spec)
7
7
 
8
+ require "yard"
9
+ YARD::Rake::YardocTask.new(:yard)
10
+
8
11
  task default: :spec
data/SECURITY.md CHANGED
@@ -33,14 +33,18 @@ This fork is maintained by **Ismael Marin**. If you discover a security vulnerab
33
33
 
34
34
  ## HTTP MCP authentication
35
35
 
36
- - Set `config.http_mcp_token` and/or `ENV["RAILS_AI_BRIDGE_MCP_TOKEN"]` (**ENV overrides** the config value when set).
37
- - When a token is configured, clients must send `Authorization: Bearer <token>` to the HTTP MCP endpoint (`auto_mount` or `rails ai:serve_http`).
38
- - When no token is configured, HTTP MCP is **unauthenticated** (backward compatible for local use); **set a token** before exposing the port beyond localhost.
36
+ - **Shared secret:** set `config.http_mcp_token` and/or `ENV["RAILS_AI_BRIDGE_MCP_TOKEN"]` (**ENV overrides** the config value when set). Clients send `Authorization: Bearer <token>`.
37
+ - **Custom resolver / JWT:** configure `config.mcp_token_resolver` or `config.mcp_jwt_decoder` (see [docs/GUIDE.md](docs/GUIDE.md)); clients still use a Bearer header; the gem does not ship a JWT library — you verify and decode inside your lambda.
38
+ - **Rack env context:** after a successful auth, `env["rails_ai_bridge.mcp.context"]` may contain **PII or claims** (e.g. JWT payload). Do not dump full Rack `env` to logs or APM; treat this key like session-derived data.
39
+ - When **no** auth mechanism is configured, HTTP MCP is **unauthenticated** (backward compatible for local use); configure one of the above before exposing the port beyond localhost.
40
+ - **Misconfiguration guard:** setting `:bearer_token` strategy without a `token_resolver` and without a static token causes **boot failure** (`ConfigurationError`) so the endpoint cannot start in an accidentally open state.
41
+ - **Rate limiting:** optional `config.mcp.rate_limit_max_requests` is an **in-memory, per-process** sliding window keyed by client IP (`request.ip`). It is **not** shared across Puma workers or hosts. Treat this as a light guard — use a reverse proxy, WAF, or `rack-attack` for strict distributed quotas.
42
+ - **MCP HTTP JSON logs:** when `config.mcp.http_log_json` is enabled, log lines include `client_ip` and path; treat log sinks like any operational data store (retention, access control).
39
43
 
40
44
  ## Production
41
45
 
42
- - `config.auto_mount = true` in **production** raises at boot unless **both** `config.allow_auto_mount_in_production = true` and a non-empty MCP token are set.
43
- - `rails ai:serve_http` in **production** requires a non-empty MCP token (config or ENV).
46
+ - `config.auto_mount = true` in **production** raises at boot unless **both** `config.allow_auto_mount_in_production = true` and an MCP auth mechanism is configured (shared token, `mcp_token_resolver`, or `mcp_jwt_decoder`).
47
+ - `rails ai:serve_http` in **production** requires an auth mechanism (not necessarily a static shared secret).
44
48
 
45
49
  ## Operational Security Guidance
46
50
 
@@ -48,3 +52,7 @@ This fork is maintained by **Ismael Marin**. If you discover a security vulnerab
48
52
  - If you enable HTTP transport, keep it bound to `127.0.0.1` unless you add your own network isolation and authentication controls.
49
53
  - Do **not** expose `auto_mount` on public or shared production surfaces without an explicit threat model review.
50
54
  - Treat generated files such as `AGENTS.md`, `CLAUDE.md`, `.cursorrules`, and `.ai-context.json` as internal engineering documentation.
55
+
56
+ ## Presets, exclusions, and MCP
57
+
58
+ `rails_get_schema`, `rails_get_model_details`, and other tools build on the same introspection pipeline as `rails ai:bridge`. When you use `config.excluded_tables`, `config.excluded_models`, `config.disabled_introspection_categories`, or presets such as `:regulated`, treat the HTTP/stdio MCP surface with the **same data-classification assumptions** as your committed context files: the tools remain read-only but can still reveal structure you chose to omit from markdown.
data/UPGRADING.md ADDED
@@ -0,0 +1,87 @@
1
+ # Upgrading rails-ai-bridge
2
+
3
+ ## Upgrading from 1.x to 2.x
4
+
5
+ **No configuration changes required.** Every `config.*` attribute from 1.x is still available — `Configuration` now delegates to focused sub-objects but exposes the same flat DSL. See `CHANGELOG.md` for the full list of internal changes.
6
+
7
+ ---
8
+
9
+ ## New in 2.x — `config.mcp` settings
10
+
11
+ MCP HTTP operational configuration lives under `config.mcp` (a `Config::Mcp` object). All attributes are also accessible as flat delegators on `config` directly.
12
+
13
+ ### Rate limiting
14
+
15
+ ```ruby
16
+ RailsAiBridge.configure do |config|
17
+ # Explicit ceiling: 100 requests per 60-second sliding window per client IP
18
+ config.mcp.rate_limit_max_requests = 100
19
+ config.mcp.rate_limit_window_seconds = 60
20
+
21
+ # Set to 0 to disable rate limiting entirely
22
+ # config.mcp.rate_limit_max_requests = 0
23
+ end
24
+ ```
25
+
26
+ When `rate_limit_max_requests` is `nil` (default), the gem may apply an **implicit** per-IP ceiling from `security_profile` (`:strict` / `:balanced` / `:relaxed`), unless `mode` suppresses it:
27
+
28
+ - **`mode: :dev`** — no implicit limit.
29
+ - **`mode: :hybrid`** (default) — implicit limit only when `Rails.env.production?`.
30
+ - **`mode: :production`** — implicit limit in every Rails environment.
31
+
32
+ Set `config.mcp.rate_limit_max_requests = 0` to **disable** limiting entirely (including implicit). A **positive integer** always overrides the profile.
33
+
34
+ > **Note:** the rate limiter is **in-memory and per-process**. It is not shared across Puma workers or hosts. Use a reverse proxy, WAF, or `rack-attack` for strict distributed quotas.
35
+
36
+ ### Structured logging
37
+
38
+ ```ruby
39
+ RailsAiBridge.configure do |config|
40
+ # Emit one JSON line per MCP HTTP response to Rails.logger
41
+ config.mcp.http_log_json = true
42
+ end
43
+ ```
44
+
45
+ Each log line includes `msg`, `event`, `http_status`, `path`, `client_ip`, and `request_id` (when present). Tokens and full Rack `env` are never logged. The flag is read **on each request** (unlike the rate-limit snapshot taken at `HttpTransportApp.build`).
46
+
47
+ ### Post-auth authorization (`authorize`)
48
+
49
+ ```ruby
50
+ RailsAiBridge.configure do |config|
51
+ # Called after successful auth; returning falsey yields HTTP 403
52
+ config.mcp.authorize = ->(context, request) {
53
+ context[:role] == "admin"
54
+ }
55
+ end
56
+ ```
57
+
58
+ The lambda is read and called **on every request** (like `http_log_json`), so changes take effect immediately without rebuilding the transport app. If the lambda raises a `StandardError`, the gem treats it as a 403 and logs the error — it does not propagate as a 500.
59
+
60
+ ### Production boot guard
61
+
62
+ ```ruby
63
+ RailsAiBridge.configure do |config|
64
+ # Raise at boot in production unless an auth mechanism is configured
65
+ config.mcp.require_auth_in_production = true
66
+ end
67
+ ```
68
+
69
+ When `true` in a production environment, Rails boot fails unless at least one MCP HTTP auth mechanism is configured:
70
+ - `config.http_mcp_token`, or
71
+ - `ENV["RAILS_AI_BRIDGE_MCP_TOKEN"]`, or
72
+ - `config.mcp_token_resolver`, or
73
+ - `config.mcp_jwt_decoder`
74
+
75
+ Default is `false`.
76
+
77
+ ---
78
+
79
+ ## `strategy :bearer_token` misconfiguration guard
80
+
81
+ Rails **boot** raises `RailsAiBridge::ConfigurationError` if you configure `:bearer_token` strategy without a resolver or static token — that combination would leave HTTP MCP unauthenticated.
82
+
83
+ ---
84
+
85
+ ## Resolver / JWT return values
86
+
87
+ Return **`nil`** (or `false`) when a token is invalid. Returning `false` explicitly is treated as auth failure (401).
@@ -0,0 +1,27 @@
1
+ # Roadmap: context files & assistant UX (pre–major release)
2
+
3
+ This track is **separate** from [roadmap-mcp-v2.md](roadmap-mcp-v2.md) (MCP HTTP auth, rate limits, logging). It covers **improving the static context** the gem generates (CLAUDE.md, Cursor rules, Copilot instructions, etc.) so IDEs and AI clients get **clearer, more actionable** help from the same introspection data.
4
+
5
+ **Progress summary:** [roadmaps.md](roadmaps.md).
6
+
7
+ ## Goals (high level)
8
+
9
+ - Sharpen per-assistant formats (structure, length, cross-links) based on real usage feedback.
10
+ - Reduce duplication and noise while keeping "always-on" rules discoverable.
11
+ - Align tool references and workflow hints across Claude, Cursor, Copilot, Windsurf, Codex, etc.
12
+ - Separate LLM provider serializers from domain infrastructure (done: `Serializers::Providers::` namespace).
13
+ - DRY the formatter hierarchy (done: `SectionFormatter` template method base).
14
+
15
+ ## In progress
16
+
17
+ - Refining per-provider serializer output (context quality improvements on `pr5-context-quality` branch)
18
+ - Custom Rails directory introspection coverage gaps
19
+
20
+ ## Relation to versioning
21
+
22
+ **No semver bump is implied by this doc alone.** A future **major release** (e.g. 2.0.0) should follow **after** this work when the maintainers are ready to communicate breaking or wide-ranging changes to generated files and defaults — not as a fixed milestone on the MCP roadmap.
23
+
24
+ ## References
25
+
26
+ - [docs/GUIDE.md](GUIDE.md) — install, formats, MCP setup
27
+ - [CHANGELOG.md](../CHANGELOG.md) — user-visible changes
@@ -0,0 +1,45 @@
1
+ # Roadmap: MCP HTTP auth (v2 track)
2
+
3
+ High-level direction for the `RailsAiBridge::Mcp` HTTP authentication layer, rate limiting, and related docs. The detailed task list may live in your issue tracker; this file is the **human-readable** anchor for contributors.
4
+
5
+ ## Engineering principles (this track)
6
+
7
+ These are **priorities** for code that lands under `lib/rails_ai_bridge/mcp/` and related Rack paths — not optional polish at the end.
8
+
9
+ 1. **Tests gate implementation** — Write or extend a failing spec for the behavior, run it, then implement. No "implement first, test later" for new behavior.
10
+
11
+ 2. **YARD on every public surface** — Any new or changed public Ruby class or method needs an English YARD summary, `@param`, `@return`, and `@raise` when applicable — before the change is considered done.
12
+
13
+ 3. **Clear method boundaries** — Keep Rack/guard logic separate from "run host lambda safely." Strategies stay easy to read, review, and extend without growing god methods.
14
+
15
+ 4. **Self-review before merge** — Run through `rails-code-review` (and security/architecture skills when touching auth or production boot).
16
+
17
+ 5. **User-facing documentation per slice** — Update `CHANGELOG` `[Unreleased]`, and touch `GUIDE` / `SECURITY` / `UPGRADING` when user-visible behavior changes.
18
+
19
+ ## Done (v2 snapshot)
20
+
21
+ - `Mcp::Authenticator` — consolidated strategy resolution (replaces `McpHttpAuth` + `Mcp::HttpAuth`)
22
+ - `Mcp::Auth::Strategies::BearerToken` — static token + optional `token_resolver`, digest compare
23
+ - `Mcp::Auth::Strategies::Jwt` — host-supplied `jwt_decoder`; no JWT gem bundled
24
+ - `Config::Auth` — flat auth sub-config (`http_mcp_token`, `mcp_token_resolver`, `mcp_jwt_decoder`)
25
+ - `Config::Mcp` — MCP HTTP operational config (`rate_limit_*`, `http_log_json`, `authorize`, `require_auth_in_production`, `mode`, `security_profile`)
26
+ - `Mcp::HttpRateLimiter` — in-process sliding window per IP, mutex, prune empty buckets
27
+ - `Mcp::HttpStructuredLog` — one JSON line per MCP HTTP outcome; no token logging
28
+ - `HttpTransportApp` — single Rack entry: path match → auth → authorize → rate limit → log → transport
29
+ - Boot / config validation — `:bearer_token` requires resolver or static token; `require_auth_in_production` alignment
30
+ - Docs: GUIDE / SECURITY / UPGRADING / CHANGELOG / roadmaps
31
+
32
+ ## Next (follow-up, non-blocking)
33
+
34
+ - Log sampling (emit every Nth request instead of every request)
35
+ - Metrics hooks (expose request counts / rate-limit hits for APM)
36
+ - Heavier load testing / benchmarks
37
+ - Community-driven tweaks (e.g. per-route rate limits, response-size logging)
38
+
39
+ ## References
40
+
41
+ - [docs/roadmaps.md](roadmaps.md) — quick progress tables for all tracks
42
+ - [docs/GUIDE.md](GUIDE.md) — MCP HTTP configuration
43
+ - [docs/roadmap-context-assistants.md](roadmap-context-assistants.md) — IDE / context file improvements
44
+ - [SECURITY.md](../SECURITY.md) — threat model and production notes
45
+ - [UPGRADING.md](../UPGRADING.md) — breaking / new config knobs
data/docs/roadmaps.md ADDED
@@ -0,0 +1,48 @@
1
+ # Roadmaps — progress index
2
+
3
+ Entry point to see **which tracks exist** and **what is left**. Details live in each linked file; update the tables here when you close slices (or track work in GitHub Issues / Projects instead).
4
+
5
+ **Audience:** These documents are mainly for **maintainers and contributors**. Gem users usually rely on [GUIDE.md](GUIDE.md), [CHANGELOG.md](../CHANGELOG.md), and the README. Keeping roadmaps **in git** is normal for open-source gems (transparency, onboarding); they are not required to *use* the gem.
6
+
7
+ ## MCP HTTP track (`RailsAiBridge::Mcp`)
8
+
9
+ **Primary doc:** [roadmap-mcp-v2.md](roadmap-mcp-v2.md) (engineering principles + narrative snapshot).
10
+
11
+ | Area | Status |
12
+ |------|--------|
13
+ | Auth (`Mcp::Authenticator`, Bearer, JWT, resolver, boot guards) | Done |
14
+ | `authorize` + 403 | Done |
15
+ | In-process rate limit + bucket prune (`Mcp::HttpRateLimiter`) | Done |
16
+ | `mode` / `security_profile` + effective limits (`Config::Mcp`) | Done |
17
+ | JSON HTTP logging (`http_log_json`, `Mcp::HttpStructuredLog`) | Done |
18
+ | Docs (GUIDE / SECURITY / UPGRADING / CHANGELOG) for the above | Done |
19
+
20
+ **Optional / follow-up (non-blocking):** log sampling, metrics hooks, heavier load testing, community-driven tweaks.
21
+
22
+ ---
23
+
24
+ ## v2.0.0 — context files & assistant UX
25
+
26
+ **Primary doc:** [roadmap-context-assistants.md](roadmap-context-assistants.md).
27
+
28
+ | Area | Status |
29
+ |------|--------|
30
+ | Serializer formatter extraction (37 `Formatters::*` classes) | Done |
31
+ | `Config::Auth`, `Config::Server`, `Config::Introspection`, `Config::Output` façade | Done |
32
+ | `Config::Mcp` sub-object for MCP HTTP config | Done |
33
+ | `Mcp::Authenticator` consolidation | Done |
34
+ | Provider serializers extracted to `Serializers::Providers::` | Done |
35
+ | `SectionFormatter` template method base (DRY guard pattern) | Done |
36
+ | Concrete tasks per assistant / format | **In progress** |
37
+ | Major release (2.0.0) | **Deferred** until this track + communication plan |
38
+
39
+ ---
40
+
41
+ ## Where to look in the repo
42
+
43
+ | Need | File |
44
+ |------|------|
45
+ | Implementation rules (tests, YARD, docs per slice) | [roadmap-mcp-v2.md](roadmap-mcp-v2.md) § Engineering principles |
46
+ | What the MCP stack already includes (narrative) | [roadmap-mcp-v2.md](roadmap-mcp-v2.md) § Done vs upcoming |
47
+ | Plan for generated context (CLAUDE.md, rules, etc.) | [roadmap-context-assistants.md](roadmap-context-assistants.md) |
48
+ | User-facing shipped / upcoming changes | [CHANGELOG.md](../CHANGELOG.md) `[Unreleased]` and version sections |
@@ -24,53 +24,81 @@ module RailsAiBridge
24
24
 
25
25
  def create_initializer
26
26
  standard_count = RailsAiBridge::Configuration::PRESETS[:standard].size
27
- full_count = RailsAiBridge::Configuration::PRESETS[:full].size
27
+ full_count = RailsAiBridge::Configuration::PRESETS[:full].size
28
+ regulated_count = RailsAiBridge::Configuration::PRESETS[:regulated].size
28
29
 
29
30
  create_file "config/initializers/rails_ai_bridge.rb", <<~RUBY
30
31
  # frozen_string_literal: true
31
32
 
32
33
  RailsAiBridge.configure do |config|
33
- # Introspector preset:
34
- # :standard — #{standard_count} core introspectors (schema, models, routes, jobs, gems, conventions, controllers, tests, migrations)
35
- # :full — all #{full_count} introspectors (adds views, turbo, auth, API, config, assets, devops, etc.)
34
+ # --- Introspector preset ---
35
+ # :standard — #{standard_count} core introspectors (schema, models, routes, jobs, gems, conventions, controllers, tests, migrations)
36
+ # :full — all #{full_count} introspectors (adds views, turbo, auth, API, config, assets, devops, etc.)
37
+ # :regulated — #{regulated_count} introspectors — no schema/models/migrations (for apps with strict data governance)
36
38
  # config.preset = :standard
37
39
 
38
40
  # Or cherry-pick individual introspectors:
39
41
  # config.introspectors += %i[views turbo auth api]
40
42
 
41
- # Models to exclude from introspection
43
+ # Disable whole product categories at runtime (schema + models + migrations, api, views/turbo/i18n):
44
+ # config.disabled_introspection_categories << :domain_metadata
45
+
46
+ # --- Security exclusions ---
47
+ # Tables to hide from schema + model introspection (exact name or glob, e.g. "pii_*"):
48
+ # config.excluded_tables += %w[secrets audit_logs pii_*]
49
+
50
+ # Models to exclude from introspection:
42
51
  # config.excluded_models += %w[AdminUser InternalThing]
43
52
 
44
- # Paths to exclude from code search
53
+ # Paths excluded from rails_search_code:
45
54
  # config.excluded_paths += %w[vendor/bundle]
46
55
 
47
- # Context mode for generated files (CLAUDE.md, .cursorrules, etc.)
48
- # :compact — smart, ≤150 lines, references MCP tools for details (default)
49
- # :full — dumps everything into context files (good for small apps <30 models)
56
+ # --- Context output ---
57
+ # :compact — ≤150 lines, references MCP tools for details (default)
58
+ # :full — full dump (good for small apps)
50
59
  # config.context_mode = :compact
51
-
52
- # Max lines for CLAUDE.md in compact mode
53
60
  # config.claude_max_lines = 150
54
-
55
- # Max response size for MCP tool results (chars). Safety net for large apps.
56
61
  # config.max_tool_response_chars = 120_000
57
62
 
58
- # Optional: path to markdown merged into compact Copilot + Codex (default: config/rails_ai_bridge/overrides.md).
59
- # The install stub uses <!-- rails-ai-bridge:omit-merge --> on line 1 — delete it when adding real rules
60
- # (until then nothing is merged). See config/rails_ai_bridge/overrides.md.example for an outline.
63
+ # Team rules merged into compact Copilot/Codex output (remove omit-merge line when ready):
61
64
  # config.assistant_overrides_path = "config/rails_ai_bridge/overrides.md"
62
65
 
63
- # Compact file model name caps (0 = MCP pointer only, no names listed)
66
+ # Compact model list caps (0 = MCP pointer only, no names listed):
64
67
  # config.copilot_compact_model_list_limit = 5
65
68
  # config.codex_compact_model_list_limit = 3
66
69
 
67
- # Auto-mount HTTP MCP endpoint at /mcp (see SECURITY.md — production needs token + explicit opt-in)
70
+ # ==========================================================================
71
+ # HTTP MCP / auto_mount — SECURITY CRITICAL
72
+ # ==========================================================================
73
+ # Exposes read-only MCP tools over HTTP. Still reveals routes, schema, and
74
+ # code layout — treat as sensitive. Prefer stdio (`rails ai:serve`) for local
75
+ # AI clients.
76
+ #
77
+ # In production you MUST configure one auth mechanism AND set
78
+ # allow_auto_mount_in_production = true. Options (highest priority first):
79
+ #
80
+ # 1. JWT decoder (no JWT gem required — supply your own lambda):
81
+ # config.mcp_jwt_decoder = ->(token) {
82
+ # JWT.decode(token, credentials.jwt_secret, true, algorithm: "HS256").first
83
+ # rescue JWT::DecodeError
84
+ # nil
85
+ # }
86
+ #
87
+ # 2. Token resolver (Devise, database lookup, etc.):
88
+ # config.mcp_token_resolver = ->(token) { User.find_by(mcp_api_token: token) }
89
+ #
90
+ # 3. Static shared secret:
91
+ # config.http_mcp_token = "generate-a-long-random-secret"
92
+ # # ENV["RAILS_AI_BRIDGE_MCP_TOKEN"] overrides this when set
93
+ #
94
+ # IMPORTANT: Token comparison is timing-safe but does NOT prevent
95
+ # brute-force guessing. Add rate limiting on the MCP endpoint in
96
+ # production (e.g. Rack::Attack throttle on config.http_path).
97
+ #
68
98
  # config.auto_mount = false
69
99
  # config.allow_auto_mount_in_production = false
70
- # config.http_mcp_token = "generate-a-long-random-secret"
71
- # ENV["RAILS_AI_BRIDGE_MCP_TOKEN"] overrides http_mcp_token when set
72
- # config.http_path = "/mcp"
73
- # config.http_port = 6029
100
+ # config.http_path = "/mcp"
101
+ # config.http_port = 6029
74
102
  end
75
103
  RUBY
76
104
 
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module RailsAiBridge
6
+ # Reads and writes which assistant formats +rails ai:bridge+ regenerates by default.
7
+ #
8
+ # The install generator creates {RELATIVE_PATH} with a YAML +formats+ list (a subset of
9
+ # {FORMAT_KEYS}). When the file is missing, invalid, or lists no recognized formats,
10
+ # callers treat the result as +nil+ and fall back to generating all formats.
11
+ #
12
+ # @example install.yml
13
+ # formats:
14
+ # - claude
15
+ # - codex
16
+ #
17
+ # @see RailsAiBridge::Generators::InstallGenerator
18
+ class AssistantFormatsPreference
19
+ # Path of the preference file relative to +Rails.root+.
20
+ RELATIVE_PATH = "config/rails_ai_bridge/install.yml"
21
+
22
+ # All recognized format keys (order is not significant).
23
+ FORMAT_KEYS = %i[claude codex cursor windsurf copilot json].freeze
24
+
25
+ class << self
26
+ # Absolute path to the preference file for the current Rails application.
27
+ #
28
+ # @return [Pathname, nil] +nil+ when +Rails.application+ is unavailable
29
+ def config_path
30
+ return nil unless defined?(Rails) && Rails.application
31
+
32
+ Rails.root.join(RELATIVE_PATH)
33
+ end
34
+
35
+ # Returns the subset of formats requested for the default +rails ai:bridge+ task.
36
+ # Returns +nil+ when the file is absent, invalid, or contains no recognized formats
37
+ # (callers should interpret +nil+ as "generate all formats").
38
+ #
39
+ # @return [Array<Symbol>, nil]
40
+ def formats_for_default_bridge_task
41
+ path = config_path
42
+ return nil unless path&.file?
43
+
44
+ data = load_yaml(path)
45
+ return nil unless data.is_a?(Hash)
46
+
47
+ raw = data["formats"] || data[:formats]
48
+ return nil if raw.nil?
49
+
50
+ fmts = Array(raw).map { |f| f.to_s.downcase.to_sym } & FORMAT_KEYS
51
+ fmts.empty? ? nil : fmts.uniq
52
+ end
53
+
54
+ # Writes +install.yml+ with the requested formats filtered against {FORMAT_KEYS}.
55
+ #
56
+ # @param formats [Array<Symbol, String>] desired format keys
57
+ # @raise [RailsAiBridge::Error] when +Rails.application+ is unavailable
58
+ # @return [void]
59
+ def write!(formats:)
60
+ path = config_path
61
+ raise Error, "Rails app not available" unless path
62
+
63
+ path.dirname.mkpath
64
+ list = Array(formats).map { |f| f.to_s.downcase }.uniq & FORMAT_KEYS.map(&:to_s)
65
+ File.write(path.to_s, YAML.dump({ "formats" => list }))
66
+ end
67
+
68
+ private
69
+
70
+ # @param path [Pathname]
71
+ # @return [Hash, nil] parsed data or +nil+ on syntax/IO error
72
+ def load_yaml(path)
73
+ YAML.safe_load(
74
+ File.read(path),
75
+ permitted_classes: [ Symbol ],
76
+ permitted_symbols: [],
77
+ aliases: true
78
+ )
79
+ rescue Psych::SyntaxError, Errno::ENOENT
80
+ nil
81
+ end
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiBridge
4
+ module Config
5
+ # Holds authentication and authorization settings for the MCP HTTP endpoint.
6
+ #
7
+ # Strategy priority (highest → lowest):
8
+ # 1. {#mcp_jwt_decoder} — caller-supplied JWT decoding lambda
9
+ # 2. {#mcp_token_resolver} — caller-supplied token resolution lambda
10
+ # 3. {#http_mcp_token} / +ENV["RAILS_AI_BRIDGE_MCP_TOKEN"]+ — static bearer token
11
+ # 4. None configured — open access
12
+ #
13
+ # @see Mcp::Authenticator
14
+ class Auth
15
+ # @return [String, nil] static bearer token for HTTP MCP auth
16
+ attr_accessor :http_mcp_token
17
+
18
+ # @return [Boolean] allow auto-mounting the MCP endpoint in production
19
+ attr_accessor :allow_auto_mount_in_production
20
+
21
+ # @return [Proc, nil] lambda resolving a raw bearer token to an auth context
22
+ attr_accessor :mcp_token_resolver
23
+
24
+ # @return [Proc, nil] lambda decoding a raw JWT to a payload hash
25
+ attr_accessor :mcp_jwt_decoder
26
+
27
+ def initialize
28
+ @http_mcp_token = nil
29
+ @allow_auto_mount_in_production = false
30
+ @mcp_token_resolver = nil
31
+ @mcp_jwt_decoder = nil
32
+ end
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsAiBridge
4
+ module Config
5
+ # Holds introspector selection, exclusion rules, and caching settings.
6
+ class Introspection
7
+ # @return [Array<Symbol>] active introspector keys
8
+ attr_accessor :introspectors
9
+
10
+ # @return [Array<String>] directory names excluded from code search
11
+ attr_accessor :excluded_paths
12
+
13
+ # @return [Array<String>] model class names excluded from introspection
14
+ attr_accessor :excluded_models
15
+
16
+ # @return [Array<String>] table names/patterns excluded from schema introspection
17
+ attr_accessor :excluded_tables
18
+
19
+ # @return [Array<Symbol>] product-level category keys that subtract introspectors at runtime
20
+ attr_accessor :disabled_introspection_categories
21
+
22
+ # @return [Integer] TTL in seconds for cached introspection results
23
+ attr_accessor :cache_ttl
24
+
25
+ # @return [Boolean] include credential key names in config introspection output
26
+ attr_accessor :expose_credentials_key_names
27
+
28
+ # @return [Hash{Symbol => Class}] additional custom introspector classes
29
+ attr_accessor :additional_introspectors
30
+
31
+ # @return [Array<String>] extra file extensions allowed for rails_search_code
32
+ attr_accessor :search_code_allowed_file_types
33
+
34
+ def initialize
35
+ @introspectors = Configuration::PRESETS[:standard].dup
36
+ @excluded_paths = %w[node_modules tmp log vendor .git]
37
+ @excluded_models = %w[
38
+ ApplicationRecord
39
+ ActiveStorage::Blob ActiveStorage::Attachment ActiveStorage::VariantRecord
40
+ ActionText::RichText ActionText::EncryptedRichText
41
+ ActionMailbox::InboundEmail ActionMailbox::Record
42
+ ]
43
+ @excluded_tables = []
44
+ @disabled_introspection_categories = []
45
+ @cache_ttl = 30
46
+ @expose_credentials_key_names = false
47
+ @additional_introspectors = {}
48
+ @search_code_allowed_file_types = []
49
+ end
50
+
51
+ # Switch the active introspector list to a named preset.
52
+ #
53
+ # @param name [Symbol, String] preset key from {Configuration::PRESETS}
54
+ # @raise [ArgumentError] when the preset is unknown
55
+ def preset=(name)
56
+ name = name.to_sym
57
+ unless Configuration::PRESETS.key?(name)
58
+ raise ArgumentError, "Unknown preset: #{name}. Valid presets: #{Configuration::PRESETS.keys.join(', ')}"
59
+ end
60
+
61
+ @introspectors = Configuration::PRESETS[name].dup
62
+ end
63
+
64
+ # Introspectors after removing those disabled by {#disabled_introspection_categories}.
65
+ #
66
+ # @return [Array<Symbol>]
67
+ def effective_introspectors
68
+ disabled = @disabled_introspection_categories.flat_map do |c|
69
+ Configuration::INTROSPECTION_CATEGORY_INTROSPECTORS[c.to_sym] || []
70
+ end.uniq
71
+ @introspectors.reject { |i| disabled.include?(i) }
72
+ end
73
+
74
+ # Whether a table name matches any {#excluded_tables} pattern (exact or glob).
75
+ #
76
+ # @param table_name [String, nil]
77
+ # @return [Boolean]
78
+ def excluded_table?(table_name)
79
+ return false if table_name.nil? || table_name.to_s.empty?
80
+
81
+ @excluded_tables.any? { |pat| ExclusionHelper.table_pattern_match?(pat.to_s, table_name.to_s) }
82
+ end
83
+ end
84
+ end
85
+ end