Skip to content

Validate icons given as Hashes through MCP::Icon - #586

Open
koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:validate_icons_given_as_hashes
Open

koic wants to merge 1 commit into
modelcontextprotocol:mainfrom
koic:validate_icons_given_as_hashes

Conversation

@koic

@koic koic commented Oct 1, 2026

Copy link
Copy Markdown
Member

Motivation and Context

MCP::Icon.new checks an icon against the specification's Icon type, but the places that take icons never saw that check when an application passed a Hash: Server.new(icons:), the class-level icons of a tool, prompt, resource, and resource template, and Resource.new(icons:) and ResourceTemplate.new(icons:) stored whatever they were given, and to_h serialized each element with its own to_h. A Hash is a supported way to give an icon (the SDK's own tests pass wire-shaped ones), and on that path { src: "x", sizes: "51x51" } reached the client as a JSON string, while a Hash written with the keyword names, such as { mime_type: "image/png", src: "x" }, was emitted with the key mime_type, which the wire Icon does not have. The TypeScript SDK types those places as Icon[] and the Python SDK's models hold list[Icon], so neither lets an unchecked shape through there.

Every place that takes icons now passes them through Icon.from_list, which keeps nil, converts each element of an Array with Icon.from, and refuses anything else. Icon.from returns an Icon as it is, builds one from a Hash, and refuses any other object. A Hash may use the keyword names of Icon.new or the wire name mimeType, as Symbols or Strings, so both spellings serialize the same way; a key outside the Icon type, or one member given under two spellings, is refused rather than emitted. A rejected element is reported with its position (icons[1]: ...) and the message Icon.new would give, at definition time, where the annotations of a tool or resource are already built from a Hash the same way. The docs gain an Icons page describing MCP::Icon, the Hash form, and where icons attach.
The page sits right after the Resources page in the navigation, so the pages that followed move down by one.

How Has This Been Tested?

New tests in test/mcp/icon_test.rb, test/mcp/tool_test.rb, test/mcp/prompt_test.rb, test/mcp/resource_test.rb, test/mcp/resource_template_test.rb, and test/mcp/server_test.rb. Against the previous library, a Hash icon with a String sizes was serialized as given and a mime_type: key was emitted as mime_type.

Breaking Changes

An element of icons that is neither an MCP::Icon nor a Hash is refused with ArgumentError, as is an icons that is neither nil nor an Array; a Hash icon with a key that is not a Symbol or a String, a key outside the Icon type, a member given under two spellings, or a value MCP::Icon.new rejects now raises at definition time, and through Server#icons=, instead of being serialized. A Hash written with the keyword names now serializes with the wire names. The icons readers of a server, tool, prompt, resource, and resource template return a new, frozen Array of MCP::Icon instances where they returned the Array and the Hashes as given, so a later change to the Array given is no longer seen, and adding to the Array read back raises FrozenError instead of slipping past the checks.

Types of changes

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to change)
  • Documentation update

Checklist

  • I have read the MCP Documentation
  • My code follows the repository's style guidelines
  • New and existing tests pass locally
  • I have added appropriate error handling
  • I have added or updated documentation as needed

## Motivation and Context

`MCP::Icon.new` checks an icon against the specification's `Icon` type, but the places that take icons never
saw that check when an application passed a Hash: `Server.new(icons:)`, the class-level `icons` of a tool,
prompt, resource, and resource template, and `Resource.new(icons:)` and `ResourceTemplate.new(icons:)` stored
whatever they were given, and `to_h` serialized each element with its own `to_h`. A Hash is a supported way
to give an icon (the SDK's own tests pass wire-shaped ones), and on that path `{ src: "x", sizes: "51x51" }`
reached the client as a JSON string, while a Hash written with the keyword names, such as
`{ mime_type: "image/png", src: "x" }`, was emitted with the key `mime_type`, which the wire `Icon` does not have.
The TypeScript SDK types those places as `Icon[]` and the Python SDK's models hold `list[Icon]`, so neither lets
an unchecked shape through there.

Every place that takes icons now passes them through `Icon.from_list`, which keeps `nil`, converts each element
of an Array with `Icon.from`, and refuses anything else. `Icon.from` returns an `Icon` as it is, builds one from
a Hash, and refuses any other object. A Hash may use the keyword names of `Icon.new` or the wire name `mimeType`,
as Symbols or Strings, so both spellings serialize the same way; a key outside the `Icon` type, or one member
given under two spellings, is refused rather than emitted. A rejected element is reported with its position
(`icons[1]: ...`) and the message `Icon.new` would give, at definition time, where the annotations of a tool
or resource are already built from a Hash the same way. The docs gain an Icons page describing `MCP::Icon`,
the Hash form, and where icons attach.
The page sits right after the Resources page in the navigation, so the pages that followed move down by one.

## How Has This Been Tested?

New tests in `test/mcp/icon_test.rb`, `test/mcp/tool_test.rb`, `test/mcp/prompt_test.rb`, `test/mcp/resource_test.rb`,
`test/mcp/resource_template_test.rb`, and `test/mcp/server_test.rb`. Against the previous library, a Hash icon with
a String `sizes` was serialized as given and a `mime_type:` key was emitted as `mime_type`.

## Breaking Changes

An element of `icons` that is neither an `MCP::Icon` nor a Hash is refused with `ArgumentError`, as is
an `icons` that is neither `nil` nor an Array; a Hash icon with a key that is not a Symbol or a String,
a key outside the `Icon` type, a member given under two spellings, or a value `MCP::Icon.new` rejects now raises
at definition time, and through `Server#icons=`, instead of being serialized. A Hash written with
the keyword names now serializes with the wire names. The `icons` readers of a server, tool, prompt, resource,
and resource template return a new, frozen Array of `MCP::Icon` instances where they returned the Array and
the Hashes as given, so a later change to the Array given is no longer seen, and adding to the Array read back
raises `FrozenError` instead of slipping past the checks.

This branch has not been deployed

No deployments
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.

2 participants