From 48689306d12a1609111d4db8b7260c9b530e5270 Mon Sep 17 00:00:00 2001 From: Yuki Minamiya Date: Wed, 10 Jun 2026 11:10:25 +0900 Subject: [PATCH] Add `annotations` field to `MCP::Resource` and `MCP::ResourceTemplate` per MCP specification ## Motivation and Context The MCP specification defines an optional `annotations` field on both the `Resource` and `ResourceTemplate` types: https://modelcontextprotocol.io/specification/2025-11-25/server/resources#annotations Annotations provide hints to clients about how to use or display a resource (`audience`, `priority`, `lastModified`). The SDK already ships the `MCP::Annotations` class, and `MCP::Tool` and the content classes already accept annotations, but plain `MCP::Resource` and `MCP::ResourceTemplate` had no way to declare them. Following `MCP::Tool`'s pattern, the field accepts an `MCP::Annotations` instance and serializes it via `annotations&.to_h`, which also normalizes `last_modified` to the spec's `lastModified` key. ## How Has This Been Tested? Added tests in `test/mcp/resource_test.rb` and `test/mcp/resource_template_test.rb`: - `#to_h` omits `annotations` when nil - `#to_h` includes `annotations` when present (also asserting that `last_modified` is serialized as `lastModified`) The full unit suite and RuboCop pass locally. ## Breaking Changes None. `annotations:` is a new optional keyword argument defaulting to `nil`, and it is omitted from `#to_h` output when not set. --- lib/mcp/resource.rb | 6 ++++-- lib/mcp/resource_template.rb | 6 ++++-- test/mcp/resource_template_test.rb | 18 ++++++++++++++++++ test/mcp/resource_test.rb | 14 ++++++++++++++ 4 files changed, 40 insertions(+), 4 deletions(-) diff --git a/lib/mcp/resource.rb b/lib/mcp/resource.rb index e4e55fcd..8b5f469e 100644 --- a/lib/mcp/resource.rb +++ b/lib/mcp/resource.rb @@ -5,15 +5,16 @@ module MCP class Resource - attr_reader :uri, :name, :title, :description, :icons, :mime_type, :size, :meta + attr_reader :uri, :name, :title, :description, :icons, :mime_type, :annotations, :size, :meta - def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, size: nil, meta: nil) + def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, annotations: nil, size: nil, meta: nil) @uri = uri @name = name @title = title @description = description @icons = icons @mime_type = mime_type + @annotations = annotations @size = size @meta = meta end @@ -26,6 +27,7 @@ def to_h description: description, icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) }, mimeType: mime_type, + annotations: annotations&.to_h, size: size, _meta: meta, }.compact diff --git a/lib/mcp/resource_template.rb b/lib/mcp/resource_template.rb index b23ecd01..e078174f 100644 --- a/lib/mcp/resource_template.rb +++ b/lib/mcp/resource_template.rb @@ -2,15 +2,16 @@ module MCP class ResourceTemplate - attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :meta + attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :annotations, :meta - def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil) + def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, annotations: nil, meta: nil) @uri_template = uri_template @name = name @title = title @description = description @icons = icons @mime_type = mime_type + @annotations = annotations @meta = meta end @@ -22,6 +23,7 @@ def to_h description: description, icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) }, mimeType: mime_type, + annotations: annotations&.to_h, _meta: meta, }.compact end diff --git a/test/mcp/resource_template_test.rb b/test/mcp/resource_template_test.rb index 8d25be04..4a6ef5f8 100644 --- a/test/mcp/resource_template_test.rb +++ b/test/mcp/resource_template_test.rb @@ -53,5 +53,23 @@ class ResourceTemplateTest < ActiveSupport::TestCase assert_equal meta, resource_template.to_h[:_meta] end + + test "#to_h omits annotations when nil" do + resource_template = ResourceTemplate.new(uri_template: "file:///{path}", name: "template_without_annotations") + + refute resource_template.to_h.key?(:annotations) + end + + test "#to_h includes annotations when present" do + annotations = Annotations.new(audience: ["user"], priority: 0.8, last_modified: "2025-01-12T15:00:58Z") + resource_template = ResourceTemplate.new( + uri_template: "file:///{path}", + name: "template_with_annotations", + annotations: annotations, + ) + + expected = { audience: ["user"], priority: 0.8, lastModified: "2025-01-12T15:00:58Z" } + assert_equal expected, resource_template.to_h[:annotations] + end end end diff --git a/test/mcp/resource_test.rb b/test/mcp/resource_test.rb index a3d6a1b8..a721ad39 100644 --- a/test/mcp/resource_test.rb +++ b/test/mcp/resource_test.rb @@ -67,5 +67,19 @@ class ResourceTest < ActiveSupport::TestCase assert_equal 0, resource.to_h[:size] end + + test "#to_h omits annotations when nil" do + resource = Resource.new(uri: "file:///test.txt", name: "resource_without_annotations") + + refute resource.to_h.key?(:annotations) + end + + test "#to_h includes annotations when present" do + annotations = Annotations.new(audience: ["user"], priority: 0.8, last_modified: "2025-01-12T15:00:58Z") + resource = Resource.new(uri: "file:///test.txt", name: "resource_with_annotations", annotations: annotations) + + expected = { audience: ["user"], priority: 0.8, lastModified: "2025-01-12T15:00:58Z" } + assert_equal expected, resource.to_h[:annotations] + end end end