Skip to content
Open
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
1 change: 1 addition & 0 deletions lib/mcp.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
require_relative "json_rpc_handler"
require_relative "mcp/configuration"
require_relative "mcp/content"
require_relative "mcp/icon"
require_relative "mcp/instrumentation"
require_relative "mcp/methods"
require_relative "mcp/prompt"
Expand Down
22 changes: 22 additions & 0 deletions lib/mcp/icon.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

module MCP
class Icon
SUPPORTED_THEMES = ["light", "dark"]

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

def initialize(mime_type: nil, sizes: nil, src: nil, theme: nil)
raise ArgumentError, 'The value of theme must specify "light" or "dark".' if theme && !SUPPORTED_THEMES.include?(theme)

@mime_type = mime_type
@sizes = sizes
@src = src
@theme = theme
end

def to_h
{ mimeType: mime_type, sizes: sizes, src: src, theme: theme }.compact
end
end
end
14 changes: 13 additions & 1 deletion lib/mcp/prompt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class << self

attr_reader :title_value
attr_reader :description_value
attr_reader :icons_value
attr_reader :arguments_value
attr_reader :meta_value

Expand All @@ -19,6 +20,7 @@ def to_h
name: name_value,
title: title_value,
description: description_value,
icons: icons&.map(&:to_h),
arguments: arguments_value&.map(&:to_h),
_meta: meta_value,
}.compact
Expand All @@ -29,6 +31,7 @@ def inherited(subclass)
subclass.instance_variable_set(:@name_value, nil)
subclass.instance_variable_set(:@title_value, nil)
subclass.instance_variable_set(:@description_value, nil)
subclass.instance_variable_set(:@icons_value, nil)
subclass.instance_variable_set(:@arguments_value, nil)
subclass.instance_variable_set(:@meta_value, nil)
end
Expand Down Expand Up @@ -61,6 +64,14 @@ def description(value = NOT_SET)
end
end

def icons(value = NOT_SET)
if value == NOT_SET
@icons_value
else
@icons_value = value
end
end

def arguments(value = NOT_SET)
if value == NOT_SET
@arguments_value
Expand All @@ -77,11 +88,12 @@ def meta(value = NOT_SET)
end
end

def define(name: nil, title: nil, description: nil, arguments: [], meta: nil, &block)
def define(name: nil, title: nil, description: nil, icons: [], arguments: [], meta: nil, &block)
Class.new(self) do
prompt_name name
title title
description description
icons icons
arguments arguments
define_singleton_method(:template) do |args, server_context: nil|
instance_exec(args, server_context:, &block)
Expand Down
6 changes: 4 additions & 2 deletions lib/mcp/resource.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

module MCP
class Resource
attr_reader :uri, :name, :title, :description, :mime_type
attr_reader :uri, :name, :title, :description, :icons, :mime_type

def initialize(uri:, name:, title: nil, description: nil, mime_type: nil)
def initialize(uri:, name:, title: nil, description: nil, icons: [], mime_type: nil)
@uri = uri
@name = name
@title = title
@description = description
@icons = icons
@mime_type = mime_type
end

Expand All @@ -18,6 +19,7 @@ def to_h
name: name,
title: title,
description: description,
icons: icons.map(&:to_h),
mimeType: mime_type,
}.compact
end
Expand Down
6 changes: 4 additions & 2 deletions lib/mcp/resource_template.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@

module MCP
class ResourceTemplate
attr_reader :uri_template, :name, :title, :description, :mime_type
attr_reader :uri_template, :name, :title, :description, :icons, :mime_type

def initialize(uri_template:, name:, title: nil, description: nil, mime_type: nil)
def initialize(uri_template:, name:, title: nil, description: nil, icons: [], mime_type: nil)
@uri_template = uri_template
@name = name
@title = title
@description = description
@icons = icons
@mime_type = mime_type
end

Expand All @@ -18,6 +19,7 @@ def to_h
name: name,
title: title,
description: description,
icons: icons.map(&:to_h),
mimeType: mime_type,
}.compact
end
Expand Down
5 changes: 4 additions & 1 deletion lib/mcp/server.rb
Original file line number Diff line number Diff line change
Expand Up @@ -40,10 +40,11 @@ def initialize(method_name)

include Instrumentation

attr_accessor :description, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport
attr_accessor :description, :icons, :name, :title, :version, :website_url, :instructions, :tools, :prompts, :resources, :server_context, :configuration, :capabilities, :transport

def initialize(
description: nil,
icons: [],
name: "model_context_protocol",
title: nil,
version: DEFAULT_VERSION,
Expand All @@ -59,6 +60,7 @@ def initialize(
transport: nil
)
@description = description
@icons = icons
@name = name
@title = title
@version = version
Expand Down Expand Up @@ -288,6 +290,7 @@ def default_capabilities
def server_info
@server_info ||= {
description:,
icons:,
name:,
title:,
version:,
Expand Down
14 changes: 13 additions & 1 deletion lib/mcp/tool.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class << self

attr_reader :title_value
attr_reader :description_value
attr_reader :icons_value
attr_reader :annotations_value
attr_reader :meta_value

Expand All @@ -19,6 +20,7 @@ def to_h
name: name_value,
title: title_value,
description: description_value,
icons: icons&.map(&:to_h),
inputSchema: input_schema_value.to_h,
outputSchema: @output_schema_value&.to_h,
annotations: annotations_value&.to_h,
Expand All @@ -31,6 +33,7 @@ def inherited(subclass)
subclass.instance_variable_set(:@name_value, nil)
subclass.instance_variable_set(:@title_value, nil)
subclass.instance_variable_set(:@description_value, nil)
subclass.instance_variable_set(:@icons_value, nil)
subclass.instance_variable_set(:@input_schema_value, nil)
subclass.instance_variable_set(:@output_schema_value, nil)
subclass.instance_variable_set(:@annotations_value, nil)
Expand Down Expand Up @@ -71,6 +74,14 @@ def description(value = NOT_SET)
end
end

def icons(value = NOT_SET)
if value == NOT_SET
@icons_value
else
@icons_value = value
end
end

def input_schema(value = NOT_SET)
if value == NOT_SET
input_schema_value
Expand Down Expand Up @@ -107,11 +118,12 @@ def annotations(hash = NOT_SET)
end
end

def define(name: nil, title: nil, description: nil, input_schema: nil, output_schema: nil, meta: nil, annotations: nil, &block)
def define(name: nil, title: nil, description: nil, icons: [], input_schema: nil, output_schema: nil, meta: nil, annotations: nil, &block)
Class.new(self) do
tool_name name
title title
description description
icons icons
input_schema input_schema
meta meta
output_schema output_schema
Expand Down
48 changes: 48 additions & 0 deletions test/mcp/icon_test.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

require "test_helper"

module MCP
class IconTest < ActiveSupport::TestCase
def test_initialization
icon = Icon.new(mime_type: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light")

assert_equal("image/png", icon.mime_type)
assert_equal(["48x48", "96x96"], icon.sizes)
assert_equal("https://example.com", icon.src)
assert_equal("light", icon.theme)

assert_equal({ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }, icon.to_h)
end

def test_initialization_by_default
icon = Icon.new

assert_nil(icon.mime_type)
assert_nil(icon.sizes)
assert_nil(icon.src)
assert_nil(icon.theme)

assert_empty(icon.to_h)
end

def test_valid_theme_for_light
assert_nothing_raised do
Icon.new(theme: "light")
end
end

def test_valid_theme_for_dark
assert_nothing_raised do
Icon.new(theme: "dark")
end
end

def test_invalid_theme
exception = assert_raises(ArgumentError) do
Icon.new(theme: "unexpected")
end
assert_equal('The value of theme must specify "light" or "dark".', exception.message)
end
end
end
5 changes: 5 additions & 0 deletions test/mcp/prompt_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ module MCP
class PromptTest < ActiveSupport::TestCase
class TestPrompt < Prompt
description "Test prompt"
icons [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }]
arguments [
Prompt::Argument.new(name: "test_argument", description: "Test argument", required: true),
]
Expand Down Expand Up @@ -43,6 +44,7 @@ def template(args, server_context:)
class MockPrompt < Prompt
prompt_name "my_mock_prompt"
description "a mock prompt for testing"
icons [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }]
arguments [
Prompt::Argument.new(name: "test_argument", description: "Test argument", required: true),
]
Expand All @@ -64,6 +66,7 @@ def template(args, server_context:)

assert_equal "my_mock_prompt", prompt.name_value
assert_equal "a mock prompt for testing", prompt.description
assert_equal([{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }], prompt.icons)
assert_equal "test_argument", prompt.arguments.first.name
assert_equal "Test argument", prompt.arguments.first.description
assert prompt.arguments.first.required
Expand Down Expand Up @@ -112,6 +115,7 @@ def template(args, server_context:)
name: "mock_prompt",
title: "Mock Prompt",
description: "a mock prompt for testing",
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
arguments: [
Prompt::Argument.new(
name: "test_argument",
Expand All @@ -135,6 +139,7 @@ def template(args, server_context:)

assert_equal "mock_prompt", prompt.name_value
assert_equal "a mock prompt for testing", prompt.description
assert_equal([{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }], prompt.icons)
assert_equal "test_argument", prompt.arguments.first.name
assert_equal "Test argument title", prompt.arguments.first.title
assert_equal "This is a test argument description", prompt.arguments.first.description
Expand Down
4 changes: 4 additions & 0 deletions test/mcp/server_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class ServerTest < ActiveSupport::TestCase
name: "test-resource",
title: "Test Resource",
description: "Test resource",
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
mime_type: "text/plain",
)

Expand All @@ -60,6 +61,7 @@ class ServerTest < ActiveSupport::TestCase
name: "test-resource",
title: "Test Resource",
description: "Test resource",
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
mime_type: "text/plain",
)

Expand All @@ -69,6 +71,7 @@ class ServerTest < ActiveSupport::TestCase

@server = Server.new(
description: "Test server",
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
name: @server_name,
title: "Example Server Display Name",
version: "1.2.3",
Expand Down Expand Up @@ -142,6 +145,7 @@ class ServerTest < ActiveSupport::TestCase
},
serverInfo: {
description: "Test server",
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
name: @server_name,
title: "Example Server Display Name",
version: "1.2.3",
Expand Down
29 changes: 21 additions & 8 deletions test/mcp/tool_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ class ToolTest < ActiveSupport::TestCase
class TestTool < Tool
tool_name "test_tool"
description "a test tool for testing"
icons [Icon.new(mime_type: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light")]
input_schema({ properties: { message: { type: "string" } }, required: ["message"] })
annotations(
destructive_hint: false,
Expand All @@ -26,13 +27,22 @@ def call(message:, server_context: nil)
end
end

test "#to_h returns a hash with name, description, and inputSchema" do
test "#to_h returns a hash including name, description, icons, and inputSchema" do
expected = {
name: "mock_tool",
title: "Mock Tool",
description: "a mock tool for testing",
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
inputSchema: { type: "object" },
}
tool = Tool.define(
name: "mock_tool",
title: "Mock Tool",
description: "a mock tool for testing",
icons: [Icon.new(mime_type: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light")],
)
assert_equal({ name: "mock_tool", title: "Mock Tool", description: "a mock tool for testing", inputSchema: { type: "object" } }, tool.to_h)

assert_equal(expected, tool.to_h)
end

test "#to_h does not have `:title` key when title is omitted" do
Expand Down Expand Up @@ -308,19 +318,22 @@ def call(message, server_context: nil)
end

test "#to_h includes outputSchema when present" do
tool = Tool.define(
name: "mock_tool",
title: "Mock Tool",
description: "a mock tool for testing",
output_schema: { properties: { result: { type: "string" } }, required: ["result"] },
)
expected = {
name: "mock_tool",
title: "Mock Tool",
description: "a mock tool for testing",
icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
inputSchema: { type: "object" },
outputSchema: { type: "object", properties: { result: { type: "string" } }, required: ["result"] },
}
tool = Tool.define(
name: "mock_tool",
title: "Mock Tool",
description: "a mock tool for testing",
icons: [Icon.new(mime_type: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light")],
output_schema: { properties: { result: { type: "string" } }, required: ["result"] },
)

assert_equal expected, tool.to_h
end

Expand Down