resque-mcp 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +6 -0
- data/README.md +17 -1
- data/app/controllers/resque/mcp/endpoint_controller.rb +10 -2
- data/lib/resque/mcp/configuration.rb +37 -0
- data/lib/resque/mcp/engine.rb +7 -0
- data/lib/resque/mcp/version.rb +1 -1
- metadata +11 -5
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 0415f5a9bbc7dddcc1c4def5de45835d49df86158b5261567af19ece94d6d36c
|
|
4
|
+
data.tar.gz: 6f4f8511f80a3107abd83e7e24053d0322dabb5feec212e52271bfe7bd844684
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 95a256a4a7e92257568a474254fbd052306f0e915cebad24bfc6ff237a4a678ead0ddfae17611b0f6ab0f4b2f9a195ace9cc4eda54cd0b2d5122afc0827ebdb3
|
|
7
|
+
data.tar.gz: 2494d7df431b2b14f5a27eabc148f6a19670246afbb7051ad0144c2637773c52acaeb5d022b86e0e47d87cf691d044f6e6e1574c8640dd0389f8356189a494cf
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.4.0]
|
|
6
|
+
|
|
7
|
+
- Security: bump the minimum `mcp` dependency to `>= 0.23, < 1` for the DNS-rebinding fix (CVE-2026-63118); `<= 0.22` did not validate `Host`/`Origin` headers on the Streamable HTTP transport. Protection is now on by default, with loopback always allowed.
|
|
8
|
+
- Config: `allowed_hosts` and `allowed_origins` for the DNS-rebinding allowlists. `allowed_hosts` defaults to inheriting the host's Rails `config.hosts` string entries, so apps that already configure Rails host authorization need nothing extra; an explicit list replaces it, and `[]` allows loopback only.
|
|
9
|
+
- Config: `mcp_transport_options` forwards arbitrary settings to the mcp gem's transport (e.g. `max_request_bytes`); the DNS-rebinding keys always override it, so it can tune but never weaken protection.
|
|
10
|
+
|
|
5
11
|
## [0.3.0]
|
|
6
12
|
|
|
7
13
|
- `worker_stats` tool: list registered workers (state, subscribed queues, per-worker processed/failed counts, start time, current job with filtered args preview) with a `state` filter (`working`/`idle`/`all`), global counts including `heartbeat_expired`, and the standard pagination envelope. Workers whose heartbeat is older than `Resque.prune_interval` are flagged as likely dead.
|
data/README.md
CHANGED
|
@@ -34,6 +34,22 @@ Resque::Mcp.configure do |c|
|
|
|
34
34
|
# token could be created by e.g.: `bin/rails runner 'puts SecureRandom.base58(32)'`
|
|
35
35
|
c.auth_token = Rails.application.credentials.dig(:resque_mcp, :token)
|
|
36
36
|
|
|
37
|
+
# DNS-rebinding protection (CVE-2026-63118) rejects any Host outside the
|
|
38
|
+
# allowlist with 403; loopback (127.0.0.1/::1/localhost) is always allowed.
|
|
39
|
+
# By default the allowlist is inherited from your Rails config.hosts, so if
|
|
40
|
+
# that already lists your domain(s) you need nothing here. Only plain
|
|
41
|
+
# hostname strings are honored (here and when inherited) — regexps, IPAddrs,
|
|
42
|
+
# and ".sub.domain" wildcards are dropped, so give concrete hostnames:
|
|
43
|
+
# c.allowed_hosts = ["resque.example.com"]
|
|
44
|
+
|
|
45
|
+
# Optional: extra permitted Origin values beyond same-origin.
|
|
46
|
+
# c.allowed_origins = ["https://resque.example.com"]
|
|
47
|
+
|
|
48
|
+
# Optional: escape hatch for mcp settings this gem doesn't expose directly,
|
|
49
|
+
# forwarded to the transport (the DNS-rebinding keys above always take
|
|
50
|
+
# precedence and can't be weakened here):
|
|
51
|
+
# c.mcp_transport_options = { max_request_bytes: 8 * 1024 * 1024 }
|
|
52
|
+
|
|
37
53
|
# Optional: which job-args keys to mask as [FILTERED] in tool responses.
|
|
38
54
|
# Defaults to your app's config.filter_parameters; an explicit list
|
|
39
55
|
# replaces it (merge yourself if you want both):
|
|
@@ -41,7 +57,7 @@ Resque::Mcp.configure do |c|
|
|
|
41
57
|
end
|
|
42
58
|
```
|
|
43
59
|
|
|
44
|
-
The token is **required** — the endpoint answers `503` until one is configured, and `401` on any request without a matching `Authorization: Bearer` header. The engine talks to whatever `Resque.redis` your app already configured; it never opens its own Redis connection.
|
|
60
|
+
The token is **required** — the endpoint answers `503` until one is configured, and `401` on any request without a matching `Authorization: Bearer` header. The endpoint also validates the `Host`/`Origin` headers against DNS rebinding: the allowed hosts default to your Rails `config.hosts`, so a non-loopback request gets `403` unless its Host is in that list (or in an explicit `allowed_hosts`). The engine talks to whatever `Resque.redis` your app already configured; it never opens its own Redis connection.
|
|
45
61
|
|
|
46
62
|
Job arguments shown by any tool are filtered through `ActiveSupport::ParameterFilter` **before** preview/truncation, using your Rails `filter_parameters` by default — the same keys you hide from your logs are hidden from the model. Filters match hash keys (at any depth, same semantics as Rails log filtering, including anchored dot-notation like `/\Acredit_card\.code\z/`); positional scalar args have no key and pass through. Set `c.filter_parameters = []` to disable.
|
|
47
63
|
|
|
@@ -7,9 +7,17 @@ module Resque
|
|
|
7
7
|
|
|
8
8
|
def handle
|
|
9
9
|
server = ServerFactory.build(environment: Rails.env.to_s)
|
|
10
|
-
|
|
11
|
-
|
|
10
|
+
config = Resque::Mcp.config
|
|
11
|
+
# Passthrough first; our security-critical keys override, so nothing
|
|
12
|
+
# in mcp_transport_options can weaken the DNS-rebinding posture.
|
|
13
|
+
options = config.mcp_transport_options.merge(
|
|
14
|
+
stateless: true,
|
|
15
|
+
enable_json_response: true,
|
|
16
|
+
dns_rebinding_protection: true,
|
|
17
|
+
allowed_hosts: config.allowed_hosts,
|
|
18
|
+
allowed_origins: config.allowed_origins
|
|
12
19
|
)
|
|
20
|
+
transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(server, **options)
|
|
13
21
|
|
|
14
22
|
status, headers, body = transport.handle_request(request)
|
|
15
23
|
headers.each { |key, value| response.set_header(key, value) }
|
|
@@ -4,6 +4,43 @@ module Resque
|
|
|
4
4
|
module Mcp
|
|
5
5
|
class Configuration
|
|
6
6
|
attr_accessor :auth_token
|
|
7
|
+
|
|
8
|
+
# DNS-rebinding protection (mcp >= 0.23, CVE-2026-63118). The transport
|
|
9
|
+
# validates Host/Origin headers; loopback hosts are always allowed.
|
|
10
|
+
#
|
|
11
|
+
# nil (default) inherits the host's Rails config.hosts; an explicit list
|
|
12
|
+
# replaces it; [] means loopback only. Either way, only plain hostname
|
|
13
|
+
# strings are honored — regexps, IPAddrs, and leading-dot subdomain
|
|
14
|
+
# wildcards (".example.com") can't map to the SDK's exact-hostname
|
|
15
|
+
# matching (and a non-string would crash its downcase), so they're
|
|
16
|
+
# dropped from both paths. allowed_origins adds extra permitted Origin
|
|
17
|
+
# values beyond same-origin.
|
|
18
|
+
attr_writer :allowed_hosts, :allowed_origins
|
|
19
|
+
|
|
20
|
+
# Internal seam: a callable returning the host's config.hosts, read
|
|
21
|
+
# lazily (the engine wires it) so post-boot changes are never missed.
|
|
22
|
+
attr_accessor :default_allowed_hosts
|
|
23
|
+
|
|
24
|
+
def allowed_hosts
|
|
25
|
+
list = (defined?(@allowed_hosts) && @allowed_hosts) ? @allowed_hosts : default_allowed_hosts&.call
|
|
26
|
+
Array(list).select { |h| h.is_a?(String) && !h.start_with?(".") }
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def allowed_origins
|
|
30
|
+
@allowed_origins || []
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Extra settings for the mcp gem, forwarded verbatim to its
|
|
34
|
+
# StreamableHTTPTransport (e.g. max_request_bytes:). Tuning only — the
|
|
35
|
+
# controller applies the security-critical keys (allowed_hosts,
|
|
36
|
+
# allowed_origins, stateless, dns_rebinding_protection) *after* these,
|
|
37
|
+
# so a passthrough value can never weaken them.
|
|
38
|
+
attr_writer :mcp_transport_options
|
|
39
|
+
|
|
40
|
+
def mcp_transport_options
|
|
41
|
+
@mcp_transport_options || {}
|
|
42
|
+
end
|
|
43
|
+
|
|
7
44
|
# nil = inherit the host default (the engine wires Rails'
|
|
8
45
|
# filter_parameters as a lazy source); an explicit list replaces it
|
|
9
46
|
# ([] disables filtering).
|
data/lib/resque/mcp/engine.rb
CHANGED
|
@@ -14,6 +14,13 @@ module Resque
|
|
|
14
14
|
initializer "resque_mcp.filter_parameters" do
|
|
15
15
|
Resque::Mcp.config.default_filter_parameters = -> { Rails.application.config.filter_parameters }
|
|
16
16
|
end
|
|
17
|
+
|
|
18
|
+
# allowed_hosts inherits the host's config.hosts (the same list Rails'
|
|
19
|
+
# own host authorization uses) unless explicitly configured, read
|
|
20
|
+
# lazily for the same reason as the filter list above.
|
|
21
|
+
initializer "resque_mcp.allowed_hosts" do
|
|
22
|
+
Resque::Mcp.config.default_allowed_hosts = -> { Rails.application.config.hosts }
|
|
23
|
+
end
|
|
17
24
|
end
|
|
18
25
|
end
|
|
19
26
|
end
|
data/lib/resque/mcp/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: resque-mcp
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josch Bockler
|
|
@@ -33,16 +33,22 @@ dependencies:
|
|
|
33
33
|
name: mcp
|
|
34
34
|
requirement: !ruby/object:Gem::Requirement
|
|
35
35
|
requirements:
|
|
36
|
-
- - "
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '0.23'
|
|
39
|
+
- - "<"
|
|
37
40
|
- !ruby/object:Gem::Version
|
|
38
|
-
version: '
|
|
41
|
+
version: '1'
|
|
39
42
|
type: :runtime
|
|
40
43
|
prerelease: false
|
|
41
44
|
version_requirements: !ruby/object:Gem::Requirement
|
|
42
45
|
requirements:
|
|
43
|
-
- - "
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '0.23'
|
|
49
|
+
- - "<"
|
|
44
50
|
- !ruby/object:Gem::Version
|
|
45
|
-
version: '
|
|
51
|
+
version: '1'
|
|
46
52
|
- !ruby/object:Gem::Dependency
|
|
47
53
|
name: activesupport
|
|
48
54
|
requirement: !ruby/object:Gem::Requirement
|