mcp 0.8.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.
- checksums.yaml +4 -4
- data/README.md +176 -5
- data/lib/mcp/client/stdio.rb +222 -0
- data/lib/mcp/client.rb +21 -3
- data/lib/mcp/progress.rb +22 -0
- data/lib/mcp/prompt.rb +4 -0
- data/lib/mcp/resource.rb +3 -0
- data/lib/mcp/server/transports/stdio_transport.rb +6 -4
- data/lib/mcp/server/transports/streamable_http_transport.rb +140 -31
- data/lib/mcp/server/transports.rb +10 -0
- data/lib/mcp/server.rb +71 -39
- data/lib/mcp/server_context.rb +44 -0
- data/lib/mcp/server_session.rb +79 -0
- data/lib/mcp/tool.rb +5 -0
- data/lib/mcp/transport.rb +2 -2
- data/lib/mcp/version.rb +1 -1
- data/lib/mcp.rb +11 -24
- metadata +8 -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: 29a39b8c5bb27a2fcdc8d084dce2cd79dfada5981a18d156bc1de78604035b2e
|
|
4
|
+
data.tar.gz: d969675cb0bb08b9ee3971a9bd90891767c7b28d68fe60579cf4857a06ff3680
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
|
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
|
|
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
|
|
249
|
-
def
|
|
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
|
-
|
|
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:
|
|
@@ -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
|
|
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:
|
|
126
|
+
params: params,
|
|
109
127
|
})
|
|
110
128
|
end
|
|
111
129
|
|
data/lib/mcp/progress.rb
ADDED
|
@@ -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
data/lib/mcp/resource.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require_relative "../../transport"
|
|
4
3
|
require "json"
|
|
4
|
+
require_relative "../../transport"
|
|
5
5
|
|
|
6
6
|
module MCP
|
|
7
7
|
class Server
|
|
@@ -10,17 +10,19 @@ module MCP
|
|
|
10
10
|
STATUS_INTERRUPTED = Signal.list["INT"] + 128
|
|
11
11
|
|
|
12
12
|
def initialize(server)
|
|
13
|
-
|
|
13
|
+
super(server)
|
|
14
14
|
@open = false
|
|
15
|
+
@session = nil
|
|
15
16
|
$stdin.set_encoding("UTF-8")
|
|
16
17
|
$stdout.set_encoding("UTF-8")
|
|
17
|
-
super
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def open
|
|
21
21
|
@open = true
|
|
22
|
+
@session = ServerSession.new(server: @server, transport: self)
|
|
22
23
|
while @open && (line = $stdin.gets)
|
|
23
|
-
|
|
24
|
+
response = @session.handle_json(line.strip)
|
|
25
|
+
send_response(response) if response
|
|
24
26
|
end
|
|
25
27
|
rescue Interrupt
|
|
26
28
|
warn("\nExiting...")
|