rails-ai-context 0.15.5 → 0.15.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +28 -5
- data/docs/GUIDE.md +182 -6
- data/lib/rails_ai_context/configuration.rb +55 -0
- data/lib/rails_ai_context/introspectors/controller_introspector.rb +4 -7
- data/lib/rails_ai_context/introspectors/model_introspector.rb +13 -4
- data/lib/rails_ai_context/introspectors/schema_introspector.rb +5 -3
- data/lib/rails_ai_context/introspectors/view_template_introspector.rb +4 -5
- data/lib/rails_ai_context/tools/get_config.rb +2 -16
- data/lib/rails_ai_context/tools/get_controllers.rb +2 -4
- data/lib/rails_ai_context/tools/get_edit_context.rb +4 -2
- data/lib/rails_ai_context/tools/get_gems.rb +3 -1
- data/lib/rails_ai_context/tools/get_model_details.rb +58 -8
- data/lib/rails_ai_context/tools/get_routes.rb +11 -15
- data/lib/rails_ai_context/tools/get_schema.rb +6 -9
- data/lib/rails_ai_context/tools/get_test_info.rb +11 -3
- data/lib/rails_ai_context/tools/get_view.rb +9 -4
- data/lib/rails_ai_context/tools/search_code.rb +8 -4
- data/lib/rails_ai_context/tools/validate.rb +6 -6
- data/lib/rails_ai_context/version.rb +1 -1
- data/server.json +2 -2
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0eb46f9da0fc24418021dbb2ca4a6850017ab555ac0bbe98b589777ef30593a2
|
|
4
|
+
data.tar.gz: 5b77c8e0b1c7832c17ea84fffdc96524060fc594e4691f11a84b5e010158f925
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 13a6f6e8e274cf47210b6cb94f160ba00e4cebe3349eb6c1e2612b4ec53561ab87d99690c9949c4eae389427bf7e80850b1c5a930283792bfa3294010303ec04
|
|
7
|
+
data.tar.gz: 5ae9e78447e0ec33496b608729be0088bf57f180a37f27690f7eaa0b6ca0a3c1a6637887a747d3de67bec808636a139be88e2b9ba72e71db0e07faa156541182
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.15.6] - 2026-03-22
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- **7 new configurable options** — `excluded_controllers`, `excluded_route_prefixes`, `excluded_concerns`, `excluded_filters`, `excluded_middleware`, `search_extensions`, `concern_paths` for stack-specific customization.
|
|
13
|
+
- **Configurable file size limits** — `max_file_size`, `max_test_file_size`, `max_schema_file_size`, `max_view_total_size`, `max_view_file_size`, `max_search_results`, `max_validate_files` all exposed via `Configuration`.
|
|
14
|
+
- **Class methods in model detail** — `rails_get_model_details` now shows class methods section.
|
|
15
|
+
- **Custom validate methods** — `validate :method_name` calls extracted from source and shown in model detail.
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
|
|
19
|
+
- **Schema defaults always visible** — Null and Default columns always shown (NOT NULL marked bold). Previous token-saving logic accidentally hid critical migration data.
|
|
20
|
+
- **Optional associations** — `belongs_to` with `optional: true` now shows `[optional]` flag.
|
|
21
|
+
- **Concern methods inline** — shows public methods from concern source files (e.g. `PlanLimitable — can_cook?, increment_cook_count!`).
|
|
22
|
+
- **MCP tool error messages** — all tools now show available values on error/not-found for AI self-correction.
|
|
23
|
+
|
|
8
24
|
## [0.15.5] - 2026-03-22
|
|
9
25
|
|
|
10
26
|
### Fixed
|
data/README.md
CHANGED
|
@@ -295,22 +295,45 @@ end
|
|
|
295
295
|
|
|
296
296
|
| Option | Default | Description |
|
|
297
297
|
|--------|---------|-------------|
|
|
298
|
+
| **Presets & Introspectors** | | |
|
|
298
299
|
| `preset` | `:standard` | Introspector preset (`:standard` or `:full`) |
|
|
299
300
|
| `introspectors` | 13 core | Array of introspector symbols |
|
|
301
|
+
| **Context Generation** | | |
|
|
300
302
|
| `context_mode` | `:compact` | `:compact` (≤150 lines) or `:full` (dump everything) |
|
|
301
303
|
| `claude_max_lines` | `150` | Max lines for CLAUDE.md in compact mode |
|
|
302
|
-
| `
|
|
303
|
-
| `
|
|
304
|
-
|
|
|
305
|
-
| `
|
|
304
|
+
| `generate_root_files` | `true` | Generate root files (CLAUDE.md, etc.) — set `false` for split rules only |
|
|
305
|
+
| `output_dir` | `Rails.root` | Output directory for generated context files |
|
|
306
|
+
| **MCP Server** | | |
|
|
307
|
+
| `server_name` | `"rails-ai-context"` | MCP server name |
|
|
308
|
+
| `server_version` | gem version | MCP server version |
|
|
306
309
|
| `auto_mount` | `false` | Auto-mount HTTP MCP endpoint |
|
|
307
310
|
| `http_path` | `"/mcp"` | HTTP endpoint path |
|
|
308
311
|
| `http_port` | `6029` | HTTP server port |
|
|
309
312
|
| `http_bind` | `"127.0.0.1"` | HTTP server bind address |
|
|
310
313
|
| `cache_ttl` | `30` | Cache TTL in seconds |
|
|
314
|
+
| `max_tool_response_chars` | `120_000` | Safety cap for MCP tool responses |
|
|
311
315
|
| `live_reload` | `:auto` | `:auto`, `true`, or `false` — MCP live reload |
|
|
312
316
|
| `live_reload_debounce` | `1.5` | Debounce interval in seconds |
|
|
313
|
-
|
|
|
317
|
+
| **Filtering & Exclusions** | | |
|
|
318
|
+
| `excluded_models` | internal Rails models | Models to skip during introspection |
|
|
319
|
+
| `excluded_paths` | `node_modules tmp log vendor .git` | Paths excluded from code search |
|
|
320
|
+
| `sensitive_patterns` | `.env .env.* config/master.key config/credentials.yml.enc config/credentials/*.yml.enc *.pem *.key` | File patterns blocked from search and read tools |
|
|
321
|
+
| `excluded_controllers` | `DeviseController` etc. | Controller classes hidden from listings |
|
|
322
|
+
| `excluded_route_prefixes` | `action_mailbox/ active_storage/ rails/` etc. | Route controller prefixes hidden with app_only |
|
|
323
|
+
| `excluded_concerns` | Rails/Devise/framework patterns | Regex patterns for concerns to hide |
|
|
324
|
+
| `excluded_filters` | `verify_authenticity_token` etc. | Framework filter names hidden from controller output |
|
|
325
|
+
| `excluded_middleware` | standard Rack/Rails middleware | Default middleware hidden from config output |
|
|
326
|
+
| **File Size Limits** | | |
|
|
327
|
+
| `max_file_size` | `2_000_000` | Per-file read limit for tools (bytes) |
|
|
328
|
+
| `max_test_file_size` | `500_000` | Test file read limit (bytes) |
|
|
329
|
+
| `max_schema_file_size` | `10_000_000` | schema.rb / structure.sql parse limit (bytes) |
|
|
330
|
+
| `max_view_total_size` | `5_000_000` | Total aggregated view content for UI patterns (bytes) |
|
|
331
|
+
| `max_view_file_size` | `500_000` | Per-view file during aggregation (bytes) |
|
|
332
|
+
| `max_search_results` | `100` | Max search results per call |
|
|
333
|
+
| `max_validate_files` | `20` | Max files per validate call |
|
|
334
|
+
| **Search & Discovery** | | |
|
|
335
|
+
| `search_extensions` | `rb js erb yml yaml json ts tsx vue svelte haml slim` | File extensions for Ruby fallback search |
|
|
336
|
+
| `concern_paths` | `app/models/concerns app/controllers/concerns` | Where to look for concern source files |
|
|
314
337
|
</details>
|
|
315
338
|
|
|
316
339
|
---
|
data/docs/GUIDE.md
CHANGED
|
@@ -297,6 +297,8 @@ Returns model details: associations, validations, scopes, enums, callbacks, conc
|
|
|
297
297
|
|-------|------|-------------|
|
|
298
298
|
| `model` | string | Model class name (e.g. `User`). Case-insensitive. Omit for listing. |
|
|
299
299
|
| `detail` | string | `summary` / `standard` (default) / `full`. Ignored when model is specified. |
|
|
300
|
+
| `limit` | integer | Max models to return when listing. Default: 50. |
|
|
301
|
+
| `offset` | integer | Skip models for pagination. Default: 0. |
|
|
300
302
|
|
|
301
303
|
**Examples:**
|
|
302
304
|
|
|
@@ -329,6 +331,7 @@ Returns all routes: HTTP verbs, paths, controller actions, route names.
|
|
|
329
331
|
| `detail` | string | `summary` / `standard` (default) / `full` |
|
|
330
332
|
| `limit` | integer | Max routes to return. Default: 100 (standard), 200 (full). |
|
|
331
333
|
| `offset` | integer | Skip routes for pagination. Default: 0. |
|
|
334
|
+
| `app_only` | boolean | Filter out internal Rails routes (Active Storage, Action Mailbox, Conductor, etc.). Default: true. |
|
|
332
335
|
|
|
333
336
|
**Examples:**
|
|
334
337
|
|
|
@@ -386,7 +389,7 @@ rails_get_controllers(detail: "full")
|
|
|
386
389
|
|
|
387
390
|
Returns application configuration. No parameters.
|
|
388
391
|
|
|
389
|
-
**Returns:** cache store, session store, timezone, queue adapter, mailer settings, custom middleware, initializers,
|
|
392
|
+
**Returns:** cache store, session store, timezone, queue adapter, mailer settings, custom middleware (framework defaults are filtered out), notable initializers, CurrentAttributes classes.
|
|
390
393
|
|
|
391
394
|
```
|
|
392
395
|
rails_get_config()
|
|
@@ -441,6 +444,108 @@ rails_get_conventions()
|
|
|
441
444
|
→ Architecture: [MVC, Service objects, Concerns], Patterns: [STI, Polymorphism], ...
|
|
442
445
|
```
|
|
443
446
|
|
|
447
|
+
### rails_get_stimulus
|
|
448
|
+
|
|
449
|
+
Returns Stimulus controller details: targets, values, actions, outlets, classes.
|
|
450
|
+
|
|
451
|
+
**Parameters:**
|
|
452
|
+
|
|
453
|
+
| Param | Type | Description |
|
|
454
|
+
|-------|------|-------------|
|
|
455
|
+
| `controller` | string | Specific Stimulus controller name (e.g. `hello`, `filter-form`). Case-insensitive. |
|
|
456
|
+
| `detail` | string | `summary` / `standard` (default) / `full` |
|
|
457
|
+
| `limit` | integer | Max controllers to return when listing. Default: 50. |
|
|
458
|
+
| `offset` | integer | Skip controllers for pagination. Default: 0. |
|
|
459
|
+
|
|
460
|
+
**Examples:**
|
|
461
|
+
|
|
462
|
+
```
|
|
463
|
+
rails_get_stimulus()
|
|
464
|
+
→ Standard: controller names with targets and actions
|
|
465
|
+
|
|
466
|
+
rails_get_stimulus(detail: "summary")
|
|
467
|
+
→ Names with target/action counts
|
|
468
|
+
|
|
469
|
+
rails_get_stimulus(controller: "filter-form")
|
|
470
|
+
→ Full detail: targets, actions, values, outlets, classes, file path
|
|
471
|
+
|
|
472
|
+
rails_get_stimulus(detail: "full")
|
|
473
|
+
→ All controllers with all details
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
### rails_get_view
|
|
477
|
+
|
|
478
|
+
Returns view template contents, partials, and Stimulus controller references.
|
|
479
|
+
|
|
480
|
+
**Parameters:**
|
|
481
|
+
|
|
482
|
+
| Param | Type | Description |
|
|
483
|
+
|-------|------|-------------|
|
|
484
|
+
| `controller` | string | Filter views by controller name (e.g. `cooks`, `brand_profiles`). Use `layouts` for layout files. |
|
|
485
|
+
| `path` | string | Specific view path relative to `app/views` (e.g. `cooks/index.html.erb`). Returns full content. |
|
|
486
|
+
| `detail` | string | `summary` / `standard` (default) / `full` |
|
|
487
|
+
|
|
488
|
+
**Examples:**
|
|
489
|
+
|
|
490
|
+
```
|
|
491
|
+
rails_get_view()
|
|
492
|
+
→ Standard: all view files with partial/stimulus refs
|
|
493
|
+
|
|
494
|
+
rails_get_view(controller: "cooks")
|
|
495
|
+
→ All templates and partials for CooksController
|
|
496
|
+
|
|
497
|
+
rails_get_view(path: "cooks/index.html.erb")
|
|
498
|
+
→ Full template content
|
|
499
|
+
|
|
500
|
+
rails_get_view(controller: "layouts")
|
|
501
|
+
→ Layout files
|
|
502
|
+
|
|
503
|
+
rails_get_view(controller: "cooks", detail: "full")
|
|
504
|
+
→ Full template content for all cooks views
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### rails_get_edit_context
|
|
508
|
+
|
|
509
|
+
Returns just enough context to make a surgical Edit to a file. Returns the target area with line numbers and surrounding code.
|
|
510
|
+
|
|
511
|
+
**Parameters:**
|
|
512
|
+
|
|
513
|
+
| Param | Type | Description |
|
|
514
|
+
|-------|------|-------------|
|
|
515
|
+
| `file` | string | **Required.** File path relative to Rails root (e.g. `app/models/cook.rb`). |
|
|
516
|
+
| `near` | string | **Required.** What to find — a method name, keyword, or string to locate (e.g. `scope`, `def index`). |
|
|
517
|
+
| `context_lines` | integer | Lines of context above and below the match. Default: 5. |
|
|
518
|
+
|
|
519
|
+
**Examples:**
|
|
520
|
+
|
|
521
|
+
```
|
|
522
|
+
rails_get_edit_context(file: "app/models/cook.rb", near: "scope")
|
|
523
|
+
→ Code around the first scope with line numbers, expanded to full method
|
|
524
|
+
|
|
525
|
+
rails_get_edit_context(file: "app/controllers/cooks_controller.rb", near: "def index")
|
|
526
|
+
→ The index action source with surrounding context
|
|
527
|
+
```
|
|
528
|
+
|
|
529
|
+
### rails_validate
|
|
530
|
+
|
|
531
|
+
Validates syntax of multiple files at once (Ruby, ERB, JavaScript).
|
|
532
|
+
|
|
533
|
+
**Parameters:**
|
|
534
|
+
|
|
535
|
+
| Param | Type | Description |
|
|
536
|
+
|-------|------|-------------|
|
|
537
|
+
| `files` | array | **Required.** File paths relative to Rails root (e.g. `["app/models/cook.rb", "app/views/cooks/index.html.erb"]`). |
|
|
538
|
+
|
|
539
|
+
**Examples:**
|
|
540
|
+
|
|
541
|
+
```
|
|
542
|
+
rails_validate(files: ["app/models/cook.rb"])
|
|
543
|
+
→ ✓ app/models/cook.rb — syntax OK
|
|
544
|
+
|
|
545
|
+
rails_validate(files: ["app/models/cook.rb", "app/controllers/cooks_controller.rb", "app/views/cooks/index.html.erb"])
|
|
546
|
+
→ Checks all three files, reports pass/fail for each
|
|
547
|
+
```
|
|
548
|
+
|
|
444
549
|
### rails_search_code
|
|
445
550
|
|
|
446
551
|
Ripgrep-powered regex search across the codebase.
|
|
@@ -450,8 +555,10 @@ Ripgrep-powered regex search across the codebase.
|
|
|
450
555
|
| Param | Type | Description |
|
|
451
556
|
|-------|------|-------------|
|
|
452
557
|
| `pattern` | string | **Required.** Regex pattern to search for. |
|
|
558
|
+
| `path` | string | Subdirectory to search in (e.g. `app/models`, `config`). Default: entire app. |
|
|
453
559
|
| `file_type` | string | Filter by file type (e.g. `rb`, `erb`, `js`). Alphanumeric only. |
|
|
454
|
-
| `max_results` | integer | Max results to return. Default:
|
|
560
|
+
| `max_results` | integer | Max results to return. Default: 30, max: 100. |
|
|
561
|
+
| `context_lines` | integer | Lines of context before and after each match (like grep -C). Default: 0, max: 5. |
|
|
455
562
|
|
|
456
563
|
**Examples:**
|
|
457
564
|
|
|
@@ -464,18 +571,28 @@ rails_search_code(pattern: "class.*Controller", file_type: "rb")
|
|
|
464
571
|
|
|
465
572
|
rails_search_code(pattern: "def create", file_type: "rb", max_results: 50)
|
|
466
573
|
→ First 50 create methods across the codebase
|
|
574
|
+
|
|
575
|
+
rails_search_code(pattern: "current_user", path: "app/controllers")
|
|
576
|
+
→ Search only in app/controllers/
|
|
577
|
+
|
|
578
|
+
rails_search_code(pattern: "validates", context_lines: 2)
|
|
579
|
+
→ Matches with 2 lines of context before and after
|
|
467
580
|
```
|
|
468
581
|
|
|
469
|
-
**Security:** Uses `Open3.capture2` with array arguments (no shell injection). Validates file_type. Blocks path traversal. Respects `excluded_paths` config.
|
|
582
|
+
**Security:** Uses `Open3.capture2` with array arguments (no shell injection). Validates file_type. Blocks path traversal. Respects `excluded_paths` and `sensitive_patterns` config.
|
|
470
583
|
|
|
471
584
|
### Detail Level Summary
|
|
472
585
|
|
|
473
|
-
|
|
474
|
-
|
|
586
|
+
All tools that support `detail` use these three levels. Default limits vary by tool — schema defaults shown below:
|
|
587
|
+
|
|
588
|
+
| Level | What it returns | Schema default limit | Best for |
|
|
589
|
+
|-------|----------------|---------------------|----------|
|
|
475
590
|
| `summary` | Names + counts | 50 | Getting the landscape, understanding what exists |
|
|
476
591
|
| `standard` | Names + key details | 15 | Working context, column types, action names |
|
|
477
592
|
| `full` | Everything | 5 | Deep inspection, indexes, FKs, constraints |
|
|
478
593
|
|
|
594
|
+
Other tools default to higher limits (e.g. models/controllers/stimulus: 50 for all levels, routes: 100/200).
|
|
595
|
+
|
|
479
596
|
### Recommended Workflow
|
|
480
597
|
|
|
481
598
|
1. **Start with `detail:"summary"`** to see what exists
|
|
@@ -636,6 +753,49 @@ RailsAiContext.configure do |config|
|
|
|
636
753
|
# Paths to exclude from code search
|
|
637
754
|
config.excluded_paths += %w[vendor/bundle]
|
|
638
755
|
|
|
756
|
+
# Sensitive file patterns blocked from search and read tools
|
|
757
|
+
# config.sensitive_patterns += %w[config/my_secret.yml]
|
|
758
|
+
|
|
759
|
+
# Controllers hidden from listings (e.g. Devise internals)
|
|
760
|
+
# config.excluded_controllers += %w[MyInternalController]
|
|
761
|
+
|
|
762
|
+
# Route prefixes hidden with app_only (e.g. admin frameworks)
|
|
763
|
+
# config.excluded_route_prefixes += %w[admin/]
|
|
764
|
+
|
|
765
|
+
# Regex patterns for concerns to hide from model output
|
|
766
|
+
# config.excluded_concerns += [/MyInternal::/]
|
|
767
|
+
|
|
768
|
+
# Framework filter names hidden from controller output
|
|
769
|
+
# config.excluded_filters += %w[my_internal_filter]
|
|
770
|
+
|
|
771
|
+
# Default middleware hidden from config output
|
|
772
|
+
# config.excluded_middleware += %w[MyMiddleware]
|
|
773
|
+
|
|
774
|
+
# --- File size limits ---
|
|
775
|
+
|
|
776
|
+
# Per-file read limit for tools (default: 2MB)
|
|
777
|
+
# config.max_file_size = 2_000_000
|
|
778
|
+
|
|
779
|
+
# Test file read limit (default: 500KB)
|
|
780
|
+
# config.max_test_file_size = 500_000
|
|
781
|
+
|
|
782
|
+
# schema.rb / structure.sql parse limit (default: 10MB)
|
|
783
|
+
# config.max_schema_file_size = 10_000_000
|
|
784
|
+
|
|
785
|
+
# Max search results per call (default: 100)
|
|
786
|
+
# config.max_search_results = 100
|
|
787
|
+
|
|
788
|
+
# Max files per validate call (default: 20)
|
|
789
|
+
# config.max_validate_files = 20
|
|
790
|
+
|
|
791
|
+
# --- Search and file discovery ---
|
|
792
|
+
|
|
793
|
+
# File extensions for Ruby fallback search
|
|
794
|
+
# config.search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
|
|
795
|
+
|
|
796
|
+
# Where to look for concern source files
|
|
797
|
+
# config.concern_paths = %w[app/models/concerns app/controllers/concerns]
|
|
798
|
+
|
|
639
799
|
# --- Live reload ---
|
|
640
800
|
|
|
641
801
|
# Auto-invalidate MCP tool caches on file changes
|
|
@@ -667,6 +827,7 @@ end
|
|
|
667
827
|
| `cache_ttl` | Integer | `30` | Cache TTL in seconds for introspection results |
|
|
668
828
|
| `excluded_models` | Array | internal Rails models | Models to skip |
|
|
669
829
|
| `excluded_paths` | Array | `node_modules tmp log vendor .git` | Paths excluded from code search |
|
|
830
|
+
| `sensitive_patterns` | Array | `.env`, `.key`, `.pem`, credentials | File patterns blocked from search and read tools |
|
|
670
831
|
| `output_dir` | String | `nil` (Rails.root) | Where to write context files |
|
|
671
832
|
| `auto_mount` | Boolean | `false` | Auto-mount HTTP MCP endpoint |
|
|
672
833
|
| `http_path` | String | `"/mcp"` | HTTP endpoint path |
|
|
@@ -675,7 +836,22 @@ end
|
|
|
675
836
|
| `live_reload` | Symbol/Boolean | `:auto` | `:auto`, `true`, or `false` — enable MCP live reload |
|
|
676
837
|
| `live_reload_debounce` | Float | `1.5` | Debounce interval in seconds for live reload |
|
|
677
838
|
| `server_name` | String | `"rails-ai-context"` | MCP server name |
|
|
839
|
+
| `server_version` | String | gem version | MCP server version |
|
|
678
840
|
| `generate_root_files` | Boolean | `true` | Generate root files (CLAUDE.md, .windsurfrules, etc.) — set `false` for split rules only |
|
|
841
|
+
| `max_file_size` | Integer | `2_000_000` | Per-file read limit for tools (2MB) |
|
|
842
|
+
| `max_test_file_size` | Integer | `500_000` | Test file read limit (500KB) |
|
|
843
|
+
| `max_schema_file_size` | Integer | `10_000_000` | schema.rb / structure.sql parse limit (10MB) |
|
|
844
|
+
| `max_view_total_size` | Integer | `5_000_000` | Total aggregated view content for UI patterns (5MB) |
|
|
845
|
+
| `max_view_file_size` | Integer | `500_000` | Per-view file during aggregation (500KB) |
|
|
846
|
+
| `max_search_results` | Integer | `100` | Max search results per call |
|
|
847
|
+
| `max_validate_files` | Integer | `20` | Max files per validate call |
|
|
848
|
+
| `excluded_controllers` | Array | `DeviseController`, etc. | Controller classes hidden from listings |
|
|
849
|
+
| `excluded_route_prefixes` | Array | `action_mailbox/`, `active_storage/`, etc. | Route controller prefixes hidden with `app_only` |
|
|
850
|
+
| `excluded_concerns` | Array | framework regex patterns | Regex patterns for concerns to hide from model output |
|
|
851
|
+
| `excluded_filters` | Array | `verify_authenticity_token`, etc. | Framework filter names hidden from controller output |
|
|
852
|
+
| `excluded_middleware` | Array | standard Rails middleware | Default middleware hidden from config output |
|
|
853
|
+
| `search_extensions` | Array | `rb js erb yml yaml json ts tsx vue svelte haml slim` | File extensions for Ruby fallback search |
|
|
854
|
+
| `concern_paths` | Array | `app/models/concerns app/controllers/concerns` | Where to look for concern source files |
|
|
679
855
|
|
|
680
856
|
### Root file generation
|
|
681
857
|
|
|
@@ -750,7 +926,7 @@ config.preset = :full
|
|
|
750
926
|
|
|
751
927
|
```ruby
|
|
752
928
|
# Start with standard, add specific ones
|
|
753
|
-
config.introspectors += %i[views turbo auth api
|
|
929
|
+
config.introspectors += %i[views turbo auth api]
|
|
754
930
|
|
|
755
931
|
# Or build from scratch
|
|
756
932
|
config.introspectors = %i[schema models routes gems auth api]
|
|
@@ -60,6 +60,26 @@ module RailsAiContext
|
|
|
60
60
|
# When false, only generates split rule files (.claude/rules/, .cursor/rules/, etc.)
|
|
61
61
|
attr_accessor :generate_root_files
|
|
62
62
|
|
|
63
|
+
# File size limits (bytes) — increase for larger projects
|
|
64
|
+
attr_accessor :max_file_size # Per-file read limit for tools (default: 2MB)
|
|
65
|
+
attr_accessor :max_test_file_size # Test file read limit (default: 500KB)
|
|
66
|
+
attr_accessor :max_schema_file_size # schema.rb / structure.sql parse limit (default: 10MB)
|
|
67
|
+
attr_accessor :max_view_total_size # Total aggregated view content for UI patterns (default: 5MB)
|
|
68
|
+
attr_accessor :max_view_file_size # Per-view file during aggregation (default: 500KB)
|
|
69
|
+
attr_accessor :max_search_results # Max search results per call (default: 100)
|
|
70
|
+
attr_accessor :max_validate_files # Max files per validate call (default: 20)
|
|
71
|
+
|
|
72
|
+
# Filtering — customize what's hidden from AI output
|
|
73
|
+
attr_accessor :excluded_controllers # Controller classes hidden from listings (e.g. DeviseController)
|
|
74
|
+
attr_accessor :excluded_route_prefixes # Route controller prefixes hidden with app_only (e.g. action_mailbox/)
|
|
75
|
+
attr_accessor :excluded_concerns # Regex patterns for concerns to hide (e.g. /Devise::Models/)
|
|
76
|
+
attr_accessor :excluded_filters # Framework filter names hidden from controller output
|
|
77
|
+
attr_accessor :excluded_middleware # Default middleware hidden from config output
|
|
78
|
+
|
|
79
|
+
# Search and file discovery
|
|
80
|
+
attr_accessor :search_extensions # File extensions for Ruby fallback search (default: rb,js,erb,yml,yaml,json)
|
|
81
|
+
attr_accessor :concern_paths # Where to look for concern source files (default: app/models/concerns)
|
|
82
|
+
|
|
63
83
|
def initialize
|
|
64
84
|
@server_name = "rails-ai-context"
|
|
65
85
|
@server_version = RailsAiContext::VERSION
|
|
@@ -87,6 +107,41 @@ module RailsAiContext
|
|
|
87
107
|
@live_reload = :auto
|
|
88
108
|
@live_reload_debounce = 1.5
|
|
89
109
|
@generate_root_files = true
|
|
110
|
+
@max_file_size = 2_000_000
|
|
111
|
+
@max_test_file_size = 500_000
|
|
112
|
+
@max_schema_file_size = 10_000_000
|
|
113
|
+
@max_view_total_size = 5_000_000
|
|
114
|
+
@max_view_file_size = 500_000
|
|
115
|
+
@max_search_results = 100
|
|
116
|
+
@max_validate_files = 20
|
|
117
|
+
@excluded_controllers = %w[DeviseController Devise::OmniauthCallbacksController]
|
|
118
|
+
@excluded_route_prefixes = %w[action_mailbox/ active_storage/ rails/ conductor/ devise/ turbo/]
|
|
119
|
+
@excluded_concerns = [
|
|
120
|
+
/::Generated/,
|
|
121
|
+
/\A(ActiveRecord|ActiveModel|ActiveSupport|ActionText|ActionMailbox|ActiveStorage)/,
|
|
122
|
+
/\A(ActionDispatch|ActionController|ActionView|AbstractController)/,
|
|
123
|
+
/\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/
|
|
124
|
+
]
|
|
125
|
+
@excluded_filters = %w[
|
|
126
|
+
verify_authenticity_token verify_same_origin_request
|
|
127
|
+
turbo_tracking_request_id handle_unverified_request
|
|
128
|
+
mark_for_same_origin_verification
|
|
129
|
+
]
|
|
130
|
+
@excluded_middleware = %w[
|
|
131
|
+
Rack::Sendfile ActionDispatch::Static ActionDispatch::Executor
|
|
132
|
+
ActionDispatch::ServerTiming Rack::Runtime
|
|
133
|
+
ActionDispatch::RequestId ActionDispatch::RemoteIp
|
|
134
|
+
Rails::Rack::Logger ActionDispatch::ShowExceptions
|
|
135
|
+
ActionDispatch::DebugExceptions ActionDispatch::Callbacks
|
|
136
|
+
ActionDispatch::Cookies ActionDispatch::Session::CookieStore
|
|
137
|
+
ActionDispatch::Flash ActionDispatch::ContentSecurityPolicy::Middleware
|
|
138
|
+
ActionDispatch::PermissionsPolicy::Middleware ActionDispatch::ActionableExceptions
|
|
139
|
+
Rack::Head Rack::ConditionalGet Rack::ETag Rack::TempfileReaper
|
|
140
|
+
ActiveRecord::Migration::CheckPending ActionDispatch::HostAuthorization
|
|
141
|
+
Rack::MethodOverride ActionDispatch::Session::AbstractSecureStore
|
|
142
|
+
]
|
|
143
|
+
@search_extensions = %w[rb js erb yml yaml json ts tsx vue svelte haml slim]
|
|
144
|
+
@concern_paths = %w[app/models/concerns app/controllers/concerns]
|
|
90
145
|
end
|
|
91
146
|
|
|
92
147
|
def preset=(name)
|
|
@@ -9,12 +9,9 @@ module RailsAiContext
|
|
|
9
9
|
class ControllerIntrospector
|
|
10
10
|
attr_reader :app
|
|
11
11
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
turbo_tracking_request_id handle_unverified_request
|
|
16
|
-
mark_for_same_origin_verification
|
|
17
|
-
].freeze
|
|
12
|
+
def excluded_filters
|
|
13
|
+
RailsAiContext.configuration.excluded_filters
|
|
14
|
+
end
|
|
18
15
|
|
|
19
16
|
def initialize(app)
|
|
20
17
|
@app = app
|
|
@@ -164,7 +161,7 @@ module RailsAiContext
|
|
|
164
161
|
|
|
165
162
|
ctrl._process_action_callbacks.filter_map do |cb|
|
|
166
163
|
next if cb.filter.is_a?(Proc) || cb.filter.to_s.start_with?("_")
|
|
167
|
-
next if
|
|
164
|
+
next if excluded_filters.include?(cb.filter.to_s)
|
|
168
165
|
|
|
169
166
|
filter = { name: cb.filter.to_s, kind: cb.kind.to_s }
|
|
170
167
|
filter[:only] = cb.instance_variable_get(:@if)&.filter_map { |c| extract_action_condition(c) }&.flatten
|
|
@@ -59,6 +59,7 @@ module RailsAiContext
|
|
|
59
59
|
table_name: model.table_name,
|
|
60
60
|
associations: extract_associations(model),
|
|
61
61
|
validations: extract_validations(model),
|
|
62
|
+
custom_validates: extract_custom_validates(model),
|
|
62
63
|
scopes: extract_scopes(model),
|
|
63
64
|
enums: extract_enums(model),
|
|
64
65
|
callbacks: extract_callbacks(model),
|
|
@@ -109,6 +110,17 @@ module RailsAiContext
|
|
|
109
110
|
[]
|
|
110
111
|
end
|
|
111
112
|
|
|
113
|
+
# Extract custom validate :method_name calls from source
|
|
114
|
+
# These are business-rule validators that model.validators doesn't include
|
|
115
|
+
def extract_custom_validates(model)
|
|
116
|
+
source_path = model_source_path(model)
|
|
117
|
+
return [] unless source_path && File.exist?(source_path)
|
|
118
|
+
|
|
119
|
+
File.read(source_path).scan(/^\s*validate\s+:(\w+)/).flatten
|
|
120
|
+
rescue
|
|
121
|
+
[]
|
|
122
|
+
end
|
|
123
|
+
|
|
112
124
|
def model_source_path(model)
|
|
113
125
|
root = app.root.to_s
|
|
114
126
|
underscored = model.name.underscore
|
|
@@ -156,11 +168,8 @@ module RailsAiContext
|
|
|
156
168
|
|
|
157
169
|
def framework_concern?(name)
|
|
158
170
|
return true if name.nil?
|
|
159
|
-
return true if name.include?("::Generated")
|
|
160
|
-
return true if name.match?(/\A(ActiveRecord|ActiveModel|ActiveSupport|ActionText|ActionMailbox|ActiveStorage|ActionDispatch|ActionController|ActionView|AbstractController)/)
|
|
161
|
-
return true if name.match?(/\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/)
|
|
162
171
|
return true if %w[Kernel JSON PP Marshal MessagePack].include?(name)
|
|
163
|
-
|
|
172
|
+
RailsAiContext.configuration.excluded_concerns.any? { |pattern| name.match?(pattern) }
|
|
164
173
|
end
|
|
165
174
|
|
|
166
175
|
def extract_public_class_methods(model)
|
|
@@ -150,7 +150,9 @@ module RailsAiContext
|
|
|
150
150
|
File.join(app.root, "db", "structure.sql")
|
|
151
151
|
end
|
|
152
152
|
|
|
153
|
-
|
|
153
|
+
def max_schema_file_size
|
|
154
|
+
RailsAiContext.configuration.max_schema_file_size
|
|
155
|
+
end
|
|
154
156
|
|
|
155
157
|
# Fallback: parse schema file as text when DB isn't connected.
|
|
156
158
|
# Tries db/schema.rb first, then db/structure.sql.
|
|
@@ -166,7 +168,7 @@ module RailsAiContext
|
|
|
166
168
|
end
|
|
167
169
|
|
|
168
170
|
def parse_schema_rb(path)
|
|
169
|
-
return { error: "schema.rb too large (#{File.size(path)} bytes)" } if File.size(path) >
|
|
171
|
+
return { error: "schema.rb too large (#{File.size(path)} bytes)" } if File.size(path) > max_schema_file_size
|
|
170
172
|
content = File.read(path)
|
|
171
173
|
tables = {}
|
|
172
174
|
current_table = nil
|
|
@@ -209,7 +211,7 @@ module RailsAiContext
|
|
|
209
211
|
end
|
|
210
212
|
|
|
211
213
|
def parse_structure_sql(path) # rubocop:disable Metrics/MethodLength
|
|
212
|
-
return { error: "structure.sql too large (#{File.size(path)} bytes)" } if File.size(path) >
|
|
214
|
+
return { error: "structure.sql too large (#{File.size(path)} bytes)" } if File.size(path) > max_schema_file_size
|
|
213
215
|
content = File.read(path)
|
|
214
216
|
tables = {}
|
|
215
217
|
|
|
@@ -61,15 +61,14 @@ module RailsAiContext
|
|
|
61
61
|
partials
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
-
MAX_TOTAL_VIEW_SIZE = 5_000_000 # 5MB cap for aggregated view content
|
|
65
|
-
MAX_SINGLE_VIEW_SIZE = 500_000 # 500KB per file
|
|
66
|
-
|
|
67
64
|
def collect_all_view_content(views_dir)
|
|
65
|
+
max_total = RailsAiContext.configuration.max_view_total_size
|
|
66
|
+
max_single = RailsAiContext.configuration.max_view_file_size
|
|
68
67
|
content = +""
|
|
69
68
|
Dir.glob(File.join(views_dir, "**", "*.{erb,haml,slim}")).each do |path|
|
|
70
69
|
next if File.directory?(path)
|
|
71
|
-
next if File.size(path) >
|
|
72
|
-
break if content.bytesize >=
|
|
70
|
+
next if File.size(path) > max_single
|
|
71
|
+
break if content.bytesize >= max_total
|
|
73
72
|
content << (File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace) rescue "")
|
|
74
73
|
end
|
|
75
74
|
content
|
|
@@ -6,21 +6,6 @@ module RailsAiContext
|
|
|
6
6
|
tool_name "rails_get_config"
|
|
7
7
|
description "Get Rails application configuration including cache store, session store, timezone, middleware stack, and initializers."
|
|
8
8
|
|
|
9
|
-
# Default Rails middleware — suppress to only show app-specific middleware
|
|
10
|
-
DEFAULT_MIDDLEWARE = %w[
|
|
11
|
-
Rack::Sendfile ActionDispatch::Static ActionDispatch::Executor
|
|
12
|
-
ActionDispatch::ServerTiming Rack::Runtime
|
|
13
|
-
ActionDispatch::RequestId ActionDispatch::RemoteIp
|
|
14
|
-
Rails::Rack::Logger ActionDispatch::ShowExceptions
|
|
15
|
-
ActionDispatch::DebugExceptions ActionDispatch::Callbacks
|
|
16
|
-
ActionDispatch::Cookies ActionDispatch::Session::CookieStore
|
|
17
|
-
ActionDispatch::Flash ActionDispatch::ContentSecurityPolicy::Middleware
|
|
18
|
-
ActionDispatch::PermissionsPolicy::Middleware ActionDispatch::ActionableExceptions
|
|
19
|
-
Rack::Head Rack::ConditionalGet Rack::ETag Rack::TempfileReaper
|
|
20
|
-
ActiveRecord::Migration::CheckPending ActionDispatch::HostAuthorization
|
|
21
|
-
Rack::MethodOverride ActionDispatch::Session::AbstractSecureStore
|
|
22
|
-
].freeze
|
|
23
|
-
|
|
24
9
|
input_schema(properties: {})
|
|
25
10
|
|
|
26
11
|
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
@@ -45,7 +30,8 @@ module RailsAiContext
|
|
|
45
30
|
Propshaft::Server WebConsole::Middleware ActionDispatch::Reloader
|
|
46
31
|
Bullet::Rack ActiveSupport::Cache::Strategy::LocalCache
|
|
47
32
|
]
|
|
48
|
-
|
|
33
|
+
excluded_mw = RailsAiContext.configuration.excluded_middleware
|
|
34
|
+
custom = data[:middleware_stack].reject { |m| excluded_mw.include?(m) || dev_middleware.include?(m) }
|
|
49
35
|
if custom.any?
|
|
50
36
|
lines << "" << "## Custom Middleware"
|
|
51
37
|
custom.each { |m| lines << "- #{m}" }
|
|
@@ -42,7 +42,7 @@ module RailsAiContext
|
|
|
42
42
|
controllers = data[:controllers] || {}
|
|
43
43
|
|
|
44
44
|
# Filter out framework-internal controllers for listings/error messages
|
|
45
|
-
framework_controllers =
|
|
45
|
+
framework_controllers = RailsAiContext.configuration.excluded_controllers
|
|
46
46
|
app_controller_names = controllers.keys.reject { |name| framework_controllers.include?(name) }.sort
|
|
47
47
|
|
|
48
48
|
# Specific controller — always full detail (searches ALL controllers including framework)
|
|
@@ -219,11 +219,9 @@ module RailsAiContext
|
|
|
219
219
|
lines.join("\n")
|
|
220
220
|
end
|
|
221
221
|
|
|
222
|
-
MAX_CONTROLLER_SIZE = 2_000_000 # 2MB safety limit
|
|
223
|
-
|
|
224
222
|
private_class_method def self.extract_method_with_lines(file_path, method_name)
|
|
225
223
|
return nil unless File.exist?(file_path)
|
|
226
|
-
return nil if File.size(file_path) >
|
|
224
|
+
return nil if File.size(file_path) > RailsAiContext.configuration.max_file_size
|
|
227
225
|
source_lines = File.readlines(file_path)
|
|
228
226
|
start_idx = source_lines.index { |l| l.match?(/^\s*def\s+#{Regexp.escape(method_name.to_s)}\b/) }
|
|
229
227
|
return nil unless start_idx
|
|
@@ -6,7 +6,9 @@ module RailsAiContext
|
|
|
6
6
|
tool_name "rails_get_edit_context"
|
|
7
7
|
description "Get just enough context to make a surgical Edit to a file. Returns the target area with line numbers and surrounding code. Purpose-built to replace Read + Edit workflow with a single call."
|
|
8
8
|
|
|
9
|
-
|
|
9
|
+
def self.max_file_size
|
|
10
|
+
RailsAiContext.configuration.max_file_size
|
|
11
|
+
end
|
|
10
12
|
|
|
11
13
|
input_schema(
|
|
12
14
|
properties: {
|
|
@@ -55,7 +57,7 @@ module RailsAiContext
|
|
|
55
57
|
rescue Errno::ENOENT
|
|
56
58
|
return text_response("File not found: #{file}")
|
|
57
59
|
end
|
|
58
|
-
if File.size(full_path) >
|
|
60
|
+
if File.size(full_path) > max_file_size
|
|
59
61
|
return text_response("File too large: #{file}")
|
|
60
62
|
end
|
|
61
63
|
|
|
@@ -38,7 +38,9 @@ module RailsAiContext
|
|
|
38
38
|
lines << "- **#{g[:name]}**: #{g[:note]}"
|
|
39
39
|
end
|
|
40
40
|
else
|
|
41
|
-
|
|
41
|
+
all_cats = (gems[:notable_gems] || []).map { |g| g[:category] }.uniq.sort
|
|
42
|
+
hint = all_cats.any? ? " Available categories: #{all_cats.join(', ')}" : ""
|
|
43
|
+
lines << "_No notable gems found#{" in category '#{category}'" unless category == 'all'}.#{hint}_"
|
|
42
44
|
end
|
|
43
45
|
|
|
44
46
|
text_response(lines.join("\n"))
|
|
@@ -123,6 +123,7 @@ module RailsAiContext
|
|
|
123
123
|
detail += " (class: #{a[:class_name]})" if a[:class_name] && a[:class_name] != a[:name].to_s.classify
|
|
124
124
|
detail += " through: #{a[:through]}" if a[:through]
|
|
125
125
|
detail += " [polymorphic]" if a[:polymorphic]
|
|
126
|
+
detail += " [optional]" if a[:optional]
|
|
126
127
|
detail += " dependent: #{a[:dependent]}" if a[:dependent]
|
|
127
128
|
lines << detail
|
|
128
129
|
end
|
|
@@ -157,6 +158,11 @@ module RailsAiContext
|
|
|
157
158
|
end
|
|
158
159
|
end
|
|
159
160
|
|
|
161
|
+
# Custom validate methods (business rules)
|
|
162
|
+
if data[:custom_validates]&.any?
|
|
163
|
+
lines << "- **Custom:** #{data[:custom_validates].map { |v| "`#{v}`" }.join(', ')}"
|
|
164
|
+
end
|
|
165
|
+
|
|
160
166
|
# Enums
|
|
161
167
|
if data[:enums]&.any?
|
|
162
168
|
lines << "" << "## Enums"
|
|
@@ -181,18 +187,30 @@ module RailsAiContext
|
|
|
181
187
|
|
|
182
188
|
# Concerns — filter out framework/gem internal modules
|
|
183
189
|
if data[:concerns]&.any?
|
|
190
|
+
excluded_patterns = RailsAiContext.configuration.excluded_concerns
|
|
184
191
|
app_concerns = data[:concerns].reject do |c|
|
|
185
|
-
|
|
186
|
-
c.match?(
|
|
187
|
-
c.match?(/\A(Devise::Models|Devise::Orm|Bullet::|Turbo::|GlobalID::|Rolify::)/) ||
|
|
188
|
-
%w[Kernel JSON PP Marshal MessagePack].any? { |mod| c == mod || c.start_with?("#{mod}::") }
|
|
192
|
+
%w[Kernel JSON PP Marshal MessagePack].include?(c) ||
|
|
193
|
+
excluded_patterns.any? { |pattern| c.match?(pattern) }
|
|
189
194
|
end
|
|
190
195
|
if app_concerns.any?
|
|
191
196
|
lines << "" << "## Concerns"
|
|
192
|
-
|
|
197
|
+
app_concerns.each do |c|
|
|
198
|
+
methods = extract_concern_methods(c)
|
|
199
|
+
if methods&.any?
|
|
200
|
+
lines << "- **#{c}** — #{methods.join(', ')}"
|
|
201
|
+
else
|
|
202
|
+
lines << "- #{c}"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
193
205
|
end
|
|
194
206
|
end
|
|
195
207
|
|
|
208
|
+
# Class methods (e.g. Plan.free, Plan.pro)
|
|
209
|
+
if data[:class_methods]&.any?
|
|
210
|
+
lines << "" << "## Class methods"
|
|
211
|
+
lines << data[:class_methods].first(15).map { |m| "- `#{m}`" }.join("\n")
|
|
212
|
+
end
|
|
213
|
+
|
|
196
214
|
# Key instance methods — include signatures from source if available
|
|
197
215
|
if data[:instance_methods]&.any?
|
|
198
216
|
lines << "" << "## Key instance methods"
|
|
@@ -220,7 +238,7 @@ module RailsAiContext
|
|
|
220
238
|
private_class_method def self.extract_method_signatures(model_name)
|
|
221
239
|
path = Rails.root.join("app", "models", "#{model_name.underscore}.rb")
|
|
222
240
|
return nil unless File.exist?(path)
|
|
223
|
-
return nil if File.size(path) >
|
|
241
|
+
return nil if File.size(path) > max_file_size
|
|
224
242
|
|
|
225
243
|
source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
226
244
|
signatures = []
|
|
@@ -241,13 +259,45 @@ module RailsAiContext
|
|
|
241
259
|
nil
|
|
242
260
|
end
|
|
243
261
|
|
|
244
|
-
|
|
262
|
+
# Extract public method names from a concern's source file
|
|
263
|
+
private_class_method def self.extract_concern_methods(concern_name)
|
|
264
|
+
max_size = RailsAiContext.configuration.max_file_size
|
|
265
|
+
underscore = concern_name.underscore
|
|
266
|
+
# Search configurable concern paths
|
|
267
|
+
path = RailsAiContext.configuration.concern_paths
|
|
268
|
+
.map { |dir| Rails.root.join(dir, "#{underscore}.rb") }
|
|
269
|
+
.find { |p| File.exist?(p) }
|
|
270
|
+
return nil unless path
|
|
271
|
+
return nil if File.size(path) > max_size
|
|
272
|
+
|
|
273
|
+
source = File.read(path, encoding: "UTF-8", invalid: :replace, undef: :replace)
|
|
274
|
+
methods = []
|
|
275
|
+
in_private = false
|
|
276
|
+
|
|
277
|
+
source.each_line do |line|
|
|
278
|
+
in_private = true if line.match?(/\A\s*(private|protected)\s*$/)
|
|
279
|
+
in_private = false if line.match?(/\A\s*public\s*$/)
|
|
280
|
+
next if in_private
|
|
281
|
+
|
|
282
|
+
if (match = line.match(/\A\s*def\s+([\w?!]+)/))
|
|
283
|
+
methods << match[1] unless match[1].start_with?("_")
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
methods.empty? ? nil : methods
|
|
288
|
+
rescue
|
|
289
|
+
nil
|
|
290
|
+
end
|
|
291
|
+
|
|
292
|
+
def self.max_file_size
|
|
293
|
+
RailsAiContext.configuration.max_file_size
|
|
294
|
+
end
|
|
245
295
|
|
|
246
296
|
private_class_method def self.extract_model_structure(model_name)
|
|
247
297
|
path = "app/models/#{model_name.underscore}.rb"
|
|
248
298
|
full_path = Rails.root.join(path)
|
|
249
299
|
return nil unless File.exist?(full_path)
|
|
250
|
-
return nil if File.size(full_path) >
|
|
300
|
+
return nil if File.size(full_path) > max_file_size
|
|
251
301
|
|
|
252
302
|
source_lines = File.readlines(full_path)
|
|
253
303
|
sections = []
|
|
@@ -32,14 +32,9 @@ module RailsAiContext
|
|
|
32
32
|
}
|
|
33
33
|
)
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
# Framework routes that add noise to summary listings
|
|
40
|
-
FRAMEWORK_PREFIXES = %w[
|
|
41
|
-
devise/ turbo/
|
|
42
|
-
].freeze
|
|
35
|
+
def self.route_prefixes
|
|
36
|
+
RailsAiContext.configuration.excluded_route_prefixes
|
|
37
|
+
end
|
|
43
38
|
|
|
44
39
|
annotations(read_only_hint: true, destructive_hint: false, idempotent_hint: true, open_world_hint: false)
|
|
45
40
|
|
|
@@ -53,12 +48,13 @@ module RailsAiContext
|
|
|
53
48
|
|
|
54
49
|
# Filter out internal Rails routes by default
|
|
55
50
|
if app_only
|
|
56
|
-
by_controller = by_controller.reject { |k, _|
|
|
51
|
+
by_controller = by_controller.reject { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
|
|
57
52
|
end
|
|
58
53
|
|
|
59
|
-
# Filter by controller
|
|
54
|
+
# Filter by controller — accepts both slash and :: notation
|
|
60
55
|
if controller
|
|
61
|
-
|
|
56
|
+
normalized = controller.downcase.tr("::", "/").delete_suffix("controller")
|
|
57
|
+
filtered = by_controller.select { |k, _| k.downcase.include?(normalized) }
|
|
62
58
|
return text_response("No routes for '#{controller}'. Controllers: #{by_controller.keys.sort.join(', ')}") if filtered.empty?
|
|
63
59
|
by_controller = filtered
|
|
64
60
|
end
|
|
@@ -70,8 +66,8 @@ module RailsAiContext
|
|
|
70
66
|
case detail
|
|
71
67
|
when "summary"
|
|
72
68
|
# Separate app routes from framework routes for cleaner output
|
|
73
|
-
app_routes = controller ? by_controller : by_controller.reject { |k, _|
|
|
74
|
-
framework_routes = controller ? {} : by_controller.select { |k, _|
|
|
69
|
+
app_routes = controller ? by_controller : by_controller.reject { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
|
|
70
|
+
framework_routes = controller ? {} : by_controller.select { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
|
|
75
71
|
|
|
76
72
|
lines = [ "# Routes Summary (#{filtered_total} routes)", "" ]
|
|
77
73
|
|
|
@@ -116,8 +112,8 @@ module RailsAiContext
|
|
|
116
112
|
when "standard"
|
|
117
113
|
limit ||= 100
|
|
118
114
|
# Separate app vs framework routes (unless user filtered by controller)
|
|
119
|
-
app_routes = controller ? by_controller : by_controller.reject { |k, _|
|
|
120
|
-
framework_routes = controller ? {} : by_controller.select { |k, _|
|
|
115
|
+
app_routes = controller ? by_controller : by_controller.reject { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
|
|
116
|
+
framework_routes = controller ? {} : by_controller.select { |k, _| route_prefixes.any? { |p| k.downcase.start_with?(p) } }
|
|
121
117
|
|
|
122
118
|
lines = [ "# Routes (#{filtered_total} routes)", "" ]
|
|
123
119
|
count = 0
|
|
@@ -123,23 +123,20 @@ module RailsAiContext
|
|
|
123
123
|
|
|
124
124
|
private_class_method def self.format_table_markdown(name, data)
|
|
125
125
|
columns = data[:columns] || []
|
|
126
|
-
|
|
127
|
-
has_defaults = columns.any? { |c| c[:default] }
|
|
126
|
+
# Always show Nullable and Default — agents need these for migrations and validations
|
|
127
|
+
has_defaults = columns.any? { |c| c.key?(:default) && !c[:default].nil? }
|
|
128
128
|
|
|
129
129
|
lines = [ "## Table: #{name}", "" ]
|
|
130
130
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
sep = "|--------|-----"
|
|
134
|
-
header += " | Nullable" if has_nullable
|
|
135
|
-
sep += "-|----------" if has_nullable
|
|
131
|
+
header = "| Column | Type | Null"
|
|
132
|
+
sep = "|--------|------|-----"
|
|
136
133
|
header += " | Default" if has_defaults
|
|
137
134
|
sep += "-|---------" if has_defaults
|
|
138
135
|
lines << "#{header} |" << "#{sep}|"
|
|
139
136
|
|
|
140
137
|
columns.each do |col|
|
|
141
|
-
|
|
142
|
-
line
|
|
138
|
+
nullable = col.key?(:null) ? (col[:null] ? "yes" : "**NO**") : "yes"
|
|
139
|
+
line = "| #{col[:name]} | #{col[:type]} | #{nullable}"
|
|
143
140
|
line += " | #{col[:default]}" if has_defaults
|
|
144
141
|
lines << "#{line} |"
|
|
145
142
|
end
|
|
@@ -119,7 +119,9 @@ module RailsAiContext
|
|
|
119
119
|
end
|
|
120
120
|
end
|
|
121
121
|
|
|
122
|
-
|
|
122
|
+
def self.max_test_file_size
|
|
123
|
+
RailsAiContext.configuration.max_test_file_size
|
|
124
|
+
end
|
|
123
125
|
|
|
124
126
|
private_class_method def self.find_test_file(name, type, detail = "full")
|
|
125
127
|
# Normalize: accept "Bonus::CrisesController", "bonus/crises", "Crises"
|
|
@@ -151,7 +153,7 @@ module RailsAiContext
|
|
|
151
153
|
rescue Errno::ENOENT
|
|
152
154
|
next
|
|
153
155
|
end
|
|
154
|
-
next if File.size(path) >
|
|
156
|
+
next if File.size(path) > max_test_file_size
|
|
155
157
|
content = File.read(path)
|
|
156
158
|
|
|
157
159
|
# Summary/standard: return just test names (saves 2000+ tokens vs full source)
|
|
@@ -169,7 +171,13 @@ module RailsAiContext
|
|
|
169
171
|
return "# #{rel}\n\n```ruby\n#{content}\n```"
|
|
170
172
|
end
|
|
171
173
|
|
|
172
|
-
|
|
174
|
+
# List nearby test files to help the agent find the right one
|
|
175
|
+
test_dirs = candidates.map { |c| File.dirname(Rails.root.join(c)) }.uniq
|
|
176
|
+
nearby = test_dirs.flat_map do |dir|
|
|
177
|
+
Dir.exist?(dir) ? Dir.glob(File.join(dir, "*")).map { |f| f.sub("#{Rails.root}/", "") }.first(10) : []
|
|
178
|
+
end
|
|
179
|
+
hint = nearby.any? ? "\n\nFiles in test directory: #{nearby.join(', ')}" : ""
|
|
180
|
+
"No test file found for #{name}. Searched: #{candidates.join(', ')}#{hint}"
|
|
173
181
|
end
|
|
174
182
|
end
|
|
175
183
|
end
|
|
@@ -161,7 +161,9 @@ module RailsAiContext
|
|
|
161
161
|
text_response(lines.join("\n"))
|
|
162
162
|
end
|
|
163
163
|
|
|
164
|
-
|
|
164
|
+
def self.max_file_size
|
|
165
|
+
RailsAiContext.configuration.max_file_size
|
|
166
|
+
end
|
|
165
167
|
|
|
166
168
|
private_class_method def self.read_view_file(path)
|
|
167
169
|
# Reject path traversal attempts before any filesystem operation
|
|
@@ -174,7 +176,10 @@ module RailsAiContext
|
|
|
174
176
|
|
|
175
177
|
# Path traversal protection (resolves symlinks)
|
|
176
178
|
unless File.exist?(full_path)
|
|
177
|
-
|
|
179
|
+
dir = File.dirname(path)
|
|
180
|
+
siblings = Dir.glob(File.join(views_dir, dir, "*")).map { |f| "#{dir}/#{File.basename(f)}" }.sort.first(10)
|
|
181
|
+
hint = siblings.any? ? " Files in #{dir}/: #{siblings.join(', ')}" : ""
|
|
182
|
+
return text_response("View not found: #{path}.#{hint}")
|
|
178
183
|
end
|
|
179
184
|
begin
|
|
180
185
|
unless File.realpath(full_path).start_with?(File.realpath(views_dir))
|
|
@@ -183,8 +188,8 @@ module RailsAiContext
|
|
|
183
188
|
rescue Errno::ENOENT
|
|
184
189
|
return text_response("View not found: #{path}")
|
|
185
190
|
end
|
|
186
|
-
if File.size(full_path) >
|
|
187
|
-
return text_response("File too large: #{path} (#{File.size(full_path)} bytes)")
|
|
191
|
+
if File.size(full_path) > max_file_size
|
|
192
|
+
return text_response("File too large: #{path} (#{File.size(full_path)} bytes, max: #{max_file_size})")
|
|
188
193
|
end
|
|
189
194
|
|
|
190
195
|
content = compress_tailwind(strip_svg(File.read(full_path)))
|
|
@@ -8,7 +8,9 @@ module RailsAiContext
|
|
|
8
8
|
tool_name "rails_search_code"
|
|
9
9
|
description "Search the Rails codebase for a pattern using ripgrep (rg) or Ruby fallback. Returns matching lines with file paths and line numbers. Useful for finding usages, implementations, and patterns."
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
def self.max_results_cap
|
|
12
|
+
RailsAiContext.configuration.max_search_results
|
|
13
|
+
end
|
|
12
14
|
|
|
13
15
|
input_schema(
|
|
14
16
|
properties: {
|
|
@@ -59,7 +61,7 @@ module RailsAiContext
|
|
|
59
61
|
end
|
|
60
62
|
|
|
61
63
|
# Cap max_results and context_lines
|
|
62
|
-
max_results = [ max_results.to_i,
|
|
64
|
+
max_results = [ max_results.to_i, max_results_cap ].min
|
|
63
65
|
max_results = 30 if max_results < 1
|
|
64
66
|
context_lines = [ [ context_lines.to_i, 0 ].max, 5 ].min
|
|
65
67
|
|
|
@@ -67,7 +69,8 @@ module RailsAiContext
|
|
|
67
69
|
|
|
68
70
|
# Path traversal protection
|
|
69
71
|
unless Dir.exist?(search_path)
|
|
70
|
-
|
|
72
|
+
top_dirs = Dir.glob(File.join(root, "*")).select { |f| File.directory?(f) }.map { |f| File.basename(f) }.sort
|
|
73
|
+
return text_response("Path not found: #{path}. Top-level directories: #{top_dirs.first(15).join(', ')}")
|
|
71
74
|
end
|
|
72
75
|
|
|
73
76
|
begin
|
|
@@ -143,7 +146,8 @@ module RailsAiContext
|
|
|
143
146
|
rescue RegexpError => e
|
|
144
147
|
return [ { file: "error", line_number: 0, content: "Invalid regex: #{e.message}" } ]
|
|
145
148
|
end
|
|
146
|
-
|
|
149
|
+
extensions = RailsAiContext.configuration.search_extensions.join(",")
|
|
150
|
+
glob = file_type ? "**/*.#{file_type}" : "**/*.{#{extensions}}"
|
|
147
151
|
excluded = RailsAiContext.configuration.excluded_paths
|
|
148
152
|
sensitive = RailsAiContext.configuration.sensitive_patterns
|
|
149
153
|
|
|
@@ -8,7 +8,9 @@ module RailsAiContext
|
|
|
8
8
|
tool_name "rails_validate"
|
|
9
9
|
description "Validate syntax of multiple files at once (Ruby, ERB, JavaScript). Replaces separate ruby -c, erb check, and node -c calls. Returns pass/fail for each file with error details."
|
|
10
10
|
|
|
11
|
-
|
|
11
|
+
def self.max_files
|
|
12
|
+
RailsAiContext.configuration.max_validate_files
|
|
13
|
+
end
|
|
12
14
|
|
|
13
15
|
input_schema(
|
|
14
16
|
properties: {
|
|
@@ -28,8 +30,8 @@ module RailsAiContext
|
|
|
28
30
|
return text_response("No files provided.")
|
|
29
31
|
end
|
|
30
32
|
|
|
31
|
-
if files.size >
|
|
32
|
-
return text_response("Too many files (#{files.size}). Maximum is #{
|
|
33
|
+
if files.size > max_files
|
|
34
|
+
return text_response("Too many files (#{files.size}). Maximum is #{max_files} per call.")
|
|
33
35
|
end
|
|
34
36
|
|
|
35
37
|
results = []
|
|
@@ -152,10 +154,8 @@ module RailsAiContext
|
|
|
152
154
|
|
|
153
155
|
# Basic JavaScript validation when node is not available.
|
|
154
156
|
# Checks for unmatched braces, brackets, and parentheses.
|
|
155
|
-
MAX_VALIDATE_FILE_SIZE = 2_000_000
|
|
156
|
-
|
|
157
157
|
private_class_method def self.validate_javascript_fallback(full_path)
|
|
158
|
-
return [ false, "file too large for basic validation" ] if File.size(full_path) >
|
|
158
|
+
return [ false, "file too large for basic validation" ] if File.size(full_path) > RailsAiContext.configuration.max_file_size
|
|
159
159
|
content = File.read(full_path)
|
|
160
160
|
stack = []
|
|
161
161
|
openers = { "{" => "}", "[" => "]", "(" => ")" }
|
data/server.json
CHANGED
|
@@ -7,11 +7,11 @@
|
|
|
7
7
|
"url": "https://github.com/crisnahine/rails-ai-context",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "0.15.
|
|
10
|
+
"version": "0.15.5",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "mcpb",
|
|
14
|
-
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.15.
|
|
14
|
+
"identifier": "https://github.com/crisnahine/rails-ai-context/releases/download/v0.15.5/rails-ai-context-mcp.mcpb",
|
|
15
15
|
"fileSha256": "dd711a0ad6c4de943ae4da94eaf59a6dc9494b9d57f726e24649ed4e2f156990",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|