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
2 changes: 1 addition & 1 deletion docs/_server/cancellation.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Cancellation
nav_order: 13
nav_order: 14
---

# Cancellation
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/completion.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Completion
nav_order: 16
nav_order: 17
redirect_from:
- /server/completions/
---
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/configuration.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Configuration
nav_order: 20
nav_order: 21
---

# Configuration
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/custom-methods.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Custom Methods
nav_order: 21
nav_order: 22
---

# Custom Methods
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/elicitation.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Elicitation
nav_order: 9
nav_order: 10
---

# Elicitation
Expand Down
83 changes: 83 additions & 0 deletions docs/_server/icons.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
---
layout: default
title: Icons
nav_order: 7
---

# Icons

The MCP spec lets a server attach [icons](https://modelcontextprotocol.io/specification/2025-11-25/basic#icons)
to its own `serverInfo` and to each tool, prompt, resource, and resource template, so a client can show a visual identifier next to them.

## Defining Icons

`MCP::Icon.new` takes the members of the specification's `Icon` type as keyword arguments:

- `src`: the URI of the icon, required. The specification allows an HTTP/HTTPS URL or a `data:` URI with Base64-encoded image data
- `mime_type`: a MIME type such as `"image/png"` or `"image/svg+xml"`, for when the type of the source is missing or generic
- `sizes`: an Array of Strings in `WxH` form, such as `["48x48", "96x96"]`, or `["any"]` for a scalable format like SVG
- `theme`: `"light"` or `"dark"` when the icon is designed for one background

```ruby
icon = MCP::Icon.new(src: "https://example.com/icon.png", mime_type: "image/png", sizes: ["48x48"])
```

Each argument is checked against the type the specification's schema declares, and an `ArgumentError` is raised at definition time for
a `src` that is missing, empty, or not a String, and, when one of the optional arguments is given, for a `sizes` that is not an Array of Strings,
a `mime_type` that is not a String, or a `theme` other than the two values.
What a value means is not checked: the scheme of `src` and the `WxH` form of a size are the server author's to get right, and a client applies
the security rules of the specification (HTTPS or `data:` URIs only, the same origin as the server, size limits) when it fetches an icon.

A Hash is accepted wherever an `MCP::Icon` is and is converted through `MCP::Icon.new`, so it is checked the same way.
Its keys may be Symbols or Strings and may use the keyword names above or the wire name `mimeType`; a Hash with any other key,
or with the same member given twice, is refused.

```ruby
icons = [{ src: "https://example.com/icon.png", mimeType: "image/png", sizes: ["48x48"] }]
```

## Attaching Icons

- `MCP::Server.new(icons: [...])` advertises the icons in `serverInfo`
- Tools and prompts take `icons [...]` in a class definition, or `icons:` on `MCP::Tool.define` and `MCP::Prompt.define`
- Resources and resource templates take `icons [...]` in a class definition, `icons:` on `define`, `icons:` on `MCP::Resource.new`
and `MCP::ResourceTemplate.new`, and `icons:` on `MCP::Server#define_resource` and `MCP::Server#define_resource_template`

```ruby
class WeatherTool < MCP::Tool
description "Reports the weather"
icons [MCP::Icon.new(src: "https://example.com/weather.png", mime_type: "image/png", sizes: ["48x48"])]

def self.call(server_context:)
MCP::Tool::Response.new([{ type: "text", text: "sunny" }])
end
end

prompt = MCP::Prompt.define(
name: "greeting",
description: "Greets the user",
icons: [{ src: "https://example.com/greeting.svg", mimeType: "image/svg+xml", sizes: ["any"] }]
) do |args, server_context:|
MCP::Prompt::Result.new(
messages: [
MCP::Prompt::Message.new(role: "user", content: MCP::Content::Text.new("Hello!"))
]
)
end

server = MCP::Server.new(
name: "weather_server",
icons: [
MCP::Icon.new(src: "https://example.com/server.png", theme: "light"),
MCP::Icon.new(src: "https://example.com/server-dark.png", theme: "dark")
],
tools: [WeatherTool],
prompts: [prompt]
)
```

An `icons` of `nil` or `[]` leaves the `icons` member out of the wire representation.

{: .note }
> Icons were added in the 2025-11-25 revision of the specification. The icons in `serverInfo` are sent only when
> the negotiated protocol version is 2025-11-25 or later; the icons of tools, prompts, and resources are listed as defined.
2 changes: 1 addition & 1 deletion docs/_server/logging.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Logging
nav_order: 17
nav_order: 18
---

# Logging
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/mrtr.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Multi Round-Trip Requests
nav_order: 10
nav_order: 11
redirect_from:
- /server/multi-round-trip-results/
---
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/notifications.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Notifications
nav_order: 11
nav_order: 12
---

# Notifications
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/pagination.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Pagination
nav_order: 18
nav_order: 19
---

# Pagination
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/ping.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Ping
nav_order: 15
nav_order: 16
---

# Ping
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/progress.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Progress
nav_order: 14
nav_order: 15
---

# Progress
Expand Down
2 changes: 2 additions & 0 deletions docs/_server/prompts.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ end
The [`server_context`](/server/server-context/) parameter is the `server_context` passed into the server and can be used to pass per request information,
e.g. around authentication state or user preferences.

Icons for prompts are documented on the [Icons](/server/icons/) page.

## Key Components

- `MCP::Prompt::Argument` - Defines input parameters for the prompt template with name, title, description, and required flag
Expand Down
2 changes: 2 additions & 0 deletions docs/_server/resources.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,8 @@ server.resources_read_handler do |params|
end
```

Icons for resources and resource templates are documented on the [Icons](/server/icons/) page.

## Reading Binary Resources

For binary resources, respond with a base64-encoded `blob` field instead of `text`.
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/roots.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Roots
nav_order: 7
nav_order: 8
---

# Roots
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/sampling.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Sampling
nav_order: 8
nav_order: 9
---

# Sampling
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/server-context.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Server Context
nav_order: 19
nav_order: 20
---

# Server Context
Expand Down
2 changes: 1 addition & 1 deletion docs/_server/subscriptions.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
layout: default
title: Subscriptions
nav_order: 12
nav_order: 13
redirect_from:
- /server/notification-subscriptions/
---
Expand Down
2 changes: 2 additions & 0 deletions docs/_server/tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,8 @@ end
The [`server_context`](/server/server-context/) parameter is the `server_context` passed into the server and can be used to pass per request information,
e.g. around authentication state.

Icons for tools are documented on the [Icons](/server/icons/) page.

## Tool argument keys

Tool arguments are delivered as a `Hash` whose keys are Ruby symbols at every nesting level, including nested objects
Expand Down
61 changes: 61 additions & 0 deletions lib/mcp/icon.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,70 @@ module MCP
# is not judged: `src` may be any non-empty `String` (the schema types it as a URI, which an empty `String`
# is not, and the specification allows an HTTP/HTTPS URL or a `data:` URI), and a size may be any `String`
# (the specification expects `WxH` or `"any"`).
#
# Wherever an `Icon` is accepted (`Server.new(icons:)` and the `icons` of a tool, prompt, resource, or resource template),
# a Hash is accepted too and converted through `Icon.from`, so it is checked the same way.
class Icon
SUPPORTED_THEMES = ["light", "dark"].freeze

# The keys `from` accepts in a Hash: the keyword names of `new` and the wire name `mimeType`,
# as Symbols or Strings.
HASH_KEYWORDS = {
"src" => :src,
"mimeType" => :mime_type,
"mime_type" => :mime_type,
"sizes" => :sizes,
"theme" => :theme,
}.freeze
private_constant :HASH_KEYWORDS

class << self
# Returns `value` when it is already an `Icon`, builds one from a Hash, and refuses anything else.
def from(value)
return value if value.is_a?(Icon)

unless value.is_a?(Hash)
raise ArgumentError, "An icon must be an MCP::Icon or a Hash (got #{value.class})."
end

keywords = {}
value.each do |key, member|
unless key.is_a?(Symbol) || key.is_a?(String)
raise ArgumentError, "An icon Hash key must be a Symbol or a String (got #{key.class})."
end

keyword = HASH_KEYWORDS[key.to_s]
unless keyword
raise ArgumentError, "An icon Hash may only hold src, mimeType (or mime_type), sizes, and theme (got #{key.inspect})."
end
raise ArgumentError, "An icon Hash gives #{keyword} twice." if keywords.key?(keyword)

keywords[keyword] = member
end

new(**keywords)
end

# Converts the `icons` argument of a server, tool, prompt, resource, or resource template: `nil` stays `nil`,
# each element of an Array goes through `from` into a frozen Array, and anything else is refused.
# The frozen Array keeps a later `<<` on a reader from adding an element these checks never saw.
def from_list(value)
return if value.nil?

unless value.is_a?(Array)
raise ArgumentError, "icons must be nil or an Array of MCP::Icon or Hash (got #{value.class})."
end

icons = value.each_with_index.map do |icon, index|
from(icon)
rescue ArgumentError => e
raise ArgumentError, "icons[#{index}]: #{e.message}"
end

icons.freeze
end
end

attr_reader :mime_type, :sizes, :src, :theme

def initialize(mime_type: nil, sizes: nil, src:, theme: nil)
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/prompt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ def icons(value = NOT_SET)
if value == NOT_SET
@icons_value
else
@icons_value = value
@icons_value = Icon.from_list(value)
end
end

Expand Down
4 changes: 2 additions & 2 deletions lib/mcp/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ def icons(value = NOT_SET)
if value == NOT_SET
@icons_value
else
@icons_value = value
@icons_value = Icon.from_list(value)
end
end

Expand Down Expand Up @@ -147,7 +147,7 @@ def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type:
@name = name
@title = title
@description = description
@icons = icons
@icons = Icon.from_list(icons)
@mime_type = mime_type
@annotations = annotations
@size = size
Expand Down
4 changes: 2 additions & 2 deletions lib/mcp/resource_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def icons(value = NOT_SET)
if value == NOT_SET
@icons_value
else
@icons_value = value
@icons_value = Icon.from_list(value)
end
end

Expand Down Expand Up @@ -158,7 +158,7 @@ def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mi
@name = name
@title = title
@description = description
@icons = icons
@icons = Icon.from_list(icons)
@mime_type = mime_type
@annotations = annotations
@meta = meta
Expand Down
11 changes: 8 additions & 3 deletions lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -181,8 +181,13 @@ class ValidationError < StandardError; end
Methods::RESOURCES_READ,
].freeze

attr_accessor :description, :icons, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resource_templates, :server_context, :configuration, :capabilities, :transport, :logging_message_notification
attr_reader :resources, :page_size, :client_capabilities, :ttl_ms, :cache_scope, :request_state_security
attr_accessor :description, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resource_templates, :server_context, :configuration, :capabilities, :transport, :logging_message_notification
attr_reader :icons, :resources, :page_size, :client_capabilities, :ttl_ms, :cache_scope, :request_state_security

# Replaces the icons advertised in `serverInfo`; a Hash is converted through `Icon.from`.
def icons=(value)
@icons = Icon.from_list(value)
end

def initialize(
description: nil,
Expand All @@ -207,7 +212,7 @@ def initialize(
transport: nil
)
@description = description
@icons = icons
@icons = Icon.from_list(icons)
@name = name
@title = title
@version = version
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ def icons(value = NOT_SET)
if value == NOT_SET
@icons_value
else
@icons_value = value
@icons_value = Icon.from_list(value)
end
end

Expand Down
Loading
Loading