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
data/lib/mcp/server.rb
ADDED
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "sinatra/base"
|
|
3
|
+
require "puma"
|
|
4
|
+
require "securerandom"
|
|
5
|
+
require_relative "enhanced_sse_server"
|
|
6
|
+
|
|
7
|
+
module MCP
|
|
8
|
+
class Server
|
|
9
|
+
attr_reader :name, :version, :type, :port, :tools
|
|
10
|
+
|
|
11
|
+
def initialize(name:, version:, type: "stdio", port: nil)
|
|
12
|
+
@name = name
|
|
13
|
+
@version = version
|
|
14
|
+
@type = type.to_s
|
|
15
|
+
@port = port
|
|
16
|
+
@tools = {}
|
|
17
|
+
@running = false
|
|
18
|
+
|
|
19
|
+
validate_configuration
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def add_tool(name, &block)
|
|
23
|
+
unless block_given?
|
|
24
|
+
raise ArgumentError, "Block required for tool '#{name}'"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
@tools[name.to_s] = block
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def start(io_in = $stdin, io_out = $stdout)
|
|
31
|
+
@running = true
|
|
32
|
+
|
|
33
|
+
case @type
|
|
34
|
+
when "stdio"
|
|
35
|
+
start_stdio_server(io_in, io_out)
|
|
36
|
+
when "sse"
|
|
37
|
+
start_sse_server
|
|
38
|
+
when "enhanced_sse"
|
|
39
|
+
start_enhanced_sse_server
|
|
40
|
+
else
|
|
41
|
+
raise ArgumentError, "Unknown server type: #{@type}"
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def stop
|
|
46
|
+
@running = false
|
|
47
|
+
@sse_server.stop if @sse_server
|
|
48
|
+
@enhanced_server.stop if @enhanced_server
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def list_tools
|
|
52
|
+
tool_list = @tools.keys.map do |tool_name|
|
|
53
|
+
{
|
|
54
|
+
name: tool_name,
|
|
55
|
+
description: "Tool: #{tool_name}",
|
|
56
|
+
inputSchema: {
|
|
57
|
+
type: "object",
|
|
58
|
+
properties: {},
|
|
59
|
+
required: []
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
{
|
|
65
|
+
tools: tool_list
|
|
66
|
+
}
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def call_tool(name, arguments = {})
|
|
70
|
+
tool_name = name.to_s
|
|
71
|
+
|
|
72
|
+
unless @tools.key?(tool_name)
|
|
73
|
+
raise Error, "Tool '#{tool_name}' not found"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
begin
|
|
77
|
+
result = @tools[tool_name].call(arguments)
|
|
78
|
+
|
|
79
|
+
# Ensure result has the expected MCP format
|
|
80
|
+
if result.is_a?(Hash) && result.key?(:content)
|
|
81
|
+
result
|
|
82
|
+
else
|
|
83
|
+
# Wrap simple results in MCP format
|
|
84
|
+
{
|
|
85
|
+
content: [{ type: "text", text: result.to_s }]
|
|
86
|
+
}
|
|
87
|
+
end
|
|
88
|
+
rescue => e
|
|
89
|
+
raise Error, "Error executing tool '#{tool_name}': #{e.message}"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
private
|
|
94
|
+
|
|
95
|
+
def validate_configuration
|
|
96
|
+
case @type
|
|
97
|
+
when "stdio"
|
|
98
|
+
# No additional validation needed for stdio
|
|
99
|
+
when "sse", "enhanced_sse"
|
|
100
|
+
if @port.nil?
|
|
101
|
+
raise ArgumentError, "Port is required for SSE server type"
|
|
102
|
+
end
|
|
103
|
+
unless @port.is_a?(Integer) && @port > 0 && @port < 65536
|
|
104
|
+
raise ArgumentError, "Port must be a valid integer between 1 and 65535"
|
|
105
|
+
end
|
|
106
|
+
else
|
|
107
|
+
raise ArgumentError, "Server type must be 'stdio', 'sse', or 'enhanced_sse'"
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def start_stdio_server(io_in, io_out)
|
|
112
|
+
@io_in = io_in
|
|
113
|
+
@io_out = io_out
|
|
114
|
+
|
|
115
|
+
puts "MCP Server '#{@name}' v#{@version} (stdio) starting..."
|
|
116
|
+
puts "Available tools: #{@tools.keys.join(', ')}"
|
|
117
|
+
puts "Ready to accept JSON-RPC requests on stdin..."
|
|
118
|
+
|
|
119
|
+
process_stdio_requests
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def start_sse_server
|
|
123
|
+
puts "MCP Server '#{@name}' v#{@version} (SSE) starting on port #{@port}..."
|
|
124
|
+
puts "Available tools: #{@tools.keys.join(', ')}"
|
|
125
|
+
|
|
126
|
+
app = create_sse_app
|
|
127
|
+
@sse_server = Puma::Server.new(app)
|
|
128
|
+
@sse_server.add_tcp_listener("0.0.0.0", @port)
|
|
129
|
+
|
|
130
|
+
puts "SSE Server ready at http://localhost:#{@port}"
|
|
131
|
+
puts "MCP SSE Protocol Endpoints:"
|
|
132
|
+
puts " GET /sse - Get message endpoint (returns 'event: endpoint\\ndata: /mcp/message')"
|
|
133
|
+
puts " POST /mcp/message - Send JSON-RPC requests and receive SSE responses"
|
|
134
|
+
puts " GET /health - Health check"
|
|
135
|
+
|
|
136
|
+
@sse_server.run.join
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def start_enhanced_sse_server
|
|
140
|
+
puts "MCP Server '#{@name}' v#{@version} (Enhanced SSE) starting on port #{@port}..."
|
|
141
|
+
puts "Available tools: #{@tools.keys.join(', ')}"
|
|
142
|
+
|
|
143
|
+
# Create Enhanced SSE server app
|
|
144
|
+
app = create_enhanced_sse_app
|
|
145
|
+
@enhanced_server = Puma::Server.new(app)
|
|
146
|
+
@enhanced_server.add_tcp_listener("0.0.0.0", @port)
|
|
147
|
+
|
|
148
|
+
puts "Enhanced SSE Server ready at http://localhost:#{@port}"
|
|
149
|
+
puts "MCP SSE Protocol Endpoints (Enhanced):"
|
|
150
|
+
puts " GET /sse - Get message endpoint (returns 'event: endpoint\\ndata: /mcp/message')"
|
|
151
|
+
puts " POST /mcp/message - Send JSON-RPC requests and receive SSE responses"
|
|
152
|
+
puts " GET /sse/events - Advanced SSE endpoint with connection management"
|
|
153
|
+
puts " POST /mcp/broadcast - Broadcast to all connected SSE clients"
|
|
154
|
+
puts " GET /ws/connect - WebSocket-like connection (long polling)"
|
|
155
|
+
puts " POST /ws/send/:id - Send message to specific connection"
|
|
156
|
+
puts " GET /health - Health check with detailed info"
|
|
157
|
+
puts " GET /connections - View active connections"
|
|
158
|
+
|
|
159
|
+
# Start the Enhanced SSE server
|
|
160
|
+
@enhanced_server.run.join
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def process_stdio_requests
|
|
164
|
+
while @running
|
|
165
|
+
begin
|
|
166
|
+
line = @io_in.gets
|
|
167
|
+
break unless line
|
|
168
|
+
|
|
169
|
+
line = line.strip
|
|
170
|
+
next if line.empty?
|
|
171
|
+
|
|
172
|
+
request = JSON.parse(line)
|
|
173
|
+
response = handle_request(request)
|
|
174
|
+
|
|
175
|
+
@io_out.puts response.to_json
|
|
176
|
+
@io_out.flush
|
|
177
|
+
rescue JSON::ParserError => e
|
|
178
|
+
send_stdio_error_response(nil, -32700, "Parse error: #{e.message}")
|
|
179
|
+
rescue => e
|
|
180
|
+
send_stdio_error_response(nil, -32603, "Internal error: #{e.message}")
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def create_sse_app
|
|
186
|
+
server_instance = self
|
|
187
|
+
connections = []
|
|
188
|
+
connections_mutex = Mutex.new
|
|
189
|
+
|
|
190
|
+
Sinatra.new do
|
|
191
|
+
set :server, :puma
|
|
192
|
+
set :bind, '0.0.0.0'
|
|
193
|
+
set :port, server_instance.port
|
|
194
|
+
|
|
195
|
+
# Enable CORS
|
|
196
|
+
before do
|
|
197
|
+
headers 'Access-Control-Allow-Origin' => '*',
|
|
198
|
+
'Access-Control-Allow-Methods' => ['GET', 'POST', 'OPTIONS'],
|
|
199
|
+
'Access-Control-Allow-Headers' => 'Content-Type'
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
options '*' do
|
|
203
|
+
200
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# SSE endpoint - returns the message endpoint for subsequent requests
|
|
207
|
+
get '/sse' do
|
|
208
|
+
content_type 'text/event-stream'
|
|
209
|
+
headers 'Cache-Control' => 'no-cache',
|
|
210
|
+
'Connection' => 'keep-alive',
|
|
211
|
+
'X-Accel-Buffering' => 'no'
|
|
212
|
+
|
|
213
|
+
stream(:keep_open) do |out|
|
|
214
|
+
connections_mutex.synchronize do
|
|
215
|
+
connections << out
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Send endpoint event as per MCP SSE protocol
|
|
219
|
+
out << "event: endpoint\ndata: /mcp/message\n\n"
|
|
220
|
+
|
|
221
|
+
# Heartbeat timer to keep connection alive
|
|
222
|
+
heartbeat = ::EventMachine.add_periodic_timer(15) do
|
|
223
|
+
begin
|
|
224
|
+
out << "event: ping\n"
|
|
225
|
+
out << "data: #{Time.now.to_i}\n\n"
|
|
226
|
+
rescue => e
|
|
227
|
+
puts "Heartbeat error: #{e.message}"
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Clean up when connection closes
|
|
232
|
+
out.callback do
|
|
233
|
+
connections_mutex.synchronize do
|
|
234
|
+
connections.delete(out)
|
|
235
|
+
::EventMachine.cancel_timer(heartbeat)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
out.errback do
|
|
240
|
+
connections_mutex.synchronize do
|
|
241
|
+
connections.delete(out)
|
|
242
|
+
::EventMachine.cancel_timer(heartbeat)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
# MCP message endpoint - handles POST requests and returns SSE responses
|
|
249
|
+
post '/mcp/message' do
|
|
250
|
+
content_type 'text/event-stream'
|
|
251
|
+
headers 'Cache-Control' => 'no-cache',
|
|
252
|
+
'Connection' => 'keep-alive',
|
|
253
|
+
'X-Accel-Buffering' => 'no'
|
|
254
|
+
|
|
255
|
+
stream(:keep_open) do |out|
|
|
256
|
+
begin
|
|
257
|
+
request_data = JSON.parse(request.body.read)
|
|
258
|
+
response = server_instance.send(:handle_request, request_data)
|
|
259
|
+
|
|
260
|
+
# Return JSON-RPC response in SSE format
|
|
261
|
+
out << "data: #{response.to_json}\n\n"
|
|
262
|
+
rescue JSON::ParserError => e
|
|
263
|
+
error_response = {
|
|
264
|
+
jsonrpc: "2.0",
|
|
265
|
+
id: nil,
|
|
266
|
+
error: {
|
|
267
|
+
code: -32700,
|
|
268
|
+
message: "Parse error: #{e.message}"
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
out << "data: #{error_response.to_json}\n\n"
|
|
272
|
+
rescue => e
|
|
273
|
+
error_response = {
|
|
274
|
+
jsonrpc: "2.0",
|
|
275
|
+
id: nil,
|
|
276
|
+
error: {
|
|
277
|
+
code: -32603,
|
|
278
|
+
message: "Internal error: #{e.message}"
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
out << "data: #{error_response.to_json}\n\n"
|
|
282
|
+
ensure
|
|
283
|
+
out.close
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
# Health check endpoint
|
|
289
|
+
get '/health' do
|
|
290
|
+
content_type 'application/json'
|
|
291
|
+
{
|
|
292
|
+
status: 'ok',
|
|
293
|
+
server: server_instance.name,
|
|
294
|
+
version: server_instance.version,
|
|
295
|
+
type: server_instance.type,
|
|
296
|
+
tools_count: server_instance.tools.size,
|
|
297
|
+
protocol: 'MCP SSE',
|
|
298
|
+
active_connections: connections.length,
|
|
299
|
+
endpoints: {
|
|
300
|
+
sse: '/sse',
|
|
301
|
+
message: '/mcp/message'
|
|
302
|
+
}
|
|
303
|
+
}.to_json
|
|
304
|
+
end
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def create_enhanced_sse_app
|
|
309
|
+
server_instance = self
|
|
310
|
+
sse_connections = []
|
|
311
|
+
ws_connections = {}
|
|
312
|
+
connection_mutex = Mutex.new
|
|
313
|
+
|
|
314
|
+
Sinatra.new do
|
|
315
|
+
set :server, :puma
|
|
316
|
+
set :bind, '0.0.0.0'
|
|
317
|
+
set :port, server_instance.port
|
|
318
|
+
|
|
319
|
+
# Enable CORS
|
|
320
|
+
before do
|
|
321
|
+
headers 'Access-Control-Allow-Origin' => '*',
|
|
322
|
+
'Access-Control-Allow-Methods' => ['GET', 'POST', 'OPTIONS'],
|
|
323
|
+
'Access-Control-Allow-Headers' => 'Content-Type'
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
options '*' do
|
|
327
|
+
200
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
# Standard SSE endpoint
|
|
331
|
+
get '/sse' do
|
|
332
|
+
content_type 'text/event-stream'
|
|
333
|
+
headers 'Cache-Control' => 'no-cache',
|
|
334
|
+
'Connection' => 'keep-alive'
|
|
335
|
+
|
|
336
|
+
response = "event: endpoint\ndata: /mcp/message\n\n"
|
|
337
|
+
response
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Enhanced SSE endpoint with connection management
|
|
341
|
+
get '/sse/events' do
|
|
342
|
+
content_type 'text/event-stream'
|
|
343
|
+
headers 'Cache-Control' => 'no-cache',
|
|
344
|
+
'Connection' => 'keep-alive',
|
|
345
|
+
'X-Accel-Buffering' => 'no'
|
|
346
|
+
|
|
347
|
+
connection_id = SecureRandom.hex(8)
|
|
348
|
+
|
|
349
|
+
stream do |out|
|
|
350
|
+
connection_mutex.synchronize do
|
|
351
|
+
sse_connections << {
|
|
352
|
+
id: connection_id,
|
|
353
|
+
response: out,
|
|
354
|
+
created_at: Time.now
|
|
355
|
+
}
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
begin
|
|
359
|
+
out << "event: endpoint\n"
|
|
360
|
+
out << "data: /mcp/message\n\n"
|
|
361
|
+
|
|
362
|
+
out << "event: connected\n"
|
|
363
|
+
out << "data: #{connection_id}\n\n"
|
|
364
|
+
|
|
365
|
+
loop do
|
|
366
|
+
sleep 30
|
|
367
|
+
out << "event: heartbeat\n"
|
|
368
|
+
out << "data: #{Time.now.to_i}\n\n"
|
|
369
|
+
end
|
|
370
|
+
rescue => e
|
|
371
|
+
puts "SSE connection error: #{e.message}"
|
|
372
|
+
ensure
|
|
373
|
+
connection_mutex.synchronize do
|
|
374
|
+
sse_connections.reject! { |conn| conn[:id] == connection_id }
|
|
375
|
+
end
|
|
376
|
+
end
|
|
377
|
+
end
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
# MCP message endpoint
|
|
381
|
+
post '/mcp/message' do
|
|
382
|
+
content_type 'text/event-stream'
|
|
383
|
+
headers 'Cache-Control' => 'no-cache',
|
|
384
|
+
'Connection' => 'keep-alive'
|
|
385
|
+
|
|
386
|
+
begin
|
|
387
|
+
request_data = JSON.parse(request.body.read)
|
|
388
|
+
response = server_instance.send(:handle_request, request_data)
|
|
389
|
+
|
|
390
|
+
sse_response = "data: #{response.to_json}\n\n"
|
|
391
|
+
sse_response
|
|
392
|
+
rescue JSON::ParserError => e
|
|
393
|
+
error_response = {
|
|
394
|
+
jsonrpc: "2.0",
|
|
395
|
+
id: nil,
|
|
396
|
+
error: {
|
|
397
|
+
code: -32700,
|
|
398
|
+
message: "Parse error: #{e.message}"
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
"data: #{error_response.to_json}\n\n"
|
|
402
|
+
rescue => e
|
|
403
|
+
error_response = {
|
|
404
|
+
jsonrpc: "2.0",
|
|
405
|
+
id: nil,
|
|
406
|
+
error: {
|
|
407
|
+
code: -32603,
|
|
408
|
+
message: "Internal error: #{e.message}"
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
"data: #{error_response.to_json}\n\n"
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Broadcast endpoint - improved connection management
|
|
416
|
+
post '/mcp/broadcast' do
|
|
417
|
+
content_type 'application/json'
|
|
418
|
+
|
|
419
|
+
begin
|
|
420
|
+
request_data = JSON.parse(request.body.read)
|
|
421
|
+
response = server_instance.send(:handle_request, request_data)
|
|
422
|
+
|
|
423
|
+
broadcasted_count = 0
|
|
424
|
+
failed_connections = []
|
|
425
|
+
|
|
426
|
+
connection_mutex.synchronize do
|
|
427
|
+
sse_connections.each do |connection|
|
|
428
|
+
begin
|
|
429
|
+
connection[:response] << "event: broadcast\n"
|
|
430
|
+
connection[:response] << "data: #{response.to_json}\n\n"
|
|
431
|
+
broadcasted_count += 1
|
|
432
|
+
rescue => e
|
|
433
|
+
puts "Broadcast error to connection #{connection[:id]}: #{e.message}"
|
|
434
|
+
failed_connections << connection[:id]
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# Clean up failed connections
|
|
439
|
+
sse_connections.reject! { |conn| failed_connections.include?(conn[:id]) }
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
{
|
|
443
|
+
status: 'broadcasted',
|
|
444
|
+
message: 'Message sent to connected clients',
|
|
445
|
+
clients: broadcasted_count,
|
|
446
|
+
failed: failed_connections.length
|
|
447
|
+
}.to_json
|
|
448
|
+
rescue => e
|
|
449
|
+
{ status: 'error', message: e.message }.to_json
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
|
|
453
|
+
# WebSocket simulation - improved with proper cleanup
|
|
454
|
+
get '/ws/connect' do
|
|
455
|
+
content_type 'text/event-stream'
|
|
456
|
+
headers 'Cache-Control' => 'no-cache',
|
|
457
|
+
'Connection' => 'keep-alive',
|
|
458
|
+
'X-Accel-Buffering' => 'no'
|
|
459
|
+
|
|
460
|
+
connection_id = SecureRandom.hex(8)
|
|
461
|
+
|
|
462
|
+
stream(:keep_open) do |out|
|
|
463
|
+
connection_mutex.synchronize do
|
|
464
|
+
ws_connections[connection_id] = out
|
|
465
|
+
end
|
|
466
|
+
|
|
467
|
+
# Heartbeat for WebSocket simulation
|
|
468
|
+
heartbeat = ::EventMachine.add_periodic_timer(15) do
|
|
469
|
+
begin
|
|
470
|
+
out << "event: ping\n"
|
|
471
|
+
out << "data: #{Time.now.to_i}\n\n"
|
|
472
|
+
rescue => e
|
|
473
|
+
puts "WebSocket heartbeat error: #{e.message}"
|
|
474
|
+
end
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
out << "event: ws_connected\n"
|
|
478
|
+
out << "data: #{connection_id}\n\n"
|
|
479
|
+
|
|
480
|
+
out.callback do
|
|
481
|
+
connection_mutex.synchronize do
|
|
482
|
+
ws_connections.delete(connection_id)
|
|
483
|
+
::EventMachine.cancel_timer(heartbeat)
|
|
484
|
+
end
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
out.errback do
|
|
488
|
+
connection_mutex.synchronize do
|
|
489
|
+
ws_connections.delete(connection_id)
|
|
490
|
+
::EventMachine.cancel_timer(heartbeat)
|
|
491
|
+
end
|
|
492
|
+
end
|
|
493
|
+
end
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
# Send to WebSocket connection
|
|
497
|
+
post '/ws/send/:connection_id' do
|
|
498
|
+
connection_id = params[:connection_id]
|
|
499
|
+
|
|
500
|
+
connection_mutex.synchronize do
|
|
501
|
+
if ws_connections[connection_id]
|
|
502
|
+
begin
|
|
503
|
+
request_data = JSON.parse(request.body.read)
|
|
504
|
+
response = server_instance.send(:handle_request, request_data)
|
|
505
|
+
|
|
506
|
+
ws_connections[connection_id] << "event: ws_message\n"
|
|
507
|
+
ws_connections[connection_id] << "data: #{response.to_json}\n\n"
|
|
508
|
+
|
|
509
|
+
{ status: 'sent', connection_id: connection_id }.to_json
|
|
510
|
+
rescue => e
|
|
511
|
+
{ status: 'error', message: e.message }.to_json
|
|
512
|
+
ensure
|
|
513
|
+
# Clean up failed connection
|
|
514
|
+
ws_connections.delete(connection_id) if e
|
|
515
|
+
end
|
|
516
|
+
else
|
|
517
|
+
status 404
|
|
518
|
+
{ status: 'error', message: 'Connection not found' }.to_json
|
|
519
|
+
end
|
|
520
|
+
end
|
|
521
|
+
end
|
|
522
|
+
|
|
523
|
+
# Enhanced health check
|
|
524
|
+
get '/health' do
|
|
525
|
+
content_type 'application/json'
|
|
526
|
+
{
|
|
527
|
+
status: 'ok',
|
|
528
|
+
server: server_instance.name,
|
|
529
|
+
version: server_instance.version,
|
|
530
|
+
type: 'enhanced_sse',
|
|
531
|
+
tools_count: server_instance.tools.size,
|
|
532
|
+
protocol: 'MCP SSE (Enhanced Sinatra)',
|
|
533
|
+
endpoints: {
|
|
534
|
+
sse: '/sse',
|
|
535
|
+
sse_events: '/sse/events',
|
|
536
|
+
message: '/mcp/message',
|
|
537
|
+
broadcast: '/mcp/broadcast',
|
|
538
|
+
ws_connect: '/ws/connect',
|
|
539
|
+
ws_send: '/ws/send/:connection_id'
|
|
540
|
+
},
|
|
541
|
+
connections: {
|
|
542
|
+
sse: sse_connections.length,
|
|
543
|
+
ws: ws_connections.length
|
|
544
|
+
},
|
|
545
|
+
uptime: Time.now.to_i
|
|
546
|
+
}.to_json
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Connection status - improved thread safety
|
|
550
|
+
get '/connections' do
|
|
551
|
+
content_type 'application/json'
|
|
552
|
+
|
|
553
|
+
connection_mutex.synchronize do
|
|
554
|
+
{
|
|
555
|
+
sse_connections: sse_connections.map { |conn|
|
|
556
|
+
{
|
|
557
|
+
id: conn[:id],
|
|
558
|
+
created_at: conn[:created_at].iso8601,
|
|
559
|
+
age_seconds: (Time.now - conn[:created_at]).to_i
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
ws_connections: ws_connections.keys,
|
|
563
|
+
total: sse_connections.length + ws_connections.length
|
|
564
|
+
}.to_json
|
|
565
|
+
end
|
|
566
|
+
end
|
|
567
|
+
end
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def handle_request(request)
|
|
571
|
+
request_id = request["id"]
|
|
572
|
+
method = request["method"]
|
|
573
|
+
params = request["params"] || {}
|
|
574
|
+
|
|
575
|
+
case method
|
|
576
|
+
when "tools/list"
|
|
577
|
+
{
|
|
578
|
+
jsonrpc: "2.0",
|
|
579
|
+
id: request_id,
|
|
580
|
+
result: list_tools
|
|
581
|
+
}
|
|
582
|
+
when "tools/call"
|
|
583
|
+
tool_name = params["name"]
|
|
584
|
+
arguments = params["arguments"] || {}
|
|
585
|
+
|
|
586
|
+
begin
|
|
587
|
+
result = call_tool(tool_name, arguments)
|
|
588
|
+
{
|
|
589
|
+
jsonrpc: "2.0",
|
|
590
|
+
id: request_id,
|
|
591
|
+
result: result
|
|
592
|
+
}
|
|
593
|
+
rescue Error => e
|
|
594
|
+
{
|
|
595
|
+
jsonrpc: "2.0",
|
|
596
|
+
id: request_id,
|
|
597
|
+
error: {
|
|
598
|
+
code: -32000,
|
|
599
|
+
message: e.message
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
end
|
|
603
|
+
else
|
|
604
|
+
{
|
|
605
|
+
jsonrpc: "2.0",
|
|
606
|
+
id: request_id,
|
|
607
|
+
error: {
|
|
608
|
+
code: -32601,
|
|
609
|
+
message: "Method not found: #{method}"
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
end
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def send_stdio_error_response(request_id, code, message)
|
|
616
|
+
response = {
|
|
617
|
+
jsonrpc: "2.0",
|
|
618
|
+
id: request_id,
|
|
619
|
+
error: {
|
|
620
|
+
code: code,
|
|
621
|
+
message: message
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
@io_out.puts response.to_json
|
|
626
|
+
@io_out.flush
|
|
627
|
+
end
|
|
628
|
+
end
|
|
629
|
+
end
|
data/lib/mcp/sse_client.rb
CHANGED
data/lib/mcp/stdio_client.rb
CHANGED
|
@@ -31,8 +31,10 @@ module MCP
|
|
|
31
31
|
@running = false
|
|
32
32
|
@stdout_writer.close
|
|
33
33
|
@stderr_writer.close
|
|
34
|
-
@reader.join if @reader
|
|
34
|
+
@reader.join if @reader && !@pending_requests.empty?
|
|
35
35
|
@server.stop
|
|
36
|
+
@server.io.stdin.close
|
|
37
|
+
@server.io.stdout.close
|
|
36
38
|
@stdout.close
|
|
37
39
|
@stderr.close
|
|
38
40
|
end
|
data/lib/mcp.rb
CHANGED
|
@@ -3,7 +3,8 @@ require File.expand_path("../mcp/client", __FILE__)
|
|
|
3
3
|
require File.expand_path("../mcp/stdio_client", __FILE__)
|
|
4
4
|
require File.expand_path("../mcp/sse_client", __FILE__)
|
|
5
5
|
require File.expand_path("../mcp/convert", __FILE__)
|
|
6
|
+
require File.expand_path("../mcp/server", __FILE__)
|
|
6
7
|
|
|
7
8
|
module MCP
|
|
8
9
|
LATEST_PROTOCOL_VERSION = "2024-11-05".freeze
|
|
9
|
-
end
|
|
10
|
+
end
|