resque-mcp 0.4.0 → 0.5.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 +7 -0
- data/app/controllers/resque/mcp/endpoint_controller.rb +65 -7
- data/lib/resque/mcp/server_factory.rb +4 -0
- data/lib/resque/mcp/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa2476b4d89a552651fd9fefbde7ebb001f75fbf684fa5182388c39cd501bcc5
|
|
4
|
+
data.tar.gz: bf79be53a377e49347c692bb78d2f2519e03756b63d07311bf473601414305ec
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 75df4bb783023d55034a99235970952ea70f931fea145b3cebd48efa7404654c982d65a604ec29ee52086c3b3bbffae9f360eca7acc44e594a2d44a368bf9c40
|
|
7
|
+
data.tar.gz: a182d35074d8e72bc0f638cff1793405b37427093750810f0061f750c57f0adce34974d1e970cfcb1e4fd58f4f6663d659880bdd80f486e4fc42ecf07ae3a679
|
data/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
|
|
3
3
|
## [Unreleased]
|
|
4
4
|
|
|
5
|
+
## [0.5.0]
|
|
6
|
+
|
|
7
|
+
- MCP SDK 1.x support: the `mcp` dependency widens to `>= 0.23, < 2`, admitting the SDK's API-stable 1.x line. No configuration changes; existing deployments behave as before.
|
|
8
|
+
- Sessionless 2026-07-28 lifecycle (SEP-2575, needs `mcp >= 1.2`): clients may skip the `initialize` handshake and carry the protocol version per request, and discover the server via `server/discover`. Older `mcp` versions keep serving the handshake lifecycle unchanged.
|
|
9
|
+
- Capabilities are now declared explicitly instead of taking the SDK's default, dropping the `listChanged`/`subscribe` notification flags a per-request stateless endpoint cannot push. Empty `prompts`/`resources` capabilities stay declared, so `prompts/list` and `resources/list` keep answering with empty lists.
|
|
10
|
+
- `subscriptions/listen` is declined with a correlatable JSON-RPC `-32601` error instead of an opaque `500`: a per-request stateless server has no notification stream to open. The refusal ships with HTTP 200 so clients that only parse successful responses still see it.
|
|
11
|
+
|
|
5
12
|
## [0.4.0]
|
|
6
13
|
|
|
7
14
|
- 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.
|
|
@@ -6,6 +6,8 @@ module Resque
|
|
|
6
6
|
before_action :require_auth_token, only: :handle
|
|
7
7
|
|
|
8
8
|
def handle
|
|
9
|
+
# Rack 3 input may not be rewindable: read before the transport does.
|
|
10
|
+
@raw_jsonrpc_body = request.raw_post
|
|
9
11
|
server = ServerFactory.build(environment: Rails.env.to_s)
|
|
10
12
|
config = Resque::Mcp.config
|
|
11
13
|
# Passthrough first; our security-critical keys override, so nothing
|
|
@@ -20,21 +22,77 @@ module Resque
|
|
|
20
22
|
transport = ::MCP::Server::Transports::StreamableHTTPTransport.new(server, **options)
|
|
21
23
|
|
|
22
24
|
status, headers, body = transport.handle_request(request)
|
|
23
|
-
|
|
25
|
+
render_transport_response(status, headers, body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def method_not_allowed
|
|
29
|
+
head :method_not_allowed
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Rack 3 forbids these on a response; the SSE headers carry one.
|
|
35
|
+
HOP_BY_HOP_HEADERS = %w[
|
|
36
|
+
connection keep-alive proxy-authenticate proxy-authorization
|
|
37
|
+
te trailer transfer-encoding upgrade
|
|
38
|
+
].freeze
|
|
39
|
+
|
|
40
|
+
# A callable body is a stream the transport wants to keep open (today
|
|
41
|
+
# only `subscriptions/listen`); stateless has nothing to push, so decline.
|
|
42
|
+
def render_transport_response(status, headers, body)
|
|
43
|
+
if body.respond_to?(:call)
|
|
44
|
+
# 200, not 501: clients that reject on `!response.ok` never parse the
|
|
45
|
+
# body, and the transport headers describe the declined stream.
|
|
46
|
+
return render json: {
|
|
47
|
+
jsonrpc: "2.0",
|
|
48
|
+
id: jsonrpc_request_id,
|
|
49
|
+
error: {
|
|
50
|
+
code: -32601,
|
|
51
|
+
message: "Method not found: #{jsonrpc_method_label} is not supported by this stateless endpoint"
|
|
52
|
+
}
|
|
53
|
+
}, status: :ok
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
headers.each do |key, value|
|
|
57
|
+
next if HOP_BY_HOP_HEADERS.include?(key.to_s.downcase)
|
|
58
|
+
response.set_header(key, value)
|
|
59
|
+
end
|
|
24
60
|
|
|
25
61
|
payload = body.first
|
|
26
|
-
|
|
27
|
-
|
|
62
|
+
return head status unless payload
|
|
63
|
+
|
|
64
|
+
content_type = transport_content_type(headers)
|
|
65
|
+
if content_type && !content_type.start_with?("application/json")
|
|
66
|
+
render body: payload, content_type: content_type, status: status
|
|
28
67
|
else
|
|
29
|
-
|
|
68
|
+
render json: payload, status: status
|
|
30
69
|
end
|
|
31
70
|
end
|
|
32
71
|
|
|
33
|
-
|
|
34
|
-
|
|
72
|
+
# A null id leaves a strict client unable to correlate the error.
|
|
73
|
+
def jsonrpc_payload
|
|
74
|
+
@jsonrpc_payload ||= begin
|
|
75
|
+
parsed = JSON.parse(@raw_jsonrpc_body.to_s)
|
|
76
|
+
parsed.is_a?(Hash) ? parsed : {}
|
|
77
|
+
rescue JSON::ParserError
|
|
78
|
+
{}
|
|
79
|
+
end
|
|
35
80
|
end
|
|
36
81
|
|
|
37
|
-
|
|
82
|
+
def jsonrpc_request_id
|
|
83
|
+
id = jsonrpc_payload["id"]
|
|
84
|
+
id if id.is_a?(String) || id.is_a?(Integer)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def jsonrpc_method_label
|
|
88
|
+
method_name = jsonrpc_payload["method"]
|
|
89
|
+
method_name.is_a?(String) ? method_name : "this method"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def transport_content_type(headers)
|
|
93
|
+
_, value = headers.find { |key, _| key.to_s.casecmp("content-type").zero? }
|
|
94
|
+
value
|
|
95
|
+
end
|
|
38
96
|
|
|
39
97
|
# No reliable boot-time hook exists (initializer ordering), so a
|
|
40
98
|
# missing token is caught per request: 503, never silently open.
|
|
@@ -10,6 +10,10 @@ module Resque
|
|
|
10
10
|
name: "resque-mcp",
|
|
11
11
|
version: Resque::Mcp::VERSION,
|
|
12
12
|
tools: [Tools::Overview, Tools::QueueStats, Tools::WorkerStats, Tools::ListFailures, Tools::GetFailure],
|
|
13
|
+
# Explicit, or the SDK advertises `listChanged`/`subscribe` streams
|
|
14
|
+
# we decline. This hash replaces the defaults and the SDK refuses
|
|
15
|
+
# methods whose key is absent — hence the empty prompts/resources.
|
|
16
|
+
capabilities: {tools: {}, prompts: {}, resources: {}, logging: {}},
|
|
13
17
|
server_context: {adapter: Adapter.new, environment: environment}
|
|
14
18
|
)
|
|
15
19
|
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.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Josch Bockler
|
|
@@ -38,7 +38,7 @@ dependencies:
|
|
|
38
38
|
version: '0.23'
|
|
39
39
|
- - "<"
|
|
40
40
|
- !ruby/object:Gem::Version
|
|
41
|
-
version: '
|
|
41
|
+
version: '2'
|
|
42
42
|
type: :runtime
|
|
43
43
|
prerelease: false
|
|
44
44
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -48,7 +48,7 @@ dependencies:
|
|
|
48
48
|
version: '0.23'
|
|
49
49
|
- - "<"
|
|
50
50
|
- !ruby/object:Gem::Version
|
|
51
|
-
version: '
|
|
51
|
+
version: '2'
|
|
52
52
|
- !ruby/object:Gem::Dependency
|
|
53
53
|
name: activesupport
|
|
54
54
|
requirement: !ruby/object:Gem::Requirement
|