diff --git a/lib/mcp/content.rb b/lib/mcp/content.rb index 1a1ee03b..591ecb4b 100644 --- a/lib/mcp/content.rb +++ b/lib/mcp/content.rb @@ -3,56 +3,60 @@ module MCP module Content class Text - attr_reader :text, :annotations + attr_reader :text, :annotations, :meta - def initialize(text, annotations: nil) + def initialize(text, annotations: nil, meta: nil) @text = text @annotations = annotations + @meta = meta end def to_h - { text: text, annotations: annotations, type: "text" }.compact + { text: text, annotations: annotations, _meta: meta, type: "text" }.compact end end class Image - attr_reader :data, :mime_type, :annotations + attr_reader :data, :mime_type, :annotations, :meta - def initialize(data, mime_type, annotations: nil) + def initialize(data, mime_type, annotations: nil, meta: nil) @data = data @mime_type = mime_type @annotations = annotations + @meta = meta end def to_h - { data: data, mimeType: mime_type, annotations: annotations, type: "image" }.compact + { data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "image" }.compact end end class Audio - attr_reader :data, :mime_type, :annotations + attr_reader :data, :mime_type, :annotations, :meta - def initialize(data, mime_type, annotations: nil) + def initialize(data, mime_type, annotations: nil, meta: nil) @data = data @mime_type = mime_type @annotations = annotations + @meta = meta end def to_h - { data: data, mimeType: mime_type, annotations: annotations, type: "audio" }.compact + { data: data, mimeType: mime_type, annotations: annotations, _meta: meta, type: "audio" }.compact end end class EmbeddedResource - attr_reader :resource, :annotations + attr_reader :resource, :annotations, :meta - def initialize(resource, annotations: nil) + def initialize(resource, annotations: nil, meta: nil) @resource = resource @annotations = annotations + @meta = meta end def to_h - { resource: resource.to_h, annotations: annotations, type: "resource" }.compact + { resource: resource.to_h, annotations: annotations, _meta: meta, type: "resource" }.compact end end end diff --git a/lib/mcp/prompt/result.rb b/lib/mcp/prompt/result.rb index b55b2424..c1fb7404 100644 --- a/lib/mcp/prompt/result.rb +++ b/lib/mcp/prompt/result.rb @@ -3,15 +3,16 @@ module MCP class Prompt class Result - attr_reader :description, :messages + attr_reader :description, :messages, :meta - def initialize(description: nil, messages: []) + def initialize(description: nil, messages: [], meta: nil) @description = description @messages = messages + @meta = meta end def to_h - { description: description, messages: messages.map(&:to_h) }.compact + { description: description, messages: messages.map(&:to_h), _meta: meta }.compact end end end diff --git a/lib/mcp/resource.rb b/lib/mcp/resource.rb index 51d92e96..990156b4 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 + attr_reader :uri, :name, :title, :description, :icons, :mime_type, :meta - def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil) + def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil) @uri = uri @name = name @title = title @description = description @icons = icons @mime_type = mime_type + @meta = meta end def to_h @@ -24,6 +25,7 @@ def to_h description: description, icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) }, mimeType: mime_type, + _meta: meta, }.compact end end diff --git a/lib/mcp/resource/contents.rb b/lib/mcp/resource/contents.rb index 8322d641..a7569b05 100644 --- a/lib/mcp/resource/contents.rb +++ b/lib/mcp/resource/contents.rb @@ -3,23 +3,24 @@ module MCP class Resource class Contents - attr_reader :uri, :mime_type + attr_reader :uri, :mime_type, :meta - def initialize(uri:, mime_type: nil) + def initialize(uri:, mime_type: nil, meta: nil) @uri = uri @mime_type = mime_type + @meta = meta end def to_h - { uri: uri, mimeType: mime_type }.compact + { uri: uri, mimeType: mime_type, _meta: meta }.compact end end class TextContents < Contents attr_reader :text - def initialize(text:, uri:, mime_type:) - super(uri: uri, mime_type: mime_type) + def initialize(text:, uri:, mime_type:, meta: nil) + super(uri: uri, mime_type: mime_type, meta: meta) @text = text end @@ -31,8 +32,8 @@ def to_h class BlobContents < Contents attr_reader :data - def initialize(data:, uri:, mime_type:) - super(uri: uri, mime_type: mime_type) + def initialize(data:, uri:, mime_type:, meta: nil) + super(uri: uri, mime_type: mime_type, meta: meta) @data = data end diff --git a/lib/mcp/resource_template.rb b/lib/mcp/resource_template.rb index 871600e8..b23ecd01 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 + attr_reader :uri_template, :name, :title, :description, :icons, :mime_type, :meta - def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil) + def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil, meta: nil) @uri_template = uri_template @name = name @title = title @description = description @icons = icons @mime_type = mime_type + @meta = meta end def to_h @@ -21,6 +22,7 @@ def to_h description: description, icons: icons&.then { |icons| icons.empty? ? nil : icons.map(&:to_h) }, mimeType: mime_type, + _meta: meta, }.compact end end diff --git a/lib/mcp/tool/response.rb b/lib/mcp/tool/response.rb index 0d9dccbb..2532ea83 100644 --- a/lib/mcp/tool/response.rb +++ b/lib/mcp/tool/response.rb @@ -5,9 +5,9 @@ class Tool class Response NOT_GIVEN = Object.new.freeze - attr_reader :content, :structured_content + attr_reader :content, :structured_content, :meta - def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, structured_content: nil) + def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, structured_content: nil, meta: nil) if deprecated_error != NOT_GIVEN warn("Passing `error` with the 2nd argument of `Response.new` is deprecated. Use keyword argument like `Response.new(content, error: error)` instead.", uplevel: 1) error = deprecated_error @@ -16,6 +16,7 @@ def initialize(content = nil, deprecated_error = NOT_GIVEN, error: false, struct @content = content || [] @error = error @structured_content = structured_content + @meta = meta end def error? @@ -23,7 +24,7 @@ def error? end def to_h - { content: content, isError: error?, structuredContent: @structured_content }.compact + { content: content, isError: error?, structuredContent: @structured_content, _meta: meta }.compact end end end diff --git a/test/mcp/content_test.rb b/test/mcp/content_test.rb index f2f5cad2..fc2103ee 100644 --- a/test/mcp/content_test.rb +++ b/test/mcp/content_test.rb @@ -28,6 +28,19 @@ class ImageTest < ActiveSupport::TestCase refute result.key?(:annotations) end + + test "#to_h with meta" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + image = Image.new("base64data", "image/png", meta: meta) + + assert_equal meta, image.to_h[:_meta] + end + + test "#to_h without meta omits the key" do + image = Image.new("base64data", "image/png") + + refute image.to_h.key?(:_meta) + end end class AudioTest < ActiveSupport::TestCase @@ -53,6 +66,19 @@ class AudioTest < ActiveSupport::TestCase refute result.key?(:annotations) end + + test "#to_h with meta" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + audio = Audio.new("base64data", "audio/wav", meta: meta) + + assert_equal meta, audio.to_h[:_meta] + end + + test "#to_h without meta omits the key" do + audio = Audio.new("base64data", "audio/wav") + + refute audio.to_h.key?(:_meta) + end end class EmbeddedResourceTest < ActiveSupport::TestCase @@ -92,6 +118,52 @@ def resource.to_h refute result.key?(:annotations) end + + test "#to_h with meta" do + resource = Object.new + def resource.to_h + { uri: "test://x" } + end + + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + embedded = EmbeddedResource.new(resource, meta: meta) + + assert_equal meta, embedded.to_h[:_meta] + end + + test "#to_h without meta omits the key" do + resource = Object.new + def resource.to_h + { uri: "test://x" } + end + + embedded = EmbeddedResource.new(resource) + + refute embedded.to_h.key?(:_meta) + end + end + + class TextTest < ActiveSupport::TestCase + test "#to_h returns correct format per MCP spec" do + text = Text.new("hello") + result = text.to_h + + assert_equal "text", result[:type] + assert_equal "hello", result[:text] + end + + test "#to_h with meta" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + text = Text.new("hello", meta: meta) + + assert_equal meta, text.to_h[:_meta] + end + + test "#to_h without meta omits the key" do + text = Text.new("hello") + + refute text.to_h.key?(:_meta) + end end end end diff --git a/test/mcp/prompt/result_test.rb b/test/mcp/prompt/result_test.rb new file mode 100644 index 00000000..62f161e2 --- /dev/null +++ b/test/mcp/prompt/result_test.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +require "test_helper" + +module MCP + class Prompt + class ResultTest < ActiveSupport::TestCase + test "#to_h returns description and messages" do + result = Prompt::Result.new( + description: "a prompt", + messages: [Prompt::Message.new(role: "user", content: Content::Text.new("hi"))], + ) + + hash = result.to_h + + assert_equal "a prompt", hash[:description] + assert_equal 1, hash[:messages].size + end + + test "#to_h includes _meta when present" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + result = Prompt::Result.new( + description: "a prompt", + messages: [Prompt::Message.new(role: "user", content: Content::Text.new("hi"))], + meta: meta, + ) + + assert_equal meta, result.to_h[:_meta] + end + + test "#to_h omits _meta when nil" do + result = Prompt::Result.new( + description: "a prompt", + messages: [Prompt::Message.new(role: "user", content: Content::Text.new("hi"))], + ) + + refute result.to_h.key?(:_meta) + end + end + end +end diff --git a/test/mcp/resource/contents_test.rb b/test/mcp/resource/contents_test.rb index 53efaa6a..f89f8f22 100644 --- a/test/mcp/resource/contents_test.rb +++ b/test/mcp/resource/contents_test.rb @@ -70,6 +70,78 @@ class ContentsTest < ActiveSupport::TestCase assert_equal({ uri: "test://binary", blob: "base64data" }, result) refute result.key?(:mimeType) end + + test "Contents#to_h omits _meta when nil" do + contents = Resource::Contents.new(uri: "test://example", mime_type: "text/plain") + + refute contents.to_h.key?(:_meta) + end + + test "Contents#to_h includes _meta when present" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + contents = Resource::Contents.new(uri: "test://example", mime_type: "text/plain", meta: meta) + + assert_equal meta, contents.to_h[:_meta] + end + + test "TextContents#to_h includes _meta when present" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + text_contents = Resource::TextContents.new( + uri: "test://text", + mime_type: "text/plain", + text: "Hello", + meta: meta, + ) + + result = text_contents.to_h + + assert_equal meta, result[:_meta] + assert_equal "Hello", result[:text] + end + + test "TextContents#to_h omits _meta when nil" do + text_contents = Resource::TextContents.new( + uri: "test://text", + mime_type: "text/plain", + text: "Hello", + ) + + refute text_contents.to_h.key?(:_meta) + end + + test "BlobContents#to_h includes _meta when present" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + blob_contents = Resource::BlobContents.new( + uri: "test://binary", + mime_type: "image/png", + data: "base64data", + meta: meta, + ) + + result = blob_contents.to_h + + assert_equal meta, result[:_meta] + assert_equal "base64data", result[:blob] + end + + test "BlobContents#to_h omits _meta when nil" do + blob_contents = Resource::BlobContents.new( + uri: "test://binary", + mime_type: "image/png", + data: "base64data", + ) + + refute blob_contents.to_h.key?(:_meta) + end + + test "Contents#to_h preserves empty _meta hash" do + contents = Resource::Contents.new(uri: "test://example", mime_type: "text/plain", meta: {}) + + result = contents.to_h + + assert result.key?(:_meta) + assert_equal({}, result[:_meta]) + end end end end diff --git a/test/mcp/resource_template_test.rb b/test/mcp/resource_template_test.rb index 6b8a565e..8d25be04 100644 --- a/test/mcp/resource_template_test.rb +++ b/test/mcp/resource_template_test.rb @@ -36,5 +36,22 @@ class ResourceTemplateTest < ActiveSupport::TestCase assert_equal expected_icons, resource_template.to_h[:icons] end + + test "#to_h omits _meta when nil" do + resource_template = ResourceTemplate.new(uri_template: "file:///{path}", name: "template_without_meta") + + refute resource_template.to_h.key?(:_meta) + end + + test "#to_h includes _meta when present" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + resource_template = ResourceTemplate.new( + uri_template: "file:///{path}", + name: "template_with_meta", + meta: meta, + ) + + assert_equal meta, resource_template.to_h[:_meta] + end end end diff --git a/test/mcp/resource_test.rb b/test/mcp/resource_test.rb index 9e780120..fb2254d4 100644 --- a/test/mcp/resource_test.rb +++ b/test/mcp/resource_test.rb @@ -36,5 +36,18 @@ class ResourceTest < ActiveSupport::TestCase assert_equal expected_icons, resource.to_h[:icons] end + + test "#to_h omits _meta when nil" do + resource = Resource.new(uri: "file:///test.txt", name: "resource_without_meta") + + refute resource.to_h.key?(:_meta) + end + + test "#to_h includes _meta when present" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + resource = Resource.new(uri: "file:///test.txt", name: "resource_with_meta", meta: meta) + + assert_equal meta, resource.to_h[:_meta] + end end end diff --git a/test/mcp/tool/response_test.rb b/test/mcp/tool/response_test.rb index 6277e18b..122dbb07 100644 --- a/test/mcp/tool/response_test.rb +++ b/test/mcp/tool/response_test.rb @@ -122,6 +122,20 @@ class ResponseTest < ActiveSupport::TestCase assert_equal structured_content, actual[:structuredContent] refute actual[:isError] end + + test "#to_h includes _meta when present" do + meta = { "application/vnd.ant.mcp-app" => { "csp" => "default-src 'self'" } } + response = Response.new([{ type: "text", text: "ok" }], meta: meta) + actual = response.to_h + + assert_equal meta, actual[:_meta] + end + + test "#to_h omits _meta when nil" do + response = Response.new([{ type: "text", text: "ok" }]) + + refute response.to_h.key?(:_meta) + end end end end