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.
@@ -117,20 +117,27 @@ module JsonRpcHandler
117
117
  end
118
118
 
119
119
  def handle_request_error(error, id, id_validation_pattern)
120
- error_type = error.respond_to?(:error_type) ? error.error_type : nil
121
-
122
- code, message = case error_type
123
- when :invalid_request then [ErrorCode::INVALID_REQUEST, "Invalid Request"]
124
- when :invalid_params then [ErrorCode::INVALID_PARAMS, "Invalid params"]
125
- when :parse_error then [ErrorCode::PARSE_ERROR, "Parse error"]
126
- when :internal_error then [ErrorCode::INTERNAL_ERROR, "Internal error"]
127
- else [ErrorCode::INTERNAL_ERROR, "Internal error"]
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: error.message,
140
+ data: data,
134
141
  })
135
142
  end
136
143
 
@@ -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
- validate_response_content_type!(response, method, params)
22
- response.body
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
- raise RequestHandlerError.new(
46
- "The #{method} request is not found",
47
- { method: method, params: params },
48
- error_type: :not_found,
49
- original_error: e,
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 validate_response_content_type!(response, method, params)
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
- return if content_type&.include?("application/json")
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
- "Unsupported Content-Type: #{content_type.inspect}. This client only supports JSON responses.",
219
+ "No valid JSON-RPC response found in SSE stream",
101
220
  { method: method, params: params },
102
- error_type: :unsupported_media_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 the list of tools available from the server.
47
- # Each call will make a new request – the result is not cached.
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
- response = request(method: "tools/list")
106
+ # TODO: consider renaming to `list_all_tools`.
107
+ all_tools = []
108
+ seen = Set.new
109
+ cursor = nil
58
110
 
59
- response.dig("result", "tools")&.map do |tool|
60
- Tool.new(
61
- name: tool["name"],
62
- description: tool["description"],
63
- input_schema: tool["inputSchema"],
64
- )
65
- end || []
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 the list of resources available from the server.
69
- # Each call will make a new request – the result is not cached.
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
- response = request(method: "resources/list")
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
- response.dig("result", "resources") || []
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 the list of resource templates available from the server.
79
- # Each call will make a new request the result is not cached.
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
- response = request(method: "resources/templates/list")
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
- response.dig("result", "resourceTemplates") || []
220
+ ListPromptsResult.new(
221
+ prompts: result["prompts"] || [],
222
+ next_cursor: result["nextCursor"],
223
+ meta: result["_meta"],
224
+ )
86
225
  end
87
226
 
88
- # Returns the list of prompts available from the server.
89
- # Each call will make a new request the result is not cached.
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
- response = request(method: "prompts/list")
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
- response.dig("result", "prompts") || []
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)
@@ -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, :instrumentation_callback
10
+ attr_writer :exception_reporter, :around_request
11
11
 
12
- def initialize(exception_reporter: nil, instrumentation_callback: nil, protocol_version: nil,
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