APM integrations need to tag a span with the subject of the request, but the
user-defined `server_context` hash passed to `Server.new` never reaches
`around_request` or `instrumentation_callback`. The exception reporter's
`server_context` argument is a different value — it describes where a failure
occurred (`{ request: ... }`, `{ notification: ... }`), not who made the call —
so there is currently no way to read the request subject from a hook.
Add `Configuration#instrument_server_context`, off by default. When enabled,
`instrument_call` puts the host's `server_context` into the instrumentation data
before invoking `around_request`, so it is readable both before and after
`request_handler.call`.
The flag is opt-in rather than unconditional because the hash is
application-supplied and may hold values a tracing backend should not receive.
Hosts that do not expose a `server_context` reader are unaffected.
`instrument_call` reads it through `self.server_context`: the existing
`server_context:` keyword shadows the reader, and the value wanted here is the
user-defined hash, not the reporter context.
Follows the same shape as the earlier additions of `tool_arguments` (#218) and
`client` (#221) to this hash.
279 lines
10 KiB
Ruby
279 lines
10 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "test_helper"
|
|
|
|
module MCP
|
|
class ConfigurationTest < ActiveSupport::TestCase
|
|
test "initializes with a default no-op exception reporter" do
|
|
config = Configuration.new
|
|
assert_respond_to config, :exception_reporter
|
|
|
|
# The default reporter should be callable but do nothing
|
|
exception = StandardError.new("test error")
|
|
server_context = { test: "context" }
|
|
|
|
assert_nothing_raised do
|
|
config.exception_reporter.call(exception, server_context)
|
|
end
|
|
end
|
|
|
|
test "allows setting a custom exception reporter" do
|
|
config = Configuration.new
|
|
reported_exception = nil
|
|
reported_context = nil
|
|
|
|
config.exception_reporter = ->(exception, server_context) do
|
|
reported_exception = exception
|
|
reported_context = server_context
|
|
end
|
|
|
|
test_exception = StandardError.new("test error")
|
|
test_context = { foo: "bar" }
|
|
|
|
config.exception_reporter.call(test_exception, test_context)
|
|
|
|
assert_equal test_exception, reported_exception
|
|
assert_equal test_context, reported_context
|
|
end
|
|
|
|
# https://github.com/modelcontextprotocol/modelcontextprotocol/blob/14ec41c/schema/draft/schema.ts#L15
|
|
test "initializes with default protocol version" do
|
|
config = Configuration.new
|
|
assert_equal Configuration::LATEST_STABLE_PROTOCOL_VERSION, config.protocol_version
|
|
end
|
|
|
|
test "uses the draft protocol version when protocol_version is set to nil" do
|
|
config = Configuration.new(protocol_version: nil)
|
|
assert_equal Configuration::LATEST_STABLE_PROTOCOL_VERSION, config.protocol_version
|
|
end
|
|
|
|
test "raises ArgumentError when setting the draft protocol version" do
|
|
exception = assert_raises(ArgumentError) do
|
|
# DRAFT-2025-v3 is the latest draft protocol version:
|
|
# https://github.com/modelcontextprotocol/modelcontextprotocol/blob/14ec41c/schema/draft/schema.ts#L15
|
|
Configuration.new(protocol_version: "DRAFT-2025-v3")
|
|
end
|
|
|
|
assert_equal("protocol_version must be 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
|
|
end
|
|
|
|
test "raises ArgumentError when protocol_version is not a supported protocol version" do
|
|
config = Configuration.new
|
|
exception = assert_raises(ArgumentError) do
|
|
custom_version = "2025-03-27"
|
|
config.protocol_version = custom_version
|
|
end
|
|
assert_equal("protocol_version must be 2026-07-28, 2025-11-25, 2025-06-18, 2025-03-26, or 2024-11-05", exception.message)
|
|
end
|
|
|
|
test "exposes the SEP-2575 modern protocol versions" do
|
|
assert_equal "2026-07-28", Configuration::LATEST_MODERN_PROTOCOL_VERSION
|
|
assert_equal ["2026-07-28"], Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS
|
|
assert Configuration.modern_protocol_version?("2026-07-28")
|
|
refute Configuration.modern_protocol_version?("2025-11-25")
|
|
end
|
|
|
|
test "accepts 2026-07-28 as the protocol version" do
|
|
# 2026-07-28 serves both lifecycles of the dual-era model (SEP-2575), so it is negotiable through
|
|
# the legacy `initialize` handshake and settable as the fallback version.
|
|
config = Configuration.new(protocol_version: Configuration::LATEST_MODERN_PROTOCOL_VERSION)
|
|
assert_equal "2026-07-28", config.protocol_version
|
|
end
|
|
|
|
test "raises ArgumentError when protocol_version is not a boolean value" do
|
|
config = Configuration.new
|
|
exception = assert_raises(ArgumentError) do
|
|
config.validate_tool_call_arguments = "true"
|
|
end
|
|
assert_equal("validate_tool_call_arguments must be a boolean", exception.message)
|
|
end
|
|
|
|
test "raises ArgumentError when validate_tool_call_results is not a boolean value" do
|
|
config = Configuration.new
|
|
exception = assert_raises(ArgumentError) do
|
|
config.validate_tool_call_results = "true"
|
|
end
|
|
assert_equal("validate_tool_call_results must be a boolean", exception.message)
|
|
end
|
|
|
|
test "merges protocol version from other configuration" do
|
|
config1 = Configuration.new(protocol_version: "2025-03-26")
|
|
config2 = Configuration.new(protocol_version: "2025-06-18")
|
|
config3 = Configuration.new
|
|
|
|
merged = config1.merge(config2)
|
|
assert_equal "2025-06-18", merged.protocol_version
|
|
|
|
merged = config1.merge(config3)
|
|
assert_equal "2025-03-26", merged.protocol_version
|
|
|
|
merged = config3.merge(config1)
|
|
assert_equal "2025-03-26", merged.protocol_version
|
|
end
|
|
|
|
test "defaults validate_tool_call_arguments to true" do
|
|
config = Configuration.new
|
|
assert config.validate_tool_call_arguments
|
|
end
|
|
|
|
test "can set validate_tool_call_arguments to false" do
|
|
config = Configuration.new(validate_tool_call_arguments: false)
|
|
refute config.validate_tool_call_arguments
|
|
end
|
|
|
|
test "validate_tool_call_arguments? returns false when set" do
|
|
config = Configuration.new(validate_tool_call_arguments: false)
|
|
refute config.validate_tool_call_arguments?
|
|
end
|
|
|
|
test "validate_tool_call_arguments? returns true when not set" do
|
|
config = Configuration.new
|
|
assert config.validate_tool_call_arguments?
|
|
end
|
|
|
|
test "merge preserves validate_tool_call_arguments from other config" do
|
|
config1 = Configuration.new(validate_tool_call_arguments: false)
|
|
config2 = Configuration.new
|
|
merged = config1.merge(config2)
|
|
assert merged.validate_tool_call_arguments?
|
|
end
|
|
|
|
test "merge preserves validate_tool_call_arguments from self when other not set" do
|
|
config1 = Configuration.new(validate_tool_call_arguments: false)
|
|
config2 = Configuration.new
|
|
merged = config2.merge(config1)
|
|
refute merged.validate_tool_call_arguments
|
|
end
|
|
|
|
test "defaults instrument_server_context to false" do
|
|
config = Configuration.new
|
|
|
|
refute_predicate config, :instrument_server_context?
|
|
end
|
|
|
|
test "accepts instrument_server_context in the constructor" do
|
|
config = Configuration.new(instrument_server_context: true)
|
|
|
|
assert_predicate config, :instrument_server_context?
|
|
end
|
|
|
|
test "raises ArgumentError when instrument_server_context is not a boolean value" do
|
|
config = Configuration.new
|
|
|
|
exception = assert_raises(ArgumentError) do
|
|
config.instrument_server_context = "true"
|
|
end
|
|
assert_equal("instrument_server_context must be a boolean", exception.message)
|
|
end
|
|
|
|
test "merge carries instrument_server_context from the other configuration" do
|
|
base = Configuration.new(instrument_server_context: true)
|
|
other = Configuration.new(instrument_server_context: false)
|
|
|
|
refute_predicate base.merge(other), :instrument_server_context?
|
|
assert_predicate other.merge(base), :instrument_server_context?
|
|
end
|
|
|
|
test "defaults validate_tool_call_results to false" do
|
|
config = Configuration.new
|
|
refute config.validate_tool_call_results
|
|
end
|
|
|
|
test "can set validate_tool_call_results to true" do
|
|
config = Configuration.new(validate_tool_call_results: true)
|
|
assert config.validate_tool_call_results
|
|
end
|
|
|
|
test "validate_tool_call_results? returns false when not set" do
|
|
config = Configuration.new
|
|
refute config.validate_tool_call_results?
|
|
end
|
|
|
|
test "validate_tool_call_results? returns true when set" do
|
|
config = Configuration.new(validate_tool_call_results: true)
|
|
assert config.validate_tool_call_results?
|
|
end
|
|
|
|
test "merge preserves validate_tool_call_results from other config" do
|
|
config1 = Configuration.new(validate_tool_call_results: true)
|
|
config2 = Configuration.new
|
|
merged = config1.merge(config2)
|
|
refute merged.validate_tool_call_results?
|
|
end
|
|
|
|
test "merge preserves validate_tool_call_results from self when other set" do
|
|
config1 = Configuration.new(validate_tool_call_results: true)
|
|
config2 = Configuration.new
|
|
merged = config2.merge(config1)
|
|
assert merged.validate_tool_call_results
|
|
end
|
|
|
|
test "initializes with a default pass-through around_request" do
|
|
config = Configuration.new
|
|
called = false
|
|
config.around_request.call({}) { called = true }
|
|
assert called
|
|
end
|
|
|
|
test "allows setting a custom around_request" do
|
|
config = Configuration.new
|
|
call_log = []
|
|
config.around_request = ->(_data, &request_handler) {
|
|
call_log << :before
|
|
request_handler.call
|
|
call_log << :after
|
|
}
|
|
|
|
config.around_request.call({}) { call_log << :execute }
|
|
assert_equal([:before, :execute, :after], call_log)
|
|
end
|
|
|
|
test "around_request? returns false by default" do
|
|
config = Configuration.new
|
|
refute config.around_request?
|
|
end
|
|
|
|
test "around_request? returns true when set" do
|
|
config = Configuration.new
|
|
config.around_request = ->(_data, &request_handler) { request_handler.call }
|
|
assert config.around_request?
|
|
end
|
|
|
|
test "merge preserves around_request from other config" do
|
|
custom = ->(_data, &request_handler) { request_handler.call }
|
|
config1 = Configuration.new
|
|
config2 = Configuration.new(around_request: custom)
|
|
merged = config1.merge(config2)
|
|
assert_equal custom, merged.around_request
|
|
end
|
|
|
|
test "merge preserves around_request from self when other not set" do
|
|
custom = ->(_data, &request_handler) { request_handler.call }
|
|
config1 = Configuration.new(around_request: custom)
|
|
config2 = Configuration.new
|
|
merged = config1.merge(config2)
|
|
assert_equal custom, merged.around_request
|
|
end
|
|
|
|
test "raises ArgumentError when protocol_version is not a supported value" do
|
|
exception = assert_raises(ArgumentError) do
|
|
Configuration.new(protocol_version: "1999-12-31")
|
|
end
|
|
assert_match(/\Aprotocol_version must be/, exception.message)
|
|
end
|
|
|
|
test "raises ArgumentError when validate_tool_call_arguments is not a boolean" do
|
|
exception = assert_raises(ArgumentError) do
|
|
Configuration.new(validate_tool_call_arguments: "true")
|
|
end
|
|
assert_equal("validate_tool_call_arguments must be a boolean", exception.message)
|
|
end
|
|
|
|
test "raises ArgumentError when validate_tool_call_results is not a boolean" do
|
|
exception = assert_raises(ArgumentError) do
|
|
Configuration.new(validate_tool_call_results: "true")
|
|
end
|
|
assert_equal("validate_tool_call_results must be a boolean", exception.message)
|
|
end
|
|
end
|
|
end
|