diff --git a/README.md b/README.md index c97362d..920471c 100644 --- a/README.md +++ b/README.md @@ -329,6 +329,13 @@ class TestToolWithStructuredContentResponse < ModelContextProtocol::Server::Tool required: ["temperature", "conditions", "humidity"] } end + # Optional security requirements for the tool + security_schemes do + [ + {type: "noauth"}, + {type: "oauth2", scopes: ["search.read"]} + ] + end end def call @@ -362,6 +369,7 @@ end Key features: - Define input and output JSON schemas +- Declare tool security schemes (e.g., noauth, oauth2 scopes) - Return text, image, audio, or embedded resource content - Support for structured content responses - Cancellable and progressable operations diff --git a/lib/model_context_protocol/server/tool.rb b/lib/model_context_protocol/server/tool.rb index 383e6b2..31b16fc 100644 --- a/lib/model_context_protocol/server/tool.rb +++ b/lib/model_context_protocol/server/tool.rb @@ -83,7 +83,7 @@ def serialized end class << self - attr_reader :name, :description, :title, :input_schema, :output_schema, :annotations + attr_reader :name, :description, :title, :input_schema, :output_schema, :annotations, :security_schemes def define(&block) definition_dsl = DefinitionDSL.new @@ -95,6 +95,7 @@ def define(&block) @input_schema = definition_dsl.input_schema @output_schema = definition_dsl.output_schema @annotations = definition_dsl.annotations + @security_schemes = definition_dsl.security_schemes end def inherited(subclass) @@ -104,6 +105,7 @@ def inherited(subclass) subclass.instance_variable_set(:@input_schema, @input_schema) subclass.instance_variable_set(:@output_schema, @output_schema) subclass.instance_variable_set(:@annotations, @annotations) + subclass.instance_variable_set(:@security_schemes, @security_schemes) end def call(arguments, client_logger, server_logger, context = {}) @@ -123,6 +125,7 @@ def definition result[:title] = @title if @title result[:outputSchema] = @output_schema if @output_schema result[:annotations] = @annotations if @annotations + result[:securitySchemes] = @security_schemes if @security_schemes result end end @@ -157,6 +160,11 @@ def annotations(&block) @annotations = instance_eval(&block) if block_given? @annotations end + + def security_schemes(&block) + @security_schemes = instance_eval(&block) if block_given? + @security_schemes + end end end end diff --git a/spec/lib/model_context_protocol/server/tool_spec.rb b/spec/lib/model_context_protocol/server/tool_spec.rb index f7f8898..0405188 100644 --- a/spec/lib/model_context_protocol/server/tool_spec.rb +++ b/spec/lib/model_context_protocol/server/tool_spec.rb @@ -335,6 +335,53 @@ child_tool = Class.new(parent_tool) expect(child_tool.annotations).to eq({readOnlyHint: true}) end + + it "sets security schemes when provided" do + tool_with_security_schemes = Class.new(ModelContextProtocol::Server::Tool) do + define do + name "search" + description "Search indexed documents" + input_schema { {type: "object", properties: {}, required: []} } + security_schemes do + [ + {type: "noauth"}, + {type: "oauth2", scopes: ["search.read"]} + ] + end + end + end + + expect(tool_with_security_schemes.security_schemes).to eq( + [ + {type: "noauth"}, + {type: "oauth2", scopes: ["search.read"]} + ] + ) + end + + it "inherits security schemes in subclasses" do + parent_tool = Class.new(ModelContextProtocol::Server::Tool) do + define do + name "search" + description "Search indexed documents" + input_schema { {type: "object", properties: {}, required: []} } + security_schemes do + [ + {type: "noauth"}, + {type: "oauth2", scopes: ["search.read"]} + ] + end + end + end + + child_tool = Class.new(parent_tool) + expect(child_tool.security_schemes).to eq( + [ + {type: "noauth"}, + {type: "oauth2", scopes: ["search.read"]} + ] + ) + end end describe "definition" do @@ -430,6 +477,52 @@ annotations: {readOnlyHint: true} ) end + + it "includes security schemes when provided" do + tool_with_security_schemes = Class.new(ModelContextProtocol::Server::Tool) do + define do + name "search" + description "Search indexed documents" + input_schema do + { + type: "object", + properties: { + q: { + type: "string", + description: "Search query" + } + }, + required: ["q"] + } + end + security_schemes do + [ + {type: "noauth"}, + {type: "oauth2", scopes: ["search.read"]} + ] + end + end + end + + expect(tool_with_security_schemes.definition).to eq( + name: "search", + description: "Search indexed documents", + inputSchema: { + type: "object", + properties: { + q: { + type: "string", + description: "Search query" + } + }, + required: ["q"] + }, + securitySchemes: [ + {type: "noauth"}, + {type: "oauth2", scopes: ["search.read"]} + ] + ) + end end describe "optional title field" do @@ -458,6 +551,11 @@ def call metadata = tool_without_title.definition expect(metadata).not_to have_key(:annotations) end + + it "does not include security schemes in definition when not provided" do + metadata = tool_without_title.definition + expect(metadata).not_to have_key(:securitySchemes) + end end describe "server logger integration" do