Skip to content

Fix destructiveHint/openWorldHint coerced to true when explicitly false#147

Open
rickreyhsig-wealthbox wants to merge 1 commit into
patvice:mainfrom
rickreyhsig-wealthbox:fix/annotation-hint-false-coercion
Open

Fix destructiveHint/openWorldHint coerced to true when explicitly false#147
rickreyhsig-wealthbox wants to merge 1 commit into
patvice:mainfrom
rickreyhsig-wealthbox:fix/annotation-hint-false-coercion

Conversation

@rickreyhsig-wealthbox

Copy link
Copy Markdown

Summary

Fixes #143.

Annotation#initialize reads destructiveHint/openWorldHint with annotation["key"] || default. Since both default to true, an explicit false from the server (false || true) is silently discarded and replaced with true. readOnlyHint/idempotentHint don't have this problem — their defaults are already false, so || never overrides an explicit value either way.

RubyLLM::MCP::Annotation.new({"destructiveHint" => false}).destructive_hint
# => true (should be false)

Fix

Swapped || for fetch(key, default), which only applies the default when the key is absent from the hash — an explicit false (or true) passes through untouched:

@destructive_hint = annotation.fetch("destructiveHint", true)
@open_world_hint  = annotation.fetch("openWorldHint", true)

Tests

Updated the existing RubyLLM::MCP::Annotation spec block in tool_spec.rb, which had a test explicitly documenting the old (buggy) behavior — flipped it to assert the correct behavior instead, and dropped stale comments elsewhere in that block referencing the old || semantics.

Verified directly against the real RubyLLM::MCP::Annotation class (Ruby 3.2, gem's own dependency chain) for both the fixed fields and the two fields that were already correct — all pass:

PASS: explicit false destructiveHint preserved (got false, expected false)
PASS: explicit false openWorldHint preserved (got false, expected false)
PASS: missing destructiveHint defaults true (got true, expected true)
PASS: missing openWorldHint defaults true (got true, expected true)
PASS: explicit true destructiveHint stays true (got true, expected true)
PASS: explicit true openWorldHint stays true (got true, expected true)
PASS: readOnlyHint unaffected (got false, expected false)
PASS: idempotentHint unaffected (got false, expected false)

I wasn't able to run the full spec suite locally — tool_spec.rb's before(:all) spins up the fixture MCP servers over bun, and getting those running requires installing @modelcontextprotocol/sdk for each fixture app individually, which felt out of scope for verifying this change. Happy to do so if that's actually needed for review.

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 patvice#143
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Annotations for destructive and openworld get coerced into true regardless if explicitly set to false

1 participant