ruby-mcp-client 0.6.0 → 0.6.1
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/lib/mcp_client/json_rpc_common.rb +84 -0
- data/lib/mcp_client/server_factory.rb +52 -18
- data/lib/mcp_client/server_sse/json_rpc_transport.rb +246 -0
- data/lib/mcp_client/server_sse/reconnect_monitor.rb +226 -0
- data/lib/mcp_client/server_sse/sse_parser.rb +131 -0
- data/lib/mcp_client/server_sse.rb +53 -678
- data/lib/mcp_client/server_stdio/json_rpc_transport.rb +122 -0
- data/lib/mcp_client/server_stdio.rb +33 -125
- data/lib/mcp_client/version.rb +4 -1
- data/lib/mcp_client.rb +10 -4
- metadata +7 -2
@@ -0,0 +1,131 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
|
5
|
+
module MCPClient
|
6
|
+
class ServerSSE
|
7
|
+
# === Wire-level SSE parsing & dispatch ===
|
8
|
+
module SseParser
|
9
|
+
# Parse and handle a raw SSE event payload.
|
10
|
+
# @param event_data [String] the raw event chunk
|
11
|
+
def parse_and_handle_sse_event(event_data)
|
12
|
+
event = parse_sse_event(event_data)
|
13
|
+
return if event.nil?
|
14
|
+
|
15
|
+
case event[:event]
|
16
|
+
when 'endpoint'
|
17
|
+
handle_endpoint_event(event[:data])
|
18
|
+
when 'ping'
|
19
|
+
# no-op
|
20
|
+
when 'message'
|
21
|
+
handle_message_event(event)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
# Handle a "message" SSE event (payload is JSON-RPC over SSE)
|
26
|
+
# @param event [Hash] the parsed SSE event (with :data, :id, etc)
|
27
|
+
def handle_message_event(event)
|
28
|
+
return if event[:data].empty?
|
29
|
+
|
30
|
+
begin
|
31
|
+
data = JSON.parse(event[:data])
|
32
|
+
|
33
|
+
return if process_error_in_message(data)
|
34
|
+
return if process_notification(data)
|
35
|
+
|
36
|
+
process_response(data)
|
37
|
+
rescue MCPClient::Errors::ConnectionError
|
38
|
+
raise
|
39
|
+
rescue JSON::ParserError => e
|
40
|
+
@logger.warn("Failed to parse JSON from event data: #{e.message}")
|
41
|
+
rescue StandardError => e
|
42
|
+
@logger.error("Error processing SSE event: #{e.message}")
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
# Process a JSON-RPC error() in the SSE stream.
|
47
|
+
# @param data [Hash] the parsed JSON payload
|
48
|
+
# @return [Boolean] true if we saw & handled an error
|
49
|
+
def process_error_in_message(data)
|
50
|
+
return unless data['error']
|
51
|
+
|
52
|
+
error_message = data['error']['message'] || 'Unknown server error'
|
53
|
+
error_code = data['error']['code']
|
54
|
+
|
55
|
+
handle_sse_auth_error_message(error_message) if authorization_error?(error_message, error_code)
|
56
|
+
|
57
|
+
@logger.error("Server error: #{error_message}")
|
58
|
+
true
|
59
|
+
end
|
60
|
+
|
61
|
+
# Process a JSON-RPC notification (no id => notification)
|
62
|
+
# @param data [Hash] the parsed JSON payload
|
63
|
+
# @return [Boolean] true if we saw & handled a notification
|
64
|
+
def process_notification(data)
|
65
|
+
return false unless data['method'] && !data.key?('id')
|
66
|
+
|
67
|
+
@notification_callback&.call(data['method'], data['params'])
|
68
|
+
true
|
69
|
+
end
|
70
|
+
|
71
|
+
# Process a JSON-RPC response (id => response)
|
72
|
+
# @param data [Hash] the parsed JSON payload
|
73
|
+
# @return [Boolean] true if we saw & handled a response
|
74
|
+
def process_response(data)
|
75
|
+
return false unless data['id']
|
76
|
+
|
77
|
+
@mutex.synchronize do
|
78
|
+
@tools_data = data['result']['tools'] if data['result'] && data['result']['tools']
|
79
|
+
|
80
|
+
@sse_results[data['id']] =
|
81
|
+
if data['error']
|
82
|
+
{ 'isError' => true,
|
83
|
+
'content' => [{ 'type' => 'text', 'text' => data['error'].to_json }] }
|
84
|
+
else
|
85
|
+
data['result']
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
true
|
90
|
+
end
|
91
|
+
|
92
|
+
# Parse a raw SSE chunk into its :event, :data, :id fields
|
93
|
+
# @param event_data [String] the raw SSE block
|
94
|
+
# @return [Hash,nil] parsed fields or nil if it was pure comment/blank
|
95
|
+
def parse_sse_event(event_data)
|
96
|
+
event = { event: 'message', data: '', id: nil }
|
97
|
+
data_lines = []
|
98
|
+
has_content = false
|
99
|
+
|
100
|
+
event_data.each_line do |line|
|
101
|
+
line = line.chomp
|
102
|
+
next if line.empty? # blank line
|
103
|
+
next if line.start_with?(':') # SSE comment
|
104
|
+
|
105
|
+
has_content = true
|
106
|
+
if line.start_with?('event:')
|
107
|
+
event[:event] = line[6..].strip
|
108
|
+
elsif line.start_with?('data:')
|
109
|
+
data_lines << line[5..].strip
|
110
|
+
elsif line.start_with?('id:')
|
111
|
+
event[:id] = line[3..].strip
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
event[:data] = data_lines.join("\n")
|
116
|
+
has_content ? event : nil
|
117
|
+
end
|
118
|
+
|
119
|
+
# Handle the special "endpoint" control frame (for SSE handshake)
|
120
|
+
# @param data [String] the raw endpoint payload
|
121
|
+
def handle_endpoint_event(data)
|
122
|
+
@mutex.synchronize do
|
123
|
+
@rpc_endpoint = data
|
124
|
+
@sse_connected = true
|
125
|
+
@connection_established = true
|
126
|
+
@connection_cv.broadcast
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|