mcp 0.12.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4bd75cccadc2edeac35f1784b485b51437d5e504dd9dda10f436b263a813f7b4
4
- data.tar.gz: f8b241425d0f10a292718572ccb2f68119b7d58bc14c9da46e6e3f8603a44b1c
3
+ metadata.gz: f6a956181733036c09b8431db1d1da47e3757165123efd5d5dab5fe1cc522e6a
4
+ data.tar.gz: fc611e55e4f88e9ca61ad64d4d2578c6e5a9680ceb4d667337787098ca59bbde
5
5
  SHA512:
6
- metadata.gz: 8008f588d3053237d5c6eec6520979a456fc5dc7ab26f7db9f93eeda7ff72f3f0aeb67d591a590cd260e2281ef601df99a950cb62f75f53f2085a65acf02a8ae
7
- data.tar.gz: 2c7af049ec323b39e72ab046731e698319897b3e84f1625d8a3ca735a1775a1ab04baaf207e5e6ab0da9c274165653b13f23d9908d19771c5e247795935e910a
6
+ metadata.gz: '010991cbd4a1b18214d12d8ce3cb09b3658cba4a1cf72c7475ff76688fe87eef4137bd76eb5ebd7b15309e1aec7ed84909125858accd474f052e1a915ec37ad8'
7
+ data.tar.gz: 5b9b2994a4fb3769bda007a39ce0cf02c2a9b4fbeeecb48dc74aa4ca66e4d6e066c545f5d473b339561af06cf14f8e546a6e87a92d4d9c0939fe90858cddb3a8
data/README.md CHANGED
@@ -53,6 +53,7 @@ It implements the Model Context Protocol specification, handling model context r
53
53
  - `resources/templates/list` - Lists all registered resource templates and their schemas
54
54
  - `completion/complete` - Returns autocompletion suggestions for prompt arguments and resource URIs
55
55
  - `sampling/createMessage` - Requests LLM completion from the client (server-to-client)
56
+ - `elicitation/create` - Requests user input from the client (server-to-client)
56
57
 
57
58
  ### Usage
58
59
 
@@ -103,14 +104,56 @@ $ ruby examples/stdio_server.rb
103
104
  {"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
104
105
  ```
105
106
 
106
- #### Rails Controller
107
+ #### Streamable HTTP Transport
107
108
 
108
- When added to a Rails controller on a route that handles POST requests, your server will be compliant with non-streaming
109
- [Streamable HTTP](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http) transport
110
- requests.
109
+ `MCP::Server::Transports::StreamableHTTPTransport` is a standard Rack app, so it can be mounted in any Rack-compatible framework.
110
+ The following examples show two common integration styles in Rails.
111
111
 
112
- You can use `StreamableHTTPTransport#handle_request` to handle requests with proper HTTP
113
- status codes (e.g., 202 Accepted for notifications).
112
+ > [!IMPORTANT]
113
+ > `MCP::Server::Transports::StreamableHTTPTransport` stores session and SSE stream state in memory,
114
+ > so it must run in a single process. Use a single-process server (e.g., Puma with `workers 0`).
115
+ > Multi-process configurations (Unicorn, or Puma with `workers > 0`) fork separate processes that
116
+ > do not share memory, which breaks session management and SSE connections.
117
+ >
118
+ > When running multiple server instances behind a load balancer, configure your load balancer to use
119
+ > sticky sessions (session affinity) so that requests with the same `Mcp-Session-Id` header are always
120
+ > routed to the same instance.
121
+ >
122
+ > Stateless mode (`stateless: true`) does not use sessions and works with any server configuration.
123
+
124
+ ##### Rails (mount)
125
+
126
+ `StreamableHTTPTransport` is a Rack app that can be mounted directly in Rails routes:
127
+
128
+ ```ruby
129
+ # config/routes.rb
130
+ server = MCP::Server.new(
131
+ name: "my_server",
132
+ title: "Example Server Display Name",
133
+ version: "1.0.0",
134
+ instructions: "Use the tools of this server as a last resort",
135
+ tools: [SomeTool, AnotherTool],
136
+ prompts: [MyPrompt],
137
+ )
138
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
139
+
140
+ Rails.application.routes.draw do
141
+ mount transport => "/mcp"
142
+ end
143
+ ```
144
+
145
+ `mount` directs all HTTP methods on `/mcp` to the transport. `StreamableHTTPTransport` internally dispatches
146
+ `POST` (client-to-server JSON-RPC messages, with responses optionally streamed via SSE),
147
+ `GET` (optional standalone SSE stream for server-to-client messages), and `DELETE` (session termination) per
148
+ the [MCP Streamable HTTP transport spec](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http),
149
+ so no additional route configuration is needed.
150
+
151
+ ##### Rails (controller)
152
+
153
+ While the mount approach creates a single server at boot time, the controller approach creates a new server per request.
154
+ This allows you to customize tools, prompts, or configuration based on the request (e.g., different tools per route).
155
+
156
+ `StreamableHTTPTransport#handle_request` returns proper HTTP status codes (e.g., 202 Accepted for notifications):
114
157
 
115
158
  ```ruby
116
159
  class McpController < ActionController::API
@@ -133,18 +176,6 @@ class McpController < ActionController::API
133
176
  end
134
177
  ```
135
178
 
136
- > [!IMPORTANT]
137
- > `MCP::Server::Transports::StreamableHTTPTransport` stores session and SSE stream state in memory,
138
- > so it must run in a single process. Use a single-process server (e.g., Puma with `workers 0`).
139
- > Multi-process configurations (Unicorn, or Puma with `workers > 0`) fork separate processes that
140
- > do not share memory, which breaks session management and SSE connections.
141
- >
142
- > When running multiple server instances behind a load balancer, configure your load balancer to use
143
- > sticky sessions (session affinity) so that requests with the same `Mcp-Session-Id` header are always
144
- > routed to the same instance.
145
- >
146
- > Stateless mode (`stateless: true`) does not use sessions and works with any server configuration.
147
-
148
179
  ### Configuration
149
180
 
150
181
  The gem can be configured using the `MCP.configure` block:
@@ -159,15 +190,17 @@ MCP.configure do |config|
159
190
  end
160
191
  }
161
192
 
162
- config.instrumentation_callback = ->(data) {
163
- puts "Got instrumentation data #{data.inspect}"
193
+ config.around_request = ->(data, &request_handler) {
194
+ logger.info("Start: #{data[:method]}")
195
+ request_handler.call
196
+ logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
164
197
  }
165
198
  end
166
199
  ```
167
200
 
168
201
  or by creating an explicit configuration and passing it into the server.
169
202
  This is useful for systems where an application hosts more than one MCP server but
170
- they might require different instrumentation callbacks.
203
+ they might require different configurations.
171
204
 
172
205
  ```ruby
173
206
  configuration = MCP::Configuration.new
@@ -179,8 +212,10 @@ configuration.exception_reporter = ->(exception, server_context) {
179
212
  end
180
213
  }
181
214
 
182
- configuration.instrumentation_callback = ->(data) {
183
- puts "Got instrumentation data #{data.inspect}"
215
+ configuration.around_request = ->(data, &request_handler) {
216
+ logger.info("Start: #{data[:method]}")
217
+ request_handler.call
218
+ logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
184
219
  }
185
220
 
186
221
  server = MCP::Server.new(
@@ -193,7 +228,8 @@ server = MCP::Server.new(
193
228
 
194
229
  #### `server_context`
195
230
 
196
- The `server_context` is a user-defined hash that is passed into the server instance and made available to tools, prompts, and exception/instrumentation callbacks. It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
231
+ The `server_context` is a user-defined hash that is passed into the server instance and made available to tool and prompt calls.
232
+ It can be used to provide contextual information such as authentication state, user IDs, or request-specific data.
197
233
 
198
234
  **Type:**
199
235
 
@@ -210,7 +246,9 @@ server = MCP::Server.new(
210
246
  )
211
247
  ```
212
248
 
213
- This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
249
+ This hash is then passed as the `server_context` keyword argument to tool and prompt calls.
250
+ Note that exception and instrumentation callbacks do not receive this user-defined hash.
251
+ See the relevant sections below for the arguments they receive.
214
252
 
215
253
  #### Request-specific `_meta` Parameter
216
254
 
@@ -263,7 +301,9 @@ end
263
301
  The exception reporter receives:
264
302
 
265
303
  - `exception`: The Ruby exception object that was raised
266
- - `server_context`: The context hash provided to the server
304
+ - `server_context`: A hash describing where the failure occurred (e.g., `{ request: <raw JSON-RPC request> }`
305
+ for request handling, `{ notification: "tools_list_changed" }` for notification delivery).
306
+ This is not the user-defined `server_context` passed to `Server.new`.
267
307
 
268
308
  **Signature:**
269
309
 
@@ -271,9 +311,67 @@ The exception reporter receives:
271
311
  exception_reporter = ->(exception, server_context) { ... }
272
312
  ```
273
313
 
274
- ##### Instrumentation Callback
314
+ ##### Around Request
275
315
 
276
- The instrumentation callback receives a hash with the following possible keys:
316
+ The `around_request` hook wraps request handling, allowing you to execute code before and after each request.
317
+ This is useful for Application Performance Monitoring (APM) tracing, logging, or other observability needs.
318
+
319
+ The hook receives a `data` hash and a `request_handler` block. You must call `request_handler.call` to execute the request:
320
+
321
+ **Signature:**
322
+
323
+ ```ruby
324
+ around_request = ->(data, &request_handler) { request_handler.call }
325
+ ```
326
+
327
+ **`data` availability by timing:**
328
+
329
+ - Before `request_handler.call`: `method`
330
+ - After `request_handler.call`: `tool_name`, `tool_arguments`, `prompt_name`, `resource_uri`, `error`, `client`
331
+ - Not available inside `around_request`: `duration` (added after `around_request` returns)
332
+
333
+ > [!NOTE]
334
+ > `tool_name`, `prompt_name` and `resource_uri` may only be populated for the corresponding request methods
335
+ > (`tools/call`, `prompts/get`, `resources/read`), and may not be set depending on how the request is handled
336
+ > (for example, `prompt_name` is not recorded when the prompt is not found).
337
+ > `duration` is added after `around_request` returns, so it is not visible from within the hook.
338
+
339
+ **Example:**
340
+
341
+ ```ruby
342
+ MCP.configure do |config|
343
+ config.around_request = ->(data, &request_handler) {
344
+ logger.info("Start: #{data[:method]}")
345
+ request_handler.call
346
+ logger.info("Done: #{data[:method]}, tool: #{data[:tool_name]}")
347
+ }
348
+ end
349
+ ```
350
+
351
+ ##### Instrumentation Callback (soft-deprecated)
352
+
353
+ > [!NOTE]
354
+ > `instrumentation_callback` is soft-deprecated. Use `around_request` instead.
355
+ >
356
+ > To migrate, wrap the call in `begin/ensure` so the callback still runs when the request fails:
357
+ >
358
+ > ```ruby
359
+ > # Before
360
+ > config.instrumentation_callback = ->(data) { log(data) }
361
+ >
362
+ > # After
363
+ > config.around_request = ->(data, &request_handler) do
364
+ > request_handler.call
365
+ > ensure
366
+ > log(data)
367
+ > end
368
+ > ```
369
+ >
370
+ > Note that `data[:duration]` is not available inside `around_request`.
371
+ > If you need it, measure elapsed time yourself within the hook, or keep using `instrumentation_callback`.
372
+
373
+ The instrumentation callback is called after each request finishes, whether successfully or with an error.
374
+ It receives a hash with the following possible keys:
277
375
 
278
376
  - `method`: (String) The protocol method called (e.g., "ping", "tools/list")
279
377
  - `tool_name`: (String, optional) The name of the tool called
@@ -284,25 +382,10 @@ The instrumentation callback receives a hash with the following possible keys:
284
382
  - `duration`: (Float) Duration of the call in seconds
285
383
  - `client`: (Hash, optional) Client information with `name` and `version` keys, from the initialize request
286
384
 
287
- > [!NOTE]
288
- > `tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
289
- > This is to avoid potential issues with metric cardinality.
290
-
291
- **Type:**
385
+ **Signature:**
292
386
 
293
387
  ```ruby
294
388
  instrumentation_callback = ->(data) { ... }
295
- # where data is a Hash with keys as described above
296
- ```
297
-
298
- **Example:**
299
-
300
- ```ruby
301
- MCP.configure do |config|
302
- config.instrumentation_callback = ->(data) {
303
- puts "Instrumentation: #{data.inspect}"
304
- }
305
- end
306
389
  ```
307
390
 
308
391
  ### Server Protocol Version
@@ -823,8 +906,7 @@ This enables servers to leverage the client's LLM capabilities without needing d
823
906
  **Using Sampling in Tools:**
824
907
 
825
908
  Tools that accept a `server_context:` parameter can call `create_sampling_message` on it.
826
- The request is automatically routed to the correct client session.
827
- Set `server.server_context = server` so that `server_context.create_sampling_message` delegates to the server:
909
+ The request is automatically routed to the correct client session:
828
910
 
829
911
  ```ruby
830
912
  class SummarizeTool < MCP::Tool
@@ -852,7 +934,6 @@ class SummarizeTool < MCP::Tool
852
934
  end
853
935
 
854
936
  server = MCP::Server.new(name: "my_server", tools: [SummarizeTool])
855
- server.server_context = server
856
937
  ```
857
938
 
858
939
  **Parameters:**
@@ -873,86 +954,8 @@ Optional:
873
954
  - `tools:` (Array) - Tools available to the LLM (requires `sampling.tools` capability)
874
955
  - `tool_choice:` (Hash) - Tool selection mode (e.g., `{ mode: "auto" }`)
875
956
 
876
- **Direct Usage:**
877
-
878
- `Server#create_sampling_message` can also be called directly outside of tools:
879
-
880
- ```ruby
881
- result = server.create_sampling_message(
882
- messages: [
883
- { role: "user", content: { type: "text", text: "What is the capital of France?" } }
884
- ],
885
- max_tokens: 100,
886
- system_prompt: "You are a helpful assistant.",
887
- temperature: 0.7
888
- )
889
- ```
890
-
891
- Result contains the LLM response:
892
-
893
- ```ruby
894
- {
895
- role: "assistant",
896
- content: { type: "text", text: "The capital of France is Paris." },
897
- model: "claude-3-sonnet-20240307",
898
- stopReason: "endTurn"
899
- }
900
- ```
901
-
902
- For multi-client transports (e.g., `StreamableHTTPTransport`), use `server_context.create_sampling_message` inside tools
903
- to route the request to the correct client session.
904
-
905
- **Tool Use in Sampling:**
906
-
907
- When tools are provided in a sampling request, the LLM can call them during generation.
908
- The server must handle tool calls and continue the conversation with tool results:
909
-
910
- ```ruby
911
- result = server.create_sampling_message(
912
- messages: [
913
- { role: "user", content: { type: "text", text: "What's the weather in Paris?" } }
914
- ],
915
- max_tokens: 1000,
916
- tools: [
917
- {
918
- name: "get_weather",
919
- description: "Get weather for a city",
920
- inputSchema: {
921
- type: "object",
922
- properties: { city: { type: "string" } },
923
- required: ["city"]
924
- }
925
- }
926
- ],
927
- tool_choice: { mode: "auto" }
928
- )
929
-
930
- if result[:stopReason] == "toolUse"
931
- tool_results = result[:content].map do |tool_use|
932
- weather_data = get_weather(tool_use[:input][:city])
933
-
934
- {
935
- type: "tool_result",
936
- toolUseId: tool_use[:id],
937
- content: [{ type: "text", text: weather_data.to_json }]
938
- }
939
- end
940
-
941
- final_result = server.create_sampling_message(
942
- messages: [
943
- { role: "user", content: { type: "text", text: "What's the weather in Paris?" } },
944
- { role: "assistant", content: result[:content] },
945
- { role: "user", content: tool_results }
946
- ],
947
- max_tokens: 1000,
948
- tools: [...]
949
- )
950
- end
951
- ```
952
-
953
957
  **Error Handling:**
954
958
 
955
- - Raises `RuntimeError` if transport is not set
956
959
  - Raises `RuntimeError` if client does not support `sampling` capability
957
960
  - Raises `RuntimeError` if `tools` are used but client lacks `sampling.tools` capability
958
961
  - Raises `StandardError` if client returns an error response
@@ -1085,6 +1088,108 @@ The SDK automatically enforces the 100-item limit per the MCP specification.
1085
1088
  The server validates that the referenced prompt, resource, or resource template is registered before calling the handler.
1086
1089
  Requests for unknown references return an error.
1087
1090
 
1091
+ ### Elicitation
1092
+
1093
+ The MCP Ruby SDK supports [elicitation](https://modelcontextprotocol.io/specification/2025-11-25/client/elicitation),
1094
+ which allows servers to request additional information from users through the client during tool execution.
1095
+
1096
+ Elicitation is a **server-to-client request**. The server sends a request and blocks until the user responds via the client.
1097
+
1098
+ #### Capabilities
1099
+
1100
+ Clients must declare the `elicitation` capability during initialization. The server checks this before sending any elicitation request
1101
+ and raises a `RuntimeError` if the client does not support it.
1102
+
1103
+ For URL mode support, the client must also declare `elicitation.url` capability.
1104
+
1105
+ #### Using Elicitation in Tools
1106
+
1107
+ Tools that accept a `server_context:` parameter can call `create_form_elicitation` on it:
1108
+
1109
+ ```ruby
1110
+ server.define_tool(name: "collect_info", description: "Collect user info") do |server_context:|
1111
+ result = server_context.create_form_elicitation(
1112
+ message: "Please provide your name",
1113
+ requested_schema: {
1114
+ type: "object",
1115
+ properties: { name: { type: "string" } },
1116
+ required: ["name"],
1117
+ },
1118
+ )
1119
+
1120
+ MCP::Tool::Response.new([{ type: "text", text: "Hello, #{result[:content][:name]}" }])
1121
+ end
1122
+ ```
1123
+
1124
+ #### Form Mode
1125
+
1126
+ Form mode collects structured data from the user directly through the MCP client:
1127
+
1128
+ ```ruby
1129
+ server.define_tool(name: "collect_contact", description: "Collect contact info") do |server_context:|
1130
+ result = server_context.create_form_elicitation(
1131
+ message: "Please provide your contact information",
1132
+ requested_schema: {
1133
+ type: "object",
1134
+ properties: {
1135
+ name: { type: "string", description: "Your full name" },
1136
+ email: { type: "string", format: "email", description: "Your email address" },
1137
+ },
1138
+ required: ["name", "email"],
1139
+ },
1140
+ )
1141
+
1142
+ text = case result[:action]
1143
+ when "accept"
1144
+ "Hello, #{result[:content][:name]} (#{result[:content][:email]})"
1145
+ when "decline"
1146
+ "User declined"
1147
+ when "cancel"
1148
+ "User cancelled"
1149
+ end
1150
+
1151
+ MCP::Tool::Response.new([{ type: "text", text: text }])
1152
+ end
1153
+ ```
1154
+
1155
+ #### URL Mode
1156
+
1157
+ URL mode directs the user to an external URL for out-of-band interactions such as OAuth flows:
1158
+
1159
+ ```ruby
1160
+ server.define_tool(name: "authorize_github", description: "Authorize GitHub") do |server_context:|
1161
+ elicitation_id = SecureRandom.uuid
1162
+
1163
+ result = server_context.create_url_elicitation(
1164
+ message: "Please authorize access to your GitHub account",
1165
+ url: "https://example.com/oauth/authorize?elicitation_id=#{elicitation_id}",
1166
+ elicitation_id: elicitation_id,
1167
+ )
1168
+
1169
+ server_context.notify_elicitation_complete(elicitation_id: elicitation_id)
1170
+
1171
+ MCP::Tool::Response.new([{ type: "text", text: "Authorization complete" }])
1172
+ end
1173
+ ```
1174
+
1175
+ #### URLElicitationRequiredError
1176
+
1177
+ When a tool cannot proceed until an out-of-band elicitation is completed, raise `MCP::Server::URLElicitationRequiredError`.
1178
+ This returns a JSON-RPC error with code `-32042` to the client:
1179
+
1180
+ ```ruby
1181
+ server.define_tool(name: "access_github", description: "Access GitHub") do |server_context:|
1182
+ raise MCP::Server::URLElicitationRequiredError.new([
1183
+ {
1184
+ mode: "url",
1185
+ elicitationId: SecureRandom.uuid,
1186
+ url: "https://example.com/oauth/authorize",
1187
+ message: "GitHub authorization is required.",
1188
+ },
1189
+ ])
1190
+ end
1191
+ ```
1192
+
1088
1193
  ### Logging
1089
1194
 
1090
1195
  The MCP Ruby SDK supports structured logging through the `notify_log_message` method, following the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
@@ -1250,7 +1355,6 @@ end
1250
1355
  ### Unsupported Features (to be implemented in future versions)
1251
1356
 
1252
1357
  - Resource subscriptions
1253
- - Elicitation
1254
1358
 
1255
1359
  ## Building an MCP Client
1256
1360
 
@@ -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
 
@@ -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
@@ -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 = yield block
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 for initialize, ping, progress or cancelled
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
 
@@ -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
@@ -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"
@@ -546,7 +560,7 @@ module MCP
546
560
  end
547
561
  end
548
562
 
549
- [200, SSE_HEADERS, body]
563
+ [200, SSE_HEADERS.dup, body]
550
564
  end
551
565
 
552
566
  # Returns the SSE stream available for server-to-client messages.
@@ -628,7 +642,7 @@ module MCP
628
642
  def setup_sse_stream(session_id)
629
643
  body = create_sse_body(session_id)
630
644
 
631
- [200, SSE_HEADERS, body]
645
+ [200, SSE_HEADERS.dup, body]
632
646
  end
633
647
 
634
648
  def create_sse_body(session_id)
data/lib/mcp/server.rb CHANGED
@@ -31,14 +31,27 @@ module MCP
31
31
  MAX_COMPLETION_VALUES = 100
32
32
 
33
33
  class RequestHandlerError < StandardError
34
- attr_reader :error_type
35
- attr_reader :original_error
34
+ attr_reader :error_type, :original_error, :error_code, :error_data
36
35
 
37
- def initialize(message, request, error_type: :internal_error, original_error: nil)
36
+ def initialize(message, request, error_type: :internal_error, original_error: nil, error_code: nil, error_data: nil)
38
37
  super(message)
39
38
  @request = request
40
39
  @error_type = error_type
41
40
  @original_error = original_error
41
+ @error_code = error_code
42
+ @error_data = error_data
43
+ end
44
+ end
45
+
46
+ class URLElicitationRequiredError < RequestHandlerError
47
+ def initialize(elicitations)
48
+ super(
49
+ "URL elicitation required",
50
+ nil,
51
+ error_type: :url_elicitation_required,
52
+ error_code: -32042,
53
+ error_data: { elicitations: elicitations },
54
+ )
42
55
  end
43
56
  end
44
57
 
@@ -114,7 +127,6 @@ module MCP
114
127
  # No op handlers for currently unsupported methods
115
128
  Methods::RESOURCES_SUBSCRIBE => ->(_) { {} },
116
129
  Methods::RESOURCES_UNSUBSCRIBE => ->(_) { {} },
117
- Methods::ELICITATION_CREATE => ->(_) {},
118
130
  }
119
131
  @transport = transport
120
132
  end
@@ -206,44 +218,6 @@ module MCP
206
218
  report_exception(e, { notification: "log_message" })
207
219
  end
208
220
 
209
- # Sends a `sampling/createMessage` request to the client.
210
- # For single-client transports (e.g., `StdioTransport`). For multi-client transports
211
- # (e.g., `StreamableHTTPTransport`), use `ServerSession#create_sampling_message` instead
212
- # to ensure the request is routed to the correct client.
213
- def create_sampling_message(
214
- messages:,
215
- max_tokens:,
216
- system_prompt: nil,
217
- model_preferences: nil,
218
- include_context: nil,
219
- temperature: nil,
220
- stop_sequences: nil,
221
- metadata: nil,
222
- tools: nil,
223
- tool_choice: nil,
224
- related_request_id: nil
225
- )
226
- unless @transport
227
- raise "Cannot send sampling request without a transport."
228
- end
229
-
230
- params = build_sampling_params(
231
- @client_capabilities,
232
- messages: messages,
233
- max_tokens: max_tokens,
234
- system_prompt: system_prompt,
235
- model_preferences: model_preferences,
236
- include_context: include_context,
237
- temperature: temperature,
238
- stop_sequences: stop_sequences,
239
- metadata: metadata,
240
- tools: tools,
241
- tool_choice: tool_choice,
242
- )
243
-
244
- @transport.send_request(Methods::SAMPLING_CREATE_MESSAGE, params)
245
- end
246
-
247
221
  # Sets a custom handler for `resources/read` requests.
248
222
  # The block receives the parsed request params and should return resource
249
223
  # contents. The return value is set as the `contents` field of the response.
@@ -375,7 +349,7 @@ module MCP
375
349
  def handle_request(request, method, session: nil, related_request_id: nil)
376
350
  handler = @handlers[method]
377
351
  unless handler
378
- instrument_call("unsupported_method") do
352
+ instrument_call("unsupported_method", server_context: { request: request }) do
379
353
  client = session&.client || @client
380
354
  add_instrumentation_data(client: client) if client
381
355
  end
@@ -385,7 +359,12 @@ module MCP
385
359
  Methods.ensure_capability!(method, capabilities)
386
360
 
387
361
  ->(params) {
388
- instrument_call(method) do
362
+ reported_exception = nil
363
+ instrument_call(
364
+ method,
365
+ server_context: { request: request },
366
+ exception_already_reported: ->(e) { reported_exception.equal?(e) },
367
+ ) do
389
368
  result = case method
390
369
  when Methods::INITIALIZE
391
370
  init(params, session: session)
@@ -415,11 +394,14 @@ module MCP
415
394
  rescue RequestHandlerError => e
416
395
  report_exception(e.original_error || e, { request: request })
417
396
  add_instrumentation_data(error: e.error_type)
397
+ reported_exception = e
418
398
  raise e
419
399
  rescue => e
420
400
  report_exception(e, { request: request })
421
401
  add_instrumentation_data(error: :internal_error)
422
- raise RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
402
+ wrapped = RequestHandlerError.new("Internal error handling #{method} request", request, original_error: e)
403
+ reported_exception = wrapped
404
+ raise wrapped
423
405
  end
424
406
  }
425
407
  end
@@ -43,6 +43,42 @@ module MCP
43
43
  end
44
44
  end
45
45
 
46
+ # Delegates to the session so the request is scoped to the originating client.
47
+ # Falls back to `@context` (via `method_missing`) when `@notification_target`
48
+ # does not support elicitation.
49
+ def create_form_elicitation(**kwargs)
50
+ if @notification_target.respond_to?(:create_form_elicitation)
51
+ @notification_target.create_form_elicitation(**kwargs, related_request_id: @related_request_id)
52
+ elsif @context.respond_to?(:create_form_elicitation)
53
+ @context.create_form_elicitation(**kwargs, related_request_id: @related_request_id)
54
+ else
55
+ raise NoMethodError, "undefined method 'create_form_elicitation' for #{self}"
56
+ end
57
+ end
58
+
59
+ # Delegates to the session so the request is scoped to the originating client.
60
+ # Falls back to `@context` when `@notification_target` does not support URL mode elicitation.
61
+ def create_url_elicitation(**kwargs)
62
+ if @notification_target.respond_to?(:create_url_elicitation)
63
+ @notification_target.create_url_elicitation(**kwargs, related_request_id: @related_request_id)
64
+ elsif @context.respond_to?(:create_url_elicitation)
65
+ @context.create_url_elicitation(**kwargs, related_request_id: @related_request_id)
66
+ else
67
+ raise NoMethodError, "undefined method 'create_url_elicitation' for #{self}"
68
+ end
69
+ end
70
+
71
+ # Delegates to the session so the notification is scoped to the originating client.
72
+ def notify_elicitation_complete(**kwargs)
73
+ if @notification_target.respond_to?(:notify_elicitation_complete)
74
+ @notification_target.notify_elicitation_complete(**kwargs)
75
+ elsif @context.respond_to?(:notify_elicitation_complete)
76
+ @context.notify_elicitation_complete(**kwargs)
77
+ else
78
+ raise NoMethodError, "undefined method 'notify_elicitation_complete' for #{self}"
79
+ end
80
+ end
81
+
46
82
  def method_missing(name, ...)
47
83
  if @context.respond_to?(name)
48
84
  @context.public_send(name, ...)
@@ -47,6 +47,35 @@ module MCP
47
47
  send_to_transport_request(Methods::SAMPLING_CREATE_MESSAGE, params, related_request_id: related_request_id)
48
48
  end
49
49
 
50
+ # Sends an `elicitation/create` request (form mode) scoped to this session.
51
+ def create_form_elicitation(message:, requested_schema:, related_request_id: nil)
52
+ unless client_capabilities&.dig(:elicitation)
53
+ raise "Client does not support elicitation. " \
54
+ "The client must declare the `elicitation` capability during initialization."
55
+ end
56
+
57
+ params = { mode: "form", message: message, requestedSchema: requested_schema }
58
+ send_to_transport_request(Methods::ELICITATION_CREATE, params, related_request_id: related_request_id)
59
+ end
60
+
61
+ # Sends an `elicitation/create` request (URL mode) scoped to this session.
62
+ def create_url_elicitation(message:, url:, elicitation_id:, related_request_id: nil)
63
+ unless client_capabilities&.dig(:elicitation, :url)
64
+ raise "Client does not support URL mode elicitation. " \
65
+ "The client must declare the `elicitation.url` capability during initialization."
66
+ end
67
+
68
+ params = { mode: "url", message: message, url: url, elicitationId: elicitation_id }
69
+ send_to_transport_request(Methods::ELICITATION_CREATE, params, related_request_id: related_request_id)
70
+ end
71
+
72
+ # Sends an elicitation complete notification scoped to this session.
73
+ def notify_elicitation_complete(elicitation_id:)
74
+ send_to_transport(Methods::NOTIFICATIONS_ELICITATION_COMPLETE, { elicitationId: elicitation_id })
75
+ rescue => e
76
+ @server.report_exception(e, notification: "elicitation_complete")
77
+ end
78
+
50
79
  # Sends a progress notification to this session only.
51
80
  def notify_progress(progress_token:, progress:, total: nil, message: nil, related_request_id: nil)
52
81
  params = {
@@ -5,9 +5,9 @@ module MCP
5
5
  class Response
6
6
  NOT_GIVEN = Object.new.freeze
7
7
 
8
- attr_reader :content, :structured_content
8
+ attr_reader :content, :structured_content, :meta
9
9
 
10
- def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, structured_content: nil)
10
+ def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, structured_content: nil, meta: nil)
11
11
  if deprecated_error != NOT_GIVEN
12
12
  warn("Passing `error` with the 2nd argument of `Response.new` is deprecated. Use keyword argument like `Response.new(content, error: error)` instead.", uplevel: 1)
13
13
  error = deprecated_error
@@ -16,6 +16,7 @@ module MCP
16
16
  @content = content || []
17
17
  @error = error
18
18
  @structured_content = structured_content
19
+ @meta = meta
19
20
  end
20
21
 
21
22
  def error?
@@ -23,7 +24,7 @@ module MCP
23
24
  end
24
25
 
25
26
  def to_h
26
- { content: content, isError: error?, structuredContent: @structured_content }.compact
27
+ { content: content, isError: error?, structuredContent: @structured_content, _meta: meta }.compact
27
28
  end
28
29
  end
29
30
  end
data/lib/mcp/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MCP
4
- VERSION = "0.12.0"
4
+ VERSION = "0.13.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.12.0
4
+ version: 0.13.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Model Context Protocol
@@ -78,7 +78,7 @@ licenses:
78
78
  - Apache-2.0
79
79
  metadata:
80
80
  allowed_push_host: https://rubygems.org
81
- changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.12.0
81
+ changelog_uri: https://github.com/modelcontextprotocol/ruby-sdk/releases/tag/v0.13.0
82
82
  homepage_uri: https://github.com/modelcontextprotocol/ruby-sdk
83
83
  source_code_uri: https://github.com/modelcontextprotocol/ruby-sdk
84
84
  bug_tracker_uri: https://github.com/modelcontextprotocol/ruby-sdk/issues