mcp 0.7.1 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +180 -5
- data/lib/mcp/client/stdio.rb +222 -0
- data/lib/mcp/client.rb +12 -2
- data/lib/mcp/content.rb +28 -1
- data/lib/mcp/progress.rb +21 -0
- data/lib/mcp/prompt.rb +8 -3
- data/lib/mcp/resource.rb +3 -0
- data/lib/mcp/server/transports/stdio_transport.rb +1 -1
- data/lib/mcp/server/transports/streamable_http_transport.rb +13 -24
- data/lib/mcp/server/transports.rb +10 -0
- data/lib/mcp/server.rb +62 -7
- data/lib/mcp/server_context.rb +26 -0
- data/lib/mcp/tool/schema.rb +0 -4
- data/lib/mcp/tool.rb +5 -0
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +10 -24
- metadata +7 -31
- data/.gitattributes +0 -4
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/ci.yml +0 -54
- data/.github/workflows/release.yml +0 -57
- data/.gitignore +0 -10
- data/.rubocop.yml +0 -15
- data/AGENTS.md +0 -107
- data/CHANGELOG.md +0 -151
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -29
- data/RELEASE.md +0 -12
- data/Rakefile +0 -17
- data/SECURITY.md +0 -21
- data/bin/console +0 -15
- data/bin/generate-gh-pages.sh +0 -119
- data/bin/rake +0 -31
- data/bin/setup +0 -8
- data/dev.yml +0 -30
- data/docs/_config.yml +0 -6
- data/docs/index.md +0 -7
- data/docs/latest/index.html +0 -19
- data/examples/README.md +0 -197
- data/examples/http_client.rb +0 -184
- data/examples/http_server.rb +0 -169
- data/examples/stdio_server.rb +0 -94
- data/examples/streamable_http_client.rb +0 -207
- data/examples/streamable_http_server.rb +0 -172
- data/mcp.gemspec +0 -35
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d5cd1f7d23be518ff8bff1b1710ff8e8c4ba86ba3d55417e991f921057c0841d
|
|
4
|
+
data.tar.gz: 8e6bba0111698a39ff5aeaa0b4a34e51822018ac943b13b828f9bd2965062ddb
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1f9689d2ecb0a2b4e5ba6888e515d577a12d1f41cd48be8459c2d90ad3b7e4cbe7f557c23aac134a8426bf7bfaba2c3579ab496817ade2ff0fd24056286e52cd
|
|
7
|
+
data.tar.gz: 03601ddc6bf751a75ec6bbadc67458ab9d49367feac7b834d90072028e41a2d1046ed3da6551832482884f435c8779620071f1aa5df66753cf9afade090eb220
|
data/README.md
CHANGED
|
@@ -108,11 +108,12 @@ The server supports sending notifications to clients when lists of tools, prompt
|
|
|
108
108
|
|
|
109
109
|
#### Notification Methods
|
|
110
110
|
|
|
111
|
-
The server provides
|
|
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
|
+
- `notify_progress` - Send a progress notification for long-running operations
|
|
116
117
|
- `notify_log_message` - Send a structured logging notification message
|
|
117
118
|
|
|
118
119
|
#### Notification Format
|
|
@@ -122,8 +123,72 @@ Notifications follow the JSON-RPC 2.0 specification and use these method names:
|
|
|
122
123
|
- `notifications/tools/list_changed`
|
|
123
124
|
- `notifications/prompts/list_changed`
|
|
124
125
|
- `notifications/resources/list_changed`
|
|
126
|
+
- `notifications/progress`
|
|
125
127
|
- `notifications/message`
|
|
126
128
|
|
|
129
|
+
### Progress
|
|
130
|
+
|
|
131
|
+
The MCP Ruby SDK supports progress tracking for long-running tool operations,
|
|
132
|
+
following the [MCP Progress specification](https://modelcontextprotocol.io/specification/latest/server/utilities/progress).
|
|
133
|
+
|
|
134
|
+
#### How Progress Works
|
|
135
|
+
|
|
136
|
+
1. **Client Request**: The client sends a `progressToken` in the `_meta` field when calling a tool
|
|
137
|
+
2. **Server Notification**: The server sends `notifications/progress` messages back to the client during tool execution
|
|
138
|
+
3. **Tool Integration**: Tools call `server_context.report_progress` to report incremental progress
|
|
139
|
+
|
|
140
|
+
#### Server-Side: Tool with Progress
|
|
141
|
+
|
|
142
|
+
Tools that accept a `server_context:` parameter can call `report_progress` on it.
|
|
143
|
+
The server automatically wraps the context in an `MCP::ServerContext` instance that provides this method:
|
|
144
|
+
|
|
145
|
+
```ruby
|
|
146
|
+
class LongRunningTool < MCP::Tool
|
|
147
|
+
description "A tool that reports progress during execution"
|
|
148
|
+
input_schema(
|
|
149
|
+
properties: {
|
|
150
|
+
count: { type: "integer" },
|
|
151
|
+
},
|
|
152
|
+
required: ["count"]
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
def self.call(count:, server_context:)
|
|
156
|
+
count.times do |i|
|
|
157
|
+
# Do work here.
|
|
158
|
+
server_context.report_progress(i + 1, total: count, message: "Processing item #{i + 1}")
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
MCP::Tool::Response.new([{ type: "text", text: "Done" }])
|
|
162
|
+
end
|
|
163
|
+
end
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
The `server_context.report_progress` method accepts:
|
|
167
|
+
|
|
168
|
+
- `progress` (required) — current progress value (numeric)
|
|
169
|
+
- `total:` (optional) — total expected value, so clients can display a percentage
|
|
170
|
+
- `message:` (optional) — human-readable status message
|
|
171
|
+
|
|
172
|
+
#### Server-Side: Direct `notify_progress` Usage
|
|
173
|
+
|
|
174
|
+
You can also call `notify_progress` directly on the server instance:
|
|
175
|
+
|
|
176
|
+
```ruby
|
|
177
|
+
server.notify_progress(
|
|
178
|
+
progress_token: "token-123",
|
|
179
|
+
progress: 50,
|
|
180
|
+
total: 100, # optional
|
|
181
|
+
message: "halfway" # optional
|
|
182
|
+
)
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
**Key Features:**
|
|
186
|
+
|
|
187
|
+
- Tools report progress via `server_context.report_progress`
|
|
188
|
+
- `report_progress` is a no-op when no `progressToken` was provided by the client
|
|
189
|
+
- `notify_progress` is a no-op when no transport is configured
|
|
190
|
+
- Supports both numeric and string progress tokens
|
|
191
|
+
|
|
127
192
|
### Logging
|
|
128
193
|
|
|
129
194
|
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).
|
|
@@ -242,11 +307,12 @@ When added to a Rails controller on a route that handles POST requests, your ser
|
|
|
242
307
|
[Streamable HTTP](https://modelcontextprotocol.io/specification/latest/basic/transports#streamable-http) transport
|
|
243
308
|
requests.
|
|
244
309
|
|
|
245
|
-
You can use
|
|
310
|
+
You can use `StreamableHTTPTransport#handle_request` to handle requests with proper HTTP
|
|
311
|
+
status codes (e.g., 202 Accepted for notifications).
|
|
246
312
|
|
|
247
313
|
```ruby
|
|
248
|
-
class
|
|
249
|
-
def
|
|
314
|
+
class McpController < ActionController::Base
|
|
315
|
+
def create
|
|
250
316
|
server = MCP::Server.new(
|
|
251
317
|
name: "my_server",
|
|
252
318
|
title: "Example Server Display Name",
|
|
@@ -256,7 +322,11 @@ class ApplicationController < ActionController::Base
|
|
|
256
322
|
prompts: [MyPrompt],
|
|
257
323
|
server_context: { user_id: current_user.id },
|
|
258
324
|
)
|
|
259
|
-
|
|
325
|
+
transport = MCP::Server::Transports::StreamableHTTPTransport.new(server)
|
|
326
|
+
server.transport = transport
|
|
327
|
+
status, headers, body = transport.handle_request(request)
|
|
328
|
+
|
|
329
|
+
render(json: body.first, status: status, headers: headers)
|
|
260
330
|
end
|
|
261
331
|
end
|
|
262
332
|
```
|
|
@@ -375,6 +445,50 @@ server = MCP::Server.new(
|
|
|
375
445
|
|
|
376
446
|
This hash is then passed as the `server_context` argument to tool and prompt calls, and is included in exception and instrumentation callbacks.
|
|
377
447
|
|
|
448
|
+
#### Request-specific `_meta` Parameter
|
|
449
|
+
|
|
450
|
+
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`.
|
|
451
|
+
|
|
452
|
+
**Access Pattern:**
|
|
453
|
+
|
|
454
|
+
When a client includes `_meta` in the request params, it becomes available as `server_context[:_meta]`:
|
|
455
|
+
|
|
456
|
+
```ruby
|
|
457
|
+
class MyTool < MCP::Tool
|
|
458
|
+
def self.call(message:, server_context:)
|
|
459
|
+
# Access provider-specific metadata
|
|
460
|
+
session_id = server_context.dig(:_meta, :session_id)
|
|
461
|
+
request_id = server_context.dig(:_meta, :request_id)
|
|
462
|
+
|
|
463
|
+
# Access server's original context
|
|
464
|
+
user_id = server_context.dig(:user_id)
|
|
465
|
+
|
|
466
|
+
MCP::Tool::Response.new([{
|
|
467
|
+
type: "text",
|
|
468
|
+
text: "Processing for user #{user_id} in session #{session_id}"
|
|
469
|
+
}])
|
|
470
|
+
end
|
|
471
|
+
end
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
**Client Request Example:**
|
|
475
|
+
|
|
476
|
+
```json
|
|
477
|
+
{
|
|
478
|
+
"jsonrpc": "2.0",
|
|
479
|
+
"id": 1,
|
|
480
|
+
"method": "tools/call",
|
|
481
|
+
"params": {
|
|
482
|
+
"name": "my_tool",
|
|
483
|
+
"arguments": { "message": "Hello" },
|
|
484
|
+
"_meta": {
|
|
485
|
+
"session_id": "abc123",
|
|
486
|
+
"request_id": "req_456"
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
```
|
|
491
|
+
|
|
378
492
|
#### Configuration Block Data
|
|
379
493
|
|
|
380
494
|
##### Exception Reporter
|
|
@@ -968,6 +1082,52 @@ class CustomTransport
|
|
|
968
1082
|
end
|
|
969
1083
|
```
|
|
970
1084
|
|
|
1085
|
+
### Stdio Transport Layer
|
|
1086
|
+
|
|
1087
|
+
Use the `MCP::Client::Stdio` transport to interact with MCP servers running as subprocesses over standard input/output.
|
|
1088
|
+
|
|
1089
|
+
`MCP::Client::Stdio.new` accepts the following keyword arguments:
|
|
1090
|
+
|
|
1091
|
+
| Parameter | Required | Description |
|
|
1092
|
+
|---|---|---|
|
|
1093
|
+
| `command:` | Yes | The command to spawn the server process (e.g., `"ruby"`, `"bundle"`, `"npx"`). |
|
|
1094
|
+
| `args:` | No | An array of arguments passed to the command. Defaults to `[]`. |
|
|
1095
|
+
| `env:` | No | A hash of environment variables to set for the server process. Defaults to `nil`. |
|
|
1096
|
+
| `read_timeout:` | No | Timeout in seconds for waiting for a server response. Defaults to `nil` (no timeout). |
|
|
1097
|
+
|
|
1098
|
+
Example usage:
|
|
1099
|
+
|
|
1100
|
+
```ruby
|
|
1101
|
+
stdio_transport = MCP::Client::Stdio.new(
|
|
1102
|
+
command: "bundle",
|
|
1103
|
+
args: ["exec", "ruby", "path/to/server.rb"],
|
|
1104
|
+
env: { "API_KEY" => "my_secret_key" },
|
|
1105
|
+
read_timeout: 30
|
|
1106
|
+
)
|
|
1107
|
+
client = MCP::Client.new(transport: stdio_transport)
|
|
1108
|
+
|
|
1109
|
+
# List available tools.
|
|
1110
|
+
tools = client.tools
|
|
1111
|
+
tools.each do |tool|
|
|
1112
|
+
puts "Tool: #{tool.name} - #{tool.description}"
|
|
1113
|
+
end
|
|
1114
|
+
|
|
1115
|
+
# Call a specific tool.
|
|
1116
|
+
response = client.call_tool(
|
|
1117
|
+
tool: tools.first,
|
|
1118
|
+
arguments: { message: "Hello, world!" }
|
|
1119
|
+
)
|
|
1120
|
+
|
|
1121
|
+
# Close the transport when done.
|
|
1122
|
+
stdio_transport.close
|
|
1123
|
+
```
|
|
1124
|
+
|
|
1125
|
+
The stdio transport automatically handles:
|
|
1126
|
+
|
|
1127
|
+
- Spawning the server process with `Open3.popen3`
|
|
1128
|
+
- MCP protocol initialization handshake (`initialize` request + `notifications/initialized`)
|
|
1129
|
+
- JSON-RPC 2.0 message framing over newline-delimited JSON
|
|
1130
|
+
|
|
971
1131
|
### HTTP Transport Layer
|
|
972
1132
|
|
|
973
1133
|
Use the `MCP::Client::HTTP` transport to interact with MCP servers using simple HTTP requests.
|
|
@@ -1000,8 +1160,17 @@ response = client.call_tool(
|
|
|
1000
1160
|
tool: tools.first,
|
|
1001
1161
|
arguments: { message: "Hello, world!" }
|
|
1002
1162
|
)
|
|
1163
|
+
|
|
1164
|
+
# Call a tool with progress tracking.
|
|
1165
|
+
response = client.call_tool(
|
|
1166
|
+
tool: tools.first,
|
|
1167
|
+
arguments: { count: 10 },
|
|
1168
|
+
progress_token: "my-progress-token"
|
|
1169
|
+
)
|
|
1003
1170
|
```
|
|
1004
1171
|
|
|
1172
|
+
The server will send `notifications/progress` back to the client during execution.
|
|
1173
|
+
|
|
1005
1174
|
#### HTTP Authorization
|
|
1006
1175
|
|
|
1007
1176
|
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 +1197,12 @@ The client provides a wrapper class for tools returned by the server:
|
|
|
1028
1197
|
|
|
1029
1198
|
This class provides easy access to tool properties like name, description, input schema, and output schema.
|
|
1030
1199
|
|
|
1200
|
+
## Conformance Testing
|
|
1201
|
+
|
|
1202
|
+
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).
|
|
1203
|
+
|
|
1204
|
+
See [conformance/README.md](conformance/README.md) for usage instructions.
|
|
1205
|
+
|
|
1031
1206
|
## Documentation
|
|
1032
1207
|
|
|
1033
1208
|
- [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.
|
|
@@ -90,6 +94,7 @@ module MCP
|
|
|
90
94
|
#
|
|
91
95
|
# @param tool [MCP::Client::Tool] The tool to be called.
|
|
92
96
|
# @param arguments [Object, nil] The arguments to pass to the tool.
|
|
97
|
+
# @param progress_token [String, Integer, nil] A token to request progress notifications from the server during tool execution.
|
|
93
98
|
# @return [Hash] The full JSON-RPC response from the transport.
|
|
94
99
|
#
|
|
95
100
|
# @example
|
|
@@ -100,12 +105,17 @@ module MCP
|
|
|
100
105
|
# @note
|
|
101
106
|
# The exact requirements for `arguments` are determined by the transport layer in use.
|
|
102
107
|
# Consult the documentation for your transport (e.g., MCP::Client::HTTP) for details.
|
|
103
|
-
def call_tool(tool:, arguments: nil)
|
|
108
|
+
def call_tool(tool:, arguments: nil, progress_token: nil)
|
|
109
|
+
params = { name: tool.name, arguments: arguments }
|
|
110
|
+
if progress_token
|
|
111
|
+
params[:_meta] = { progressToken: progress_token }
|
|
112
|
+
end
|
|
113
|
+
|
|
104
114
|
transport.send_request(request: {
|
|
105
115
|
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
106
116
|
id: request_id,
|
|
107
117
|
method: "tools/call",
|
|
108
|
-
params:
|
|
118
|
+
params: params,
|
|
109
119
|
})
|
|
110
120
|
end
|
|
111
121
|
|
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,
|
|
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
|
data/lib/mcp/progress.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MCP
|
|
4
|
+
class Progress
|
|
5
|
+
def initialize(server:, progress_token:)
|
|
6
|
+
@server = server
|
|
7
|
+
@progress_token = progress_token
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def report(progress, total: nil, message: nil)
|
|
11
|
+
return unless @progress_token
|
|
12
|
+
|
|
13
|
+
@server.notify_progress(
|
|
14
|
+
progress_token: @progress_token,
|
|
15
|
+
progress: progress,
|
|
16
|
+
total: total,
|
|
17
|
+
message: message,
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
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
|
|
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,
|
|
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
|
|
data/lib/mcp/resource.rb
CHANGED