From 67084b086a89a357e3511130db5e4d9acbff60c1 Mon Sep 17 00:00:00 2001 From: rickreyhsig-wealthbox Date: Tue, 7 Jul 2026 15:32:49 -0400 Subject: [PATCH] Fix destructiveHint/openWorldHint coerced true when explicitly false Annotation#initialize used `annotation["key"] || default`, which discards an explicit `false` from the server and falls back to the truthy default. `fetch(key, default)` only applies the default when the key is absent, so an explicit false is preserved. readOnlyHint and idempotentHint are unaffected since their defaults are already false. Fixes #143 --- lib/ruby_llm/mcp/tool.rb | 4 ++-- spec/ruby_llm/mcp/tool_spec.rb | 14 +++++--------- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/lib/ruby_llm/mcp/tool.rb b/lib/ruby_llm/mcp/tool.rb index 9f49d00..071fb02 100644 --- a/lib/ruby_llm/mcp/tool.rb +++ b/lib/ruby_llm/mcp/tool.rb @@ -8,9 +8,9 @@ class Annotation def initialize(annotation) @title = annotation["title"] || "" @read_only_hint = annotation["readOnlyHint"] || false - @destructive_hint = annotation["destructiveHint"] || true + @destructive_hint = annotation.fetch("destructiveHint", true) @idempotent_hint = annotation["idempotentHint"] || false - @open_world_hint = annotation["openWorldHint"] || true + @open_world_hint = annotation.fetch("openWorldHint", true) end def to_h diff --git a/spec/ruby_llm/mcp/tool_spec.rb b/spec/ruby_llm/mcp/tool_spec.rb index f82dbf1..21867fc 100644 --- a/spec/ruby_llm/mcp/tool_spec.rb +++ b/spec/ruby_llm/mcp/tool_spec.rb @@ -958,14 +958,12 @@ def execute describe RubyLLM::MCP::Annotation do describe "#initialize" do it "parses truthy annotation fields from hash" do - # NOTE: The implementation uses || for defaults, so false values get - # replaced with defaults. Only truthy values are preserved. annotation_data = { "title" => "My Custom Tool", "readOnlyHint" => true, - "destructiveHint" => true, # Using true since false gets default + "destructiveHint" => true, "idempotentHint" => true, - "openWorldHint" => true # Using true since false gets default + "openWorldHint" => true } annotation = described_class.new(annotation_data) @@ -1000,16 +998,14 @@ def execute expect(annotation.open_world_hint).to be(true) # Default end - it "applies defaults when false is provided (current || behavior)" do - # This test documents the current behavior: false values get defaults + it "preserves an explicit false instead of falling back to the default" do annotation = described_class.new({ "destructiveHint" => false, "openWorldHint" => false }) - # Because || false returns the right-hand side default - expect(annotation.destructive_hint).to be(true) # Got default - expect(annotation.open_world_hint).to be(true) # Got default + expect(annotation.destructive_hint).to be(false) + expect(annotation.open_world_hint).to be(false) end end