mcp 0.8.0 → 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 +174 -5
- data/lib/mcp/client/stdio.rb +222 -0
- data/lib/mcp/client.rb +12 -2
- data/lib/mcp/progress.rb +21 -0
- data/lib/mcp/prompt.rb +4 -0
- 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 +7 -19
- data/lib/mcp/server/transports.rb +10 -0
- data/lib/mcp/server.rb +40 -4
- data/lib/mcp/server_context.rb +26 -0
- data/lib/mcp/tool.rb +5 -0
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +10 -24
- metadata +7 -36
- data/.gitattributes +0 -4
- data/.github/dependabot.yml +0 -6
- data/.github/workflows/ci.yml +0 -54
- data/.github/workflows/conformance.yml +0 -29
- data/.github/workflows/release.yml +0 -57
- data/.gitignore +0 -11
- data/.rubocop.yml +0 -15
- data/AGENTS.md +0 -107
- data/CHANGELOG.md +0 -168
- data/CODE_OF_CONDUCT.md +0 -74
- data/Gemfile +0 -29
- data/RELEASE.md +0 -12
- data/Rakefile +0 -56
- 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/conformance/README.md +0 -103
- data/conformance/expected_failures.yml +0 -9
- data/conformance/runner.rb +0 -101
- data/conformance/server.rb +0 -547
- 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:
|
|
@@ -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/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
data/lib/mcp/resource.rb
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../transport"
|
|
4
3
|
require "json"
|
|
5
4
|
require "securerandom"
|
|
5
|
+
require_relative "../../transport"
|
|
6
6
|
|
|
7
7
|
module MCP
|
|
8
8
|
class Server
|
|
@@ -154,13 +154,7 @@ module MCP
|
|
|
154
154
|
return success_response
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
-
session_id = request.env["HTTP_MCP_SESSION_ID"]
|
|
158
|
-
|
|
159
|
-
return [
|
|
160
|
-
400,
|
|
161
|
-
{ "Content-Type" => "application/json" },
|
|
162
|
-
[{ error: "Missing session ID" }.to_json],
|
|
163
|
-
] unless session_id
|
|
157
|
+
return missing_session_id_response unless (session_id = request.env["HTTP_MCP_SESSION_ID"])
|
|
164
158
|
|
|
165
159
|
cleanup_session(session_id)
|
|
166
160
|
success_response
|
|
@@ -193,6 +187,8 @@ module MCP
|
|
|
193
187
|
return not_acceptable_response(required_types) unless accept_header
|
|
194
188
|
|
|
195
189
|
accepted_types = parse_accept_header(accept_header)
|
|
190
|
+
return if accepted_types.include?("*/*")
|
|
191
|
+
|
|
196
192
|
missing_types = required_types - accepted_types
|
|
197
193
|
return not_acceptable_response(required_types) unless missing_types.empty?
|
|
198
194
|
|
|
@@ -257,31 +253,23 @@ module MCP
|
|
|
257
253
|
|
|
258
254
|
def handle_regular_request(body_string, session_id)
|
|
259
255
|
unless @stateless
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
return [400, { "Content-Type" => "application/json" }, [{ error: "Invalid session ID" }.to_json]]
|
|
256
|
+
if session_id && !session_exists?(session_id)
|
|
257
|
+
return session_not_found_response
|
|
263
258
|
end
|
|
264
259
|
end
|
|
265
260
|
|
|
266
|
-
response = @server.handle_json(body_string)
|
|
261
|
+
response = @server.handle_json(body_string)
|
|
267
262
|
|
|
268
263
|
# Stream can be nil since stateless mode doesn't retain streams
|
|
269
264
|
stream = get_session_stream(session_id) if session_id
|
|
270
265
|
|
|
271
266
|
if stream
|
|
272
267
|
send_response_to_stream(stream, response, session_id)
|
|
273
|
-
elsif response.nil? && notification_request?(body_string)
|
|
274
|
-
[202, { "Content-Type" => "application/json" }, [response]]
|
|
275
268
|
else
|
|
276
269
|
[200, { "Content-Type" => "application/json" }, [response]]
|
|
277
270
|
end
|
|
278
271
|
end
|
|
279
272
|
|
|
280
|
-
def notification_request?(body_string)
|
|
281
|
-
body = parse_request_body(body_string)
|
|
282
|
-
body.is_a?(Hash) && body["method"].start_with?("notifications/")
|
|
283
|
-
end
|
|
284
|
-
|
|
285
273
|
def get_session_stream(session_id)
|
|
286
274
|
@mutex.synchronize { @sessions[session_id]&.fetch(:stream, nil) }
|
|
287
275
|
end
|