ruby-sdk/lib/mcp/configuration.rb
seunghan Kim 14e02da475 Expose the user-defined server_context in instrumentation data
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.
2026-08-08 08:33:07 +09:00

215 lines
7.6 KiB
Ruby

# frozen_string_literal: true
module MCP
class Configuration
LATEST_STABLE_PROTOCOL_VERSION = "2026-07-28"
ROOTS_SAMPLING_LOGGING_DEPRECATED_PROTOCOL_VERSION = "2026-07-28"
SUPPORTED_STABLE_PROTOCOL_VERSIONS = [
LATEST_STABLE_PROTOCOL_VERSION, "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05",
].freeze
DEFAULT_NEGOTIATED_PROTOCOL_VERSION = "2025-03-26"
# Protocol versions of the stateless "modern" lifecycle introduced by the MCP 2026-07-28 spec release (SEP-2575).
# 2026-07-28 serves both lifecycles of the dual-era model: it is negotiable through the legacy `initialize`
# handshake (so it also appears in `SUPPORTED_STABLE_PROTOCOL_VERSIONS`), and it is the version of the modern
# lifecycle, where each request carries its own version in `_meta` and is validated against this list
# independently, with no handshake.
# https://github.com/modelcontextprotocol/modelcontextprotocol/pull/2575
LATEST_MODERN_PROTOCOL_VERSION = "2026-07-28"
SUPPORTED_MODERN_PROTOCOL_VERSIONS = [LATEST_MODERN_PROTOCOL_VERSION].freeze
class << self
def modern_protocol_version?(version)
SUPPORTED_MODERN_PROTOCOL_VERSIONS.include?(version)
end
end
attr_writer :exception_reporter, :around_request
# @deprecated Use {#around_request=} instead. `instrumentation_callback`
# fires only after a request completes and cannot wrap execution in a
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
# @see #around_request=
attr_writer :instrumentation_callback
def initialize(exception_reporter: nil, around_request: nil, instrumentation_callback: nil, protocol_version: nil,
validate_tool_call_arguments: true, validate_tool_call_results: false, instrument_server_context: false)
@exception_reporter = exception_reporter
@around_request = around_request
@instrumentation_callback = instrumentation_callback
@protocol_version = protocol_version
if protocol_version
validate_protocol_version!(protocol_version)
end
validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
validate_value_of_validate_tool_call_results!(validate_tool_call_results)
validate_value_of_instrument_server_context!(instrument_server_context)
@validate_tool_call_arguments = validate_tool_call_arguments
@validate_tool_call_results = validate_tool_call_results
@instrument_server_context = instrument_server_context
end
def protocol_version=(protocol_version)
validate_protocol_version!(protocol_version)
@protocol_version = protocol_version
end
def validate_tool_call_arguments=(validate_tool_call_arguments)
validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
@validate_tool_call_arguments = validate_tool_call_arguments
end
# Opt in to exposing the user-defined `server_context` in the
# `around_request` / `instrumentation_callback` data hash. Off by default:
# the hash is application-supplied and may hold values a tracing backend
# should not receive, so surfacing it has to be a deliberate choice.
def instrument_server_context=(instrument_server_context)
validate_value_of_instrument_server_context!(instrument_server_context)
@instrument_server_context = instrument_server_context
end
def validate_tool_call_results=(validate_tool_call_results)
validate_value_of_validate_tool_call_results!(validate_tool_call_results)
@validate_tool_call_results = validate_tool_call_results
end
def protocol_version
@protocol_version || LATEST_STABLE_PROTOCOL_VERSION
end
def protocol_version?
!@protocol_version.nil?
end
def exception_reporter
@exception_reporter || default_exception_reporter
end
def exception_reporter?
!@exception_reporter.nil?
end
def around_request
@around_request || default_around_request
end
def around_request?
!@around_request.nil?
end
# @deprecated Use {#around_request} instead. `instrumentation_callback`
# fires only after a request completes and cannot wrap execution in a
# surrounding block (e.g. for Application Performance Monitoring (APM) spans).
# @see #around_request
def instrumentation_callback
@instrumentation_callback || default_instrumentation_callback
end
# @deprecated Use {#around_request?} instead.
# @see #around_request?
def instrumentation_callback?
!@instrumentation_callback.nil?
end
attr_reader :validate_tool_call_arguments
attr_reader :validate_tool_call_results
def instrument_server_context?
!!@instrument_server_context
end
def validate_tool_call_arguments?
!!@validate_tool_call_arguments
end
def validate_tool_call_results?
!!@validate_tool_call_results
end
def merge(other)
return self if other.nil?
exception_reporter = if other.exception_reporter?
other.exception_reporter
else
@exception_reporter
end
around_request = if other.around_request?
other.around_request
else
@around_request
end
instrumentation_callback = if other.instrumentation_callback?
other.instrumentation_callback
else
@instrumentation_callback
end
protocol_version = if other.protocol_version?
other.protocol_version
else
@protocol_version
end
validate_tool_call_arguments = other.validate_tool_call_arguments
validate_tool_call_results = other.validate_tool_call_results
Configuration.new(
exception_reporter: exception_reporter,
around_request: around_request,
instrumentation_callback: instrumentation_callback,
protocol_version: protocol_version,
validate_tool_call_arguments: validate_tool_call_arguments,
validate_tool_call_results: validate_tool_call_results,
instrument_server_context: other.instrument_server_context?,
)
end
private
def validate_protocol_version!(protocol_version)
unless SUPPORTED_STABLE_PROTOCOL_VERSIONS.include?(protocol_version)
message = "protocol_version must be #{SUPPORTED_STABLE_PROTOCOL_VERSIONS[0...-1].join(", ")}, or #{SUPPORTED_STABLE_PROTOCOL_VERSIONS[-1]}"
raise ArgumentError, message
end
end
def validate_value_of_validate_tool_call_arguments!(validate_tool_call_arguments)
unless validate_tool_call_arguments.is_a?(TrueClass) || validate_tool_call_arguments.is_a?(FalseClass)
raise ArgumentError, "validate_tool_call_arguments must be a boolean"
end
end
def validate_value_of_validate_tool_call_results!(validate_tool_call_results)
unless validate_tool_call_results.is_a?(TrueClass) || validate_tool_call_results.is_a?(FalseClass)
raise ArgumentError, "validate_tool_call_results must be a boolean"
end
end
def validate_value_of_instrument_server_context!(instrument_server_context)
unless instrument_server_context.is_a?(TrueClass) || instrument_server_context.is_a?(FalseClass)
raise ArgumentError, "instrument_server_context must be a boolean"
end
end
def default_exception_reporter
@default_exception_reporter ||= ->(exception, server_context) {}
end
def default_around_request
@default_around_request ||= ->(_data, &request_handler) { request_handler.call }
end
# @deprecated Use {#default_around_request} instead.
def default_instrumentation_callback
@default_instrumentation_callback ||= ->(data) {}
end
end
end