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.
- checksums.yaml +4 -4
- data/README.md +288 -5
- data/lib/mcp/angelo_sse_server.rb +200 -0
- data/lib/mcp/enhanced_sse_server.rb +300 -0
- data/lib/mcp/server.rb +629 -0
- data/lib/mcp/sse_client.rb +3 -0
- data/lib/mcp/stdio_client.rb +3 -1
- data/lib/mcp.rb +2 -1
- metadata +13 -22
|
@@ -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
|