packages feed

mcp-server-0.1.0.17: test/Spec/ProtocolVersionNegotiation.hs

{-# LANGUAGE OverloadedStrings #-}

module Spec.ProtocolVersionNegotiation (spec) where

import Data.Aeson (Value(..), object, (.=))
import qualified Data.Aeson.KeyMap as KM
import Test.Hspec

import MCP.Server.Handlers (handleInitialize)
import MCP.Server.JsonRpc (JsonRpcRequest(..), RequestId(..), JsonRpcResponse(..), JsonRpcError(..))
import MCP.Server.Types (McpServerInfo(..))

-- | Test that server performs proper version negotiation according to MCP spec
--
-- From the spec: "If the server supports the requested protocol version,
-- it MUST respond with the same version. Otherwise, the server MUST respond
-- with another protocol version it supports."
--
-- This means if a client sends protocolVersion "2025-11-25" and the server
-- only supports "2025-06-18", the server should respond with:
-- {
--   "jsonrpc": "2.0",
--   "id": 0,
--   "result": {
--     "protocolVersion": "2025-06-18",
--     ...
--   }
-- }
--
-- NOT with an error like: {"jsonrpc":"2.0","id":0,"error":{"code":-32602,...}}
spec :: Spec
spec = describe "Protocol Version Negotiation" $ do
  let testServerInfo = McpServerInfo
        { serverName = "Test Server"
        , serverVersion = "1.0.0"
        , serverInstructions = "Test server for version negotiation"
        }

  it "Server should respond with supported version when client requests unsupported version" $ do
    -- Create an initialize request with a newer protocol version
    -- that the library doesn't support
    let params = object
          [ "protocolVersion" .= String "2025-11-25"  -- Newer than library supports
          , "capabilities" .= object []
          , "clientInfo" .= object
              [ "name" .= String "test-client"
              , "version" .= String "1.0.0"
              ]
          ]
        request = JsonRpcRequest
          { requestJsonrpc = "2.0"
          , requestId = RequestIdNumber 0
          , requestMethod = "initialize"
          , requestParams = Just params
          }

    -- Call handleInitialize directly (it runs in IO monad)
    response <- handleInitialize testServerInfo request

    -- Check if it's an error response
    case responseError response of
      Just err -> do
        -- This is the bug! Server returned an error instead of negotiating
        expectationFailure $
          "Server returned error instead of negotiating version. " ++
          "Per MCP spec, server MUST respond with a supported version, " ++
          "not an error. Error was: " ++ show (errorMessage err)
      Nothing -> do
        -- Good! Now verify the result has protocolVersion
        case responseResult response of
          Nothing -> expectationFailure "Response has no result"
          Just (Object result) -> do
            case KM.lookup "protocolVersion" result of
              Just (String version) -> do
                -- The server should have responded with its supported version
                -- We expect "2025-06-18" based on the library
                version `shouldBe` "2025-06-18"
              Just other -> expectationFailure $ "protocolVersion is not a string: " ++ show other
              Nothing -> expectationFailure "Response missing protocolVersion"
          Just other -> expectationFailure $ "Result is not an object: " ++ show other

  it "Server should respond with same version when client requests supported version" $ do
    -- This test verifies the happy path: client requests "2025-06-18"
    -- and server responds with "2025-06-18"
    let params = object
          [ "protocolVersion" .= String "2025-06-18"  -- Library's supported version
          , "capabilities" .= object []
          , "clientInfo" .= object
              [ "name" .= String "test-client"
              , "version" .= String "1.0.0"
              ]
          ]
        request = JsonRpcRequest
          { requestJsonrpc = "2.0"
          , requestId = RequestIdNumber 0
          , requestMethod = "initialize"
          , requestParams = Just params
          }

    response <- handleInitialize testServerInfo request

    -- Check it's not an error
    case responseError response of
      Just err -> expectationFailure $ "Unexpected error: " ++ show (errorMessage err)
      Nothing -> do
        -- Verify protocolVersion matches
        case responseResult response of
          Nothing -> expectationFailure "Response has no result"
          Just (Object result) -> do
            case KM.lookup "protocolVersion" result of
              Just (String version) -> version `shouldBe` "2025-06-18"
              Just other -> expectationFailure $ "protocolVersion is not a string: " ++ show other
              Nothing -> expectationFailure "Response missing protocolVersion"
          Just other -> expectationFailure $ "Result is not an object: " ++ show other