mcp 0.12.0 → 0.14.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 +467 -131
- data/lib/json_rpc_handler.rb +16 -9
- data/lib/mcp/client/http.rb +133 -14
- data/lib/mcp/client/paginated_result.rb +13 -0
- data/lib/mcp/client.rb +195 -22
- 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 +4 -5
- 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/pagination.rb +42 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +35 -8
- data/lib/mcp/server.rb +82 -60
- data/lib/mcp/server_context.rb +54 -0
- data/lib/mcp/server_session.rb +45 -0
- data/lib/mcp/tool/response.rb +4 -3
- data/lib/mcp/version.rb +1 -1
- metadata +6 -4
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
|
@@ -1,25 +1,42 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../methods"
|
|
4
|
+
|
|
3
5
|
module MCP
|
|
4
6
|
class Client
|
|
7
|
+
# TODO: HTTP GET for SSE streaming is not yet implemented.
|
|
8
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#listening-for-messages-from-the-server
|
|
9
|
+
# TODO: Resumability and redelivery with Last-Event-ID is not yet implemented.
|
|
10
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#resumability-and-redelivery
|
|
5
11
|
class HTTP
|
|
6
12
|
ACCEPT_HEADER = "application/json, text/event-stream"
|
|
13
|
+
SESSION_ID_HEADER = "Mcp-Session-Id"
|
|
14
|
+
PROTOCOL_VERSION_HEADER = "MCP-Protocol-Version"
|
|
7
15
|
|
|
8
|
-
attr_reader :url
|
|
16
|
+
attr_reader :url, :session_id, :protocol_version
|
|
9
17
|
|
|
10
18
|
def initialize(url:, headers: {}, &block)
|
|
11
19
|
@url = url
|
|
12
20
|
@headers = headers
|
|
13
21
|
@faraday_customizer = block
|
|
22
|
+
@session_id = nil
|
|
23
|
+
@protocol_version = nil
|
|
14
24
|
end
|
|
15
25
|
|
|
26
|
+
# Sends a JSON-RPC request and returns the parsed response body.
|
|
27
|
+
# After a successful `initialize` handshake, the session ID and protocol
|
|
28
|
+
# version returned by the server are captured and automatically included
|
|
29
|
+
# on subsequent requests.
|
|
16
30
|
def send_request(request:)
|
|
17
31
|
method = request[:method] || request["method"]
|
|
18
32
|
params = request[:params] || request["params"]
|
|
19
33
|
|
|
20
|
-
response = client.post("", request)
|
|
21
|
-
|
|
22
|
-
|
|
34
|
+
response = client.post("", request, session_headers)
|
|
35
|
+
body = parse_response_body(response, method, params)
|
|
36
|
+
|
|
37
|
+
capture_session_info(method, response, body)
|
|
38
|
+
|
|
39
|
+
body
|
|
23
40
|
rescue Faraday::BadRequestError => e
|
|
24
41
|
raise RequestHandlerError.new(
|
|
25
42
|
"The #{method} request is invalid",
|
|
@@ -42,12 +59,25 @@ module MCP
|
|
|
42
59
|
original_error: e,
|
|
43
60
|
)
|
|
44
61
|
rescue Faraday::ResourceNotFound => e
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
62
|
+
# Per spec, 404 is the session-expired signal only when the request
|
|
63
|
+
# actually carried an `Mcp-Session-Id`. A 404 without a session attached
|
|
64
|
+
# (e.g. wrong URL or a stateless server) surfaces as a generic not-found.
|
|
65
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
|
|
66
|
+
if @session_id
|
|
67
|
+
clear_session
|
|
68
|
+
raise SessionExpiredError.new(
|
|
69
|
+
"The #{method} request is not found",
|
|
70
|
+
{ method: method, params: params },
|
|
71
|
+
original_error: e,
|
|
72
|
+
)
|
|
73
|
+
else
|
|
74
|
+
raise RequestHandlerError.new(
|
|
75
|
+
"The #{method} request is not found",
|
|
76
|
+
{ method: method, params: params },
|
|
77
|
+
error_type: :not_found,
|
|
78
|
+
original_error: e,
|
|
79
|
+
)
|
|
80
|
+
end
|
|
51
81
|
rescue Faraday::UnprocessableEntityError => e
|
|
52
82
|
raise RequestHandlerError.new(
|
|
53
83
|
"The #{method} request is unprocessable",
|
|
@@ -64,6 +94,28 @@ module MCP
|
|
|
64
94
|
)
|
|
65
95
|
end
|
|
66
96
|
|
|
97
|
+
# Terminates the session by sending an HTTP DELETE to the MCP endpoint
|
|
98
|
+
# with the current `Mcp-Session-Id` header, and clears locally tracked
|
|
99
|
+
# session state afterward. No-op when no session has been established.
|
|
100
|
+
#
|
|
101
|
+
# Per spec, the server MAY respond with HTTP 405 Method Not Allowed when
|
|
102
|
+
# it does not support client-initiated termination, and returns 404 for
|
|
103
|
+
# a session it has already terminated. Both mean the session is gone —
|
|
104
|
+
# the desired end state. Other errors surface to the caller; local
|
|
105
|
+
# session state is cleared either way.
|
|
106
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
|
|
107
|
+
def close
|
|
108
|
+
return unless @session_id
|
|
109
|
+
|
|
110
|
+
begin
|
|
111
|
+
client.delete("", nil, session_headers)
|
|
112
|
+
rescue Faraday::ClientError => e
|
|
113
|
+
raise unless [404, 405].include?(e.response&.dig(:status))
|
|
114
|
+
ensure
|
|
115
|
+
clear_session
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
|
|
67
119
|
private
|
|
68
120
|
|
|
69
121
|
attr_reader :headers
|
|
@@ -84,6 +136,31 @@ module MCP
|
|
|
84
136
|
end
|
|
85
137
|
end
|
|
86
138
|
|
|
139
|
+
# Per spec, the client MUST include `MCP-Session-Id` (when the server assigned one)
|
|
140
|
+
# and `MCP-Protocol-Version` on all requests after `initialize`.
|
|
141
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#session-management
|
|
142
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#protocol-version-header
|
|
143
|
+
def session_headers
|
|
144
|
+
request_headers = {}
|
|
145
|
+
request_headers[SESSION_ID_HEADER] = @session_id if @session_id
|
|
146
|
+
request_headers[PROTOCOL_VERSION_HEADER] = @protocol_version if @protocol_version
|
|
147
|
+
request_headers
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def capture_session_info(method, response, body)
|
|
151
|
+
return unless method.to_s == Methods::INITIALIZE
|
|
152
|
+
|
|
153
|
+
# Faraday normalizes header names to lowercase.
|
|
154
|
+
session_id = response.headers[SESSION_ID_HEADER.downcase]
|
|
155
|
+
@session_id ||= session_id unless session_id.to_s.empty?
|
|
156
|
+
@protocol_version ||= body.is_a?(Hash) ? body.dig("result", "protocolVersion") : nil
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def clear_session
|
|
160
|
+
@session_id = nil
|
|
161
|
+
@protocol_version = nil
|
|
162
|
+
end
|
|
163
|
+
|
|
87
164
|
def require_faraday!
|
|
88
165
|
require "faraday"
|
|
89
166
|
rescue LoadError
|
|
@@ -92,14 +169,56 @@ module MCP
|
|
|
92
169
|
"See https://rubygems.org/gems/faraday for more details."
|
|
93
170
|
end
|
|
94
171
|
|
|
95
|
-
def
|
|
172
|
+
def require_event_stream_parser!
|
|
173
|
+
require "event_stream_parser"
|
|
174
|
+
rescue LoadError
|
|
175
|
+
raise LoadError, "The 'event_stream_parser' gem is required to parse SSE responses. " \
|
|
176
|
+
"Add it to your Gemfile: gem 'event_stream_parser', '>= 1.0'. " \
|
|
177
|
+
"See https://rubygems.org/gems/event_stream_parser for more details."
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def parse_response_body(response, method, params)
|
|
181
|
+
# 202 Accepted is the server's ACK for a JSON-RPC notification or response; no body is expected.
|
|
182
|
+
# https://modelcontextprotocol.io/specification/2025-11-25/basic/transports#sending-messages-to-the-server
|
|
183
|
+
return if response.status == 202
|
|
184
|
+
|
|
96
185
|
content_type = response.headers["Content-Type"]
|
|
97
|
-
|
|
186
|
+
|
|
187
|
+
if content_type&.include?("text/event-stream")
|
|
188
|
+
parse_sse_response(response.body, method, params)
|
|
189
|
+
elsif content_type&.include?("application/json")
|
|
190
|
+
response.body
|
|
191
|
+
else
|
|
192
|
+
raise RequestHandlerError.new(
|
|
193
|
+
"Unsupported Content-Type: #{content_type.inspect}. Expected application/json or text/event-stream.",
|
|
194
|
+
{ method: method, params: params },
|
|
195
|
+
error_type: :unsupported_media_type,
|
|
196
|
+
)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def parse_sse_response(body, method, params)
|
|
201
|
+
require_event_stream_parser!
|
|
202
|
+
|
|
203
|
+
json_rpc_response = nil
|
|
204
|
+
parser = EventStreamParser::Parser.new
|
|
205
|
+
parser.feed(body.to_s) do |_type, data, _id|
|
|
206
|
+
next if data.empty?
|
|
207
|
+
|
|
208
|
+
begin
|
|
209
|
+
parsed = JSON.parse(data)
|
|
210
|
+
json_rpc_response = parsed if parsed.is_a?(Hash) && (parsed.key?("result") || parsed.key?("error"))
|
|
211
|
+
rescue JSON::ParserError
|
|
212
|
+
next
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
return json_rpc_response if json_rpc_response
|
|
98
217
|
|
|
99
218
|
raise RequestHandlerError.new(
|
|
100
|
-
"
|
|
219
|
+
"No valid JSON-RPC response found in SSE stream",
|
|
101
220
|
{ method: method, params: params },
|
|
102
|
-
error_type: :
|
|
221
|
+
error_type: :parse_error,
|
|
103
222
|
)
|
|
104
223
|
end
|
|
105
224
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class Client
|
|
5
|
+
# Result objects returned by `list_tools`, `list_prompts`, `list_resources`, and `list_resource_templates`.
|
|
6
|
+
# Each carries the page items, an optional opaque `next_cursor` string for continuing pagination,
|
|
7
|
+
# and an optional `meta` hash mirroring the MCP `_meta` response field.
|
|
8
|
+
ListToolsResult = Struct.new(:tools, :next_cursor, :meta, keyword_init: true)
|
|
9
|
+
ListPromptsResult = Struct.new(:prompts, :next_cursor, :meta, keyword_init: true)
|
|
10
|
+
ListResourcesResult = Struct.new(:resources, :next_cursor, :meta, keyword_init: true)
|
|
11
|
+
ListResourceTemplatesResult = Struct.new(:resource_templates, :next_cursor, :meta, keyword_init: true)
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/mcp/client.rb
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "client/stdio"
|
|
4
4
|
require_relative "client/http"
|
|
5
|
+
require_relative "client/paginated_result"
|
|
5
6
|
require_relative "client/tool"
|
|
6
7
|
|
|
7
8
|
module MCP
|
|
@@ -27,6 +28,21 @@ module MCP
|
|
|
27
28
|
end
|
|
28
29
|
end
|
|
29
30
|
|
|
31
|
+
# Raised when a server response fails client-side validation, e.g., a success response
|
|
32
|
+
# whose `result` field is missing or has the wrong type. This is distinct from a
|
|
33
|
+
# server-returned JSON-RPC error, which is raised as `ServerError`.
|
|
34
|
+
class ValidationError < StandardError; end
|
|
35
|
+
|
|
36
|
+
# Raised when the server responds 404 to a request containing a session ID,
|
|
37
|
+
# indicating the session has expired. Inherits from `RequestHandlerError` for
|
|
38
|
+
# backward compatibility with callers that rescue the generic error. Per spec,
|
|
39
|
+
# clients MUST start a new session with a fresh `initialize` request in response.
|
|
40
|
+
class SessionExpiredError < RequestHandlerError
|
|
41
|
+
def initialize(message, request, original_error: nil)
|
|
42
|
+
super(message, request, error_type: :not_found, original_error: original_error)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
30
46
|
# Initializes a new MCP::Client instance.
|
|
31
47
|
#
|
|
32
48
|
# @param transport [Object] The transport object to use for communication with the server.
|
|
@@ -43,8 +59,41 @@ module MCP
|
|
|
43
59
|
# So keeping it public
|
|
44
60
|
attr_reader :transport
|
|
45
61
|
|
|
46
|
-
# Returns
|
|
47
|
-
#
|
|
62
|
+
# Returns a single page of tools from the server.
|
|
63
|
+
#
|
|
64
|
+
# @param cursor [String, nil] Cursor from a previous page response.
|
|
65
|
+
# @return [MCP::Client::ListToolsResult] Result with `tools` (Array<MCP::Client::Tool>)
|
|
66
|
+
# and `next_cursor` (String or nil).
|
|
67
|
+
#
|
|
68
|
+
# @example Iterate all pages
|
|
69
|
+
# cursor = nil
|
|
70
|
+
# loop do
|
|
71
|
+
# page = client.list_tools(cursor: cursor)
|
|
72
|
+
# page.tools.each { |tool| puts tool.name }
|
|
73
|
+
# cursor = page.next_cursor
|
|
74
|
+
# break unless cursor
|
|
75
|
+
# end
|
|
76
|
+
def list_tools(cursor: nil)
|
|
77
|
+
params = cursor ? { cursor: cursor } : nil
|
|
78
|
+
response = request(method: "tools/list", params: params)
|
|
79
|
+
result = response["result"] || {}
|
|
80
|
+
|
|
81
|
+
tools = (result["tools"] || []).map do |tool|
|
|
82
|
+
Tool.new(
|
|
83
|
+
name: tool["name"],
|
|
84
|
+
description: tool["description"],
|
|
85
|
+
input_schema: tool["inputSchema"],
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
ListToolsResult.new(tools: tools, next_cursor: result["nextCursor"], meta: result["_meta"])
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Returns every tool available on the server. Iterates through all pages automatically
|
|
93
|
+
# when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
|
|
94
|
+
# Use {#list_tools} when you need fine-grained cursor control.
|
|
95
|
+
#
|
|
96
|
+
# Each call will make a new request - the result is not cached.
|
|
48
97
|
#
|
|
49
98
|
# @return [Array<MCP::Client::Tool>] An array of available tools.
|
|
50
99
|
#
|
|
@@ -54,45 +103,151 @@ module MCP
|
|
|
54
103
|
# puts tool.name
|
|
55
104
|
# end
|
|
56
105
|
def tools
|
|
57
|
-
|
|
106
|
+
# TODO: consider renaming to `list_all_tools`.
|
|
107
|
+
all_tools = []
|
|
108
|
+
seen = Set.new
|
|
109
|
+
cursor = nil
|
|
58
110
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
111
|
+
loop do
|
|
112
|
+
page = list_tools(cursor: cursor)
|
|
113
|
+
all_tools.concat(page.tools)
|
|
114
|
+
next_cursor = page.next_cursor
|
|
115
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
116
|
+
|
|
117
|
+
seen << next_cursor
|
|
118
|
+
cursor = next_cursor
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
all_tools
|
|
66
122
|
end
|
|
67
123
|
|
|
68
|
-
# Returns
|
|
69
|
-
#
|
|
124
|
+
# Returns a single page of resources from the server.
|
|
125
|
+
#
|
|
126
|
+
# @param cursor [String, nil] Cursor from a previous page response.
|
|
127
|
+
# @return [MCP::Client::ListResourcesResult] Result with `resources` (Array<Hash>)
|
|
128
|
+
# and `next_cursor` (String or nil).
|
|
129
|
+
def list_resources(cursor: nil)
|
|
130
|
+
params = cursor ? { cursor: cursor } : nil
|
|
131
|
+
response = request(method: "resources/list", params: params)
|
|
132
|
+
result = response["result"] || {}
|
|
133
|
+
|
|
134
|
+
ListResourcesResult.new(
|
|
135
|
+
resources: result["resources"] || [],
|
|
136
|
+
next_cursor: result["nextCursor"],
|
|
137
|
+
meta: result["_meta"],
|
|
138
|
+
)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
# Returns every resource available on the server. Iterates through all pages automatically
|
|
142
|
+
# when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
|
|
143
|
+
# Use {#list_resources} when you need fine-grained cursor control.
|
|
144
|
+
#
|
|
145
|
+
# Each call will make a new request - the result is not cached.
|
|
70
146
|
#
|
|
71
147
|
# @return [Array<Hash>] An array of available resources.
|
|
72
148
|
def resources
|
|
73
|
-
|
|
149
|
+
# TODO: consider renaming to `list_all_resources`.
|
|
150
|
+
all_resources = []
|
|
151
|
+
seen = Set.new
|
|
152
|
+
cursor = nil
|
|
153
|
+
|
|
154
|
+
loop do
|
|
155
|
+
page = list_resources(cursor: cursor)
|
|
156
|
+
all_resources.concat(page.resources)
|
|
157
|
+
next_cursor = page.next_cursor
|
|
158
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
159
|
+
|
|
160
|
+
seen << next_cursor
|
|
161
|
+
cursor = next_cursor
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
all_resources
|
|
165
|
+
end
|
|
74
166
|
|
|
75
|
-
|
|
167
|
+
# Returns a single page of resource templates from the server.
|
|
168
|
+
#
|
|
169
|
+
# @param cursor [String, nil] Cursor from a previous page response.
|
|
170
|
+
# @return [MCP::Client::ListResourceTemplatesResult] Result with `resource_templates`
|
|
171
|
+
# (Array<Hash>) and `next_cursor` (String or nil).
|
|
172
|
+
def list_resource_templates(cursor: nil)
|
|
173
|
+
params = cursor ? { cursor: cursor } : nil
|
|
174
|
+
response = request(method: "resources/templates/list", params: params)
|
|
175
|
+
result = response["result"] || {}
|
|
176
|
+
|
|
177
|
+
ListResourceTemplatesResult.new(
|
|
178
|
+
resource_templates: result["resourceTemplates"] || [],
|
|
179
|
+
next_cursor: result["nextCursor"],
|
|
180
|
+
meta: result["_meta"],
|
|
181
|
+
)
|
|
76
182
|
end
|
|
77
183
|
|
|
78
|
-
# Returns
|
|
79
|
-
#
|
|
184
|
+
# Returns every resource template available on the server. Iterates through all pages automatically
|
|
185
|
+
# when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
|
|
186
|
+
# Use {#list_resource_templates} when you need fine-grained cursor control.
|
|
187
|
+
#
|
|
188
|
+
# Each call will make a new request - the result is not cached.
|
|
80
189
|
#
|
|
81
190
|
# @return [Array<Hash>] An array of available resource templates.
|
|
82
191
|
def resource_templates
|
|
83
|
-
|
|
192
|
+
# TODO: consider renaming to `list_all_resource_templates`.
|
|
193
|
+
all_templates = []
|
|
194
|
+
seen = Set.new
|
|
195
|
+
cursor = nil
|
|
196
|
+
|
|
197
|
+
loop do
|
|
198
|
+
page = list_resource_templates(cursor: cursor)
|
|
199
|
+
all_templates.concat(page.resource_templates)
|
|
200
|
+
next_cursor = page.next_cursor
|
|
201
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
202
|
+
|
|
203
|
+
seen << next_cursor
|
|
204
|
+
cursor = next_cursor
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
all_templates
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# Returns a single page of prompts from the server.
|
|
211
|
+
#
|
|
212
|
+
# @param cursor [String, nil] Cursor from a previous page response.
|
|
213
|
+
# @return [MCP::Client::ListPromptsResult] Result with `prompts` (Array<Hash>)
|
|
214
|
+
# and `next_cursor` (String or nil).
|
|
215
|
+
def list_prompts(cursor: nil)
|
|
216
|
+
params = cursor ? { cursor: cursor } : nil
|
|
217
|
+
response = request(method: "prompts/list", params: params)
|
|
218
|
+
result = response["result"] || {}
|
|
84
219
|
|
|
85
|
-
|
|
220
|
+
ListPromptsResult.new(
|
|
221
|
+
prompts: result["prompts"] || [],
|
|
222
|
+
next_cursor: result["nextCursor"],
|
|
223
|
+
meta: result["_meta"],
|
|
224
|
+
)
|
|
86
225
|
end
|
|
87
226
|
|
|
88
|
-
# Returns
|
|
89
|
-
#
|
|
227
|
+
# Returns every prompt available on the server. Iterates through all pages automatically
|
|
228
|
+
# when the server paginates, so the full collection is returned regardless of the server's `page_size` setting.
|
|
229
|
+
# Use {#list_prompts} when you need fine-grained cursor control.
|
|
230
|
+
#
|
|
231
|
+
# Each call will make a new request - the result is not cached.
|
|
90
232
|
#
|
|
91
233
|
# @return [Array<Hash>] An array of available prompts.
|
|
92
234
|
def prompts
|
|
93
|
-
|
|
235
|
+
# TODO: consider renaming to `list_all_prompts`.
|
|
236
|
+
all_prompts = []
|
|
237
|
+
seen = Set.new
|
|
238
|
+
cursor = nil
|
|
239
|
+
|
|
240
|
+
loop do
|
|
241
|
+
page = list_prompts(cursor: cursor)
|
|
242
|
+
all_prompts.concat(page.prompts)
|
|
243
|
+
next_cursor = page.next_cursor
|
|
244
|
+
break if next_cursor.nil? || seen.include?(next_cursor)
|
|
245
|
+
|
|
246
|
+
seen << next_cursor
|
|
247
|
+
cursor = next_cursor
|
|
248
|
+
end
|
|
94
249
|
|
|
95
|
-
|
|
250
|
+
all_prompts
|
|
96
251
|
end
|
|
97
252
|
|
|
98
253
|
# Calls a tool via the transport layer and returns the full response from the server.
|
|
@@ -163,6 +318,24 @@ module MCP
|
|
|
163
318
|
response.dig("result", "completion") || { "values" => [], "hasMore" => false }
|
|
164
319
|
end
|
|
165
320
|
|
|
321
|
+
# Sends a `ping` request to the server to verify the connection is alive.
|
|
322
|
+
# Per the MCP spec, the server responds with an empty result.
|
|
323
|
+
#
|
|
324
|
+
# @return [Hash] An empty hash on success.
|
|
325
|
+
# @raise [ServerError] If the server returns a JSON-RPC error.
|
|
326
|
+
# @raise [ValidationError] If the response `result` is missing or not a Hash.
|
|
327
|
+
#
|
|
328
|
+
# @example
|
|
329
|
+
# client.ping # => {}
|
|
330
|
+
#
|
|
331
|
+
# @see https://modelcontextprotocol.io/specification/latest/basic/utilities/ping
|
|
332
|
+
def ping
|
|
333
|
+
result = request(method: Methods::PING)["result"]
|
|
334
|
+
raise ValidationError, "Response validation failed: missing or invalid `result`" unless result.is_a?(Hash)
|
|
335
|
+
|
|
336
|
+
result
|
|
337
|
+
end
|
|
338
|
+
|
|
166
339
|
private
|
|
167
340
|
|
|
168
341
|
def request(method:, params: nil)
|
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
|