mcp 0.10.0 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +577 -355
- data/lib/json_rpc_handler.rb +1 -1
- data/lib/mcp/client/http.rb +4 -1
- data/lib/mcp/client.rb +62 -48
- data/lib/mcp/progress.rb +3 -1
- data/lib/mcp/server/transports/stdio_transport.rb +35 -0
- data/lib/mcp/server/transports/streamable_http_transport.rb +289 -83
- data/lib/mcp/server.rb +181 -23
- data/lib/mcp/server_context.rb +16 -2
- data/lib/mcp/server_session.rb +35 -7
- data/lib/mcp/tool/schema.rb +1 -14
- data/lib/mcp/transport.rb +14 -0
- data/lib/mcp/version.rb +1 -1
- metadata +5 -3
data/lib/json_rpc_handler.rb
CHANGED
data/lib/mcp/client/http.rb
CHANGED
|
@@ -7,9 +7,10 @@ module MCP
|
|
|
7
7
|
|
|
8
8
|
attr_reader :url
|
|
9
9
|
|
|
10
|
-
def initialize(url:, headers: {})
|
|
10
|
+
def initialize(url:, headers: {}, &block)
|
|
11
11
|
@url = url
|
|
12
12
|
@headers = headers
|
|
13
|
+
@faraday_customizer = block
|
|
13
14
|
end
|
|
14
15
|
|
|
15
16
|
def send_request(request:)
|
|
@@ -78,6 +79,8 @@ module MCP
|
|
|
78
79
|
headers.each do |key, value|
|
|
79
80
|
faraday.headers[key] = value
|
|
80
81
|
end
|
|
82
|
+
|
|
83
|
+
@faraday_customizer&.call(faraday)
|
|
81
84
|
end
|
|
82
85
|
end
|
|
83
86
|
|
data/lib/mcp/client.rb
CHANGED
|
@@ -6,6 +6,27 @@ require_relative "client/tool"
|
|
|
6
6
|
|
|
7
7
|
module MCP
|
|
8
8
|
class Client
|
|
9
|
+
class ServerError < StandardError
|
|
10
|
+
attr_reader :code, :data
|
|
11
|
+
|
|
12
|
+
def initialize(message, code:, data: nil)
|
|
13
|
+
super(message)
|
|
14
|
+
@code = code
|
|
15
|
+
@data = data
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
class RequestHandlerError < StandardError
|
|
20
|
+
attr_reader :error_type, :original_error, :request
|
|
21
|
+
|
|
22
|
+
def initialize(message, request, error_type: :internal_error, original_error: nil)
|
|
23
|
+
super(message)
|
|
24
|
+
@request = request
|
|
25
|
+
@error_type = error_type
|
|
26
|
+
@original_error = original_error
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
9
30
|
# Initializes a new MCP::Client instance.
|
|
10
31
|
#
|
|
11
32
|
# @param transport [Object] The transport object to use for communication with the server.
|
|
@@ -33,11 +54,7 @@ module MCP
|
|
|
33
54
|
# puts tool.name
|
|
34
55
|
# end
|
|
35
56
|
def tools
|
|
36
|
-
response =
|
|
37
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
38
|
-
id: request_id,
|
|
39
|
-
method: "tools/list",
|
|
40
|
-
})
|
|
57
|
+
response = request(method: "tools/list")
|
|
41
58
|
|
|
42
59
|
response.dig("result", "tools")&.map do |tool|
|
|
43
60
|
Tool.new(
|
|
@@ -53,11 +70,7 @@ module MCP
|
|
|
53
70
|
#
|
|
54
71
|
# @return [Array<Hash>] An array of available resources.
|
|
55
72
|
def resources
|
|
56
|
-
response =
|
|
57
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
58
|
-
id: request_id,
|
|
59
|
-
method: "resources/list",
|
|
60
|
-
})
|
|
73
|
+
response = request(method: "resources/list")
|
|
61
74
|
|
|
62
75
|
response.dig("result", "resources") || []
|
|
63
76
|
end
|
|
@@ -67,11 +80,7 @@ module MCP
|
|
|
67
80
|
#
|
|
68
81
|
# @return [Array<Hash>] An array of available resource templates.
|
|
69
82
|
def resource_templates
|
|
70
|
-
response =
|
|
71
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
72
|
-
id: request_id,
|
|
73
|
-
method: "resources/templates/list",
|
|
74
|
-
})
|
|
83
|
+
response = request(method: "resources/templates/list")
|
|
75
84
|
|
|
76
85
|
response.dig("result", "resourceTemplates") || []
|
|
77
86
|
end
|
|
@@ -81,11 +90,7 @@ module MCP
|
|
|
81
90
|
#
|
|
82
91
|
# @return [Array<Hash>] An array of available prompts.
|
|
83
92
|
def prompts
|
|
84
|
-
response =
|
|
85
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
86
|
-
id: request_id,
|
|
87
|
-
method: "prompts/list",
|
|
88
|
-
})
|
|
93
|
+
response = request(method: "prompts/list")
|
|
89
94
|
|
|
90
95
|
response.dig("result", "prompts") || []
|
|
91
96
|
end
|
|
@@ -119,12 +124,7 @@ module MCP
|
|
|
119
124
|
params[:_meta] = { progressToken: progress_token }
|
|
120
125
|
end
|
|
121
126
|
|
|
122
|
-
|
|
123
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
124
|
-
id: request_id,
|
|
125
|
-
method: "tools/call",
|
|
126
|
-
params: params,
|
|
127
|
-
})
|
|
127
|
+
request(method: "tools/call", params: params)
|
|
128
128
|
end
|
|
129
129
|
|
|
130
130
|
# Reads a resource from the server by URI and returns the contents.
|
|
@@ -132,12 +132,7 @@ module MCP
|
|
|
132
132
|
# @param uri [String] The URI of the resource to read.
|
|
133
133
|
# @return [Array<Hash>] An array of resource contents (text or blob).
|
|
134
134
|
def read_resource(uri:)
|
|
135
|
-
response =
|
|
136
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
137
|
-
id: request_id,
|
|
138
|
-
method: "resources/read",
|
|
139
|
-
params: { uri: uri },
|
|
140
|
-
})
|
|
135
|
+
response = request(method: "resources/read", params: { uri: uri })
|
|
141
136
|
|
|
142
137
|
response.dig("result", "contents") || []
|
|
143
138
|
end
|
|
@@ -147,31 +142,50 @@ module MCP
|
|
|
147
142
|
# @param name [String] The name of the prompt to get.
|
|
148
143
|
# @return [Hash] A hash containing the prompt details.
|
|
149
144
|
def get_prompt(name:)
|
|
150
|
-
response =
|
|
151
|
-
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
152
|
-
id: request_id,
|
|
153
|
-
method: "prompts/get",
|
|
154
|
-
params: { name: name },
|
|
155
|
-
})
|
|
145
|
+
response = request(method: "prompts/get", params: { name: name })
|
|
156
146
|
|
|
157
147
|
response.fetch("result", {})
|
|
158
148
|
end
|
|
159
149
|
|
|
150
|
+
# Requests completion suggestions from the server for a prompt argument or resource template URI.
|
|
151
|
+
#
|
|
152
|
+
# @param ref [Hash] The reference, e.g. `{ type: "ref/prompt", name: "my_prompt" }`
|
|
153
|
+
# or `{ type: "ref/resource", uri: "file:///{path}" }`.
|
|
154
|
+
# @param argument [Hash] The argument being completed, e.g. `{ name: "language", value: "py" }`.
|
|
155
|
+
# @param context [Hash, nil] Optional context with previously resolved arguments.
|
|
156
|
+
# @return [Hash] The completion result with `"values"`, `"hasMore"`, and optionally `"total"`.
|
|
157
|
+
def complete(ref:, argument:, context: nil)
|
|
158
|
+
params = { ref: ref, argument: argument }
|
|
159
|
+
params[:context] = context if context
|
|
160
|
+
|
|
161
|
+
response = request(method: "completion/complete", params: params)
|
|
162
|
+
|
|
163
|
+
response.dig("result", "completion") || { "values" => [], "hasMore" => false }
|
|
164
|
+
end
|
|
165
|
+
|
|
160
166
|
private
|
|
161
167
|
|
|
162
|
-
def
|
|
163
|
-
|
|
164
|
-
|
|
168
|
+
def request(method:, params: nil)
|
|
169
|
+
request_body = {
|
|
170
|
+
jsonrpc: JsonRpcHandler::Version::V2_0,
|
|
171
|
+
id: request_id,
|
|
172
|
+
method: method,
|
|
173
|
+
}
|
|
174
|
+
request_body[:params] = params if params
|
|
165
175
|
|
|
166
|
-
|
|
167
|
-
attr_reader :error_type, :original_error, :request
|
|
176
|
+
response = transport.send_request(request: request_body)
|
|
168
177
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
@original_error = original_error
|
|
178
|
+
# Guard with `is_a?(Hash)` because custom transports may return non-Hash values.
|
|
179
|
+
if response.is_a?(Hash) && response.key?("error")
|
|
180
|
+
error = response["error"]
|
|
181
|
+
raise ServerError.new(error["message"], code: error["code"], data: error["data"])
|
|
174
182
|
end
|
|
183
|
+
|
|
184
|
+
response
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def request_id
|
|
188
|
+
SecureRandom.uuid
|
|
175
189
|
end
|
|
176
190
|
end
|
|
177
191
|
end
|
data/lib/mcp/progress.rb
CHANGED
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
|
|
3
3
|
module MCP
|
|
4
4
|
class Progress
|
|
5
|
-
def initialize(notification_target:, progress_token:)
|
|
5
|
+
def initialize(notification_target:, progress_token:, related_request_id: nil)
|
|
6
6
|
@notification_target = notification_target
|
|
7
7
|
@progress_token = progress_token
|
|
8
|
+
@related_request_id = related_request_id
|
|
8
9
|
end
|
|
9
10
|
|
|
10
11
|
def report(progress, total: nil, message: nil)
|
|
@@ -16,6 +17,7 @@ module MCP
|
|
|
16
17
|
progress: progress,
|
|
17
18
|
total: total,
|
|
18
19
|
message: message,
|
|
20
|
+
related_request_id: @related_request_id,
|
|
19
21
|
)
|
|
20
22
|
end
|
|
21
23
|
end
|
|
@@ -53,6 +53,41 @@ module MCP
|
|
|
53
53
|
MCP.configuration.exception_reporter.call(e, { error: "Failed to send notification" })
|
|
54
54
|
false
|
|
55
55
|
end
|
|
56
|
+
|
|
57
|
+
def send_request(method, params = nil)
|
|
58
|
+
request_id = generate_request_id
|
|
59
|
+
request = { jsonrpc: "2.0", id: request_id, method: method }
|
|
60
|
+
request[:params] = params if params
|
|
61
|
+
|
|
62
|
+
begin
|
|
63
|
+
send_response(request)
|
|
64
|
+
rescue => e
|
|
65
|
+
MCP.configuration.exception_reporter.call(e, { error: "Failed to send request" })
|
|
66
|
+
raise
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
while @open && (line = $stdin.gets)
|
|
70
|
+
begin
|
|
71
|
+
parsed = JSON.parse(line.strip, symbolize_names: true)
|
|
72
|
+
rescue JSON::ParserError => e
|
|
73
|
+
MCP.configuration.exception_reporter.call(e, { error: "Failed to parse response" })
|
|
74
|
+
raise
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
if parsed[:id] == request_id && !parsed.key?(:method)
|
|
78
|
+
if parsed[:error]
|
|
79
|
+
raise StandardError, "Client returned an error for #{method} request (code: #{parsed[:error][:code]}): #{parsed[:error][:message]}"
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
return parsed[:result]
|
|
83
|
+
else
|
|
84
|
+
response = @session ? @session.handle(parsed) : @server.handle(parsed)
|
|
85
|
+
send_response(response) if response
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
raise "Transport closed while waiting for response to #{method} request."
|
|
90
|
+
end
|
|
56
91
|
end
|
|
57
92
|
end
|
|
58
93
|
end
|