mcp 0.11.0 → 0.13.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/README.md +997 -885
- data/lib/json_rpc_handler.rb +16 -9
- data/lib/mcp/client/http.rb +4 -1
- data/lib/mcp/configuration.rb +38 -2
- data/lib/mcp/content.rb +16 -12
- data/lib/mcp/instrumentation.rb +23 -2
- data/lib/mcp/methods.rb +3 -2
- data/lib/mcp/prompt/result.rb +4 -3
- data/lib/mcp/resource/contents.rb +8 -7
- data/lib/mcp/resource.rb +4 -2
- data/lib/mcp/resource_template.rb +4 -2
- data/lib/mcp/server/transports/streamable_http_transport.rb +46 -17
- data/lib/mcp/server.rb +27 -45
- data/lib/mcp/server_context.rb +36 -0
- data/lib/mcp/server_session.rb +29 -0
- data/lib/mcp/tool/response.rb +4 -3
- data/lib/mcp/transport.rb +1 -0
- data/lib/mcp/version.rb +1 -1
- metadata +2 -2
data/lib/json_rpc_handler.rb
CHANGED
|
@@ -117,20 +117,27 @@ module JsonRpcHandler
|
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def handle_request_error(error, id, id_validation_pattern)
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
120
|
+
if error.respond_to?(:error_code) && error.error_code
|
|
121
|
+
code = error.error_code
|
|
122
|
+
message = error.message
|
|
123
|
+
else
|
|
124
|
+
error_type = error.respond_to?(:error_type) ? error.error_type : nil
|
|
125
|
+
|
|
126
|
+
code, message = case error_type
|
|
127
|
+
when :invalid_request then [ErrorCode::INVALID_REQUEST, "Invalid Request"]
|
|
128
|
+
when :invalid_params then [ErrorCode::INVALID_PARAMS, "Invalid params"]
|
|
129
|
+
when :parse_error then [ErrorCode::PARSE_ERROR, "Parse error"]
|
|
130
|
+
when :internal_error then [ErrorCode::INTERNAL_ERROR, "Internal error"]
|
|
131
|
+
else [ErrorCode::INTERNAL_ERROR, "Internal error"]
|
|
132
|
+
end
|
|
128
133
|
end
|
|
129
134
|
|
|
135
|
+
data = error.respond_to?(:error_data) && error.error_data ? error.error_data : error.message
|
|
136
|
+
|
|
130
137
|
error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
|
|
131
138
|
code: code,
|
|
132
139
|
message: message,
|
|
133
|
-
data:
|
|
140
|
+
data: data,
|
|
134
141
|
})
|
|
135
142
|
end
|
|
136
143
|
|
data/lib/mcp/client/http.rb
CHANGED
|
@@ -7,9 +7,10 @@ module MCP
|
|
|
7
7
|
|
|
8
8
|
attr_reader :url
|
|
9
9
|
|
|
10
|
-
def initialize(url:, headers: {})
|
|
10
|
+
def initialize(url:, headers: {}, &block)
|
|
11
11
|
@url = url
|
|
12
12
|
@headers = headers
|
|
13
|
+
@faraday_customizer = block
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def send_request(request:)
|
|
@@ -78,6 +79,8 @@ module MCP
|
|
|
78
79
|
headers.each do |key, value|
|
|
79
80
|
faraday.headers[key] = value
|
|
80
81
|
end
|
|
82
|
+
|
|
83
|
+
@faraday_customizer&.call(faraday)
|
|
81
84
|
end
|
|
82
85
|
end
|
|
83
86
|
|
data/lib/mcp/configuration.rb
CHANGED
|
@@ -7,11 +7,18 @@ module MCP
|
|
|
7
7
|
LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05",
|
|
8
8
|
]
|
|
9
9
|
|
|
10
|
-
attr_writer :exception_reporter, :
|
|
10
|
+
attr_writer :exception_reporter, :around_request
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# @deprecated Use {#around_request=} instead. `instrumentation_callback`
|
|
13
|
+
# fires only after a request completes and cannot wrap execution in a
|
|
14
|
+
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
|
|
15
|
+
# @see #around_request=
|
|
16
|
+
attr_writer :instrumentation_callback
|
|
17
|
+
|
|
18
|
+
def initialize(exception_reporter: nil, around_request: nil, instrumentation_callback: nil, protocol_version: nil,
|
|
13
19
|
validate_tool_call_arguments: true)
|
|
14
20
|
@exception_reporter = exception_reporter
|
|
21
|
+
@around_request = around_request
|
|
15
22
|
@instrumentation_callback = instrumentation_callback
|
|
16
23
|
@protocol_version = protocol_version
|
|
17
24
|
if protocol_version
|
|
@@ -50,10 +57,24 @@ module MCP
|
|
|
50
57
|
!@exception_reporter.nil?
|
|
51
58
|
end
|
|
52
59
|
|
|
60
|
+
def around_request
|
|
61
|
+
@around_request || default_around_request
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def around_request?
|
|
65
|
+
!@around_request.nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @deprecated Use {#around_request} instead. `instrumentation_callback`
|
|
69
|
+
# fires only after a request completes and cannot wrap execution in a
|
|
70
|
+
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
|
|
71
|
+
# @see #around_request
|
|
53
72
|
def instrumentation_callback
|
|
54
73
|
@instrumentation_callback || default_instrumentation_callback
|
|
55
74
|
end
|
|
56
75
|
|
|
76
|
+
# @deprecated Use {#around_request?} instead.
|
|
77
|
+
# @see #around_request?
|
|
57
78
|
def instrumentation_callback?
|
|
58
79
|
!@instrumentation_callback.nil?
|
|
59
80
|
end
|
|
@@ -72,20 +93,30 @@ module MCP
|
|
|
72
93
|
else
|
|
73
94
|
@exception_reporter
|
|
74
95
|
end
|
|
96
|
+
|
|
97
|
+
around_request = if other.around_request?
|
|
98
|
+
other.around_request
|
|
99
|
+
else
|
|
100
|
+
@around_request
|
|
101
|
+
end
|
|
102
|
+
|
|
75
103
|
instrumentation_callback = if other.instrumentation_callback?
|
|
76
104
|
other.instrumentation_callback
|
|
77
105
|
else
|
|
78
106
|
@instrumentation_callback
|
|
79
107
|
end
|
|
108
|
+
|
|
80
109
|
protocol_version = if other.protocol_version?
|
|
81
110
|
other.protocol_version
|
|
82
111
|
else
|
|
83
112
|
@protocol_version
|
|
84
113
|
end
|
|
114
|
+
|
|
85
115
|
validate_tool_call_arguments = other.validate_tool_call_arguments
|
|
86
116
|
|
|
87
117
|
Configuration.new(
|
|
88
118
|
exception_reporter: exception_reporter,
|
|
119
|
+
around_request: around_request,
|
|
89
120
|
instrumentation_callback: instrumentation_callback,
|
|
90
121
|
protocol_version: protocol_version,
|
|
91
122
|
validate_tool_call_arguments: validate_tool_call_arguments,
|
|
@@ -111,6 +142,11 @@ module MCP
|
|
|
111
142
|
@default_exception_reporter ||= ->(exception, server_context) {}
|
|
112
143
|
end
|
|
113
144
|
|
|
145
|
+
def default_around_request
|
|
146
|
+
@default_around_request ||= ->(_data, &request_handler) { request_handler.call }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @deprecated Use {#default_around_request} instead.
|
|
114
150
|
def default_instrumentation_callback
|
|
115
151
|
@default_instrumentation_callback ||= ->(data) {}
|
|
116
152
|
end
|
data/lib/mcp/content.rb
CHANGED
|
@@ -3,56 +3,60 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
module Content
|
|
5
5
|
class Text
|
|
6
|
-
attr_reader :text, :annotations
|
|
6
|
+
attr_reader :text, :annotations, :meta
|
|
7
7
|
|
|
8
|
-
def initialize(text, annotations: nil)
|
|
8
|
+
def initialize(text, annotations: nil, meta: nil)
|
|
9
9
|
@text = text
|
|
10
10
|
@annotations = annotations
|
|
11
|
+
@meta = meta
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
|
-
{ text: text, annotations: annotations, type: "text" }.compact
|
|
15
|
+
{ text: text, annotations: annotations, _meta: meta, type: "text" }.compact
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
class Image
|
|
19
|
-
attr_reader :data, :mime_type, :annotations
|
|
20
|
+
attr_reader :data, :mime_type, :annotations, :meta
|
|
20
21
|
|
|
21
|
-
def initialize(data, mime_type, annotations: nil)
|
|
22
|
+
def initialize(data, mime_type, annotations: nil, meta: nil)
|
|
22
23
|
@data = data
|
|
23
24
|
@mime_type = mime_type
|
|
24
25
|
@annotations = annotations
|
|
26
|
+
@meta = meta
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def to_h
|
|
28
|
-
{ data: data, mimeType: mime_type, annotations: annotations, type: "image" }.compact
|
|
30
|
+
{ data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "image" }.compact
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
class Audio
|
|
33
|
-
attr_reader :data, :mime_type, :annotations
|
|
35
|
+
attr_reader :data, :mime_type, :annotations, :meta
|
|
34
36
|
|
|
35
|
-
def initialize(data, mime_type, annotations: nil)
|
|
37
|
+
def initialize(data, mime_type, annotations: nil, meta: nil)
|
|
36
38
|
@data = data
|
|
37
39
|
@mime_type = mime_type
|
|
38
40
|
@annotations = annotations
|
|
41
|
+
@meta = meta
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def to_h
|
|
42
|
-
{ data: data, mimeType: mime_type, annotations: annotations, type: "audio" }.compact
|
|
45
|
+
{ data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "audio" }.compact
|
|
43
46
|
end
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
class EmbeddedResource
|
|
47
|
-
attr_reader :resource, :annotations
|
|
50
|
+
attr_reader :resource, :annotations, :meta
|
|
48
51
|
|
|
49
|
-
def initialize(resource, annotations: nil)
|
|
52
|
+
def initialize(resource, annotations: nil, meta: nil)
|
|
50
53
|
@resource = resource
|
|
51
54
|
@annotations = annotations
|
|
55
|
+
@meta = meta
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
def to_h
|
|
55
|
-
{ resource: resource.to_h, annotations: annotations, type: "resource" }.compact
|
|
59
|
+
{ resource: resource.to_h, annotations: annotations, _meta: meta, type: "resource" }.compact
|
|
56
60
|
end
|
|
57
61
|
end
|
|
58
62
|
end
|
data/lib/mcp/instrumentation.rb
CHANGED
|
@@ -2,19 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
module Instrumentation
|
|
5
|
-
def instrument_call(method, &block)
|
|
5
|
+
def instrument_call(method, server_context: {}, exception_already_reported: nil, &block)
|
|
6
6
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
7
7
|
begin
|
|
8
8
|
@instrumentation_data = {}
|
|
9
9
|
add_instrumentation_data(method: method)
|
|
10
10
|
|
|
11
|
-
result =
|
|
11
|
+
result = configuration.around_request.call(@instrumentation_data, &block)
|
|
12
12
|
|
|
13
13
|
result
|
|
14
|
+
rescue => e
|
|
15
|
+
already_reported = begin
|
|
16
|
+
!!exception_already_reported&.call(e)
|
|
17
|
+
# rubocop:disable Lint/RescueException
|
|
18
|
+
rescue Exception
|
|
19
|
+
# rubocop:enable Lint/RescueException
|
|
20
|
+
# The predicate is expected to be side-effect-free and return a boolean.
|
|
21
|
+
# Any exception raised from it (including non-StandardError such as SystemExit)
|
|
22
|
+
# must not shadow the original exception.
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless already_reported
|
|
27
|
+
add_instrumentation_data(error: :internal_error) unless @instrumentation_data.key?(:error)
|
|
28
|
+
configuration.exception_reporter.call(e, server_context)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
raise
|
|
14
32
|
ensure
|
|
15
33
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
16
34
|
add_instrumentation_data(duration: end_time - start_time)
|
|
17
35
|
|
|
36
|
+
# Backward compatibility: `instrumentation_callback` is soft-deprecated
|
|
37
|
+
# in favor of `around_request`, but existing callers still expect it
|
|
38
|
+
# to fire after every request until it is removed in a future version.
|
|
18
39
|
configuration.instrumentation_callback.call(@instrumentation_data)
|
|
19
40
|
end
|
|
20
41
|
end
|
data/lib/mcp/methods.rb
CHANGED
|
@@ -33,6 +33,7 @@ module MCP
|
|
|
33
33
|
NOTIFICATIONS_MESSAGE = "notifications/message"
|
|
34
34
|
NOTIFICATIONS_PROGRESS = "notifications/progress"
|
|
35
35
|
NOTIFICATIONS_CANCELLED = "notifications/cancelled"
|
|
36
|
+
NOTIFICATIONS_ELICITATION_COMPLETE = "notifications/elicitation/complete"
|
|
36
37
|
|
|
37
38
|
class MissingRequiredCapabilityError < StandardError
|
|
38
39
|
attr_reader :method
|
|
@@ -79,8 +80,8 @@ module MCP
|
|
|
79
80
|
require_capability!(method, capabilities, :sampling)
|
|
80
81
|
when ELICITATION_CREATE
|
|
81
82
|
require_capability!(method, capabilities, :elicitation)
|
|
82
|
-
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED
|
|
83
|
-
# No specific capability required
|
|
83
|
+
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED, NOTIFICATIONS_ELICITATION_COMPLETE
|
|
84
|
+
# No specific capability required.
|
|
84
85
|
end
|
|
85
86
|
end
|
|
86
87
|
|
data/lib/mcp/prompt/result.rb
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Prompt
|
|
5
5
|
class Result
|
|
6
|
-
attr_reader :description, :messages
|
|
6
|
+
attr_reader :description, :messages, :meta
|
|
7
7
|
|
|
8
|
-
def initialize(description: nil, messages: [])
|
|
8
|
+
def initialize(description: nil, messages: [], meta: nil)
|
|
9
9
|
@description = description
|
|
10
10
|
@messages = messages
|
|
11
|
+
@meta = meta
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
|
-
{ description: description, messages: messages.map(&:to_h) }.compact
|
|
15
|
+
{ description: description, messages: messages.map(&:to_h), _meta: meta }.compact
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -3,23 +3,24 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Resource
|
|
5
5
|
class Contents
|
|
6
|
-
attr_reader :uri, :mime_type
|
|
6
|
+
attr_reader :uri, :mime_type, :meta
|
|
7
7
|
|
|
8
|
-
def initialize(uri:, mime_type: nil)
|
|
8
|
+
def initialize(uri:, mime_type: nil, meta: nil)
|
|
9
9
|
@uri = uri
|
|
10
10
|
@mime_type = mime_type
|
|
11
|
+
@meta = meta
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
|
-
{ uri: uri, mimeType: mime_type }.compact
|
|
15
|
+
{ uri: uri, mimeType: mime_type, _meta: meta }.compact
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
class TextContents < Contents
|
|
19
20
|
attr_reader :text
|
|
20
21
|
|
|
21
|
-
def initialize(text:, uri:, mime_type:)
|
|
22
|
-
super(uri: uri, mime_type: mime_type)
|
|
22
|
+
def initialize(text:, uri:, mime_type:, meta: nil)
|
|
23
|
+
super(uri: uri, mime_type: mime_type, meta: meta)
|
|
23
24
|
@text = text
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -31,8 +32,8 @@ module MCP
|
|
|
31
32
|
class BlobContents < Contents
|
|
32
33
|
attr_reader :data
|
|
33
34
|
|
|
34
|
-
def initialize(data:, uri:, mime_type:)
|
|
35
|
-
super(uri: uri, mime_type: mime_type)
|
|
35
|
+
def initialize(data:, uri:, mime_type:, meta: nil)
|
|
36
|
+
super(uri: uri, mime_type: mime_type, meta: meta)
|
|
36
37
|
@data = data
|
|
37
38
|
end
|
|
38
39
|
|
data/lib/mcp/resource.rb
CHANGED
|
@@ -5,15 +5,16 @@ require_relative "resource/embedded"
|
|
|
5
5
|
|
|
6
6
|
module MCP
|
|
7
7
|
class Resource
|
|
8
|
-
attr_reader :uri, :name, :title, :description, :icons, :mime_type
|
|
8
|
+
attr_reader :uri, :name, :title, :description, :icons, :mime_type, :meta
|
|
9
9
|
|
|
10
|
-
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil)
|
|
10
|
+
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
11
11
|
@uri = uri
|
|
12
12
|
@name = name
|
|
13
13
|
@title = title
|
|
14
14
|
@description = description
|
|
15
15
|
@icons = icons
|
|
16
16
|
@mime_type = mime_type
|
|
17
|
+
@meta = meta
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def to_h
|
|
@@ -24,6 +25,7 @@ module MCP
|
|
|
24
25
|
description: description,
|
|
25
26
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
26
27
|
mimeType: mime_type,
|
|
28
|
+
_meta: meta,
|
|
27
29
|
}.compact
|
|
28
30
|
end
|
|
29
31
|
end
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class ResourceTemplate
|
|
5
|
-
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type
|
|
5
|
+
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :meta
|
|
6
6
|
|
|
7
|
-
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil)
|
|
7
|
+
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
8
8
|
@uri_template = uri_template
|
|
9
9
|
@name = name
|
|
10
10
|
@title = title
|
|
11
11
|
@description = description
|
|
12
12
|
@icons = icons
|
|
13
13
|
@mime_type = mime_type
|
|
14
|
+
@meta = meta
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def to_h
|
|
@@ -21,6 +22,7 @@ module MCP
|
|
|
21
22
|
description: description,
|
|
22
23
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
23
24
|
mimeType: mime_type,
|
|
25
|
+
_meta: meta,
|
|
24
26
|
}.compact
|
|
25
27
|
end
|
|
26
28
|
end
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "../../transport"
|
|
5
5
|
|
|
6
|
+
# This file is autoloaded only when `StreamableHTTPTransport` is referenced,
|
|
7
|
+
# so the `rack` dependency does not affect `StdioTransport` users.
|
|
8
|
+
begin
|
|
9
|
+
require "rack"
|
|
10
|
+
rescue LoadError
|
|
11
|
+
raise LoadError, "The 'rack' gem is required to use the StreamableHTTPTransport. " \
|
|
12
|
+
"Add it to your Gemfile: gem 'rack'"
|
|
13
|
+
end
|
|
14
|
+
|
|
6
15
|
module MCP
|
|
7
16
|
class Server
|
|
8
17
|
module Transports
|
|
@@ -15,7 +24,7 @@ module MCP
|
|
|
15
24
|
|
|
16
25
|
def initialize(server, stateless: false, session_idle_timeout: nil)
|
|
17
26
|
super(server)
|
|
18
|
-
# Maps `session_id` to `{
|
|
27
|
+
# Maps `session_id` to `{ get_sse_stream: stream_object, server_session: ServerSession, last_active_at: float_from_monotonic_clock }`.
|
|
19
28
|
@sessions = {}
|
|
20
29
|
@mutex = Mutex.new
|
|
21
30
|
|
|
@@ -39,6 +48,11 @@ module MCP
|
|
|
39
48
|
STREAM_WRITE_ERRORS = [IOError, Errno::EPIPE, Errno::ECONNRESET].freeze
|
|
40
49
|
SESSION_REAP_INTERVAL = 60
|
|
41
50
|
|
|
51
|
+
# Rack app interface. This transport can be mounted as a Rack app.
|
|
52
|
+
def call(env)
|
|
53
|
+
handle_request(Rack::Request.new(env))
|
|
54
|
+
end
|
|
55
|
+
|
|
42
56
|
def handle_request(request)
|
|
43
57
|
case request.env["REQUEST_METHOD"]
|
|
44
58
|
when "POST"
|
|
@@ -61,7 +75,7 @@ module MCP
|
|
|
61
75
|
end
|
|
62
76
|
|
|
63
77
|
removed_sessions.each do |session|
|
|
64
|
-
close_stream_safely(session[:
|
|
78
|
+
close_stream_safely(session[:get_sse_stream])
|
|
65
79
|
close_post_request_streams(session)
|
|
66
80
|
end
|
|
67
81
|
end
|
|
@@ -113,7 +127,7 @@ module MCP
|
|
|
113
127
|
failed_sessions = []
|
|
114
128
|
|
|
115
129
|
@sessions.each do |sid, session|
|
|
116
|
-
next unless (stream = session[:
|
|
130
|
+
next unless (stream = session[:get_sse_stream])
|
|
117
131
|
|
|
118
132
|
if session_expired?(session)
|
|
119
133
|
failed_sessions << sid
|
|
@@ -247,7 +261,7 @@ module MCP
|
|
|
247
261
|
end
|
|
248
262
|
|
|
249
263
|
removed_sessions.each do |session|
|
|
250
|
-
close_stream_safely(session[:
|
|
264
|
+
close_stream_safely(session[:get_sse_stream])
|
|
251
265
|
close_post_request_streams(session)
|
|
252
266
|
end
|
|
253
267
|
end
|
|
@@ -267,6 +281,9 @@ module MCP
|
|
|
267
281
|
accept_error = validate_accept_header(request, REQUIRED_POST_ACCEPT_TYPES)
|
|
268
282
|
return accept_error if accept_error
|
|
269
283
|
|
|
284
|
+
content_type_error = validate_content_type(request)
|
|
285
|
+
return content_type_error if content_type_error
|
|
286
|
+
|
|
270
287
|
body_string = request.body.read
|
|
271
288
|
session_id = extract_session_id(request)
|
|
272
289
|
|
|
@@ -334,7 +351,7 @@ module MCP
|
|
|
334
351
|
end
|
|
335
352
|
|
|
336
353
|
if session
|
|
337
|
-
close_stream_safely(session[:
|
|
354
|
+
close_stream_safely(session[:get_sse_stream])
|
|
338
355
|
close_post_request_streams(session)
|
|
339
356
|
end
|
|
340
357
|
end
|
|
@@ -358,7 +375,7 @@ module MCP
|
|
|
358
375
|
def cleanup_and_collect_stream(session_id, streams_to_close)
|
|
359
376
|
return unless (removed = cleanup_session_unsafe(session_id))
|
|
360
377
|
|
|
361
|
-
streams_to_close << removed[:
|
|
378
|
+
streams_to_close << removed[:get_sse_stream]
|
|
362
379
|
removed[:post_request_streams]&.each_value { |stream| streams_to_close << stream }
|
|
363
380
|
end
|
|
364
381
|
|
|
@@ -399,6 +416,18 @@ module MCP
|
|
|
399
416
|
end
|
|
400
417
|
end
|
|
401
418
|
|
|
419
|
+
def validate_content_type(request)
|
|
420
|
+
content_type = request.env["CONTENT_TYPE"]
|
|
421
|
+
media_type = content_type&.split(";")&.first&.strip&.downcase
|
|
422
|
+
return if media_type == "application/json"
|
|
423
|
+
|
|
424
|
+
[
|
|
425
|
+
415,
|
|
426
|
+
{ "Content-Type" => "application/json" },
|
|
427
|
+
[{ error: "Unsupported Media Type: Content-Type must be application/json" }.to_json],
|
|
428
|
+
]
|
|
429
|
+
end
|
|
430
|
+
|
|
402
431
|
def not_acceptable_response(required_types)
|
|
403
432
|
[
|
|
404
433
|
406,
|
|
@@ -449,7 +478,7 @@ module MCP
|
|
|
449
478
|
|
|
450
479
|
@mutex.synchronize do
|
|
451
480
|
@sessions[session_id] = {
|
|
452
|
-
|
|
481
|
+
get_sse_stream: nil,
|
|
453
482
|
server_session: server_session,
|
|
454
483
|
last_active_at: Process.clock_gettime(Process::CLOCK_MONOTONIC),
|
|
455
484
|
}
|
|
@@ -531,7 +560,7 @@ module MCP
|
|
|
531
560
|
end
|
|
532
561
|
end
|
|
533
562
|
|
|
534
|
-
[200, SSE_HEADERS, body]
|
|
563
|
+
[200, SSE_HEADERS.dup, body]
|
|
535
564
|
end
|
|
536
565
|
|
|
537
566
|
# Returns the SSE stream available for server-to-client messages.
|
|
@@ -543,7 +572,7 @@ module MCP
|
|
|
543
572
|
if related_request_id
|
|
544
573
|
session.dig(:post_request_streams, related_request_id)
|
|
545
574
|
else
|
|
546
|
-
session[:
|
|
575
|
+
session[:get_sse_stream]
|
|
547
576
|
end
|
|
548
577
|
end
|
|
549
578
|
|
|
@@ -572,7 +601,7 @@ module MCP
|
|
|
572
601
|
end
|
|
573
602
|
|
|
574
603
|
if removed
|
|
575
|
-
close_stream_safely(removed[:
|
|
604
|
+
close_stream_safely(removed[:get_sse_stream])
|
|
576
605
|
|
|
577
606
|
removed[:post_request_streams]&.each_value do |stream|
|
|
578
607
|
close_stream_safely(stream)
|
|
@@ -583,7 +612,7 @@ module MCP
|
|
|
583
612
|
end
|
|
584
613
|
|
|
585
614
|
def get_session_stream(session_id)
|
|
586
|
-
@mutex.synchronize { @sessions[session_id]&.fetch(:
|
|
615
|
+
@mutex.synchronize { @sessions[session_id]&.fetch(:get_sse_stream, nil) }
|
|
587
616
|
end
|
|
588
617
|
|
|
589
618
|
def session_exists?(session_id)
|
|
@@ -613,7 +642,7 @@ module MCP
|
|
|
613
642
|
def setup_sse_stream(session_id)
|
|
614
643
|
body = create_sse_body(session_id)
|
|
615
644
|
|
|
616
|
-
[200, SSE_HEADERS, body]
|
|
645
|
+
[200, SSE_HEADERS.dup, body]
|
|
617
646
|
end
|
|
618
647
|
|
|
619
648
|
def create_sse_body(session_id)
|
|
@@ -626,8 +655,8 @@ module MCP
|
|
|
626
655
|
def store_stream_for_session(session_id, stream)
|
|
627
656
|
@mutex.synchronize do
|
|
628
657
|
session = @sessions[session_id]
|
|
629
|
-
if session && !session[:
|
|
630
|
-
session[:
|
|
658
|
+
if session && !session[:get_sse_stream]
|
|
659
|
+
session[:get_sse_stream] = stream
|
|
631
660
|
else
|
|
632
661
|
# Either session was removed, or another request already established a stream.
|
|
633
662
|
stream.close
|
|
@@ -652,13 +681,13 @@ module MCP
|
|
|
652
681
|
end
|
|
653
682
|
|
|
654
683
|
def session_active_with_stream?(session_id)
|
|
655
|
-
@mutex.synchronize { @sessions.key?(session_id) && @sessions[session_id][:
|
|
684
|
+
@mutex.synchronize { @sessions.key?(session_id) && @sessions[session_id][:get_sse_stream] }
|
|
656
685
|
end
|
|
657
686
|
|
|
658
687
|
def send_keepalive_ping(session_id)
|
|
659
688
|
@mutex.synchronize do
|
|
660
|
-
if @sessions[session_id] && @sessions[session_id][:
|
|
661
|
-
send_ping_to_stream(@sessions[session_id][:
|
|
689
|
+
if @sessions[session_id] && @sessions[session_id][:get_sse_stream]
|
|
690
|
+
send_ping_to_stream(@sessions[session_id][:get_sse_stream])
|
|
662
691
|
end
|
|
663
692
|
end
|
|
664
693
|
rescue *STREAM_WRITE_ERRORS => e
|