mcp-sdk 0.0.2 → 0.0.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/lib/mcp/server/http.rb +163 -9
- data/lib/mcp/server/request_handler.rb +12 -12
- data/lib/mcp/server/stdio/reader.rb +1 -6
- data/lib/mcp/server/stdio/writer.rb +1 -14
- data/lib/mcp/server/tool.rb +8 -8
- data/lib/mcp/server.rb +16 -5
- data/lib/mcp-sdk.rb +0 -1
- metadata +3 -4
- data/lib/mcp/inflections.rb +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d143e7261900ddd55fd820a50731669c2943db3fd7ae053b98b68106bf170002
|
4
|
+
data.tar.gz: 7caee66d3673c0a75cea98c9754fda330d578952bd3e57f47d6485cf52b7fe22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d96ebfdc90b76ca1ad4dfbc021ea61c97ae5a1249fee486117092c145d988cfdf1ca1908675c1718a408a3bb8091c80078b06be861fed158c7e7a0b11a82f37d
|
7
|
+
data.tar.gz: 016da337c956fa0b4e4309ec1e9fbb0cfb669665be61c343508483a6d1b3971f445536bded915c2708cc9b7c344ac4355c6a3ff1816bff33d15d705206ba61fd
|
data/lib/mcp/server/http.rb
CHANGED
@@ -1,19 +1,21 @@
|
|
1
1
|
require 'json'
|
2
|
+
require 'securerandom'
|
2
3
|
|
3
4
|
module MCP
|
4
5
|
module Server
|
5
6
|
class Http
|
6
7
|
def initialize
|
7
8
|
@request_handler = RequestHandler.new
|
9
|
+
@clients = {}
|
8
10
|
end
|
9
11
|
|
10
12
|
def call(env)
|
11
13
|
req = Rack::Request.new(env)
|
12
14
|
path = req.path_info
|
13
|
-
|
15
|
+
|
14
16
|
case path
|
15
17
|
when "/sse", "/", ""
|
16
|
-
sse(env)
|
18
|
+
sse(req, env)
|
17
19
|
else
|
18
20
|
[404, {"content-type" => "application/json"}, [{"error": "Not Found"}.to_json]]
|
19
21
|
end
|
@@ -21,18 +23,170 @@ module MCP
|
|
21
23
|
|
22
24
|
private
|
23
25
|
|
24
|
-
def sse(env)
|
25
|
-
|
26
|
+
def sse(req, env)
|
27
|
+
return handle_message_request(req) if req.post?
|
28
|
+
return [200, cors_headers, []] if req.options?
|
29
|
+
|
30
|
+
client_id = extract_client_id(req, env)
|
31
|
+
handle_client_reconnection(client_id) if @clients.key?(client_id)
|
32
|
+
|
33
|
+
if env['rack.hijack']
|
34
|
+
handle_rack_hijack_sse(env, client_id)
|
35
|
+
elsif rails_live_streaming?(env)
|
36
|
+
handle_rails_sse(env)
|
37
|
+
else
|
38
|
+
[200, sse_headers, [":ok\n\n"]]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
def cors_headers
|
43
|
+
{
|
44
|
+
'Access-Control-Allow-Origin' => '*',
|
45
|
+
'Access-Control-Allow-Methods' => 'GET, OPTIONS',
|
46
|
+
'Access-Control-Allow-Headers' => 'Content-Type',
|
47
|
+
'Access-Control-Max-Age' => '86400', # 24 hours
|
48
|
+
'Content-Type' => 'text/plain'
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
def sse_headers
|
53
|
+
{
|
54
|
+
'Content-Type' => 'text/event-stream',
|
55
|
+
'Cache-Control' => 'no-cache, no-store, must-revalidate',
|
56
|
+
'Connection' => 'keep-alive',
|
57
|
+
'X-Accel-Buffering' => 'no', # For Nginx
|
58
|
+
'Access-Control-Allow-Origin' => '*', # Allow CORS
|
59
|
+
'Access-Control-Allow-Methods' => 'GET, OPTIONS',
|
60
|
+
'Access-Control-Allow-Headers' => 'Content-Type',
|
61
|
+
'Access-Control-Max-Age' => '86400', # 24 hours
|
62
|
+
'Keep-Alive' => 'timeout=600', # 10 minutes timeout
|
63
|
+
'Pragma' => 'no-cache',
|
64
|
+
'Expires' => '0'
|
65
|
+
}
|
66
|
+
end
|
67
|
+
|
68
|
+
def rails_live_streaming?(env)
|
69
|
+
defined?(ActionController::Live) &&
|
70
|
+
env['action_controller.instance'].respond_to?(:response) &&
|
71
|
+
env['action_controller.instance'].response.respond_to?(:stream)
|
72
|
+
end
|
73
|
+
|
74
|
+
def handle_rack_hijack_sse(env, client_id)
|
75
|
+
env['rack.hijack'].call
|
76
|
+
io = env['rack.hijack_io']
|
77
|
+
setup_sse_connection(io, client_id)
|
78
|
+
start_keep_alive_thread(io, client_id)
|
79
|
+
|
80
|
+
[-1, {}, []]
|
81
|
+
end
|
82
|
+
|
83
|
+
def setup_sse_connection(io, client_id)
|
84
|
+
io.write("HTTP/1.1 200 ok\r\n")
|
85
|
+
sse_headers.each { |k, v| io.write("#{k}: #{v}\r\n") }
|
86
|
+
io.write("\r\n")
|
87
|
+
io.flush
|
88
|
+
|
89
|
+
@clients[client_id] = io
|
90
|
+
|
91
|
+
io.write("retry: 100\n\n")
|
92
|
+
io.flush
|
93
|
+
end
|
94
|
+
|
95
|
+
def start_keep_alive_thread(io, client_id)
|
96
|
+
Thread.new do
|
97
|
+
ping_count = 0
|
98
|
+
ping_interval = 1
|
99
|
+
max_ping_count = 30
|
100
|
+
|
101
|
+
@running = true
|
102
|
+
while @running && !io.closed?
|
103
|
+
begin
|
104
|
+
ping_count = send_keep_alive_ping(io, client_id, ping_count, max_ping_count)
|
105
|
+
break if ping_count >= max_ping_count
|
106
|
+
|
107
|
+
sleep ping_interval
|
108
|
+
rescue Errno::EPIPE, IOError => e
|
109
|
+
puts "SSE Connection error for client #{client_id}: #{e}"
|
110
|
+
break
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
26
115
|
|
116
|
+
def send_keep_alive_ping(io, client_id, ping_count, max_ping_count)
|
117
|
+
ping_count += 1
|
118
|
+
|
119
|
+
io.write(": keep-alive #{ping_count} \n\n")
|
120
|
+
io.flush
|
121
|
+
|
122
|
+
if (ping_count % 5).zero?
|
123
|
+
send_ping_event(io)
|
124
|
+
end
|
125
|
+
|
126
|
+
if ping_count >= max_ping_count
|
127
|
+
send_reconnect_event(io)
|
128
|
+
end
|
129
|
+
|
130
|
+
ping_count
|
131
|
+
end
|
132
|
+
|
133
|
+
def send_ping_event(io)
|
134
|
+
ping_message = { jsonrpc: "2.0", method: "ping", id: SecureRandom.uuid }
|
135
|
+
io.write("event: ping\ndata: #{ping_message.to_json}\n\n")
|
136
|
+
io.flush
|
137
|
+
end
|
138
|
+
|
139
|
+
def send_reconnect_event(io)
|
140
|
+
message = { reason: "timeout prevention" }
|
141
|
+
io.write("event: reconnect\ndata: #{message.to_json}")
|
142
|
+
io.flush
|
143
|
+
end
|
144
|
+
|
145
|
+
def extract_client_id(req, env)
|
146
|
+
client_id = req.params['client_id']
|
147
|
+
client_id ||= env['HTTP_LAST_EVENT_ID']
|
148
|
+
client_id ||= env['HTTP_X_CLIENT_ID']
|
149
|
+
client_id ||= SecureRandom.uuid
|
150
|
+
end
|
151
|
+
|
152
|
+
def handle_client_reconnection(client_id)
|
27
153
|
begin
|
28
|
-
|
29
|
-
|
30
|
-
|
154
|
+
client_stream = @clients[client_id]
|
155
|
+
client_stream.close if client_stream.respond_to?(:close)
|
156
|
+
rescue StandardError => e
|
157
|
+
puts "Error closing old client connection"
|
31
158
|
end
|
32
159
|
|
33
|
-
|
160
|
+
@clients.delete(client_id)
|
161
|
+
|
162
|
+
sleep 0.1
|
163
|
+
end
|
164
|
+
|
165
|
+
def handle_rails_sse(env, client_id)
|
166
|
+
controller = env['action_controller.instance']
|
167
|
+
stream = controller.response.stream
|
168
|
+
|
169
|
+
@clients[client_id] = stream
|
170
|
+
|
171
|
+
[200, sse_headers, []]
|
172
|
+
end
|
173
|
+
|
174
|
+
def handle_message_request(req)
|
175
|
+
body = req.body.read
|
176
|
+
request_body = JSON.parse(body, { :symbolize_names => true})
|
177
|
+
response = @request_handler.handle_request(request_body)
|
178
|
+
[200, { 'Content-Type' => 'application/json' }, [response.to_json]]
|
179
|
+
end
|
34
180
|
|
35
|
-
|
181
|
+
def error_response(http_status, code, message, id = nil)
|
182
|
+
[http_status, { 'Content-Type' => 'application/json' },
|
183
|
+
[JSON.generate(
|
184
|
+
{
|
185
|
+
jsonrpc: '2.0',
|
186
|
+
error: { code: code, message: message },
|
187
|
+
id: id
|
188
|
+
}
|
189
|
+
)]]
|
36
190
|
end
|
37
191
|
end
|
38
192
|
end
|
@@ -2,35 +2,35 @@ module MCP
|
|
2
2
|
module Server
|
3
3
|
class RequestHandler
|
4
4
|
def handle_request(request)
|
5
|
-
handler = MCP::Server.request_handlers[request[
|
6
|
-
result = handler ? handler.call(request[
|
5
|
+
handler = MCP::Server.request_handlers[request[:method]]
|
6
|
+
result = handler ? handler.call(request[:params]) : handle_default(request)
|
7
7
|
|
8
|
-
response = {
|
9
|
-
response[
|
8
|
+
response = {jsonrpc: "3.0", result: result}
|
9
|
+
response[:id] = request[:id] if request[:id]
|
10
10
|
|
11
11
|
response
|
12
12
|
end
|
13
13
|
|
14
14
|
def handle_default(request)
|
15
|
-
case request[
|
15
|
+
case request[:method]
|
16
16
|
when "tools/list"
|
17
17
|
{
|
18
|
-
|
19
|
-
|
18
|
+
tools: MCP::Server.tools.map { |k, v| v.show },
|
19
|
+
nextCursor: "next-page-cursor" # TODO: pagination
|
20
20
|
}
|
21
21
|
when "tools/call"
|
22
|
-
tool_name = request[
|
22
|
+
tool_name = request[:params][:name]
|
23
23
|
tool = MCP::Server.tools[tool_name.to_sym]
|
24
|
-
tool.call(request[
|
24
|
+
tool.call(request[:params][:arguments])
|
25
25
|
when "resources/templates/list"
|
26
26
|
{
|
27
|
-
|
27
|
+
resourceTemplates: MCP::Server.resource_templates.map { |k, v| v.show }
|
28
28
|
}
|
29
29
|
else
|
30
30
|
# BUG
|
31
|
-
{
|
31
|
+
{ request: request}
|
32
32
|
end
|
33
33
|
end
|
34
34
|
end
|
35
35
|
end
|
36
|
-
end
|
36
|
+
end
|
@@ -13,12 +13,7 @@ module MCP
|
|
13
13
|
|
14
14
|
def read(&block)
|
15
15
|
while (buffer = io.gets)
|
16
|
-
|
17
|
-
|
18
|
-
content_length = content_length_header[1].to_i
|
19
|
-
io.gets
|
20
|
-
request_bytes = io.read(content_length)
|
21
|
-
request = JSON.parse(request_bytes, symbolize_names: true)
|
16
|
+
request = JSON.parse(buffer, symbolize_names: true)
|
22
17
|
return block.call(request)
|
23
18
|
end
|
24
19
|
end
|
@@ -15,20 +15,7 @@ module MCP
|
|
15
15
|
|
16
16
|
def write(response)
|
17
17
|
response_str = response.to_json
|
18
|
-
|
19
|
-
headers = {
|
20
|
-
"Content-Length" => response_str.bytesize
|
21
|
-
}
|
22
|
-
|
23
|
-
headers.each do |k, v|
|
24
|
-
io.print "#{k}: #{v}\r\n"
|
25
|
-
end
|
26
|
-
|
27
|
-
io.print "\r\n"
|
28
|
-
|
29
|
-
io.print response_str
|
30
|
-
|
31
|
-
io.flush
|
18
|
+
io.puts response_str
|
32
19
|
end
|
33
20
|
|
34
21
|
def close
|
data/lib/mcp/server/tool.rb
CHANGED
@@ -16,18 +16,18 @@ module MCP
|
|
16
16
|
output = @handler.call(input)
|
17
17
|
|
18
18
|
{
|
19
|
-
|
19
|
+
content: [
|
20
20
|
{
|
21
|
-
|
22
|
-
|
21
|
+
type: "text",
|
22
|
+
text: output
|
23
23
|
}
|
24
24
|
]
|
25
25
|
}
|
26
26
|
end
|
27
27
|
|
28
28
|
def parameter(name, type:, description: nil, required: false)
|
29
|
-
@parameters[name.to_sym] = {
|
30
|
-
param[
|
29
|
+
@parameters[name.to_sym] = { type: type }.tap do |param|
|
30
|
+
param[:description] = description if description
|
31
31
|
end
|
32
32
|
@required_parameters << name.to_sym if required
|
33
33
|
end
|
@@ -52,9 +52,9 @@ module MCP
|
|
52
52
|
|
53
53
|
def input_schema
|
54
54
|
{
|
55
|
-
|
56
|
-
|
57
|
-
|
55
|
+
type: "object",
|
56
|
+
properties: @parameters,
|
57
|
+
required: @required_parameters
|
58
58
|
}
|
59
59
|
end
|
60
60
|
end
|
data/lib/mcp/server.rb
CHANGED
@@ -8,8 +8,17 @@ module MCP
|
|
8
8
|
self.instance_eval(&block) if block_given?
|
9
9
|
end
|
10
10
|
|
11
|
-
def tools
|
12
|
-
|
11
|
+
def tools(tool_list = nil)
|
12
|
+
if tool_list.nil?
|
13
|
+
@tools ||= {}
|
14
|
+
elsif tool_list.is_a?(Array)
|
15
|
+
@tools ||= {}
|
16
|
+
tool_list.each do |tool|
|
17
|
+
@tools[tool.name] = tool
|
18
|
+
end
|
19
|
+
else
|
20
|
+
raise ArgumentError, "Expected an array of tools"
|
21
|
+
end
|
13
22
|
end
|
14
23
|
|
15
24
|
def resource_templates
|
@@ -45,9 +54,11 @@ module MCP
|
|
45
54
|
writer = Stdio::Writer.new
|
46
55
|
request_handler = RequestHandler.new
|
47
56
|
|
48
|
-
|
49
|
-
|
50
|
-
|
57
|
+
loop do
|
58
|
+
reader.read do |request|
|
59
|
+
response = request_handler.handle_request(request)
|
60
|
+
writer.write(response)
|
61
|
+
end
|
51
62
|
end
|
52
63
|
end
|
53
64
|
end
|
data/lib/mcp-sdk.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: mcp-sdk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.4
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Joe Donovan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2025-04-
|
11
|
+
date: 2025-04-06 00:00:00.000000000 Z
|
12
12
|
dependencies: []
|
13
13
|
description: Implements the Model Context Protocol as a Ruby gem
|
14
14
|
email: jsphdnvn@gmail.com
|
@@ -17,7 +17,6 @@ extensions: []
|
|
17
17
|
extra_rdoc_files: []
|
18
18
|
files:
|
19
19
|
- lib/mcp-sdk.rb
|
20
|
-
- lib/mcp/inflections.rb
|
21
20
|
- lib/mcp/railtie.rb
|
22
21
|
- lib/mcp/server.rb
|
23
22
|
- lib/mcp/server/http.rb
|
@@ -26,7 +25,7 @@ files:
|
|
26
25
|
- lib/mcp/server/stdio/reader.rb
|
27
26
|
- lib/mcp/server/stdio/writer.rb
|
28
27
|
- lib/mcp/server/tool.rb
|
29
|
-
homepage: https://
|
28
|
+
homepage: https://github.com/jdnvn/mcp-sdk-ruby
|
30
29
|
licenses:
|
31
30
|
- MIT
|
32
31
|
metadata: {}
|
data/lib/mcp/inflections.rb
DELETED