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.
@@ -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