mcp 0.3.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/.github/dependabot.yml +6 -0
- data/.github/workflows/ci.yml +22 -7
- data/.github/workflows/release.yml +34 -2
- data/.rubocop.yml +3 -0
- data/AGENTS.md +107 -0
- data/CHANGELOG.md +58 -0
- data/Gemfile +6 -4
- data/README.md +135 -39
- data/RELEASE.md +12 -0
- data/bin/generate-gh-pages.sh +119 -0
- data/dev.yml +1 -2
- data/docs/_config.yml +6 -0
- data/docs/index.md +7 -0
- data/docs/latest/index.html +19 -0
- data/examples/http_server.rb +0 -2
- data/examples/stdio_server.rb +0 -1
- data/examples/streamable_http_server.rb +0 -2
- data/lib/json_rpc_handler.rb +151 -0
- data/lib/mcp/client/http.rb +23 -7
- data/lib/mcp/client.rb +62 -5
- data/lib/mcp/configuration.rb +38 -14
- data/lib/mcp/content.rb +2 -3
- data/lib/mcp/icon.rb +22 -0
- data/lib/mcp/instrumentation.rb +1 -1
- data/lib/mcp/methods.rb +3 -0
- data/lib/mcp/prompt/argument.rb +9 -5
- data/lib/mcp/prompt/message.rb +1 -2
- data/lib/mcp/prompt/result.rb +1 -2
- data/lib/mcp/prompt.rb +32 -4
- data/lib/mcp/resource/contents.rb +1 -2
- data/lib/mcp/resource/embedded.rb +1 -2
- data/lib/mcp/resource.rb +4 -3
- data/lib/mcp/resource_template.rb +4 -3
- data/lib/mcp/server/transports/streamable_http_transport.rb +96 -18
- data/lib/mcp/server.rb +92 -26
- data/lib/mcp/string_utils.rb +3 -4
- data/lib/mcp/tool/annotations.rb +1 -1
- data/lib/mcp/tool/input_schema.rb +6 -52
- data/lib/mcp/tool/output_schema.rb +3 -51
- data/lib/mcp/tool/response.rb +5 -4
- data/lib/mcp/tool/schema.rb +65 -0
- data/lib/mcp/tool.rb +47 -8
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +2 -0
- data/mcp.gemspec +5 -2
- metadata +16 -18
- data/.cursor/rules/release-changelogs.mdc +0 -17
data/lib/mcp/methods.rb
CHANGED
|
@@ -21,6 +21,7 @@ module MCP
|
|
|
21
21
|
|
|
22
22
|
ROOTS_LIST = "roots/list"
|
|
23
23
|
SAMPLING_CREATE_MESSAGE = "sampling/createMessage"
|
|
24
|
+
ELICITATION_CREATE = "elicitation/create"
|
|
24
25
|
|
|
25
26
|
# Notification methods
|
|
26
27
|
NOTIFICATIONS_INITIALIZED = "notifications/initialized"
|
|
@@ -76,6 +77,8 @@ module MCP
|
|
|
76
77
|
require_capability!(method, capabilities, :roots, :listChanged)
|
|
77
78
|
when SAMPLING_CREATE_MESSAGE
|
|
78
79
|
require_capability!(method, capabilities, :sampling)
|
|
80
|
+
when ELICITATION_CREATE
|
|
81
|
+
require_capability!(method, capabilities, :elicitation)
|
|
79
82
|
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED
|
|
80
83
|
# No specific capability required for initialize, ping, progress or cancelled
|
|
81
84
|
end
|
data/lib/mcp/prompt/argument.rb
CHANGED
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
# typed: strict
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module MCP
|
|
5
4
|
class Prompt
|
|
6
5
|
class Argument
|
|
7
|
-
attr_reader :name, :
|
|
6
|
+
attr_reader :name, :title, :description, :required
|
|
8
7
|
|
|
9
|
-
def initialize(name:, description: nil, required: false)
|
|
8
|
+
def initialize(name:, title: nil, description: nil, required: false)
|
|
10
9
|
@name = name
|
|
10
|
+
@title = title
|
|
11
11
|
@description = description
|
|
12
12
|
@required = required
|
|
13
|
-
@arguments = arguments
|
|
14
13
|
end
|
|
15
14
|
|
|
16
15
|
def to_h
|
|
17
|
-
{
|
|
16
|
+
{
|
|
17
|
+
name: name,
|
|
18
|
+
title: title,
|
|
19
|
+
description: description,
|
|
20
|
+
required: required,
|
|
21
|
+
}.compact
|
|
18
22
|
end
|
|
19
23
|
end
|
|
20
24
|
end
|
data/lib/mcp/prompt/message.rb
CHANGED
data/lib/mcp/prompt/result.rb
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# typed: strict
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module MCP
|
|
@@ -12,7 +11,7 @@ module MCP
|
|
|
12
11
|
end
|
|
13
12
|
|
|
14
13
|
def to_h
|
|
15
|
-
{ description
|
|
14
|
+
{ description: description, messages: messages.map(&:to_h) }.compact
|
|
16
15
|
end
|
|
17
16
|
end
|
|
18
17
|
end
|
data/lib/mcp/prompt.rb
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# typed: strict
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module MCP
|
|
@@ -8,14 +7,23 @@ module MCP
|
|
|
8
7
|
|
|
9
8
|
attr_reader :title_value
|
|
10
9
|
attr_reader :description_value
|
|
10
|
+
attr_reader :icons_value
|
|
11
11
|
attr_reader :arguments_value
|
|
12
|
+
attr_reader :meta_value
|
|
12
13
|
|
|
13
14
|
def template(args, server_context: nil)
|
|
14
15
|
raise NotImplementedError, "Subclasses must implement template"
|
|
15
16
|
end
|
|
16
17
|
|
|
17
18
|
def to_h
|
|
18
|
-
{
|
|
19
|
+
{
|
|
20
|
+
name: name_value,
|
|
21
|
+
title: title_value,
|
|
22
|
+
description: description_value,
|
|
23
|
+
icons: icons&.map(&:to_h),
|
|
24
|
+
arguments: arguments_value&.map(&:to_h),
|
|
25
|
+
_meta: meta_value,
|
|
26
|
+
}.compact
|
|
19
27
|
end
|
|
20
28
|
|
|
21
29
|
def inherited(subclass)
|
|
@@ -23,7 +31,9 @@ module MCP
|
|
|
23
31
|
subclass.instance_variable_set(:@name_value, nil)
|
|
24
32
|
subclass.instance_variable_set(:@title_value, nil)
|
|
25
33
|
subclass.instance_variable_set(:@description_value, nil)
|
|
34
|
+
subclass.instance_variable_set(:@icons_value, nil)
|
|
26
35
|
subclass.instance_variable_set(:@arguments_value, nil)
|
|
36
|
+
subclass.instance_variable_set(:@meta_value, nil)
|
|
27
37
|
end
|
|
28
38
|
|
|
29
39
|
def prompt_name(value = NOT_SET)
|
|
@@ -54,6 +64,14 @@ module MCP
|
|
|
54
64
|
end
|
|
55
65
|
end
|
|
56
66
|
|
|
67
|
+
def icons(value = NOT_SET)
|
|
68
|
+
if value == NOT_SET
|
|
69
|
+
@icons_value
|
|
70
|
+
else
|
|
71
|
+
@icons_value = value
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
57
75
|
def arguments(value = NOT_SET)
|
|
58
76
|
if value == NOT_SET
|
|
59
77
|
@arguments_value
|
|
@@ -62,15 +80,25 @@ module MCP
|
|
|
62
80
|
end
|
|
63
81
|
end
|
|
64
82
|
|
|
65
|
-
def
|
|
83
|
+
def meta(value = NOT_SET)
|
|
84
|
+
if value == NOT_SET
|
|
85
|
+
@meta_value
|
|
86
|
+
else
|
|
87
|
+
@meta_value = value
|
|
88
|
+
end
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def define(name: nil, title: nil, description: nil, icons: [], arguments: [], meta: nil, &block)
|
|
66
92
|
Class.new(self) do
|
|
67
93
|
prompt_name name
|
|
68
94
|
title title
|
|
69
95
|
description description
|
|
96
|
+
icons icons
|
|
70
97
|
arguments arguments
|
|
71
98
|
define_singleton_method(:template) do |args, server_context: nil|
|
|
72
|
-
instance_exec(args, server_context
|
|
99
|
+
instance_exec(args, server_context: server_context, &block)
|
|
73
100
|
end
|
|
101
|
+
meta meta
|
|
74
102
|
end
|
|
75
103
|
end
|
|
76
104
|
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
# typed: strict
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module MCP
|
|
@@ -11,7 +10,7 @@ module MCP
|
|
|
11
10
|
end
|
|
12
11
|
|
|
13
12
|
def to_h
|
|
14
|
-
{ resource: resource.to_h, annotations: }.compact
|
|
13
|
+
{ resource: resource.to_h, annotations: annotations }.compact
|
|
15
14
|
end
|
|
16
15
|
end
|
|
17
16
|
end
|
data/lib/mcp/resource.rb
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# typed: strict
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module MCP
|
|
5
4
|
class Resource
|
|
6
|
-
attr_reader :uri, :name, :title, :description, :mime_type
|
|
5
|
+
attr_reader :uri, :name, :title, :description, :icons, :mime_type
|
|
7
6
|
|
|
8
|
-
def initialize(uri:, name:, title: nil, description: nil, mime_type: nil)
|
|
7
|
+
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil)
|
|
9
8
|
@uri = uri
|
|
10
9
|
@name = name
|
|
11
10
|
@title = title
|
|
12
11
|
@description = description
|
|
12
|
+
@icons = icons
|
|
13
13
|
@mime_type = mime_type
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -19,6 +19,7 @@ module MCP
|
|
|
19
19
|
name: name,
|
|
20
20
|
title: title,
|
|
21
21
|
description: description,
|
|
22
|
+
icons: icons.map(&:to_h),
|
|
22
23
|
mimeType: mime_type,
|
|
23
24
|
}.compact
|
|
24
25
|
end
|
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
# typed: strict
|
|
2
1
|
# frozen_string_literal: true
|
|
3
2
|
|
|
4
3
|
module MCP
|
|
5
4
|
class ResourceTemplate
|
|
6
|
-
attr_reader :uri_template, :name, :title, :description, :mime_type
|
|
5
|
+
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type
|
|
7
6
|
|
|
8
|
-
def initialize(uri_template:, name:, title: nil, description: nil, mime_type: nil)
|
|
7
|
+
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil)
|
|
9
8
|
@uri_template = uri_template
|
|
10
9
|
@name = name
|
|
11
10
|
@title = title
|
|
12
11
|
@description = description
|
|
12
|
+
@icons = icons
|
|
13
13
|
@mime_type = mime_type
|
|
14
14
|
end
|
|
15
15
|
|
|
@@ -19,6 +19,7 @@ module MCP
|
|
|
19
19
|
name: name,
|
|
20
20
|
title: title,
|
|
21
21
|
description: description,
|
|
22
|
+
icons: icons.map(&:to_h),
|
|
22
23
|
mimeType: mime_type,
|
|
23
24
|
}.compact
|
|
24
25
|
end
|
|
@@ -8,13 +8,18 @@ module MCP
|
|
|
8
8
|
class Server
|
|
9
9
|
module Transports
|
|
10
10
|
class StreamableHTTPTransport < Transport
|
|
11
|
-
def initialize(server)
|
|
12
|
-
super
|
|
11
|
+
def initialize(server, stateless: false)
|
|
12
|
+
super(server)
|
|
13
13
|
# { session_id => { stream: stream_object }
|
|
14
14
|
@sessions = {}
|
|
15
15
|
@mutex = Mutex.new
|
|
16
|
+
|
|
17
|
+
@stateless = stateless
|
|
16
18
|
end
|
|
17
19
|
|
|
20
|
+
REQUIRED_POST_ACCEPT_TYPES = ["application/json", "text/event-stream"].freeze
|
|
21
|
+
REQUIRED_GET_ACCEPT_TYPES = ["text/event-stream"].freeze
|
|
22
|
+
|
|
18
23
|
def handle_request(request)
|
|
19
24
|
case request.env["REQUEST_METHOD"]
|
|
20
25
|
when "POST"
|
|
@@ -24,7 +29,7 @@ module MCP
|
|
|
24
29
|
when "DELETE"
|
|
25
30
|
handle_delete(request)
|
|
26
31
|
else
|
|
27
|
-
|
|
32
|
+
method_not_allowed_response
|
|
28
33
|
end
|
|
29
34
|
end
|
|
30
35
|
|
|
@@ -35,9 +40,12 @@ module MCP
|
|
|
35
40
|
end
|
|
36
41
|
|
|
37
42
|
def send_notification(method, params = nil, session_id: nil)
|
|
43
|
+
# Stateless mode doesn't support notifications
|
|
44
|
+
raise "Stateless mode does not support notifications" if @stateless
|
|
45
|
+
|
|
38
46
|
notification = {
|
|
39
47
|
jsonrpc: "2.0",
|
|
40
|
-
method
|
|
48
|
+
method: method,
|
|
41
49
|
}
|
|
42
50
|
notification[:params] = params if params
|
|
43
51
|
|
|
@@ -100,6 +108,9 @@ module MCP
|
|
|
100
108
|
end
|
|
101
109
|
|
|
102
110
|
def handle_post(request)
|
|
111
|
+
accept_error = validate_accept_header(request, REQUIRED_POST_ACCEPT_TYPES)
|
|
112
|
+
return accept_error if accept_error
|
|
113
|
+
|
|
103
114
|
body_string = request.body.read
|
|
104
115
|
session_id = extract_session_id(request)
|
|
105
116
|
|
|
@@ -108,8 +119,8 @@ module MCP
|
|
|
108
119
|
|
|
109
120
|
if body["method"] == "initialize"
|
|
110
121
|
handle_initialization(body_string, body)
|
|
111
|
-
elsif body
|
|
112
|
-
|
|
122
|
+
elsif notification?(body) || response?(body)
|
|
123
|
+
handle_accepted
|
|
113
124
|
else
|
|
114
125
|
handle_regular_request(body_string, session_id)
|
|
115
126
|
end
|
|
@@ -119,6 +130,13 @@ module MCP
|
|
|
119
130
|
end
|
|
120
131
|
|
|
121
132
|
def handle_get(request)
|
|
133
|
+
if @stateless
|
|
134
|
+
return method_not_allowed_response
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
accept_error = validate_accept_header(request, REQUIRED_GET_ACCEPT_TYPES)
|
|
138
|
+
return accept_error if accept_error
|
|
139
|
+
|
|
122
140
|
session_id = extract_session_id(request)
|
|
123
141
|
|
|
124
142
|
return missing_session_id_response unless session_id
|
|
@@ -128,6 +146,13 @@ module MCP
|
|
|
128
146
|
end
|
|
129
147
|
|
|
130
148
|
def handle_delete(request)
|
|
149
|
+
success_response = [200, { "Content-Type" => "application/json" }, [{ success: true }.to_json]]
|
|
150
|
+
|
|
151
|
+
if @stateless
|
|
152
|
+
# Stateless mode doesn't support sessions, so we can just return a success response
|
|
153
|
+
return success_response
|
|
154
|
+
end
|
|
155
|
+
|
|
131
156
|
session_id = request.env["HTTP_MCP_SESSION_ID"]
|
|
132
157
|
|
|
133
158
|
return [
|
|
@@ -137,7 +162,7 @@ module MCP
|
|
|
137
162
|
] unless session_id
|
|
138
163
|
|
|
139
164
|
cleanup_session(session_id)
|
|
140
|
-
|
|
165
|
+
success_response
|
|
141
166
|
end
|
|
142
167
|
|
|
143
168
|
def cleanup_session(session_id)
|
|
@@ -162,51 +187,100 @@ module MCP
|
|
|
162
187
|
request.env["HTTP_MCP_SESSION_ID"]
|
|
163
188
|
end
|
|
164
189
|
|
|
190
|
+
def validate_accept_header(request, required_types)
|
|
191
|
+
accept_header = request.env["HTTP_ACCEPT"]
|
|
192
|
+
return not_acceptable_response(required_types) unless accept_header
|
|
193
|
+
|
|
194
|
+
accepted_types = parse_accept_header(accept_header)
|
|
195
|
+
missing_types = required_types - accepted_types
|
|
196
|
+
return not_acceptable_response(required_types) unless missing_types.empty?
|
|
197
|
+
|
|
198
|
+
nil
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def parse_accept_header(header)
|
|
202
|
+
header.split(",").map do |part|
|
|
203
|
+
part.split(";").first.strip
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
def not_acceptable_response(required_types)
|
|
208
|
+
[
|
|
209
|
+
406,
|
|
210
|
+
{ "Content-Type" => "application/json" },
|
|
211
|
+
[{ error: "Not Acceptable: Accept header must include #{required_types.join(" and ")}" }.to_json],
|
|
212
|
+
]
|
|
213
|
+
end
|
|
214
|
+
|
|
165
215
|
def parse_request_body(body_string)
|
|
166
216
|
JSON.parse(body_string)
|
|
167
217
|
rescue JSON::ParserError, TypeError
|
|
168
218
|
[400, { "Content-Type" => "application/json" }, [{ error: "Invalid JSON" }.to_json]]
|
|
169
219
|
end
|
|
170
220
|
|
|
221
|
+
def notification?(body)
|
|
222
|
+
!body["id"] && !!body["method"]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def response?(body)
|
|
226
|
+
!!body["id"] && !body["method"]
|
|
227
|
+
end
|
|
228
|
+
|
|
171
229
|
def handle_initialization(body_string, body)
|
|
172
|
-
session_id =
|
|
230
|
+
session_id = nil
|
|
173
231
|
|
|
174
|
-
@
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
232
|
+
unless @stateless
|
|
233
|
+
session_id = SecureRandom.uuid
|
|
234
|
+
|
|
235
|
+
@mutex.synchronize do
|
|
236
|
+
@sessions[session_id] = {
|
|
237
|
+
stream: nil,
|
|
238
|
+
}
|
|
239
|
+
end
|
|
178
240
|
end
|
|
179
241
|
|
|
180
242
|
response = @server.handle_json(body_string)
|
|
181
243
|
|
|
182
244
|
headers = {
|
|
183
245
|
"Content-Type" => "application/json",
|
|
184
|
-
"Mcp-Session-Id" => session_id,
|
|
185
246
|
}
|
|
186
247
|
|
|
248
|
+
headers["Mcp-Session-Id"] = session_id if session_id
|
|
249
|
+
|
|
187
250
|
[200, headers, [response]]
|
|
188
251
|
end
|
|
189
252
|
|
|
190
|
-
def
|
|
253
|
+
def handle_accepted
|
|
191
254
|
[202, {}, []]
|
|
192
255
|
end
|
|
193
256
|
|
|
194
257
|
def handle_regular_request(body_string, session_id)
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
258
|
+
unless @stateless
|
|
259
|
+
# If session ID is provided, but not in the sessions hash, return an error
|
|
260
|
+
if session_id && !@sessions.key?(session_id)
|
|
261
|
+
return [400, { "Content-Type" => "application/json" }, [{ error: "Invalid session ID" }.to_json]]
|
|
262
|
+
end
|
|
198
263
|
end
|
|
199
264
|
|
|
200
|
-
response = @server.handle_json(body_string)
|
|
265
|
+
response = @server.handle_json(body_string) || ""
|
|
266
|
+
|
|
267
|
+
# Stream can be nil since stateless mode doesn't retain streams
|
|
201
268
|
stream = get_session_stream(session_id) if session_id
|
|
202
269
|
|
|
203
270
|
if stream
|
|
204
271
|
send_response_to_stream(stream, response, session_id)
|
|
272
|
+
elsif response.nil? && notification_request?(body_string)
|
|
273
|
+
[202, { "Content-Type" => "application/json" }, [response]]
|
|
205
274
|
else
|
|
206
275
|
[200, { "Content-Type" => "application/json" }, [response]]
|
|
207
276
|
end
|
|
208
277
|
end
|
|
209
278
|
|
|
279
|
+
def notification_request?(body_string)
|
|
280
|
+
body = parse_request_body(body_string)
|
|
281
|
+
body.is_a?(Hash) && body["method"].start_with?("notifications/")
|
|
282
|
+
end
|
|
283
|
+
|
|
210
284
|
def get_session_stream(session_id)
|
|
211
285
|
@mutex.synchronize { @sessions[session_id]&.fetch(:stream, nil) }
|
|
212
286
|
end
|
|
@@ -228,6 +302,10 @@ module MCP
|
|
|
228
302
|
@mutex.synchronize { @sessions.key?(session_id) }
|
|
229
303
|
end
|
|
230
304
|
|
|
305
|
+
def method_not_allowed_response
|
|
306
|
+
[405, { "Content-Type" => "application/json" }, [{ error: "Method not allowed" }.to_json]]
|
|
307
|
+
end
|
|
308
|
+
|
|
231
309
|
def missing_session_id_response
|
|
232
310
|
[400, { "Content-Type" => "application/json" }, [{ error: "Missing session ID" }.to_json]]
|
|
233
311
|
end
|