# frozen_string_literal: true require "test_helper" module MCP class ServerContextTest < ActiveSupport::TestCase test "ServerContext delegates method calls to the underlying context" do context = { user: "test_user" } progress = Progress.new(notification_target: mock, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: mock) assert_equal "test_user", server_context[:user] end test "ServerContext respond_to? returns true for methods on the underlying context" do context = { user: "test_user" } progress = Progress.new(notification_target: mock, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: mock) assert_respond_to server_context, :[] assert_respond_to server_context, :keys end test "ServerContext respond_to? returns false for methods not on the underlying context" do context = { user: "test_user" } progress = Progress.new(notification_target: mock, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: mock) refute_respond_to server_context, :nonexistent_method end test "ServerContext raises NoMethodError for methods not on the underlying context" do context = { user: "test_user" } progress = Progress.new(notification_target: mock, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: mock) assert_raises(NoMethodError) { server_context.nonexistent_method } end test "ServerContext#list_roots delegates to notification_target" do notification_target = mock notification_target.expects(:list_roots).with( related_request_id: nil, ).returns({ roots: [{ uri: "file:///project", name: "Project" }] }) context = mock progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: notification_target) result = server_context.list_roots assert_equal [{ uri: "file:///project", name: "Project" }], result[:roots] end test "ServerContext#list_roots stamps the originating request id" do # Per SEP-2260, server-to-client requests are associated with the originating client request automatically. notification_target = mock notification_target.expects(:list_roots).with(related_request_id: "req-1").returns({ roots: [] }) progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new( mock, progress: progress, notification_target: notification_target, related_request_id: "req-1", ) server_context.list_roots end test "ServerContext#create_sampling_message stamps related_request_id non-overridably" do # A caller-supplied `related_request_id:` must not detach the request from its originating client request (SEP-2260): # the literal keyword after `**kwargs` wins. notification_target = mock notification_target.expects(:create_sampling_message).with( messages: [], max_tokens: 5, related_request_id: "req-1", ).returns({}) progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new( mock, progress: progress, notification_target: notification_target, related_request_id: "req-1", ) server_context.create_sampling_message(messages: [], max_tokens: 5, related_request_id: "attacker") end test "ServerContext#create_form_elicitation stamps related_request_id non-overridably" do notification_target = mock notification_target.expects(:create_form_elicitation).with( message: "hi", requested_schema: {}, related_request_id: "req-1", ).returns({}) progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new( mock, progress: progress, notification_target: notification_target, related_request_id: "req-1", ) server_context.create_form_elicitation(message: "hi", requested_schema: {}, related_request_id: "attacker") end test "ServerContext#create_url_elicitation stamps related_request_id non-overridably" do notification_target = mock notification_target.expects(:create_url_elicitation).with( message: "hi", url: "https://example.com", elicitation_id: "el-1", related_request_id: "req-1", ).returns({}) progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new( mock, progress: progress, notification_target: notification_target, related_request_id: "req-1", ) server_context.create_url_elicitation( message: "hi", url: "https://example.com", elicitation_id: "el-1", related_request_id: "attacker", ) end test "ServerContext#list_roots raises NoMethodError when notification_target does not respond" do notification_target = mock context = mock progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: notification_target) assert_raises(NoMethodError) { server_context.list_roots } end test "ServerContext#ping delegates to notification_target" do notification_target = mock notification_target.expects(:ping).with(related_request_id: nil).returns({}) context = mock progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: notification_target) result = server_context.ping assert_equal({}, result) end test "ServerContext forwards a per-request timeout to the notification target" do notification_target = mock notification_target.expects(:ping).with(related_request_id: nil, timeout: 12).returns({}) notification_target.expects(:list_roots).with(related_request_id: nil, timeout: 34).returns({ roots: [] }) notification_target.expects(:create_form_elicitation).with( message: "Approve?", requested_schema: { type: "object", properties: {} }, related_request_id: nil, timeout: 56, ).returns({ action: "accept" }) progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(mock, progress: progress, notification_target: notification_target) server_context.ping(timeout: 12) server_context.list_roots(timeout: 34) server_context.create_form_elicitation( message: "Approve?", requested_schema: { type: "object", properties: {} }, timeout: 56, ) end test "ServerContext omits timeout entirely when the caller does not ask for one" do # A notification target predating the keyword (a custom object standing in for a session) keeps working, # and the transport applies its own default. notification_target = mock notification_target.expects(:ping).with(related_request_id: nil).returns({}) progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(mock, progress: progress, notification_target: notification_target) server_context.ping end test "ServerContext#ping raises NoMethodError when notification_target does not respond" do notification_target = mock context = mock progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: notification_target) assert_raises(NoMethodError) { server_context.ping } end test "ServerContext#create_sampling_message delegates to notification_target over context" do notification_target = mock notification_target.expects(:create_sampling_message).with( messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, related_request_id: nil, ).returns({ role: "assistant", content: { type: "text", text: "Hi" } }) context = mock progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: notification_target) result = server_context.create_sampling_message( messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, ) assert_equal "Hi", result[:content][:text] end test "ServerContext#create_sampling_message falls back to context when notification_target does not respond" do notification_target = mock context = mock context.expects(:create_sampling_message).with( messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, related_request_id: nil, ).returns({ role: "assistant", content: { type: "text", text: "Fallback" } }) progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: notification_target) result = server_context.create_sampling_message( messages: [{ role: "user", content: { type: "text", text: "Hello" } }], max_tokens: 100, ) assert_equal "Fallback", result[:content][:text] end test "ServerContext#create_form_elicitation delegates to notification_target" do notification_target = mock notification_target.expects(:create_form_elicitation).with( message: "Please provide your name", requested_schema: { type: "object", properties: { name: { type: "string" } } }, related_request_id: nil, ).returns(action: "accept", content: { name: "test_user" }) context = mock progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: notification_target) result = server_context.create_form_elicitation( message: "Please provide your name", requested_schema: { type: "object", properties: { name: { type: "string" } } }, ) assert_equal "accept", result[:action] end test "ServerContext#create_url_elicitation delegates to notification_target" do notification_target = mock notification_target.expects(:create_url_elicitation).with( message: "Please authorize", url: "https://example.com/oauth", elicitation_id: "abc-123", related_request_id: nil, ).returns(action: "accept") context = mock progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: notification_target) result = server_context.create_url_elicitation( message: "Please authorize", url: "https://example.com/oauth", elicitation_id: "abc-123", ) assert_equal "accept", result[:action] end test "ServerContext#notify_elicitation_complete delegates to notification_target" do notification_target = mock notification_target.expects(:notify_elicitation_complete).with(elicitation_id: "abc-123") context = mock progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: notification_target) server_context.notify_elicitation_complete(elicitation_id: "abc-123") end test "ServerContext delegates to custom object context" do context = Object.new def context.custom_method "custom_value" end progress = Progress.new(notification_target: mock, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: mock) assert_equal "custom_value", server_context.custom_method end test "ServerContext forwards positional, keyword, and block arguments to the context" do context = Object.new def context.combine(prefix, suffix:, &block) block.call("#{prefix}-#{suffix}") end progress = Progress.new(notification_target: mock, progress_token: nil) server_context = ServerContext.new(context, progress: progress, notification_target: mock) result = server_context.combine("a", suffix: "b", &:upcase) assert_equal "A-B", result end test "ServerContext#report_progress works with nil context" do progress = mock progress.expects(:report).with(50, total: 100, message: nil).once server_context = ServerContext.new(nil, progress: progress, notification_target: mock) server_context.report_progress(50, total: 100) end test "ServerContext#notify_log_message is a no-op when notification_target is nil" do progress = Progress.new(notification_target: nil, progress_token: nil) server_context = ServerContext.new(nil, progress: progress, notification_target: nil) assert_nothing_raised { server_context.notify_log_message(data: "test", level: "info") } end test "ServerContext#notify_log_message drops messages on modern requests without a logLevel" do # Per SEP-2575, without `io.modelcontextprotocol/logLevel` in `_meta`, the server MUST NOT send # any `notifications/message` for the request. notification_target = mock notification_target.expects(:notify_log_message).never server_context = build_modern_server_context(notification_target, log_level: nil) server_context.notify_log_message(data: "test", level: "error") end test "ServerContext#notify_log_message drops messages below the modern request logLevel" do notification_target = mock notification_target.expects(:notify_log_message).never server_context = build_modern_server_context(notification_target, log_level: "error") server_context.notify_log_message(data: "test", level: "info") end test "ServerContext#notify_log_message delegates messages at or above the modern request logLevel" do notification_target = mock notification_target.expects(:notify_log_message).with( data: "test", level: "error", logger: nil, related_request_id: nil, ).once server_context = build_modern_server_context(notification_target, log_level: "warning") server_context.notify_log_message(data: "test", level: "error") end test "ServerContext exposes envelope data and falls back to the session on legacy requests" do notification_target = mock notification_target.stubs(:client).returns({ name: "legacy_client", version: "1.0" }) notification_target.stubs(:client_capabilities).returns({ roots: {} }) progress = Progress.new(notification_target: notification_target, progress_token: nil) legacy_context = ServerContext.new(nil, progress: progress, notification_target: notification_target) modern_context = build_modern_server_context(notification_target) refute_predicate legacy_context, :modern? assert_equal({ name: "legacy_client", version: "1.0" }, legacy_context.client_info) assert_equal({ roots: {} }, legacy_context.client_capabilities) assert_nil legacy_context.protocol_version assert_predicate modern_context, :modern? assert_equal({ name: "modern_client", version: "2.0" }, modern_context.client_info) assert_equal({ elicitation: { form: {} } }, modern_context.client_capabilities) assert_equal "2026-07-28", modern_context.protocol_version end test "ServerContext#require_client_capability! matches string-keyed capabilities and requires a path" do envelope = RequestEnvelope.new( protocol_version: "2026-07-28", client_info: { name: "c", version: "1" }, client_capabilities: { "elicitation" => { "form" => {} } }, ) progress = Progress.new(notification_target: nil, progress_token: nil) server_context = ServerContext.new(nil, progress: progress, notification_target: nil, envelope: envelope) assert_nothing_raised { server_context.require_client_capability!(:elicitation, :form) } assert_raises(ArgumentError) { server_context.require_client_capability! } error = assert_raises(Server::MissingRequiredClientCapabilityError) do server_context.require_client_capability!(:sampling) end assert_equal({ requiredCapabilities: { sampling: {} } }, error.error_data) end test "ServerContext#notify_resources_updated delegates to notification_target" do notification_target = mock notification_target.expects(:notify_resources_updated).with(uri: "test://resource-1").once progress = Progress.new(notification_target: notification_target, progress_token: nil) server_context = ServerContext.new(nil, progress: progress, notification_target: notification_target) server_context.notify_resources_updated(uri: "test://resource-1") end test "ServerContext#notify_resources_updated is a no-op when notification_target is nil" do progress = Progress.new(notification_target: nil, progress_token: nil) server_context = ServerContext.new(nil, progress: progress, notification_target: nil) assert_nothing_raised { server_context.notify_resources_updated(uri: "test://resource-1") } end # Tool without server_context parameter class SimpleToolWithoutContext < Tool tool_name "simple_without_context" description "A tool that doesn't use server_context" input_schema({ properties: { message: { type: "string" } }, required: ["message"] }) class << self def call(message:) Tool::Response.new([ { type: "text", content: "SimpleToolWithoutContext: #{message}" }, ]) end end end # Tool with optional server_context parameter class ToolWithOptionalContext < Tool tool_name "tool_with_optional_context" description "A tool with optional server_context" input_schema({ properties: { message: { type: "string" } }, required: ["message"] }) class << self def call(message:, server_context: nil) user = server_context.respond_to?(:[]) ? server_context[:user] : nil context_info = user ? "with context: #{user}" : "no context" Tool::Response.new([ { type: "text", content: "ToolWithOptionalContext: #{message} (#{context_info})" }, ]) end end end # Tool with required server_context parameter class ToolWithRequiredContext < Tool tool_name "tool_with_required_context" description "A tool that requires server_context" input_schema({ properties: { message: { type: "string" } }, required: ["message"] }) class << self def call(message:, server_context:) Tool::Response.new([ { type: "text", content: "ToolWithRequiredContext: #{message} for user #{server_context[:user]}" }, ]) end end end setup do @server_with_context = Server.new( name: "test_server", tools: [SimpleToolWithoutContext, ToolWithOptionalContext, ToolWithRequiredContext], server_context: { user: "test_user" }, ) @server_without_context = Server.new( name: "test_server_no_context", tools: [SimpleToolWithoutContext, ToolWithOptionalContext], ) end test "tool without server_context parameter works when server has context" do request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "simple_without_context", arguments: { message: "Hello" }, }, } response = @server_with_context.handle(request) assert response[:result] assert_equal "SimpleToolWithoutContext: Hello", response[:result][:content][0][:content] end test "tool with optional server_context receives context when server has it" do request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "tool_with_optional_context", arguments: { message: "Hello" }, }, } response = @server_with_context.handle(request) assert response[:result] assert_equal "ToolWithOptionalContext: Hello (with context: test_user)", response[:result][:content][0][:content] end test "tool with optional server_context works when server has no context" do request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "tool_with_optional_context", arguments: { message: "Hello" }, }, } response = @server_without_context.handle(request) assert response[:result] assert_equal "ToolWithOptionalContext: Hello (no context)", response[:result][:content][0][:content] end test "tool with required server_context receives context" do request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "tool_with_required_context", arguments: { message: "Hello" }, }, } response = @server_with_context.handle(request) assert response[:result] assert_equal "ToolWithRequiredContext: Hello for user test_user", response[:result][:content][0][:content] end test "tool with required server_context returns protocol error in JSON-RPC format when server has no context" do server_no_context = Server.new( name: "test_server_no_context", tools: [ToolWithRequiredContext], ) request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "tool_with_required_context", arguments: { message: "Hello" }, }, } response = server_no_context.handle(request) assert_nil response[:result] assert_equal(-32603, response[:error][:code]) assert_equal "Internal error", response[:error][:message] assert_equal "Internal error calling tool tool_with_required_context", response[:error][:data] end test "call_tool_with_args correctly detects server_context parameter presence" do # Tool without server_context refute SimpleToolWithoutContext.method(:call).parameters.any? { |_type, name| name == :server_context } # Tool with optional server_context assert ToolWithOptionalContext.method(:call).parameters.any? { |_type, name| name == :server_context } # Tool with required server_context assert ToolWithRequiredContext.method(:call).parameters.any? { |_type, name| name == :server_context } end test "tools can use splat kwargs to accept any arguments including server_context" do class FlexibleTool < Tool tool_name "flexible_tool" class << self def call(**kwargs) message = kwargs[:message] context = kwargs[:server_context] Tool::Response.new([ { type: "text", content: "FlexibleTool: #{message} (context: #{context ? "present" : "absent"})", }, ]) end end end server = Server.new( name: "test_server", tools: [FlexibleTool], server_context: { user: "test_user" }, ) request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "flexible_tool", arguments: { message: "Hello" }, }, } response = server.handle(request) assert response[:result] assert_equal "FlexibleTool: Hello (context: present)", response[:result][:content][0][:content] end # Prompt tests # Prompt without server_context parameter class SimplePromptWithoutContext < Prompt prompt_name "simple_prompt_without_context" description "A prompt that doesn't use server_context" arguments [Prompt::Argument.new(name: "message", required: true)] class << self def template(args) Prompt::Result.new( messages: [ Prompt::Message.new( role: "user", content: Content::Text.new("SimplePromptWithoutContext: #{args[:message]}"), ), ], ) end end end # Prompt with optional server_context parameter class PromptWithOptionalContext < Prompt prompt_name "prompt_with_optional_context" description "A prompt with optional server_context" arguments [Prompt::Argument.new(name: "message", required: true)] class << self def template(args, server_context: nil) user = server_context.respond_to?(:[]) ? server_context[:user] : nil context_info = user ? "with context: #{user}" : "no context" Prompt::Result.new( messages: [ Prompt::Message.new( role: "user", content: Content::Text.new("PromptWithOptionalContext: #{args[:message]} (#{context_info})"), ), ], ) end end end # Prompt with required server_context parameter class PromptWithRequiredContext < Prompt prompt_name "prompt_with_required_context" description "A prompt that requires server_context" arguments [Prompt::Argument.new(name: "message", required: true)] class << self def template(args, server_context:) Prompt::Result.new( messages: [ Prompt::Message.new( role: "user", content: Content::Text.new( "PromptWithRequiredContext: #{args[:message]} for user #{server_context[:user]}", ), ), ], ) end end end test "prompt without server_context parameter works when server has context" do server = Server.new( name: "test_server", prompts: [SimplePromptWithoutContext], server_context: { user: "test_user" }, ) request = { jsonrpc: "2.0", id: 1, method: "prompts/get", params: { name: "simple_prompt_without_context", arguments: { message: "Hello" }, }, } response = server.handle(request) assert response[:result] assert_equal "SimplePromptWithoutContext: Hello", response[:result][:messages][0][:content][:text] end test "prompt with optional server_context receives context when server has it" do server = Server.new( name: "test_server", prompts: [PromptWithOptionalContext], server_context: { user: "test_user" }, ) request = { jsonrpc: "2.0", id: 1, method: "prompts/get", params: { name: "prompt_with_optional_context", arguments: { message: "Hello" }, }, } response = server.handle(request) assert response[:result] assert_equal "PromptWithOptionalContext: Hello (with context: test_user)", response[:result][:messages][0][:content][:text] end test "prompt with optional server_context works when server has no context" do server = Server.new( name: "test_server", prompts: [PromptWithOptionalContext], ) request = { jsonrpc: "2.0", id: 1, method: "prompts/get", params: { name: "prompt_with_optional_context", arguments: { message: "Hello" }, }, } response = server.handle(request) assert response[:result] assert_equal "PromptWithOptionalContext: Hello (no context)", response[:result][:messages][0][:content][:text] end test "prompt with required server_context receives context" do server = Server.new( name: "test_server", prompts: [PromptWithRequiredContext], server_context: { user: "test_user" }, ) request = { jsonrpc: "2.0", id: 1, method: "prompts/get", params: { name: "prompt_with_required_context", arguments: { message: "Hello" }, }, } response = server.handle(request) assert response[:result] assert_equal "PromptWithRequiredContext: Hello for user test_user", response[:result][:messages][0][:content][:text] end test "prompts can use splat kwargs to accept any arguments including server_context" do class FlexiblePrompt < Prompt prompt_name "flexible_prompt" arguments [Prompt::Argument.new(name: "message", required: true)] class << self def template(args, **kwargs) message = args[:message] context = kwargs[:server_context] Prompt::Result.new( messages: [ Prompt::Message.new( role: "user", content: Content::Text.new("FlexiblePrompt: #{message} (context: #{context ? "present" : "absent"})"), ), ], ) end end end server = Server.new( name: "test_server", prompts: [FlexiblePrompt], server_context: { user: "test_user" }, ) request = { jsonrpc: "2.0", id: 1, method: "prompts/get", params: { name: "flexible_prompt", arguments: { message: "Hello" }, }, } response = server.handle(request) assert response[:result] assert_equal "FlexiblePrompt: Hello (context: present)", response[:result][:messages][0][:content][:text] end test "tool receives _meta when provided in request params" do class ToolWithMeta < Tool tool_name "tool_with_meta" description "A tool that uses _meta" input_schema({ properties: { message: { type: "string" } }, required: ["message"] }) class << self def call(message:, server_context:) meta_info = server_context.dig(:_meta, :provider, :metadata) Tool::Response.new([ { type: "text", content: "Message: #{message}, Metadata: #{meta_info}" }, ]) end end end server = Server.new( name: "test_server", tools: [ToolWithMeta], ) request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "tool_with_meta", arguments: { message: "Hello" }, _meta: { provider: { metadata: "test_value", }, }, }, } response = server.handle(request) assert response[:result] assert_equal "Message: Hello, Metadata: test_value", response[:result][:content][0][:content] end test "_meta is nested within server_context" do class ToolWithNestedMeta < Tool tool_name "tool_with_nested_meta" description "A tool that uses nested _meta" input_schema({ properties: { message: { type: "string" } }, required: ["message"] }) class << self def call(message:, server_context:) user = server_context[:user] session_id = server_context.dig(:_meta, :session_id) Tool::Response.new([ { type: "text", content: "User: #{user}, Session: #{session_id}, Message: #{message}" }, ]) end end end server = Server.new( name: "test_server", tools: [ToolWithNestedMeta], server_context: { user: "test_user", original_field: "value" }, ) request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "tool_with_nested_meta", arguments: { message: "Hello" }, _meta: { session_id: "abc123", }, }, } response = server.handle(request) assert response[:result] assert_equal "User: test_user, Session: abc123, Message: Hello", response[:result][:content][0][:content] end test "_meta preserves original server_context" do class ToolPreservesContext < Tool tool_name "tool_preserves_context" description "A tool that checks context preservation" class << self def call(server_context:) priority = server_context[:priority] meta_priority = server_context.dig(:_meta, :priority) Tool::Response.new([ { type: "text", content: "Context priority: #{priority}, Meta priority: #{meta_priority}" }, ]) end end end server = Server.new( name: "test_server", tools: [ToolPreservesContext], server_context: { priority: "low" }, ) request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "tool_preserves_context", arguments: {}, _meta: { priority: "high", }, }, } response = server.handle(request) assert response[:result] assert_equal "Context priority: low, Meta priority: high", response[:result][:content][0][:content] end test "prompt receives _meta when provided in request params" do class PromptWithMeta < Prompt prompt_name "prompt_with_meta" description "A prompt that uses _meta" arguments [Prompt::Argument.new(name: "message", required: true)] class << self def template(args, server_context:) meta_info = server_context.dig(:_meta, :request_id) Prompt::Result.new( messages: [ Prompt::Message.new( role: "user", content: Content::Text.new("Message: #{args[:message]}, Request ID: #{meta_info}"), ), ], ) end end end server = Server.new( name: "test_server", prompts: [PromptWithMeta], ) request = { jsonrpc: "2.0", id: 1, method: "prompts/get", params: { name: "prompt_with_meta", arguments: { message: "Hello" }, _meta: { request_id: "req_12345", }, }, } response = server.handle(request) assert response[:result] assert_equal "Message: Hello, Request ID: req_12345", response[:result][:messages][0][:content][:text] end test "non-Hash server_context is preserved when _meta is present" do server_obj = Object.new def server_obj.custom_method "custom_value" end tool_class = Class.new(Tool) do tool_name "tool_with_non_hash_context" define_singleton_method(:call) do |server_context:, **_args| Tool::Response.new([ { type: "text", content: "custom: #{server_context.custom_method}" }, ]) end end server = Server.new( name: "test_server", tools: [tool_class], server_context: server_obj, ) request = { jsonrpc: "2.0", id: 1, method: "tools/call", params: { name: "tool_with_non_hash_context", arguments: {}, _meta: { progressToken: "token-1" }, }, } response = server.handle(request) assert response[:result] assert_equal "custom: custom_value", response[:result][:content][0][:content] end test "_meta is nested within server_context for prompts" do class PromptWithNestedContext < Prompt prompt_name "prompt_with_nested_context" description "A prompt that uses nested context" arguments [Prompt::Argument.new(name: "message", required: true)] class << self def template(args, server_context:) user = server_context[:user] trace_id = server_context.dig(:_meta, :trace_id) Prompt::Result.new( messages: [ Prompt::Message.new( role: "user", content: Content::Text.new("User: #{user}, Trace: #{trace_id}, Message: #{args[:message]}"), ), ], ) end end end server = Server.new( name: "test_server", prompts: [PromptWithNestedContext], server_context: { user: "prompt_user" }, ) request = { jsonrpc: "2.0", id: 1, method: "prompts/get", params: { name: "prompt_with_nested_context", arguments: { message: "World" }, _meta: { trace_id: "trace_xyz789", }, }, } response = server.handle(request) assert response[:result] assert_equal "User: prompt_user, Trace: trace_xyz789, Message: World", response[:result][:messages][0][:content][:text] end private def build_modern_server_context(notification_target, log_level: nil) envelope = RequestEnvelope.new( protocol_version: "2026-07-28", client_info: { name: "modern_client", version: "2.0" }, client_capabilities: { elicitation: { form: {} } }, log_level: log_level, ) progress = Progress.new(notification_target: notification_target, progress_token: nil) ServerContext.new(nil, progress: progress, notification_target: notification_target, envelope: envelope) end end end