Commit 2b8eedf
Changed files (7)
spec
spec/elelem/mcp/http_server_spec.rb
@@ -0,0 +1,89 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::MCP::HttpServer do
+ let(:url) { "https://mcp.example.com/sse" }
+ let(:http) { double("http") }
+
+ describe "#parse_sse" do
+ subject { described_class.allocate }
+
+ it "parses single SSE event" do
+ response = double
+ allow(response).to receive(:read_body).and_yield("data: {\"result\": \"ok\"}\n\n")
+
+ result = subject.send(:parse_sse, response)
+ expect(result).to eq({ "result" => "ok" })
+ end
+
+ it "parses chunked SSE events" do
+ response = double
+ chunks = ["data: {\"id\"", ": 1}\n\ndata: {\"id\": 2}\n\n"]
+ allow(response).to receive(:read_body) do |&block|
+ chunks.each { |c| block.call(c) }
+ end
+
+ result = subject.send(:parse_sse, response)
+ expect(result).to eq({ "id" => 2 })
+ end
+
+ it "ignores non-data lines" do
+ response = double
+ allow(response).to receive(:read_body).and_yield("event: message\ndata: {\"value\": 42}\n\n")
+
+ result = subject.send(:parse_sse, response)
+ expect(result).to eq({ "value" => 42 })
+ end
+
+ it "returns last event when multiple present" do
+ response = double
+ allow(response).to receive(:read_body).and_yield("data: {\"n\": 1}\n\ndata: {\"n\": 2}\n\n")
+
+ result = subject.send(:parse_sse, response)
+ expect(result).to eq({ "n" => 2 })
+ end
+ end
+
+ describe "#parse_response" do
+ subject { described_class.allocate }
+
+ it "parses JSON response" do
+ response = double(content_type: "application/json", body: '{"tools": []}')
+ result = subject.send(:parse_response, response)
+ expect(result).to eq({ "tools" => [] })
+ end
+
+ it "parses SSE response" do
+ response = double(content_type: "text/event-stream")
+ allow(response).to receive(:read_body).and_yield("data: {\"ok\": true}\n\n")
+
+ result = subject.send(:parse_response, response)
+ expect(result).to eq({ "ok" => true })
+ end
+
+ it "returns nil for empty body" do
+ response = double(content_type: "application/json", body: "")
+ result = subject.send(:parse_response, response)
+ expect(result).to be_nil
+ end
+ end
+
+ describe "#request_headers" do
+ subject do
+ server = described_class.allocate
+ server.instance_variable_set(:@headers, {})
+ server.instance_variable_set(:@session_id, nil)
+ server
+ end
+
+ it "includes Accept header" do
+ headers = subject.send(:request_headers)
+ expect(headers["Accept"]).to eq("application/json, text/event-stream")
+ end
+
+ it "includes session ID when set" do
+ subject.instance_variable_set(:@session_id, "abc123")
+ headers = subject.send(:request_headers)
+ expect(headers["Mcp-Session-Id"]).to eq("abc123")
+ end
+ end
+end
spec/elelem/mcp/token_storage_spec.rb
@@ -0,0 +1,62 @@
+# frozen_string_literal: true
+
+RSpec.describe Elelem::MCP::TokenStorage do
+ subject { described_class.new }
+
+ let(:resource_url) { "https://example.com/mcp" }
+ let(:token_dir) { File.expand_path("~/.config/elelem/tokens") }
+
+ after do
+ Dir.glob(File.join(token_dir, "*.json")).each { |f| File.delete(f) rescue nil }
+ end
+
+ describe "#save and #load" do
+ it "stores and retrieves tokens" do
+ subject.save(resource_url, access_token: "abc123", refresh_token: "refresh456", expires_in: 3600)
+
+ stored = subject.load(resource_url)
+ expect(stored[:access_token]).to eq("abc123")
+ expect(stored[:refresh_token]).to eq("refresh456")
+ expect(stored[:expires_at]).to be > Time.now.to_i
+ end
+
+ it "returns nil for unknown resource" do
+ expect(subject.load("https://unknown.com")).to be_nil
+ end
+
+ it "handles missing refresh_token" do
+ subject.save(resource_url, access_token: "abc123")
+
+ stored = subject.load(resource_url)
+ expect(stored[:access_token]).to eq("abc123")
+ expect(stored[:refresh_token]).to be_nil
+ end
+ end
+
+ describe "#save_client and #load_client" do
+ it "stores and retrieves client registration" do
+ client_data = { "client_id" => "elelem-123", "client_secret" => nil }
+ subject.save_client(resource_url, client_data)
+
+ stored = subject.load_client(resource_url)
+ expect(stored[:client_id]).to eq("elelem-123")
+ end
+
+ it "returns nil for unknown client" do
+ expect(subject.load_client("https://unknown.com")).to be_nil
+ end
+ end
+
+ describe "file permissions" do
+ it "creates token files with 0600 permissions" do
+ subject.save(resource_url, access_token: "secret")
+
+ files = Dir.glob(File.join(token_dir, "*.json"))
+ expect(files).not_to be_empty
+ files.each do |f|
+ mode = File.stat(f).mode & 0o777
+ expect(mode).to eq(0o600)
+ end
+ end
+ end
+end
spec/spec_helper.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+require_relative "../lib/elelem/mcp"
+
+RSpec.configure do |config|
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
.rspec
@@ -0,0 +1,1 @@
+--require spec_helper
Gemfile
@@ -6,3 +6,4 @@ gemspec name: "elelem-mcp"
gem "irb"
gem "rake", "~> 13.0"
+gem "rspec", "~> 3.0"
Gemfile.lock
@@ -0,0 +1,173 @@
+PATH
+ remote: .
+ specs:
+ elelem-mcp (0.1.0)
+ digest (~> 3.2)
+ elelem (~> 0.1)
+ fileutils (~> 1.8)
+ json (~> 2.21)
+ net-http (~> 0.9)
+ open3 (~> 0.2)
+ uri (~> 1.1)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ base64 (0.3.0)
+ bigdecimal (4.1.2)
+ date (3.5.1)
+ diff-lcs (1.6.2)
+ digest (3.2.1)
+ elelem (0.10.0)
+ base64 (~> 0.1)
+ date (~> 3.0)
+ digest (~> 3.0)
+ erb (~> 6.0)
+ fileutils (~> 1.0)
+ json (~> 2.0)
+ json_schemer (~> 2.0)
+ logger (~> 1.0)
+ net-hippie (~> 1.0)
+ open3 (~> 0.1)
+ optparse (~> 0.1)
+ pathname (~> 0.1)
+ reline (~> 0.6)
+ securerandom (~> 0.1)
+ shellwords (~> 0.2)
+ stringio (~> 3.0)
+ tempfile (~> 0.3)
+ uri (~> 1.0)
+ webrick (~> 1.9)
+ erb (6.0.7)
+ fileutils (1.8.0)
+ hana (1.3.7)
+ io-console (0.9.2)
+ irb (1.18.0)
+ pp (>= 0.6.0)
+ prism (>= 1.3.0)
+ rdoc (>= 4.0.0)
+ reline (>= 0.4.2)
+ json (2.21.2)
+ json_schemer (2.5.0)
+ bigdecimal
+ hana (~> 1.3)
+ regexp_parser (~> 2.0)
+ simpleidn (~> 0.2)
+ logger (1.7.0)
+ monitor (0.2.0)
+ net-hippie (1.5.1)
+ base64 (~> 0.1)
+ json (~> 2.0)
+ logger (~> 1.0)
+ monitor (~> 0.1)
+ net-http (~> 0.1)
+ openssl (~> 4.0)
+ resolv (~> 0.1)
+ timeout (~> 0.1)
+ net-http (0.9.1)
+ uri (>= 0.11.1)
+ open3 (0.2.1)
+ openssl (4.0.2)
+ optparse (0.8.1)
+ pathname (0.5.0)
+ pp (0.6.4)
+ prettyprint
+ prettyprint (0.2.0)
+ prism (1.9.0)
+ rake (13.4.2)
+ rbs (4.2.0)
+ logger
+ prism (>= 1.6.0)
+ tsort
+ rdoc (8.0.0)
+ erb
+ prism (>= 1.6.0)
+ rbs (>= 4.0.0)
+ tsort
+ regexp_parser (2.12.0)
+ reline (0.7.0)
+ io-console (~> 0.5)
+ resolv (0.7.2)
+ rspec (3.13.2)
+ rspec-core (~> 3.13.0)
+ rspec-expectations (~> 3.13.0)
+ rspec-mocks (~> 3.13.0)
+ rspec-core (3.13.6)
+ rspec-support (~> 3.13.0)
+ rspec-expectations (3.13.5)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-mocks (3.13.8)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.13.0)
+ rspec-support (3.13.7)
+ securerandom (0.4.1)
+ shellwords (0.2.2)
+ simpleidn (0.3.0)
+ stringio (3.2.0)
+ tempfile (0.3.1)
+ timeout (0.6.1)
+ tsort (0.2.0)
+ uri (1.1.1)
+ webrick (1.9.2)
+
+PLATFORMS
+ ruby
+ x86_64-linux
+
+DEPENDENCIES
+ elelem-mcp!
+ irb
+ rake (~> 13.0)
+ rspec (~> 3.0)
+
+CHECKSUMS
+ base64 (0.3.0) sha256=27337aeabad6ffae05c265c450490628ef3ebd4b67be58257393227588f5a97b
+ bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
+ bundler (4.0.20) sha256=7978a8ac648767f5e635bc522445b79e80a52b907a39a36c2d8085ed6bc762ae
+ date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
+ diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
+ digest (3.2.1) sha256=ab3312b4e272d7d5dc41c564c86a25861a1f34ac5153374199a0b74861395947
+ elelem (0.10.0) sha256=de67f3a28351640da471e6e80e7a1d79e83e293cbc502a9e98c3c609cad2e237
+ elelem-mcp (0.1.0)
+ erb (6.0.7) sha256=c5ca6dc25b0ef974a44dc8f59fe847577122483b1968a38dec305c60bf91ee92
+ fileutils (1.8.0) sha256=8c6b1df54e2540bdb2f39258f08af78853aa70bad52b4d394bbc6424593c6e02
+ hana (1.3.7) sha256=5425db42d651fea08859811c29d20446f16af196308162894db208cac5ce9b0d
+ io-console (0.9.2) sha256=efa74f891dd03c0939a931dfc6e74c2813d904763d456ea9762b0525e748db08
+ irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
+ json (2.21.2) sha256=1f1d3b7cf2b3ba1a69beca0bb6db13d5438b80bff3cd54cdaaa620b9b07c1c6a
+ json_schemer (2.5.0) sha256=2f01fb4cce721a4e08dd068fc2030cffd0702a7f333f1ea2be6e8991f00ae396
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
+ monitor (0.2.0) sha256=18698584f161ca6611d73130663990ca7dc439d9d88095226e683035d66f1f0f
+ net-hippie (1.5.1) sha256=76ecfc7d8df7866c3ed4f2e25fe963d87537451a1fb0d318b58eca161127fbcc
+ net-http (0.9.1) sha256=25ba0b67c63e89df626ed8fac771d0ad24ad151a858af2cc8e6a716ca4336996
+ open3 (0.2.1) sha256=8e2d7d2113526351201438c1aa35c8139f0141c9e8913baa007c898973bf3952
+ openssl (4.0.2) sha256=1037ad2868ae58df9ad917891c0c0f9815a1172f6846d4bcdd508e4c2ee747c2
+ optparse (0.8.1) sha256=42bea10d53907ccff4f080a69991441d611fbf8733b60ed1ce9ee365ce03bd1a
+ pathname (0.5.0) sha256=d5a331784f6e1f2fefb31c2ff0b8855aabfb661d807284ead9fa47b883d81623
+ pp (0.6.4) sha256=dfcb0fce700c41456265922884f9fe195d7fbb0674a3578e6c0f69588e82b570
+ prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
+ rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
+ rbs (4.2.0) sha256=51f7b886dcc05bc09e10b901daa6a81829f6adc03101d6ca9ea4aac6103e0674
+ rdoc (8.0.0) sha256=03bf8c08a9639658855a0cfd77c0abca8325c227693f7f33f82957811348c469
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
+ reline (0.7.0) sha256=5b012d8e55dbf9d450f12bde2cf7d15ff546ae80b3f8f3b30e570d431815583d
+ resolv (0.7.2) sha256=626d044d975ab2daac759bf898416f1b51e2cb8dcd6727c2b5b5b28b97ead2e1
+ rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
+ rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
+ rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
+ rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
+ rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
+ securerandom (0.4.1) sha256=cc5193d414a4341b6e225f0cb4446aceca8e50d5e1888743fac16987638ea0b1
+ shellwords (0.2.2) sha256=b8695a791de2f71472de5abdc3f4332f6535a4177f55d8f99e7e44266cd32f94
+ simpleidn (0.3.0) sha256=12ca730bed2f3db04d11e9bfd1bca3e11fb37f55b21eb2e9793fb5814bf54d03
+ stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
+ tempfile (0.3.1) sha256=0bb53ab646744e505eb3102147e22ae130a626a15563e882428c1ec973fed76a
+ timeout (0.6.1) sha256=78f57368a7e7bbadec56971f78a3f5ecbcfb59b7fcbb0a3ed6ddc08a5094accb
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
+ uri (1.1.1) sha256=379fa58d27ffb1387eaada68c749d1426738bd0f654d812fcc07e7568f5c57c6
+ webrick (1.9.2) sha256=beb4a15fc474defed24a3bda4ffd88a490d517c9e4e6118c3edce59e45864131
+
+BUNDLED WITH
+ 4.0.20
Rakefile
@@ -1,4 +1,8 @@
# frozen_string_literal: true
require "bundler/gem_tasks"
-task default: %i[]
+require "rspec/core/rake_task"
+
+RSpec::Core::RakeTask.new(:spec)
+
+task default: %i[spec]