Skip to content

Extend modules instead of inheriting classes #48

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 1 commit into from
Closed
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
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,9 @@ require "mcp"
require "mcp/server/transports/stdio_transport"

# Create a simple tool
class ExampleTool < MCP::Tool
module ExampleTool
extend MCP::Tool

description "A simple example tool that echoes back its arguments"
input_schema(
properties: {
Expand Down Expand Up @@ -275,7 +277,9 @@ This gem provides a `MCP::Tool` class that can be used to create tools in two wa
1. As a class definition:

```ruby
class MyTool < MCP::Tool
module MyTool
extend MCP::Tool

description "This tool performs specific functionality..."
input_schema(
properties: {
Expand Down Expand Up @@ -338,7 +342,9 @@ The `MCP::Prompt` class provides two ways to create prompts:
1. As a class definition with metadata:

```ruby
class MyPrompt < MCP::Prompt
module MyPrompt
extend MCP::Prompt

prompt_name "my_prompt" # Optional - defaults to underscored class name
description "This prompt performs specific functionality..."
arguments [
Expand Down
117 changes: 59 additions & 58 deletions lib/mcp/prompt.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,16 @@
# frozen_string_literal: true

module MCP
class Prompt
class << self
NOT_SET = Object.new

attr_reader :description_value
attr_reader :arguments_value

def template(args, server_context:)
raise NotImplementedError, "Subclasses must implement template"
end

def to_h
{ name: name_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
end

def inherited(subclass)
super
subclass.instance_variable_set(:@name_value, nil)
subclass.instance_variable_set(:@description_value, nil)
subclass.instance_variable_set(:@arguments_value, nil)
end

def prompt_name(value = NOT_SET)
if value == NOT_SET
@name_value
else
@name_value = value
end
end
module Prompt
NOT_SET = Object.new

def name_value
@name_value || StringUtils.handle_from_class_name(name)
end

def description(value = NOT_SET)
if value == NOT_SET
@description_value
else
@description_value = value
end
end

def arguments(value = NOT_SET)
if value == NOT_SET
@arguments_value
else
@arguments_value = value
end
end
attr_reader :description_value
attr_reader :arguments_value

class << self
def define(name: nil, description: nil, arguments: [], &block)
Class.new(self) do
Module.new do
extend Prompt
prompt_name name
description description
arguments arguments
Expand All @@ -62,21 +20,64 @@ def define(name: nil, description: nil, arguments: [], &block)
end
end
end
end

def validate_arguments!(args)
missing = required_args - args.keys
return if missing.empty?
def template(args, server_context:)
raise NotImplementedError, "Prompts must implement template"
end

def to_h
{ name: name_value, description: description_value, arguments: arguments_value.map(&:to_h) }.compact
end

raise MCP::Server::RequestHandlerError.new(
"Missing required arguments: #{missing.join(", ")}", nil, error_type: :missing_required_arguments
)
def extended(mod)
super
mod.instance_variable_set(:@name_value, nil)
mod.instance_variable_set(:@description_value, nil)
mod.instance_variable_set(:@arguments_value, nil)
end

def prompt_name(value = NOT_SET)
if value == NOT_SET
@name_value
else
@name_value = value
end
end

def name_value
@name_value || StringUtils.handle_from_class_name(name)
end

private
def description(value = NOT_SET)
if value == NOT_SET
@description_value
else
@description_value = value
end
end

def required_args
arguments_value.filter_map { |arg| arg.name.to_sym if arg.required }
def arguments(value = NOT_SET)
if value == NOT_SET
@arguments_value
else
@arguments_value = value
end
end

def validate_arguments!(args)
missing = required_args - args.keys
return if missing.empty?

raise MCP::Server::RequestHandlerError.new(
"Missing required arguments: #{missing.join(", ")}", nil, error_type: :missing_required_arguments
)
end

private

def required_args
arguments_value.filter_map { |arg| arg.name.to_sym if arg.required }
end
end
end
2 changes: 1 addition & 1 deletion lib/mcp/prompt/argument.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# frozen_string_literal: true

module MCP
class Prompt
module Prompt
class Argument
attr_reader :name, :description, :required, :arguments

Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/prompt/message.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# frozen_string_literal: true

module MCP
class Prompt
module Prompt
class Message
attr_reader :role, :content

Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/prompt/result.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
# frozen_string_literal: true

module MCP
class Prompt
module Prompt
class Result
attr_reader :description, :messages

Expand Down
129 changes: 65 additions & 64 deletions lib/mcp/tool.rb
Original file line number Diff line number Diff line change
@@ -1,84 +1,85 @@
# frozen_string_literal: true

module MCP
class Tool
class << self
NOT_SET = Object.new
module Tool
NOT_SET = Object.new

attr_reader :description_value
attr_reader :input_schema_value
attr_reader :annotations_value
attr_reader :description_value
attr_reader :input_schema_value
attr_reader :annotations_value

def call(*args, server_context:)
raise NotImplementedError, "Subclasses must implement call"
class << self
def define(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
Module.new do
extend Tool
tool_name name
description description
input_schema input_schema
annotations annotations if annotations
define_singleton_method(:call, &block) if block
end
end
end

def to_h
result = {
name: name_value,
description: description_value,
inputSchema: input_schema_value.to_h,
}
result[:annotations] = annotations_value.to_h if annotations_value
result
end
def call(*args, server_context:)
raise NotImplementedError, "Tools must implement call"
end

def inherited(subclass)
super
subclass.instance_variable_set(:@name_value, nil)
subclass.instance_variable_set(:@description_value, nil)
subclass.instance_variable_set(:@input_schema_value, nil)
subclass.instance_variable_set(:@annotations_value, nil)
end
def to_h
result = {
name: name_value,
description: description_value,
inputSchema: input_schema_value.to_h,
}
result[:annotations] = annotations_value.to_h if annotations_value
result
end

def tool_name(value = NOT_SET)
if value == NOT_SET
name_value
else
@name_value = value
end
end
def extended(mod)
super
mod.instance_variable_set(:@name_value, nil)
mod.instance_variable_set(:@description_value, nil)
mod.instance_variable_set(:@input_schema_value, nil)
mod.instance_variable_set(:@annotations_value, nil)
end

def name_value
@name_value || StringUtils.handle_from_class_name(name)
def tool_name(value = NOT_SET)
if value == NOT_SET
name_value
else
@name_value = value
end
end

def description(value = NOT_SET)
if value == NOT_SET
@description_value
else
@description_value = value
end
end
def name_value
@name_value || StringUtils.handle_from_class_name(name)
end

def input_schema(value = NOT_SET)
if value == NOT_SET
input_schema_value
elsif value.is_a?(Hash)
properties = value[:properties] || value["properties"] || {}
required = value[:required] || value["required"] || []
@input_schema_value = InputSchema.new(properties:, required:)
elsif value.is_a?(InputSchema)
@input_schema_value = value
end
def description(value = NOT_SET)
if value == NOT_SET
@description_value
else
@description_value = value
end
end

def annotations(hash = NOT_SET)
if hash == NOT_SET
@annotations_value
else
@annotations_value = Annotations.new(**hash)
end
def input_schema(value = NOT_SET)
if value == NOT_SET
input_schema_value
elsif value.is_a?(Hash)
properties = value[:properties] || value["properties"] || {}
required = value[:required] || value["required"] || []
@input_schema_value = InputSchema.new(properties:, required:)
elsif value.is_a?(InputSchema)
@input_schema_value = value
end
end

def define(name: nil, description: nil, input_schema: nil, annotations: nil, &block)
Class.new(self) do
tool_name name
description description
input_schema input_schema
self.annotations(annotations) if annotations
define_singleton_method(:call, &block) if block
end
def annotations(hash = NOT_SET)
if hash == NOT_SET
@annotations_value
else
@annotations_value = Annotations.new(**hash)
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/tool/annotations.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module MCP
class Tool
module Tool
class Annotations
attr_reader :title, :read_only_hint, :destructive_hint, :idempotent_hint, :open_world_hint

Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/tool/input_schema.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module MCP
class Tool
module Tool
class InputSchema
attr_reader :properties, :required

Expand Down
2 changes: 1 addition & 1 deletion lib/mcp/tool/response.rb
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# frozen_string_literal: true

module MCP
class Tool
module Tool
class Response
attr_reader :content, :is_error

Expand Down
Loading