packages feed

mcp-server-0.2.0.2: test/Spec/HandlerExceptions.hs

{-# LANGUAGE OverloadedStrings #-}

-- | A handler that throws (rather than returning an error value) must still
-- produce a response for its request id: a -32603 internal error. Without
-- this the exception escapes the transport and the client waits forever.
-- Asynchronous exceptions are deliberately not covered here — cancellation
-- relies on them propagating, which "Spec.Cancellation" pins down.
module Spec.HandlerExceptions (spec) where

import Control.Exception (throwIO, ErrorCall (..))
import Data.Aeson
import qualified Data.Text as T
import MCP.Server
import MCP.Server.Handlers (handleMcpMessage)
import MCP.Server.JsonRpc
import Test.Hspec

throwingServer :: McpServerHandlers
throwingServer = noHandlers
  { tools = Just
      ( \_ -> pure []
      , \_ _ _ -> throwIO (ErrorCall "tool exploded")
      )
  , prompts = Just
      ( \_ -> pure []
      , \_ _ _ -> error "prompt exploded"
      )
  }

call :: T.Text -> Value -> IO (Maybe JsonRpcMessage)
call method params =
  handleMcpMessage (McpServerInfo "T" "1" "") defaultCacheHints
    noNotificationSupport (\_ -> pure ()) throwingServer anonymousContext
    (JsonRpcMessageRequest (JsonRpcRequest "2.0" (RequestIdNumber 7) method (Just params)))

errorOf :: Maybe JsonRpcMessage -> (RequestId, Int, T.Text)
errorOf (Just (JsonRpcMessageResponse r)) = case responseError r of
  Just e  -> (responseId r, errorCode e, errorMessage e)
  Nothing -> error $ "expected an error response, got " ++ show (responseResult r)
errorOf other = error $ "expected a response, got " ++ show other

spec :: Spec
spec = describe "Handler exceptions" $ do
  it "a throwing tool handler yields -32603 for its request id" $ do
    (rid, code, msg) <- errorOf <$> call "tools/call" (object ["name" .= ("x" :: T.Text), "arguments" .= object []])
    rid `shouldBe` RequestIdNumber 7
    code `shouldBe` (-32603)
    msg `shouldSatisfy` T.isInfixOf "tool exploded"

  it "a throwing prompt handler yields -32603 (pure error, forced inside the handler)" $ do
    (rid, code, msg) <- errorOf <$> call "prompts/get" (object ["name" .= ("x" :: T.Text), "arguments" .= object []])
    rid `shouldBe` RequestIdNumber 7
    code `shouldBe` (-32603)
    msg `shouldSatisfy` T.isInfixOf "prompt exploded"