woods 1.6.1 → 2.0.0.beta2
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 +2035 -0
- data/CONTRIBUTING.md +253 -87
- data/README.md +161 -513
- data/SECURITY.md +92 -0
- data/assets/woods-wordmark-white-with-bg.png +0 -0
- data/docs/AGENT_GUIDE.md +204 -0
- data/docs/AGENT_SETUP.md +205 -0
- data/docs/BACKEND_MATRIX.md +470 -0
- data/docs/CONFIGURATION_REFERENCE.md +655 -0
- data/docs/CONSOLE_MCP_SETUP.md +829 -0
- data/docs/DOCKER_SETUP.md +454 -0
- data/docs/EMBEDDING_MODELS.md +136 -0
- data/docs/EVALUATION.md +91 -0
- data/docs/EXTRACTOR_REFERENCE.md +765 -0
- data/docs/FAQ.md +544 -0
- data/docs/GETTING_STARTED.md +183 -0
- data/docs/INCREMENTAL_EXTRACTION.md +455 -0
- data/docs/INTERNALS.md +418 -0
- data/docs/MCP_HTTP_TRANSPORT.md +144 -0
- data/docs/MCP_SERVERS.md +231 -0
- data/docs/MCP_TOOL_COOKBOOK.md +987 -0
- data/docs/MCP_WORKTREE_SETUP.md +127 -0
- data/docs/NOTION_INTEGRATION.md +283 -0
- data/docs/OBSIDIAN_INTEGRATION.md +170 -0
- data/docs/PUBLISHED_INDEX.md +213 -0
- data/docs/README.md +94 -0
- data/docs/RETRIEVAL_GUIDE.md +267 -0
- data/docs/TOKEN_BENCHMARK.md +68 -0
- data/docs/TROUBLESHOOTING.md +841 -0
- data/docs/UNBLOCKED_INTEGRATION.md +279 -0
- data/docs/UPGRADING_TO_2.md +321 -0
- data/docs/WATCH_DAEMON.md +667 -0
- data/docs/WHY_WOODS.md +219 -0
- data/exe/woods-console +40 -4
- data/exe/woods-console-mcp +21 -35
- data/exe/woods-mcp +20 -7
- data/exe/woods-mcp-http +80 -11
- data/exe/woods-mcp-start +57 -52
- data/lib/generators/woods/install_generator.rb +6 -5
- data/lib/generators/woods/pgvector_generator.rb +6 -3
- data/lib/generators/woods/templates/add_pgvector_to_woods.rb.erb +29 -9
- data/lib/generators/woods/templates/create_woods_tables.rb.erb +5 -1
- data/lib/generators/woods/templates/woods.rb.tt +49 -28
- data/lib/tasks/woods.rake +622 -168
- data/lib/tasks/woods_checks.rake +107 -0
- data/lib/tasks/woods_evaluation.rake +164 -80
- data/lib/woods/ast/call_site_extractor.rb +6 -15
- data/lib/woods/ast/method_extractor.rb +19 -9
- data/lib/woods/ast/parser.rb +54 -8
- data/lib/woods/atomic_file.rb +171 -2
- data/lib/woods/builder.rb +310 -22
- data/lib/woods/cache/cache_middleware.rb +7 -2
- data/lib/woods/cache/cache_store.rb +9 -1
- data/lib/woods/cache/solid_cache_store.rb +6 -4
- data/lib/woods/change_set.rb +88 -0
- data/lib/woods/checks/generation_resolution.rb +34 -0
- data/lib/woods/checks/moved_messages.rb +186 -0
- data/lib/woods/chunking/semantic_chunker.rb +160 -18
- data/lib/woods/console/audit_logger.rb +12 -3
- data/lib/woods/console/bridge_protocol.rb +3 -16
- data/lib/woods/console/connection_manager.rb +51 -136
- data/lib/woods/console/dispatch_pipeline.rb +42 -12
- data/lib/woods/console/embedded_executor.rb +806 -149
- data/lib/woods/console/eval_guard.rb +27 -20
- data/lib/woods/console/input_contract.rb +78 -0
- data/lib/woods/console/model_validator.rb +29 -1
- data/lib/woods/console/rack_middleware.rb +65 -42
- data/lib/woods/console/redactor.rb +26 -8
- data/lib/woods/console/safe_context.rb +58 -10
- data/lib/woods/console/scope_predicate_parser.rb +41 -0
- data/lib/woods/console/server.rb +119 -247
- data/lib/woods/console/sql_noise_stripper.rb +125 -16
- data/lib/woods/console/sql_table_scanner.rb +82 -22
- data/lib/woods/console/sql_validator.rb +459 -29
- data/lib/woods/console/table_gate.rb +2 -2
- data/lib/woods/console/tool_specs.rb +463 -90
- data/lib/woods/console/tools/tier1.rb +1 -5
- data/lib/woods/console/tools/tier4.rb +18 -9
- data/lib/woods/coordination/lock_heartbeat.rb +103 -0
- data/lib/woods/coordination/pipeline_lock.rb +263 -53
- data/lib/woods/db/migrations/007_typed_snapshot_units.rb +45 -0
- data/lib/woods/db/migrator.rb +3 -9
- data/lib/woods/db/schema_version.rb +47 -2
- data/lib/woods/dependency_graph.rb +898 -64
- data/lib/woods/embedding/fake.rb +138 -0
- data/lib/woods/embedding/indexer.rb +832 -40
- data/lib/woods/embedding/openai.rb +77 -19
- data/lib/woods/embedding/provider.rb +189 -11
- data/lib/woods/embedding/text_preparer.rb +1 -1
- data/lib/woods/embedding/token_counter.rb +0 -7
- data/lib/woods/evaluation/ablation_agent_payload.rb +38 -0
- data/lib/woods/evaluation/ablation_executor.rb +67 -0
- data/lib/woods/evaluation/ablation_provenance.rb +38 -0
- data/lib/woods/evaluation/ablation_report_writer.rb +43 -0
- data/lib/woods/evaluation/ablation_runner.rb +173 -0
- data/lib/woods/evaluation/ablation_summary.rb +65 -0
- data/lib/woods/evaluation/ablation_task.rb +66 -0
- data/lib/woods/evaluation/ablation_task_set.rb +77 -0
- data/lib/woods/evaluation/ablation_timed_executor.rb +91 -0
- data/lib/woods/evaluation/ablation_worktree.rb +71 -0
- data/lib/woods/evaluation/baseline.rb +60 -0
- data/lib/woods/evaluation/baseline_runner.rb +11 -3
- data/lib/woods/evaluation/evaluator.rb +41 -8
- data/lib/woods/evaluation/query_set.rb +79 -13
- data/lib/woods/evaluation/report_generator.rb +20 -1
- data/lib/woods/export/unit_facts.rb +0 -11
- data/lib/woods/extracted_unit.rb +22 -63
- data/lib/woods/extractor.rb +2783 -238
- data/lib/woods/extractors/action_cable_extractor.rb +9 -4
- data/lib/woods/extractors/ast_source_extraction.rb +20 -2
- data/lib/woods/extractors/caching_extractor.rb +46 -12
- data/lib/woods/extractors/callback_analyzer.rb +39 -9
- data/lib/woods/extractors/component_discovery.rb +123 -0
- data/lib/woods/extractors/concern_extractor.rb +17 -3
- data/lib/woods/extractors/controller_extractor.rb +389 -29
- data/lib/woods/extractors/decorator_extractor.rb +7 -14
- data/lib/woods/extractors/engine_extractor.rb +53 -8
- data/lib/woods/extractors/event_extractor.rb +55 -4
- data/lib/woods/extractors/factory_extractor.rb +49 -11
- data/lib/woods/extractors/graphql_extractor.rb +162 -66
- data/lib/woods/extractors/i18n_extractor.rb +6 -1
- data/lib/woods/extractors/job_extractor.rb +51 -21
- data/lib/woods/extractors/lib_extractor.rb +23 -17
- data/lib/woods/extractors/line_neutralizer.rb +171 -0
- data/lib/woods/extractors/mailer_extractor.rb +9 -1
- data/lib/woods/extractors/manager_extractor.rb +19 -2
- data/lib/woods/extractors/migration_extractor.rb +22 -11
- data/lib/woods/extractors/model_extractor.rb +292 -57
- data/lib/woods/extractors/package_extractor.rb +154 -0
- data/lib/woods/extractors/phlex_extractor.rb +18 -3
- data/lib/woods/extractors/policy_extractor.rb +6 -5
- data/lib/woods/extractors/poro_extractor.rb +13 -14
- data/lib/woods/extractors/pundit_extractor.rb +3 -3
- data/lib/woods/extractors/rails_source_extractor.rb +24 -7
- data/lib/woods/extractors/rake_task_extractor.rb +158 -30
- data/lib/woods/extractors/reference_patterns.rb +38 -0
- data/lib/woods/extractors/route_extractor.rb +58 -2
- data/lib/woods/extractors/scheduled_job_extractor.rb +51 -35
- data/lib/woods/extractors/serializer_extractor.rb +3 -4
- data/lib/woods/extractors/service_extractor.rb +11 -1
- data/lib/woods/extractors/shared_dependency_scanner.rb +24 -34
- data/lib/woods/extractors/shared_utility_methods.rb +36 -6
- data/lib/woods/extractors/source_nesting.rb +560 -0
- data/lib/woods/extractors/state_machine_extractor.rb +30 -18
- data/lib/woods/extractors/test_mapping_extractor.rb +26 -9
- data/lib/woods/extractors/view_component_extractor.rb +28 -3
- data/lib/woods/extractors/view_engines/erb.rb +17 -3
- data/lib/woods/feedback/gap_detector.rb +9 -3
- data/lib/woods/feedback/store.rb +7 -1
- data/lib/woods/filename_utils.rb +29 -1
- data/lib/woods/flow_analysis/operation_extractor.rb +22 -10
- data/lib/woods/flow_assembler.rb +147 -26
- data/lib/woods/flow_document.rb +1 -0
- data/lib/woods/flow_precomputer.rb +175 -22
- data/lib/woods/gem_mapper.rb +285 -0
- data/lib/woods/generation.rb +185 -0
- data/lib/woods/git_command.rb +38 -0
- data/lib/woods/git_provenance.rb +16 -2
- data/lib/woods/graph_analyzer.rb +564 -87
- data/lib/woods/index_artifact.rb +93 -23
- data/lib/woods/mcp/bearer_auth.rb +102 -13
- data/lib/woods/mcp/bootstrap_state.rb +77 -0
- data/lib/woods/mcp/bootstrapper.rb +582 -77
- data/lib/woods/mcp/config_resolver.rb +66 -6
- data/lib/woods/mcp/errors.rb +60 -0
- data/lib/woods/mcp/index_reader.rb +836 -117
- data/lib/woods/mcp/index_reader_pinning.rb +78 -0
- data/lib/woods/mcp/origin_guard.rb +66 -7
- data/lib/woods/mcp/protocol_policy.rb +98 -0
- data/lib/woods/mcp/provider_probe.rb +45 -6
- data/lib/woods/mcp/renderers/markdown_renderer.rb +72 -4
- data/lib/woods/mcp/renderers/plain_renderer.rb +54 -6
- data/lib/woods/mcp/server.rb +898 -152
- data/lib/woods/mcp/tasks/extension.rb +196 -0
- data/lib/woods/mcp/tasks/request_capture.rb +45 -0
- data/lib/woods/mcp/tasks/store.rb +518 -0
- data/lib/woods/mcp/tool_contract.rb +171 -0
- data/lib/woods/mcp/tool_response_renderer.rb +7 -0
- data/lib/woods/model_name_cache.rb +19 -1
- data/lib/woods/notion/client.rb +132 -36
- data/lib/woods/notion/exporter.rb +456 -61
- data/lib/woods/notion/mappers/column_mapper.rb +34 -5
- data/lib/woods/notion/mappers/migration_mapper.rb +32 -8
- data/lib/woods/notion/mappers/model_mapper.rb +21 -6
- data/lib/woods/notion/mappers/shared.rb +45 -3
- data/lib/woods/notion/sync_manifest.rb +258 -0
- data/lib/woods/obsidian/errors.rb +6 -0
- data/lib/woods/obsidian/name_mapper.rb +40 -24
- data/lib/woods/obsidian/vault_exporter.rb +103 -36
- data/lib/woods/operator/pipeline_guard.rb +118 -21
- data/lib/woods/operator/status_reporter.rb +20 -3
- data/lib/woods/path_dispatcher.rb +276 -0
- data/lib/woods/payload_store.rb +236 -0
- data/lib/woods/published_index/edge_shaper.rb +61 -0
- data/lib/woods/published_index/generation_catalog.rb +72 -0
- data/lib/woods/published_index/typed_unit_reader.rb +48 -0
- data/lib/woods/published_index.rb +287 -0
- data/lib/woods/railtie.rb +69 -30
- data/lib/woods/railtie_support.rb +167 -0
- data/lib/woods/release.rb +12 -0
- data/lib/woods/reload_policy.rb +206 -0
- data/lib/woods/resilience/circuit_breaker.rb +47 -8
- data/lib/woods/resilience/index_validator.rb +296 -10
- data/lib/woods/resilience/retryable_provider.rb +71 -6
- data/lib/woods/resolved_config.rb +55 -11
- data/lib/woods/retrieval/context_assembler.rb +132 -40
- data/lib/woods/retrieval/query_classifier.rb +26 -8
- data/lib/woods/retrieval/ranker.rb +193 -28
- data/lib/woods/retrieval/search_executor.rb +206 -39
- data/lib/woods/retriever.rb +317 -71
- data/lib/woods/retry_after.rb +22 -2
- data/lib/woods/ruby_analyzer/class_analyzer.rb +10 -14
- data/lib/woods/ruby_analyzer/fqn_builder.rb +2 -0
- data/lib/woods/ruby_analyzer/mermaid_renderer.rb +14 -4
- data/lib/woods/ruby_analyzer/method_analyzer.rb +1 -1
- data/lib/woods/ruby_analyzer/trace_enricher.rb +3 -0
- data/lib/woods/ruby_analyzer.rb +21 -5
- data/lib/woods/session_tracer/file_store.rb +138 -19
- data/lib/woods/session_tracer/middleware.rb +1 -2
- data/lib/woods/session_tracer/redis_store.rb +122 -12
- data/lib/woods/session_tracer/session_flow_assembler.rb +57 -17
- data/lib/woods/session_tracer/session_flow_document.rb +56 -14
- data/lib/woods/session_tracer/solid_cache_coordination.rb +192 -0
- data/lib/woods/session_tracer/solid_cache_store.rb +560 -91
- data/lib/woods/session_tracer/store.rb +14 -1
- data/lib/woods/storage/metadata_store.rb +230 -26
- data/lib/woods/storage/pgvector.rb +180 -22
- data/lib/woods/storage/qdrant.rb +367 -41
- data/lib/woods/storage/snapshotter/metadata.rb +79 -16
- data/lib/woods/storage/snapshotter/vector.rb +128 -17
- data/lib/woods/storage/snapshotter.rb +23 -5
- data/lib/woods/storage/vector_store.rb +49 -8
- data/lib/woods/storage_identity.rb +28 -0
- data/lib/woods/tasks.rb +53 -2
- data/lib/woods/temporal/json_snapshot_store.rb +112 -42
- data/lib/woods/temporal/snapshot_store.rb +139 -42
- data/lib/woods/unblocked/client.rb +119 -17
- data/lib/woods/unblocked/document_builder.rb +34 -2
- data/lib/woods/unblocked/exporter.rb +63 -27
- data/lib/woods/unblocked/rate_limiter.rb +23 -9
- data/lib/woods/unblocked/sync_manifest.rb +16 -8
- data/lib/woods/update_check.rb +24 -1
- data/lib/woods/util/uuid5.rb +124 -0
- data/lib/woods/version.rb +1 -1
- data/lib/woods/watch/daemon.rb +1345 -0
- data/lib/woods/watch/listen_watcher.rb +81 -0
- data/lib/woods/watch/polling_watcher.rb +137 -0
- data/lib/woods/watch/status.rb +169 -0
- data/lib/woods/watch/tree_scan.rb +163 -0
- data/lib/woods/watch/watcher.rb +100 -0
- data/lib/woods.rb +138 -9
- data/plugin/.claude-plugin/plugin.json +18 -0
- data/plugin/hooks/hooks.json +29 -0
- data/plugin/hooks/woods-post-edit.sh +226 -0
- data/plugin/hooks/woods-session-start.sh +77 -0
- data/plugin/skills/woods-agent-enable/SKILL.md +51 -0
- data/plugin/skills/woods-diagnose/SKILL.md +75 -0
- data/plugin/skills/woods-investigate/SKILL.md +39 -0
- data/plugin/skills/woods-mcp-config/SKILL.md +101 -0
- data/plugin/skills/woods-setup/SKILL.md +99 -0
- metadata +134 -23
- data/lib/woods/console/adapters/cache_adapter.rb +0 -58
- data/lib/woods/console/adapters/good_job_adapter.rb +0 -33
- data/lib/woods/console/adapters/job_adapter.rb +0 -74
- data/lib/woods/console/adapters/sidekiq_adapter.rb +0 -33
- data/lib/woods/console/adapters/solid_queue_adapter.rb +0 -33
- data/lib/woods/console/bridge.rb +0 -210
- data/lib/woods/formatting/claude_adapter.rb +0 -98
- data/lib/woods/formatting/generic_adapter.rb +0 -56
- data/lib/woods/formatting/gpt_adapter.rb +0 -64
- data/lib/woods/notion/mapper.rb +0 -40
- data/lib/woods/observability/health_check.rb +0 -79
- data/lib/woods/observability/instrumentation.rb +0 -34
|
@@ -0,0 +1,829 @@
|
|
|
1
|
+
# Console MCP Server Setup
|
|
2
|
+
|
|
3
|
+
The Console MCP Server gives MCP-capable coding tools and agents live access to your Rails application: database counts, record lookups, and schema inspection. It does not expose job-monitoring tools in supported modes. Database work on the request's current connection is rolled back, subject to the side-effect limits documented below.
|
|
4
|
+
|
|
5
|
+
## Transport options at a glance
|
|
6
|
+
|
|
7
|
+
| Option | How it works | When to use |
|
|
8
|
+
|--------|-------------|-------------|
|
|
9
|
+
| [Stdio via rake](#option-a-stdio-via-rake-recommended) | Rake task boots Rails, runs MCP in-process | Local dev, simplest setup |
|
|
10
|
+
| [Docker](#option-b-docker) | Same rake task, piped through `docker exec -i` | Docker/Compose environments |
|
|
11
|
+
| [HTTP Rack middleware](#option-c-http-rack-middleware) | Middleware mounts `/mcp/console` endpoint | Shared access, multiple clients |
|
|
12
|
+
| [Launcher wrapper](#option-d-launcher-wrapper) | Execs the embedded server directly, through Docker, or through SSH | Centralized process-launch config |
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## Option A: Stdio via Rake (recommended)
|
|
17
|
+
|
|
18
|
+
The simplest setup. The `woods:console` rake task boots Rails, then starts the embedded MCP server using stdio transport. All queries run in-process via ActiveRecord, no separate bridge process needed.
|
|
19
|
+
|
|
20
|
+
### Prerequisites
|
|
21
|
+
|
|
22
|
+
1. `gem 'woods'` in your Gemfile
|
|
23
|
+
2. `bundle install`
|
|
24
|
+
3. Deliberately enable Console MCP in `config/initializers/woods.rb` after reviewing the trust boundary:
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
Woods.configure do |config|
|
|
28
|
+
config.console_mcp_enabled = true
|
|
29
|
+
config.console_mcp_token = ENV["WOODS_CONSOLE_MCP_TOKEN"]
|
|
30
|
+
end
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The stdio and Docker entry points exit with status 1 while this setting is false. Enabling it grants the MCP process live read access under the blocked-table, redaction, and credential-scanning controls described below.
|
|
34
|
+
|
|
35
|
+
The token authenticates HTTP requests; a stdio client does not send it. Production Rails boot nevertheless requires a token of at least 32 characters whenever Console MCP is enabled, even for a stdio-only setup. Store `WOODS_CONSOLE_MCP_TOKEN` in the application's normal secret store. Outside production a missing token warns and leaves Console HTTP guarded with 401 — the boot warning names both transports, so a stdio-only setup can tell that the 401 is not its symptom and that its own session still works.
|
|
36
|
+
|
|
37
|
+
### How it works
|
|
38
|
+
|
|
39
|
+
The rake task does two things before starting the MCP server:
|
|
40
|
+
|
|
41
|
+
1. **Captures stdout before Rails boots.** Rails boot emits OpenTelemetry warnings, gem notices, and other output to stdout. An MCP client cannot parse these as JSON-RPC, they break the protocol. The rake task redirects stdout → stderr immediately, saves the real stdout fd, and restores it after boot completes.
|
|
42
|
+
2. **Calls `Rails.application.eager_load!`** to load all application models. Without eager loading, only the models that happen to be autoloaded before the first query appear in the registry.
|
|
43
|
+
|
|
44
|
+
### MCP client configuration
|
|
45
|
+
|
|
46
|
+
Add this server entry to your MCP client's project configuration:
|
|
47
|
+
|
|
48
|
+
```json
|
|
49
|
+
{
|
|
50
|
+
"mcpServers": {
|
|
51
|
+
"rails-console": {
|
|
52
|
+
"command": "bundle",
|
|
53
|
+
"args": ["exec", "rake", "woods:console"],
|
|
54
|
+
"cwd": "/path/to/your/rails-app"
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
### What happens under the hood
|
|
61
|
+
|
|
62
|
+
```
|
|
63
|
+
MCP client
|
|
64
|
+
│
|
|
65
|
+
│ spawns via stdio
|
|
66
|
+
│
|
|
67
|
+
▼
|
|
68
|
+
rake woods:console
|
|
69
|
+
│
|
|
70
|
+
├─ capture $stdout before boot
|
|
71
|
+
├─ Rake::Task[:environment].invoke (Rails boots)
|
|
72
|
+
├─ load exe/woods-console
|
|
73
|
+
│ ├─ Rails.application.eager_load!
|
|
74
|
+
│ ├─ build model registry from ActiveRecord::Base.descendants
|
|
75
|
+
│ ├─ Server.build_embedded(model_validator:, safe_context:, ...)
|
|
76
|
+
│ └─ MCP::Server::Transports::StdioTransport.new(server).open
|
|
77
|
+
│
|
|
78
|
+
└─ MCP server responds to tool calls via stdin/stdout
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Like the HTTP path, the stdio server hands `SafeContext` the connection
|
|
82
|
+
*pool*, so every tool call leases a fresh connection and returns it when the
|
|
83
|
+
rolled-back transaction ends. A long-lived stdio session therefore survives a
|
|
84
|
+
database failover or a `wait_timeout` recycle instead of failing every call on
|
|
85
|
+
a stale connection until the client restarts it.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## Option B: Docker
|
|
90
|
+
|
|
91
|
+
Same embedded approach as Option A, but piped through `docker exec -i`. The `-i` flag keeps stdin open for the MCP protocol. The container must be running before the MCP client starts.
|
|
92
|
+
|
|
93
|
+
### Prerequisites
|
|
94
|
+
|
|
95
|
+
- Running container with Rails app
|
|
96
|
+
- `woods` gem in the container's Gemfile
|
|
97
|
+
|
|
98
|
+
### MCP client configuration
|
|
99
|
+
|
|
100
|
+
**Plain Docker:**
|
|
101
|
+
|
|
102
|
+
```json
|
|
103
|
+
{
|
|
104
|
+
"mcpServers": {
|
|
105
|
+
"rails-console": {
|
|
106
|
+
"command": "docker",
|
|
107
|
+
"args": [
|
|
108
|
+
"exec", "-i",
|
|
109
|
+
"your_app_web_1",
|
|
110
|
+
"bundle", "exec", "rake", "woods:console"
|
|
111
|
+
]
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
**Docker Compose** (when the service name is `web`):
|
|
118
|
+
|
|
119
|
+
```json
|
|
120
|
+
{
|
|
121
|
+
"mcpServers": {
|
|
122
|
+
"rails-console": {
|
|
123
|
+
"command": "docker",
|
|
124
|
+
"args": [
|
|
125
|
+
"compose", "exec", "-T", "web",
|
|
126
|
+
"bundle", "exec", "rake", "woods:console"
|
|
127
|
+
],
|
|
128
|
+
"cwd": "/absolute/host/path/to/app"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
> **Note:** Compose uses the service name and `-T` to disable its pseudo-TTY. Plain `docker exec` uses the exact container name from `docker ps` and needs `-i` to keep stdin open.
|
|
135
|
+
|
|
136
|
+
### Environment variables
|
|
137
|
+
|
|
138
|
+
If your Rails app requires environment variables at boot (credentials, database URL), pass them via `docker exec -e` or ensure they are set in the container already:
|
|
139
|
+
|
|
140
|
+
```json
|
|
141
|
+
{
|
|
142
|
+
"mcpServers": {
|
|
143
|
+
"rails-console": {
|
|
144
|
+
"command": "docker",
|
|
145
|
+
"args": [
|
|
146
|
+
"exec", "-i",
|
|
147
|
+
"-e", "RAILS_ENV=development",
|
|
148
|
+
"your_app_web_1",
|
|
149
|
+
"bundle", "exec", "rake", "woods:console"
|
|
150
|
+
]
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
---
|
|
157
|
+
|
|
158
|
+
## Option C: HTTP rack middleware
|
|
159
|
+
|
|
160
|
+
Mount the console as a Rack middleware endpoint. The MCP client connects over HTTP using the streamable-http transport instead of spawning a subprocess. Useful when multiple clients need shared access, or when stdio subprocess spawning is not practical.
|
|
161
|
+
|
|
162
|
+
### Prerequisites
|
|
163
|
+
|
|
164
|
+
1. `gem 'woods'` in Gemfile
|
|
165
|
+
2. `bundle install`
|
|
166
|
+
3. A running Rails server accessible to the MCP client
|
|
167
|
+
|
|
168
|
+
### Rails configuration
|
|
169
|
+
|
|
170
|
+
In an initializer (`config/initializers/woods.rb`):
|
|
171
|
+
|
|
172
|
+
```ruby
|
|
173
|
+
Woods.configure do |config|
|
|
174
|
+
config.console_mcp_enabled = true
|
|
175
|
+
config.console_mcp_token = ENV.fetch('WOODS_CONSOLE_MCP_TOKEN')
|
|
176
|
+
config.console_mcp_allowed_origins = [
|
|
177
|
+
'https://rails.internal.example', # public Rails/MCP Host
|
|
178
|
+
'https://agent.example' # browser client Origin, when applicable
|
|
179
|
+
]
|
|
180
|
+
config.console_redacted_columns =
|
|
181
|
+
Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS + %w[ssn]
|
|
182
|
+
end
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Set `WOODS_CONSOLE_MCP_TOKEN` to a random value of at least 32 characters in
|
|
186
|
+
the Rails server environment. The middleware stack registers automatically via
|
|
187
|
+
the gem's Railtie and requires `Authorization: Bearer <token>` on every Console
|
|
188
|
+
request. Missing or incorrect tokens receive `401 Unauthorized`.
|
|
189
|
+
|
|
190
|
+
For non-loopback access, `console_mcp_allowed_origins` must include the public
|
|
191
|
+
Rails/MCP host. If a browser-based client sends an `Origin` header from a
|
|
192
|
+
different host, include that exact origin too. This allow-list controls both
|
|
193
|
+
DNS-rebinding Host checks and browser CORS; keep Rails' own `config.hosts`, TLS,
|
|
194
|
+
and proxy rules aligned with it. Server-to-server clients normally omit
|
|
195
|
+
`Origin`, but their request `Host` must still be allowed.
|
|
196
|
+
|
|
197
|
+
Do not mount `Woods::Console::RackMiddleware` by itself. The Railtie composes
|
|
198
|
+
`OriginGuard`, `BearerAuth`, and the Console middleware in the supported order.
|
|
199
|
+
|
|
200
|
+
Streamable HTTP is stateless by default. For a legacy client that still
|
|
201
|
+
requires MCP session IDs, a custom guarded mount may pass `stateless: false`;
|
|
202
|
+
the default Railtie mount remains stateless.
|
|
203
|
+
|
|
204
|
+
### MCP client configuration
|
|
205
|
+
|
|
206
|
+
For an MCP client that supports Streamable HTTP:
|
|
207
|
+
|
|
208
|
+
```json
|
|
209
|
+
{
|
|
210
|
+
"mcpServers": {
|
|
211
|
+
"rails-console": {
|
|
212
|
+
"type": "streamable-http",
|
|
213
|
+
"url": "http://localhost:3000/mcp/console",
|
|
214
|
+
"headers": {
|
|
215
|
+
"Authorization": "Bearer <same WOODS_CONSOLE_MCP_TOKEN value>"
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
For production or staging, use HTTPS in addition to the mandatory bearer token.
|
|
223
|
+
|
|
224
|
+
### What happens under the hood
|
|
225
|
+
|
|
226
|
+
The middleware lazy-initializes the MCP server on first request:
|
|
227
|
+
|
|
228
|
+
```
|
|
229
|
+
First HTTP request to /mcp/console
|
|
230
|
+
│
|
|
231
|
+
├─ mutex-locked initialization
|
|
232
|
+
│ ├─ Rails.application.eager_load!
|
|
233
|
+
│ ├─ build model registry from ActiveRecord::Base.descendants
|
|
234
|
+
│ └─ Server.build_embedded(...)
|
|
235
|
+
│ └─ StreamableHTTPTransport wraps the server
|
|
236
|
+
│
|
|
237
|
+
└─ subsequent requests: transport.handle_request(rack_request)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Each request gets its own database connection from the connection pool. `SafeContext` wraps that connection in a rolled-back transaction.
|
|
241
|
+
|
|
242
|
+
### Security note
|
|
243
|
+
|
|
244
|
+
The HTTP endpoint grants read access to live database data. In production environments:
|
|
245
|
+
|
|
246
|
+
- Restrict the path to internal networks or authenticated users
|
|
247
|
+
- Use `console_redacted_columns` to redact sensitive fields (see [Configuration Options](#configuration-options))
|
|
248
|
+
- Consider mounting only in `development` and `staging` environments
|
|
249
|
+
|
|
250
|
+
---
|
|
251
|
+
|
|
252
|
+
## Option D: Launcher wrapper
|
|
253
|
+
|
|
254
|
+
`woods-console-mcp` is a process launcher. It replaces itself with the same
|
|
255
|
+
embedded server used by Options A-C, either directly or through `docker exec`
|
|
256
|
+
or `ssh`. It does not enable additional tool tiers.
|
|
257
|
+
|
|
258
|
+
This option requires the Woods gem and `woods-console-mcp` executable on the
|
|
259
|
+
host that runs the MCP client. If Woods exists only inside a container, use
|
|
260
|
+
[Option B](#option-b-docker) instead. When Woods is in a host application
|
|
261
|
+
bundle rather than installed as a standalone executable, launch it through
|
|
262
|
+
that bundle and set the application directory as `cwd`:
|
|
263
|
+
|
|
264
|
+
```json
|
|
265
|
+
{
|
|
266
|
+
"command": "bundle",
|
|
267
|
+
"args": ["exec", "woods-console-mcp"],
|
|
268
|
+
"cwd": "/absolute/host/path/to/app"
|
|
269
|
+
}
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### How it works
|
|
273
|
+
|
|
274
|
+
```
|
|
275
|
+
MCP client
|
|
276
|
+
│
|
|
277
|
+
├─ spawns: woods-console-mcp (reads console.yml)
|
|
278
|
+
│
|
|
279
|
+
▼
|
|
280
|
+
ConnectionManager
|
|
281
|
+
│
|
|
282
|
+
└─ exec direct / docker exec -i / ssh
|
|
283
|
+
└─ bundle exec rake woods:console
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
### Configuration
|
|
287
|
+
|
|
288
|
+
Create `~/.woods/console.yml` (or point `WOODS_CONSOLE_CONFIG` to any YAML file):
|
|
289
|
+
|
|
290
|
+
Direct:
|
|
291
|
+
|
|
292
|
+
```yaml
|
|
293
|
+
mode: direct
|
|
294
|
+
directory: /path/to/rails-app
|
|
295
|
+
command: bundle exec rake woods:console
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
Docker:
|
|
299
|
+
|
|
300
|
+
```yaml
|
|
301
|
+
mode: docker
|
|
302
|
+
container: my_app_web_1
|
|
303
|
+
command: bundle exec rake woods:console
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
SSH:
|
|
307
|
+
|
|
308
|
+
```yaml
|
|
309
|
+
mode: ssh
|
|
310
|
+
host: app.example.com
|
|
311
|
+
user: deploy
|
|
312
|
+
command: cd /app && bundle exec rake woods:console
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
Override config path with environment variable:
|
|
316
|
+
|
|
317
|
+
```bash
|
|
318
|
+
WOODS_CONSOLE_CONFIG=/path/to/console.yml woods-console-mcp
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
### MCP client configuration
|
|
322
|
+
|
|
323
|
+
```json
|
|
324
|
+
{
|
|
325
|
+
"mcpServers": {
|
|
326
|
+
"rails-console": {
|
|
327
|
+
"command": "woods-console-mcp",
|
|
328
|
+
"env": {
|
|
329
|
+
"WOODS_CONSOLE_CONFIG": "/path/to/console.yml"
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
> **Tier support:** Every launcher target exposes the embedded surface: 9 Tier 1 tools by default, or 11 tools when `console_sql` and `console_query` are explicitly enabled.
|
|
337
|
+
|
|
338
|
+
---
|
|
339
|
+
|
|
340
|
+
## Tool support by mode
|
|
341
|
+
|
|
342
|
+
The codebase keeps schemas for 31 possible tools as an inventory. Supported
|
|
343
|
+
servers register only executable tools: the 9 Tier 1 tools by default, plus
|
|
344
|
+
`console_sql` and `console_query` when `embedded_read_tools: true` (or
|
|
345
|
+
`config.console_embedded_read_tools = true`). Tier 2, Tier 3, and
|
|
346
|
+
`console_eval` are not registered in any supported mode.
|
|
347
|
+
|
|
348
|
+
### Tier 1: Read-only (9 tools): supported in all modes
|
|
349
|
+
|
|
350
|
+
| Tool | Description |
|
|
351
|
+
|------|-------------|
|
|
352
|
+
| `console_status` | Health check: available models and database adapter |
|
|
353
|
+
| `console_schema` | Column types, nullability, defaults; optionally includes indexes |
|
|
354
|
+
| `console_count` | Record count matching scope conditions |
|
|
355
|
+
| `console_sample` | Random sample of records (max 25) |
|
|
356
|
+
| `console_find` | Find a record by primary key or unique column |
|
|
357
|
+
| `console_pluck` | Extract column values with optional distinct (max 1000 rows) |
|
|
358
|
+
| `console_aggregate` | Run `sum`, `average`, `minimum`, `maximum`, or `count` on a column (column optional for `count`) |
|
|
359
|
+
| `console_association_count` | Count associated records for a specific record |
|
|
360
|
+
| `console_recent` | Recently created/updated records (max 50) |
|
|
361
|
+
|
|
362
|
+
### Tier 2: Domain-aware (9 tools): inventory only, not executable
|
|
363
|
+
|
|
364
|
+
| Tool | Description |
|
|
365
|
+
|------|-------------|
|
|
366
|
+
| `console_diagnose_model` | Full model diagnostic: counts, recent records, aggregates |
|
|
367
|
+
| `console_data_snapshot` | Record with associations for debugging (depth 1–3) |
|
|
368
|
+
| `console_validate_record` | Run validations on an existing record with optional changes |
|
|
369
|
+
| `console_validate_with` | Validate attributes against a model without persisting |
|
|
370
|
+
| `console_check_setting` | Check a configuration setting value |
|
|
371
|
+
| `console_update_setting` | Update a setting (requires confirmation) |
|
|
372
|
+
| `console_check_policy` | Check authorization policy for a record and user |
|
|
373
|
+
| `console_check_eligibility` | Check feature eligibility for a record |
|
|
374
|
+
| `console_decorate` | Invoke a decorator and return computed attributes |
|
|
375
|
+
|
|
376
|
+
### Tier 3: Analytics (10 tools): Inventory only, not executable
|
|
377
|
+
|
|
378
|
+
| Tool | Description |
|
|
379
|
+
|------|-------------|
|
|
380
|
+
| `console_slow_endpoints` | Slowest endpoints by response time |
|
|
381
|
+
| `console_error_rates` | Error rates by controller or overall |
|
|
382
|
+
| `console_throughput` | Request throughput over time |
|
|
383
|
+
| `console_job_queues` | Job queue statistics |
|
|
384
|
+
| `console_job_failures` | Recent job failures |
|
|
385
|
+
| `console_job_find` | Find a job by ID, optionally retry (requires confirmation) |
|
|
386
|
+
| `console_job_schedule` | Scheduled/upcoming jobs |
|
|
387
|
+
| `console_redis_info` | Redis server information by section |
|
|
388
|
+
| `console_cache_stats` | Cache store statistics |
|
|
389
|
+
| `console_channel_status` | ActionCable channel status |
|
|
390
|
+
|
|
391
|
+
### Tier 4: Guarded (3 tools): Read tools opt-in; eval inventory only
|
|
392
|
+
|
|
393
|
+
| Tool | Description |
|
|
394
|
+
|------|-------------|
|
|
395
|
+
| `console_eval` | Inventory schema only; not registered by supported modes |
|
|
396
|
+
| `console_sql` | Execute read-only SQL, `SELECT` and `WITH...SELECT` only |
|
|
397
|
+
| `console_query` | Enhanced query builder with joins, grouping, and HAVING |
|
|
398
|
+
|
|
399
|
+
`console_sql` and `console_query` register with `embedded_read_tools: true`.
|
|
400
|
+
`console_eval` remains unavailable; legacy unsafe-eval settings fail closed at boot.
|
|
401
|
+
|
|
402
|
+
---
|
|
403
|
+
|
|
404
|
+
## Configuration options
|
|
405
|
+
|
|
406
|
+
Set these in your Rails initializer:
|
|
407
|
+
|
|
408
|
+
```ruby
|
|
409
|
+
Woods.configure do |config|
|
|
410
|
+
# Master on/off switch for the Console MCP feature (Layer 0). Default: false.
|
|
411
|
+
# Applies to every transport: stdio, launcher wrapper, and Rack.
|
|
412
|
+
# When false, stdio entry points exit with a "disabled" notice and the Rack
|
|
413
|
+
# middleware passes the request through to the host app untouched (the
|
|
414
|
+
# console path is indistinguishable from an unknown route). Set to true only
|
|
415
|
+
# after configuring the layers below that match your threat model.
|
|
416
|
+
config.console_mcp_enabled = true
|
|
417
|
+
|
|
418
|
+
# URL path for the Rack middleware endpoint. Default: '/mcp/console'.
|
|
419
|
+
config.console_mcp_path = '/mcp/console'
|
|
420
|
+
|
|
421
|
+
# HTTP Origin + Host allow-list. Defaults to loopback only. Non-loopback
|
|
422
|
+
# Rack deployments must include their public MCP host; browser clients from
|
|
423
|
+
# another origin need that exact origin listed too.
|
|
424
|
+
config.console_mcp_allowed_origins = [
|
|
425
|
+
'https://rails.internal.example',
|
|
426
|
+
'https://agent.example'
|
|
427
|
+
]
|
|
428
|
+
|
|
429
|
+
# Layer 1. Table names that must never appear in a response.
|
|
430
|
+
# Default: Woods::DEFAULT_CONSOLE_BLOCKED_TABLES (8 tables, see below).
|
|
431
|
+
# Matched against :model (resolved via ActiveRecord), :table, and :sql args.
|
|
432
|
+
# A blocked table rejects the tool call at dispatch, before the executor runs.
|
|
433
|
+
# Case-insensitive.
|
|
434
|
+
config.console_blocked_tables = Woods::DEFAULT_CONSOLE_BLOCKED_TABLES + %w[authorizations]
|
|
435
|
+
|
|
436
|
+
# Layer 2. Content-shape credential scanner. Walks the final response tree
|
|
437
|
+
# and replaces credential-shaped substrings (Stripe sk_*, AWS AKIA*, GCP
|
|
438
|
+
# private keys, GitHub ghp_*, generic high-entropy tokens) with "[REDACTED]".
|
|
439
|
+
# Runs regardless of column naming, so it catches leaks that column-based
|
|
440
|
+
# redaction alone would miss. Default: [] (every pattern active).
|
|
441
|
+
#
|
|
442
|
+
# Accepts a list of pattern symbols to skip. Useful when a rule produces
|
|
443
|
+
# false positives in your data. Pass the :all sentinel to disable the
|
|
444
|
+
# scanner entirely (no layer-2 processing runs).
|
|
445
|
+
config.console_disabled_scanner_patterns = %i[stripe_publishable_key]
|
|
446
|
+
# config.console_disabled_scanner_patterns = %i[all] # disable scanner entirely
|
|
447
|
+
|
|
448
|
+
# Layer 2 augmentation, boot-time credential index. Default: true.
|
|
449
|
+
# Woods::Console::CredentialIndex walks Rails.application.credentials.config
|
|
450
|
+
# once at server boot and substring-redacts those values from every MCP
|
|
451
|
+
# response, so credentials whose shape no scanner pattern recognizes
|
|
452
|
+
# (Twilio auth tokens, hand-rolled HMAC seeds, third-party webhook
|
|
453
|
+
# signing keys) are still caught when their exact contents appear.
|
|
454
|
+
# See "console_credential_defense_enabled" section below for scope and
|
|
455
|
+
# multi-DB caveats.
|
|
456
|
+
config.console_credential_defense_enabled = true
|
|
457
|
+
|
|
458
|
+
# Layer 3. Column names to redact from all query results.
|
|
459
|
+
# Default: Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS (~30 credential-shaped
|
|
460
|
+
# columns, see below). Replaced with "[REDACTED]" in output.
|
|
461
|
+
config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS + %w[ssn]
|
|
462
|
+
|
|
463
|
+
# Layer 3, EAV (key-value) redaction patterns. Default: [].
|
|
464
|
+
# See `console_redacted_key_values` section below for the pattern contract.
|
|
465
|
+
config.console_redacted_key_values = [
|
|
466
|
+
{ key_column: 'key', value_column: 'value',
|
|
467
|
+
sensitive_keys: %w[stripe_access_token oauth_token] }
|
|
468
|
+
]
|
|
469
|
+
|
|
470
|
+
# Unlock console_sql / console_query in the embedded executor. Default: false.
|
|
471
|
+
# Flows through to the Rack middleware AND the stdio entry point (rake / rails runner).
|
|
472
|
+
# See "Unlocking console_sql / console_query in embedded mode" below.
|
|
473
|
+
config.console_embedded_read_tools = false
|
|
474
|
+
end
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
### `console_mcp_enabled` (layer 0: feature gate)
|
|
478
|
+
|
|
479
|
+
Until this flag is `true`, none of the transports route traffic:
|
|
480
|
+
|
|
481
|
+
- `exe/woods-console` prints a notice to stderr and exits 1. `exe/woods-console-mcp` execs that target, so MCP clients see the same startup failure.
|
|
482
|
+
- `Woods::Console::RackMiddleware` passes the request through to the host app (typically its 404), so a disabled console path is indistinguishable from an unknown route. Non-matching paths always pass through untouched.
|
|
483
|
+
|
|
484
|
+
Keep the flag off in environments where the Console isn't needed (production web tier, CI). Flip it on per-environment, e.g. in `config/environments/development.rb` or a staging-only initializer, once the layers below are configured for that environment's threat model.
|
|
485
|
+
|
|
486
|
+
### `console_blocked_tables` (layer 1: table gate)
|
|
487
|
+
|
|
488
|
+
Entries are lowercased table names. A tool call is rejected at dispatch time when:
|
|
489
|
+
|
|
490
|
+
- `:model` argument resolves to a model whose `table_name` is blocked
|
|
491
|
+
- `:table` argument names a blocked table
|
|
492
|
+
- `:sql` argument references a blocked table (matched on identifier tokens, case-insensitive)
|
|
493
|
+
|
|
494
|
+
**Ships with a curated default list** (`Woods::DEFAULT_CONSOLE_BLOCKED_TABLES`, 8 tables) covering common auth/credential storage across Devise, Doorkeeper, Rodauth, Sorcery, OmniAuth, and hand-rolled token systems: `sessions`, `api_keys`, `credentials`, `oauth_applications`, `oauth_access_tokens`, `oauth_refresh_tokens`, `identities`, `active_storage_blobs`.
|
|
495
|
+
|
|
496
|
+
`users` / `accounts` are intentionally excluded, many apps expose safe columns from these and should decide explicitly, as are PII-heavy but auth-unrelated tables (`payments`, `addresses`), which are an org-specific compliance concern.
|
|
497
|
+
|
|
498
|
+
Extend or override rather than reassigning blindly:
|
|
499
|
+
|
|
500
|
+
```ruby
|
|
501
|
+
# Extend: keep all defaults plus app-specific tables
|
|
502
|
+
config.console_blocked_tables = Woods::DEFAULT_CONSOLE_BLOCKED_TABLES + %w[authorizations settings]
|
|
503
|
+
|
|
504
|
+
# Remove a default that over-blocks in your app
|
|
505
|
+
config.console_blocked_tables = Woods::DEFAULT_CONSOLE_BLOCKED_TABLES - %w[active_storage_blobs]
|
|
506
|
+
|
|
507
|
+
# Replace entirely: only do this if you've audited the default list against your schema
|
|
508
|
+
config.console_blocked_tables = %w[only_this]
|
|
509
|
+
|
|
510
|
+
# Disable Layer 1 entirely (other layers still apply)
|
|
511
|
+
config.console_blocked_tables = []
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Use this to wall off tables that shouldn't appear in agent context regardless of redaction posture, EAV credential stores, audit logs with full request bodies, or PII stores with legal access restrictions. Rejection is observable via the `console.table_gate.rejected` structured log line.
|
|
515
|
+
|
|
516
|
+
### `console_disabled_scanner_patterns` (layer 2: content scanner)
|
|
517
|
+
|
|
518
|
+
The scanner runs after Layer 3 redaction, so it catches credential shapes that column and EAV patterns miss, e.g. a Stripe key pasted into a free-text `note` field, a JWT returned from a custom SQL query, or an access token logged by a callback. See `lib/woods/console/credential_scanner.rb` for the full rule list.
|
|
519
|
+
|
|
520
|
+
Scanner hits emit a `console.credential_scan.hits` warn-level structured log line with per-pattern counts, so you can audit how often the net fires in practice. Prefer fixing the upstream cause (moving the secret out of the leaking column) over disabling a rule, per-pattern opt-outs are an escape hatch, not a primary knob.
|
|
521
|
+
|
|
522
|
+
Setting `console_disabled_scanner_patterns = %i[all]` disables the entire scanner. No layer-2 processing runs. Layer 3 (column + EAV redaction) and Layer 4 (SqlValidator + SafeContext) continue to fire. Use this only when the pattern scanner interferes with a legitimate workflow and the remaining layers cover the threat model; prefer a per-pattern opt-out otherwise.
|
|
523
|
+
|
|
524
|
+
### `console_credential_defense_enabled` (boot-time credential index)
|
|
525
|
+
|
|
526
|
+
`Woods::Console::CredentialIndex` walks `Rails.application.credentials.config`
|
|
527
|
+
once at server boot, collects string leaves with length >= 12, and
|
|
528
|
+
substring-redacts those values from every MCP response. `console_eval` is not
|
|
529
|
+
registered in a supported mode, so this setting does not enable eval.
|
|
530
|
+
|
|
531
|
+
**Restart required after credential rotation.** The index is built once at process start and held in memory for the lifetime of the MCP process. When a host app rotates Rails credentials (`rails credentials:edit`), the MCP process keeps the pre-rotation secrets in its Set until the process is restarted, new secrets are not picked up automatically. Only the Layer 2 shape-pattern scanner (Stripe `sk_*`, AWS `AKIA*`, etc.) can catch newly-rotated values before restart.
|
|
532
|
+
|
|
533
|
+
**Rebuild hook for rotation jobs.** If you rotate credentials from a Rake task or a deployment hook and want to avoid a full restart, call:
|
|
534
|
+
|
|
535
|
+
```ruby
|
|
536
|
+
Woods::Console::Server.rebuild_credential_index(rails_app: Rails.application)
|
|
537
|
+
```
|
|
538
|
+
|
|
539
|
+
This rebuilds the index from the current credentials and hot-swaps it into the active scanner. The swap is atomic on MRI, in-flight scans see either the old or the new index, never a partial one. The method is a no-op (returns `nil`) when no server has been built yet or when `console_credential_defense_enabled` is `false`.
|
|
540
|
+
|
|
541
|
+
**Rotation warning.** At boot time, Woods checks whether any credentials file (`config/credentials.yml.enc`, `config/credentials/<env>.yml.enc`) was modified *after* the process started. When it detects this, it emits a `console.credential_index.stale` warn-level structured log line with the file path, mtime, and a hint to restart or call `rebuild_credential_index`. This check is on by default; disable it with:
|
|
542
|
+
|
|
543
|
+
```ruby
|
|
544
|
+
config.console_credential_rotation_warning = false
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
**Multi-DB / sharded caveat.** The index reflects only the credentials available to the *Rails process* that boots the Console MCP server. A separate database that holds its own secrets (e.g., a vendored CMS app sharing the same Rails host) is not in scope, for those, lean on Layer 3 (`console_redacted_columns` / `console_redacted_key_values`) and Layer 1 (`console_blocked_tables`).
|
|
548
|
+
|
|
549
|
+
**Missing master key.** In environments without `config/master.key` (CI, fresh checkouts), the index build catches `MissingKeyError` / `InvalidMessage` by class name and returns an empty index. The server still boots and the configured table, scanner, and redaction layers remain in effect.
|
|
550
|
+
|
|
551
|
+
Set the flag to `false` only when the host intentionally opts out of exact-value credential indexing and the remaining scanner/redaction layers cover its response data.
|
|
552
|
+
|
|
553
|
+
### `console_redacted_columns`
|
|
554
|
+
|
|
555
|
+
Redaction replaces matching column values with `"[REDACTED]"` before the MCP response is sent. Column names are matched by string, case-sensitive, use the exact names from your database schema.
|
|
556
|
+
|
|
557
|
+
Redacted columns are also **refused as query inputs**: as the `column` of `console_aggregate`, as a scope or `by:` key (including `_matches` forms), and as an order or grouping key. EAV `value_column` entries from `console_redacted_key_values` are refused in those predicate, ordering, and grouping positions for the same reason; EAV `key_column` entries remain valid predicates because they identify the row whose value should be masked. Aggregates refuse either column of an EAV pair. Masking output alone would leave a comparison, ordering, or aggregate oracle over the secret.
|
|
558
|
+
|
|
559
|
+
For `console_query`'s `select`, three expression shapes are additionally refused, because the redactor masks by output header name and these shapes rename or read the protected value:
|
|
560
|
+
|
|
561
|
+
- **An `AS` alias over a redacted column** (`password_digest AS note`) — the value would return plaintext under a header no redaction rule matches.
|
|
562
|
+
- **An aggregate over a redacted column, aliased or bare** (`SUM(salary)`, `SUM(salary) AS total`) — the aggregate itself is the secret's value. Aggregates over either column of a `console_redacted_key_values` pair are refused the same way.
|
|
563
|
+
- **An `AS` alias over either column of a `console_redacted_key_values` pair** — the positional EAV rule resolves key/value columns by header name, so renaming either header disarms it.
|
|
564
|
+
- **Any `AS` alias whose name collides with a protected header** (`id AS value` when `value` is an EAV value column) — the duplicate header could shadow the real column and steal its mask. Alias names are compared case-insensitively, matching unquoted SQL identifier folding.
|
|
565
|
+
|
|
566
|
+
Direct, unaliased selection of a redacted column stays allowed: the output header keeps the column's real name, and the positional redactor masks the value as usual. For an EAV pair, the **value column is only accepted when its paired key column is selected directly too** (that is what lets the positional rule mask it); selecting the value column alone is refused across `console_query`, `console_sample`, `console_find`, `console_pluck`, and `console_recent`.
|
|
567
|
+
|
|
568
|
+
`console_sql` applies a stricter form of the same rule because arbitrary SQL can rename output headers. A protected identifier is accepted only as a direct, unaliased outer `SELECT` column; aliases, aggregates, predicates, CTE shapes, ordering/grouping uses, and an EAV value without its paired key are refused before adapter execution. Use `console_query` when a protected column must participate in a more complex structured query.
|
|
569
|
+
|
|
570
|
+
`console_query`'s `having` is covered by the same oracle rule: an aggregate over a protected column (`MAX(amount) > ?`), a bare predicate on a redacted column (`salary > ?`), or a predicate on an EAV value column is refused, since repeated guesses reveal the protected value from whether a row is returned. EAV key predicates stay allowed so callers can select the rows whose paired values need redaction.
|
|
571
|
+
|
|
572
|
+
**Ships with a curated credential default list** (`Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS`, 31 columns) covering Devise, Doorkeeper, Rodauth, has_secure_password, devise-two-factor, and common hand-rolled auth shapes: `password`, `password_digest`, `password_salt`, `encrypted_password`, `crypted_password`, `salt`, `otp_secret`, `encrypted_otp_secret`, `two_factor_secret`, `backup_codes`, `consumed_timestep`, `reset_password_token`, `confirmation_token`, `unlock_token`, `remember_token`, `invitation_token`, `access_token`, `refresh_token`, `auth_token`, `api_token`, `api_key`, `bearer_token`, `client_secret`, `webhook_secret`, `signing_secret`, `session_secret`, `private_key`, `encrypted_private_key`, `key_hash`, `token`, `secret`.
|
|
573
|
+
|
|
574
|
+
Extend or override rather than reassigning blindly:
|
|
575
|
+
|
|
576
|
+
```ruby
|
|
577
|
+
# Extend: keep all defaults plus app-specific columns
|
|
578
|
+
config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS + %w[cart_token share_token]
|
|
579
|
+
|
|
580
|
+
# Add PII on top of the credential defaults
|
|
581
|
+
config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS + %w[email phone_number ssn]
|
|
582
|
+
|
|
583
|
+
# Remove a default that over-redacts in your app (e.g., `token` is a non-secret slug)
|
|
584
|
+
config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS - %w[token]
|
|
585
|
+
|
|
586
|
+
# Replace entirely: only do this if you've audited the default list against your schema
|
|
587
|
+
config.console_redacted_columns = %w[password_digest api_key]
|
|
588
|
+
```
|
|
589
|
+
|
|
590
|
+
Columns intentionally **excluded** from the default list because they cause over-redaction in apps that use them legitimately:
|
|
591
|
+
|
|
592
|
+
- `key`: ActiveStorage blob keys, EAV key columns, translation keys
|
|
593
|
+
- `name`: universal non-secret identifier
|
|
594
|
+
- PII columns (`ssn`, `tax_id`, `dob`), org-specific compliance concern, prefer explicit opt-in
|
|
595
|
+
|
|
596
|
+
Redaction is shape-aware and covers every tool that returns row data:
|
|
597
|
+
|
|
598
|
+
| Tool | Output shape | How redaction applies |
|
|
599
|
+
| ------------------------------------- | --------------------------------------------------------- | --------------------- |
|
|
600
|
+
| `console_find` | `{record: Hash}` | Redacted column keys are replaced inside the nested record |
|
|
601
|
+
| `console_sample`, `console_recent` | `{records: [Hash, ...]}` | Each record hash is redacted |
|
|
602
|
+
| `console_sql`, `console_query` | `{columns: [...], rows: [[...], ...], count: N}` | Positional, rows are redacted by matching the `columns` header |
|
|
603
|
+
| `console_pluck` | `{columns: [...], values: [[...], ...]}` or `{values: [...]}` for a single column | Positional, multi-column rows and flat single-column arrays both covered |
|
|
604
|
+
| `console_count`, `console_aggregate`, `console_association_count`, `console_schema` | No row data | Nothing to redact |
|
|
605
|
+
|
|
606
|
+
Redaction is defense-in-depth, prefer not storing plaintext secrets in database columns in the first place, but it keeps configured credential columns out of the agent's transcript when `console_sample`, `console_find`, or the Tier 4 read tools return matching rows.
|
|
607
|
+
|
|
608
|
+
### `console_redacted_key_values`
|
|
609
|
+
|
|
610
|
+
Column-name redaction falls short when credentials are stored in a **key-value (EAV)** table, e.g. a Stripe Connect `authorizations` row of `{key: "stripe_access_token", value: "sk_live_..."}`. The column holding the secret is called `value`, which is generic: adding `value` to `console_redacted_columns` would over-redact every unrelated row in the table.
|
|
611
|
+
|
|
612
|
+
`console_redacted_key_values` takes one or more patterns that describe "when a row has `key_column` set to one of these names, redact its `value_column`":
|
|
613
|
+
|
|
614
|
+
```ruby
|
|
615
|
+
# Example: an `authorizations` table: pattern works on both MySQL and PostgreSQL.
|
|
616
|
+
config.console_redacted_key_values = [
|
|
617
|
+
{
|
|
618
|
+
key_column: 'key',
|
|
619
|
+
value_column: 'value',
|
|
620
|
+
sensitive_keys: %w[stripe_access_token stripe_publishable_key stripe_user_id
|
|
621
|
+
oauth_token refresh_token client_secret]
|
|
622
|
+
}
|
|
623
|
+
]
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
```ruby
|
|
627
|
+
# An app with a generic `settings` table on MySQL or PostgreSQL uses a different
|
|
628
|
+
# column layout: patterns stack without interfering.
|
|
629
|
+
config.console_redacted_key_values = [
|
|
630
|
+
{ key_column: 'name', value_column: 'value',
|
|
631
|
+
sensitive_keys: %w[smtp_password slack_webhook_url] },
|
|
632
|
+
{ key_column: 'key', value_column: 'value',
|
|
633
|
+
sensitive_keys: %w[stripe_access_token oauth_token] }
|
|
634
|
+
]
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
Behavior:
|
|
638
|
+
|
|
639
|
+
| Response shape | EAV redaction applies when |
|
|
640
|
+
| --------------------------------------------------------------------- | ------------------------------------------------------------------- |
|
|
641
|
+
| `console_find`, `{record: {..., key: ..., value: ...}}` | `record[key_column]` ∈ `sensitive_keys` → `record[value_column] = "[REDACTED]"` |
|
|
642
|
+
| `console_sample`, `console_recent`, `{records: [{key:, value:}, ...]}` | Per-row, each record is evaluated against every configured pattern |
|
|
643
|
+
| `console_sql`, `console_query`, `{columns: [...], rows: [[...]]}` | Positional, `key_column` and `value_column` resolved to indexes once, per row lookup afterwards |
|
|
644
|
+
| `console_pluck`, `{columns: [...], values: [[...]]}` | Same positional logic as `rows` |
|
|
645
|
+
|
|
646
|
+
A pattern is skipped silently when its `key_column` or `value_column` is absent from the current `columns` header, so unrelated queries pay nothing for the configuration. Comparison is case-sensitive and coerces the key cell through `to_s` before matching, so `:stripe_access_token` and `"stripe_access_token"` both fire.
|
|
647
|
+
|
|
648
|
+
`console_redacted_columns` and `console_redacted_key_values` run in a single pass, configure both for apps that store credentials in both dedicated columns (e.g. `crypted_password`) and EAV rows (e.g. `authorizations.value`).
|
|
649
|
+
|
|
650
|
+
### Unlocking `console_sql` / `console_query`
|
|
651
|
+
|
|
652
|
+
All three embedded transports (Options A, B, C) and the launcher wrapper
|
|
653
|
+
(Option D) start the same embedded server, so they all read one setting:
|
|
654
|
+
`console_embedded_read_tools`. Default `false`, `console_sql` and
|
|
655
|
+
`console_query` return an `error_type: "unsupported"` refusal without ever
|
|
656
|
+
touching ActiveRecord, and neither is registered in `tools/list`.
|
|
657
|
+
|
|
658
|
+
```ruby
|
|
659
|
+
# config/initializers/woods.rb
|
|
660
|
+
Woods.configure do |config|
|
|
661
|
+
config.console_mcp_enabled = true # mount the Rack middleware via Railtie
|
|
662
|
+
config.console_mcp_token = ENV.fetch('WOODS_CONSOLE_MCP_TOKEN')
|
|
663
|
+
config.console_embedded_read_tools = true # unlock console_sql / console_query
|
|
664
|
+
config.console_redacted_columns = Woods::DEFAULT_CONSOLE_REDACTED_COLUMNS
|
|
665
|
+
end
|
|
666
|
+
```
|
|
667
|
+
|
|
668
|
+
With the flag on, every request through `console_sql` / `console_query` runs
|
|
669
|
+
these controls, in order:
|
|
670
|
+
|
|
671
|
+
1. `SqlValidator` rejects DML/DDL (`INSERT`/`UPDATE`/`DELETE`/`MERGE`/`DROP`/`TRUNCATE`/`ALTER`/`CREATE`/`REPLACE`), row-lock clauses (`FOR UPDATE`, `FOR SHARE`, `LOCK IN SHARE MODE`), writable CTEs (every `AS (...)` body, not just the first), `UNION`/`INTO`/`COPY`, multi-statement and comment-hidden injections, and most administrative keywords (`DO`, `SET`, `LISTEN`, `NOTIFY`, `CALL`, `LOAD`, `VACUUM`, `PREPARE`, transaction control, `EXPLAIN ANALYZE`) at the string level. Enforces a read-only **function allowlist** (`ALLOWED_FUNCTIONS`), anything not on it is rejected by name, quoted forms (`"pg_terminate_backend"(…)`) included. Only `SELECT`, `WITH…SELECT`, and plain `EXPLAIN` pass.
|
|
672
|
+
2. `TableGate` refuses any SQL, model, or join that touches a `console_blocked_tables` entry.
|
|
673
|
+
3. `SafeContext` wraps every request in a rolled-back transaction with a short statement timeout. **It does NOT cover async side effects**: ActiveJob `perform_later`, ActionMailer `deliver_later`, direct HTTP egress, `Thread.new`-spawned work, `after_rollback` callbacks, and writes through a different shard all execute as live. Treat the Console MCP as an admin-trust boundary, not a sandbox.
|
|
674
|
+
4. `CredentialScanner` + column/EAV redaction scrub results.
|
|
675
|
+
|
|
676
|
+
Keep the flag off when the host requires a narrower database capability.
|
|
677
|
+
|
|
678
|
+
---
|
|
679
|
+
|
|
680
|
+
## Safety model
|
|
681
|
+
|
|
682
|
+
The executable Console surface uses the following defense layers in every
|
|
683
|
+
supported transport (stdio, Docker/SSH launcher, and HTTP).
|
|
684
|
+
|
|
685
|
+
| # | Layer | Knob | Fires at | Purpose |
|
|
686
|
+
|---|-------|------|----------|---------|
|
|
687
|
+
| 0 | Feature gate | `console_mcp_enabled` | Process start / request entry | Master on/off switch, feature is inert until an operator opts in |
|
|
688
|
+
| 1 | Blocked tables | `console_blocked_tables` | Tool dispatch, before executor | Reject any tool call that touches a named table (model, table, or sql arg) |
|
|
689
|
+
| 2 | Credential scanner | `console_disabled_scanner_patterns` (`[:all]` to disable entirely) | After executor, before render | Content-shape redaction of credential-shaped strings anywhere in the response tree |
|
|
690
|
+
| 3 | Column + EAV redaction | `console_redacted_columns`, `console_redacted_key_values` | After executor, before Layer 2 | Identity-based redaction by column name and by key/value row shape |
|
|
691
|
+
| 4 | SqlValidator + SafeContext | built-in | Inside executor | SQL deny-list for `console_sql`; transaction-rollback for every request |
|
|
692
|
+
|
|
693
|
+
Layers 0–3 are configured via `Woods.configure`. Layer 4 is always on and has no knobs. Observability hooks, `console.table_gate.rejected` for Layer 1, `console.credential_scan.hits` for Layer 2, emit structured log lines via `Woods::Observability::StructuredLogger` so operators can audit enforcement without scraping MCP wire traffic.
|
|
694
|
+
|
|
695
|
+
### Confirmation and audit inventory
|
|
696
|
+
|
|
697
|
+
No currently executable tool claims a confirmation or privileged audit-log
|
|
698
|
+
contract. Tier 2, Tier 3, and `console_eval` remain unregistered inventory.
|
|
699
|
+
|
|
700
|
+
### Current-connection rollback
|
|
701
|
+
|
|
702
|
+
Database work performed through the request's current Active Record connection
|
|
703
|
+
runs inside a transaction that is **always rolled back**:
|
|
704
|
+
|
|
705
|
+
```ruby
|
|
706
|
+
def with_rolled_back_transaction
|
|
707
|
+
@connection.transaction do
|
|
708
|
+
set_timeout # statement timeout before any query
|
|
709
|
+
yield # run the tool
|
|
710
|
+
raise ActiveRecord::Rollback # always roll back
|
|
711
|
+
end
|
|
712
|
+
end
|
|
713
|
+
```
|
|
714
|
+
|
|
715
|
+
This means direct mutations on that connection are discarded. It is a final
|
|
716
|
+
guard for ordinary database work, not a sandbox or a universal side-effect
|
|
717
|
+
guarantee. It does **not** undo Active Job or mail delivery, HTTP/network calls,
|
|
718
|
+
threaded work, `after_rollback` callbacks, or writes through another connection
|
|
719
|
+
or shard. The validator, table gate, credential defenses, and operator trust
|
|
720
|
+
boundary remain necessary.
|
|
721
|
+
|
|
722
|
+
- Direct accidental mutation on the wrapped connection is rolled back.
|
|
723
|
+
- External, asynchronous, callback, and cross-connection effects may execute live.
|
|
724
|
+
|
|
725
|
+
### Statement timeout
|
|
726
|
+
|
|
727
|
+
Each transaction sets a statement timeout before any query runs. The default is **5000ms** (5 seconds). Timeout enforcement is adapter-specific:
|
|
728
|
+
|
|
729
|
+
| Adapter | Mechanism | Scope |
|
|
730
|
+
|---------|-----------|-------|
|
|
731
|
+
| PostgreSQL | `SET statement_timeout = '5000ms'` | All statement types |
|
|
732
|
+
| MySQL | `SET max_execution_time = 5000` (session scope; the prior value is restored after the transaction) | SELECT only (MySQL limitation) |
|
|
733
|
+
| Other | Best-effort (skipped gracefully) | n/a |
|
|
734
|
+
|
|
735
|
+
### SQL validation (tier 4 `console_sql`)
|
|
736
|
+
|
|
737
|
+
`SqlValidator` rejects non-read-only SQL at the string level, before any database interaction.
|
|
738
|
+
|
|
739
|
+
Validation runs **once**, inside the executor, with the dialect of the live adapter. There is deliberately no earlier dialect-blind pre-check in the tool handler: a validator built without a dialect is the conservative MySQL+PostgreSQL union, and running it first meant a MySQL host rejected statements whose `\'`/backtick grammar produces a spuriously forbidden PostgreSQL view — the adapter-aware acceptance below could never be reached on a real transport. The executor raises `SqlValidationError` for anything it refuses, which the dispatch pipeline renders as a tool error, so nothing is ungated.
|
|
740
|
+
|
|
741
|
+
|
|
742
|
+
- **Allowed prefixes:** `SELECT`, `WITH...SELECT`, and plain `EXPLAIN`. `EXPLAIN ANALYZE` is rejected, it executes the query rather than just planning it (both the whitespace and `EXPLAIN (ANALYZE, …)` option-list spellings).
|
|
743
|
+
- **Rejected prefixes:** `INSERT`, `UPDATE`, `DELETE`, `MERGE`, `DROP`, `ALTER`, `TRUNCATE`, `CREATE`, `GRANT`, `REVOKE`
|
|
744
|
+
- **Rejected anywhere in query:** `UNION`, `INTO`, `COPY`; row-lock clauses (`FOR UPDATE`, `FOR NO KEY UPDATE`, `FOR SHARE`, `FOR KEY SHARE`, `FOR UPDATE NOWAIT`/`SKIP LOCKED`, MySQL `LOCK IN SHARE MODE`) — these take live row locks even inside the rolled-back transaction. The lock check is adapter-aware: `console_sql` validates with the active adapter's dialect, including MySQL double-quoted strings/backtick identifiers and PostgreSQL quoted identifiers/E-strings. Unknown adapters conservatively scan both normalizations. Every view is scanned under both MySQL executable-comment (`/*!...*/`) semantics, so `#` comments and version-guarded comments cannot split a clause apart.
|
|
745
|
+
- **Function allowlist (the authoritative function control):** every function-call-shaped identifier must appear in `ALLOWED_FUNCTIONS`, a conservative set of pure read-only functions (aggregates, window functions, string/number/date/JSON readers) kept portable across MySQL, PostgreSQL, and SQLite. Anything else is rejected by name, quoted forms (`"pg_terminate_backend"(…)`) included. This is an allowlist because a denylist cannot enumerate every side-effecting function (`nextval`, `pg_advisory_lock`, `pg_terminate_backend`, …). A legacy `DANGEROUS_FUNCTIONS` denylist (`pg_sleep`, `lo_import`, `lo_export`, `pg_read_file`, `pg_write_file`, `load_file`, `sleep`, `benchmark`) still runs first as belt-and-suspenders.
|
|
746
|
+
- **Rejected patterns:** multiple statements (semicolons), writable CTEs (every `AS (...)` body is checked, so a writable CTE in any WITH position is refused — `WITH a AS (SELECT 1), b AS (DELETE FROM users RETURNING *) SELECT * FROM b`), a CTE list attached to top-level DML (`WITH a AS (SELECT 1) DELETE FROM users RETURNING *`), comment-hidden injections
|
|
747
|
+
|
|
748
|
+
### Model and column validation
|
|
749
|
+
|
|
750
|
+
Before any query runs, the model name is checked against the registry built from `ActiveRecord::Base.descendants`. Unrecognized model names raise `ValidationError` without touching the database. Column names are validated against the model's `column_names` before pluck, aggregate, and recent operations.
|
|
751
|
+
|
|
752
|
+
For `console_query`, a schema-qualified column reference such as `orders.total` is validated for **ownership**: the table side is gated through `TableGate` (a blocked table is refused) and the column must actually exist on that table, so a blocked-table column cannot be smuggled through `select`, `order`, or `having`. Bare columns validate against the active model.
|
|
753
|
+
|
|
754
|
+
Scope hashes accept Ransack-style predicate suffixes (`_eq`, `_not_eq`, `_gt`, `_gteq`, `_lt`, `_lteq`, `_in`, `_not_in`, `_null`, `_not_null`, `_present`, `_blank`, `_matches`), see the [cookbook](MCP_TOOL_COOKBOOK.md#scope-predicates) for the full table. Every column name in a suffixed key is validated before an Arel predicate is built, so SQL injection via column names is not possible.
|
|
755
|
+
|
|
756
|
+
---
|
|
757
|
+
|
|
758
|
+
## Troubleshooting
|
|
759
|
+
|
|
760
|
+
### MCP client shows no tools or "connection refused"
|
|
761
|
+
|
|
762
|
+
- **Rake/Docker:** Check that `cwd` in MCP config points to the Rails app root (where `Rakefile` lives).
|
|
763
|
+
- **HTTP:** Check that the Rails server is running and listening on the expected port. An unauthenticated `curl http://localhost:3000/mcp/console` should return `401` when the enabled middleware and bearer-auth guard are mounted. A request with the configured bearer token proceeds to MCP protocol handling.
|
|
764
|
+
- **All modes:** Run `bundle exec rake woods:console` directly in a terminal. It should hang (waiting for MCP protocol input) rather than exit immediately. If it exits, check the error output.
|
|
765
|
+
|
|
766
|
+
### Rails boot noise breaks MCP protocol
|
|
767
|
+
|
|
768
|
+
The rake task redirects stdout to stderr before Rails boots specifically to prevent this. If you see JSON parse errors from the MCP client, check:
|
|
769
|
+
|
|
770
|
+
1. You are using `bundle exec rake woods:console`, not `rails runner exe/woods-console` directly (the runner path handles this too, but via a different mechanism).
|
|
771
|
+
2. No `puts` or `print` calls run at boot in your initializers before the task can capture stdout.
|
|
772
|
+
3. Try running `bundle exec rake woods:console 2>/dev/null` to isolate, the MCP protocol output goes to stdout, Rails noise goes to stderr.
|
|
773
|
+
|
|
774
|
+
### Models not visible to `console_status`
|
|
775
|
+
|
|
776
|
+
`console_status` returns the list of models registered at startup. If a model is missing:
|
|
777
|
+
|
|
778
|
+
1. Check that it inherits from `ActiveRecord::Base` (not from an intermediate abstract class that doesn't itself inherit AR).
|
|
779
|
+
2. Check that `model.table_exists?` returns true, models for tables that don't exist are excluded.
|
|
780
|
+
3. Check that `eager_load!` succeeds. If your app has a directory that fails to load (e.g., `app/graphql/` requiring an uninstalled gem), Zeitwerk may abort early and skip models defined later alphabetically. Look for `NameError` in the boot output.
|
|
781
|
+
|
|
782
|
+
### `console_sql` rejects my query
|
|
783
|
+
|
|
784
|
+
`SqlValidator` is conservative by design. If a valid read-only query is rejected:
|
|
785
|
+
|
|
786
|
+
- `UNION` in any position is blocked, use `console_query` with joins instead.
|
|
787
|
+
- Plain `EXPLAIN` is allowed; `EXPLAIN ANALYZE` is **rejected** because it executes the query rather than just planning it.
|
|
788
|
+
- A function is rejected unless it is on the read-only allowlist (`ALLOWED_FUNCTIONS`). If a legitimate pure/read function is missing, that is the list to extend, deliberately.
|
|
789
|
+
- Queries with semicolons are blocked even if the second statement is a comment, strip trailing semicolons.
|
|
790
|
+
|
|
791
|
+
### A tool from the 31-schema inventory is not listed
|
|
792
|
+
|
|
793
|
+
See [Tool Support by Mode](#tool-support-by-mode): only 9 (or 11 with
|
|
794
|
+
`console_embedded_read_tools`) are ever registered. For `console_sql` /
|
|
795
|
+
`console_query`, see [Unlocking `console_sql` / `console_query`](#unlocking-console_sql--console_query). Tier 2, Tier 3, and `console_eval` are inventory-only in every mode, no flag registers them.
|
|
796
|
+
|
|
797
|
+
### `console_eval` and `WOODS_CONSOLE_UNSAFE_EVAL`
|
|
798
|
+
|
|
799
|
+
`console_eval` is not registered by a supported server mode. Setting
|
|
800
|
+
`WOODS_CONSOLE_UNSAFE_EVAL=true`, enabling the equivalent configuration flag,
|
|
801
|
+
or passing the legacy confirmation/audit options causes server construction to
|
|
802
|
+
fail closed with `Woods::ConfigurationError`.
|
|
803
|
+
|
|
804
|
+
### Slow first request on HTTP/Rack middleware
|
|
805
|
+
|
|
806
|
+
The middleware lazy-initializes the MCP server on the first request, which includes `Rails.application.eager_load!`. This can take several seconds on large apps. Subsequent requests are fast. If you want to pre-warm, call a health check endpoint that touches the middleware path at app startup.
|
|
807
|
+
|
|
808
|
+
### Timeout errors on large models
|
|
809
|
+
|
|
810
|
+
The default statement timeout is 5000ms (5 seconds). If you are hitting timeouts on models with millions of rows, use `scope` to narrow the query:
|
|
811
|
+
|
|
812
|
+
```
|
|
813
|
+
console_count(model: "Order", scope: { status: "pending" })
|
|
814
|
+
```
|
|
815
|
+
|
|
816
|
+
The timeout is set per-transaction in `SafeContext` and is not currently configurable via `Woods.configure`. To change it, pass `timeout_ms:` to `SafeContext.new` directly if you are constructing the server programmatically.
|
|
817
|
+
|
|
818
|
+
## Database dialect validation
|
|
819
|
+
|
|
820
|
+
Blocked-table matching normalizes qualified identifiers even when whitespace or
|
|
821
|
+
comments surround the schema separator. SQL validation uses the connected adapter's
|
|
822
|
+
quote and comment rules. MySQL also reads the executing session's `ANSI_QUOTES`
|
|
823
|
+
and `NO_BACKSLASH_ESCAPES` settings for validation, protected-column scanning, and
|
|
824
|
+
table gating; adjacent subtraction operators are not assumed to begin a comment.
|
|
825
|
+
Direct scanner callers without session settings use conservative quote-mode scans.
|
|
826
|
+
The contributor live-backend lane exercises these boundaries
|
|
827
|
+
through Console requests against PostgreSQL and MySQL. Keep read tools disabled
|
|
828
|
+
unless live SQL access is needed, and retain the configured blocked-table and
|
|
829
|
+
redaction policies when diagnosing a rejected request.
|