rails-mcp-server 1.6.0 → 1.6.1
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 +15 -0
- data/README.md +13 -8
- data/docs/AGENT.md +2 -2
- data/docs/COPILOT_AGENT.md +2 -2
- data/lib/rails-mcp-server/tools/execute_ruby.rb +67 -6
- data/lib/rails-mcp-server/version.rb +1 -1
- 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: 950673f40d56ddea684d938e4e55c2e8db18c3ca44e94988f63565a9db950869
|
|
4
|
+
data.tar.gz: 00712156bdfdfbfc1ec33bd08e31ccb0ad4e7427e3e1e31282de62b4f7b5cc1b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 98ebb4452eaa610b1430c652c94a339022f10bc05ba7a00b867e8b8c9aaa04d033ea149d923bfe131be52455b044b0feb4d90af24a55670708610e2a57b198a1
|
|
7
|
+
data.tar.gz: 8ecf71ec6b09ed73b35e6f9ea64ec88bfcbbf1f91e2bbb8f033216d0a19d0c6af1493c23fb67be14f99e7933cc6dcc7d64f09d173d20c8c2d6a53f4d25ef88ef
|
data/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.6.1] - 2026-08-04
|
|
11
|
+
|
|
12
|
+
### Security
|
|
13
|
+
|
|
14
|
+
- **`execute_ruby` process-execution hardening**: Closed a command-execution path and tightened the static filter.
|
|
15
|
+
- **`require` restricted to a tiny data-lib allowlist**: `require_relative` and dynamic `require`s are refused, and literal `require "lib"` is refused except for a small allowlist of pure-data libraries not always preloaded (`csv`, `tzinfo`, `date`, `time`). Under `bin/rails runner` the app's models, ActiveRecord, and the stdlib Rails loads on boot are already available, so inspection code needs almost no requires — and every dangerous stdlib escape has to be required first. This closes `require "pty"` (`PTY.spawn`/`PTY.getpty` started a child process outside the `Kernel#system` guard, giving arbitrary host command execution), along with `open3`, `fiddle`, `ffi`, and `socket`, at the source rather than by enumerating individual APIs. (`tzinfo` pairs with the existing system-timezone read-path allowlist so `Time.zone` code keeps working.)
|
|
16
|
+
- **Native/PTY patterns**: `PTY`, `Fiddle`, and `FFI` are added to the forbidden-pattern scan as defense in depth.
|
|
17
|
+
- **Dynamic dispatch to execution sinks hard-blocked**: `send`/`public_send`/`__send__`/`const_get` aimed by name at an execution or eval sink (`system`, `exec`, `spawn`, `fork`, `eval`, `popen`, `Open3`, `Process`, `PTY`, …) are now rejected outright instead of merely gated behind `confirm_risky`. Benign dynamic dispatch (e.g. `record.send(:name)`) is unaffected.
|
|
18
|
+
- **Honest framing**: the tool description and docs no longer call `execute_ruby` a read-only sandbox. It runs caller-supplied Ruby with the privileges of the server process; the controls are best-effort guardrails, not an isolation boundary.
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- **ReDoS in the `execute_ruby` static scan**: Rewrote the `require`/dynamic-dispatch matchers to remove an ambiguous `\s*\(?\s*` construct that backtracked in polynomial time on adversarial whitespace input (CodeQL alert). Matching is now linear.
|
|
23
|
+
|
|
10
24
|
## [1.6.0] - 2026-08-03
|
|
11
25
|
|
|
12
26
|
### Added
|
|
@@ -355,6 +369,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
355
369
|
|
|
356
370
|
## Version History Summary
|
|
357
371
|
|
|
372
|
+
- **v1.6.1** (2026-08-04): `execute_ruby` process-execution hardening (blocks the `require "pty"` → `PTY.spawn` command-execution path, restricts `require` to a data-lib allowlist, hard-blocks dynamic dispatch to execution sinks) and a ReDoS fix in the static scan
|
|
358
373
|
- **v1.6.0** (2026-08-03): Sandbox hardening for `execute_ruby`, version-manager Ruby resolution, namespaced model resolution, dependency + security updates (drops Ruby 3.2)
|
|
359
374
|
- **v1.5.1** (2026-03-04): Relaxed dependency version constraints for better compatibility
|
|
360
375
|
- **v1.4.0** (2025-12-10): Context-efficient architecture with progressive tool discovery (67% token reduction)
|
data/README.md
CHANGED
|
@@ -17,7 +17,7 @@ This Rails MCP Server implements the MCP specification to give AI models access
|
|
|
17
17
|
- Get database schema information
|
|
18
18
|
- Analyze controller-view relationships
|
|
19
19
|
- Analyze environment configurations
|
|
20
|
-
- Execute
|
|
20
|
+
- Execute Ruby code in the project context for custom queries
|
|
21
21
|
- Access comprehensive Rails, Turbo, Stimulus, and Kamal documentation
|
|
22
22
|
- Context-efficient architecture with progressive tool discovery
|
|
23
23
|
- Seamless integration with LLM clients
|
|
@@ -360,7 +360,7 @@ The server uses a progressive tool discovery architecture to minimize context us
|
|
|
360
360
|
- **`switch_project`** - Select the active Rails project
|
|
361
361
|
- **`search_tools`** - Discover available tools by category or keyword
|
|
362
362
|
- **`execute_tool`** - Invoke internal analyzers with parameters
|
|
363
|
-
- **`execute_ruby`** - Run
|
|
363
|
+
- **`execute_ruby`** - Run Ruby code in the project context for custom queries
|
|
364
364
|
|
|
365
365
|
This design reduces initial context from ~2,400 tokens to ~800 tokens while maintaining full functionality.
|
|
366
366
|
|
|
@@ -412,7 +412,7 @@ After switching, you'll see a Quick Start guide with common commands.
|
|
|
412
412
|
|
|
413
413
|
#### 4. `execute_ruby`
|
|
414
414
|
|
|
415
|
-
**Description:** Execute
|
|
415
|
+
**Description:** Execute Ruby code in the Rails project context, for inspection and exploration. Runs with the privileges of the server process — see the Security note below; this is not a sandbox for untrusted code.
|
|
416
416
|
|
|
417
417
|
**Parameters:**
|
|
418
418
|
|
|
@@ -429,16 +429,21 @@ After switching, you'll see a Quick Start guide with common commands.
|
|
|
429
429
|
|
|
430
430
|
**Note:** Use `puts` to see output from your code.
|
|
431
431
|
|
|
432
|
-
**Security:**
|
|
432
|
+
**Security:** `execute_ruby` runs Ruby that you — or your coding agent — supply, inside your Rails application, with the privileges of the process that started the server. **It is not a security sandbox for untrusted code.** The guardrails below reduce accidental damage and block the obvious escapes, but real Ruby is expressive enough that a determined caller can work around a pattern-based filter; treat the controls as defense-in-depth, not an isolation boundary.
|
|
433
433
|
|
|
434
|
-
|
|
434
|
+
Because you start the server yourself — normally locally, against your own project — the realistic risk is *running code you didn't intend to*, for example when a coding agent is steered by prompt injection into calling `execute_ruby` with a hostile payload. That payload would run as you. So: only enable this tool for projects and clients you trust, and actually review code before approving a `confirm_risky` re-run.
|
|
435
|
+
|
|
436
|
+
Guardrails applied:
|
|
437
|
+
|
|
438
|
+
- **No writes / shell / network:** file writes, `system`/`exec`/backticks/`spawn`, and network libraries are blocked by both static analysis and runtime overrides.
|
|
439
|
+
- **Almost no `require`:** `require_relative` and dynamic `require`s are refused, and `require "lib"` is refused for everything except a tiny allowlist of pure-data libraries (`csv` and the timezone libs) that Rails doesn't always preload. Under `bin/rails runner` the app's models, ActiveRecord, and the stdlib Rails loads on boot (`json`, `yaml`, `set`, `date`, …) are already available, so inspection code needs no requires anyway — and every dangerous stdlib escape (`pty`, `open3`, `fiddle`, `ffi`, `socket`) has to be required first, so refusing them removes that whole class of bypass at the source.
|
|
435
440
|
- **Confined file reads:** reads are limited to the project directory (via all of `File`/`IO` `read`/`readlines`/`binread`/`foreach` and `File.open`), plus a small allowlist of read-only system timezone paths (e.g. `/usr/share/zoneinfo`) that Rails needs when code touches `Time.zone`. Paths are symlink-resolved (`realpath`) so a link inside the project cannot point outside it.
|
|
436
441
|
- **No sensitive files:** `.env`, credentials, keys, and any `.gitignore`d path are refused.
|
|
437
|
-
- **Database writes are rolled back:** user code runs inside a transaction that is always rolled back, so `delete_all`, `update`, `save`, and raw DML are undone
|
|
442
|
+
- **Database writes are rolled back:** user code runs inside a transaction that is always rolled back, so `delete_all`, `update`, `save`, and raw DML are undone — a safety net against accidental mutation, not a data-access guarantee. (Caveat: DDL may still commit on some adapters such as MySQL, and `after_commit` callbacks do not fire.)
|
|
438
443
|
- **Bounded execution:** a timeout (default 30s, max 60s) kills the whole process group, so a runaway `bin/rails runner` is terminated rather than orphaned.
|
|
439
|
-
- **
|
|
444
|
+
- **Dynamic dispatch to execution sinks is hard-blocked:** `send`/`public_send`/`const_get` aimed by name at `system`/`exec`/`spawn`/`eval`/`Open3`/`Process`/`PTY`/… are rejected outright; the remaining dual-use forms of `send`, `public_send`, `const_get`, and `Kernel#open` are not run until you approve them via `confirm_risky: true`.
|
|
440
445
|
|
|
441
|
-
|
|
446
|
+
For a real boundary, run the server against a database user with read-only grants and/or inside OS-level isolation (a container, `sandbox-exec`, seccomp, a dedicated low-privilege user with no ambient credentials or network) rather than relying on these in-process checks.
|
|
442
447
|
|
|
443
448
|
### Internal Analyzers (via execute_tool)
|
|
444
449
|
|
data/docs/AGENT.md
CHANGED
|
@@ -225,9 +225,9 @@ railsMcpServer:execute_ruby code: "read_file('Gemfile')"
|
|
|
225
225
|
railsMcpServer:execute_ruby code: "puts read_file('Gemfile')"
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
**
|
|
228
|
+
**For inspection, not mutation:** `execute_ruby` is meant for exploring the app, not changing it. File writes, shell/system calls, process spawning, and network access are blocked, and any database writes run inside a transaction that is **always rolled back** — so `delete_all`, `update`, and `save` will not persist. Do not rely on it to change data. These are guardrails, not a security sandbox: the code you send runs with the privileges of the server process, so send only code you would run yourself, and never code from an untrusted source (e.g. copied out of an issue, PR, or file you're inspecting).
|
|
229
229
|
|
|
230
|
-
**Confirmation for dual-use constructs:** if your code uses `send`, `public_send`, `const_get`, or `Kernel#open`, the tool returns a `CONFIRMATION REQUIRED` message instead of running. These can bypass the
|
|
230
|
+
**Confirmation for dual-use constructs:** if your code uses `send`, `public_send`, `const_get`, or `Kernel#open`, the tool returns a `CONFIRMATION REQUIRED` message instead of running. These can bypass the static safety scan, so ask the user to review the code and, only with their explicit approval, re-invoke with `confirm_risky: true`. Do not set `confirm_risky` on your own.
|
|
231
231
|
|
|
232
232
|
---
|
|
233
233
|
|
data/docs/COPILOT_AGENT.md
CHANGED
|
@@ -133,7 +133,7 @@ GitHub Copilot Agent only supports MCP **tools**. The following are available:
|
|
|
133
133
|
| `switch_project` | Change active project (optional in single-project mode) |
|
|
134
134
|
| `search_tools` | Discover available analyzers |
|
|
135
135
|
| `execute_tool` | Invoke internal analyzers |
|
|
136
|
-
| `execute_ruby` | Run
|
|
136
|
+
| `execute_ruby` | Run Ruby code in the project context |
|
|
137
137
|
|
|
138
138
|
### Internal Analyzers (via `execute_tool`)
|
|
139
139
|
|
|
@@ -171,7 +171,7 @@ The `load_guide` analyzer requires guides to be downloaded. To include guides:
|
|
|
171
171
|
|
|
172
172
|
### Network Restrictions
|
|
173
173
|
|
|
174
|
-
GitHub Copilot Agent runs in a sandboxed environment with firewall restrictions. The MCP server
|
|
174
|
+
GitHub Copilot Agent runs in a sandboxed environment with firewall restrictions. The MCP server is used here to inspect the repository and runs with the permissions of that agent environment; note that `execute_ruby` executes real Ruby with those permissions and is not itself an isolation boundary (see the [security notes](../README.md#4-execute_ruby)).
|
|
175
175
|
|
|
176
176
|
## Troubleshooting
|
|
177
177
|
|
|
@@ -3,17 +3,27 @@ module RailsMcpServer
|
|
|
3
3
|
tool_name "execute_ruby"
|
|
4
4
|
|
|
5
5
|
description <<~DESC
|
|
6
|
-
Execute
|
|
6
|
+
Execute Ruby code in the context of the Rails project, for inspection and
|
|
7
|
+
exploration. Use this for:
|
|
7
8
|
- Complex queries that would require multiple tool calls
|
|
8
9
|
- Filtering/transforming data before returning
|
|
9
10
|
- Custom exploration of the codebase
|
|
10
11
|
|
|
12
|
+
This runs with the privileges of the rails-mcp-server process. The
|
|
13
|
+
restrictions below are best-effort guardrails against accidental writes
|
|
14
|
+
and obvious escapes, not a security boundary for untrusted code; only run
|
|
15
|
+
code you would run yourself.
|
|
16
|
+
|
|
11
17
|
RESTRICTIONS:
|
|
12
18
|
- Cannot create, modify, or delete files
|
|
13
19
|
- Cannot read .env, credentials, key files, or .gitignore'd files
|
|
14
20
|
- Cannot access files outside the project directory (read-only system data
|
|
15
21
|
such as timezone files under /usr/share/zoneinfo is allowed)
|
|
16
22
|
- Cannot execute shell commands or system calls
|
|
23
|
+
- Cannot `require` arbitrary libraries or `require_relative` project files.
|
|
24
|
+
Rails and the stdlib it loads (json, yaml, set, ...) are already
|
|
25
|
+
available under `bin/rails runner`; only a few pure-data libraries not
|
|
26
|
+
always preloaded (csv, and the timezone libs) may be required
|
|
17
27
|
- Database writes run inside a transaction that is always rolled back, so
|
|
18
28
|
treat this as read-only for data too (note: DDL may still commit on some
|
|
19
29
|
adapters, and after_commit callbacks do not fire)
|
|
@@ -33,7 +43,7 @@ module RailsMcpServer
|
|
|
33
43
|
DESC
|
|
34
44
|
|
|
35
45
|
arguments do
|
|
36
|
-
required(:code).filled(:string).description("Ruby code to execute (
|
|
46
|
+
required(:code).filled(:string).description("Ruby code to execute (inspection operations only)")
|
|
37
47
|
optional(:timeout).filled(:integer).description("Timeout in seconds. Default: 30, Max: 60")
|
|
38
48
|
optional(:confirm_risky).filled(:bool).description("Set true ONLY after the user has explicitly approved running code that uses sandbox-bypass-capable constructs (send, public_send, const_get, Kernel#open). When false/absent, such code is not executed; the tool returns a CONFIRMATION REQUIRED message instead.")
|
|
39
49
|
end
|
|
@@ -61,6 +71,22 @@ module RailsMcpServer
|
|
|
61
71
|
/Process\.(spawn|exec|fork)/i,
|
|
62
72
|
/Shellwords/i,
|
|
63
73
|
|
|
74
|
+
# Pseudo-terminals and native/syscall bridges. PTY.spawn / PTY.getpty
|
|
75
|
+
# start a child process outside the Kernel#system guard; Fiddle and FFI
|
|
76
|
+
# can call libc (e.g. system(3), execve(2)) directly. None of these are
|
|
77
|
+
# needed for read-only inspection.
|
|
78
|
+
/\bPTY\b/,
|
|
79
|
+
/\bFiddle\b/,
|
|
80
|
+
/\bFFI\b/,
|
|
81
|
+
|
|
82
|
+
# Dynamic dispatch aimed at an execution/eval sink *by name* is hard
|
|
83
|
+
# blocked. The general send/public_send/const_get forms stay in the
|
|
84
|
+
# confirmation tier below; only a dangerous literal target is rejected
|
|
85
|
+
# outright, so `record.send(:name)` still works while
|
|
86
|
+
# `Process.send(:spawn, ...)` or `const_get("Open3")` do not.
|
|
87
|
+
/\b(?:public_send|__send__|send)\s*(?:\(\s*)?[:'"](?:system|exec|spawn|fork|eval|popen|syscall|`)/i,
|
|
88
|
+
/\bconst_get\s*(?:\(\s*)?['"](?:Open3|Process|PTY|Kernel|Socket|Fiddle|FFI|Binding|ObjectSpace|TCPSocket|UDPSocket)\b/i,
|
|
89
|
+
|
|
64
90
|
# Network access
|
|
65
91
|
/Net::(HTTP|FTP|SMTP)/i,
|
|
66
92
|
/URI\.(open|parse)/i,
|
|
@@ -91,11 +117,31 @@ module RailsMcpServer
|
|
|
91
117
|
/Rails\.application\.credentials/i,
|
|
92
118
|
/Rails\.application\.secrets/i,
|
|
93
119
|
|
|
94
|
-
# Load/require
|
|
95
|
-
|
|
96
|
-
|
|
120
|
+
# Load/require. Under `bin/rails runner` Rails, the app's models/gems, and
|
|
121
|
+
# the stdlib Rails loads on boot are already available, so inspection code
|
|
122
|
+
# almost never needs `require`. Dynamic requires and require_relative
|
|
123
|
+
# (loads/executes arbitrary project files) are refused outright; literal
|
|
124
|
+
# `require "lib"` is refused unless the lib is on REQUIRE_ALLOWLIST. This
|
|
125
|
+
# keeps dangerous stdlib escapes (`pty`, `open3`, `fiddle`, `ffi`,
|
|
126
|
+
# `socket`) out while still allowing the few pure-data libs that aren't
|
|
127
|
+
# always preloaded (e.g. csv, the timezone libs).
|
|
128
|
+
/\brequire_relative\b/i,
|
|
129
|
+
/require\s+[^'"]/i,
|
|
130
|
+
/load\s*[(\s]+[^)]*\$/i
|
|
97
131
|
].freeze
|
|
98
132
|
|
|
133
|
+
# The only libraries a literal `require` may name. All are pure-Ruby, with
|
|
134
|
+
# no process/network/native-call surface: csv (not always preloaded) and
|
|
135
|
+
# the timezone libs Rails uses when code touches Time.zone. Everything else
|
|
136
|
+
# — notably any process/native bridge — is rejected. Matched
|
|
137
|
+
# case-insensitively; a trailing ".rb" is ignored.
|
|
138
|
+
REQUIRE_ALLOWLIST = %w[csv tzinfo date time].freeze
|
|
139
|
+
|
|
140
|
+
# Extracts a literal require target from `require "x"`, `require'x'`, or
|
|
141
|
+
# `require("x")`. Dynamic (non-literal) requires are already rejected by the
|
|
142
|
+
# /require\s+[^'"]/ pattern above.
|
|
143
|
+
REQUIRE_STATEMENT = /\brequire\b\s*(?:\(\s*)?(['"])([^'"]+)\1/
|
|
144
|
+
|
|
99
145
|
# Dual-use constructs that are NOT hard-blocked (they have legitimate
|
|
100
146
|
# read-only uses) but can defeat the static safety scan, so running them
|
|
101
147
|
# requires explicit user confirmation via confirm_risky: true.
|
|
@@ -182,7 +228,22 @@ module RailsMcpServer
|
|
|
182
228
|
FORBIDDEN_PATTERNS.each do |pattern|
|
|
183
229
|
if code.match?(pattern)
|
|
184
230
|
return "REJECTED: Code contains forbidden pattern (#{pattern.source.split("\\").first}...). " \
|
|
185
|
-
"This tool only allows
|
|
231
|
+
"This tool only allows a restricted set of inspection operations."
|
|
232
|
+
end
|
|
233
|
+
end
|
|
234
|
+
|
|
235
|
+
validate_requires(code)
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# Rejects any literal `require` of a library outside REQUIRE_ALLOWLIST.
|
|
239
|
+
# (require_relative and dynamic requires are already rejected by the
|
|
240
|
+
# forbidden patterns.) Returns an error string, or nil when permitted.
|
|
241
|
+
def validate_requires(code)
|
|
242
|
+
code.scan(REQUIRE_STATEMENT).each do |_quote, lib|
|
|
243
|
+
normalized = lib.downcase.sub(/\.rb\z/, "")
|
|
244
|
+
unless REQUIRE_ALLOWLIST.include?(normalized)
|
|
245
|
+
return "REJECTED: require of '#{lib}' is not permitted. " \
|
|
246
|
+
"Only these libraries may be required: #{REQUIRE_ALLOWLIST.join(", ")}."
|
|
186
247
|
end
|
|
187
248
|
end
|
|
188
249
|
nil
|