mcp 0.11.0 → 0.12.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.
data/README.md CHANGED
@@ -54,1208 +54,1204 @@ It implements the Model Context Protocol specification, handling model context r
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
56
 
57
- ### Custom Methods
57
+ ### Usage
58
58
 
59
- The server allows you to define custom JSON-RPC methods beyond the standard MCP protocol methods using the `define_custom_method` method:
59
+ #### Stdio Transport
60
+
61
+ If you want to build a local command-line application, you can use the stdio transport:
60
62
 
61
63
  ```ruby
62
- server = MCP::Server.new(name: "my_server")
64
+ require "mcp"
63
65
 
64
- # Define a custom method that returns a result
65
- server.define_custom_method(method_name: "add") do |params|
66
- params[:a] + params[:b]
67
- end
66
+ # Create a simple tool
67
+ class ExampleTool < MCP::Tool
68
+ description "A simple example tool that echoes back its arguments"
69
+ input_schema(
70
+ properties: {
71
+ message: { type: "string" },
72
+ },
73
+ required: ["message"]
74
+ )
68
75
 
69
- # Define a custom notification method (returns nil)
70
- server.define_custom_method(method_name: "notify") do |params|
71
- # Process notification
72
- nil
76
+ class << self
77
+ def call(message:, server_context:)
78
+ MCP::Tool::Response.new([{
79
+ type: "text",
80
+ text: "Hello from example tool! Message: #{message}",
81
+ }])
82
+ end
83
+ end
73
84
  end
74
- ```
75
-
76
- **Key Features:**
77
85
 
78
- - Accepts any method name as a string
79
- - Block receives the request parameters as a hash
80
- - Can handle both regular methods (with responses) and notifications
81
- - Prevents overriding existing MCP protocol methods
82
- - Supports instrumentation callbacks for monitoring
86
+ # Set up the server
87
+ server = MCP::Server.new(
88
+ name: "example_server",
89
+ tools: [ExampleTool],
90
+ )
83
91
 
84
- **Usage Example:**
92
+ # Create and start the transport
93
+ transport = MCP::Server::Transports::StdioTransport.new(server)
94
+ transport.open
95
+ ```
85
96
 
86
- ```ruby
87
- # Client request
88
- {
89
- "jsonrpc": "2.0",
90
- "id": 1,
91
- "method": "add",
92
- "params": { "a": 5, "b": 3 }
93
- }
97
+ You can run this script and then type in requests to the server at the command line.
94
98
 
95
- # Server response
96
- {
97
- "jsonrpc": "2.0",
98
- "id": 1,
99
- "result": 8
100
- }
99
+ ```console
100
+ $ ruby examples/stdio_server.rb
101
+ {"jsonrpc":"2.0","id":"1","method":"ping"}
102
+ {"jsonrpc":"2.0","id":"2","method":"tools/list"}
103
+ {"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
101
104
  ```
102
105
 
103
- **Error Handling:**
106
+ #### Rails Controller
104
107
 
105
- - Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
106
- - Supports the same exception reporting and instrumentation as standard methods
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.
107
111
 
108
- ### Sampling
112
+ You can use `StreamableHTTPTransport#handle_request` to handle requests with proper HTTP
113
+ status codes (e.g., 202 Accepted for notifications).
109
114
 
110
- The Model Context Protocol allows servers to request LLM completions from clients through the `sampling/createMessage` method.
111
- This enables servers to leverage the client's LLM capabilities without needing direct access to AI models.
115
+ ```ruby
116
+ class McpController < ActionController::API
117
+ def create
118
+ server = MCP::Server.new(
119
+ name: "my_server",
120
+ title: "Example Server Display Name",
121
+ version: "1.0.0",
122
+ instructions: "Use the tools of this server as a last resort",
123
+ tools: [SomeTool, AnotherTool],
124
+ prompts: [MyPrompt],
125
+ server_context: { user_id: current_user.id },
126
+ )
127
+ # Since the `MCP-Session-Id` is not shared across requests, `stateless: true` is set.
128
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
129
+ status, headers, body = transport.handle_request(request)
112
130
 
113
- **Key Concepts:**
131
+ render(json: body.first, status: status, headers: headers)
132
+ end
133
+ end
134
+ ```
114
135
 
115
- - **Server-to-Client Request**: Unlike typical MCP methods (client→server), sampling is initiated by the server
116
- - **Client Capability**: Clients must declare `sampling` capability during initialization
117
- - **Tool Support**: When using tools in sampling requests, clients must declare `sampling.tools` capability
118
- - **Human-in-the-Loop**: Clients can implement user approval before forwarding requests to LLMs
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.
119
147
 
120
- **Usage Example (Stdio transport):**
148
+ ### Configuration
121
149
 
122
- `Server#create_sampling_message` is for single-client transports (e.g., `StdioTransport`).
123
- For multi-client transports (e.g., `StreamableHTTPTransport`), use `server_context.create_sampling_message` inside tools instead,
124
- which routes the request to the correct client session.
150
+ The gem can be configured using the `MCP.configure` block:
125
151
 
126
152
  ```ruby
127
- server = MCP::Server.new(name: "my_server")
128
- transport = MCP::Server::Transports::StdioTransport.new(server)
129
- server.transport = transport
153
+ MCP.configure do |config|
154
+ config.exception_reporter = ->(exception, server_context) {
155
+ # Your exception reporting logic here
156
+ # For example with Bugsnag:
157
+ Bugsnag.notify(exception) do |report|
158
+ report.add_metadata(:model_context_protocol, server_context)
159
+ end
160
+ }
161
+
162
+ config.instrumentation_callback = ->(data) {
163
+ puts "Got instrumentation data #{data.inspect}"
164
+ }
165
+ end
130
166
  ```
131
167
 
132
- Client must declare sampling capability during initialization.
133
- This happens automatically when the client connects.
168
+ or by creating an explicit configuration and passing it into the server.
169
+ This is useful for systems where an application hosts more than one MCP server but
170
+ they might require different instrumentation callbacks.
134
171
 
135
172
  ```ruby
136
- result = server.create_sampling_message(
137
- messages: [
138
- { role: "user", content: { type: "text", text: "What is the capital of France?" } }
139
- ],
140
- max_tokens: 100,
141
- system_prompt: "You are a helpful assistant.",
142
- temperature: 0.7
173
+ configuration = MCP::Configuration.new
174
+ configuration.exception_reporter = ->(exception, server_context) {
175
+ # Your exception reporting logic here
176
+ # For example with Bugsnag:
177
+ Bugsnag.notify(exception) do |report|
178
+ report.add_metadata(:model_context_protocol, server_context)
179
+ end
180
+ }
181
+
182
+ configuration.instrumentation_callback = ->(data) {
183
+ puts "Got instrumentation data #{data.inspect}"
184
+ }
185
+
186
+ server = MCP::Server.new(
187
+ # ... all other options
188
+ configuration:,
143
189
  )
144
190
  ```
145
191
 
146
- Result contains the LLM response:
192
+ ### Server Context and Configuration Block Data
193
+
194
+ #### `server_context`
195
+
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.
197
+
198
+ **Type:**
147
199
 
148
200
  ```ruby
149
- {
150
- role: "assistant",
151
- content: { type: "text", text: "The capital of France is Paris." },
152
- model: "claude-3-sonnet-20240307",
153
- stopReason: "endTurn"
154
- }
201
+ server_context: { [String, Symbol] => Any }
155
202
  ```
156
203
 
157
- **Parameters:**
204
+ **Example:**
158
205
 
159
- Required:
206
+ ```ruby
207
+ server = MCP::Server.new(
208
+ name: "my_server",
209
+ server_context: { user_id: current_user.id, request_id: request.uuid }
210
+ )
211
+ ```
160
212
 
161
- - `messages:` (Array) - Array of message objects with `role` and `content`
162
- - `max_tokens:` (Integer) - Maximum tokens in the response
213
+ This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
163
214
 
164
- Optional:
215
+ #### Request-specific `_meta` Parameter
165
216
 
166
- - `system_prompt:` (String) - System prompt for the LLM
167
- - `model_preferences:` (Hash) - Model selection preferences (e.g., `{ intelligencePriority: 0.8 }`)
168
- - `include_context:` (String) - Context inclusion: `"none"`, `"thisServer"`, or `"allServers"` (soft-deprecated)
169
- - `temperature:` (Float) - Sampling temperature
170
- - `stop_sequences:` (Array) - Sequences that stop generation
171
- - `metadata:` (Hash) - Additional metadata
172
- - `tools:` (Array) - Tools available to the LLM (requires `sampling.tools` capability)
173
- - `tool_choice:` (Hash) - Tool selection mode (e.g., `{ mode: "auto" }`)
217
+ The MCP protocol supports a special [`_meta` parameter](https://modelcontextprotocol.io/specification/2025-06-18/basic#general-fields) in requests that allows clients to pass request-specific metadata. The server automatically extracts this parameter and makes it available to tools and prompts as a nested field within the `server_context`.
174
218
 
175
- **Using Sampling in Tools (works with both Stdio and HTTP transports):**
219
+ **Access Pattern:**
176
220
 
177
- Tools that accept a `server_context:` parameter can call `create_sampling_message` on it.
178
- The request is automatically routed to the correct client session.
179
- Set `server.server_context = server` so that `server_context.create_sampling_message` delegates to the server:
221
+ When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
180
222
 
181
223
  ```ruby
182
- class SummarizeTool < MCP::Tool
183
- description "Summarize text using LLM"
184
- input_schema(
185
- properties: {
186
- text: { type: "string" }
187
- },
188
- required: ["text"]
189
- )
224
+ class MyTool < MCP::Tool
225
+ def self.call(message:, server_context:)
226
+ # Access provider-specific metadata
227
+ session_id = server_context.dig(:_meta, :session_id)
228
+ request_id = server_context.dig(:_meta, :request_id)
190
229
 
191
- def self.call(text:, server_context:)
192
- result = server_context.create_sampling_message(
193
- messages: [
194
- { role: "user", content: { type: "text", text: "Please summarize: #{text}" } }
195
- ],
196
- max_tokens: 500
197
- )
230
+ # Access server's original context
231
+ user_id = server_context.dig(:user_id)
198
232
 
199
233
  MCP::Tool::Response.new([{
200
234
  type: "text",
201
- text: result[:content][:text]
235
+ text: "Processing for user #{user_id} in session #{session_id}"
202
236
  }])
203
237
  end
204
238
  end
239
+ ```
205
240
 
206
- server = MCP::Server.new(name: "my_server", tools: [SummarizeTool])
207
- server.server_context = server
241
+ **Client Request Example:**
242
+
243
+ ```json
244
+ {
245
+ "jsonrpc": "2.0",
246
+ "id": 1,
247
+ "method": "tools/call",
248
+ "params": {
249
+ "name": "my_tool",
250
+ "arguments": { "message": "Hello" },
251
+ "_meta": {
252
+ "session_id": "abc123",
253
+ "request_id": "req_456"
254
+ }
255
+ }
256
+ }
208
257
  ```
209
258
 
210
- **Tool Use in Sampling:**
259
+ #### Configuration Block Data
211
260
 
212
- When tools are provided in a sampling request, the LLM can call them during generation.
213
- The server must handle tool calls and continue the conversation with tool results:
261
+ ##### Exception Reporter
262
+
263
+ The exception reporter receives:
264
+
265
+ - `exception`: The Ruby exception object that was raised
266
+ - `server_context`: The context hash provided to the server
267
+
268
+ **Signature:**
214
269
 
215
270
  ```ruby
216
- result = server.create_sampling_message(
217
- messages: [
218
- { role: "user", content: { type: "text", text: "What's the weather in Paris?" } }
219
- ],
220
- max_tokens: 1000,
221
- tools: [
222
- {
223
- name: "get_weather",
224
- description: "Get weather for a city",
225
- inputSchema: {
226
- type: "object",
227
- properties: { city: { type: "string" } },
228
- required: ["city"]
229
- }
230
- }
231
- ],
232
- tool_choice: { mode: "auto" }
233
- )
271
+ exception_reporter = ->(exception, server_context) { ... }
272
+ ```
234
273
 
235
- if result[:stopReason] == "toolUse"
236
- tool_results = result[:content].map do |tool_use|
237
- weather_data = get_weather(tool_use[:input][:city])
274
+ ##### Instrumentation Callback
238
275
 
239
- {
240
- type: "tool_result",
241
- toolUseId: tool_use[:id],
242
- content: [{ type: "text", text: weather_data.to_json }]
243
- }
244
- end
276
+ The instrumentation callback receives a hash with the following possible keys:
245
277
 
246
- final_result = server.create_sampling_message(
247
- messages: [
248
- { role: "user", content: { type: "text", text: "What's the weather in Paris?" } },
249
- { role: "assistant", content: result[:content] },
250
- { role: "user", content: tool_results }
251
- ],
252
- max_tokens: 1000,
253
- tools: [...]
254
- )
278
+ - `method`: (String) The protocol method called (e.g., "ping", "tools/list")
279
+ - `tool_name`: (String, optional) The name of the tool called
280
+ - `tool_arguments`: (Hash, optional) The arguments passed to the tool
281
+ - `prompt_name`: (String, optional) The name of the prompt called
282
+ - `resource_uri`: (String, optional) The URI of the resource called
283
+ - `error`: (String, optional) Error code if a lookup failed
284
+ - `duration`: (Float) Duration of the call in seconds
285
+ - `client`: (Hash, optional) Client information with `name` and `version` keys, from the initialize request
286
+
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:**
292
+
293
+ ```ruby
294
+ 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
+ }
255
305
  end
256
306
  ```
257
307
 
258
- **Error Handling:**
308
+ ### Server Protocol Version
259
309
 
260
- - Raises `RuntimeError` if transport is not set
261
- - Raises `RuntimeError` if client does not support `sampling` capability
262
- - Raises `RuntimeError` if `tools` are used but client lacks `sampling.tools` capability
263
- - Raises `StandardError` if client returns an error response
310
+ The server's protocol version can be overridden using the `protocol_version` keyword argument:
264
311
 
265
- ### Notifications
312
+ ```ruby
313
+ configuration = MCP::Configuration.new(protocol_version: "2024-11-05")
314
+ MCP::Server.new(name: "test_server", configuration: configuration)
315
+ ```
266
316
 
267
- The server supports sending notifications to clients when lists of tools, prompts, or resources change. This enables real-time updates without polling.
317
+ If no protocol version is specified, the latest stable version will be applied by default.
318
+ The latest stable version includes new features from the [draft version](https://modelcontextprotocol.io/specification/draft).
268
319
 
269
- #### Notification Methods
320
+ This will make all new server instances use the specified protocol version instead of the default version. The protocol version can be reset to the default by setting it to `nil`:
270
321
 
271
- The server provides the following notification methods:
322
+ ```ruby
323
+ MCP::Configuration.new(protocol_version: nil)
324
+ ```
272
325
 
273
- - `notify_tools_list_changed` - Send a notification when the tools list changes
274
- - `notify_prompts_list_changed` - Send a notification when the prompts list changes
275
- - `notify_resources_list_changed` - Send a notification when the resources list changes
276
- - `notify_log_message` - Send a structured logging notification message
326
+ If an invalid `protocol_version` value is set, an `ArgumentError` is raised.
277
327
 
278
- #### Session Scoping
328
+ Be sure to check the [MCP spec](https://modelcontextprotocol.io/specification/versioning) for the protocol version to understand the supported features for the version being set.
279
329
 
280
- When using Streamable HTTP transport with multiple clients, each client connection gets its own session. Notifications are scoped as follows:
330
+ ### Exception Reporting
281
331
 
282
- - **`report_progress`** and **`notify_log_message`** called via `server_context` inside a tool handler are automatically sent only to the requesting client.
283
- No extra configuration is needed.
284
- - **`notify_tools_list_changed`**, **`notify_prompts_list_changed`**, and **`notify_resources_list_changed`** are always broadcast to all connected clients,
285
- as they represent server-wide state changes. These should be called on the `server` instance directly.
332
+ The exception reporter receives two arguments:
286
333
 
287
- #### Notification Format
334
+ - `exception`: The Ruby exception object that was raised
335
+ - `server_context`: A hash containing contextual information about where the error occurred
288
336
 
289
- Notifications follow the JSON-RPC 2.0 specification and use these method names:
337
+ The server_context hash includes:
290
338
 
291
- - `notifications/tools/list_changed`
292
- - `notifications/prompts/list_changed`
293
- - `notifications/resources/list_changed`
294
- - `notifications/progress`
295
- - `notifications/message`
339
+ - For tool calls: `{ tool_name: "name", arguments: { ... } }`
340
+ - For general request handling: `{ request: { ... } }`
296
341
 
297
- ### Progress
342
+ When an exception occurs:
298
343
 
299
- The MCP Ruby SDK supports progress tracking for long-running tool operations,
300
- following the [MCP Progress specification](https://modelcontextprotocol.io/specification/latest/server/utilities/progress).
344
+ 1. The exception is reported via the configured reporter
345
+ 2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", isError: true }`
346
+ 3. For other requests, the exception is re-raised after reporting
301
347
 
302
- #### How Progress Works
348
+ If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
303
349
 
304
- 1. **Client Request**: The client sends a `progressToken` in the `_meta` field when calling a tool
305
- 2. **Server Notification**: The server sends `notifications/progress` messages back to the client during tool execution
306
- 3. **Tool Integration**: Tools call `server_context.report_progress` to report incremental progress
350
+ ### Tools
307
351
 
308
- #### Server-Side: Tool with Progress
352
+ MCP spec includes [Tools](https://modelcontextprotocol.io/specification/latest/server/tools) which provide functionality to LLM apps.
309
353
 
310
- Tools that accept a `server_context:` parameter can call `report_progress` on it.
311
- The server automatically wraps the context in an `MCP::ServerContext` instance that provides this method:
354
+ This gem provides a `MCP::Tool` class that can be used to create tools in three ways:
355
+
356
+ 1. As a class definition:
312
357
 
313
358
  ```ruby
314
- class LongRunningTool < MCP::Tool
315
- description "A tool that reports progress during execution"
359
+ class MyTool < MCP::Tool
360
+ title "My Tool"
361
+ description "This tool performs specific functionality..."
316
362
  input_schema(
317
363
  properties: {
318
- count: { type: "integer" },
364
+ message: { type: "string" },
319
365
  },
320
- required: ["count"]
366
+ required: ["message"]
367
+ )
368
+ output_schema(
369
+ properties: {
370
+ result: { type: "string" },
371
+ success: { type: "boolean" },
372
+ timestamp: { type: "string", format: "date-time" }
373
+ },
374
+ required: ["result", "success", "timestamp"]
375
+ )
376
+ annotations(
377
+ read_only_hint: true,
378
+ destructive_hint: false,
379
+ idempotent_hint: true,
380
+ open_world_hint: false,
381
+ title: "My Tool"
321
382
  )
322
383
 
323
- def self.call(count:, server_context:)
324
- count.times do |i|
325
- # Do work here.
326
- server_context.report_progress(i + 1, total: count, message: "Processing item #{i + 1}")
327
- end
328
-
329
- MCP::Tool::Response.new([{ type: "text", text: "Done" }])
384
+ def self.call(message:, server_context:)
385
+ MCP::Tool::Response.new([{ type: "text", text: "OK" }])
330
386
  end
331
387
  end
332
- ```
333
-
334
- The `server_context.report_progress` method accepts:
335
-
336
- - `progress` (required) — current progress value (numeric)
337
- - `total:` (optional) — total expected value, so clients can display a percentage
338
- - `message:` (optional) — human-readable status message
339
-
340
- **Key Features:**
341
-
342
- - Tools report progress via `server_context.report_progress`
343
- - `report_progress` is a no-op when no `progressToken` was provided by the client
344
- - Supports both numeric and string progress tokens
345
-
346
- ### Completions
347
388
 
348
- MCP spec includes [Completions](https://modelcontextprotocol.io/specification/latest/server/utilities/completion),
349
- which enable servers to provide autocompletion suggestions for prompt arguments and resource URIs.
389
+ tool = MyTool
390
+ ```
350
391
 
351
- To enable completions, declare the `completions` capability and register a handler:
392
+ 2. By using the `MCP::Tool.define` method with a block:
352
393
 
353
394
  ```ruby
354
- server = MCP::Server.new(
355
- name: "my_server",
356
- prompts: [CodeReviewPrompt],
357
- resource_templates: [FileTemplate],
358
- capabilities: { completions: {} },
359
- )
395
+ tool = MCP::Tool.define(
396
+ name: "my_tool",
397
+ title: "My Tool",
398
+ description: "This tool performs specific functionality...",
399
+ annotations: {
400
+ read_only_hint: true,
401
+ title: "My Tool"
402
+ }
403
+ ) do |args, server_context:|
404
+ MCP::Tool::Response.new([{ type: "text", text: "OK" }])
405
+ end
406
+ ```
360
407
 
361
- server.completion_handler do |params|
362
- ref = params[:ref]
363
- argument = params[:argument]
364
- value = argument[:value]
408
+ 3. By using the `MCP::Server#define_tool` method with a block:
365
409
 
366
- case ref[:type]
367
- when "ref/prompt"
368
- values = case argument[:name]
369
- when "language"
370
- ["python", "pytorch", "pyside"].select { |v| v.start_with?(value) }
371
- else
372
- []
373
- end
374
- { completion: { values: values, hasMore: false } }
375
- when "ref/resource"
376
- { completion: { values: [], hasMore: false } }
377
- end
410
+ ```ruby
411
+ server = MCP::Server.new
412
+ server.define_tool(
413
+ name: "my_tool",
414
+ description: "This tool performs specific functionality...",
415
+ annotations: {
416
+ title: "My Tool",
417
+ read_only_hint: true
418
+ }
419
+ ) do |args, server_context:|
420
+ Tool::Response.new([{ type: "text", text: "OK" }])
378
421
  end
379
422
  ```
380
423
 
381
- The handler receives a `params` hash with:
424
+ The server_context parameter is the server_context passed into the server and can be used to pass per request information,
425
+ e.g. around authentication state.
382
426
 
383
- - `ref` - The reference (`{ type: "ref/prompt", name: "..." }` or `{ type: "ref/resource", uri: "..." }`)
384
- - `argument` - The argument being completed (`{ name: "...", value: "..." }`)
385
- - `context` (optional) - Previously resolved arguments (`{ arguments: { ... } }`)
427
+ ### Tool Annotations
386
428
 
387
- The handler must return a hash with a `completion` key containing `values` (array of strings), and optionally `total` and `hasMore`.
388
- The SDK automatically enforces the 100-item limit per the MCP specification.
429
+ Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:
389
430
 
390
- The server validates that the referenced prompt, resource, or resource template is registered before calling the handler.
391
- Requests for unknown references return an error.
431
+ - `destructive_hint`: Indicates if the tool performs destructive operations. Defaults to true
432
+ - `idempotent_hint`: Indicates if the tool's operations are idempotent. Defaults to false
433
+ - `open_world_hint`: Indicates if the tool operates in an open world context. Defaults to true
434
+ - `read_only_hint`: Indicates if the tool only reads data (doesn't modify state). Defaults to false
435
+ - `title`: A human-readable title for the tool
392
436
 
393
- ### Logging
437
+ Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
394
438
 
395
- 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).
439
+ > [!NOTE]
440
+ > This **Tool Annotations** feature is supported starting from `protocol_version: '2025-03-26'`.
396
441
 
397
- The `notifications/message` notification is used for structured logging between client and server.
442
+ ### Tool Output Schemas
398
443
 
399
- #### Log Levels
444
+ Tools can optionally define an `output_schema` to specify the expected structure of their results. This works similarly to how `input_schema` is defined and can be used in three ways:
400
445
 
401
- The SDK supports 8 log levels with increasing severity:
402
-
403
- - `debug` - Detailed debugging information
404
- - `info` - General informational messages
405
- - `notice` - Normal but significant events
406
- - `warning` - Warning conditions
407
- - `error` - Error conditions
408
- - `critical` - Critical conditions
409
- - `alert` - Action must be taken immediately
410
- - `emergency` - System is unusable
411
-
412
- #### How Logging Works
413
-
414
- 1. **Client Configuration**: The client sends a `logging/setLevel` request to configure the minimum log level
415
- 2. **Server Filtering**: The server only sends log messages at the configured level or higher severity
416
- 3. **Notification Delivery**: Log messages are sent as `notifications/message` to the client
417
-
418
- For example, if the client sets the level to `"error"` (severity 4), the server will send messages with levels: `error`, `critical`, `alert`, and `emergency`.
419
-
420
- For more details, see the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
421
-
422
- **Usage Example:**
446
+ 1. **Class definition with output_schema:**
423
447
 
424
448
  ```ruby
425
- server = MCP::Server.new(name: "my_server")
426
- transport = MCP::Server::Transports::StdioTransport.new(server)
427
- server.transport = transport
428
-
429
- # The client first configures the logging level (on the client side):
430
- transport.send_request(
431
- request: {
432
- jsonrpc: "2.0",
433
- method: "logging/setLevel",
434
- params: { level: "info" },
435
- id: session_id # Unique request ID within the session
436
- }
437
- )
438
-
439
- # Send log messages at different severity levels
440
- server.notify_log_message(
441
- data: { message: "Application started successfully" },
442
- level: "info"
443
- )
444
-
445
- server.notify_log_message(
446
- data: { message: "Configuration file not found, using defaults" },
447
- level: "warning"
448
- )
449
+ class WeatherTool < MCP::Tool
450
+ tool_name "get_weather"
451
+ description "Get current weather for a location"
449
452
 
450
- server.notify_log_message(
451
- data: {
452
- error: "Database connection failed",
453
- details: { host: "localhost", port: 5432 }
454
- },
455
- level: "error",
456
- logger: "DatabaseLogger" # Optional logger name
457
- )
458
- ```
453
+ input_schema(
454
+ properties: {
455
+ location: { type: "string" },
456
+ units: { type: "string", enum: ["celsius", "fahrenheit"] }
457
+ },
458
+ required: ["location"]
459
+ )
459
460
 
460
- **Key Features:**
461
+ output_schema(
462
+ properties: {
463
+ temperature: { type: "number" },
464
+ condition: { type: "string" },
465
+ humidity: { type: "integer" }
466
+ },
467
+ required: ["temperature", "condition", "humidity"]
468
+ )
461
469
 
462
- - Supports 8 log levels (debug, info, notice, warning, error, critical, alert, emergency) based on https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#log-levels
463
- - Server has capability `logging` to send log messages
464
- - Messages are only sent if a transport is configured
465
- - Messages are filtered based on the client's configured log level
466
- - If the log level hasn't been set by the client, no messages will be sent
470
+ def self.call(location:, units: "celsius", server_context:)
471
+ # Call weather API and structure the response
472
+ api_response = WeatherAPI.fetch(location, units)
473
+ weather_data = {
474
+ temperature: api_response.temp,
475
+ condition: api_response.description,
476
+ humidity: api_response.humidity_percent
477
+ }
467
478
 
468
- #### Transport Support
479
+ output_schema.validate_result(weather_data)
469
480
 
470
- - **stdio**: Notifications are sent as JSON-RPC 2.0 messages to stdout
471
- - **Streamable HTTP**: Notifications are sent as JSON-RPC 2.0 messages over HTTP with streaming (chunked transfer or SSE)
481
+ MCP::Tool::Response.new([{
482
+ type: "text",
483
+ text: weather_data.to_json
484
+ }])
485
+ end
486
+ end
487
+ ```
472
488
 
473
- #### Usage Example
489
+ 2. **Using Tool.define with output_schema:**
474
490
 
475
491
  ```ruby
476
- server = MCP::Server.new(name: "my_server")
477
-
478
- # Default Streamable HTTP - session oriented
479
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
480
-
481
- server.transport = transport
482
-
483
- # When tools change, notify clients
484
- server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
485
- server.notify_tools_list_changed
492
+ tool = MCP::Tool.define(
493
+ name: "calculate_stats",
494
+ description: "Calculate statistics for a dataset",
495
+ input_schema: {
496
+ properties: {
497
+ numbers: { type: "array", items: { type: "number" } }
498
+ },
499
+ required: ["numbers"]
500
+ },
501
+ output_schema: {
502
+ properties: {
503
+ mean: { type: "number" },
504
+ median: { type: "number" },
505
+ count: { type: "integer" }
506
+ },
507
+ required: ["mean", "median", "count"]
508
+ }
509
+ ) do |args, server_context:|
510
+ # Calculate statistics and validate against schema
511
+ MCP::Tool::Response.new([{ type: "text", text: "Statistics calculated" }])
512
+ end
486
513
  ```
487
514
 
488
- You can use Stateless Streamable HTTP, where notifications are not supported and all calls are request/response interactions.
489
- This mode allows for easy multi-node deployment.
490
- Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new` (`stateless` defaults to `false`):
515
+ 3. **Using OutputSchema objects:**
491
516
 
492
517
  ```ruby
493
- # Stateless Streamable HTTP - session-less
494
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
518
+ class DataTool < MCP::Tool
519
+ output_schema MCP::Tool::OutputSchema.new(
520
+ properties: {
521
+ success: { type: "boolean" },
522
+ data: { type: "object" }
523
+ },
524
+ required: ["success"]
525
+ )
526
+ end
495
527
  ```
496
528
 
497
- By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
498
- When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
529
+ Output schema may also describe an array of objects:
499
530
 
500
531
  ```ruby
501
- # Session timeout of 30 minutes
502
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
532
+ class WeatherTool < MCP::Tool
533
+ output_schema(
534
+ type: "array",
535
+ items: {
536
+ properties: {
537
+ temperature: { type: "number" },
538
+ condition: { type: "string" },
539
+ humidity: { type: "integer" }
540
+ },
541
+ required: ["temperature", "condition", "humidity"]
542
+ }
543
+ )
544
+ end
503
545
  ```
504
546
 
505
- ### Unsupported Features (to be implemented in future versions)
547
+ Please note: in this case, you must provide `type: "array"`. The default type
548
+ for output schemas is `object`.
506
549
 
507
- - Resource subscriptions
508
- - Elicitation
550
+ MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/latest/server/tools#output-schema) specifies that:
509
551
 
510
- ### Usage
552
+ - **Server Validation**: Servers MUST provide structured results that conform to the output schema
553
+ - **Client Validation**: Clients SHOULD validate structured results against the output schema
554
+ - **Better Integration**: Enables strict schema validation, type information, and improved developer experience
555
+ - **Backward Compatibility**: Tools returning structured content SHOULD also include serialized JSON in a TextContent block
511
556
 
512
- > [!IMPORTANT]
513
- > `MCP::Server::Transports::StreamableHTTPTransport` stores session and SSE stream state in memory,
514
- > so it must run in a single process. Use a single-process server (e.g., Puma with `workers 0`).
515
- > Multi-process configurations (Unicorn, or Puma with `workers > 0`) fork separate processes that
516
- > do not share memory, which breaks session management and SSE connections.
517
- > Stateless mode (`stateless: true`) does not use sessions and works with any server configuration.
557
+ The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.
518
558
 
519
- #### Rails Controller
559
+ ### Tool Responses with Structured Content
520
560
 
521
- When added to a Rails controller on a route that handles POST requests, your server will be compliant with non-streaming
522
- [Streamable HTTP](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http) transport
523
- requests.
561
+ Tools can return structured data alongside text content using the `structured_content` parameter.
524
562
 
525
- You can use `StreamableHTTPTransport#handle_request` to handle requests with proper HTTP
526
- status codes (e.g., 202 Accepted for notifications).
563
+ The structured content will be included in the JSON-RPC response as the `structuredContent` field.
527
564
 
528
565
  ```ruby
529
- class McpController < ActionController::Base
530
- def create
531
- server = MCP::Server.new(
532
- name: "my_server",
533
- title: "Example Server Display Name",
534
- version: "1.0.0",
535
- instructions: "Use the tools of this server as a last resort",
536
- tools: [SomeTool, AnotherTool],
537
- prompts: [MyPrompt],
538
- server_context: { user_id: current_user.id },
539
- )
540
- transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
541
- server.transport = transport
542
- status, headers, body = transport.handle_request(request)
566
+ class WeatherTool < MCP::Tool
567
+ description "Get current weather and return structured data"
543
568
 
544
- render(json: body.first, status: status, headers: headers)
569
+ def self.call(location:, units: "celsius", server_context:)
570
+ # Call weather API and structure the response
571
+ api_response = WeatherAPI.fetch(location, units)
572
+ weather_data = {
573
+ temperature: api_response.temp,
574
+ condition: api_response.description,
575
+ humidity: api_response.humidity_percent
576
+ }
577
+
578
+ output_schema.validate_result(weather_data)
579
+
580
+ MCP::Tool::Response.new(
581
+ [{
582
+ type: "text",
583
+ text: weather_data.to_json
584
+ }],
585
+ structured_content: weather_data
586
+ )
545
587
  end
546
588
  end
547
589
  ```
548
590
 
549
- #### Stdio Transport
591
+ ### Tool Responses with Errors
550
592
 
551
- If you want to build a local command-line application, you can use the stdio transport:
593
+ Tools can return error information alongside text content using the `error` parameter.
594
+
595
+ The error will be included in the JSON-RPC response as the `isError` field.
552
596
 
553
597
  ```ruby
554
- require "mcp"
598
+ class WeatherTool < MCP::Tool
599
+ description "Get current weather and return structured data"
555
600
 
556
- # Create a simple tool
557
- class ExampleTool < MCP::Tool
558
- description "A simple example tool that echoes back its arguments"
559
- input_schema(
560
- properties: {
561
- message: { type: "string" },
562
- },
563
- required: ["message"]
564
- )
601
+ def self.call(server_context:)
602
+ # Do something here
603
+ content = {}
565
604
 
566
- class << self
567
- def call(message:, server_context:)
568
- MCP::Tool::Response.new([{
605
+ MCP::Tool::Response.new(
606
+ [{
569
607
  type: "text",
570
- text: "Hello from example tool! Message: #{message}",
571
- }])
572
- end
608
+ text: content.to_json
609
+ }],
610
+ structured_content: content,
611
+ error: true
612
+ )
573
613
  end
574
614
  end
615
+ ```
575
616
 
576
- # Set up the server
577
- server = MCP::Server.new(
578
- name: "example_server",
579
- tools: [ExampleTool],
580
- )
581
-
582
- # Create and start the transport
583
- transport = MCP::Server::Transports::StdioTransport.new(server)
584
- transport.open
585
- ```
586
-
587
- You can run this script and then type in requests to the server at the command line.
617
+ ### Prompts
588
618
 
589
- ```console
590
- $ ruby examples/stdio_server.rb
591
- {"jsonrpc":"2.0","id":"1","method":"ping"}
592
- {"jsonrpc":"2.0","id":"2","method":"tools/list"}
593
- {"jsonrpc":"2.0","id":"3","method":"tools/call","params":{"name":"example_tool","arguments":{"message":"Hello"}}}
594
- ```
619
+ MCP spec includes [Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
595
620
 
596
- ### Configuration
621
+ The `MCP::Prompt` class provides three ways to create prompts:
597
622
 
598
- The gem can be configured using the `MCP.configure` block:
623
+ 1. As a class definition with metadata:
599
624
 
600
625
  ```ruby
601
- MCP.configure do |config|
602
- config.exception_reporter = ->(exception, server_context) {
603
- # Your exception reporting logic here
604
- # For example with Bugsnag:
605
- Bugsnag.notify(exception) do |report|
606
- report.add_metadata(:model_context_protocol, server_context)
626
+ class MyPrompt < MCP::Prompt
627
+ prompt_name "my_prompt" # Optional - defaults to underscored class name
628
+ title "My Prompt"
629
+ description "This prompt performs specific functionality..."
630
+ arguments [
631
+ MCP::Prompt::Argument.new(
632
+ name: "message",
633
+ title: "Message Title",
634
+ description: "Input message",
635
+ required: true
636
+ )
637
+ ]
638
+ meta({ version: "1.0", category: "example" })
639
+
640
+ class << self
641
+ def template(args, server_context:)
642
+ MCP::Prompt::Result.new(
643
+ description: "Response description",
644
+ messages: [
645
+ MCP::Prompt::Message.new(
646
+ role: "user",
647
+ content: MCP::Content::Text.new("User message")
648
+ ),
649
+ MCP::Prompt::Message.new(
650
+ role: "assistant",
651
+ content: MCP::Content::Text.new(args["message"])
652
+ )
653
+ ]
654
+ )
607
655
  end
608
- }
656
+ end
657
+ end
609
658
 
610
- config.instrumentation_callback = ->(data) {
611
- puts "Got instrumentation data #{data.inspect}"
612
- }
659
+ prompt = MyPrompt
660
+ ```
661
+
662
+ 2. Using the `MCP::Prompt.define` method:
663
+
664
+ ```ruby
665
+ prompt = MCP::Prompt.define(
666
+ name: "my_prompt",
667
+ title: "My Prompt",
668
+ description: "This prompt performs specific functionality...",
669
+ arguments: [
670
+ MCP::Prompt::Argument.new(
671
+ name: "message",
672
+ title: "Message Title",
673
+ description: "Input message",
674
+ required: true
675
+ )
676
+ ],
677
+ meta: { version: "1.0", category: "example" }
678
+ ) do |args, server_context:|
679
+ MCP::Prompt::Result.new(
680
+ description: "Response description",
681
+ messages: [
682
+ MCP::Prompt::Message.new(
683
+ role: "user",
684
+ content: MCP::Content::Text.new("User message")
685
+ ),
686
+ MCP::Prompt::Message.new(
687
+ role: "assistant",
688
+ content: MCP::Content::Text.new(args["message"])
689
+ )
690
+ ]
691
+ )
613
692
  end
614
693
  ```
615
694
 
616
- or by creating an explicit configuration and passing it into the server.
617
- This is useful for systems where an application hosts more than one MCP server but
618
- they might require different instrumentation callbacks.
695
+ 3. Using the `MCP::Server#define_prompt` method:
619
696
 
620
697
  ```ruby
621
- configuration = MCP::Configuration.new
622
- configuration.exception_reporter = ->(exception, server_context) {
623
- # Your exception reporting logic here
624
- # For example with Bugsnag:
625
- Bugsnag.notify(exception) do |report|
626
- report.add_metadata(:model_context_protocol, server_context)
627
- end
628
- }
698
+ server = MCP::Server.new
699
+ server.define_prompt(
700
+ name: "my_prompt",
701
+ description: "This prompt performs specific functionality...",
702
+ arguments: [
703
+ Prompt::Argument.new(
704
+ name: "message",
705
+ title: "Message Title",
706
+ description: "Input message",
707
+ required: true
708
+ )
709
+ ],
710
+ meta: { version: "1.0", category: "example" }
711
+ ) do |args, server_context:|
712
+ Prompt::Result.new(
713
+ description: "Response description",
714
+ messages: [
715
+ Prompt::Message.new(
716
+ role: "user",
717
+ content: Content::Text.new("User message")
718
+ ),
719
+ Prompt::Message.new(
720
+ role: "assistant",
721
+ content: Content::Text.new(args["message"])
722
+ )
723
+ ]
724
+ )
725
+ end
726
+ ```
629
727
 
630
- configuration.instrumentation_callback = ->(data) {
631
- puts "Got instrumentation data #{data.inspect}"
632
- }
728
+ The server_context parameter is the server_context passed into the server and can be used to pass per request information,
729
+ e.g. around authentication state or user preferences.
730
+
731
+ ### Key Components
633
732
 
733
+ - `MCP::Prompt::Argument` - Defines input parameters for the prompt template with name, title, description, and required flag
734
+ - `MCP::Prompt::Message` - Represents a message in the conversation with a role and content
735
+ - `MCP::Prompt::Result` - The output of a prompt template containing description and messages
736
+ - `MCP::Content::Text` - Text content for messages
737
+
738
+ ### Usage
739
+
740
+ Register prompts with the MCP server:
741
+
742
+ ```ruby
634
743
  server = MCP::Server.new(
635
- # ... all other options
636
- configuration:,
744
+ name: "my_server",
745
+ prompts: [MyPrompt],
746
+ server_context: { user_id: current_user.id },
637
747
  )
638
748
  ```
639
749
 
640
- ### Server Context and Configuration Block Data
750
+ The server will handle prompt listing and execution through the MCP protocol methods:
641
751
 
642
- #### `server_context`
752
+ - `prompts/list` - Lists all registered prompts and their schemas
753
+ - `prompts/get` - Retrieves and executes a specific prompt with arguments
643
754
 
644
- 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.
755
+ ### Resources
645
756
 
646
- **Type:**
757
+ MCP spec includes [Resources](https://modelcontextprotocol.io/specification/latest/server/resources).
758
+
759
+ ### Reading Resources
760
+
761
+ The `MCP::Resource` class provides a way to register resources with the server.
647
762
 
648
763
  ```ruby
649
- server_context: { [String, Symbol] => Any }
764
+ resource = MCP::Resource.new(
765
+ uri: "https://example.com/my_resource",
766
+ name: "my-resource",
767
+ title: "My Resource",
768
+ description: "Lorem ipsum dolor sit amet",
769
+ mime_type: "text/html",
770
+ )
771
+
772
+ server = MCP::Server.new(
773
+ name: "my_server",
774
+ resources: [resource],
775
+ )
650
776
  ```
651
777
 
652
- **Example:**
778
+ The server must register a handler for the `resources/read` method to retrieve a resource dynamically.
779
+
780
+ ```ruby
781
+ server.resources_read_handler do |params|
782
+ [{
783
+ uri: params[:uri],
784
+ mimeType: "text/plain",
785
+ text: "Hello from example resource! URI: #{params[:uri]}"
786
+ }]
787
+ end
788
+ ```
789
+
790
+ otherwise `resources/read` requests will be a no-op.
791
+
792
+ ### Resource Templates
793
+
794
+ The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
653
795
 
654
796
  ```ruby
797
+ resource_template = MCP::ResourceTemplate.new(
798
+ uri_template: "https://example.com/my_resource_template",
799
+ name: "my-resource-template",
800
+ title: "My Resource Template",
801
+ description: "Lorem ipsum dolor sit amet",
802
+ mime_type: "text/html",
803
+ )
804
+
655
805
  server = MCP::Server.new(
656
806
  name: "my_server",
657
- server_context: { user_id: current_user.id, request_id: request.uuid }
807
+ resource_templates: [resource_template],
658
808
  )
659
809
  ```
660
810
 
661
- This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
811
+ ### Sampling
662
812
 
663
- #### Request-specific `_meta` Parameter
813
+ The Model Context Protocol allows servers to request LLM completions from clients through the `sampling/createMessage` method.
814
+ This enables servers to leverage the client's LLM capabilities without needing direct access to AI models.
664
815
 
665
- The MCP protocol supports a special [`_meta` parameter](https://modelcontextprotocol.io/specification/2025-06-18/basic#general-fields) in requests that allows clients to pass request-specific metadata. The server automatically extracts this parameter and makes it available to tools and prompts as a nested field within the `server_context`.
816
+ **Key Concepts:**
666
817
 
667
- **Access Pattern:**
818
+ - **Server-to-Client Request**: Unlike typical MCP methods (client to server), sampling is initiated by the server
819
+ - **Client Capability**: Clients must declare `sampling` capability during initialization
820
+ - **Tool Support**: When using tools in sampling requests, clients must declare `sampling.tools` capability
821
+ - **Human-in-the-Loop**: Clients can implement user approval before forwarding requests to LLMs
668
822
 
669
- When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
823
+ **Using Sampling in Tools:**
824
+
825
+ 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:
670
828
 
671
829
  ```ruby
672
- class MyTool < MCP::Tool
673
- def self.call(message:, server_context:)
674
- # Access provider-specific metadata
675
- session_id = server_context.dig(:_meta, :session_id)
676
- request_id = server_context.dig(:_meta, :request_id)
830
+ class SummarizeTool < MCP::Tool
831
+ description "Summarize text using LLM"
832
+ input_schema(
833
+ properties: {
834
+ text: { type: "string" }
835
+ },
836
+ required: ["text"]
837
+ )
677
838
 
678
- # Access server's original context
679
- user_id = server_context.dig(:user_id)
839
+ def self.call(text:, server_context:)
840
+ result = server_context.create_sampling_message(
841
+ messages: [
842
+ { role: "user", content: { type: "text", text: "Please summarize: #{text}" } }
843
+ ],
844
+ max_tokens: 500
845
+ )
680
846
 
681
847
  MCP::Tool::Response.new([{
682
848
  type: "text",
683
- text: "Processing for user #{user_id} in session #{session_id}"
849
+ text: result[:content][:text]
684
850
  }])
685
851
  end
686
852
  end
853
+
854
+ server = MCP::Server.new(name: "my_server", tools: [SummarizeTool])
855
+ server.server_context = server
687
856
  ```
688
857
 
689
- **Client Request Example:**
858
+ **Parameters:**
690
859
 
691
- ```json
692
- {
693
- "jsonrpc": "2.0",
694
- "id": 1,
695
- "method": "tools/call",
696
- "params": {
697
- "name": "my_tool",
698
- "arguments": { "message": "Hello" },
699
- "_meta": {
700
- "session_id": "abc123",
701
- "request_id": "req_456"
702
- }
703
- }
704
- }
705
- ```
860
+ Required:
706
861
 
707
- #### Configuration Block Data
862
+ - `messages:` (Array) - Array of message objects with `role` and `content`
863
+ - `max_tokens:` (Integer) - Maximum tokens in the response
708
864
 
709
- ##### Exception Reporter
865
+ Optional:
710
866
 
711
- The exception reporter receives:
867
+ - `system_prompt:` (String) - System prompt for the LLM
868
+ - `model_preferences:` (Hash) - Model selection preferences (e.g., `{ intelligencePriority: 0.8 }`)
869
+ - `include_context:` (String) - Context inclusion: `"none"`, `"thisServer"`, or `"allServers"` (soft-deprecated)
870
+ - `temperature:` (Float) - Sampling temperature
871
+ - `stop_sequences:` (Array) - Sequences that stop generation
872
+ - `metadata:` (Hash) - Additional metadata
873
+ - `tools:` (Array) - Tools available to the LLM (requires `sampling.tools` capability)
874
+ - `tool_choice:` (Hash) - Tool selection mode (e.g., `{ mode: "auto" }`)
712
875
 
713
- - `exception`: The Ruby exception object that was raised
714
- - `server_context`: The context hash provided to the server
876
+ **Direct Usage:**
715
877
 
716
- **Signature:**
878
+ `Server#create_sampling_message` can also be called directly outside of tools:
717
879
 
718
880
  ```ruby
719
- exception_reporter = ->(exception, server_context) { ... }
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
+ )
720
889
  ```
721
890
 
722
- ##### Instrumentation Callback
723
-
724
- The instrumentation callback receives a hash with the following possible keys:
725
-
726
- - `method`: (String) The protocol method called (e.g., "ping", "tools/list")
727
- - `tool_name`: (String, optional) The name of the tool called
728
- - `tool_arguments`: (Hash, optional) The arguments passed to the tool
729
- - `prompt_name`: (String, optional) The name of the prompt called
730
- - `resource_uri`: (String, optional) The URI of the resource called
731
- - `error`: (String, optional) Error code if a lookup failed
732
- - `duration`: (Float) Duration of the call in seconds
733
- - `client`: (Hash, optional) Client information with `name` and `version` keys, from the initialize request
734
-
735
- > [!NOTE]
736
- > `tool_name`, `prompt_name` and `resource_uri` are only populated if a matching handler is registered.
737
- > This is to avoid potential issues with metric cardinality.
738
-
739
- **Type:**
891
+ Result contains the LLM response:
740
892
 
741
893
  ```ruby
742
- instrumentation_callback = ->(data) { ... }
743
- # where data is a Hash with keys as described above
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
+ }
744
900
  ```
745
901
 
746
- **Example:**
747
-
748
- ```ruby
749
- MCP.configure do |config|
750
- config.instrumentation_callback = ->(data) {
751
- puts "Instrumentation: #{data.inspect}"
752
- }
753
- end
754
- ```
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.
755
904
 
756
- ### Server Protocol Version
905
+ **Tool Use in Sampling:**
757
906
 
758
- The server's protocol version can be overridden using the `protocol_version` keyword argument:
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:
759
909
 
760
910
  ```ruby
761
- configuration = MCP::Configuration.new(protocol_version: "2024-11-05")
762
- MCP::Server.new(name: "test_server", configuration: configuration)
763
- ```
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
+ )
764
929
 
765
- If no protocol version is specified, the latest stable version will be applied by default.
766
- The latest stable version includes new features from the [draft version](https://modelcontextprotocol.io/specification/draft).
930
+ if result[:stopReason] == "toolUse"
931
+ tool_results = result[:content].map do |tool_use|
932
+ weather_data = get_weather(tool_use[:input][:city])
767
933
 
768
- This will make all new server instances use the specified protocol version instead of the default version. The protocol version can be reset to the default by setting it to `nil`:
934
+ {
935
+ type: "tool_result",
936
+ toolUseId: tool_use[:id],
937
+ content: [{ type: "text", text: weather_data.to_json }]
938
+ }
939
+ end
769
940
 
770
- ```ruby
771
- MCP::Configuration.new(protocol_version: nil)
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
772
951
  ```
773
952
 
774
- If an invalid `protocol_version` value is set, an `ArgumentError` is raised.
775
-
776
- Be sure to check the [MCP spec](https://modelcontextprotocol.io/specification/versioning) for the protocol version to understand the supported features for the version being set.
953
+ **Error Handling:**
777
954
 
778
- ### Exception Reporting
955
+ - Raises `RuntimeError` if transport is not set
956
+ - Raises `RuntimeError` if client does not support `sampling` capability
957
+ - Raises `RuntimeError` if `tools` are used but client lacks `sampling.tools` capability
958
+ - Raises `StandardError` if client returns an error response
779
959
 
780
- The exception reporter receives two arguments:
960
+ ### Notifications
781
961
 
782
- - `exception`: The Ruby exception object that was raised
783
- - `server_context`: A hash containing contextual information about where the error occurred
962
+ The server supports sending notifications to clients when lists of tools, prompts, or resources change. This enables real-time updates without polling.
784
963
 
785
- The server_context hash includes:
964
+ #### Notification Methods
786
965
 
787
- - For tool calls: `{ tool_name: "name", arguments: { ... } }`
788
- - For general request handling: `{ request: { ... } }`
966
+ The server provides the following notification methods:
789
967
 
790
- When an exception occurs:
968
+ - `notify_tools_list_changed` - Send a notification when the tools list changes
969
+ - `notify_prompts_list_changed` - Send a notification when the prompts list changes
970
+ - `notify_resources_list_changed` - Send a notification when the resources list changes
971
+ - `notify_log_message` - Send a structured logging notification message
791
972
 
792
- 1. The exception is reported via the configured reporter
793
- 2. For tool calls, a generic error response is returned to the client: `{ error: "Internal error occurred", isError: true }`
794
- 3. For other requests, the exception is re-raised after reporting
973
+ #### Session Scoping
795
974
 
796
- If no exception reporter is configured, a default no-op reporter is used that silently ignores exceptions.
975
+ When using Streamable HTTP transport with multiple clients, each client connection gets its own session. Notifications are scoped as follows:
797
976
 
798
- ### Tools
977
+ - **`report_progress`** and **`notify_log_message`** called via `server_context` inside a tool handler are automatically sent only to the requesting client.
978
+ No extra configuration is needed.
979
+ - **`notify_tools_list_changed`**, **`notify_prompts_list_changed`**, and **`notify_resources_list_changed`** are always broadcast to all connected clients,
980
+ as they represent server-wide state changes. These should be called on the `server` instance directly.
799
981
 
800
- MCP spec includes [Tools](https://modelcontextprotocol.io/specification/latest/server/tools) which provide functionality to LLM apps.
982
+ #### Notification Format
801
983
 
802
- This gem provides a `MCP::Tool` class that can be used to create tools in three ways:
984
+ Notifications follow the JSON-RPC 2.0 specification and use these method names:
803
985
 
804
- 1. As a class definition:
986
+ - `notifications/tools/list_changed`
987
+ - `notifications/prompts/list_changed`
988
+ - `notifications/resources/list_changed`
989
+ - `notifications/progress`
990
+ - `notifications/message`
805
991
 
806
- ```ruby
807
- class MyTool < MCP::Tool
808
- title "My Tool"
809
- description "This tool performs specific functionality..."
810
- input_schema(
811
- properties: {
812
- message: { type: "string" },
813
- },
814
- required: ["message"]
815
- )
816
- output_schema(
817
- properties: {
818
- result: { type: "string" },
819
- success: { type: "boolean" },
820
- timestamp: { type: "string", format: "date-time" }
821
- },
822
- required: ["result", "success", "timestamp"]
823
- )
824
- annotations(
825
- read_only_hint: true,
826
- destructive_hint: false,
827
- idempotent_hint: true,
828
- open_world_hint: false,
829
- title: "My Tool"
830
- )
992
+ ### Progress
831
993
 
832
- def self.call(message:, server_context:)
833
- MCP::Tool::Response.new([{ type: "text", text: "OK" }])
834
- end
835
- end
994
+ The MCP Ruby SDK supports progress tracking for long-running tool operations,
995
+ following the [MCP Progress specification](https://modelcontextprotocol.io/specification/latest/server/utilities/progress).
836
996
 
837
- tool = MyTool
838
- ```
997
+ #### How Progress Works
839
998
 
840
- 2. By using the `MCP::Tool.define` method with a block:
999
+ 1. **Client Request**: The client sends a `progressToken` in the `_meta` field when calling a tool
1000
+ 2. **Server Notification**: The server sends `notifications/progress` messages back to the client during tool execution
1001
+ 3. **Tool Integration**: Tools call `server_context.report_progress` to report incremental progress
841
1002
 
842
- ```ruby
843
- tool = MCP::Tool.define(
844
- name: "my_tool",
845
- title: "My Tool",
846
- description: "This tool performs specific functionality...",
847
- annotations: {
848
- read_only_hint: true,
849
- title: "My Tool"
850
- }
851
- ) do |args, server_context:|
852
- MCP::Tool::Response.new([{ type: "text", text: "OK" }])
853
- end
854
- ```
1003
+ #### Server-Side: Tool with Progress
855
1004
 
856
- 3. By using the `MCP::Server#define_tool` method with a block:
1005
+ Tools that accept a `server_context:` parameter can call `report_progress` on it.
1006
+ The server automatically wraps the context in an `MCP::ServerContext` instance that provides this method:
857
1007
 
858
1008
  ```ruby
859
- server = MCP::Server.new
860
- server.define_tool(
861
- name: "my_tool",
862
- description: "This tool performs specific functionality...",
863
- annotations: {
864
- title: "My Tool",
865
- read_only_hint: true
866
- }
867
- ) do |args, server_context:|
868
- Tool::Response.new([{ type: "text", text: "OK" }])
869
- end
870
- ```
871
-
872
- The server_context parameter is the server_context passed into the server and can be used to pass per request information,
873
- e.g. around authentication state.
874
-
875
- ### Tool Annotations
876
-
877
- Tools can include annotations that provide additional metadata about their behavior. The following annotations are supported:
878
-
879
- - `destructive_hint`: Indicates if the tool performs destructive operations. Defaults to true
880
- - `idempotent_hint`: Indicates if the tool's operations are idempotent. Defaults to false
881
- - `open_world_hint`: Indicates if the tool operates in an open world context. Defaults to true
882
- - `read_only_hint`: Indicates if the tool only reads data (doesn't modify state). Defaults to false
883
- - `title`: A human-readable title for the tool
884
-
885
- Annotations can be set either through the class definition using the `annotations` class method or when defining a tool using the `define` method.
886
-
887
- > [!NOTE]
888
- > This **Tool Annotations** feature is supported starting from `protocol_version: '2025-03-26'`.
889
-
890
- ### Tool Output Schemas
891
-
892
- Tools can optionally define an `output_schema` to specify the expected structure of their results. This works similarly to how `input_schema` is defined and can be used in three ways:
893
-
894
- 1. **Class definition with output_schema:**
895
-
896
- ```ruby
897
- class WeatherTool < MCP::Tool
898
- tool_name "get_weather"
899
- description "Get current weather for a location"
900
-
1009
+ class LongRunningTool < MCP::Tool
1010
+ description "A tool that reports progress during execution"
901
1011
  input_schema(
902
- properties: {
903
- location: { type: "string" },
904
- units: { type: "string", enum: ["celsius", "fahrenheit"] }
905
- },
906
- required: ["location"]
907
- )
908
-
909
- output_schema(
910
- properties: {
911
- temperature: { type: "number" },
912
- condition: { type: "string" },
913
- humidity: { type: "integer" }
914
- },
915
- required: ["temperature", "condition", "humidity"]
916
- )
917
-
918
- def self.call(location:, units: "celsius", server_context:)
919
- # Call weather API and structure the response
920
- api_response = WeatherAPI.fetch(location, units)
921
- weather_data = {
922
- temperature: api_response.temp,
923
- condition: api_response.description,
924
- humidity: api_response.humidity_percent
925
- }
926
-
927
- output_schema.validate_result(weather_data)
928
-
929
- MCP::Tool::Response.new([{
930
- type: "text",
931
- text: weather_data.to_json
932
- }])
933
- end
934
- end
935
- ```
936
-
937
- 2. **Using Tool.define with output_schema:**
938
-
939
- ```ruby
940
- tool = MCP::Tool.define(
941
- name: "calculate_stats",
942
- description: "Calculate statistics for a dataset",
943
- input_schema: {
944
- properties: {
945
- numbers: { type: "array", items: { type: "number" } }
946
- },
947
- required: ["numbers"]
948
- },
949
- output_schema: {
950
- properties: {
951
- mean: { type: "number" },
952
- median: { type: "number" },
953
- count: { type: "integer" }
954
- },
955
- required: ["mean", "median", "count"]
956
- }
957
- ) do |args, server_context:|
958
- # Calculate statistics and validate against schema
959
- MCP::Tool::Response.new([{ type: "text", text: "Statistics calculated" }])
960
- end
961
- ```
962
-
963
- 3. **Using OutputSchema objects:**
964
-
965
- ```ruby
966
- class DataTool < MCP::Tool
967
- output_schema MCP::Tool::OutputSchema.new(
968
- properties: {
969
- success: { type: "boolean" },
970
- data: { type: "object" }
971
- },
972
- required: ["success"]
973
- )
974
- end
975
- ```
976
-
977
- Output schema may also describe an array of objects:
978
-
979
- ```ruby
980
- class WeatherTool < MCP::Tool
981
- output_schema(
982
- type: "array",
983
- items: {
984
- properties: {
985
- temperature: { type: "number" },
986
- condition: { type: "string" },
987
- humidity: { type: "integer" }
988
- },
989
- required: ["temperature", "condition", "humidity"]
990
- }
991
- )
992
- end
993
- ```
994
-
995
- Please note: in this case, you must provide `type: "array"`. The default type
996
- for output schemas is `object`.
997
-
998
- MCP spec for the [Output Schema](https://modelcontextprotocol.io/specification/latest/server/tools#output-schema) specifies that:
999
-
1000
- - **Server Validation**: Servers MUST provide structured results that conform to the output schema
1001
- - **Client Validation**: Clients SHOULD validate structured results against the output schema
1002
- - **Better Integration**: Enables strict schema validation, type information, and improved developer experience
1003
- - **Backward Compatibility**: Tools returning structured content SHOULD also include serialized JSON in a TextContent block
1004
-
1005
- The output schema follows standard JSON Schema format and helps ensure consistent data exchange between MCP servers and clients.
1006
-
1007
- ### Tool Responses with Structured Content
1008
-
1009
- Tools can return structured data alongside text content using the `structured_content` parameter.
1010
-
1011
- The structured content will be included in the JSON-RPC response as the `structuredContent` field.
1012
-
1013
- ```ruby
1014
- class WeatherTool < MCP::Tool
1015
- description "Get current weather and return structured data"
1016
-
1017
- def self.call(location:, units: "celsius", server_context:)
1018
- # Call weather API and structure the response
1019
- api_response = WeatherAPI.fetch(location, units)
1020
- weather_data = {
1021
- temperature: api_response.temp,
1022
- condition: api_response.description,
1023
- humidity: api_response.humidity_percent
1024
- }
1025
-
1026
- output_schema.validate_result(weather_data)
1027
-
1028
- MCP::Tool::Response.new(
1029
- [{
1030
- type: "text",
1031
- text: weather_data.to_json
1032
- }],
1033
- structured_content: weather_data
1034
- )
1035
- end
1036
- end
1037
- ```
1038
-
1039
- ### Tool Responses with Errors
1040
-
1041
- Tools can return error information alongside text content using the `error` parameter.
1042
-
1043
- The error will be included in the JSON-RPC response as the `isError` field.
1044
-
1045
- ```ruby
1046
- class WeatherTool < MCP::Tool
1047
- description "Get current weather and return structured data"
1048
-
1049
- def self.call(server_context:)
1050
- # Do something here
1051
- content = {}
1052
-
1053
- MCP::Tool::Response.new(
1054
- [{
1055
- type: "text",
1056
- text: content.to_json
1057
- }],
1058
- structured_content: content,
1059
- error: true
1060
- )
1061
- end
1062
- end
1063
- ```
1064
-
1065
- ### Prompts
1066
-
1067
- MCP spec includes [Prompts](https://modelcontextprotocol.io/specification/latest/server/prompts), which enable servers to define reusable prompt templates and workflows that clients can easily surface to users and LLMs.
1068
-
1069
- The `MCP::Prompt` class provides three ways to create prompts:
1070
-
1071
- 1. As a class definition with metadata:
1072
-
1073
- ```ruby
1074
- class MyPrompt < MCP::Prompt
1075
- prompt_name "my_prompt" # Optional - defaults to underscored class name
1076
- title "My Prompt"
1077
- description "This prompt performs specific functionality..."
1078
- arguments [
1079
- MCP::Prompt::Argument.new(
1080
- name: "message",
1081
- title: "Message Title",
1082
- description: "Input message",
1083
- required: true
1084
- )
1085
- ]
1086
- meta({ version: "1.0", category: "example" })
1087
-
1088
- class << self
1089
- def template(args, server_context:)
1090
- MCP::Prompt::Result.new(
1091
- description: "Response description",
1092
- messages: [
1093
- MCP::Prompt::Message.new(
1094
- role: "user",
1095
- content: MCP::Content::Text.new("User message")
1096
- ),
1097
- MCP::Prompt::Message.new(
1098
- role: "assistant",
1099
- content: MCP::Content::Text.new(args["message"])
1100
- )
1101
- ]
1102
- )
1103
- end
1104
- end
1105
- end
1106
-
1107
- prompt = MyPrompt
1108
- ```
1109
-
1110
- 2. Using the `MCP::Prompt.define` method:
1111
-
1112
- ```ruby
1113
- prompt = MCP::Prompt.define(
1114
- name: "my_prompt",
1115
- title: "My Prompt",
1116
- description: "This prompt performs specific functionality...",
1117
- arguments: [
1118
- MCP::Prompt::Argument.new(
1119
- name: "message",
1120
- title: "Message Title",
1121
- description: "Input message",
1122
- required: true
1123
- )
1124
- ],
1125
- meta: { version: "1.0", category: "example" }
1126
- ) do |args, server_context:|
1127
- MCP::Prompt::Result.new(
1128
- description: "Response description",
1129
- messages: [
1130
- MCP::Prompt::Message.new(
1131
- role: "user",
1132
- content: MCP::Content::Text.new("User message")
1133
- ),
1134
- MCP::Prompt::Message.new(
1135
- role: "assistant",
1136
- content: MCP::Content::Text.new(args["message"])
1137
- )
1138
- ]
1139
- )
1140
- end
1141
- ```
1142
-
1143
- 3. Using the `MCP::Server#define_prompt` method:
1144
-
1145
- ```ruby
1146
- server = MCP::Server.new
1147
- server.define_prompt(
1148
- name: "my_prompt",
1149
- description: "This prompt performs specific functionality...",
1150
- arguments: [
1151
- Prompt::Argument.new(
1152
- name: "message",
1153
- title: "Message Title",
1154
- description: "Input message",
1155
- required: true
1156
- )
1157
- ],
1158
- meta: { version: "1.0", category: "example" }
1159
- ) do |args, server_context:|
1160
- Prompt::Result.new(
1161
- description: "Response description",
1162
- messages: [
1163
- Prompt::Message.new(
1164
- role: "user",
1165
- content: Content::Text.new("User message")
1166
- ),
1167
- Prompt::Message.new(
1168
- role: "assistant",
1169
- content: Content::Text.new(args["message"])
1170
- )
1171
- ]
1012
+ properties: {
1013
+ count: { type: "integer" },
1014
+ },
1015
+ required: ["count"]
1172
1016
  )
1017
+
1018
+ def self.call(count:, server_context:)
1019
+ count.times do |i|
1020
+ # Do work here.
1021
+ server_context.report_progress(i + 1, total: count, message: "Processing item #{i + 1}")
1022
+ end
1023
+
1024
+ MCP::Tool::Response.new([{ type: "text", text: "Done" }])
1025
+ end
1173
1026
  end
1174
1027
  ```
1175
1028
 
1176
- The server_context parameter is the server_context passed into the server and can be used to pass per request information,
1177
- e.g. around authentication state or user preferences.
1029
+ The `server_context.report_progress` method accepts:
1178
1030
 
1179
- ### Key Components
1031
+ - `progress` (required) — current progress value (numeric)
1032
+ - `total:` (optional) — total expected value, so clients can display a percentage
1033
+ - `message:` (optional) — human-readable status message
1180
1034
 
1181
- - `MCP::Prompt::Argument` - Defines input parameters for the prompt template with name, title, description, and required flag
1182
- - `MCP::Prompt::Message` - Represents a message in the conversation with a role and content
1183
- - `MCP::Prompt::Result` - The output of a prompt template containing description and messages
1184
- - `MCP::Content::Text` - Text content for messages
1035
+ **Key Features:**
1185
1036
 
1186
- ### Usage
1037
+ - Tools report progress via `server_context.report_progress`
1038
+ - `report_progress` is a no-op when no `progressToken` was provided by the client
1039
+ - Supports both numeric and string progress tokens
1187
1040
 
1188
- Register prompts with the MCP server:
1041
+ ### Completions
1042
+
1043
+ MCP spec includes [Completions](https://modelcontextprotocol.io/specification/latest/server/utilities/completion),
1044
+ which enable servers to provide autocompletion suggestions for prompt arguments and resource URIs.
1045
+
1046
+ To enable completions, declare the `completions` capability and register a handler:
1189
1047
 
1190
1048
  ```ruby
1191
1049
  server = MCP::Server.new(
1192
1050
  name: "my_server",
1193
- prompts: [MyPrompt],
1194
- server_context: { user_id: current_user.id },
1051
+ prompts: [CodeReviewPrompt],
1052
+ resource_templates: [FileTemplate],
1053
+ capabilities: { completions: {} },
1195
1054
  )
1055
+
1056
+ server.completion_handler do |params|
1057
+ ref = params[:ref]
1058
+ argument = params[:argument]
1059
+ value = argument[:value]
1060
+
1061
+ case ref[:type]
1062
+ when "ref/prompt"
1063
+ values = case argument[:name]
1064
+ when "language"
1065
+ ["python", "pytorch", "pyside"].select { |v| v.start_with?(value) }
1066
+ else
1067
+ []
1068
+ end
1069
+ { completion: { values: values, hasMore: false } }
1070
+ when "ref/resource"
1071
+ { completion: { values: [], hasMore: false } }
1072
+ end
1073
+ end
1196
1074
  ```
1197
1075
 
1198
- The server will handle prompt listing and execution through the MCP protocol methods:
1076
+ The handler receives a `params` hash with:
1199
1077
 
1200
- - `prompts/list` - Lists all registered prompts and their schemas
1201
- - `prompts/get` - Retrieves and executes a specific prompt with arguments
1078
+ - `ref` - The reference (`{ type: "ref/prompt", name: "..." }` or `{ type: "ref/resource", uri: "..." }`)
1079
+ - `argument` - The argument being completed (`{ name: "...", value: "..." }`)
1080
+ - `context` (optional) - Previously resolved arguments (`{ arguments: { ... } }`)
1202
1081
 
1203
- ### Resources
1082
+ The handler must return a hash with a `completion` key containing `values` (array of strings), and optionally `total` and `hasMore`.
1083
+ The SDK automatically enforces the 100-item limit per the MCP specification.
1204
1084
 
1205
- MCP spec includes [Resources](https://modelcontextprotocol.io/specification/latest/server/resources).
1085
+ The server validates that the referenced prompt, resource, or resource template is registered before calling the handler.
1086
+ Requests for unknown references return an error.
1206
1087
 
1207
- ### Reading Resources
1088
+ ### Logging
1208
1089
 
1209
- The `MCP::Resource` class provides a way to register resources with the server.
1090
+ 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).
1091
+
1092
+ The `notifications/message` notification is used for structured logging between client and server.
1093
+
1094
+ #### Log Levels
1095
+
1096
+ The SDK supports 8 log levels with increasing severity:
1097
+
1098
+ - `debug` - Detailed debugging information
1099
+ - `info` - General informational messages
1100
+ - `notice` - Normal but significant events
1101
+ - `warning` - Warning conditions
1102
+ - `error` - Error conditions
1103
+ - `critical` - Critical conditions
1104
+ - `alert` - Action must be taken immediately
1105
+ - `emergency` - System is unusable
1106
+
1107
+ #### How Logging Works
1108
+
1109
+ 1. **Client Configuration**: The client sends a `logging/setLevel` request to configure the minimum log level
1110
+ 2. **Server Filtering**: The server only sends log messages at the configured level or higher severity
1111
+ 3. **Notification Delivery**: Log messages are sent as `notifications/message` to the client
1112
+
1113
+ For example, if the client sets the level to `"error"` (severity 4), the server will send messages with levels: `error`, `critical`, `alert`, and `emergency`.
1114
+
1115
+ For more details, see the [MCP Logging specification](https://modelcontextprotocol.io/specification/latest/server/utilities/logging).
1116
+
1117
+ **Usage Example:**
1210
1118
 
1211
1119
  ```ruby
1212
- resource = MCP::Resource.new(
1213
- uri: "https://example.com/my_resource",
1214
- name: "my-resource",
1215
- title: "My Resource",
1216
- description: "Lorem ipsum dolor sit amet",
1217
- mime_type: "text/html",
1120
+ server = MCP::Server.new(name: "my_server")
1121
+ transport = MCP::Server::Transports::StdioTransport.new(server)
1122
+
1123
+ # The client first configures the logging level (on the client side):
1124
+ transport.send_request(
1125
+ request: {
1126
+ jsonrpc: "2.0",
1127
+ method: "logging/setLevel",
1128
+ params: { level: "info" },
1129
+ id: session_id # Unique request ID within the session
1130
+ }
1218
1131
  )
1219
1132
 
1220
- server = MCP::Server.new(
1221
- name: "my_server",
1222
- resources: [resource],
1133
+ # Send log messages at different severity levels
1134
+ server.notify_log_message(
1135
+ data: { message: "Application started successfully" },
1136
+ level: "info"
1137
+ )
1138
+
1139
+ server.notify_log_message(
1140
+ data: { message: "Configuration file not found, using defaults" },
1141
+ level: "warning"
1142
+ )
1143
+
1144
+ server.notify_log_message(
1145
+ data: {
1146
+ error: "Database connection failed",
1147
+ details: { host: "localhost", port: 5432 }
1148
+ },
1149
+ level: "error",
1150
+ logger: "DatabaseLogger" # Optional logger name
1223
1151
  )
1224
1152
  ```
1225
1153
 
1226
- The server must register a handler for the `resources/read` method to retrieve a resource dynamically.
1154
+ **Key Features:**
1155
+
1156
+ - Supports 8 log levels (debug, info, notice, warning, error, critical, alert, emergency) based on https://modelcontextprotocol.io/specification/2025-06-18/server/utilities/logging#log-levels
1157
+ - Server has capability `logging` to send log messages
1158
+ - Messages are only sent if a transport is configured
1159
+ - Messages are filtered based on the client's configured log level
1160
+ - If the log level hasn't been set by the client, no messages will be sent
1161
+
1162
+ #### Transport Support
1163
+
1164
+ - **stdio**: Notifications are sent as JSON-RPC 2.0 messages to stdout
1165
+ - **Streamable HTTP**: Notifications are sent as JSON-RPC 2.0 messages over HTTP with streaming (chunked transfer or SSE)
1166
+
1167
+ #### Usage Example
1227
1168
 
1228
1169
  ```ruby
1229
- server.resources_read_handler do |params|
1230
- [{
1231
- uri: params[:uri],
1232
- mimeType: "text/plain",
1233
- text: "Hello from example resource! URI: #{params[:uri]}"
1234
- }]
1170
+ server = MCP::Server.new(name: "my_server")
1171
+
1172
+ # Default Streamable HTTP - session oriented
1173
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
1174
+
1175
+ # When tools change, notify clients
1176
+ server.define_tool(name: "new_tool") { |**args| { result: "ok" } }
1177
+ server.notify_tools_list_changed
1178
+ ```
1179
+
1180
+ You can use Stateless Streamable HTTP, where notifications are not supported and all calls are request/response interactions.
1181
+ This mode allows for easy multi-node deployment.
1182
+ Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new` (`stateless` defaults to `false`):
1183
+
1184
+ ```ruby
1185
+ # Stateless Streamable HTTP - session-less
1186
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
1187
+ ```
1188
+
1189
+ By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
1190
+ When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
1191
+
1192
+ ```ruby
1193
+ # Session timeout of 30 minutes
1194
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
1195
+ ```
1196
+
1197
+ ### Advanced
1198
+
1199
+ #### Custom Methods
1200
+
1201
+ The server allows you to define custom JSON-RPC methods beyond the standard MCP protocol methods using the `define_custom_method` method:
1202
+
1203
+ ```ruby
1204
+ server = MCP::Server.new(name: "my_server")
1205
+
1206
+ # Define a custom method that returns a result
1207
+ server.define_custom_method(method_name: "add") do |params|
1208
+ params[:a] + params[:b]
1209
+ end
1210
+
1211
+ # Define a custom notification method (returns nil)
1212
+ server.define_custom_method(method_name: "notify") do |params|
1213
+ # Process notification
1214
+ nil
1235
1215
  end
1236
1216
  ```
1237
1217
 
1238
- otherwise `resources/read` requests will be a no-op.
1218
+ **Key Features:**
1239
1219
 
1240
- ### Resource Templates
1220
+ - Accepts any method name as a string
1221
+ - Block receives the request parameters as a hash
1222
+ - Can handle both regular methods (with responses) and notifications
1223
+ - Prevents overriding existing MCP protocol methods
1224
+ - Supports instrumentation callbacks for monitoring
1241
1225
 
1242
- The `MCP::ResourceTemplate` class provides a way to register resource templates with the server.
1226
+ **Usage Example:**
1243
1227
 
1244
1228
  ```ruby
1245
- resource_template = MCP::ResourceTemplate.new(
1246
- uri_template: "https://example.com/my_resource_template",
1247
- name: "my-resource-template",
1248
- title: "My Resource Template",
1249
- description: "Lorem ipsum dolor sit amet",
1250
- mime_type: "text/html",
1251
- )
1229
+ # Client request
1230
+ {
1231
+ "jsonrpc": "2.0",
1232
+ "id": 1,
1233
+ "method": "add",
1234
+ "params": { "a": 5, "b": 3 }
1235
+ }
1252
1236
 
1253
- server = MCP::Server.new(
1254
- name: "my_server",
1255
- resource_templates: [resource_template],
1256
- )
1237
+ # Server response
1238
+ {
1239
+ "jsonrpc": "2.0",
1240
+ "id": 1,
1241
+ "result": 8
1242
+ }
1257
1243
  ```
1258
1244
 
1245
+ **Error Handling:**
1246
+
1247
+ - Raises `MCP::Server::MethodAlreadyDefinedError` if trying to override an existing method
1248
+ - Supports the same exception reporting and instrumentation as standard methods
1249
+
1250
+ ### Unsupported Features (to be implemented in future versions)
1251
+
1252
+ - Resource subscriptions
1253
+ - Elicitation
1254
+
1259
1255
  ## Building an MCP Client
1260
1256
 
1261
1257
  The `MCP::Client` class provides an interface for interacting with MCP servers.
@@ -1405,6 +1401,18 @@ client.tools # will make the call using Bearer auth
1405
1401
 
1406
1402
  You can add any custom headers needed for your authentication scheme, or for any other purpose. The client will include these headers on every request.
1407
1403
 
1404
+ #### Customizing the Faraday Connection
1405
+
1406
+ You can pass a block to `MCP::Client::HTTP.new` to customize the underlying Faraday connection.
1407
+ The block is called after the default middleware is configured, so you can add middleware or swap the HTTP adapter:
1408
+
1409
+ ```ruby
1410
+ http_transport = MCP::Client::HTTP.new(url: "https://api.example.com/mcp") do |faraday|
1411
+ faraday.use MyApp::Middleware::HttpRecorder
1412
+ faraday.adapter :typhoeus
1413
+ end
1414
+ ```
1415
+
1408
1416
  ### Tool Objects
1409
1417
 
1410
1418
  The client provides a wrapper class for tools returned by the server: