mcp 0.7.0 → 0.10.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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +182 -5
  3. data/lib/mcp/client/stdio.rb +222 -0
  4. data/lib/mcp/client.rb +21 -3
  5. data/lib/mcp/content.rb +28 -1
  6. data/lib/mcp/progress.rb +22 -0
  7. data/lib/mcp/prompt.rb +8 -3
  8. data/lib/mcp/resource/contents.rb +2 -2
  9. data/lib/mcp/resource.rb +3 -0
  10. data/lib/mcp/server/transports/stdio_transport.rb +6 -4
  11. data/lib/mcp/server/transports/streamable_http_transport.rb +146 -36
  12. data/lib/mcp/server/transports.rb +10 -0
  13. data/lib/mcp/server.rb +98 -43
  14. data/lib/mcp/server_context.rb +44 -0
  15. data/lib/mcp/server_session.rb +79 -0
  16. data/lib/mcp/tool/schema.rb +0 -4
  17. data/lib/mcp/tool.rb +5 -0
  18. data/lib/mcp/transport.rb +2 -2
  19. data/lib/mcp/version.rb +1 -1
  20. data/lib/mcp.rb +11 -24
  21. metadata +8 -30
  22. data/.gitattributes +0 -4
  23. data/.github/dependabot.yml +0 -6
  24. data/.github/workflows/ci.yml +0 -54
  25. data/.github/workflows/release.yml +0 -57
  26. data/.gitignore +0 -10
  27. data/.rubocop.yml +0 -15
  28. data/AGENTS.md +0 -107
  29. data/CHANGELOG.md +0 -143
  30. data/CODE_OF_CONDUCT.md +0 -74
  31. data/Gemfile +0 -29
  32. data/RELEASE.md +0 -12
  33. data/Rakefile +0 -17
  34. data/bin/console +0 -15
  35. data/bin/generate-gh-pages.sh +0 -119
  36. data/bin/rake +0 -31
  37. data/bin/setup +0 -8
  38. data/dev.yml +0 -30
  39. data/docs/_config.yml +0 -6
  40. data/docs/index.md +0 -7
  41. data/docs/latest/index.html +0 -19
  42. data/examples/README.md +0 -197
  43. data/examples/http_client.rb +0 -184
  44. data/examples/http_server.rb +0 -169
  45. data/examples/stdio_server.rb +0 -94
  46. data/examples/streamable_http_client.rb +0 -207
  47. data/examples/streamable_http_server.rb +0 -172
  48. data/mcp.gemspec +0 -35
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: de660064625f4c983293603731aad08da2dc60fe6efdc92a79c11501182610e4
4
- data.tar.gz: 663b210aee91776875b90c06457651ff33fe2c125826ee188a8143bf5569e5e9
3
+ metadata.gz: 29a39b8c5bb27a2fcdc8d084dce2cd79dfada5981a18d156bc1de78604035b2e
4
+ data.tar.gz: d969675cb0bb08b9ee3971a9bd90891767c7b28d68fe60579cf4857a06ff3680
5
5
  SHA512:
6
- metadata.gz: 77e28efcff07e26bf49e1509b988f137008ae19391dd86e4ebb31be7d63fdfdcb740a4d49840b4b28b319f3ccd757e8034207eedfed72e2b33b3b464ebd4ff2a
7
- data.tar.gz: 3d83677fe719a766f73b2e91fe6e8c5109bbbb7143ef5c674a154226d0f517fb28ed3b8cce6bbac37136770892258bde3267963000f8aa43d77faaa2a83a1083
6
+ metadata.gz: 3ff992068b54cc35acd43bc9e93edb3959f270a19eedf52540748344f2a94488829cbfec65dedb2772d67dd883f7536523af25d6419ded2cc71944359ff2d016
7
+ data.tar.gz: 0f716e3f54ca10619f95787c65dfe01fecc2356474ddf2909dfb2918f544f3fc8aa2a1a798836b2c556a9178ad842472fd79820ad83e96e855e4ca1ded4f5d8b
data/README.md CHANGED
@@ -108,13 +108,22 @@ The server supports sending notifications to clients when lists of tools, prompt
108
108
 
109
109
  #### Notification Methods
110
110
 
111
- The server provides three notification methods:
111
+ The server provides the following notification methods:
112
112
 
113
113
  - `notify_tools_list_changed` - Send a notification when the tools list changes
114
114
  - `notify_prompts_list_changed` - Send a notification when the prompts list changes
115
115
  - `notify_resources_list_changed` - Send a notification when the resources list changes
116
116
  - `notify_log_message` - Send a structured logging notification message
117
117
 
118
+ #### Session Scoping
119
+
120
+ When using Streamable HTTP transport with multiple clients, each client connection gets its own session. Notifications are scoped as follows:
121
+
122
+ - **`report_progress`** and **`notify_log_message`** called via `server_context` inside a tool handler are automatically sent only to the requesting client.
123
+ No extra configuration is needed.
124
+ - **`notify_tools_list_changed`**, **`notify_prompts_list_changed`**, and **`notify_resources_list_changed`** are always broadcast to all connected clients,
125
+ as they represent server-wide state changes. These should be called on the `server` instance directly.
126
+
118
127
  #### Notification Format
119
128
 
120
129
  Notifications follow the JSON-RPC 2.0 specification and use these method names:
@@ -122,8 +131,58 @@ Notifications follow the JSON-RPC 2.0 specification and use these method names:
122
131
  - `notifications/tools/list_changed`
123
132
  - `notifications/prompts/list_changed`
124
133
  - `notifications/resources/list_changed`
134
+ - `notifications/progress`
125
135
  - `notifications/message`
126
136
 
137
+ ### Progress
138
+
139
+ The MCP Ruby SDK supports progress tracking for long-running tool operations,
140
+ following the [MCP Progress specification](https://modelcontextprotocol.io/specification/latest/server/utilities/progress).
141
+
142
+ #### How Progress Works
143
+
144
+ 1. **Client Request**: The client sends a `progressToken` in the `_meta` field when calling a tool
145
+ 2. **Server Notification**: The server sends `notifications/progress` messages back to the client during tool execution
146
+ 3. **Tool Integration**: Tools call `server_context.report_progress` to report incremental progress
147
+
148
+ #### Server-Side: Tool with Progress
149
+
150
+ Tools that accept a `server_context:` parameter can call `report_progress` on it.
151
+ The server automatically wraps the context in an `MCP::ServerContext` instance that provides this method:
152
+
153
+ ```ruby
154
+ class LongRunningTool < MCP::Tool
155
+ description "A tool that reports progress during execution"
156
+ input_schema(
157
+ properties: {
158
+ count: { type: "integer" },
159
+ },
160
+ required: ["count"]
161
+ )
162
+
163
+ def self.call(count:, server_context:)
164
+ count.times do |i|
165
+ # Do work here.
166
+ server_context.report_progress(i + 1, total: count, message: "Processing item #{i + 1}")
167
+ end
168
+
169
+ MCP::Tool::Response.new([{ type: "text", text: "Done" }])
170
+ end
171
+ end
172
+ ```
173
+
174
+ The `server_context.report_progress` method accepts:
175
+
176
+ - `progress` (required) — current progress value (numeric)
177
+ - `total:` (optional) — total expected value, so clients can display a percentage
178
+ - `message:` (optional) — human-readable status message
179
+
180
+ **Key Features:**
181
+
182
+ - Tools report progress via `server_context.report_progress`
183
+ - `report_progress` is a no-op when no `progressToken` was provided by the client
184
+ - Supports both numeric and string progress tokens
185
+
127
186
  ### Logging
128
187
 
129
188
  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).
@@ -228,6 +287,14 @@ Set `stateless: true` in `MCP::Server::Transports::StreamableHTTPTransport.new`
228
287
  transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, stateless: true)
229
288
  ```
230
289
 
290
+ By default, sessions do not expire. To mitigate session hijacking risks, you can set a `session_idle_timeout` (in seconds).
291
+ When configured, sessions that receive no HTTP requests for this duration are automatically expired and cleaned up:
292
+
293
+ ```ruby
294
+ # Session timeout of 30 minutes
295
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server, session_idle_timeout: 1800)
296
+ ```
297
+
231
298
  ### Unsupported Features (to be implemented in future versions)
232
299
 
233
300
  - Resource subscriptions
@@ -242,11 +309,12 @@ When added to a Rails controller on a route that handles POST requests, your ser
242
309
  [Streamable HTTP](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http) transport
243
310
  requests.
244
311
 
245
- You can use the `Server#handle_json` method to handle requests.
312
+ You can use `StreamableHTTPTransport#handle_request` to handle requests with proper HTTP
313
+ status codes (e.g., 202 Accepted for notifications).
246
314
 
247
315
  ```ruby
248
- class ApplicationController < ActionController::Base
249
- def index
316
+ class McpController < ActionController::Base
317
+ def create
250
318
  server = MCP::Server.new(
251
319
  name: "my_server",
252
320
  title: "Example Server Display Name",
@@ -256,7 +324,11 @@ class ApplicationController < ActionController::Base
256
324
  prompts: [MyPrompt],
257
325
  server_context: { user_id: current_user.id },
258
326
  )
259
- render(json: server.handle_json(request.body.read))
327
+ transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
328
+ server.transport = transport
329
+ status, headers, body = transport.handle_request(request)
330
+
331
+ render(json: body.first, status: status, headers: headers)
260
332
  end
261
333
  end
262
334
  ```
@@ -375,6 +447,50 @@ server = MCP::Server.new(
375
447
 
376
448
  This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
377
449
 
450
+ #### Request-specific `_meta` Parameter
451
+
452
+ 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`.
453
+
454
+ **Access Pattern:**
455
+
456
+ When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
457
+
458
+ ```ruby
459
+ class MyTool < MCP::Tool
460
+ def self.call(message:, server_context:)
461
+ # Access provider-specific metadata
462
+ session_id = server_context.dig(:_meta, :session_id)
463
+ request_id = server_context.dig(:_meta, :request_id)
464
+
465
+ # Access server's original context
466
+ user_id = server_context.dig(:user_id)
467
+
468
+ MCP::Tool::Response.new([{
469
+ type: "text",
470
+ text: "Processing for user #{user_id} in session #{session_id}"
471
+ }])
472
+ end
473
+ end
474
+ ```
475
+
476
+ **Client Request Example:**
477
+
478
+ ```json
479
+ {
480
+ "jsonrpc": "2.0",
481
+ "id": 1,
482
+ "method": "tools/call",
483
+ "params": {
484
+ "name": "my_tool",
485
+ "arguments": { "message": "Hello" },
486
+ "_meta": {
487
+ "session_id": "abc123",
488
+ "request_id": "req_456"
489
+ }
490
+ }
491
+ }
492
+ ```
493
+
378
494
  #### Configuration Block Data
379
495
 
380
496
  ##### Exception Reporter
@@ -968,6 +1084,52 @@ class CustomTransport
968
1084
  end
969
1085
  ```
970
1086
 
1087
+ ### Stdio Transport Layer
1088
+
1089
+ Use the `MCP::Client::Stdio` transport to interact with MCP servers running as subprocesses over standard input/output.
1090
+
1091
+ `MCP::Client::Stdio.new` accepts the following keyword arguments:
1092
+
1093
+ | Parameter | Required | Description |
1094
+ |---|---|---|
1095
+ | `command:` | Yes | The command to spawn the server process (e.g., `"ruby"`, `"bundle"`, `"npx"`). |
1096
+ | `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
1097
+ | `env:` | No | A hash of environment variables to set for the server process. Defaults to `nil`. |
1098
+ | `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil` (no timeout). |
1099
+
1100
+ Example usage:
1101
+
1102
+ ```ruby
1103
+ stdio_transport = MCP::Client::Stdio.new(
1104
+ command: "bundle",
1105
+ args: ["exec", "ruby", "path/to/server.rb"],
1106
+ env: { "API_KEY" => "my_secret_key" },
1107
+ read_timeout: 30
1108
+ )
1109
+ client = MCP::Client.new(transport: stdio_transport)
1110
+
1111
+ # List available tools.
1112
+ tools = client.tools
1113
+ tools.each do |tool|
1114
+ puts "Tool: #{tool.name} - #{tool.description}"
1115
+ end
1116
+
1117
+ # Call a specific tool.
1118
+ response = client.call_tool(
1119
+ tool: tools.first,
1120
+ arguments: { message: "Hello, world!" }
1121
+ )
1122
+
1123
+ # Close the transport when done.
1124
+ stdio_transport.close
1125
+ ```
1126
+
1127
+ The stdio transport automatically handles:
1128
+
1129
+ - Spawning the server process with `Open3.popen3`
1130
+ - MCP protocol initialization handshake (`initialize` request + `notifications/initialized`)
1131
+ - JSON-RPC 2.0 message framing over newline-delimited JSON
1132
+
971
1133
  ### HTTP Transport Layer
972
1134
 
973
1135
  Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
@@ -1000,8 +1162,17 @@ response = client.call_tool(
1000
1162
  tool: tools.first,
1001
1163
  arguments: { message: "Hello, world!" }
1002
1164
  )
1165
+
1166
+ # Call a tool with progress tracking.
1167
+ response = client.call_tool(
1168
+ tool: tools.first,
1169
+ arguments: { count: 10 },
1170
+ progress_token: "my-progress-token"
1171
+ )
1003
1172
  ```
1004
1173
 
1174
+ The server will send `notifications/progress` back to the client during execution.
1175
+
1005
1176
  #### HTTP Authorization
1006
1177
 
1007
1178
  By default, the HTTP transport layer provides no authentication to the server, but you can provide custom headers if you need authentication. For example, to use Bearer token authentication:
@@ -1028,6 +1199,12 @@ The client provides a wrapper class for tools returned by the server:
1028
1199
 
1029
1200
  This class provides easy access to tool properties like name, description, input schema, and output schema.
1030
1201
 
1202
+ ## Conformance Testing
1203
+
1204
+ The `conformance/` directory contains a test server and runner that validate the SDK against the MCP specification using [`@modelcontextprotocol/conformance`](https://github.com/modelcontextprotocol/conformance).
1205
+
1206
+ See [conformance/README.md](conformance/README.md) for usage instructions.
1207
+
1031
1208
  ## Documentation
1032
1209
 
1033
1210
  - [SDK API documentation](https://rubydoc.info/gems/mcp)
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "open3"
5
+ require "securerandom"
6
+ require "timeout"
7
+ require_relative "../../json_rpc_handler"
8
+ require_relative "../configuration"
9
+ require_relative "../methods"
10
+ require_relative "../version"
11
+
12
+ module MCP
13
+ class Client
14
+ class Stdio
15
+ # Seconds to wait for the server process to exit before sending SIGTERM.
16
+ # Matches the Python and TypeScript SDKs' shutdown timeout:
17
+ # https://github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/stdio/__init__.py#L48
18
+ # https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.27.1/src/client/stdio.ts#L221
19
+ CLOSE_TIMEOUT = 2
20
+ STDERR_READ_SIZE = 4096
21
+
22
+ attr_reader :command, :args, :env
23
+
24
+ def initialize(command:, args: [], env: nil, read_timeout: nil)
25
+ @command = command
26
+ @args = args
27
+ @env = env
28
+ @read_timeout = read_timeout
29
+ @stdin = nil
30
+ @stdout = nil
31
+ @stderr = nil
32
+ @wait_thread = nil
33
+ @stderr_thread = nil
34
+ @started = false
35
+ @initialized = false
36
+ end
37
+
38
+ def send_request(request:)
39
+ start unless @started
40
+ initialize_session unless @initialized
41
+
42
+ write_message(request)
43
+ read_response(request)
44
+ end
45
+
46
+ def start
47
+ raise "MCP::Client::Stdio already started" if @started
48
+
49
+ spawn_env = @env || {}
50
+ @stdin, @stdout, @stderr, @wait_thread = Open3.popen3(spawn_env, @command, *@args)
51
+ @stdout.set_encoding("UTF-8")
52
+ @stdin.set_encoding("UTF-8")
53
+
54
+ # Drain stderr in the background to prevent the pipe buffer from filling up,
55
+ # which would cause the server process to block and deadlock.
56
+ @stderr_thread = Thread.new do
57
+ loop do
58
+ @stderr.readpartial(STDERR_READ_SIZE)
59
+ end
60
+ rescue IOError
61
+ nil
62
+ end
63
+
64
+ @started = true
65
+ rescue Errno::ENOENT, Errno::EACCES, Errno::ENOEXEC => e
66
+ raise RequestHandlerError.new(
67
+ "Failed to spawn server process: #{e.message}",
68
+ {},
69
+ error_type: :internal_error,
70
+ original_error: e,
71
+ )
72
+ end
73
+
74
+ def close
75
+ return unless @started
76
+
77
+ @stdin.close
78
+ @stdout.close
79
+ @stderr.close
80
+
81
+ begin
82
+ Timeout.timeout(CLOSE_TIMEOUT) { @wait_thread.value }
83
+ rescue Timeout::Error
84
+ begin
85
+ Process.kill("TERM", @wait_thread.pid)
86
+ Timeout.timeout(CLOSE_TIMEOUT) { @wait_thread.value }
87
+ rescue Timeout::Error
88
+ begin
89
+ Process.kill("KILL", @wait_thread.pid)
90
+ rescue Errno::ESRCH
91
+ nil
92
+ end
93
+ rescue Errno::ESRCH
94
+ nil
95
+ end
96
+ end
97
+
98
+ @stderr_thread.join(CLOSE_TIMEOUT)
99
+ @started = false
100
+ @initialized = false
101
+ end
102
+
103
+ private
104
+
105
+ # The client MUST send a protocol version it supports. This SHOULD be the latest version.
106
+ # https://modelcontextprotocol.io/specification/2025-11-25/basic/lifecycle#version-negotiation
107
+ #
108
+ # Always sends `LATEST_STABLE_PROTOCOL_VERSION`, matching the Python and TypeScript SDKs:
109
+ # https://github.com/modelcontextprotocol/python-sdk/blob/v1.26.0/src/mcp/client/session.py#L175
110
+ # https://github.com/modelcontextprotocol/typescript-sdk/blob/v1.27.1/src/client/index.ts#L495
111
+ def initialize_session
112
+ init_request = {
113
+ jsonrpc: JsonRpcHandler::Version::V2_0,
114
+ id: SecureRandom.uuid,
115
+ method: MCP::Methods::INITIALIZE,
116
+ params: {
117
+ protocolVersion: MCP::Configuration::LATEST_STABLE_PROTOCOL_VERSION,
118
+ capabilities: {},
119
+ clientInfo: { name: "mcp-ruby-client", version: MCP::VERSION },
120
+ },
121
+ }
122
+
123
+ write_message(init_request)
124
+ response = read_response(init_request)
125
+
126
+ if response.key?("error")
127
+ error = response["error"]
128
+ raise RequestHandlerError.new(
129
+ "Server initialization failed: #{error["message"]}",
130
+ { method: MCP::Methods::INITIALIZE },
131
+ error_type: :internal_error,
132
+ )
133
+ end
134
+
135
+ unless response.key?("result")
136
+ raise RequestHandlerError.new(
137
+ "Server initialization failed: missing result in response",
138
+ { method: MCP::Methods::INITIALIZE },
139
+ error_type: :internal_error,
140
+ )
141
+ end
142
+
143
+ notification = {
144
+ jsonrpc: JsonRpcHandler::Version::V2_0,
145
+ method: MCP::Methods::NOTIFICATIONS_INITIALIZED,
146
+ }
147
+ write_message(notification)
148
+
149
+ @initialized = true
150
+ end
151
+
152
+ def write_message(message)
153
+ ensure_running!
154
+ json = JSON.generate(message)
155
+ @stdin.puts(json)
156
+ @stdin.flush
157
+ rescue IOError, Errno::EPIPE => e
158
+ raise RequestHandlerError.new(
159
+ "Failed to write to server process",
160
+ {},
161
+ error_type: :internal_error,
162
+ original_error: e,
163
+ )
164
+ end
165
+
166
+ def read_response(request)
167
+ request_id = request[:id] || request["id"]
168
+ method = request[:method] || request["method"]
169
+ params = request[:params] || request["params"]
170
+
171
+ loop do
172
+ ensure_running!
173
+ wait_for_readable!(method, params) if @read_timeout
174
+ line = @stdout.gets
175
+ raise_connection_error!(method, params) if line.nil?
176
+
177
+ parsed = JSON.parse(line.strip)
178
+
179
+ next unless parsed.key?("id")
180
+
181
+ return parsed if parsed["id"] == request_id
182
+ end
183
+ rescue JSON::ParserError => e
184
+ raise RequestHandlerError.new(
185
+ "Failed to parse server response",
186
+ { method: method, params: params },
187
+ error_type: :internal_error,
188
+ original_error: e,
189
+ )
190
+ end
191
+
192
+ def ensure_running!
193
+ return if @wait_thread.alive?
194
+
195
+ raise RequestHandlerError.new(
196
+ "Server process has exited",
197
+ {},
198
+ error_type: :internal_error,
199
+ )
200
+ end
201
+
202
+ def wait_for_readable!(method, params)
203
+ ready = @stdout.wait_readable(@read_timeout)
204
+ return if ready
205
+
206
+ raise RequestHandlerError.new(
207
+ "Timed out waiting for server response",
208
+ { method: method, params: params },
209
+ error_type: :internal_error,
210
+ )
211
+ end
212
+
213
+ def raise_connection_error!(method, params)
214
+ raise RequestHandlerError.new(
215
+ "Server process closed stdout unexpectedly",
216
+ { method: method, params: params },
217
+ error_type: :internal_error,
218
+ )
219
+ end
220
+ end
221
+ end
222
+ end
data/lib/mcp/client.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "client/stdio"
4
+ require_relative "client/http"
5
+ require_relative "client/tool"
6
+
3
7
  module MCP
4
8
  class Client
5
9
  # Initializes a new MCP::Client instance.
@@ -88,11 +92,17 @@ module MCP
88
92
 
89
93
  # Calls a tool via the transport layer and returns the full response from the server.
90
94
  #
95
+ # @param name [String] The name of the tool to call.
91
96
  # @param tool [MCP::Client::Tool] The tool to be called.
92
97
  # @param arguments [Object, nil] The arguments to pass to the tool.
98
+ # @param progress_token [String, Integer, nil] A token to request progress notifications from the server during tool execution.
93
99
  # @return [Hash] The full JSON-RPC response from the transport.
94
100
  #
95
- # @example
101
+ # @example Call by name
102
+ # response = client.call_tool(name: "my_tool", arguments: { foo: "bar" })
103
+ # content = response.dig("result", "content")
104
+ #
105
+ # @example Call with a tool object
96
106
  # tool = client.tools.first
97
107
  # response = client.call_tool(tool: tool, arguments: { foo: "bar" })
98
108
  # structured_content = response.dig("result", "structuredContent")
@@ -100,12 +110,20 @@ module MCP
100
110
  # @note
101
111
  # The exact requirements for `arguments` are determined by the transport layer in use.
102
112
  # Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
103
- def call_tool(tool:, arguments: nil)
113
+ def call_tool(name: nil, tool: nil, arguments: nil, progress_token: nil)
114
+ tool_name = name || tool&.name
115
+ raise ArgumentError, "Either `name:` or `tool:` must be provided." unless tool_name
116
+
117
+ params = { name: tool_name, arguments: arguments }
118
+ if progress_token
119
+ params[:_meta] = { progressToken: progress_token }
120
+ end
121
+
104
122
  transport.send_request(request: {
105
123
  jsonrpc: JsonRpcHandler::Version::V2_0,
106
124
  id: request_id,
107
125
  method: "tools/call",
108
- params: { name: tool.name, arguments: arguments },
126
+ params: params,
109
127
  })
110
128
  end
111
129
 
data/lib/mcp/content.rb CHANGED
@@ -25,7 +25,34 @@ module MCP
25
25
  end
26
26
 
27
27
  def to_h
28
- { data: data, mime_type: mime_type, annotations: annotations, type: "image" }.compact
28
+ { data: data, mimeType: mime_type, annotations: annotations, type: "image" }.compact
29
+ end
30
+ end
31
+
32
+ class Audio
33
+ attr_reader :data, :mime_type, :annotations
34
+
35
+ def initialize(data, mime_type, annotations: nil)
36
+ @data = data
37
+ @mime_type = mime_type
38
+ @annotations = annotations
39
+ end
40
+
41
+ def to_h
42
+ { data: data, mimeType: mime_type, annotations: annotations, type: "audio" }.compact
43
+ end
44
+ end
45
+
46
+ class EmbeddedResource
47
+ attr_reader :resource, :annotations
48
+
49
+ def initialize(resource, annotations: nil)
50
+ @resource = resource
51
+ @annotations = annotations
52
+ end
53
+
54
+ def to_h
55
+ { resource: resource.to_h, annotations: annotations, type: "resource" }.compact
29
56
  end
30
57
  end
31
58
  end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module MCP
4
+ class Progress
5
+ def initialize(notification_target:, progress_token:)
6
+ @notification_target = notification_target
7
+ @progress_token = progress_token
8
+ end
9
+
10
+ def report(progress, total: nil, message: nil)
11
+ return unless @progress_token
12
+ return unless @notification_target
13
+
14
+ @notification_target.notify_progress(
15
+ progress_token: @progress_token,
16
+ progress: progress,
17
+ total: total,
18
+ message: message,
19
+ )
20
+ end
21
+ end
22
+ end
data/lib/mcp/prompt.rb CHANGED
@@ -1,5 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "prompt/argument"
4
+ require_relative "prompt/message"
5
+ require_relative "prompt/result"
6
+
3
7
  module MCP
4
8
  class Prompt
5
9
  class << self
@@ -21,7 +25,7 @@ module MCP
21
25
  title: title_value,
22
26
  description: description_value,
23
27
  icons: icons_value&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) },
24
- arguments: arguments_value&.map(&:to_h),
28
+ arguments: arguments_value.empty? ? nil : arguments_value.map(&:to_h),
25
29
  _meta: meta_value,
26
30
  }.compact
27
31
  end
@@ -32,7 +36,7 @@ module MCP
32
36
  subclass.instance_variable_set(:@title_value, nil)
33
37
  subclass.instance_variable_set(:@description_value, nil)
34
38
  subclass.instance_variable_set(:@icons_value, nil)
35
- subclass.instance_variable_set(:@arguments_value, nil)
39
+ subclass.instance_variable_set(:@arguments_value, [])
36
40
  subclass.instance_variable_set(:@meta_value, nil)
37
41
  end
38
42
 
@@ -76,7 +80,7 @@ module MCP
76
80
  if value == NOT_SET
77
81
  @arguments_value
78
82
  else
79
- @arguments_value = value
83
+ @arguments_value = Array(value)
80
84
  end
81
85
  end
82
86
 
@@ -103,6 +107,7 @@ module MCP
103
107
  end
104
108
 
105
109
  def validate_arguments!(args)
110
+ args ||= {}
106
111
  missing = required_args - args.keys
107
112
  return if missing.empty?
108
113
 
@@ -11,7 +11,7 @@ module MCP
11
11
  end
12
12
 
13
13
  def to_h
14
- { uri: uri, mime_type: mime_type }.compact
14
+ { uri: uri, mimeType: mime_type }.compact
15
15
  end
16
16
  end
17
17
 
@@ -37,7 +37,7 @@ module MCP
37
37
  end
38
38
 
39
39
  def to_h
40
- super.merge(data: data)
40
+ super.merge(blob: data)
41
41
  end
42
42
  end
43
43
  end
data/lib/mcp/resource.rb CHANGED
@@ -1,5 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "resource/contents"
4
+ require_relative "resource/embedded"
5
+
3
6
  module MCP
4
7
  class Resource
5
8
  attr_reader :uri, :name, :title, :description, :icons, :mime_type