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.
@@ -92,7 +92,7 @@ module JsonRpcHandler
92
92
  end
93
93
 
94
94
  begin
95
- method = method_finder.call(method_name)
95
+ method = method_finder.call(method_name, id)
96
96
 
97
97
  if method.nil?
98
98
  return error_response(id: id, id_validation_pattern: id_validation_pattern, error: {
@@ -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 = transport.send_request(request: {
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 = transport.send_request(request: {
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 = transport.send_request(request: {
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 = transport.send_request(request: {
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
- transport.send_request(request: {
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 = transport.send_request(request: {
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 = transport.send_request(request: {
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 request_id
163
- SecureRandom.uuid
164
- end
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
- class RequestHandlerError < StandardError
167
- attr_reader :error_type, :original_error, :request
176
+ response = transport.send_request(request: request_body)
168
177
 
169
- def initialize(message, request, error_type: :internal_error, original_error: nil)
170
- super(message)
171
- @request = request
172
- @error_type = error_type
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