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 +4 -4
- data/README.md +230 -126
- data/lib/json_rpc_handler.rb +16 -9
- data/lib/mcp/configuration.rb +38 -2
- data/lib/mcp/content.rb +16 -12
- data/lib/mcp/instrumentation.rb +23 -2
- data/lib/mcp/methods.rb +3 -2
- data/lib/mcp/prompt/result.rb +4 -3
- data/lib/mcp/resource/contents.rb +8 -7
- data/lib/mcp/resource.rb +4 -2
- data/lib/mcp/resource_template.rb +4 -2
- data/lib/mcp/server/transports/streamable_http_transport.rb +16 -2
- data/lib/mcp/server.rb +27 -45
- data/lib/mcp/server_context.rb +36 -0
- data/lib/mcp/server_session.rb +29 -0
- data/lib/mcp/tool/response.rb +4 -3
- data/lib/mcp/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f6a956181733036c09b8431db1d1da47e3757165123efd5d5dab5fe1cc522e6a
|
|
4
|
+
data.tar.gz: fc611e55e4f88e9ca61ad64d4d2578c6e5a9680ceb4d667337787098ca59bbde
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
####
|
|
107
|
+
#### Streamable HTTP Transport
|
|
107
108
|
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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.
|
|
163
|
-
|
|
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
|
|
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.
|
|
183
|
-
|
|
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
|
|
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
|
|
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`:
|
|
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
|
-
#####
|
|
314
|
+
##### Around Request
|
|
275
315
|
|
|
276
|
-
The
|
|
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
|
-
|
|
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
|
|
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/configuration.rb
CHANGED
|
@@ -7,11 +7,18 @@ module MCP
|
|
|
7
7
|
LATEST_STABLE_PROTOCOL_VERSION, "2025-06-18", "2025-03-26", "2024-11-05",
|
|
8
8
|
]
|
|
9
9
|
|
|
10
|
-
attr_writer :exception_reporter, :
|
|
10
|
+
attr_writer :exception_reporter, :around_request
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
# @deprecated Use {#around_request=} instead. `instrumentation_callback`
|
|
13
|
+
# fires only after a request completes and cannot wrap execution in a
|
|
14
|
+
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
|
|
15
|
+
# @see #around_request=
|
|
16
|
+
attr_writer :instrumentation_callback
|
|
17
|
+
|
|
18
|
+
def initialize(exception_reporter: nil, around_request: nil, instrumentation_callback: nil, protocol_version: nil,
|
|
13
19
|
validate_tool_call_arguments: true)
|
|
14
20
|
@exception_reporter = exception_reporter
|
|
21
|
+
@around_request = around_request
|
|
15
22
|
@instrumentation_callback = instrumentation_callback
|
|
16
23
|
@protocol_version = protocol_version
|
|
17
24
|
if protocol_version
|
|
@@ -50,10 +57,24 @@ module MCP
|
|
|
50
57
|
!@exception_reporter.nil?
|
|
51
58
|
end
|
|
52
59
|
|
|
60
|
+
def around_request
|
|
61
|
+
@around_request || default_around_request
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def around_request?
|
|
65
|
+
!@around_request.nil?
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# @deprecated Use {#around_request} instead. `instrumentation_callback`
|
|
69
|
+
# fires only after a request completes and cannot wrap execution in a
|
|
70
|
+
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
|
|
71
|
+
# @see #around_request
|
|
53
72
|
def instrumentation_callback
|
|
54
73
|
@instrumentation_callback || default_instrumentation_callback
|
|
55
74
|
end
|
|
56
75
|
|
|
76
|
+
# @deprecated Use {#around_request?} instead.
|
|
77
|
+
# @see #around_request?
|
|
57
78
|
def instrumentation_callback?
|
|
58
79
|
!@instrumentation_callback.nil?
|
|
59
80
|
end
|
|
@@ -72,20 +93,30 @@ module MCP
|
|
|
72
93
|
else
|
|
73
94
|
@exception_reporter
|
|
74
95
|
end
|
|
96
|
+
|
|
97
|
+
around_request = if other.around_request?
|
|
98
|
+
other.around_request
|
|
99
|
+
else
|
|
100
|
+
@around_request
|
|
101
|
+
end
|
|
102
|
+
|
|
75
103
|
instrumentation_callback = if other.instrumentation_callback?
|
|
76
104
|
other.instrumentation_callback
|
|
77
105
|
else
|
|
78
106
|
@instrumentation_callback
|
|
79
107
|
end
|
|
108
|
+
|
|
80
109
|
protocol_version = if other.protocol_version?
|
|
81
110
|
other.protocol_version
|
|
82
111
|
else
|
|
83
112
|
@protocol_version
|
|
84
113
|
end
|
|
114
|
+
|
|
85
115
|
validate_tool_call_arguments = other.validate_tool_call_arguments
|
|
86
116
|
|
|
87
117
|
Configuration.new(
|
|
88
118
|
exception_reporter: exception_reporter,
|
|
119
|
+
around_request: around_request,
|
|
89
120
|
instrumentation_callback: instrumentation_callback,
|
|
90
121
|
protocol_version: protocol_version,
|
|
91
122
|
validate_tool_call_arguments: validate_tool_call_arguments,
|
|
@@ -111,6 +142,11 @@ module MCP
|
|
|
111
142
|
@default_exception_reporter ||= ->(exception, server_context) {}
|
|
112
143
|
end
|
|
113
144
|
|
|
145
|
+
def default_around_request
|
|
146
|
+
@default_around_request ||= ->(_data, &request_handler) { request_handler.call }
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
# @deprecated Use {#default_around_request} instead.
|
|
114
150
|
def default_instrumentation_callback
|
|
115
151
|
@default_instrumentation_callback ||= ->(data) {}
|
|
116
152
|
end
|
data/lib/mcp/content.rb
CHANGED
|
@@ -3,56 +3,60 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
module Content
|
|
5
5
|
class Text
|
|
6
|
-
attr_reader :text, :annotations
|
|
6
|
+
attr_reader :text, :annotations, :meta
|
|
7
7
|
|
|
8
|
-
def initialize(text, annotations: nil)
|
|
8
|
+
def initialize(text, annotations: nil, meta: nil)
|
|
9
9
|
@text = text
|
|
10
10
|
@annotations = annotations
|
|
11
|
+
@meta = meta
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
|
-
{ text: text, annotations: annotations, type: "text" }.compact
|
|
15
|
+
{ text: text, annotations: annotations, _meta: meta, type: "text" }.compact
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
class Image
|
|
19
|
-
attr_reader :data, :mime_type, :annotations
|
|
20
|
+
attr_reader :data, :mime_type, :annotations, :meta
|
|
20
21
|
|
|
21
|
-
def initialize(data, mime_type, annotations: nil)
|
|
22
|
+
def initialize(data, mime_type, annotations: nil, meta: nil)
|
|
22
23
|
@data = data
|
|
23
24
|
@mime_type = mime_type
|
|
24
25
|
@annotations = annotations
|
|
26
|
+
@meta = meta
|
|
25
27
|
end
|
|
26
28
|
|
|
27
29
|
def to_h
|
|
28
|
-
{ data: data, mimeType: mime_type, annotations: annotations, type: "image" }.compact
|
|
30
|
+
{ data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "image" }.compact
|
|
29
31
|
end
|
|
30
32
|
end
|
|
31
33
|
|
|
32
34
|
class Audio
|
|
33
|
-
attr_reader :data, :mime_type, :annotations
|
|
35
|
+
attr_reader :data, :mime_type, :annotations, :meta
|
|
34
36
|
|
|
35
|
-
def initialize(data, mime_type, annotations: nil)
|
|
37
|
+
def initialize(data, mime_type, annotations: nil, meta: nil)
|
|
36
38
|
@data = data
|
|
37
39
|
@mime_type = mime_type
|
|
38
40
|
@annotations = annotations
|
|
41
|
+
@meta = meta
|
|
39
42
|
end
|
|
40
43
|
|
|
41
44
|
def to_h
|
|
42
|
-
{ data: data, mimeType: mime_type, annotations: annotations, type: "audio" }.compact
|
|
45
|
+
{ data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "audio" }.compact
|
|
43
46
|
end
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
class EmbeddedResource
|
|
47
|
-
attr_reader :resource, :annotations
|
|
50
|
+
attr_reader :resource, :annotations, :meta
|
|
48
51
|
|
|
49
|
-
def initialize(resource, annotations: nil)
|
|
52
|
+
def initialize(resource, annotations: nil, meta: nil)
|
|
50
53
|
@resource = resource
|
|
51
54
|
@annotations = annotations
|
|
55
|
+
@meta = meta
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
def to_h
|
|
55
|
-
{ resource: resource.to_h, annotations: annotations, type: "resource" }.compact
|
|
59
|
+
{ resource: resource.to_h, annotations: annotations, _meta: meta, type: "resource" }.compact
|
|
56
60
|
end
|
|
57
61
|
end
|
|
58
62
|
end
|
data/lib/mcp/instrumentation.rb
CHANGED
|
@@ -2,19 +2,40 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
module Instrumentation
|
|
5
|
-
def instrument_call(method, &block)
|
|
5
|
+
def instrument_call(method, server_context: {}, exception_already_reported: nil, &block)
|
|
6
6
|
start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
7
7
|
begin
|
|
8
8
|
@instrumentation_data = {}
|
|
9
9
|
add_instrumentation_data(method: method)
|
|
10
10
|
|
|
11
|
-
result =
|
|
11
|
+
result = configuration.around_request.call(@instrumentation_data, &block)
|
|
12
12
|
|
|
13
13
|
result
|
|
14
|
+
rescue => e
|
|
15
|
+
already_reported = begin
|
|
16
|
+
!!exception_already_reported&.call(e)
|
|
17
|
+
# rubocop:disable Lint/RescueException
|
|
18
|
+
rescue Exception
|
|
19
|
+
# rubocop:enable Lint/RescueException
|
|
20
|
+
# The predicate is expected to be side-effect-free and return a boolean.
|
|
21
|
+
# Any exception raised from it (including non-StandardError such as SystemExit)
|
|
22
|
+
# must not shadow the original exception.
|
|
23
|
+
false
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
unless already_reported
|
|
27
|
+
add_instrumentation_data(error: :internal_error) unless @instrumentation_data.key?(:error)
|
|
28
|
+
configuration.exception_reporter.call(e, server_context)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
raise
|
|
14
32
|
ensure
|
|
15
33
|
end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
|
16
34
|
add_instrumentation_data(duration: end_time - start_time)
|
|
17
35
|
|
|
36
|
+
# Backward compatibility: `instrumentation_callback` is soft-deprecated
|
|
37
|
+
# in favor of `around_request`, but existing callers still expect it
|
|
38
|
+
# to fire after every request until it is removed in a future version.
|
|
18
39
|
configuration.instrumentation_callback.call(@instrumentation_data)
|
|
19
40
|
end
|
|
20
41
|
end
|
data/lib/mcp/methods.rb
CHANGED
|
@@ -33,6 +33,7 @@ module MCP
|
|
|
33
33
|
NOTIFICATIONS_MESSAGE = "notifications/message"
|
|
34
34
|
NOTIFICATIONS_PROGRESS = "notifications/progress"
|
|
35
35
|
NOTIFICATIONS_CANCELLED = "notifications/cancelled"
|
|
36
|
+
NOTIFICATIONS_ELICITATION_COMPLETE = "notifications/elicitation/complete"
|
|
36
37
|
|
|
37
38
|
class MissingRequiredCapabilityError < StandardError
|
|
38
39
|
attr_reader :method
|
|
@@ -79,8 +80,8 @@ module MCP
|
|
|
79
80
|
require_capability!(method, capabilities, :sampling)
|
|
80
81
|
when ELICITATION_CREATE
|
|
81
82
|
require_capability!(method, capabilities, :elicitation)
|
|
82
|
-
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED
|
|
83
|
-
# No specific capability required
|
|
83
|
+
when INITIALIZE, PING, NOTIFICATIONS_INITIALIZED, NOTIFICATIONS_PROGRESS, NOTIFICATIONS_CANCELLED, NOTIFICATIONS_ELICITATION_COMPLETE
|
|
84
|
+
# No specific capability required.
|
|
84
85
|
end
|
|
85
86
|
end
|
|
86
87
|
|
data/lib/mcp/prompt/result.rb
CHANGED
|
@@ -3,15 +3,16 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Prompt
|
|
5
5
|
class Result
|
|
6
|
-
attr_reader :description, :messages
|
|
6
|
+
attr_reader :description, :messages, :meta
|
|
7
7
|
|
|
8
|
-
def initialize(description: nil, messages: [])
|
|
8
|
+
def initialize(description: nil, messages: [], meta: nil)
|
|
9
9
|
@description = description
|
|
10
10
|
@messages = messages
|
|
11
|
+
@meta = meta
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
|
-
{ description: description, messages: messages.map(&:to_h) }.compact
|
|
15
|
+
{ description: description, messages: messages.map(&:to_h), _meta: meta }.compact
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
end
|
|
@@ -3,23 +3,24 @@
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Resource
|
|
5
5
|
class Contents
|
|
6
|
-
attr_reader :uri, :mime_type
|
|
6
|
+
attr_reader :uri, :mime_type, :meta
|
|
7
7
|
|
|
8
|
-
def initialize(uri:, mime_type: nil)
|
|
8
|
+
def initialize(uri:, mime_type: nil, meta: nil)
|
|
9
9
|
@uri = uri
|
|
10
10
|
@mime_type = mime_type
|
|
11
|
+
@meta = meta
|
|
11
12
|
end
|
|
12
13
|
|
|
13
14
|
def to_h
|
|
14
|
-
{ uri: uri, mimeType: mime_type }.compact
|
|
15
|
+
{ uri: uri, mimeType: mime_type, _meta: meta }.compact
|
|
15
16
|
end
|
|
16
17
|
end
|
|
17
18
|
|
|
18
19
|
class TextContents < Contents
|
|
19
20
|
attr_reader :text
|
|
20
21
|
|
|
21
|
-
def initialize(text:, uri:, mime_type:)
|
|
22
|
-
super(uri: uri, mime_type: mime_type)
|
|
22
|
+
def initialize(text:, uri:, mime_type:, meta: nil)
|
|
23
|
+
super(uri: uri, mime_type: mime_type, meta: meta)
|
|
23
24
|
@text = text
|
|
24
25
|
end
|
|
25
26
|
|
|
@@ -31,8 +32,8 @@ module MCP
|
|
|
31
32
|
class BlobContents < Contents
|
|
32
33
|
attr_reader :data
|
|
33
34
|
|
|
34
|
-
def initialize(data:, uri:, mime_type:)
|
|
35
|
-
super(uri: uri, mime_type: mime_type)
|
|
35
|
+
def initialize(data:, uri:, mime_type:, meta: nil)
|
|
36
|
+
super(uri: uri, mime_type: mime_type, meta: meta)
|
|
36
37
|
@data = data
|
|
37
38
|
end
|
|
38
39
|
|
data/lib/mcp/resource.rb
CHANGED
|
@@ -5,15 +5,16 @@ require_relative "resource/embedded"
|
|
|
5
5
|
|
|
6
6
|
module MCP
|
|
7
7
|
class Resource
|
|
8
|
-
attr_reader :uri, :name, :title, :description, :icons, :mime_type
|
|
8
|
+
attr_reader :uri, :name, :title, :description, :icons, :mime_type, :meta
|
|
9
9
|
|
|
10
|
-
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil)
|
|
10
|
+
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
11
11
|
@uri = uri
|
|
12
12
|
@name = name
|
|
13
13
|
@title = title
|
|
14
14
|
@description = description
|
|
15
15
|
@icons = icons
|
|
16
16
|
@mime_type = mime_type
|
|
17
|
+
@meta = meta
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def to_h
|
|
@@ -24,6 +25,7 @@ module MCP
|
|
|
24
25
|
description: description,
|
|
25
26
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
26
27
|
mimeType: mime_type,
|
|
28
|
+
_meta: meta,
|
|
27
29
|
}.compact
|
|
28
30
|
end
|
|
29
31
|
end
|
|
@@ -2,15 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class ResourceTemplate
|
|
5
|
-
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type
|
|
5
|
+
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :meta
|
|
6
6
|
|
|
7
|
-
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil)
|
|
7
|
+
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil)
|
|
8
8
|
@uri_template = uri_template
|
|
9
9
|
@name = name
|
|
10
10
|
@title = title
|
|
11
11
|
@description = description
|
|
12
12
|
@icons = icons
|
|
13
13
|
@mime_type = mime_type
|
|
14
|
+
@meta = meta
|
|
14
15
|
end
|
|
15
16
|
|
|
16
17
|
def to_h
|
|
@@ -21,6 +22,7 @@ module MCP
|
|
|
21
22
|
description: description,
|
|
22
23
|
icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
|
|
23
24
|
mimeType: mime_type,
|
|
25
|
+
_meta: meta,
|
|
24
26
|
}.compact
|
|
25
27
|
end
|
|
26
28
|
end
|
|
@@ -3,6 +3,15 @@
|
|
|
3
3
|
require "json"
|
|
4
4
|
require_relative "../../transport"
|
|
5
5
|
|
|
6
|
+
# This file is autoloaded only when `StreamableHTTPTransport` is referenced,
|
|
7
|
+
# so the `rack` dependency does not affect `StdioTransport` users.
|
|
8
|
+
begin
|
|
9
|
+
require "rack"
|
|
10
|
+
rescue LoadError
|
|
11
|
+
raise LoadError, "The 'rack' gem is required to use the StreamableHTTPTransport. " \
|
|
12
|
+
"Add it to your Gemfile: gem 'rack'"
|
|
13
|
+
end
|
|
14
|
+
|
|
6
15
|
module MCP
|
|
7
16
|
class Server
|
|
8
17
|
module Transports
|
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
data/lib/mcp/server_context.rb
CHANGED
|
@@ -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, ...)
|
data/lib/mcp/server_session.rb
CHANGED
|
@@ -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 = {
|
data/lib/mcp/tool/response.rb
CHANGED
|
@@ -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
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.
|
|
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.
|
|
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
|