Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/ruby_llm/mcp/native/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ def start

def stop
@transport&.close
ensure
@capabilities = nil
@transport = nil
@task_registry = Native::TaskRegistry.new
Expand Down
1 change: 1 addition & 0 deletions lib/ruby_llm/mcp/native/transports/streamable_http.rb
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ def alive?

def close
terminate_session
ensure
cleanup_sse_resources
cleanup_connection
end
Expand Down
17 changes: 17 additions & 0 deletions spec/ruby_llm/mcp/native/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,23 @@ def build_client(human_callback: nil, **options)
)
end

describe "#stop" do
it "releases the per-client human-in-the-loop registry even when transport teardown fails" do
client = build_client
owner_id = client.registry_owner_id
transport = instance_double(RubyLLM::MCP::Native::Transport)
allow(transport).to receive(:close).and_raise(StandardError, "teardown failed")
client.instance_variable_set(:@transport, transport)

expect(RubyLLM::MCP::Handlers::HumanInTheLoopRegistry)
.to receive(:release).with(owner_id).and_call_original

expect { client.stop }.to raise_error(StandardError, /teardown failed/)

expect(client.instance_variable_get(:@transport)).to be_nil
end
end

describe "#execute_tool" do
it "fails closed when callback returns non-normalized decision" do
client = build_client(human_callback: ->(_name, _params) { true })
Expand Down
14 changes: 14 additions & 0 deletions spec/ruby_llm/mcp/native/transports/streamable_http_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,20 @@
allow(logger).to receive(:info)
end

describe "#close" do
it "runs the remaining cleanup steps and re-raises when session termination fails" do
allow(transport).to receive(:terminate_session)
.and_raise(RubyLLM::MCP::Errors::TransportError.new(message: "session termination failed", code: nil))
expect(transport).to receive(:cleanup_sse_resources).and_call_original
expect(transport).to receive(:cleanup_connection).and_call_original

expect { transport.close }
.to raise_error(RubyLLM::MCP::Errors::TransportError, /session termination failed/)

expect(transport.instance_variable_get(:@connection)).to be_nil
end
end

describe "#handle_oauth_authorization_error" do
let(:response) { instance_double(HTTPX::Response) }

Expand Down