elelem-mcp 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a05829dc49559ed0c16cfc5eaf5ac9241fc885c66b5cb6916ade3d6b68047742
4
- data.tar.gz: 47b5e8f31f8904bd25ee8e4324cd0a76c710a4c967c98f2758f334f4d484508f
3
+ metadata.gz: 165e65bad3ab284e5049d9cffba0041be3f5c315253d48090d6dd7e754a4f715
4
+ data.tar.gz: c96c690ff8008695fa8f1eb19a4aa4202d7543a622340d813b3d2a257d2adad3
5
5
  SHA512:
6
- metadata.gz: ae2d3fc5652d3c659b4848d3cc11ad2eb6810d1618c9bd057536ed99598c8c084a86d9f9c4109ef154530d7c6f752bddc4c0b31da9b16644b88d29d0df722026
7
- data.tar.gz: b0292705cd0fdc52c9834186fadaed62201c8d61d4ff17c5c311c196bceb203c664b19556edec8c2b27a9ebc190b2665a7b4c8b26d5f6f7ca7cecec5ed425529
6
+ metadata.gz: 65822b02aa43aec269f68811654cd566baecf9629adb95e15c5ef934228f95c20a54d148cba59af8754ecc19e33a42e66543797dff149e9c5780d88a97065b35
7
+ data.tar.gz: 7d2c03dac26fc843b9f8ea8734159de93509c81963528baee425810e440a51787db1d2f24cdb21574beb82e13d31114d8df2d5e2ca0cbcfafe885b0c648c60ba
data/.rspec ADDED
@@ -0,0 +1 @@
1
+ --require spec_helper
data/Rakefile CHANGED
@@ -1,4 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "bundler/gem_tasks"
4
- task default: %i[]
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: %i[spec]
@@ -25,7 +25,7 @@ module Elelem
25
25
  "#{name}_#{tool["name"]}",
26
26
  {
27
27
  description: tool["description"],
28
- params: tool.dig("inputSchema", "properties") || {},
28
+ params: symbolize(tool.dig("inputSchema", "properties") || {}),
29
29
  required: tool.dig("inputSchema", "required") || [],
30
30
  fn: ->(a) { server(name).call(tool["name"], a) }
31
31
  }
@@ -40,6 +40,10 @@ module Elelem
40
40
 
41
41
  private
42
42
 
43
+ def symbolize(hash)
44
+ JSON.parse(JSON.generate(hash), symbolize_names: true)
45
+ end
46
+
43
47
  def load_config(configurations)
44
48
  configurations.each_with_object({}) do |path, merged|
45
49
  file = File.expand_path(path)
@@ -157,6 +161,7 @@ module Elelem
157
161
  @open_timeout = open_timeout
158
162
  @id = 0
159
163
  @session_id = nil
164
+ @access_token = nil
160
165
  handshake!
161
166
  end
162
167
 
@@ -185,7 +190,7 @@ module Elelem
185
190
  post(msg)
186
191
  end
187
192
 
188
- def post(msg)
193
+ def post(msg, retry_auth: true)
189
194
  request = Net::HTTP::Post.new(@uri)
190
195
  request["content-type"] = "application/json"
191
196
  request_headers.each { |k, v| request[k] = v }
@@ -204,7 +209,10 @@ module Elelem
204
209
  return parse_response(response)
205
210
  when Net::HTTPUnauthorized
206
211
  response.body
207
- raise "MCP: server requires OAuth (not yet supported)"
212
+ raise "Authorization failed" unless retry_auth
213
+
214
+ @access_token = OAuth.new(@uri.to_s).token
215
+ return post(msg, retry_auth: false)
208
216
  else
209
217
  raise "HTTP #{response.code}: #{response.body}"
210
218
  end
@@ -215,6 +223,7 @@ module Elelem
215
223
  def request_headers
216
224
  base = { "Accept" => "application/json, text/event-stream" }
217
225
  base["Mcp-Session-Id"] = @session_id if @session_id
226
+ base["Authorization"] = "Bearer #{@access_token}" if @access_token
218
227
  @headers.merge(base)
219
228
  end
220
229
 
@@ -0,0 +1,237 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Elelem
4
+ module MCP
5
+ class OAuth
6
+ CALLBACK_PORT = 18273
7
+ REDIRECT_URI = "http://127.0.0.1:#{CALLBACK_PORT}/callback"
8
+
9
+ def initialize(resource_url, http: HttpAdapter.new)
10
+ @resource_url = resource_url
11
+ @http = http
12
+ @storage = TokenStorage.new
13
+ end
14
+
15
+ def token
16
+ stored = @storage.load(@resource_url)
17
+ return stored[:access_token] if stored && !expired?(stored)
18
+ return refresh(stored[:refresh_token]) if stored&.dig(:refresh_token)
19
+
20
+ authorize
21
+ end
22
+
23
+ private
24
+
25
+ def expired?(stored)
26
+ return false unless stored[:expires_at]
27
+
28
+ Time.now.to_i >= stored[:expires_at] - 60
29
+ end
30
+
31
+ def authorize
32
+ metadata = discover_auth_server
33
+ client = load_or_register_client(metadata)
34
+ verifier, challenge = generate_pkce
35
+ state = SecureRandom.hex(16)
36
+
37
+ auth_url = build_auth_url(metadata, client, challenge, state)
38
+ open_browser(auth_url)
39
+
40
+ code = wait_for_callback(state)
41
+ tokens = exchange_code(metadata, client, code, verifier)
42
+
43
+ @storage.save(
44
+ @resource_url,
45
+ access_token: tokens["access_token"],
46
+ refresh_token: tokens["refresh_token"],
47
+ expires_in: tokens["expires_in"]
48
+ )
49
+
50
+ tokens["access_token"]
51
+ end
52
+
53
+ def refresh(refresh_token)
54
+ metadata = discover_auth_server
55
+ client = load_or_register_client(metadata)
56
+ uri = URI.parse(metadata["token_endpoint"])
57
+
58
+ body = {
59
+ grant_type: "refresh_token",
60
+ refresh_token: refresh_token,
61
+ client_id: client[:client_id]
62
+ }
63
+
64
+ response = post_form(uri, body)
65
+ tokens = JSON.parse(response.body)
66
+
67
+ @storage.save(
68
+ @resource_url,
69
+ access_token: tokens["access_token"],
70
+ refresh_token: tokens["refresh_token"] || refresh_token,
71
+ expires_in: tokens["expires_in"]
72
+ )
73
+
74
+ tokens["access_token"]
75
+ rescue StandardError => e
76
+ warn "Token refresh failed: #{e.message}"
77
+ authorize
78
+ end
79
+
80
+ def discover_auth_server
81
+ resource_uri = URI.parse(@resource_url)
82
+ metadata_url = "#{resource_uri.scheme}://#{resource_uri.host}/.well-known/oauth-protected-resource"
83
+
84
+ resource_metadata = fetch_json(metadata_url)
85
+ auth_server_url = resource_metadata["authorization_servers"]&.first
86
+ raise "No authorization server found" unless auth_server_url
87
+
88
+ auth_metadata_url = "#{auth_server_url}/.well-known/oauth-authorization-server"
89
+ fetch_json(auth_metadata_url)
90
+ end
91
+
92
+ def load_or_register_client(metadata)
93
+ stored = @storage.load_client(@resource_url)
94
+ return stored if stored
95
+
96
+ client = register_client(metadata)
97
+ @storage.save_client(@resource_url, client)
98
+ @storage.load_client(@resource_url)
99
+ end
100
+
101
+ def register_client(metadata)
102
+ endpoint = metadata["registration_endpoint"]
103
+ raise "Dynamic registration not supported" unless endpoint
104
+
105
+ body = {
106
+ client_name: "elelem",
107
+ redirect_uris: [REDIRECT_URI],
108
+ grant_types: %w[authorization_code refresh_token],
109
+ response_types: ["code"],
110
+ token_endpoint_auth_method: "none"
111
+ }
112
+
113
+ response = post_json(endpoint, body)
114
+ JSON.parse(response.body)
115
+ end
116
+
117
+ def generate_pkce
118
+ verifier = SecureRandom.urlsafe_base64(32)
119
+ challenge = Base64.urlsafe_encode64(
120
+ Digest::SHA256.digest(verifier),
121
+ padding: false
122
+ )
123
+ [verifier, challenge]
124
+ end
125
+
126
+ def build_auth_url(metadata, client, challenge, state)
127
+ params = {
128
+ response_type: "code",
129
+ client_id: client[:client_id],
130
+ redirect_uri: REDIRECT_URI,
131
+ scope: metadata["scopes_supported"]&.join(" ") || "openid",
132
+ state: state,
133
+ code_challenge: challenge,
134
+ code_challenge_method: "S256"
135
+ }
136
+
137
+ "#{metadata["authorization_endpoint"]}?#{URI.encode_www_form(params)}"
138
+ end
139
+
140
+ def open_browser(url)
141
+ commands = ["xdg-open", "open", "start"]
142
+ commands.each do |cmd|
143
+ return if system(cmd, url, out: File::NULL, err: File::NULL)
144
+ end
145
+ warn "Open this URL in your browser: #{url}"
146
+ end
147
+
148
+ def wait_for_callback(expected_state)
149
+ code = nil
150
+ @server = WEBrick::HTTPServer.new(
151
+ Port: CALLBACK_PORT,
152
+ Logger: WEBrick::Log.new(File::NULL),
153
+ AccessLog: []
154
+ )
155
+
156
+ at_exit { @server&.shutdown }
157
+
158
+ @server.mount_proc("/callback") do |req, res|
159
+ state = req.query["state"]
160
+ raise "State mismatch" unless state == expected_state
161
+
162
+ code = req.query["code"]
163
+ res.content_type = "text/html"
164
+ res.body = "<html><body><h1>Authorization complete</h1><p>You can close this window.</p></body></html>"
165
+ @server.shutdown
166
+ end
167
+
168
+ Timeout.timeout(120) { @server.start }
169
+ code
170
+ rescue Timeout::Error
171
+ @server.shutdown
172
+ raise "OAuth callback timed out"
173
+ end
174
+
175
+ def exchange_code(metadata, client, code, verifier)
176
+ uri = URI.parse(metadata["token_endpoint"])
177
+
178
+ body = {
179
+ grant_type: "authorization_code",
180
+ code: code,
181
+ redirect_uri: REDIRECT_URI,
182
+ client_id: client[:client_id],
183
+ code_verifier: verifier
184
+ }
185
+
186
+ response = post_form(uri, body)
187
+ JSON.parse(response.body)
188
+ end
189
+
190
+ def fetch_json(url)
191
+ response = nil
192
+ @http.get(url) { |r| response = r }
193
+ JSON.parse(response.body)
194
+ end
195
+
196
+ def post_json(url, body)
197
+ response = nil
198
+ @http.post(
199
+ url,
200
+ headers: { "Content-Type" => "application/json" },
201
+ body: body.to_json
202
+ ) { |r| response = r }
203
+ response
204
+ end
205
+
206
+ def post_form(uri, body)
207
+ response = nil
208
+ @http.post(
209
+ uri.to_s,
210
+ headers: { "Content-Type" => "application/x-www-form-urlencoded" },
211
+ body: URI.encode_www_form(body)
212
+ ) { |r| response = r }
213
+ response
214
+ end
215
+
216
+ class HttpAdapter
217
+ def get(url)
218
+ uri = URI.parse(url)
219
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |conn|
220
+ yield conn.get(uri.request_uri)
221
+ end
222
+ end
223
+
224
+ def post(url, headers: {}, body: nil)
225
+ uri = URI.parse(url)
226
+ request = Net::HTTP::Post.new(uri)
227
+ headers.each { |k, v| request[k] = v }
228
+ request.body = body
229
+
230
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https") do |conn|
231
+ yield conn.request(request)
232
+ end
233
+ end
234
+ end
235
+ end
236
+ end
237
+ end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Elelem
4
4
  module MCP
5
- VERSION = "0.1.0"
5
+ VERSION = "0.2.0"
6
6
  end
7
7
  end
data/lib/elelem/mcp.rb CHANGED
@@ -1,33 +1,40 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "base64"
3
4
  require "digest"
4
5
  require "elelem"
5
6
  require "fileutils"
6
7
  require "json"
7
8
  require "net/http"
8
9
  require "open3"
10
+ require "securerandom"
11
+ require "timeout"
9
12
  require "uri"
13
+ require "webrick"
10
14
 
11
15
  require_relative "mcp/version"
12
16
  require_relative "mcp/token_storage"
17
+ require_relative "mcp/oauth"
13
18
  require_relative "mcp/client"
14
19
 
15
- Elelem::Plugins.register(:mcp) do |agent|
16
- mcp = Elelem::MCP::Client.memoized
20
+ Elelem.configure do |config|
21
+ config.setup(:mcp) do |agent|
22
+ mcp = Elelem::MCP::Client.memoized
17
23
 
18
- Thread.new do
19
- mcp.tools.each do |name, tool|
20
- next if agent.toolbox.tools.key?(name)
24
+ Thread.new do
25
+ mcp.tools.each do |name, tool|
26
+ next if agent.toolbox.tools.key?(name)
21
27
 
22
- agent.toolbox.add(
23
- name,
24
- description: tool[:description],
25
- params: tool[:params],
26
- required: tool[:required],
27
- &tool[:fn]
28
- )
28
+ agent.toolbox.add(
29
+ name,
30
+ description: tool[:description],
31
+ params: tool[:params],
32
+ required: tool[:required],
33
+ &tool[:fn]
34
+ )
35
+ end
36
+ rescue => e
37
+ warn "MCP failed: #{e.message}"
29
38
  end
30
- rescue => e
31
- warn "MCP failed: #{e.message}"
32
39
  end
33
40
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Elelem::MCP::HttpServer do
4
+ let(:url) { "https://mcp.example.com/sse" }
5
+ let(:http) { double("http") }
6
+
7
+ describe "#parse_sse" do
8
+ subject { described_class.allocate }
9
+
10
+ it "parses single SSE event" do
11
+ response = double
12
+ allow(response).to receive(:read_body).and_yield("data: {\"result\": \"ok\"}\n\n")
13
+
14
+ result = subject.send(:parse_sse, response)
15
+ expect(result).to eq({ "result" => "ok" })
16
+ end
17
+
18
+ it "parses chunked SSE events" do
19
+ response = double
20
+ chunks = ["data: {\"id\"", ": 1}\n\ndata: {\"id\": 2}\n\n"]
21
+ allow(response).to receive(:read_body) do |&block|
22
+ chunks.each { |c| block.call(c) }
23
+ end
24
+
25
+ result = subject.send(:parse_sse, response)
26
+ expect(result).to eq({ "id" => 2 })
27
+ end
28
+
29
+ it "ignores non-data lines" do
30
+ response = double
31
+ allow(response).to receive(:read_body).and_yield("event: message\ndata: {\"value\": 42}\n\n")
32
+
33
+ result = subject.send(:parse_sse, response)
34
+ expect(result).to eq({ "value" => 42 })
35
+ end
36
+
37
+ it "returns last event when multiple present" do
38
+ response = double
39
+ allow(response).to receive(:read_body).and_yield("data: {\"n\": 1}\n\ndata: {\"n\": 2}\n\n")
40
+
41
+ result = subject.send(:parse_sse, response)
42
+ expect(result).to eq({ "n" => 2 })
43
+ end
44
+ end
45
+
46
+ describe "#parse_response" do
47
+ subject { described_class.allocate }
48
+
49
+ it "parses JSON response" do
50
+ response = double(content_type: "application/json", body: '{"tools": []}')
51
+ result = subject.send(:parse_response, response)
52
+ expect(result).to eq({ "tools" => [] })
53
+ end
54
+
55
+ it "parses SSE response" do
56
+ response = double(content_type: "text/event-stream")
57
+ allow(response).to receive(:read_body).and_yield("data: {\"ok\": true}\n\n")
58
+
59
+ result = subject.send(:parse_response, response)
60
+ expect(result).to eq({ "ok" => true })
61
+ end
62
+
63
+ it "returns nil for empty body" do
64
+ response = double(content_type: "application/json", body: "")
65
+ result = subject.send(:parse_response, response)
66
+ expect(result).to be_nil
67
+ end
68
+ end
69
+
70
+ describe "#request_headers" do
71
+ subject do
72
+ server = described_class.allocate
73
+ server.instance_variable_set(:@headers, {})
74
+ server.instance_variable_set(:@session_id, nil)
75
+ server.instance_variable_set(:@access_token, nil)
76
+ server
77
+ end
78
+
79
+ it "includes Accept header" do
80
+ headers = subject.send(:request_headers)
81
+ expect(headers["Accept"]).to eq("application/json, text/event-stream")
82
+ end
83
+
84
+ it "includes session ID when set" do
85
+ subject.instance_variable_set(:@session_id, "abc123")
86
+ headers = subject.send(:request_headers)
87
+ expect(headers["Mcp-Session-Id"]).to eq("abc123")
88
+ end
89
+
90
+ it "includes authorization when access_token set" do
91
+ subject.instance_variable_set(:@access_token, "token123")
92
+ headers = subject.send(:request_headers)
93
+ expect(headers["Authorization"]).to eq("Bearer token123")
94
+ end
95
+ end
96
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Elelem::MCP::OAuth do
4
+ let(:resource_url) { "https://mcp.example.com/sse" }
5
+ let(:http) { double("http") }
6
+ let(:storage) { instance_double(Elelem::MCP::TokenStorage) }
7
+
8
+ subject { described_class.new(resource_url, http: http) }
9
+
10
+ before do
11
+ allow(Elelem::MCP::TokenStorage).to receive(:new).and_return(storage)
12
+ end
13
+
14
+ describe "#token" do
15
+ context "with valid cached token" do
16
+ it "returns cached access_token" do
17
+ allow(storage).to receive(:load).with(resource_url).and_return({
18
+ access_token: "cached_token",
19
+ expires_at: Time.now.to_i + 3600
20
+ })
21
+
22
+ expect(subject.token).to eq("cached_token")
23
+ end
24
+ end
25
+
26
+ context "with expired token and refresh_token" do
27
+ let(:auth_metadata) do
28
+ {
29
+ "authorization_endpoint" => "https://auth.example.com/authorize",
30
+ "token_endpoint" => "https://auth.example.com/token",
31
+ "registration_endpoint" => "https://auth.example.com/register"
32
+ }
33
+ end
34
+
35
+ it "refreshes using refresh_token" do
36
+ allow(storage).to receive(:load).with(resource_url).and_return({
37
+ access_token: "old_token",
38
+ refresh_token: "refresh_abc",
39
+ expires_at: Time.now.to_i - 100
40
+ })
41
+
42
+ allow(storage).to receive(:load_client).with(resource_url).and_return({
43
+ client_id: "elelem-123"
44
+ })
45
+
46
+ resource_response = double(body: { "authorization_servers" => ["https://auth.example.com"] }.to_json)
47
+ auth_response = double(body: auth_metadata.to_json)
48
+ token_response = double(body: { "access_token" => "new_token", "expires_in" => 3600 }.to_json)
49
+
50
+ allow(http).to receive(:get).with("https://mcp.example.com/.well-known/oauth-protected-resource").and_yield(resource_response)
51
+ allow(http).to receive(:get).with("https://auth.example.com/.well-known/oauth-authorization-server").and_yield(auth_response)
52
+ allow(http).to receive(:post).and_yield(token_response)
53
+ allow(storage).to receive(:save)
54
+
55
+ expect(subject.token).to eq("new_token")
56
+ end
57
+ end
58
+ end
59
+
60
+ describe "PKCE generation" do
61
+ it "generates valid verifier and challenge" do
62
+ verifier, challenge = subject.send(:generate_pkce)
63
+
64
+ expect(verifier.length).to be >= 43
65
+ expect(challenge.length).to be >= 43
66
+ expect(challenge).not_to include("+", "/", "=")
67
+
68
+ expected_challenge = Base64.urlsafe_encode64(
69
+ Digest::SHA256.digest(verifier),
70
+ padding: false
71
+ )
72
+ expect(challenge).to eq(expected_challenge)
73
+ end
74
+ end
75
+
76
+ describe "expiration check" do
77
+ it "considers token expired when within 60 seconds of expiry" do
78
+ stored = { expires_at: Time.now.to_i + 30 }
79
+ expect(subject.send(:expired?, stored)).to be true
80
+ end
81
+
82
+ it "considers token valid when more than 60 seconds remain" do
83
+ stored = { expires_at: Time.now.to_i + 120 }
84
+ expect(subject.send(:expired?, stored)).to be false
85
+ end
86
+
87
+ it "considers token valid when no expires_at" do
88
+ stored = { expires_at: nil }
89
+ expect(subject.send(:expired?, stored)).to be false
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ RSpec.describe Elelem::MCP::TokenStorage do
4
+ subject { described_class.new }
5
+
6
+ let(:resource_url) { "https://example.com/mcp" }
7
+ let(:token_dir) { File.expand_path("~/.config/elelem/tokens") }
8
+
9
+ after do
10
+ Dir.glob(File.join(token_dir, "*.json")).each { |f| File.delete(f) rescue nil }
11
+ end
12
+
13
+ describe "#save and #load" do
14
+ it "stores and retrieves tokens" do
15
+ subject.save(resource_url, access_token: "abc123", refresh_token: "refresh456", expires_in: 3600)
16
+
17
+ stored = subject.load(resource_url)
18
+ expect(stored[:access_token]).to eq("abc123")
19
+ expect(stored[:refresh_token]).to eq("refresh456")
20
+ expect(stored[:expires_at]).to be > Time.now.to_i
21
+ end
22
+
23
+ it "returns nil for unknown resource" do
24
+ expect(subject.load("https://unknown.com")).to be_nil
25
+ end
26
+
27
+ it "handles missing refresh_token" do
28
+ subject.save(resource_url, access_token: "abc123")
29
+
30
+ stored = subject.load(resource_url)
31
+ expect(stored[:access_token]).to eq("abc123")
32
+ expect(stored[:refresh_token]).to be_nil
33
+ end
34
+ end
35
+
36
+ describe "#save_client and #load_client" do
37
+ it "stores and retrieves client registration" do
38
+ client_data = { "client_id" => "elelem-123", "client_secret" => nil }
39
+ subject.save_client(resource_url, client_data)
40
+
41
+ stored = subject.load_client(resource_url)
42
+ expect(stored[:client_id]).to eq("elelem-123")
43
+ end
44
+
45
+ it "returns nil for unknown client" do
46
+ expect(subject.load_client("https://unknown.com")).to be_nil
47
+ end
48
+ end
49
+
50
+ describe "file permissions" do
51
+ it "creates token files with 0600 permissions" do
52
+ subject.save(resource_url, access_token: "secret")
53
+
54
+ files = Dir.glob(File.join(token_dir, "*.json"))
55
+ expect(files).not_to be_empty
56
+ files.each do |f|
57
+ mode = File.stat(f).mode & 0o777
58
+ expect(mode).to eq(0o600)
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../lib/elelem/mcp"
4
+
5
+ RSpec.configure do |config|
6
+ config.disable_monkey_patching!
7
+
8
+ config.expect_with :rspec do |c|
9
+ c.syntax = :expect
10
+ end
11
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: elelem-mcp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mo khan
@@ -9,6 +9,20 @@ bindir: bin
9
9
  cert_chain: []
10
10
  date: 1980-01-02 00:00:00.000000000 Z
11
11
  dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: base64
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: '0.3'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: '0.3'
12
26
  - !ruby/object:Gem::Dependency
13
27
  name: digest
14
28
  requirement: !ruby/object:Gem::Requirement
@@ -57,14 +71,14 @@ dependencies:
57
71
  requirements:
58
72
  - - "~>"
59
73
  - !ruby/object:Gem::Version
60
- version: '2.21'
74
+ version: '3.0'
61
75
  type: :runtime
62
76
  prerelease: false
63
77
  version_requirements: !ruby/object:Gem::Requirement
64
78
  requirements:
65
79
  - - "~>"
66
80
  - !ruby/object:Gem::Version
67
- version: '2.21'
81
+ version: '3.0'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: net-http
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -93,6 +107,34 @@ dependencies:
93
107
  - - "~>"
94
108
  - !ruby/object:Gem::Version
95
109
  version: '0.2'
110
+ - !ruby/object:Gem::Dependency
111
+ name: securerandom
112
+ requirement: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - "~>"
115
+ - !ruby/object:Gem::Version
116
+ version: '0.4'
117
+ type: :runtime
118
+ prerelease: false
119
+ version_requirements: !ruby/object:Gem::Requirement
120
+ requirements:
121
+ - - "~>"
122
+ - !ruby/object:Gem::Version
123
+ version: '0.4'
124
+ - !ruby/object:Gem::Dependency
125
+ name: timeout
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - "~>"
129
+ - !ruby/object:Gem::Version
130
+ version: '0.6'
131
+ type: :runtime
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - "~>"
136
+ - !ruby/object:Gem::Version
137
+ version: '0.6'
96
138
  - !ruby/object:Gem::Dependency
97
139
  name: uri
98
140
  requirement: !ruby/object:Gem::Requirement
@@ -107,6 +149,20 @@ dependencies:
107
149
  - - "~>"
108
150
  - !ruby/object:Gem::Version
109
151
  version: '1.1'
152
+ - !ruby/object:Gem::Dependency
153
+ name: webrick
154
+ requirement: !ruby/object:Gem::Requirement
155
+ requirements:
156
+ - - "~>"
157
+ - !ruby/object:Gem::Version
158
+ version: '1.9'
159
+ type: :runtime
160
+ prerelease: false
161
+ version_requirements: !ruby/object:Gem::Requirement
162
+ requirements:
163
+ - - "~>"
164
+ - !ruby/object:Gem::Version
165
+ version: '1.9'
110
166
  description: Adds MCP server tool discovery (stdio and HTTP transports) as elelem
111
167
  tools.
112
168
  email:
@@ -115,12 +171,18 @@ executables: []
115
171
  extensions: []
116
172
  extra_rdoc_files: []
117
173
  files:
174
+ - ".rspec"
118
175
  - LICENSE.txt
119
176
  - Rakefile
120
177
  - lib/elelem/mcp.rb
121
178
  - lib/elelem/mcp/client.rb
179
+ - lib/elelem/mcp/oauth.rb
122
180
  - lib/elelem/mcp/token_storage.rb
123
181
  - lib/elelem/mcp/version.rb
182
+ - spec/elelem/mcp/http_server_spec.rb
183
+ - spec/elelem/mcp/oauth_spec.rb
184
+ - spec/elelem/mcp/token_storage_spec.rb
185
+ - spec/spec_helper.rb
124
186
  homepage: https://src.mokhan.ca/elelem/mcp
125
187
  licenses:
126
188
  - MIT