Add audience role validation for MCP::Annotations per MCP specification
The MCP specification constrains `Annotations.audience` to an array of `Role` values (`"user"` or `"assistant"`), but the Ruby SDK accepts any value. The other official SDKs already enforce this (TypeScript, Python, and PHP). This follows the existing pattern in `MCP::Icon`, which validates its constrained `theme` value in the constructor and raises `ArgumentError`. Added tests in `test/mcp/annotations_test.rb`: - valid `audience` (`["user"]`, `["assistant"]`, `["user", "assistant"]`, and an empty array) is accepted - an unknown role, a mixed valid/invalid array, or a non-array `audience` raises `ArgumentError` Existing tests used `["developers"]`, which is not a valid `Role`, and are updated to `["user"]`. The full unit suite and RuboCop pass locally. Strictly speaking this is a breaking change: a caller that currently passes an invalid `audience` (e.g. `["developers"]`) will now get an `ArgumentError`. Such values already violate the MCP specification, and `nil` / valid-role values are unaffected.
This commit is contained in:
parent
dc26613a07
commit
4c3268ccb1
2 changed files with 59 additions and 9 deletions
|
|
@ -2,9 +2,14 @@
|
||||||
|
|
||||||
module MCP
|
module MCP
|
||||||
class Annotations
|
class Annotations
|
||||||
|
SUPPORTED_AUDIENCES = ["user", "assistant"].freeze
|
||||||
|
|
||||||
attr_reader :audience, :priority, :last_modified
|
attr_reader :audience, :priority, :last_modified
|
||||||
|
|
||||||
def initialize(audience: nil, priority: nil, last_modified: nil)
|
def initialize(audience: nil, priority: nil, last_modified: nil)
|
||||||
|
if audience && !(audience.is_a?(Array) && audience.all? { |role| SUPPORTED_AUDIENCES.include?(role) })
|
||||||
|
raise ArgumentError, 'The value of audience must be an array of "user" or "assistant".'
|
||||||
|
end
|
||||||
raise ArgumentError, "The value of priority must be between 0 and 1." if priority && !priority.between?(0, 1)
|
raise ArgumentError, "The value of priority must be between 0 and 1." if priority && !priority.between?(0, 1)
|
||||||
|
|
||||||
@audience = audience
|
@audience = audience
|
||||||
|
|
|
||||||
|
|
@ -5,24 +5,24 @@ require "test_helper"
|
||||||
module MCP
|
module MCP
|
||||||
class AnnotationsTest < ActiveSupport::TestCase
|
class AnnotationsTest < ActiveSupport::TestCase
|
||||||
def test_initialization
|
def test_initialization
|
||||||
annotations = Annotations.new(audience: ["developers"], priority: 0.8)
|
annotations = Annotations.new(audience: ["user"], priority: 0.8)
|
||||||
|
|
||||||
assert_equal(["developers"], annotations.audience)
|
assert_equal(["user"], annotations.audience)
|
||||||
assert_equal(0.8, annotations.priority)
|
assert_equal(0.8, annotations.priority)
|
||||||
assert_nil(annotations.last_modified)
|
assert_nil(annotations.last_modified)
|
||||||
|
|
||||||
assert_equal({ audience: ["developers"], priority: 0.8 }, annotations.to_h)
|
assert_equal({ audience: ["user"], priority: 0.8 }, annotations.to_h)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_initialization_with_all_attributes
|
def test_initialization_with_all_attributes
|
||||||
timestamp = Time.utc(2025, 1, 12, 15, 0, 58).iso8601
|
timestamp = Time.utc(2025, 1, 12, 15, 0, 58).iso8601
|
||||||
annotations = Annotations.new(audience: ["developers"], priority: 0.8, last_modified: timestamp)
|
annotations = Annotations.new(audience: ["user"], priority: 0.8, last_modified: timestamp)
|
||||||
|
|
||||||
assert_equal(["developers"], annotations.audience)
|
assert_equal(["user"], annotations.audience)
|
||||||
assert_equal(0.8, annotations.priority)
|
assert_equal(0.8, annotations.priority)
|
||||||
assert_equal(timestamp, annotations.last_modified)
|
assert_equal(timestamp, annotations.last_modified)
|
||||||
|
|
||||||
assert_equal({ audience: ["developers"], priority: 0.8, lastModified: timestamp }, annotations.to_h)
|
assert_equal({ audience: ["user"], priority: 0.8, lastModified: timestamp }, annotations.to_h)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_initialization_by_default
|
def test_initialization_by_default
|
||||||
|
|
@ -36,13 +36,13 @@ module MCP
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_initialization_with_partial_attributes
|
def test_initialization_with_partial_attributes
|
||||||
annotations = Annotations.new(audience: ["developers"])
|
annotations = Annotations.new(audience: ["user"])
|
||||||
|
|
||||||
assert_equal(["developers"], annotations.audience)
|
assert_equal(["user"], annotations.audience)
|
||||||
assert_nil(annotations.priority)
|
assert_nil(annotations.priority)
|
||||||
assert_nil(annotations.last_modified)
|
assert_nil(annotations.last_modified)
|
||||||
|
|
||||||
assert_equal({ audience: ["developers"] }, annotations.to_h)
|
assert_equal({ audience: ["user"] }, annotations.to_h)
|
||||||
end
|
end
|
||||||
|
|
||||||
def test_initialization_with_last_modified_only
|
def test_initialization_with_last_modified_only
|
||||||
|
|
@ -81,5 +81,50 @@ module MCP
|
||||||
end
|
end
|
||||||
assert_equal("The value of priority must be between 0 and 1.", exception.message)
|
assert_equal("The value of priority must be between 0 and 1.", exception.message)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_valid_audience_with_user
|
||||||
|
assert_nothing_raised do
|
||||||
|
Annotations.new(audience: ["user"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_valid_audience_with_assistant
|
||||||
|
assert_nothing_raised do
|
||||||
|
Annotations.new(audience: ["assistant"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_valid_audience_with_multiple_roles
|
||||||
|
assert_nothing_raised do
|
||||||
|
Annotations.new(audience: ["user", "assistant"])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_valid_audience_with_empty_array
|
||||||
|
assert_nothing_raised do
|
||||||
|
Annotations.new(audience: [])
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_invalid_audience_with_unknown_role
|
||||||
|
exception = assert_raises(ArgumentError) do
|
||||||
|
Annotations.new(audience: ["developers"])
|
||||||
|
end
|
||||||
|
assert_equal('The value of audience must be an array of "user" or "assistant".', exception.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_invalid_audience_with_mixed_roles
|
||||||
|
exception = assert_raises(ArgumentError) do
|
||||||
|
Annotations.new(audience: ["user", "developers"])
|
||||||
|
end
|
||||||
|
assert_equal('The value of audience must be an array of "user" or "assistant".', exception.message)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_invalid_audience_when_not_array
|
||||||
|
exception = assert_raises(ArgumentError) do
|
||||||
|
Annotations.new(audience: "user")
|
||||||
|
end
|
||||||
|
assert_equal('The value of audience must be an array of "user" or "assistant".', exception.message)
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue