# frozen_string_literal: true

require "test_helper"

module MCP
  class ServerTest < ActiveSupport::TestCase
    include InstrumentationTestHelper
    include InitializeParamsTestHelper
    include DeprecationWarningTestHelper
    setup do
      @tool = Tool.define(
        name: "test_tool",
        title: "Test tool",
        description: "A test tool",
        meta: { foo: "bar" },
      )

      @tool_that_raises = Tool.define(
        name: "tool_that_raises",
        title: "Tool that raises",
        description: "A tool that raises",
        input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] },
      ) { raise StandardError, "Tool error" }

      @tool_with_no_args = Tool.define(
        name: "tool_with_no_args",
        title: "Tool with no args",
        description: "This tool performs specific functionality...",
        annotations: {
          read_only_hint: true,
        },
      ) do
        Tool::Response.new([{ type: "text", content: "OK" }])
      end

      @prompt = Prompt.define(
        name: "test_prompt",
        title: "Test Prompt",
        description: "Test prompt",
        arguments: [
          Prompt::Argument.new(name: "test_argument", description: "Test argument", required: true),
        ],
      ) do
        Prompt::Result.new(
          description: "Hello, world!",
          messages: [
            Prompt::Message.new(role: "user", content: Content::Text.new("Hello, world!")),
          ],
        )
      end

      @resource = Resource.new(
        uri: "https://test_resource.invalid",
        name: "test-resource",
        title: "Test Resource",
        description: "Test resource",
        icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
        mime_type: "text/plain",
      )

      @resource_template = ResourceTemplate.new(
        uri_template: "https://test_resource.invalid/{id}",
        name: "test-resource",
        title: "Test Resource",
        description: "Test resource",
        icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
        mime_type: "text/plain",
      )

      @server_name = "test_server"
      configuration = MCP::Configuration.new
      configuration.instrumentation_callback = instrumentation_helper.callback

      @server = Server.new(
        description: "Test server",
        icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
        name: @server_name,
        title: "Example Server Display Name",
        version: "1.2.3",
        instructions: "Optional instructions for the client",
        tools: [@tool, @tool_that_raises],
        prompts: [@prompt],
        resources: [@resource],
        resource_templates: [@resource_template],
        configuration: configuration,
      )
    end

    # https://modelcontextprotocol.io/specification/latest/basic/utilities/ping#behavior-requirements
    test "#handle ping request returns empty response" do
      request = {
        jsonrpc: "2.0",
        method: "ping",
        id: "123",
      }

      response = @server.handle(request)
      assert_equal(
        {
          jsonrpc: "2.0",
          id: "123",
          result: {},
        },
        response,
      )
      assert_instrumentation_data({ method: "ping" })
    end

    test "#handle_json ping request returns empty response" do
      request = JSON.generate({
        jsonrpc: "2.0",
        method: "ping",
        id: "123",
      })

      response = JSON.parse(@server.handle_json(request), symbolize_names: true)
      assert_equal(
        {
          jsonrpc: "2.0",
          id: "123",
          result: {},
        },
        response,
      )
      assert_instrumentation_data({ method: "ping" })
    end

    test "#handle server/discover returns supported versions, capabilities, server info, and instructions" do
      response = @server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })
      result = response[:result]

      assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, result[:supportedVersions]
      # Without a `subscriptions/listen`-serving transport, the `listChanged` flags are
      # stripped from the advertised capabilities (see the dedicated tests below).
      assert_equal @server.capabilities.keys, result[:capabilities].keys
      # Per the finalized spec (PR #3002), the server identity is the optional `_meta` stamp,
      # not a top-level `serverInfo` field.
      assert_equal @server_name, result.dig(:_meta, RequestEnvelope::SERVER_INFO_META_KEY, :name)
      assert_equal "1.2.3", result.dig(:_meta, RequestEnvelope::SERVER_INFO_META_KEY, :version)
      refute result.key?(:serverInfo)
      assert_equal "Optional instructions for the client", result[:instructions]
    end

    test "#handle server/discover strips listChanged and subscribe flags without a listen-serving transport" do
      server = Server.new(name: "discover_test", capabilities: {
        tools: { listChanged: true },
        resources: { listChanged: true, subscribe: true },
        logging: {},
      })

      result = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })[:result]

      assert_equal({ tools: {}, resources: {}, logging: {} }, result[:capabilities])
    end

    test "#handle server/discover keeps listChanged flags when the transport serves subscriptions/listen" do
      server = Server.new(name: "discover_test", capabilities: { tools: { listChanged: true } })
      transport = mock
      transport.stubs(:serves_subscriptions_listen?).returns(true)
      server.transport = transport

      result = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })[:result]

      assert_equal({ tools: { listChanged: true } }, result[:capabilities])
    end

    test "#handle server/discover responds before initialize and regardless of capabilities" do
      # Per SEP-2575, discovery is sessionless: no prior `initialize`, no capability gate.
      server = Server.new(name: "discover_test", capabilities: {})

      response = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })

      assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, response.dig(:result, :supportedVersions)
    end

    test "#handle server/discover does not mark the session initialized" do
      session = ServerSession.new(server: @server, transport: mock)

      @server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 }, session: session)

      refute_predicate session, :initialized?
      # `initialize` must still succeed afterwards.
      response = @server.handle(
        { jsonrpc: "2.0", method: "initialize", id: 2, params: initialize_params },
        session: session,
      )
      refute_nil response[:result]
    end

    test "#handle server/discover omits instructions when the server has none" do
      server = Server.new(name: "discover_test")

      response = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })

      refute response[:result].key?(:instructions)
    end

    test "#handle server/discover always carries the required cache hints with spec defaults" do
      # Unlike the opt-in SEP-2549 hints on list/read results, `ttlMs`/`cacheScope`
      # are REQUIRED on `DiscoverResult`.
      server = Server.new(name: "discover_test")

      result = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })[:result]

      assert_equal 0, result[:ttlMs]
      assert_equal "private", result[:cacheScope]
    end

    test "#handle server/discover reuses the configured SEP-2549 cache hints" do
      server = Server.new(name: "discover_test", ttl_ms: 3_600_000, cache_scope: "public")

      result = server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })[:result]

      assert_equal 3_600_000, result[:ttlMs]
      assert_equal "public", result[:cacheScope]
    end

    test "#define_custom_method raises for server/discover" do
      assert_raises(Server::MethodAlreadyDefinedError) do
        @server.define_custom_method(method_name: "server/discover") { {} }
      end
    end

    test "UnsupportedProtocolVersionError surfaces as -32022 with the SEP-2575 data shape" do
      server = Server.new(name: "error_test", tools: [TestTool])
      server.define_tool(name: "unsupported_version_tool") do
        raise Server::UnsupportedProtocolVersionError, "1900-01-01"
      end

      response = server.handle({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "unsupported_version_tool" },
      })

      assert_equal ErrorCodes::UNSUPPORTED_PROTOCOL_VERSION, response.dig(:error, :code)
      assert_equal "Unsupported protocol version", response.dig(:error, :message)
      assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, response.dig(:error, :data, :supported)
      assert_equal "1900-01-01", response.dig(:error, :data, :requested)
    end

    test "UnsupportedProtocolVersionError reports an unknown requested version" do
      error = Server::UnsupportedProtocolVersionError.new(nil)

      assert_equal "unknown", error.error_data[:requested]
    end

    test "UnsupportedProtocolVersionError accepts a symbol-keyed request Hash on every Ruby version" do
      # On Ruby 2.7, a keyword parameter in `initialize` would make the trailing symbol-keyed Hash split
      # into keywords and raise an ArgumentError.
      request = { name: "echo", arguments: {}, _meta: {} }

      error = Server::UnsupportedProtocolVersionError.new("1900-01-01", request)

      assert_equal "1900-01-01", error.error_data[:requested]
      assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, error.error_data[:supported]
    end

    test "MissingRequiredClientCapabilityError surfaces as -32021 with the SEP-2575 data shape" do
      server = Server.new(name: "error_test", tools: [TestTool])
      server.define_tool(name: "missing_capability_tool") do
        raise Server::MissingRequiredClientCapabilityError, { elicitation: {} }
      end

      response = server.handle({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "missing_capability_tool" },
      })

      assert_equal ErrorCodes::MISSING_REQUIRED_CLIENT_CAPABILITY, response.dig(:error, :code)
      assert_equal "Missing required client capability", response.dig(:error, :message)
      assert_equal({ elicitation: {} }, response.dig(:error, :data, :requiredCapabilities))
    end

    test "#handle tools/call with a modern envelope exposes per-request client data without touching the session" do
      server = Server.new(name: "modern_test", tools: [])
      received = nil
      server.define_tool(name: "modern_tool") do |server_context:|
        received = {
          modern: server_context.modern?,
          client_info: server_context.client_info,
          client_capabilities: server_context.client_capabilities,
          protocol_version: server_context.protocol_version,
        }
        Tool::Response.new([{ type: "text", text: "ok" }])
      end
      session = ServerSession.new(server: server, transport: mock)

      response = server.handle(
        modern_request("tools/call", { name: "modern_tool", arguments: {} }, capabilities: { elicitation: {} }),
        session: session,
      )

      refute_nil response[:result]
      assert received[:modern]
      assert_equal({ name: "modern_client", version: "2.0" }, received[:client_info])
      assert_equal({ elicitation: {} }, received[:client_capabilities])
      assert_equal "2026-07-28", received[:protocol_version]
      # Per SEP-2575, servers MUST NOT infer client state from prior requests, so nothing is stored.
      assert_nil session.client
      refute_predicate session, :initialized?
    end

    test "#handle tools/call with an unsupported envelope version returns -32022" do
      server = Server.new(name: "modern_test", tools: [])
      called = false
      server.define_tool(name: "modern_tool") do
        called = true
        Tool::Response.new([{ type: "text", text: "ok" }])
      end

      response = server.handle(modern_request("tools/call", { name: "modern_tool" }, version: "2027-01-01"))

      refute called
      assert_equal ErrorCodes::UNSUPPORTED_PROTOCOL_VERSION, response.dig(:error, :code)
      assert_equal Configuration::SUPPORTED_MODERN_PROTOCOL_VERSIONS, response.dig(:error, :data, :supported)
      assert_equal "2027-01-01", response.dig(:error, :data, :requested)
    end

    test "#handle rejects a claimed but incomplete envelope with -32602 naming the missing key" do
      # A request carrying the `protocolVersion` claim key must be validated, never silently served as legacy:
      # the spec maps missing required envelope fields to Invalid params, and the TypeScript and Python classifiers
      # answer the same way.
      server = Server.new(name: "modern_test", tools: [])
      server.define_tool(name: "modern_tool") { Tool::Response.new([{ type: "text", text: "ok" }]) }

      response = server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: {
          name: "modern_tool",
          arguments: {},
          _meta: { "io.modelcontextprotocol/protocolVersion": "2026-07-28" },
        },
      })

      assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, response.dig(:error, :code)
      assert_includes response.dig(:error, :message), "io.modelcontextprotocol/clientCapabilities"
    end

    test "#handle serves an envelope without the optional clientInfo" do
      # `clientInfo` became optional after the SEP was finalized (spec PR #3002).
      server = Server.new(name: "modern_test", tools: [])
      received_context = nil
      server.define_tool(name: "modern_tool") do |server_context:|
        received_context = server_context
        Tool::Response.new([{ type: "text", text: "ok" }])
      end

      response = server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: {
          name: "modern_tool",
          arguments: {},
          _meta: {
            "io.modelcontextprotocol/protocolVersion": "2026-07-28",
            "io.modelcontextprotocol/clientCapabilities": { elicitation: {} },
          },
        },
      })

      refute_nil response[:result]
      assert_predicate received_context, :modern?
      assert_nil received_context.client_info
      assert_equal({ elicitation: {} }, received_context.client_capabilities)
    end

    test "#handle requires the envelope for requests on a modern-locked session" do
      server = Server.new(name: "modern_test", tools: [])
      server.define_tool(name: "modern_tool") { Tool::Response.new([{ type: "text", text: "ok" }]) }
      session = ServerSession.new(server: server, transport: mock, era: :modern)

      response = server.handle(
        { jsonrpc: "2.0", method: "tools/call", id: 1, params: { name: "modern_tool" } },
        session: session,
      )

      assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, response.dig(:error, :code)
      assert_includes response.dig(:error, :message), "io.modelcontextprotocol/protocolVersion"
    end

    test "#handle rejects removed lifecycle methods on a modern-locked session with -32601" do
      session = ServerSession.new(server: @server, transport: mock, era: :modern)

      Methods::MODERN_REMOVED_METHODS.each_with_index do |method, index|
        response = @server.handle(
          { jsonrpc: "2.0", method: method, id: index + 1 },
          session: session,
        )

        assert_equal JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND, response.dig(:error, :code), "expected -32601 for #{method}"
        assert_equal index + 1, response[:id], "expected the request id to be preserved for #{method}"
      end
    end

    test "#handle rejects removed lifecycle methods carrying the envelope before the era locks" do
      # `StdioTransport` locks the era only after a response succeeds, so the first request of a connection
      # arrives with `era == nil`. The envelope on the request is what identifies it as modern there.
      Methods::MODERN_REMOVED_METHODS.each do |method|
        session = ServerSession.new(server: @server, transport: mock)

        response = @server.handle(modern_request(method, {}), session: session)

        assert_equal JsonRpcHandler::ErrorCode::METHOD_NOT_FOUND, response.dig(:error, :code), "expected -32601 for #{method}"
      end
    end

    test "#handle does not let an enveloped initialize lock the era before it is settled" do
      # Serving it would negotiate the legacy lifecycle for a request that declared the modern one,
      # pinning the connection to the wrong era for its whole lifetime.
      session = ServerSession.new(server: @server, transport: mock)

      @server.handle(modern_request("initialize", initialize_params), session: session)

      assert_nil session.era
      refute_predicate session, :initialized?
    end

    test "#handle serves an enveloped method the modern lifecycle keeps before the era locks" do
      session = ServerSession.new(server: @server, transport: mock)

      response = @server.handle(modern_request("tools/list", {}), session: session)

      refute_nil response[:result]
    end

    test "#handle counter-offers the latest handshake version for a modern initialize request and locks the legacy era" do
      # Per the SEP-2575 era model, the handshake never lands on a modern version: the client is
      # counter-offered 2025-11-25 and the connection proceeds on the legacy lifecycle it selected.
      session = ServerSession.new(server: @server, transport: mock)

      response = @server.handle(
        {
          jsonrpc: "2.0",
          method: "initialize",
          id: 1,
          params: initialize_params(protocolVersion: "2026-07-28"),
        },
        session: session,
      )

      assert_equal Configuration::LATEST_HANDSHAKE_PROTOCOL_VERSION, response.dig(:result, :protocolVersion)
      assert_equal :legacy, session.era
    end

    test "#handle server/discover requires the envelope on a modern-locked session" do
      # The 2026-07-28 conformance requirements reject an envelope-less `server/discover`
      # on the modern era with -32602 (SEP-2575).
      session = ServerSession.new(server: @server, transport: mock, era: :modern)

      response = @server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 }, session: session)

      assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, response.dig(:error, :code)
    end

    test "#handle server/discover succeeds without an envelope outside the modern era" do
      response = @server.handle({ jsonrpc: "2.0", method: "server/discover", id: 1 })

      refute_nil response[:result]
    end

    test "#handle tools/call enforces require_client_capability! from the envelope" do
      server = Server.new(name: "modern_test", tools: [])
      server.define_tool(name: "guarded_tool") do |server_context:|
        server_context.require_client_capability!(:elicitation, :form)
        Tool::Response.new([{ type: "text", text: "ok" }])
      end

      declared = server.handle(
        modern_request("tools/call", { name: "guarded_tool" }, capabilities: { elicitation: { form: {} } }),
      )
      undeclared = server.handle(modern_request("tools/call", { name: "guarded_tool" }))

      refute_nil declared[:result]
      assert_equal ErrorCodes::MISSING_REQUIRED_CLIENT_CAPABILITY, undeclared.dig(:error, :code)
      assert_equal(
        { elicitation: { form: {} } },
        undeclared.dig(:error, :data, :requiredCapabilities),
      )
    end

    test "#handle tools/call serializes an input_required result on a modern request" do
      server = Server.new(name: "mrtr_test", tools: [])
      server.define_tool(name: "mrtr_tool") do |server_context:|
        if server_context.input_responses
          Tool::Response.new([{ type: "text", text: "done" }])
        else
          Server::InputRequiredResult.new(
            input_requests: { region: { method: "elicitation/create", params: { message: "Which region?" } } },
            request_state: "state-1",
          )
        end
      end

      response = server.handle(
        modern_request("tools/call", { name: "mrtr_tool" }, capabilities: { elicitation: { form: {} } }),
      )
      result = response[:result]

      assert_equal "input_required", result[:resultType]
      assert_equal "state-1", result[:requestState]
      assert_equal(
        { method: "elicitation/create", params: { message: "Which region?" } },
        result.dig(:inputRequests, "region"),
      )
    end

    test "#handle tools/call retry leg exposes input responses and request state to the handler" do
      server = Server.new(name: "mrtr_test", tools: [])
      seen = nil
      server.define_tool(name: "mrtr_tool") do |server_context:|
        seen = {
          input_responses: server_context.input_responses,
          request_state: server_context.request_state,
          region: server_context.input_response(:region),
        }
        Tool::Response.new([{ type: "text", text: "done" }])
      end

      request = modern_request("tools/call", {
        name: "mrtr_tool",
        arguments: {},
        inputResponses: { region: { action: "accept", content: { value: "us-east-1" } } },
        requestState: "state-1",
      })
      response = server.handle(request)

      refute_nil response[:result]
      assert_equal "state-1", seen[:request_state]
      assert_equal({ action: "accept", content: { value: "us-east-1" } }, seen[:region])
      assert_equal seen[:region], seen[:input_responses][:region]
    end

    test "#handle rejects an input_required result on a legacy request with an internal error" do
      # The result type exists only in the 2026-07-28 lifecycle: pre-2026 clients treat
      # an unknown resultType as a final result, so it must never reach them.
      server = Server.new(name: "mrtr_test", tools: [])
      server.define_tool(name: "mrtr_tool") do
        Server::InputRequiredResult.new(request_state: "state-1")
      end

      response = server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: { name: "mrtr_tool" },
      })

      assert_equal JsonRpcHandler::ErrorCode::INTERNAL_ERROR, response.dig(:error, :code)
    end

    test "#handle rejects an input_required result whose embedded requests exceed declared capabilities" do
      server = Server.new(name: "mrtr_test", tools: [])
      server.define_tool(name: "mrtr_tool") do
        Server::InputRequiredResult.new(input_requests: {
          region: { method: "elicitation/create", params: { message: "?" } },
          roots: { method: "roots/list" },
        })
      end

      response = server.handle(modern_request("tools/call", { name: "mrtr_tool" }, capabilities: {}))

      assert_equal ErrorCodes::MISSING_REQUIRED_CLIENT_CAPABILITY, response.dig(:error, :code)
      assert_equal(
        { elicitation: { form: {} }, roots: {} },
        response.dig(:error, :data, :requiredCapabilities),
      )
    end

    test "#handle tools/call input_required bypasses output schema validation" do
      configuration = Configuration.new(validate_tool_call_results: true)
      server = Server.new(name: "mrtr_test", tools: [], configuration: configuration)
      server.define_tool(name: "mrtr_tool", output_schema: { properties: { value: { type: "string" } }, required: ["value"] }) do
        Server::InputRequiredResult.new(request_state: "state-1")
      end

      response = server.handle(modern_request("tools/call", { name: "mrtr_tool" }))

      assert_equal "input_required", response.dig(:result, :resultType)
    end

    test "#handle seals and unseals requestState transparently when request_state_security is set" do
      security = Server::RequestStateSecurity.new(key: "k" * 32)
      server = Server.new(name: "mrtr_test", tools: [], request_state_security: security)
      seen_state = nil
      server.define_tool(name: "mrtr_tool") do |server_context:|
        if server_context.request_state
          seen_state = server_context.request_state
          Tool::Response.new([{ type: "text", text: "done" }])
        else
          Server::InputRequiredResult.new(request_state: "plain-state")
        end
      end

      first = server.handle(modern_request("tools/call", { name: "mrtr_tool", arguments: {} }))
      sealed = first.dig(:result, :requestState)

      assert sealed.start_with?("v1.")
      refute_equal "plain-state", sealed

      retry_response = server.handle(modern_request("tools/call", {
        name: "mrtr_tool",
        arguments: {},
        requestState: sealed,
      }))

      refute_nil retry_response[:result]
      # The handler reads the plaintext it originally wrote.
      assert_equal "plain-state", seen_state
    end

    test "#handle rejects a tampered or cross-call requestState echo with -32602" do
      security = Server::RequestStateSecurity.new(key: "k" * 32)
      server = Server.new(name: "mrtr_test", tools: [], request_state_security: security)
      server.define_tool(name: "mrtr_tool") do |server_context:|
        if server_context.request_state
          Tool::Response.new([{ type: "text", text: "done" }])
        else
          Server::InputRequiredResult.new(request_state: "plain-state")
        end
      end

      first = server.handle(modern_request("tools/call", { name: "mrtr_tool", arguments: {} }))
      sealed = first.dig(:result, :requestState)

      tampered = server.handle(modern_request("tools/call", {
        name: "mrtr_tool",
        arguments: {},
        requestState: sealed.sub("v1.", "v1.x"),
      }))
      # Different arguments than the sealing call: the digest claim no longer matches.
      cross_call = server.handle(modern_request("tools/call", {
        name: "mrtr_tool",
        arguments: { other: true },
        requestState: sealed,
      }))

      assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, tampered.dig(:error, :code)
      assert_equal "Invalid or expired requestState", tampered.dig(:error, :message)
      assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, cross_call.dig(:error, :code)
    end

    test "#handle passes requestState through untouched without request_state_security" do
      server = Server.new(name: "mrtr_test", tools: [])
      seen_state = nil
      server.define_tool(name: "mrtr_tool") do |server_context:|
        seen_state = server_context.request_state
        Tool::Response.new([{ type: "text", text: "done" }])
      end

      server.handle(modern_request("tools/call", { name: "mrtr_tool", requestState: "raw-state" }))

      assert_equal "raw-state", seen_state
    end

    test "#handle prompts/get serializes an input_required result on a modern request" do
      server = Server.new(name: "mrtr_test")
      server.define_prompt(name: "mrtr_prompt", arguments: []) do |_args, server_context:|
        server_context.request_state # participates via server_context:
        Server::InputRequiredResult.new(request_state: "prompt-state")
      end

      response = server.handle(modern_request("prompts/get", { name: "mrtr_prompt", arguments: {} }))

      assert_equal "input_required", response.dig(:result, :resultType)
      assert_equal "prompt-state", response.dig(:result, :requestState)
    end

    test "#handle resources/read passes an input_required result through unwrapped and without cache hints" do
      server = Server.new(name: "mrtr_test", ttl_ms: 5000, cache_scope: "public")
      server.resources_read_handler do |_params, server_context:|
        server_context.request_state
        Server::InputRequiredResult.new(request_state: "resource-state")
      end

      response = server.handle(modern_request("resources/read", { uri: "file:///pending.txt" }))
      result = response[:result]

      assert_equal "input_required", result[:resultType]
      assert_equal "resource-state", result[:requestState]
      refute result.key?(:contents)
      refute result.key?(:ttlMs)
      refute result.key?(:cacheScope)
    end

    test "ServerSession locks the legacy era on mark_initialized! and refuses to flip eras" do
      session = ServerSession.new(server: @server, transport: mock)

      assert_nil session.era
      session.mark_initialized!
      assert_equal :legacy, session.era

      session.lock_era!(:legacy)
      assert_equal :legacy, session.era
      assert_raises(RuntimeError) { session.lock_era!(:modern) }
    end

    test "ServerSession accepts a preset era and validates era values" do
      session = ServerSession.new(server: @server, transport: mock, era: :modern)

      assert_equal :modern, session.era
      assert_raises(ArgumentError) { ServerSession.new(server: @server, transport: mock, era: :future) }
      assert_raises(ArgumentError) { session.lock_era!(:future) }
    end

    test "#handle initialize request returns protocol info, server info, and capabilities" do
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params,
      }

      response = @server.handle(request)
      refute_nil response

      expected_result = {
        jsonrpc: "2.0",
        id: 1,
        result: {
          protocolVersion: Configuration::LATEST_HANDSHAKE_PROTOCOL_VERSION,
          capabilities: {
            prompts: { listChanged: true },
            resources: { listChanged: true },
            tools: { listChanged: true },
            logging: {},
          },
          serverInfo: {
            description: "Test server",
            icons: [{ mimeType: "image/png", sizes: ["48x48", "96x96"], src: "https://example.com", theme: "light" }],
            name: @server_name,
            title: "Example Server Display Name",
            version: "1.2.3",
          },
          instructions: "Optional instructions for the client",
        },
      }

      assert_equal expected_result, response
      assert_instrumentation_data({ method: "initialize", client: { name: "test-client", version: "1.0.0" } })
    end

    test "#handle initialize result carries declared capability extensions" do
      server = Server.new(
        name: "extensions_test",
        capabilities: {
          tools: { listChanged: true },
          extensions: { "com.example/feature" => { enabled: true } },
        },
      )

      response = server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })

      assert_equal(
        { "com.example/feature" => { enabled: true } },
        response.dig(:result, :capabilities, :extensions),
      )
    end

    test "Server.new accepts an MCP::Server::Capabilities instance" do
      capabilities = Server::Capabilities.new
      capabilities.support_tools
      capabilities.support_extensions("io.modelcontextprotocol/tasks" => {})

      server = Server.new(name: "extensions_test", capabilities: capabilities)
      response = server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })

      assert_equal(
        {
          tools: {},
          extensions: { "io.modelcontextprotocol/tasks" => {} },
        },
        response.dig(:result, :capabilities),
      )
    end

    test "client-declared capability extensions are readable via client_capabilities" do
      extensions = { "com.example/feature": { enabled: true } }
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(capabilities: { extensions: extensions }),
      }

      @server.handle(request)

      assert_equal extensions, @server.client_capabilities[:extensions]
    end

    test "client-declared capability extensions are readable via the session" do
      session = ServerSession.new(server: @server, transport: mock)
      extensions = { "com.example/feature": {} }
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(capabilities: { extensions: extensions }),
      }

      @server.handle(request, session: session)

      assert_equal extensions, session.client_capabilities[:extensions]
    end

    test "#handle initialize request with clientInfo includes client in instrumentation data" do
      client_info = { name: "test_client", version: "1.0.0" }
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(clientInfo: client_info),
      }

      @server.handle(request)
      assert_instrumentation_data({ method: "initialize", client: client_info })
    end

    test "instrumentation data includes client info for subsequent requests after initialize" do
      client_info = { name: "test_client", version: "1.0.0" }
      initialize_request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(clientInfo: client_info),
      }
      @server.handle(initialize_request)

      ping_request = {
        jsonrpc: "2.0",
        method: "ping",
        id: 2,
      }
      @server.handle(ping_request)
      assert_instrumentation_data({ method: "ping", client: client_info })
    end

    test "#handle rejects duplicate initialize on an already-initialized session with -32600" do
      session = ServerSession.new(server: @server, transport: mock)

      first_request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(clientInfo: { name: "original", version: "1.0" }),
      }
      first_response = @server.handle(first_request, session: session)
      refute_nil first_response[:result]

      second_request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 2,
        params: initialize_params(clientInfo: { name: "intruder", version: "9.9" }, protocolVersion: "2024-11-05"),
      }
      second_response = @server.handle(second_request, session: session)

      assert_equal JsonRpcHandler::ErrorCode::INVALID_REQUEST, second_response[:error][:code]
      assert_equal "Invalid Request", second_response[:error][:message]
      assert_equal({ name: "original", version: "1.0" }, session.client)
    end

    test "#handle initialize with empty params returns -32602 and does not initialize the session" do
      session = ServerSession.new(server: @server, transport: mock)

      response = @server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: {} }, session: session)

      assert_equal(-32602, response[:error][:code])
      assert_equal "Invalid params", response[:error][:message]
      refute_predicate session, :initialized?
    end

    test "#handle initialize without params returns -32602" do
      response = @server.handle({ jsonrpc: "2.0", method: "initialize", id: 1 })

      assert_equal(-32602, response[:error][:code])
      assert_equal "Invalid params", response[:error][:message]
    end

    test "#handle initialize with a missing required field returns -32602" do
      [:protocolVersion, :capabilities, :clientInfo].each do |field|
        params = initialize_params.tap { |hash| hash.delete(field) }

        response = @server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: params })

        assert_equal(-32602, response[:error][:code], "expected -32602 when #{field} is missing")
        assert_includes response[:error][:data], field.to_s
      end
    end

    test "#handle initialize with clientInfo missing name or version returns -32602" do
      [{ name: "client-without-version" }, { version: "1.0.0" }].each do |client_info|
        response = @server.handle({
          jsonrpc: "2.0",
          method: "initialize",
          id: 1,
          params: initialize_params(clientInfo: client_info),
        })

        assert_equal(-32602, response[:error][:code])
        assert_includes response[:error][:data], "clientInfo"
      end
    end

    test "#handle initialize with an unsupported protocolVersion still negotiates the fallback version" do
      response = @server.handle({
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(protocolVersion: "1999-01-01"),
      })

      assert_equal Configuration::LATEST_HANDSHAKE_PROTOCOL_VERSION, response[:result][:protocolVersion]
    end

    test "instrumentation data does not include client key when no clientInfo provided" do
      request = {
        jsonrpc: "2.0",
        method: "ping",
        id: 1,
      }

      @server.handle(request)
      assert_instrumentation_data({ method: "ping" })
    end

    test "unsupported method instrumentation includes client from session" do
      session = ServerSession.new(server: @server, transport: mock)
      session.store_client_info(client: { name: "session-client", version: "1.0" })

      request = {
        jsonrpc: "2.0",
        method: "does/not/exist",
        id: 1,
      }

      @server.handle(request, session: session)
      assert_instrumentation_data({ method: "unsupported_method", client: { name: "session-client", version: "1.0" } })
    end

    test "#handle returns nil for notification requests" do
      request = {
        jsonrpc: "2.0",
        method: "some_notification",
      }

      assert_nil @server.handle(request)
      assert_instrumentation_data({ method: "unsupported_method" })
    end

    test "#handle notifications/initialized returns nil response" do
      request = {
        jsonrpc: "2.0",
        method: "notifications/initialized",
      }

      assert_nil @server.handle(request)
      assert_instrumentation_data({ method: "notifications/initialized" })
    end

    test "#handle_json notifications/initialized returns nil response" do
      request = JSON.generate({
        jsonrpc: "2.0",
        method: "notifications/initialized",
      })

      assert_nil @server.handle_json(request)
      assert_instrumentation_data({ method: "notifications/initialized" })
    end

    test "#handle tools/list returns available tools" do
      request = {
        jsonrpc: "2.0",
        method: "tools/list",
        id: 1,
      }

      response = @server.handle(request)
      result = response[:result]
      assert_kind_of Array, result[:tools]
      assert_equal "test_tool", result[:tools][0][:name]
      assert_equal "Test tool", result[:tools][0][:title]
      assert_equal "A test tool", result[:tools][0][:description]
      assert_equal(
        { "$schema": "https://json-schema.org/draft/2020-12/schema", type: "object" }, result[:tools][0][:inputSchema]
      )
      assert_equal({ foo: "bar" }, result[:tools][0][:_meta])
      assert_instrumentation_data({ method: "tools/list" })
    end

    test "#handle_json tools/list returns available tools" do
      request = JSON.generate({
        jsonrpc: "2.0",
        method: "tools/list",
        id: 1,
      })

      response = JSON.parse(@server.handle_json(request), symbolize_names: true)
      result = response[:result]
      assert_kind_of Array, result[:tools]
      assert_equal "test_tool", result[:tools][0][:name]
      assert_equal "Test tool", result[:tools][0][:title]
      assert_equal "A test tool", result[:tools][0][:description]
      assert_equal({ foo: "bar" }, result[:tools][0][:_meta])
    end

    test "#handle tools/list emits 2020-12 $schema on inputSchema and outputSchema" do
      tool_with_output = Tool.define(
        name: "tool_with_output",
        description: "tool with output schema",
        input_schema: { properties: { msg: { type: "string" } } },
        output_schema: { properties: { result: { type: "string" } } },
      ) do
        Tool::Response.new([{ type: "text", content: "OK" }])
      end
      server = Server.new(name: "test_server", tools: [tool_with_output])

      response = server.handle({ jsonrpc: "2.0", method: "tools/list", id: 1 })
      tool = response[:result][:tools][0]

      assert_equal "https://json-schema.org/draft/2020-12/schema", tool[:inputSchema][:"$schema"]
      assert_equal "https://json-schema.org/draft/2020-12/schema", tool[:outputSchema][:"$schema"]
    end

    test "#handle tools/call executes tool and returns result" do
      tool_name = "test_tool"
      tool_args = { arg: "value" }
      tool_response = Tool::Response.new([{ result: "success" }])

      if RUBY_VERSION >= "3.1"
        # Ruby 3.1+: Mocha stub preserves `method.parameters` info.
        @tool.expects(:call).with(arg: "value", server_context: is_a(ServerContext)).returns(tool_response)
      else
        # Ruby 3.0: Mocha stub changes `method.parameters`, so `accepts_server_context?` returns false.
        @tool.expects(:call).with(arg: "value").returns(tool_response)
      end

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: {
          name: tool_name,
          arguments: tool_args,
        },
        id: 1,
      }

      response = @server.handle(request)
      assert_equal tool_response.to_h, response[:result]
      assert_instrumentation_data({ method: "tools/call", tool_name: tool_name, tool_arguments: tool_args })
    end

    test "#handle_json tools/call delivers nested object arguments with symbol keys at every level" do
      received_payload = nil
      server = Server.new(name: "test_server")
      server.define_tool(
        name: "nested_args_tool",
        input_schema: { properties: { message: { type: "string" }, payload: { type: "object" } }, required: ["message"] },
      ) do |message:, payload: nil, server_context:|
        received_payload = payload
        Tool::Response.new([{ type: "text", text: "#{message} #{server_context.class}" }])
      end

      request_json = JSON.generate(
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: {
          name: "nested_args_tool",
          arguments: { message: "hi", payload: { subject: "greet", nested: { deep: "value" } } },
        },
      )

      server.handle_json(request_json)

      assert_equal({ subject: "greet", nested: { deep: "value" } }, received_payload)
      assert_equal "greet", received_payload[:subject]
      assert_nil received_payload["subject"]
    end

    test "tool receives symbol keys when called under the JSON-round-tripped argument shape" do
      received_payload = nil
      tool = Tool.define(
        name: "nested_args_tool",
        input_schema: { properties: { payload: { type: "object" } } },
      ) do |payload: nil, server_context:|
        received_payload = payload
        Tool::Response.new([{ type: "text", text: server_context.class.to_s }])
      end

      # Round-trip the arguments through JSON the way a transport does, so the tool
      # is exercised under the symbolized shape it actually receives at runtime.
      arguments = { payload: { "subject" => "greet" } }
      delivered = JSON.parse(JSON.generate(arguments), symbolize_names: true)
      tool.call(**delivered, server_context: nil)

      assert_equal({ subject: "greet" }, received_payload)
      assert_nil received_payload["subject"]
    end

    test "#handle tools/call returns tool execution error if required tool arguments are missing" do
      tool_with_required_argument = Tool.define(
        name: "test_tool",
        title: "Test tool",
        description: "A test tool",
        input_schema: { properties: { message: { type: "string" } }, required: ["message"] },
      ) do |message: nil|
        Tool::Response.new("success #{message}")
      end

      server = Server.new(
        name: "test_server",
        tools: [tool_with_required_argument],
      )

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "test_tool", arguments: {} },
        id: 1,
      }

      response = server.handle(request)

      assert_nil response[:error]
      assert(response[:result][:isError])
      assert_equal "text", response[:result][:content][0][:type]
      assert_includes response[:result][:content][0][:text], "Missing required arguments: message"
    end

    test "#handle_json tools/call executes tool and returns result" do
      tool_name = "test_tool"
      tool_args = { arg: "value" }
      tool_response = Tool::Response.new([{ result: "success" }])

      if RUBY_VERSION >= "3.1"
        # Ruby 3.1+: Mocha stub preserves `method.parameters` info.
        @tool.expects(:call).with(arg: "value", server_context: is_a(ServerContext)).returns(tool_response)
      else
        # Ruby 3.0: Mocha stub changes `method.parameters`, so `accepts_server_context?` returns false.
        @tool.expects(:call).with(arg: "value").returns(tool_response)
      end

      request = JSON.generate({
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: tool_name, arguments: tool_args },
        id: 1,
      })

      raw_response = @server.handle_json(request)
      response = JSON.parse(raw_response, symbolize_names: true) if raw_response
      assert_equal tool_response.to_h, response[:result] if response
      assert_instrumentation_data({ method: "tools/call", tool_name: tool_name, tool_arguments: { arg: "value" } })
    end

    test "#handle_json tools/call executes tool and returns result, when the tool is typed with Sorbet" do
      skip "Sorbet is not available" unless defined?(T::Sig)

      class TypedTestTool < Tool
        tool_name "test_tool"
        description "a test tool for testing"
        input_schema({ properties: { message: { type: "string" } }, required: ["message"] })

        class << self
          extend T::Sig

          sig { params(message: String, server_context: T.nilable(T.untyped)).returns(Tool::Response) }
          def call(message:, server_context: nil)
            Tool::Response.new([{ type: "text", content: "OK" }])
          end
        end
      end

      request = JSON.generate({
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "test_tool", arguments: { message: "Hello, world!" } },
        id: 1,
      })

      server = Server.new(
        name: @server_name,
        tools: [TypedTestTool],
        prompts: [@prompt],
        resources: [@resource],
        resource_templates: [@resource_template],
      )

      raw_response = server.handle_json(request)
      response = JSON.parse(raw_response, symbolize_names: true) if raw_response

      assert_equal({ content: [{ type: "text", content: "OK" }], isError: false }, response[:result])
    end

    test "#handle tools/call returns protocol error in JSON-RPC format if the tool raises an uncaught exception" do
      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: {
          name: "tool_that_raises",
          arguments: { message: "test" },
        },
        id: 1,
      }

      @server.configuration.exception_reporter.expects(:call).with do |exception, server_context|
        refute_kind_of MCP::Server::RequestHandlerError, exception
        assert_equal({ request: request }, server_context)
      end

      response = @server.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_that_raises", response[:error][:data]
      assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" }, error: :internal_error })
    end

    test "registers tools with the same class name in different namespaces" do
      module Foo
        class Example < Tool
        end
      end

      module Bar
        class Example < Tool
        end
      end

      error = assert_raises(MCP::ToolNotUnique) { Server.new(tools: [Foo::Example, Bar::Example]) }
      assert_equal(<<~MESSAGE, error.message)
        Tool names should be unique. Use `tool_name` to assign unique names to:
        example
      MESSAGE
    end

    test "registers tools with the same tool name" do
      module Baz
        class Example < Tool
          tool_name "foo"
        end
      end

      module Qux
        class Example < Tool
          tool_name "foo"
        end
      end

      error = assert_raises(MCP::ToolNotUnique) { Server.new(tools: [Baz::Example, Qux::Example]) }
      assert_equal(<<~MESSAGE, error.message)
        Tool names should be unique. Use `tool_name` to assign unique names to:
        foo
      MESSAGE
    end

    test "#handle_json returns protocol error in JSON-RPC format if the tool raises an uncaught exception" do
      request = JSON.generate({
        jsonrpc: "2.0",
        method: "tools/call",
        params: {
          name: "tool_that_raises",
          arguments: { message: "test" },
        },
        id: 1,
      })

      response = JSON.parse(@server.handle_json(request), symbolize_names: true)
      assert_nil response[:result]
      assert_equal(-32603, response[:error][:code])
      assert_equal "Internal error", response[:error][:message]
      assert_equal "Internal error calling tool tool_that_raises", response[:error][:data]
      assert_instrumentation_data({ method: "tools/call", tool_name: "tool_that_raises", tool_arguments: { message: "test" }, error: :internal_error })
    end

    test "#handle tools/call returns protocol error in JSON-RPC format if input_schema raises an error during validation" do
      tool = Tool.define(
        name: "tool_with_faulty_schema",
        title: "Tool with faulty schema",
        description: "A tool with a faulty schema",
        input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] },
      ) { Tool::Response.new("success") }

      tool.input_schema.expects(:missing_required_arguments?).raises(RuntimeError, "Unexpected schema error")

      server = Server.new(name: "test_server", tools: [tool])

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: {
          name: "tool_with_faulty_schema",
          arguments: { message: "test" },
        },
        id: 1,
      }

      response = server.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_faulty_schema", response[:error][:data]
    end

    test "#handle tools/call returns JSON-RPC error for unknown tool" do
      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: {
          name: "unknown_tool",
          arguments: { message: "test" },
        },
        id: 1,
      }

      response = @server.handle(request)
      assert_nil response[:result]
      assert_equal(-32602, response[:error][:code])
      assert_equal "Invalid params", response[:error][:message]
      assert_includes response[:error][:data], "Tool not found: unknown_tool"
      assert_instrumentation_data({ method: "tools/call", tool_name: "unknown_tool", error: :invalid_params })
    end

    test "#handle_json returns JSON-RPC error for unknown tool" do
      request = JSON.generate({
        jsonrpc: "2.0",
        method: "tools/call",
        params: {
          name: "unknown_tool",
          arguments: {},
        },
        id: 1,
      })

      response = JSON.parse(@server.handle_json(request), symbolize_names: true)
      assert_nil response[:result]
      assert_equal(-32602, response[:error][:code])
      assert_equal "Invalid params", response[:error][:message]
      assert_includes response[:error][:data], "Tool not found: unknown_tool"
    end

    test "#handle prompts/list returns list of prompts" do
      request = {
        jsonrpc: "2.0",
        method: "prompts/list",
        id: 1,
      }

      response = @server.handle(request)
      assert_equal({ prompts: [@prompt.to_h] }, response[:result])
      assert_instrumentation_data({ method: "prompts/list" })
    end

    test "#handle prompts/get returns templated prompt" do
      request = {
        jsonrpc: "2.0",
        method: "prompts/get",
        id: 1,
        params: {
          name: "test_prompt",
          arguments: { test_argument: "Hello, friend!" },
        },
      }

      expected_result = {
        description: "Hello, world!",
        messages: [
          { role: "user", content: { text: "Hello, world!", type: "text" } },
        ],
      }

      response = @server.handle(request)
      assert_equal(expected_result, response[:result])
      assert_instrumentation_data({ method: "prompts/get", prompt_name: "test_prompt" })
    end

    test "#handle prompts/get returns error if prompt is not found" do
      request = {
        jsonrpc: "2.0",
        method: "prompts/get",
        id: 1,
        params: {
          name: "unknown_prompt",
          arguments: {},
        },
      }

      response = @server.handle(request)
      # An unknown prompt is client input, so it maps to Invalid Params (-32602), not the
      # default Internal Error (-32603); matches the tools/call and completion/complete siblings.
      assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, response.dig(:error, :code)
      assert_equal("Prompt not found unknown_prompt", response[:error][:data])
      assert_instrumentation_data({ method: "prompts/get", error: :prompt_not_found })
    end

    test "#handle prompts/get returns error if prompt arguments are invalid" do
      request = {
        jsonrpc: "2.0",
        method: "prompts/get",
        id: 1,
        params: {
          name: "test_prompt",
          arguments: { "unknown_argument" => "Hello, friend!" },
        },
      }

      response = @server.handle(request)
      # A missing required argument is client input, so it maps to Invalid Params (-32602),
      # not the default Internal Error (-32603).
      assert_equal JsonRpcHandler::ErrorCode::INVALID_PARAMS, response.dig(:error, :code)
      assert_equal "Missing required arguments: test_argument", response[:error][:data]
      assert_instrumentation_data({
        method: "prompts/get",
        prompt_name: "test_prompt",
        error: :missing_required_arguments,
      })
    end

    test "#handle resources/list returns a list of resources" do
      request = {
        jsonrpc: "2.0",
        method: "resources/list",
        id: 1,
      }

      response = @server.handle(request)
      assert_equal({ resources: [@resource.to_h] }, response[:result])
      assert_instrumentation_data({ method: "resources/list" })
    end

    test "#resources_list_handler replaces the served resource collection" do
      other = Resource.new(uri: "https://other.invalid", name: "other", mime_type: "text/plain")
      @server.resources_list_handler { |_params| [other] }

      response = @server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })

      assert_equal({ resources: [other.to_h] }, response[:result])
    end

    test "#resources_list_handler receives server_context when it opts in" do
      real = Resource.new(uri: "https://real.invalid", name: "real", mime_type: "text/plain")
      demo = Resource.new(uri: "https://demo.invalid", name: "demo", mime_type: "text/plain")
      @server.resources_list_handler do |_params, server_context:|
        server_context[:authenticated] ? [real] : [demo]
      end

      @server.server_context = { authenticated: false }
      anon = @server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })
      @server.server_context = { authenticated: true }
      authed = @server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })

      assert_equal({ resources: [demo.to_h] }, anon[:result])
      assert_equal({ resources: [real.to_h] }, authed[:result])
    end

    test "#resources_list_handler paginates and stamps cache hints on the returned collection" do
      first = Resource.new(uri: "https://first.invalid", name: "first", mime_type: "text/plain")
      second = Resource.new(uri: "https://second.invalid", name: "second", mime_type: "text/plain")
      server = Server.new(name: @server_name, resources: [], page_size: 1, ttl_ms: 60_000)
      server.resources_list_handler { |_params| [first, second] }

      response = server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })

      assert_equal([first.to_h], response[:result][:resources])
      assert_equal("1", response[:result][:nextCursor])
      assert_equal(60_000, response[:result][:ttlMs])
    end

    test "#resources_list_handler serves a server with no constructor-provided resources" do
      only = Resource.new(uri: "https://only.invalid", name: "only", mime_type: "text/plain")
      server = Server.new(name: @server_name, resources: [])
      server.resources_list_handler { |_params| [only] }

      response = server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })

      assert_equal({ resources: [only.to_h] }, response[:result])
    end

    test "#handle resources/read returns an empty array of contents by default" do
      request = {
        jsonrpc: "2.0",
        method: "resources/read",
        id: 1,
        params: {
          uri: "https://test_resource.invalid",
        },
      }

      response = @server.handle(request)
      assert_equal({ contents: [] }, response[:result])
      assert_instrumentation_data({ method: "resources/read", resource_uri: "https://test_resource.invalid" })
    end

    test "#resources_read_handler sets the resources/read handler" do
      @server.resources_read_handler do |request|
        {
          uri: request[:uri],
          mimeType: "text/plain",
          text: "Lorem ipsum dolor sit amet",
        }
      end

      request = {
        jsonrpc: "2.0",
        method: "resources/read",
        id: 1,
        params: {
          uri: "https://test_resource.invalid/my_resource",
        },
      }

      response = @server.handle(request)
      assert_equal(
        { contents: { uri: "https://test_resource.invalid/my_resource", mimeType: "text/plain", text: "Lorem ipsum dolor sit amet" } },
        response[:result],
      )
    end

    test "#handle resources/read returns -32602 with the uri in error data when the handler raises ResourceNotFoundError" do
      # Per SEP-2164, resource-not-found errors use the standard JSON-RPC Invalid Params code (-32602)
      # and carry the requested URI in `data`.
      @server.resources_read_handler do |request|
        raise Server::ResourceNotFoundError.new(request[:uri], request)
      end

      response = @server.handle({
        jsonrpc: "2.0",
        method: "resources/read",
        id: 1,
        params: { uri: "file:///missing.txt" },
      })

      assert_equal(-32602, response[:error][:code])
      assert_equal("Resource not found: file:///missing.txt", response[:error][:message])
      assert_equal({ uri: "file:///missing.txt" }, response[:error][:data])
    end

    test "#handle resources/templates/list returns a list of resource templates" do
      request = {
        jsonrpc: "2.0",
        method: "resources/templates/list",
        id: 1,
      }

      response = @server.handle(request)
      assert_equal(
        {
          resourceTemplates: [@resource_template.to_h],
        },
        response[:result],
      )
      assert_instrumentation_data({ method: "resources/templates/list" })
    end

    class GreetingResource < Resource
      uri "greeting://hello"
      resource_name "greeting"
      mime_type "text/plain"

      class << self
        def contents
          [Resource::TextContents.new(uri: uri, mime_type: mime_type, text: "hello")]
        end
      end
    end

    class UserProfileTemplate < ResourceTemplate
      uri_template "users://{user_id}/profile"
      resource_template_name "user_profile"
      mime_type "text/plain"

      class << self
        def contents(user_id:)
          [Resource::TextContents.new(uri: "users://#{user_id}/profile", mime_type: mime_type, text: "profile of #{user_id}")]
        end
      end
    end

    def read_resource_request(uri)
      {
        jsonrpc: "2.0",
        method: "resources/read",
        id: 1,
        params: { uri: uri },
      }
    end

    test "#handle resources/read auto-routes to a class-based resource by exact URI" do
      configuration = MCP::Configuration.new
      configuration.instrumentation_callback = instrumentation_helper.callback
      server = Server.new(name: "test_server", resources: [GreetingResource], configuration: configuration)

      response = server.handle(read_resource_request("greeting://hello"))

      expected = [{ uri: "greeting://hello", mimeType: "text/plain", text: "hello" }]
      assert_equal({ contents: expected }, response[:result])
      assert_instrumentation_data({ method: "resources/read", resource_uri: "greeting://hello" })
    end

    test "#handle resources/read wraps a single contents object returned by a class-based resource" do
      resource = Resource.define(uri: "single://one", name: "single") do
        Resource::TextContents.new(uri: "single://one", mime_type: "text/plain", text: "only one")
      end
      server = Server.new(name: "test_server", resources: [resource])

      response = server.handle(read_resource_request("single://one"))

      expected = [{ uri: "single://one", mimeType: "text/plain", text: "only one" }]
      assert_equal({ contents: expected }, response[:result])
    end

    test "#handle resources/read passes server_context to contents that opt in" do
      received = nil
      resource = Resource.define(uri: "ctx://resource", name: "ctx") do |server_context:|
        received = server_context
        [Resource::TextContents.new(uri: "ctx://resource", mime_type: "text/plain", text: "ctx")]
      end
      server = Server.new(name: "test_server", resources: [resource])

      server.handle(read_resource_request("ctx://resource"))

      assert_instance_of ServerContext, received
    end

    test "#handle resources/read auto-routes to a class-based resource template with extracted params" do
      configuration = MCP::Configuration.new
      configuration.instrumentation_callback = instrumentation_helper.callback
      server = Server.new(name: "test_server", resource_templates: [UserProfileTemplate], configuration: configuration)

      response = server.handle(read_resource_request("users://42/profile"))

      expected = [{ uri: "users://42/profile", mimeType: "text/plain", text: "profile of 42" }]
      assert_equal({ contents: expected }, response[:result])
      assert_instrumentation_data({
        method: "resources/read",
        resource_uri: "users://42/profile",
        resource_template: "users://{user_id}/profile",
      })
    end

    test "#handle resources/read prefers an exact resource match over a template match" do
      resource = Resource.define(uri: "users://42/profile", name: "pinned_profile") do
        [Resource::TextContents.new(uri: "users://42/profile", mime_type: "text/plain", text: "pinned")]
      end
      server = Server.new(name: "test_server", resources: [resource], resource_templates: [UserProfileTemplate])

      response = server.handle(read_resource_request("users://42/profile"))

      assert_equal "pinned", response[:result][:contents].first[:text]
    end

    test "#handle resources/read returns -32602 for an unknown URI when class-based resources are registered" do
      server = Server.new(name: "test_server", resources: [GreetingResource])

      response = server.handle(read_resource_request("greeting://unknown"))

      assert_equal(-32602, response[:error][:code])
      assert_equal("Resource not found: greeting://unknown", response[:error][:message])
      assert_equal({ uri: "greeting://unknown" }, response[:error][:data])
    end

    test "#handle resources/read returns -32602 for a non-matching URI when class-based templates are registered" do
      server = Server.new(name: "test_server", resource_templates: [UserProfileTemplate])

      response = server.handle(read_resource_request("users://42/settings"))

      assert_equal(-32602, response[:error][:code])
      assert_equal({ uri: "users://42/settings" }, response[:error][:data])
    end

    test "#handle resources/read keeps returning [] for unknown URIs when only instance-based resources are registered" do
      server = Server.new(name: "test_server", resources: [@resource], resource_templates: [@resource_template])

      response = server.handle(read_resource_request("unknown://resource"))

      assert_equal({ contents: [] }, response[:result])
    end

    test "#resources_read_handler overrides auto-routing for class-based resources" do
      server = Server.new(name: "test_server", resources: [GreetingResource])
      server.resources_read_handler do |request|
        [{ uri: request[:uri], mimeType: "text/plain", text: "handler wins" }]
      end

      response = server.handle(read_resource_request("greeting://hello"))

      assert_equal "handler wins", response[:result][:contents].first[:text]
    end

    test "#handle resources/list and resources/templates/list render class-based and instance-based entries together" do
      server = Server.new(
        name: "test_server",
        resources: [@resource, GreetingResource],
        resource_templates: [@resource_template, UserProfileTemplate],
      )

      list_response = server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })
      assert_equal [@resource.to_h, GreetingResource.to_h], list_response[:result][:resources]

      templates_response = server.handle({ jsonrpc: "2.0", method: "resources/templates/list", id: 1 })
      assert_equal [@resource_template.to_h, UserProfileTemplate.to_h], templates_response[:result][:resourceTemplates]
    end

    test "#resources= rebuilds the resource index used for auto-routing" do
      server = Server.new(name: "test_server", resources: [GreetingResource])

      replacement = Resource.define(uri: "replacement://res", name: "replacement") do
        [Resource::TextContents.new(uri: "replacement://res", mime_type: "text/plain", text: "replaced")]
      end
      server.resources = [replacement]

      response = server.handle(read_resource_request("replacement://res"))
      assert_equal "replaced", response[:result][:contents].first[:text]

      stale_response = server.handle(read_resource_request("greeting://hello"))
      assert_equal(-32602, stale_response[:error][:code])
    end

    test "#define_resource registers a resource served by auto-routing" do
      server = Server.new(name: "test_server")
      server.define_resource(uri: "defined://res", name: "defined", mime_type: "text/plain") do
        [Resource::TextContents.new(uri: "defined://res", mime_type: "text/plain", text: "defined body")]
      end

      list_response = server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })
      assert_equal ["defined"], list_response[:result][:resources].map { |resource| resource[:name] }

      response = server.handle(read_resource_request("defined://res"))
      assert_equal "defined body", response[:result][:contents].first[:text]
    end

    test "#define_resource_template registers a template served by auto-routing" do
      server = Server.new(name: "test_server")
      server.define_resource_template(uri_template: "items://{item_id}", name: "item_template") do |item_id:|
        [Resource::TextContents.new(uri: "items://#{item_id}", mime_type: "text/plain", text: "item #{item_id}")]
      end

      templates_response = server.handle({ jsonrpc: "2.0", method: "resources/templates/list", id: 1 })
      assert_equal ["item_template"], templates_response[:result][:resourceTemplates].map { |template| template[:name] }

      response = server.handle(read_resource_request("items://7"))
      assert_equal "item 7", response[:result][:contents].first[:text]
    end

    test "#configure_logging_level returns empty hash on success" do
      response = @server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "logging/setLevel",
          params: {
            level: "info",
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert_equal({}, response[:result])
      refute response.key?(:error)
    end

    test "#configure_logging_level does not warn after a modern initialize request is counter-offered" do
      # `initialize` asking for 2026-07-28 lands on 2025-11-25, where logging is not deprecated,
      # so no warning fires. On the modern lifecycle itself `logging/setLevel` is a removed method
      # and never reaches this handler.
      server = Server.new(tools: [TestTool])
      server.handle(
        {
          jsonrpc: "2.0",
          method: "initialize",
          id: 1,
          params: {
            protocolVersion: "2026-07-28",
            capabilities: {},
            clientInfo: { name: "test-client", version: "1.0" },
          },
        },
      )

      response = nil
      assert_no_deprecation_warning do
        response = server.handle(
          {
            jsonrpc: "2.0",
            id: 2,
            method: "logging/setLevel",
            params: {
              level: "info",
            },
          },
        )
      end

      assert_empty response[:result]
    end

    test "#configure_logging_level does not warn when negotiated protocol version is older" do
      server = Server.new(tools: [TestTool])
      server.handle(
        {
          jsonrpc: "2.0",
          method: "initialize",
          id: 1,
          params: {
            protocolVersion: "2025-11-25",
            capabilities: {},
            clientInfo: { name: "test-client", version: "1.0" },
          },
        },
      )

      assert_no_deprecation_warning do
        server.handle(
          {
            jsonrpc: "2.0",
            id: 2,
            method: "logging/setLevel",
            params: {
              level: "info",
            },
          },
        )
      end
    end

    test "#configure_logging_level returns an error object when invalid log level is provided" do
      server = Server.new(
        tools: [TestTool],
        configuration: Configuration.new(validate_tool_call_arguments: true),
      )

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "logging/setLevel",
          params: {
            level: "invalid_level",
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert_equal(-32602, response[:error][:code])
      assert_includes response[:error][:data], "Invalid log level invalid_level"
    end

    test "#configure_logging_level returns an error object when server has not logging capability" do
      server = Server.new(
        tools: [TestTool],
        configuration: Configuration.new(validate_tool_call_arguments: true),
        capabilities: {
          tools: { listChanged: true },
          prompts: { listChanged: true },
          resources: { listChanged: true },
        },
      )

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "logging/setLevel",
          params: {
            level: "debug",
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert_equal(-32603, response[:error][:code])
      assert_includes response[:error][:data], "Server does not support logging"
    end

    test "#handle method with missing required top-level capability returns an error" do
      @server.capabilities = {}

      response = @server.handle({ jsonrpc: "2.0", method: "prompts/list", id: 1 })
      assert_equal "Server does not support prompts (required for prompts/list)", response[:error][:data]

      response = @server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })
      assert_equal "Server does not support resources (required for resources/list)", response[:error][:data]
    end

    test "#handle method with missing required nested capability returns an error" do
      @server.capabilities = { resources: {} }
      response = @server.handle({ jsonrpc: "2.0", method: "resources/subscribe", id: 1 })
      assert_equal "Server does not support resources.subscribe (required for resources/subscribe)",
        response[:error][:data]
    end

    test "#handle unknown method returns method not found error" do
      request = {
        jsonrpc: "2.0",
        id: 1,
        method: "unknown_method",
      }

      response = @server.handle(request)

      assert_equal "Method not found", response[:error][:message]
      assert_equal "unknown_method", response[:error][:data]
      assert_instrumentation_data({ method: "unsupported_method" })
    end

    test "#handle handles custom methods" do
      @server.define_custom_method(method_name: "add") do |params|
        params[:a] + params[:b]
      end

      request = {
        jsonrpc: "2.0",
        id: 1,
        method: "add",
        params: { a: 1, b: 2 },
      }

      response = @server.handle(request)
      assert_equal 3, response[:result]
      assert_instrumentation_data({ method: "add" })
    end

    test "#handle handles custom notifications" do
      @server.define_custom_method(method_name: "notify") do
        nil
      end

      request = {
        jsonrpc: "2.0",
        method: "notify",
      }

      response = @server.handle(request)
      assert_nil response
      assert_instrumentation_data({ method: "notify" })
    end

    test "#handle tools/call invokes around_request with correct data" do
      call_log = []
      data_before = nil
      data_after = nil

      configuration = MCP::Configuration.new
      configuration.instrumentation_callback = instrumentation_helper.callback
      configuration.around_request = ->(data, &request_handler) {
        data_before = data.dup
        call_log << :before
        request_handler.call
        call_log << :after
        data_after = data.dup
      }

      tool = Tool.define(name: "around_test_tool", description: "Test") do |arg:|
        Tool::Response.new([{ type: "text", text: arg }])
      end

      server = Server.new(name: "test_server", tools: [tool], configuration: configuration)

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "around_test_tool", arguments: { arg: "hello" } },
        id: 1,
      }

      server.handle(request)

      assert_equal([:before, :after], call_log)
      assert_equal("tools/call", data_before[:method])
      assert_nil(data_before[:tool_name])
      assert_equal("around_test_tool", data_after[:tool_name])
      assert_equal({ arg: "hello" }, data_after[:tool_arguments])
    end

    test "#handle around_request and instrumentation_callback coexist" do
      around_called = false
      callback_data = nil

      configuration = MCP::Configuration.new
      configuration.around_request = ->(_data, &request_handler) {
        around_called = true
        request_handler.call
      }
      configuration.instrumentation_callback = ->(data) {
        callback_data = data.dup
      }

      server = Server.new(name: "test_server", configuration: configuration)

      request = {
        jsonrpc: "2.0",
        method: "ping",
        id: 1,
      }

      server.handle(request)

      assert(around_called)
      assert_equal("ping", callback_data[:method])
      assert(callback_data[:duration])
    end

    test "#handle reports exception and sets error when around_request raises" do
      reported_exception = nil
      reported_context = nil
      callback_data = nil

      configuration = MCP::Configuration.new
      configuration.exception_reporter = ->(e, server_context) {
        reported_exception = e
        reported_context = server_context
      }
      configuration.instrumentation_callback = ->(data) { callback_data = data.dup }
      configuration.around_request = ->(_data, &_request_handler) { raise "around_request failure" }

      server = Server.new(name: "test_server", configuration: configuration)

      request = {
        jsonrpc: "2.0",
        method: "ping",
        id: 1,
      }

      response = server.handle(request)

      assert_equal("around_request failure", reported_exception.message)
      assert_equal({ request: request }, reported_context)
      assert_equal(:internal_error, callback_data[:error])
      assert_equal(JsonRpcHandler::ErrorCode::INTERNAL_ERROR, response[:error][:code])
    end

    test "#handle does not double-report exception_reporter when a tool handler raises" do
      report_count = 0
      configuration = MCP::Configuration.new
      configuration.exception_reporter = ->(_e, _server_context) { report_count += 1 }

      failing_tool = Tool.define(name: "failing_tool", description: "Always fails") do
        raise "tool failure"
      end

      server = Server.new(name: "test_server", tools: [failing_tool], configuration: configuration)

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "failing_tool", arguments: {} },
        id: 1,
      }

      server.handle(request)

      assert_equal(1, report_count)
    end

    test "#handle reports both exceptions when around_request ensure raises after tool failure" do
      reported = []
      configuration = MCP::Configuration.new
      configuration.exception_reporter = ->(e, _server_context) { reported << e.message }
      configuration.around_request = ->(_data, &request_handler) do
        request_handler.call
      ensure
        raise "around ensure boom"
      end

      failing_tool = Tool.define(name: "failing_tool", description: "Always fails") do
        raise "tool failure"
      end

      server = Server.new(name: "test_server", tools: [failing_tool], configuration: configuration)

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "failing_tool", arguments: {} },
        id: 1,
      }

      response = server.handle(request)

      assert_equal(["tool failure", "around ensure boom"], reported)
      assert_equal(JsonRpcHandler::ErrorCode::INTERNAL_ERROR, response[:error][:code])
      assert_nil(response[:error][:data])
    end

    test "#handle reports the same exception object reused across requests on every call" do
      reported = []
      configuration = MCP::Configuration.new
      configuration.exception_reporter = ->(e, _server_context) { reported << e }

      shared_error = RuntimeError.new("reused")
      shared_tool = Tool.define(name: "shared_failing_tool", description: "Always fails") do
        raise shared_error
      end

      server = Server.new(name: "test_server", tools: [shared_tool], configuration: configuration)

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "shared_failing_tool", arguments: {} },
        id: 1,
      }

      server.handle(request)
      server.handle(request)

      assert_equal(2, reported.size)
      assert_same(shared_error, reported[0])
      assert_same(shared_error, reported[1])
    end

    test "#handle reports frozen exceptions raised by tool handlers without wrapping them" do
      reported = []
      configuration = MCP::Configuration.new
      configuration.exception_reporter = ->(e, _server_context) { reported << e }

      frozen_error = RuntimeError.new("frozen failure").freeze
      frozen_tool = Tool.define(name: "frozen_tool", description: "Raises frozen") do
        raise frozen_error
      end

      server = Server.new(name: "test_server", tools: [frozen_tool], configuration: configuration)

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "frozen_tool", arguments: {} },
        id: 1,
      }

      response = server.handle(request)

      assert_equal([frozen_error], reported)
      assert_equal("Internal error calling tool frozen_tool", response[:error][:data])
    end

    test "#handle still reports via exception_reporter when around_request swallows the tool failure" do
      reported = []
      configuration = MCP::Configuration.new
      configuration.exception_reporter = ->(e, _server_context) { reported << e.message }
      configuration.around_request = ->(_data, &request_handler) do
        request_handler.call
      rescue StandardError
        { swallowed: true }
      end

      failing_tool = Tool.define(name: "failing_tool", description: "Always fails") do
        raise "tool failure"
      end

      server = Server.new(name: "test_server", tools: [failing_tool], configuration: configuration)

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "failing_tool", arguments: {} },
        id: 1,
      }

      response = server.handle(request)

      assert_equal(["tool failure"], reported)
      assert_equal({ swallowed: true }, response[:result])
    end

    test "#handle concurrent requests on a shared server report exceptions independently" do
      reported = Queue.new
      configuration = MCP::Configuration.new
      configuration.exception_reporter = ->(e, _server_context) { reported << e.message }

      failing_tool = Tool.define(name: "concurrent_tool", description: "Raises per-thread") do |i:|
        raise "thread #{i}"
      end

      server = Server.new(name: "test_server", tools: [failing_tool], configuration: configuration)

      threads = 10.times.map do |i|
        Thread.new do
          server.handle({
            jsonrpc: "2.0",
            method: "tools/call",
            params: { name: "concurrent_tool", arguments: { i: i } },
            id: i,
          })
        end
      end
      threads.each(&:join)

      messages = []
      messages << reported.pop until reported.empty?

      assert_equal(10.times.map { |i| "thread #{i}" }.sort, messages.sort)
    end

    test "#define_custom_method raises an error if the method is already defined" do
      assert_raises(Server::MethodAlreadyDefinedError) do
        @server.define_custom_method(method_name: "tools/call") do
          nil
        end
      end
    end

    test "the global configuration is used if no configuration is passed to the server" do
      server = Server.new(name: "test_server")
      assert_equal MCP.configuration.instrumentation_callback,
        server.configuration.instrumentation_callback
      assert_equal MCP.configuration.exception_reporter,
        server.configuration.exception_reporter
    end

    test "the server configuration takes precedence over the global configuration" do
      configuration = MCP::Configuration.new
      local_callback = ->(data) { puts "Local callback #{data.inspect}" }
      local_exception_reporter = ->(exception, server_context) {
        puts "Local exception reporter #{exception.inspect} #{server_context.inspect}"
      }
      configuration.instrumentation_callback = local_callback
      configuration.exception_reporter = local_exception_reporter

      server = Server.new(name: "test_server", configuration: configuration)

      assert_equal local_callback, server.configuration.instrumentation_callback
      assert_equal local_exception_reporter, server.configuration.exception_reporter
    end

    test "server uses the latest handshake version when not configured" do
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params,
      }

      response = @server.handle(request)
      assert_equal Configuration::LATEST_HANDSHAKE_PROTOCOL_VERSION, response[:result][:protocolVersion]
    end

    test "server response does not include optional parameters when configured" do
      server = Server.new(title: "Example Server Display Name", name: "test_server", website_url: "https://example.com")
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params,
      }

      response = server.handle(request)
      server_info = response[:result][:serverInfo]

      assert_equal("Example Server Display Name", server_info[:title])
      assert_equal("https://example.com", server_info[:websiteUrl])
    end

    test "server response does not include optional parameters when not configured" do
      server = Server.new(name: "test_server")
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params,
      }

      response = server.handle(request)
      refute response[:result][:serverInfo].key?(:title)
      refute response[:result][:serverInfo].key?(:website_url)
    end

    test "server response does not include icons when icons is empty" do
      server = Server.new(name: "test_server")
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params,
      }
      response = server.handle(request)

      refute response[:result][:serverInfo].key?(:icons)
    end

    test "server response does not include icons when icons is nil" do
      server = Server.new(name: "test_server", icons: nil)
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params,
      }
      response = server.handle(request)

      refute response[:result][:serverInfo].key?(:icons)
    end

    test "server response includes icons when icons is present" do
      server = Server.new(
        name: "test_server",
        icons: [Icon.new(mime_type: "image/png", sizes: ["48x48"], src: "https://example.com", theme: "light")],
      )
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params,
      }
      response = server.handle(request)
      expected_icons = [{ mimeType: "image/png", sizes: ["48x48"], src: "https://example.com", theme: "light" }]

      assert_equal expected_icons, response[:result][:serverInfo][:icons]
    end

    test "server uses default version when not configured" do
      server = Server.new(name: "test_server")
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params,
      }

      response = server.handle(request)
      assert_equal Server::DEFAULT_VERSION, response[:result][:serverInfo][:version]
    end

    test "server uses instructions when not configured" do
      server = Server.new(name: "test_server")
      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params,
      }

      response = server.handle(request)
      refute response[:result].key?(:instructions)
    end

    test "server uses description when configured with protocol version 2025-11-25" do
      configuration = Configuration.new(protocol_version: "2025-11-25")
      server = Server.new(description: "This is the MCP server used during tests.", name: "test_server", configuration: configuration)
      assert_equal("This is the MCP server used during tests.", server.description)
    end

    test "raises error if description is used with protocol version 2025-06-18" do
      configuration = Configuration.new(protocol_version: "2025-06-18")

      exception = assert_raises(ArgumentError) do
        Server.new(description: "This is the MCP server used during tests.", name: "test_server", configuration: configuration)
      end
      assert_equal("Error occurred in server_info. `description` is not supported in protocol version 2025-06-18 or earlier", exception.message)
    end

    test "server uses instructions when configured with protocol version 2025-03-26" do
      configuration = Configuration.new(protocol_version: "2025-03-26")
      server = Server.new(name: "test_server", instructions: "The server instructions.", configuration: configuration)
      assert_equal("The server instructions.", server.instructions)
    end

    test "raises error if instructions are used with protocol version 2024-11-05" do
      configuration = Configuration.new(protocol_version: "2024-11-05")

      exception = assert_raises(ArgumentError) do
        Server.new(name: "test_server", instructions: "The server instructions.", configuration: configuration)
      end
      assert_equal("`instructions` supported by protocol version 2025-03-26 or higher", exception.message)
    end

    test "server uses annotations when configured with protocol version 2025-03-26" do
      configuration = Configuration.new(protocol_version: "2025-03-26")
      server = Server.new(name: "test_server", configuration: configuration)
      server.define_tool(
        name: "defined_tool",
        annotations: { title: "test server" },
      )
      assert_equal({ destructiveHint: true, idempotentHint: false, openWorldHint: true, readOnlyHint: false, title: "test server" }, server.tools.first[1].annotations.to_h)
    end

    test "raises error if annotations are used with protocol version 2024-11-05" do
      configuration = Configuration.new(protocol_version: "2024-11-05")
      exception = assert_raises(ArgumentError) do
        server = Server.new(name: "test_server", configuration: configuration)
        server.define_tool(
          name: "defined_tool",
          annotations: { title: "test server" },
        )
      end
      assert_equal("Error occurred in defined_tool. `annotations` are supported by protocol version 2025-03-26 or higher", exception.message)
    end

    test "raises error if `title` of `server_info` is used with protocol version 2025-03-26" do
      configuration = Configuration.new(protocol_version: "2025-03-26")

      exception = assert_raises(ArgumentError) do
        Server.new(name: "test_server", title: "Example Server Display Name", configuration: configuration)
      end
      assert_equal("Error occurred in server_info. `title` or `website_url` are not supported in protocol version 2025-03-26 or earlier", exception.message)
    end

    test "raises error if `website_url` of `server_info` is used with protocol version 2025-03-26" do
      configuration = Configuration.new(protocol_version: "2025-03-26")

      exception = assert_raises(ArgumentError) do
        Server.new(name: "test_server", website_url: "https://example.com", configuration: configuration)
      end
      assert_equal("Error occurred in server_info. `title` or `website_url` are not supported in protocol version 2025-03-26 or earlier", exception.message)
    end

    test "raises error if `title` of tool is used with protocol version 2025-03-26" do
      configuration = Configuration.new(protocol_version: "2025-03-26")
      server = Server.new(name: "test_server", configuration: configuration)

      exception = assert_raises(ArgumentError) do
        server.define_tool(
          title: "Test tool",
        )
      end
      assert_equal("Error occurred in Test tool. `title` is not supported in protocol version 2025-03-26 or earlier", exception.message)
    end

    test "raises error if `title` of prompt is used with protocol version 2025-03-26" do
      configuration = Configuration.new(protocol_version: "2025-03-26")
      server = Server.new(name: "test_server", configuration: configuration)

      exception = assert_raises(ArgumentError) do
        server.define_prompt(
          title: "Test prompt",
        )
      end
      assert_equal("Error occurred in Test prompt. `title` is not supported in protocol version 2025-03-26 or earlier", exception.message)
    end

    test "raises error if `title` of resource is used with protocol version 2025-03-26" do
      configuration = Configuration.new(protocol_version: "2025-03-26")

      resource = Resource.new(
        uri: "https://test_resource.invalid",
        name: "test-resource",
        title: "Test resource",
      )
      exception = assert_raises(ArgumentError) do
        Server.new(name: "test_server", resources: [resource], configuration: configuration)
      end
      assert_equal("Error occurred in Test resource. `title` is not supported in protocol version 2025-03-26 or earlier", exception.message)
    end

    test "raises error if `title` of class-based resource is used with protocol version 2025-03-26" do
      configuration = Configuration.new(protocol_version: "2025-03-26")

      resource = Resource.define(
        uri: "https://test_resource.invalid",
        name: "test-resource",
        title: "Test resource",
      )
      exception = assert_raises(ArgumentError) do
        Server.new(name: "test_server", resources: [resource], configuration: configuration)
      end
      assert_equal("Error occurred in Test resource. `title` is not supported in protocol version 2025-03-26 or earlier", exception.message)
    end

    test "allows `$ref` in tool input schema with protocol version 2025-11-25" do
      tool = Tool.define(
        name: "ref_tool",
        description: "Tool with $ref",
        input_schema: {
          type: "object",
          "$defs": { address: { type: "object", properties: { city: { type: "string" } } } },
          properties: { address: { "$ref": "#/$defs/address" } },
        },
      )
      configuration = Configuration.new(protocol_version: "2025-11-25")

      assert_nothing_raised do
        Server.new(name: "test_server", tools: [tool], configuration: configuration)
      end
    end

    test "raises error if `$ref` in tool input schema is used with protocol version 2025-06-18" do
      tool = Tool.define(
        name: "ref_tool",
        description: "Tool with $ref",
        input_schema: {
          type: "object",
          "$defs": { address: { type: "object", properties: { city: { type: "string" } } } },
          properties: { address: { "$ref": "#/$defs/address" } },
        },
      )
      configuration = Configuration.new(protocol_version: "2025-06-18")

      exception = assert_raises(ArgumentError) do
        Server.new(name: "test_server", tools: [tool], configuration: configuration)
      end
      assert_equal(
        "Error occurred in ref_tool. `$ref` in input schemas is supported by protocol version 2025-11-25 or higher",
        exception.message,
      )
    end

    test "raises error if `title` of resource template is used with protocol version 2025-03-26" do
      configuration = Configuration.new(protocol_version: "2025-03-26")

      resource = Resource.new(
        uri: "https://test_resource.invalid",
        name: "test-resource",
        title: "Test resource template",
      )
      exception = assert_raises(ArgumentError) do
        Server.new(name: "test_server", resources: [resource], configuration: configuration)
      end
      assert_equal("Error occurred in Test resource template. `title` is not supported in protocol version 2025-03-26 or earlier", exception.message)
    end

    test "#define_tool adds a tool to the server" do
      @server.define_tool(
        name: "defined_tool",
        description: "Defined tool",
        input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] },
        output_schema: { type: "object", properties: { response: { type: "string" } }, required: ["response"] },
        meta: { foo: "bar" },
      ) do |message:|
        Tool::Response.new({ "response" => message })
      end

      stored_tool = @server.tools["defined_tool"]
      assert_not_nil(stored_tool)
      assert_equal(MCP::Tool::OutputSchema.new({ type: "object", properties: { response: { type: "string" } }, required: ["response"] }), stored_tool.output_schema)

      response = @server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "defined_tool", arguments: { message: "success" } },
        id: 1,
      })

      assert_equal({ content: { "response" => "success" }, isError: false }, response[:result])
    end

    test "#define_tool adds a tool with duplicated tool name to the server" do
      error = assert_raises(MCP::ToolNotUnique) do
        @server.define_tool(
          name: "test_tool", # NOTE: Already registered tool name
          description: "Defined tool",
          input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] },
          meta: { foo: "bar" },
        ) do |message:|
          Tool::Response.new(message)
        end
      end
      assert_match(/\ATool names should be unique. Use `tool_name` to assign unique names to/, error.message)
    end

    test "#define_tool call definition allows tool arguments and server context" do
      @server.server_context = { user_id: "123" }

      @server.define_tool(
        name: "defined_tool",
        description: "Defined tool",
        input_schema: { type: "object", properties: { message: { type: "string" } }, required: ["message"] },
      ) do |message:, server_context:|
        Tool::Response.new("success #{message} #{server_context[:user_id]}")
      end

      response = @server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        params: { name: "defined_tool", arguments: { message: "hello" } },
        id: 1,
      })

      assert_equal({ content: "success hello 123", isError: false }, response[:result])
    end

    test "#define_prompt adds a tool to the server" do
      @server.define_prompt(name: "defined_prompt", description: "Defined prompt", arguments: []) do
        Prompt::Result.new(
          description: "a prompt description",
          messages: [Prompt::Message.new(role: "user", content: Content::Text.new("a prompt message"))],
        )
      end

      response = @server.handle({
        jsonrpc: "2.0",
        method: "prompts/get",
        params: { name: "defined_prompt", arguments: {} },
        id: 1,
      })

      assert_equal(
        {
          description: "a prompt description",
          messages: [{ role: "user", content: { text: "a prompt message", type: "text" } }],
        },
        response[:result],
      )
    end

    test "server protocol version can be overridden via configuration" do
      custom_version = "2025-03-26"
      configuration = Configuration.new(protocol_version: custom_version)
      server = Server.new(name: "test_server", configuration: configuration)

      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(protocolVersion: "1999-01-01"),
      }

      response = server.handle(request)
      assert_equal custom_version, response[:result][:protocolVersion]
    end

    test "server negotiates protocol version when client requests a supported version" do
      server = Server.new(name: "test_server")

      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(protocolVersion: "2025-06-18"),
      }

      response = server.handle(request)
      assert_equal "2025-06-18", response[:result][:protocolVersion]
    end

    test "server falls back to the latest handshake version when client requests unsupported version" do
      server = Server.new(name: "test_server")

      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(protocolVersion: "1999-01-01"),
      }

      response = server.handle(request)
      assert_equal Configuration::LATEST_HANDSHAKE_PROTOCOL_VERSION, response[:result][:protocolVersion]
    end

    test "server counter-offers 2025-11-25 when the client requests 2026-07-28 via initialize" do
      # Per the SEP-2575 era model, `initialize` negotiates legacy versions only: a modern version carries
      # its own version on every request and has no handshake at all. The TypeScript and Python servers
      # answer the same way.
      server = Server.new(name: "test_server")

      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(protocolVersion: "2026-07-28"),
      }

      response = server.handle(request)
      assert_equal "2025-11-25", response[:result][:protocolVersion]
    end

    test "server removes description and icons from server_info when negotiating to 2025-06-18" do
      server = Server.new(
        name: "test_server",
        description: "A test server",
        icons: [Icon.new(src: "https://example.com/icon.png")],
      )

      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(protocolVersion: "2025-06-18"),
      }

      response = server.handle(request)
      assert_equal "2025-06-18", response[:result][:protocolVersion]
      refute response[:result][:serverInfo].key?(:description)
      refute response[:result][:serverInfo].key?(:icons)
    end

    test "server removes title and websiteUrl when negotiating to 2025-03-26" do
      server = Server.new(name: "test_server", title: "Test Server", website_url: "https://example.com")

      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(protocolVersion: "2025-03-26"),
      }

      response = server.handle(request)
      assert_equal "2025-03-26", response[:result][:protocolVersion]
      refute response[:result][:serverInfo].key?(:title)
      refute response[:result][:serverInfo].key?(:websiteUrl)
    end

    test "server removes instructions when negotiating to 2024-11-05" do
      server = Server.new(name: "test_server", instructions: "Some instructions")

      request = {
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: initialize_params(protocolVersion: "2024-11-05"),
      }

      response = server.handle(request)
      assert_equal "2024-11-05", response[:result][:protocolVersion]
      refute response[:result].key?(:instructions)
    end

    test "tools/call returns tool execution error for missing arguments" do
      configuration = Configuration.new(validate_tool_call_arguments: true)
      configuration.instrumentation_callback = instrumentation_helper.callback
      server = Server.new(tools: [TestTool], configuration: configuration)

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "tools/call",
          params: {
            name: "test_tool",
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert_nil response[:error]
      assert(response[:result][:isError])
      assert_equal "text", response[:result][:content][0][:type]
      assert_includes response[:result][:content][0][:text], "Missing required arguments"
      assert_instrumentation_data({
        method: "tools/call",
        tool_name: "test_tool",
        tool_arguments: {},
        error: :missing_required_arguments,
      })
    end

    test "tools/call returns tool execution error for invalid arguments when validate_tool_call_arguments is true" do
      configuration = Configuration.new(validate_tool_call_arguments: true)
      configuration.instrumentation_callback = instrumentation_helper.callback
      server = Server.new(tools: [TestTool], configuration: configuration)

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "tools/call",
          params: {
            name: "test_tool",
            arguments: { message: 123 },
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert_nil response[:error]
      assert(response[:result][:isError])
      assert_equal "text", response[:result][:content][0][:type]
      assert_includes response[:result][:content][0][:text], "Invalid arguments"
      assert_instrumentation_data({
        method: "tools/call",
        tool_name: "test_tool",
        tool_arguments: { message: 123 },
        error: :invalid_schema,
      })
    end

    test "tools/call returns tool execution error for nested schema validation failure" do
      server = Server.new(
        tools: [ComplexTypesTool],
        configuration: Configuration.new(validate_tool_call_arguments: true),
      )

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "tools/call",
          params: {
            name: "complex_types_tool",
            arguments: {
              numbers: [1, 2, 3],
              strings: ["a", "b", "c"],
              objects: [{ name: 123 }],
            },
          },
        },
      )

      assert_nil response[:error]
      assert(response[:result][:isError])
      assert_equal "text", response[:result][:content][0][:type]
      assert_includes response[:result][:content][0][:text], "Invalid arguments"
    end

    test "tools/call skips argument validation when validate_tool_call_arguments is false" do
      server = Server.new(
        tools: [TestTool],
        configuration: Configuration.new(validate_tool_call_arguments: false),
      )

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "tools/call",
          params: {
            name: "test_tool",
            arguments: { message: 123 },
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert response[:result], "Expected result key in response"
      assert_equal "text", response[:result][:content][0][:type]
      assert_equal "OK", response[:result][:content][0][:content]
    end

    test "tools/call validates arguments with complex types" do
      server = Server.new(
        tools: [ComplexTypesTool],
        configuration: Configuration.new(validate_tool_call_arguments: true),
      )

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "tools/call",
          params: {
            name: "complex_types_tool",
            arguments: {
              numbers: [1, 2, 3],
              strings: ["a", "b", "c"],
              objects: [{ name: "test" }],
            },
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert response[:result], "Expected result key in response"
      assert_equal "text", response[:result][:content][0][:type]
      assert_equal "OK", response[:result][:content][0][:content]
    end

    test "tools/call allows additional properties by default" do
      server = Server.new(
        tools: [TestTool],
        configuration: Configuration.new(validate_tool_call_arguments: true),
      )

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "tools/call",
          params: {
            name: "test_tool",
            arguments: {
              message: "Hello, world!",
              other_property: "I am allowed",
            },
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert response[:result], "Expected result key in response"
      assert_equal "text", response[:result][:content][0][:type]
      assert_equal "OK", response[:result][:content][0][:content]
    end

    test "tools/call returns tool execution error when additionalProperties set to false" do
      server = Server.new(
        tools: [TestToolWithAdditionalPropertiesSetToFalse],
        configuration: Configuration.new(validate_tool_call_arguments: true),
      )

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "tools/call",
          params: {
            name: "test_tool_with_additional_properties_set_to_false",
            arguments: {
              message: "Hello, world!",
              extra: 123,
            },
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert_nil response[:error]
      assert(response[:result][:isError])
      assert_equal "text", response[:result][:content][0][:type]
      assert_includes response[:result][:content][0][:text], "Invalid arguments"
    end

    test "tools/call skips output schema validation by default" do
      tool = Tool.define(
        name: "invalid_structured_content_tool",
        output_schema: {
          type: "object",
          properties: { result: { type: "string" } },
          required: ["result"],
        },
      ) do
        Tool::Response.new(
          [{ type: "text", text: "ok" }],
          structured_content: { result: 123 },
        )
      end
      server = Server.new(tools: [tool])

      response = server.handle({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "invalid_structured_content_tool" },
      })

      assert_nil response[:error]
      assert_equal({ result: 123 }, response[:result][:structuredContent])
    end

    test "tools/call validates structuredContent against output schema when enabled" do
      tool = Tool.define(
        name: "valid_structured_content_tool",
        output_schema: {
          type: "object",
          properties: { result: { type: "string" } },
          required: ["result"],
        },
      ) do
        Tool::Response.new(
          [{ type: "text", text: "ok" }],
          structured_content: { result: "success" },
        )
      end
      server = Server.new(
        tools: [tool],
        configuration: Configuration.new(validate_tool_call_results: true),
      )

      response = server.handle({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "valid_structured_content_tool" },
      })

      assert_nil response[:error]
      assert_equal({ result: "success" }, response[:result][:structuredContent])
    end

    test "tools/call returns JSON-RPC error for invalid structuredContent when output schema validation is enabled" do
      tool = Tool.define(
        name: "invalid_structured_content_tool",
        output_schema: {
          type: "object",
          properties: { result: { type: "string" } },
          required: ["result"],
        },
      ) do
        Tool::Response.new(
          [{ type: "text", text: "ok" }],
          structured_content: { result: 123 },
        )
      end
      server = Server.new(
        tools: [tool],
        configuration: Configuration.new(validate_tool_call_results: true),
      )

      response = server.handle({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "invalid_structured_content_tool" },
      })

      assert_nil response[:result]
      assert_equal(-32603, response[:error][:code])
      assert_equal "Internal error", response[:error][:message]
      assert_equal "Internal error calling tool invalid_structured_content_tool", response[:error][:data]
    end

    test "tools/call returns JSON-RPC error when output schema validation is enabled and structuredContent is missing" do
      tool = Tool.define(
        name: "missing_structured_content_tool",
        output_schema: {
          type: "object",
          properties: { result: { type: "string" } },
          required: ["result"],
        },
      ) do
        Tool::Response.new([{ type: "text", text: "ok" }])
      end
      server = Server.new(
        tools: [tool],
        configuration: Configuration.new(validate_tool_call_results: true),
      )

      response = server.handle({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "missing_structured_content_tool" },
      })

      assert_nil response[:result]
      assert_equal(-32603, response[:error][:code])
      assert_equal "Internal error", response[:error][:message]
      assert_equal "Internal error calling tool missing_structured_content_tool", response[:error][:data]
    end

    test "tools/call skips output schema validation for error responses" do
      tool = Tool.define(
        name: "error_response_tool",
        output_schema: {
          type: "object",
          properties: { result: { type: "string" } },
          required: ["result"],
        },
      ) do
        Tool::Response.new(
          [{ type: "text", text: "failed" }],
          error: true,
          structured_content: { result: 123 },
        )
      end
      server = Server.new(
        tools: [tool],
        configuration: Configuration.new(validate_tool_call_results: true),
      )

      response = server.handle({
        jsonrpc: "2.0",
        id: 1,
        method: "tools/call",
        params: { name: "error_response_tool" },
      })

      assert_nil response[:error]
      assert response[:result][:isError]
      assert_equal({ result: 123 }, response[:result][:structuredContent])
    end

    test "tools/call returns JSON-RPC -32602 protocol error when tool is not found" do
      server = Server.new(
        tools: [TestTool],
      )

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "tools/call",
          params: {
            name: "unknown_tool",
            arguments: {},
          },
        },
      )

      assert_nil response[:result]
      assert_equal(-32602, response[:error][:code])
      assert_equal "Invalid params", response[:error][:message]
      assert_includes response[:error][:data], "Tool not found: unknown_tool"
    end

    test "#handle completion/complete returns default completion result" do
      prompt = Prompt.define(name: "test") {}
      server = Server.new(
        name: "test_server",
        prompts: [prompt],
        capabilities: { completions: {} },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "test" },
          argument: { name: "arg", value: "val" },
        },
      })

      assert_equal(
        {
          jsonrpc: "2.0",
          id: 2,
          result: { completion: { values: [], hasMore: false } },
        },
        response,
      )
    end

    test "#handle completion/complete with custom handler for ref/prompt" do
      prompt = Prompt.define(
        name: "code_review",
        arguments: [Prompt::Argument.new(name: "language", required: true)],
      ) {}
      server = Server.new(
        name: "test_server",
        prompts: [prompt],
        capabilities: { completions: {} },
      )

      server.completion_handler do |_params|
        { completion: { values: ["python", "pytorch", "pyside"], total: 10, hasMore: true } }
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "code_review" },
          argument: { name: "language", value: "py" },
        },
      })

      assert_equal(
        {
          jsonrpc: "2.0",
          id: 2,
          result: { completion: { values: ["python", "pytorch", "pyside"], total: 10, hasMore: true } },
        },
        response,
      )
    end

    test "#handle completion/complete with custom handler for ref/resource" do
      template = ResourceTemplate.new(
        uri_template: "file:///{path}",
        name: "file",
      )
      server = Server.new(
        name: "test_server",
        resource_templates: [template],
        capabilities: { completions: {} },
      )

      server.completion_handler do |_params|
        { completion: { values: ["file:///src", "file:///spec"], hasMore: false } }
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/resource", uri: "file:///{path}" },
          argument: { name: "path", value: "s" },
        },
      })

      assert_equal(
        {
          jsonrpc: "2.0",
          id: 2,
          result: { completion: { values: ["file:///src", "file:///spec"], hasMore: false } },
        },
        response,
      )
    end

    test "#handle completion/complete passes context arguments to handler" do
      prompt = Prompt.define(
        name: "code_review",
        arguments: [
          Prompt::Argument.new(name: "language", required: true),
          Prompt::Argument.new(name: "framework", required: false),
        ],
      ) {}
      server = Server.new(
        name: "test_server",
        prompts: [prompt],
        capabilities: { completions: {} },
      )

      received_params = nil
      server.completion_handler do |params|
        received_params = params
        { completion: { values: ["flask"], hasMore: false } }
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "code_review" },
          argument: { name: "framework", value: "fla" },
          context: { arguments: { language: "python" } },
        },
      })

      assert_equal({ language: "python" }, received_params.dig(:context, :arguments))
    end

    test "#handle completion/complete truncates values exceeding 100 items" do
      prompt = Prompt.define(name: "test") {}
      server = Server.new(
        name: "test_server",
        prompts: [prompt],
        capabilities: { completions: {} },
      )

      server.completion_handler do |_params|
        { completion: { values: (1..150).map(&:to_s), hasMore: false } }
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "test" },
          argument: { name: "arg", value: "" },
        },
      })

      completion = response[:result][:completion]
      assert_equal 100, completion[:values].length
      assert_equal "1", completion[:values].first
      assert_equal "100", completion[:values].last
      assert(completion[:hasMore])
      assert_equal 150, completion[:total]
    end

    test "#handle completion/complete returns error for nonexistent prompt" do
      server = Server.new(
        name: "test_server",
        capabilities: { completions: {} },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "nonexistent" },
          argument: { name: "arg", value: "val" },
        },
      })

      assert_equal(-32_602, response[:error][:code])
    end

    test "#handle completion/complete returns error for nonexistent resource template" do
      server = Server.new(
        name: "test_server",
        capabilities: { completions: {} },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/resource", uri: "unknown://template" },
          argument: { name: "arg", value: "val" },
        },
      })

      assert_equal(-32_602, response[:error][:code])
    end

    test "#handle completion/complete resource-not-found error carries the uri in error data" do
      server = Server.new(
        name: "test_server",
        capabilities: { completions: {} },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/resource", uri: "unknown://template" },
          argument: { name: "arg", value: "val" },
        },
      })

      assert_equal(-32602, response[:error][:code])
      assert_equal("Resource not found: unknown://template", response[:error][:message])
      assert_equal({ uri: "unknown://template" }, response[:error][:data])
    end

    test "#handle completion/complete returns error for invalid ref type" do
      server = Server.new(
        name: "test_server",
        capabilities: { completions: {} },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/invalid" },
          argument: { name: "arg", value: "val" },
        },
      })

      assert_equal(-32_602, response[:error][:code])
    end

    test "#handle completion/complete returns error for missing ref" do
      server = Server.new(
        name: "test_server",
        capabilities: { completions: {} },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: {},
          argument: { name: "arg", value: "val" },
        },
      })

      assert_equal(-32_602, response[:error][:code])
    end

    test "#handle completion/complete with custom handler for ref/resource with resource URI" do
      resource = Resource.new(
        uri: "file:///README.md",
        name: "readme",
      )
      server = Server.new(
        name: "test_server",
        resources: [resource],
        capabilities: { completions: {} },
      )

      server.completion_handler do |_params|
        { completion: { values: ["file:///README.md"], hasMore: false } }
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/resource", uri: "file:///README.md" },
          argument: { name: "path", value: "R" },
        },
      })

      assert_equal(
        {
          jsonrpc: "2.0",
          id: 2,
          result: { completion: { values: ["file:///README.md"], hasMore: false } },
        },
        response,
      )
    end

    test "#handle completion/complete returns error for missing argument" do
      prompt = Prompt.define(name: "test") {}
      server = Server.new(
        name: "test_server",
        prompts: [prompt],
        capabilities: { completions: {} },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "test" },
        },
      })

      assert_equal(-32_602, response[:error][:code])
    end

    test "#handle completion/complete returns error for missing argument value" do
      prompt = Prompt.define(name: "test") {}
      server = Server.new(
        name: "test_server",
        prompts: [prompt],
        capabilities: { completions: {} },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "test" },
          argument: { name: "arg" },
        },
      })

      assert_equal(-32_602, response[:error][:code])
    end

    test "#handle completion/complete returns default when handler returns nil" do
      prompt = Prompt.define(name: "test") {}
      server = Server.new(
        name: "test_server",
        prompts: [prompt],
        capabilities: { completions: {} },
      )

      server.completion_handler do |_params|
        nil
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "test" },
          argument: { name: "arg", value: "" },
        },
      })

      assert_equal(
        {
          jsonrpc: "2.0",
          id: 2,
          result: { completion: { values: [], hasMore: false } },
        },
        response,
      )
    end

    test "#handle completion/complete with string-keyed handler result" do
      prompt = Prompt.define(name: "test") {}
      server = Server.new(
        name: "test_server",
        prompts: [prompt],
        capabilities: { completions: {} },
      )

      server.completion_handler do |_params|
        { "completion" => { "values" => ["alpha", "beta"], "hasMore" => true } }
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "test" },
          argument: { name: "arg", value: "" },
        },
      })

      assert_equal ["alpha", "beta"], response[:result][:completion][:values]
      assert response[:result][:completion][:hasMore]
    end

    test "#handle completion/complete returns invalid params for non-Hash params" do
      server = Server.new(
        name: "test_server",
        prompts: [],
        capabilities: { completions: {} },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: "invalid",
      })

      assert_equal(-32602, response[:error][:code])
    end

    test "#handle completion/complete returns error when completions capability is not declared" do
      server = Server.new(
        name: "test_server",
        prompts: [],
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "completion/complete",
        params: {
          ref: { type: "ref/prompt", name: "test" },
          argument: { name: "arg", value: "" },
        },
      })

      assert response[:error]
      assert_includes response[:error][:data], "completions"
    end

    test "#handle resources/subscribe returns empty result" do
      server = Server.new(
        name: "test_server",
        capabilities: { resources: { subscribe: true } },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "resources/subscribe",
        params: { uri: "https://example.com/resource" },
      })

      assert_equal(
        {
          jsonrpc: "2.0",
          id: 2,
          result: {},
        },
        response,
      )
    end

    test "#handle resources/unsubscribe returns empty result" do
      server = Server.new(
        name: "test_server",
        capabilities: { resources: { subscribe: true } },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "resources/unsubscribe",
        params: { uri: "https://example.com/resource" },
      })

      assert_equal(
        {
          jsonrpc: "2.0",
          id: 2,
          result: {},
        },
        response,
      )
    end

    test "#handle resources/subscribe with custom handler calls the handler" do
      server = Server.new(
        name: "test_server",
        capabilities: { resources: { subscribe: true } },
      )

      received_params = nil
      server.resources_subscribe_handler do |params|
        received_params = params
        {}
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "resources/subscribe",
        params: { uri: "https://example.com/resource" },
      })

      assert_equal(
        {
          jsonrpc: "2.0",
          id: 2,
          result: {},
        },
        response,
      )
      assert_equal "https://example.com/resource", received_params[:uri]
    end

    test "#handle resources/unsubscribe with custom handler calls the handler" do
      server = Server.new(
        name: "test_server",
        capabilities: { resources: { subscribe: true } },
      )

      received_params = nil
      server.resources_unsubscribe_handler do |params|
        received_params = params
        {}
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "resources/unsubscribe",
        params: { uri: "https://example.com/resource" },
      })

      assert_equal(
        {
          jsonrpc: "2.0",
          id: 2,
          result: {},
        },
        response,
      )
      assert_equal "https://example.com/resource", received_params[:uri]
    end

    test "#handle resources/subscribe without uri returns -32602" do
      server = Server.new(
        name: "test_server",
        capabilities: { resources: { subscribe: true } },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "resources/subscribe",
        params: {},
      })

      assert_equal(-32602, response[:error][:code])
      assert_equal "Invalid params", response[:error][:message]
      assert_includes response[:error][:data], "uri"
    end

    test "#handle resources/unsubscribe without uri returns -32602" do
      server = Server.new(
        name: "test_server",
        capabilities: { resources: { subscribe: true } },
      )

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "resources/unsubscribe",
        params: {},
      })

      assert_equal(-32602, response[:error][:code])
      assert_equal "Invalid params", response[:error][:message]
    end

    # Builds an initialized server that advertises the `resources.subscribe` capability.
    def subscription_server
      server = Server.new(name: "test_server", capabilities: { resources: { subscribe: true } })
      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })
      server
    end

    # Sends `method` (`resources/subscribe` or `resources/unsubscribe`) and returns the JSON-RPC result.
    def handle_subscription(server, method)
      server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: method,
        params: { uri: "https://example.com/resource" },
      })[:result]
    end

    test "#handle resources/subscribe passes a handler-returned _meta through to the result" do
      server = subscription_server
      server.resources_subscribe_handler { |_params| { _meta: { "acme.example/subscriptionId" => "sub-1" } } }

      result = handle_subscription(server, "resources/subscribe")

      assert_equal({ _meta: { "acme.example/subscriptionId" => "sub-1" } }, result)
    end

    test "#handle resources/unsubscribe passes a handler-returned _meta through to the result" do
      server = subscription_server
      server.resources_unsubscribe_handler { |_params| { _meta: { "acme.example/note" => "gone" } } }

      result = handle_subscription(server, "resources/unsubscribe")

      assert_equal({ _meta: { "acme.example/note" => "gone" } }, result)
    end

    test "#handle resources/subscribe drops a handler-returned field that is not _meta" do
      # The spec's result defines no member other than `_meta`, so a top-level field the handler adds is not
      # a subscription protocol; it stays out of the response.
      server = subscription_server
      server.resources_subscribe_handler { |_params| { subscriptionId: "sub-1" } }

      result = handle_subscription(server, "resources/subscribe")

      assert_equal({}, result)
    end

    test "#handle resources/subscribe accepts a string _meta key from the handler" do
      server = subscription_server
      server.resources_subscribe_handler { |_params| { "_meta" => { "k" => "v" } } }

      result = handle_subscription(server, "resources/subscribe")

      assert_equal({ _meta: { "k" => "v" } }, result)
    end

    test "#handle resources/subscribe ignores a handler-returned _meta that is not a hash" do
      server = subscription_server
      server.resources_subscribe_handler { |_params| { _meta: "not-a-hash" } }

      result = handle_subscription(server, "resources/subscribe")

      assert_equal({}, result)
    end

    test "#handle resources/subscribe keeps an empty result when the handler returns a non-hash" do
      server = subscription_server
      server.resources_subscribe_handler { |_params| nil }

      result = handle_subscription(server, "resources/subscribe")

      assert_equal({}, result)
    end

    test "#handle resources/subscribe without uri does not invoke a custom handler" do
      server = Server.new(
        name: "test_server",
        capabilities: { resources: { subscribe: true } },
      )

      handler_called = false
      server.resources_subscribe_handler do |_params|
        handler_called = true
        {}
      end

      server.handle({ jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params })
      server.handle({ jsonrpc: "2.0", method: "notifications/initialized" })

      response = server.handle({
        jsonrpc: "2.0",
        id: 2,
        method: "resources/subscribe",
        params: {},
      })

      assert_equal(-32602, response[:error][:code])
      refute handler_called
    end

    test "tools/call with no args" do
      server = Server.new(tools: [@tool_with_no_args])

      response = server.handle(
        {
          jsonrpc: "2.0",
          id: 1,
          method: "tools/call",
          params: {
            name: "tool_with_no_args",
          },
        },
      )

      assert_equal "2.0", response[:jsonrpc]
      assert_equal 1, response[:id]
      assert response[:result], "Expected result key in response"
      assert_equal "text", response[:result][:content][0][:type]
      assert_equal "OK", response[:result][:content][0][:content]
    end

    class TestTool < Tool
      tool_name "test_tool"
      description "a test tool for testing"
      input_schema({ properties: { message: { type: "string" } }, required: ["message"] })

      class << self
        def call(server_context: nil, **kwargs)
          Tool::Response.new([{ type: "text", content: "OK" }])
        end
      end
    end

    class TestToolWithAdditionalPropertiesSetToFalse < Tool
      tool_name "test_tool_with_additional_properties_set_to_false"
      description "a test tool with additionalProperties set to false for testing"
      input_schema({ properties: { message: { type: "string" } }, required: ["message"], additionalProperties: false })

      class << self
        def call(server_context: nil, **kwargs)
          Tool::Response.new([{ type: "text", content: "OK" }])
        end
      end
    end

    class ComplexTypesTool < Tool
      tool_name "complex_types_tool"
      description "a test tool with complex types"
      input_schema({
        properties: {
          numbers: { type: "array", items: { type: "number" } },
          strings: { type: "array", items: { type: "string" } },
          objects: {
            type: "array",
            items: {
              type: "object",
              properties: {
                name: { type: "string" },
              },
              required: ["name"],
            },
          },
        },
        required: ["numbers", "strings", "objects"],
      })

      class << self
        def call(numbers:, strings:, objects:, server_context: nil)
          Tool::Response.new([{ type: "text", content: "OK" }])
        end
      end
    end

    class JsonObjectValue
      def initialize(value)
        @value = value
      end

      def to_json(*args)
        @value.to_json(*args)
      end
    end

    test "server_context_with_meta uses accessor method, not ivar directly" do
      subclass = Class.new(Server) do
        def server_context
          { custom: "from_accessor" }
        end
      end

      server = subclass.new(name: "test", tools: [])

      received_context = nil
      server.define_tool(name: "ctx_tool") do |server_context:|
        received_context = server_context
        Tool::Response.new([{ type: "text", text: "ok" }])
      end

      request = {
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: { name: "ctx_tool", arguments: {} },
      }

      server.handle(request)
      assert_equal "from_accessor", received_context[:custom]
    end

    test "#handle tools/call passes W3C trace context _meta keys through to the handler" do
      # Per SEP-414, `traceparent`, `tracestate`, and `baggage` are reserved
      # un-prefixed `_meta` keys and must never be stripped by the SDK.
      server = Server.new(name: "trace_test", tools: [])
      received_context = nil
      server.define_tool(name: "trace_tool") do |server_context:|
        received_context = server_context
        Tool::Response.new([{ type: "text", text: "ok" }])
      end

      server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: {
          name: "trace_tool",
          arguments: {},
          _meta: {
            traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
            tracestate: "vendor=value",
            baggage: "userId=alice",
            progressToken: "token-1",
          },
        },
      })

      meta = received_context[:_meta]
      assert_equal "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01", meta[:traceparent]
      assert_equal "vendor=value", meta[:tracestate]
      assert_equal "userId=alice", meta[:baggage]
      assert_equal "token-1", meta[:progressToken]
    end

    test "#handle prompts/get passes W3C trace context _meta keys through to the handler" do
      server = Server.new(name: "trace_test", prompts: [])
      received_context = nil
      server.define_prompt(name: "trace_prompt", arguments: []) do |_args, server_context:|
        received_context = server_context
        Prompt::Result.new(
          description: "a prompt description",
          messages: [Prompt::Message.new(role: "user", content: Content::Text.new("a prompt message"))],
        )
      end

      server.handle({
        jsonrpc: "2.0",
        method: "prompts/get",
        id: 1,
        params: {
          name: "trace_prompt",
          arguments: {},
          _meta: {
            traceparent: "00-0af7651916cd43dd8448eb211c80319c-b7ad6b7169203331-01",
            tracestate: "vendor=value",
            baggage: "userId=alice",
          },
        },
      })

      meta = received_context[:_meta]
      MCP::TraceContext::META_KEYS.each do |key|
        assert meta.key?(key.to_sym), "expected _meta to retain #{key}"
      end
    end

    test "#handle tools/call mirrors non-object structuredContent when content is omitted or nil" do
      # Per SEP-2106, `structuredContent` may be any JSON value. Older clients may only read `content`,
      # so the server adds a serialized fallback when the tool provided no content blocks.
      server = Server.new(name: "structured_test", tools: [])
      server.define_tool(name: "nil_content_tool") do
        Tool::Response.new(nil, structured_content: [1, 2])
      end
      server.define_tool(name: "omitted_content_tool") do
        Tool::Response.new(structured_content: [1, 2])
      end

      ["nil_content_tool", "omitted_content_tool"].each_with_index do |tool_name, index|
        response = server.handle({
          jsonrpc: "2.0",
          method: "tools/call",
          id: index + 1,
          params: { name: tool_name, arguments: {} },
        })

        assert_equal [1, 2], response.dig(:result, :structuredContent)
        assert_equal [{ type: "text", text: "[1,2]" }], response.dig(:result, :content)
      end
    end

    test "#handle tools/call does not overwrite explicit content when structuredContent is non-object" do
      server = Server.new(name: "structured_test", tools: [])
      server.define_tool(name: "array_tool") do
        Tool::Response.new([{ type: "text", text: "two items" }], structured_content: [1, 2])
      end

      response = server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: { name: "array_tool", arguments: {} },
      })

      assert_equal [{ type: "text", text: "two items" }], response.dig(:result, :content)
    end

    test "#handle tools/call leaves object structuredContent without a text fallback" do
      server = Server.new(name: "structured_test", tools: [])
      server.define_tool(name: "object_tool") do
        Tool::Response.new(nil, structured_content: { answer: 42 })
      end

      response = server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: { name: "object_tool", arguments: {} },
      })

      assert_equal({ answer: 42 }, response.dig(:result, :structuredContent))
      assert_empty response.dig(:result, :content)
    end

    test "#handle tools/call recognizes object-like structuredContent by its JSON shape" do
      structured_content = JsonObjectValue.new(answer: 42)
      server = Server.new(name: "structured_test", tools: [])
      server.define_tool(name: "object_tool") do
        Tool::Response.new(nil, structured_content: structured_content)
      end

      response = server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: { name: "object_tool", arguments: {} },
      })

      assert_equal structured_content, response.dig(:result, :structuredContent)
      assert_empty response.dig(:result, :content)
    end

    test "#handle tools/call preserves explicit empty content for non-object structuredContent" do
      server = Server.new(name: "structured_test", tools: [])
      server.define_tool(name: "array_tool") do
        Tool::Response.new([], structured_content: [1, 2])
      end

      response = server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: { name: "array_tool", arguments: {} },
      })

      assert_equal [1, 2], response.dig(:result, :structuredContent)
      assert_empty response.dig(:result, :content)
    end

    test "#handle tools/call preserves explicit empty content for object-like structuredContent" do
      structured_content = JsonObjectValue.new(answer: 42)

      server = Server.new(name: "structured_test", tools: [])
      server.define_tool(name: "object_tool") do
        Tool::Response.new([], structured_content: structured_content)
      end

      response = server.handle({
        jsonrpc: "2.0",
        method: "tools/call",
        id: 1,
        params: { name: "object_tool", arguments: {} },
      })

      assert_equal structured_content, response.dig(:result, :structuredContent)
      assert_empty response.dig(:result, :content)
    end

    test "list results carry ttlMs and cacheScope when ttl_ms is configured" do
      # SEP-2549 cache hints. The unset cacheScope fills as "private", the side that cannot
      # leak a user-dependent result through a shared cache, matching the TypeScript and
      # Python SDK defaults (the spec names no default scope).
      server = Server.new(name: "ttl_test", ttl_ms: 5000)

      ["tools/list", "prompts/list", "resources/list", "resources/templates/list"].each_with_index do |method, index|
        result = server.handle({ jsonrpc: "2.0", method: method, id: index + 1 })[:result]

        assert_equal 5000, result[:ttlMs], "#{method} missing ttlMs"
        assert_equal "private", result[:cacheScope], "#{method} missing cacheScope"
      end
    end

    test "resources/read carries cache hints when cache_scope is configured" do
      # The ttlMs default is 0 (do not cache), the only universally safe value.
      server = Server.new(name: "ttl_test", cache_scope: "private")

      result = server.handle({
        jsonrpc: "2.0",
        method: "resources/read",
        id: 1,
        params: { uri: "file:///x" },
      })[:result]

      assert_equal 0, result[:ttlMs]
      assert_equal "private", result[:cacheScope]
    end

    test "results omit cache hints when ttl_ms and cache_scope are not configured" do
      # Wire-format regression: opt-in emission keeps default output unchanged.
      list_result = @server.handle({ jsonrpc: "2.0", method: "tools/list", id: 1 })[:result]
      read_result = @server.handle({
        jsonrpc: "2.0",
        method: "resources/read",
        id: 2,
        params: { uri: "file:///x" },
      })[:result]

      refute list_result.key?(:ttlMs)
      refute list_result.key?(:cacheScope)
      refute read_result.key?(:ttlMs)
      refute read_result.key?(:cacheScope)
    end

    test "cache hints appear alongside nextCursor when paginating" do
      tool_a = Tool.define(name: "tool_a", description: "Tool A")
      tool_b = Tool.define(name: "tool_b", description: "Tool B")
      server = Server.new(name: "ttl_test", tools: [tool_a, tool_b], page_size: 1, ttl_ms: 1000, cache_scope: "private")

      result = server.handle({ jsonrpc: "2.0", method: "tools/list", id: 1 })[:result]

      assert_not_nil result[:nextCursor]
      assert_equal 1000, result[:ttlMs]
      assert_equal "private", result[:cacheScope]
    end

    test "a resources_read_handler can override the server-level cache hints per result" do
      server = Server.new(name: "ttl_test", ttl_ms: 5000)
      server.resources_read_handler do |params|
        { contents: [{ uri: params[:uri], mimeType: "text/plain", text: "hi" }], ttlMs: 60_000 }
      end

      result = server.handle({
        jsonrpc: "2.0",
        method: "resources/read",
        id: 1,
        params: { uri: "file:///x" },
      })[:result]

      assert_equal 60_000, result[:ttlMs]
      assert_equal "private", result[:cacheScope]
      assert_equal [{ uri: "file:///x", mimeType: "text/plain", text: "hi" }], result[:contents]
    end

    test "ttl_ms and cache_scope writers reject invalid values" do
      assert_raises(ArgumentError) { Server.new(name: "ttl_test", ttl_ms: -1) }
      assert_raises(ArgumentError) { Server.new(name: "ttl_test", ttl_ms: 1.5) }
      assert_raises(ArgumentError) { Server.new(name: "ttl_test", cache_scope: "internal") }
    end

    test "#handle tools/list returns paginated results when page_size is set" do
      tool_a = Tool.define(name: "tool_a", title: "Tool A", description: "Tool A")
      tool_b = Tool.define(name: "tool_b", title: "Tool B", description: "Tool B")
      tool_c = Tool.define(name: "tool_c", title: "Tool C", description: "Tool C")

      server = Server.new(
        name: "pagination_test",
        tools: [tool_a, tool_b, tool_c],
        page_size: 2,
      )

      first_request = { jsonrpc: "2.0", method: "tools/list", id: 1 }
      first_response = server.handle(first_request)
      first_result = first_response[:result]

      assert_equal 2, first_result[:tools].size
      assert_equal "tool_a", first_result[:tools][0][:name]
      assert_equal "tool_b", first_result[:tools][1][:name]
      assert_not_nil first_result[:nextCursor]

      second_request = { jsonrpc: "2.0", method: "tools/list", id: 2, params: { cursor: first_result[:nextCursor] } }
      second_response = server.handle(second_request)
      second_result = second_response[:result]

      assert_equal 1, second_result[:tools].size
      assert_equal "tool_c", second_result[:tools][0][:name]
      # Final page omits the nextCursor key entirely (not just sets it to nil).
      refute second_result.key?(:nextCursor)
    end

    test "#handle tools/list returns all tools when page_size is not set" do
      response = @server.handle({ jsonrpc: "2.0", method: "tools/list", id: 1 })
      result = response[:result]

      assert_kind_of Array, result[:tools]
      assert_nil result[:nextCursor]
    end

    test "#handle tools/list returns error for invalid cursor" do
      server = Server.new(name: "pagination_test", tools: [@tool], page_size: 1)

      request = { jsonrpc: "2.0", method: "tools/list", id: 1, params: { cursor: "!!!invalid!!!" } }
      response = server.handle(request)

      assert_not_nil response[:error]
      assert_equal(-32602, response[:error][:code])
    end

    test "#handle prompts/list returns paginated results when page_size is set" do
      prompt_a = Prompt.define(name: "prompt_a", title: "Prompt A", description: "A") { Prompt::Result.new(description: "A", messages: []) }
      prompt_b = Prompt.define(name: "prompt_b", title: "Prompt B", description: "B") { Prompt::Result.new(description: "B", messages: []) }

      server = Server.new(name: "pagination_test", prompts: [prompt_a, prompt_b], page_size: 1)

      first_response = server.handle({ jsonrpc: "2.0", method: "prompts/list", id: 1 })
      first_result = first_response[:result]

      assert_equal 1, first_result[:prompts].size
      assert_equal "prompt_a", first_result[:prompts][0][:name]
      assert_not_nil first_result[:nextCursor]

      second_response = server.handle({ jsonrpc: "2.0", method: "prompts/list", id: 2, params: { cursor: first_result[:nextCursor] } })
      second_result = second_response[:result]

      assert_equal 1, second_result[:prompts].size
      assert_equal "prompt_b", second_result[:prompts][0][:name]
      assert_nil second_result[:nextCursor]
    end

    test "#handle resources/list returns paginated results when page_size is set" do
      resource_a = Resource.new(uri: "https://a.invalid", name: "a", description: "A", mime_type: "text/plain")
      resource_b = Resource.new(uri: "https://b.invalid", name: "b", description: "B", mime_type: "text/plain")

      server = Server.new(name: "pagination_test", resources: [resource_a, resource_b], page_size: 1)

      first_response = server.handle({ jsonrpc: "2.0", method: "resources/list", id: 1 })
      first_result = first_response[:result]

      assert_equal 1, first_result[:resources].size
      assert_equal "a", first_result[:resources][0][:name]
      assert_not_nil first_result[:nextCursor]

      second_response = server.handle({ jsonrpc: "2.0", method: "resources/list", id: 2, params: { cursor: first_result[:nextCursor] } })
      second_result = second_response[:result]

      assert_equal 1, second_result[:resources].size
      assert_equal "b", second_result[:resources][0][:name]
      assert_nil second_result[:nextCursor]
    end

    test "Server.new raises ArgumentError when page_size is zero" do
      assert_raises(ArgumentError) do
        Server.new(name: "test", page_size: 0)
      end
    end

    test "Server.new raises ArgumentError when page_size is negative" do
      assert_raises(ArgumentError) do
        Server.new(name: "test", page_size: -1)
      end
    end

    test "Server.new raises ArgumentError when page_size is non-Integer" do
      assert_raises(ArgumentError) do
        Server.new(name: "test", page_size: "10")
      end
    end

    test "page_size= raises ArgumentError for invalid values" do
      server = Server.new(name: "test")

      assert_raises(ArgumentError) { server.page_size = 0 }
      assert_raises(ArgumentError) { server.page_size = -1 }
      assert_raises(ArgumentError) { server.page_size = "5" }

      server.page_size = nil
      server.page_size = 10
      assert_equal 10, server.page_size
    end

    test "#handle tools/list returns -32602 for non-Hash params" do
      server = Server.new(name: "test", tools: [@tool], page_size: 1)

      request = { jsonrpc: "2.0", method: "tools/list", id: 1, params: [1, 2, 3] }
      response = server.handle(request)

      assert_not_nil response[:error]
      assert_equal(-32602, response[:error][:code])
    end

    test "#handle_json tools/list returns -32602 for numeric cursor (spec requires string)" do
      server = Server.new(name: "test", tools: [@tool], page_size: 1)

      request_json = '{"jsonrpc":"2.0","method":"tools/list","id":1,"params":{"cursor":0}}'
      response = JSON.parse(server.handle_json(request_json), symbolize_names: true)

      assert_not_nil response[:error]
      assert_equal(-32602, response[:error][:code])
    end

    test "#handle resources/templates/list returns paginated results when page_size is set" do
      template_a = ResourceTemplate.new(uri_template: "https://a.invalid/{id}", name: "a", description: "A", mime_type: "text/plain")
      template_b = ResourceTemplate.new(uri_template: "https://b.invalid/{id}", name: "b", description: "B", mime_type: "text/plain")

      server = Server.new(name: "pagination_test", resource_templates: [template_a, template_b], page_size: 1)

      first_response = server.handle({ jsonrpc: "2.0", method: "resources/templates/list", id: 1 })
      first_result = first_response[:result]

      assert_equal 1, first_result[:resourceTemplates].size
      assert_equal "a", first_result[:resourceTemplates][0][:name]
      assert_not_nil first_result[:nextCursor]

      second_response = server.handle({ jsonrpc: "2.0", method: "resources/templates/list", id: 2, params: { cursor: first_result[:nextCursor] } })
      second_result = second_response[:result]

      assert_equal 1, second_result[:resourceTemplates].size
      assert_equal "b", second_result[:resourceTemplates][0][:name]
      assert_nil second_result[:nextCursor]
    end

    test "an MCP Apps server advertises the extension and serves ui:// templates" do
      # SEP-1865: the extension rides the SEP-2133 `capabilities.extensions` machinery,
      # UI templates are ordinary resources, and tools link to them via `_meta.ui.resourceUri`.
      dashboard = Apps.ui_resource(uri: "ui://weather/dashboard", name: "weather_dashboard")
      capabilities = Server::Capabilities.new
      capabilities.support_tools
      capabilities.support_resources
      capabilities.support_extensions(Apps.capability)
      server = Server.new(name: "apps_test", capabilities: capabilities, resources: [dashboard])
      server.define_tool(
        name: "get_weather",
        meta: Apps.tool_meta(resource_uri: "ui://weather/dashboard"),
      ) do
        Tool::Response.new([{ type: "text", text: "Sunny" }])
      end

      server.resources_read_handler do |params|
        [{ uri: params[:uri], mimeType: Apps::RESOURCE_MIME_TYPE, text: "<html>dashboard</html>" }]
      end

      init_response = server.handle({
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: {
          protocolVersion: "2025-11-25",
          clientInfo: { name: "host", version: "1.0" },
          capabilities: { extensions: Apps.capability },
        },
      })

      tools_response = server.handle({
        jsonrpc: "2.0",
        method: "tools/list",
        id: 2,
      })

      read_response = server.handle({
        jsonrpc: "2.0",
        method: "resources/read",
        id: 3,
        params: { uri: "ui://weather/dashboard" },
      })

      assert_equal(
        { mimeTypes: [Apps::RESOURCE_MIME_TYPE] },
        init_response.dig(:result, :capabilities, :extensions, Apps::EXTENSION_ID),
      )
      tool_entry = tools_response.dig(:result, :tools, 0)

      assert_equal "ui://weather/dashboard", tool_entry.dig(:_meta, :ui, :resourceUri)
      assert_equal "<html>dashboard</html>", read_response.dig(:result, :contents, 0, :text)
      # The server can gate the text-only fallback on the client's declaration.
      assert Apps.client_supports?(server.client_capabilities)
    end

    test "an MCP Apps tool falls back to text for clients without the extension" do
      capabilities = Server::Capabilities.new
      capabilities.support_extensions(Apps.capability)
      server = Server.new(name: "apps_test", capabilities: capabilities)

      server.handle({
        jsonrpc: "2.0",
        method: "initialize",
        id: 1,
        params: {
          protocolVersion: "2025-11-25",
          clientInfo: { name: "plain_client", version: "1.0" },
          capabilities: {},
        },
      })

      refute Apps.client_supports?(server.client_capabilities)
    end

    # SEP-2322 makes `resultType` REQUIRED on every result of a 2026-07-28 server;
    # `"complete"` is the standard shape, and legacy results stay unstamped.
    test "modern results carry resultType complete" do
      server = Server.new(name: "result_type_test", tools: [result_type_tool])

      # Every method here has to be one the modern lifecycle still has. `ping` does not qualify:
      # SEP-2575 removed it, so a modern request naming it is answered with -32601, not a stamped result.
      [
        modern_request(Methods::TOOLS_LIST, {}),
        modern_request(Methods::PROMPTS_LIST, {}),
        modern_request(Methods::TOOLS_CALL, { name: "result_type_tool", arguments: {} }),
      ].each do |request|
        response = server.handle(request)
        assert_equal "complete", response.dig(:result, :resultType), "expected stamp for #{request[:method]}"
      end
    end

    test "legacy results carry no resultType" do
      server = Server.new(name: "result_type_test", tools: [result_type_tool])

      [
        { jsonrpc: "2.0", method: Methods::TOOLS_LIST, id: 1 },
        { jsonrpc: "2.0", method: Methods::PING, id: 2 },
        { jsonrpc: "2.0", method: Methods::TOOLS_CALL, id: 3, params: { name: "result_type_tool", arguments: {} } },
      ].each do |request|
        response = server.handle(request)
        refute response[:result].key?(:resultType), "expected no stamp for #{request[:method]}"
      end
    end

    test "results after a counter-offered modern initialize carry no resultType" do
      # The scenario from issue #512: a client asking `initialize` for 2026-07-28 lands on
      # 2025-11-25, where `resultType` does not exist, so its absence is the correct shape
      # (clients MUST read an absent `resultType` as "complete" on that revision).
      server = Server.new(name: "result_type_test", tools: [result_type_tool])
      session = ServerSession.new(server: server, transport: mock)

      init_response = server.handle(
        { jsonrpc: "2.0", method: "initialize", id: 1, params: initialize_params(protocolVersion: "2026-07-28") },
        session: session,
      )
      response = server.handle({ jsonrpc: "2.0", method: Methods::TOOLS_LIST, id: 2 }, session: session)

      assert_equal Configuration::LATEST_HANDSHAKE_PROTOCOL_VERSION, init_response.dig(:result, :protocolVersion)
      refute response[:result].key?(:resultType)
    end

    test "server/discover carries resultType complete" do
      server = Server.new(name: "result_type_test", tools: [result_type_tool])

      response = server.handle({ jsonrpc: "2.0", method: Methods::SERVER_DISCOVER, id: 1 })

      assert_equal "complete", response.dig(:result, :resultType)
    end

    # SEP-2549 makes the `ttlMs`/`cacheScope` cache hints REQUIRED on cacheable results at 2026-07-28;
    # unset hints get the spec defaults, and stable protocol versions keep the opt-in emission.
    test "modern cacheable results carry the default cache hints" do
      server = Server.new(name: "cache_hints_test", tools: [result_type_tool])

      response = server.handle(modern_request(Methods::TOOLS_LIST, {}))

      assert_equal 0, response.dig(:result, :ttlMs)
      assert_equal "private", response.dig(:result, :cacheScope)
    end

    test "modern non-cacheable results carry no cache hints" do
      server = Server.new(name: "cache_hints_test", tools: [result_type_tool])

      # `tools/call` is the non-cacheable example here: the spec types `CallToolResult` as a plain `Result`,
      # while the results it types as `CacheableResult` are the discover, list, and read families.
      # `ping` cannot stand in for it - SEP-2575 removed that method, so a modern request naming it is refused.
      response = server.handle(modern_request(Methods::TOOLS_CALL, { name: "result_type_tool", arguments: {} }))

      refute response[:result].key?(:ttlMs)
      refute response[:result].key?(:cacheScope)
    end

    test "configured cache hints win over the modern defaults" do
      server = Server.new(name: "cache_hints_test", tools: [result_type_tool], ttl_ms: 5000, cache_scope: "private")

      response = server.handle(modern_request(Methods::TOOLS_LIST, {}))

      assert_equal 5000, response.dig(:result, :ttlMs)
      assert_equal "private", response.dig(:result, :cacheScope)
    end

    test "modern resources/read results carry the default cache hints" do
      server = Server.new(name: "cache_hints_test")
      server.resources_read_handler do |params|
        [{ uri: params[:uri], mimeType: "text/plain", text: "hi" }]
      end

      response = server.handle(modern_request(Methods::RESOURCES_READ, { uri: "file:///x" }))

      assert_equal 0, response.dig(:result, :ttlMs)
      assert_equal "private", response.dig(:result, :cacheScope)
    end

    test "legacy cacheable results keep the opt-in cache hint emission" do
      server = Server.new(name: "cache_hints_test", tools: [result_type_tool])

      response = server.handle({ jsonrpc: "2.0", method: Methods::TOOLS_LIST, id: 1 })

      refute response[:result].key?(:ttlMs)
      refute response[:result].key?(:cacheScope)
    end

    private

    # Builds a request carrying the SEP-2575 modern `_meta` envelope.
    def modern_request(method, params, version: "2026-07-28", capabilities: {})
      {
        jsonrpc: "2.0",
        method: method,
        id: 1,
        params: params.merge(
          _meta: {
            "io.modelcontextprotocol/protocolVersion": version,
            "io.modelcontextprotocol/clientInfo": { name: "modern_client", version: "2.0" },
            "io.modelcontextprotocol/clientCapabilities": capabilities,
          },
        ),
      }
    end

    def result_type_tool
      @result_type_tool ||= Tool.define(name: "result_type_tool") do |**|
        Tool::Response.new([{ type: "text", text: "ok" }])
      end
    end
  end
end
