diff --git a/lib/ruby_llm/mcp/native/client.rb b/lib/ruby_llm/mcp/native/client.rb index da4d3c4..850638f 100644 --- a/lib/ruby_llm/mcp/native/client.rb +++ b/lib/ruby_llm/mcp/native/client.rb @@ -124,6 +124,7 @@ def start def stop @transport&.close + ensure @capabilities = nil @transport = nil @task_registry = Native::TaskRegistry.new diff --git a/lib/ruby_llm/mcp/native/transports/streamable_http.rb b/lib/ruby_llm/mcp/native/transports/streamable_http.rb index b43b225..bc31fcc 100644 --- a/lib/ruby_llm/mcp/native/transports/streamable_http.rb +++ b/lib/ruby_llm/mcp/native/transports/streamable_http.rb @@ -135,6 +135,7 @@ def alive? def close terminate_session + ensure cleanup_sse_resources cleanup_connection end diff --git a/spec/ruby_llm/mcp/native/client_spec.rb b/spec/ruby_llm/mcp/native/client_spec.rb index 3397d1d..5a7eaf4 100644 --- a/spec/ruby_llm/mcp/native/client_spec.rb +++ b/spec/ruby_llm/mcp/native/client_spec.rb @@ -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 }) diff --git a/spec/ruby_llm/mcp/native/transports/streamable_http_spec.rb b/spec/ruby_llm/mcp/native/transports/streamable_http_spec.rb index bb1e5a5..931e01f 100644 --- a/spec/ruby_llm/mcp/native/transports/streamable_http_spec.rb +++ b/spec/ruby_llm/mcp/native/transports/streamable_http_spec.rb @@ -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) }