mcp-sdk.rb 0.1.2 → 0.1.4

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,300 @@
1
+ require "json"
2
+ require "sinatra/base"
3
+ require "thread"
4
+
5
+ module MCP
6
+ # Enhanced SSE server implementation with Angelo-like features but using Sinatra
7
+ class EnhancedSSEServer < Sinatra::Base
8
+ attr_reader :mcp_server_instance, :sse_connections, :ws_connections
9
+
10
+ def initialize(mcp_server_instance)
11
+ @mcp_server_instance = mcp_server_instance
12
+ @sse_connections = []
13
+ @ws_connections = {}
14
+ @connection_mutex = Mutex.new
15
+ super()
16
+ end
17
+
18
+ configure do
19
+ set :server, :puma
20
+ set :bind, '0.0.0.0'
21
+ set :logging, true
22
+ end
23
+
24
+ # Enable CORS for all routes
25
+ before do
26
+ headers 'Access-Control-Allow-Origin' => '*',
27
+ 'Access-Control-Allow-Methods' => 'GET, POST, OPTIONS',
28
+ 'Access-Control-Allow-Headers' => 'Content-Type'
29
+ end
30
+
31
+ # Handle OPTIONS requests for CORS
32
+ options '*' do
33
+ 200
34
+ end
35
+
36
+ # Standard SSE endpoint - returns the message endpoint for subsequent requests
37
+ get '/sse' do
38
+ content_type 'text/event-stream'
39
+ headers 'Cache-Control' => 'no-cache',
40
+ 'Connection' => 'keep-alive'
41
+
42
+ # Send endpoint event as per MCP SSE protocol
43
+ response = "event: endpoint\n"
44
+ response += "data: /mcp/message\n\n"
45
+ response
46
+ end
47
+
48
+ # Enhanced SSE endpoint with connection management
49
+ get '/sse/events' do
50
+ content_type 'text/event-stream'
51
+ headers 'Cache-Control' => 'no-cache',
52
+ 'Connection' => 'keep-alive',
53
+ 'X-Accel-Buffering' => 'no' # Disable nginx buffering
54
+
55
+ # Create a unique connection ID
56
+ connection_id = SecureRandom.hex(8)
57
+
58
+ # Add connection to our list
59
+ @connection_mutex.synchronize do
60
+ @sse_connections << {
61
+ id: connection_id,
62
+ response: response,
63
+ created_at: Time.now
64
+ }
65
+ end
66
+
67
+ stream do |out|
68
+ begin
69
+ # Send initial endpoint event
70
+ out << "event: endpoint\n"
71
+ out << "data: /mcp/message\n\n"
72
+
73
+ # Send connection established event
74
+ out << "event: connected\n"
75
+ out << "data: #{connection_id}\n\n"
76
+
77
+ # Keep connection alive
78
+ loop do
79
+ sleep 30
80
+ out << "event: heartbeat\n"
81
+ out << "data: #{Time.now.to_i}\n\n"
82
+ end
83
+ rescue => e
84
+ puts "SSE connection error: #{e.message}"
85
+ ensure
86
+ # Clean up connection
87
+ @connection_mutex.synchronize do
88
+ @sse_connections.reject! { |conn| conn[:id] == connection_id }
89
+ end
90
+ end
91
+ end
92
+ end
93
+
94
+ # MCP message endpoint - handles POST requests and returns SSE responses
95
+ post '/mcp/message' do
96
+ content_type 'text/event-stream'
97
+ headers 'Cache-Control' => 'no-cache',
98
+ 'Connection' => 'keep-alive'
99
+
100
+ begin
101
+ request_data = JSON.parse(request.body.read)
102
+ response = @mcp_server_instance.send(:handle_request, request_data)
103
+
104
+ # Return JSON-RPC response in SSE format
105
+ sse_response = "data: #{response.to_json}\n\n"
106
+ sse_response
107
+ rescue JSON::ParserError => e
108
+ error_response = {
109
+ jsonrpc: "2.0",
110
+ id: nil,
111
+ error: {
112
+ code: -32700,
113
+ message: "Parse error: #{e.message}"
114
+ }
115
+ }
116
+ "data: #{error_response.to_json}\n\n"
117
+ rescue => e
118
+ error_response = {
119
+ jsonrpc: "2.0",
120
+ id: nil,
121
+ error: {
122
+ code: -32603,
123
+ message: "Internal error: #{e.message}"
124
+ }
125
+ }
126
+ "data: #{error_response.to_json}\n\n"
127
+ end
128
+ end
129
+
130
+ # Broadcast endpoint - sends messages to all connected SSE clients
131
+ post '/mcp/broadcast' do
132
+ content_type 'application/json'
133
+
134
+ begin
135
+ request_data = JSON.parse(request.body.read)
136
+ response = @mcp_server_instance.send(:handle_request, request_data)
137
+
138
+ # Broadcast to all connected SSE clients
139
+ broadcasted_count = 0
140
+ @connection_mutex.synchronize do
141
+ @sse_connections.each do |connection|
142
+ begin
143
+ connection[:response] << "event: broadcast\n"
144
+ connection[:response] << "data: #{response.to_json}\n\n"
145
+ broadcasted_count += 1
146
+ rescue => e
147
+ puts "Broadcast error to connection #{connection[:id]}: #{e.message}"
148
+ end
149
+ end
150
+ end
151
+
152
+ {
153
+ status: 'broadcasted',
154
+ message: 'Message sent to all connected clients',
155
+ clients: broadcasted_count,
156
+ response: response
157
+ }.to_json
158
+ rescue JSON::ParserError => e
159
+ error_response = {
160
+ jsonrpc: "2.0",
161
+ id: nil,
162
+ error: {
163
+ code: -32700,
164
+ message: "Parse error: #{e.message}"
165
+ }
166
+ }
167
+
168
+ # Broadcast error to all clients
169
+ @connection_mutex.synchronize do
170
+ @sse_connections.each do |connection|
171
+ begin
172
+ connection[:response] << "event: error\n"
173
+ connection[:response] << "data: #{error_response.to_json}\n\n"
174
+ rescue
175
+ # Ignore broadcast errors
176
+ end
177
+ end
178
+ end
179
+
180
+ { status: 'error', message: e.message }.to_json
181
+ rescue => e
182
+ error_response = {
183
+ jsonrpc: "2.0",
184
+ id: nil,
185
+ error: {
186
+ code: -32603,
187
+ message: "Internal error: #{e.message}"
188
+ }
189
+ }
190
+
191
+ # Broadcast error to all clients
192
+ @connection_mutex.synchronize do
193
+ @sse_connections.each do |connection|
194
+ begin
195
+ connection[:response] << "event: error\n"
196
+ connection[:response] << "data: #{error_response.to_json}\n\n"
197
+ rescue
198
+ # Ignore broadcast errors
199
+ end
200
+ end
201
+ end
202
+
203
+ { status: 'error', message: e.message }.to_json
204
+ end
205
+ end
206
+
207
+ # WebSocket simulation endpoint (using long polling)
208
+ get '/ws/connect' do
209
+ content_type 'text/event-stream'
210
+ headers 'Cache-Control' => 'no-cache',
211
+ 'Connection' => 'keep-alive'
212
+
213
+ connection_id = SecureRandom.hex(8)
214
+
215
+ stream do |out|
216
+ @ws_connections[connection_id] = out
217
+
218
+ begin
219
+ out << "event: ws_connected\n"
220
+ out << "data: #{connection_id}\n\n"
221
+
222
+ # Keep connection alive and handle incoming messages
223
+ loop do
224
+ sleep 1
225
+ # In a real WebSocket implementation, this would handle bidirectional communication
226
+ # For now, we just keep the connection alive
227
+ end
228
+ rescue => e
229
+ puts "WebSocket simulation error: #{e.message}"
230
+ ensure
231
+ @ws_connections.delete(connection_id)
232
+ end
233
+ end
234
+ end
235
+
236
+ # Send message to WebSocket connection
237
+ post '/ws/send/:connection_id' do
238
+ connection_id = params[:connection_id]
239
+
240
+ if @ws_connections[connection_id]
241
+ begin
242
+ request_data = JSON.parse(request.body.read)
243
+ response = @mcp_server_instance.send(:handle_request, request_data)
244
+
245
+ @ws_connections[connection_id] << "event: ws_message\n"
246
+ @ws_connections[connection_id] << "data: #{response.to_json}\n\n"
247
+
248
+ { status: 'sent', connection_id: connection_id }.to_json
249
+ rescue => e
250
+ { status: 'error', message: e.message }.to_json
251
+ end
252
+ else
253
+ status 404
254
+ { status: 'error', message: 'Connection not found' }.to_json
255
+ end
256
+ end
257
+
258
+ # Health check endpoint with enhanced information
259
+ get '/health' do
260
+ content_type 'application/json'
261
+ {
262
+ status: 'ok',
263
+ server: @mcp_server_instance.name,
264
+ version: @mcp_server_instance.version,
265
+ type: 'enhanced_sse',
266
+ tools_count: @mcp_server_instance.tools.size,
267
+ protocol: 'MCP SSE (Enhanced Sinatra)',
268
+ endpoints: {
269
+ sse: '/sse',
270
+ sse_events: '/sse/events',
271
+ message: '/mcp/message',
272
+ broadcast: '/mcp/broadcast',
273
+ ws_connect: '/ws/connect',
274
+ ws_send: '/ws/send/:connection_id'
275
+ },
276
+ connections: {
277
+ sse: @sse_connections.length,
278
+ ws: @ws_connections.length
279
+ },
280
+ uptime: Time.now.to_i
281
+ }.to_json
282
+ end
283
+
284
+ # Connection status endpoint
285
+ get '/connections' do
286
+ content_type 'application/json'
287
+ {
288
+ sse_connections: @sse_connections.map { |conn|
289
+ {
290
+ id: conn[:id],
291
+ created_at: conn[:created_at].iso8601,
292
+ age_seconds: (Time.now - conn[:created_at]).to_i
293
+ }
294
+ },
295
+ ws_connections: @ws_connections.keys,
296
+ total: @sse_connections.length + @ws_connections.length
297
+ }.to_json
298
+ end
299
+ end
300
+ end