{-# LANGUAGE DataKinds #-} {-# LANGUAGE OverloadedRecordDot #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE TypeApplications #-} {-# LANGUAGE ViewPatterns #-} {-# OPTIONS_GHC -Wno-orphans #-} module Main (main) where import Test.Tasty import Test.Tasty.HUnit import Data.Aeson (Value (..), decode, eitherDecode, encode, object, (.=)) import Data.Proxy (Proxy (..)) import Data.Text (Text) import qualified Data.Text as T import Polytropos import Polytropos.MCP -- Define test tool argument schemas type SearchArgs = '["query" ':= Text, "limit" ':= Int] type EchoArgs = '["message" ':= Text] -- Tool sum row and variant type TestToolsRow = '[ "search" ':= SearchArgs , "echo" := EchoArgs ] type TestTools = Variant TestToolsRow testTools :: [McpTool] testTools = [ recordToTool @"search" "Search documents" (Proxy @SearchArgs) , recordToTool @"echo" "Echo back message" (Proxy @EchoArgs) ] serverInfo :: ServerInfo serverInfo = ServerInfo "polytropos-mcp-test" "0.1.0" testHandler :: TestTools -> IO [McpContent] testHandler = eliminateUniform REC { search = \args -> pure [TextContent ("results for: " <> args.query <> " (limit=" <> T.pack (show args.limit) <> ")")] , echo = \args -> pure [TextContent ("echo: " <> args.message)] } dispatcher :: Text -> Value -> IO CallToolResult dispatcher = dispatchToolCall @TestToolsRow testHandler main :: IO () main = defaultMain $ testGroup "Polytropos.MCP Tests" [ testCase "recordToTool synthesizes name and schema" $ do case testTools of (t : _) -> do toolName t @?= "search" toolDescription t @?= "Search documents" case toolInputSchema t of Object _ -> pure () _ -> assertFailure "expected Object schema" [] -> assertFailure "empty testTools" , testCase "toolCallToVariant parses valid arguments" $ do let args = object [ "query" .= ("haskell" :: Text), "limit" .= (10 :: Int) ] case toolCallToVariant @TestToolsRow "search" args of Right (VAR { search = r }) -> do r.query @?= "haskell" r.limit @?= (10 :: Int) Right _ -> assertFailure "unexpected variant branch" Left err -> assertFailure ("parse error: " ++ err) , testCase "dispatchToolCall executes search tool" $ do let args = object [ "query" .= ("rows" :: Text), "limit" .= (5 :: Int) ] res <- dispatcher "search" args callToolIsError res @?= Nothing case callToolContent res of [TextContent txt] -> txt @?= "results for: rows (limit=5)" _ -> assertFailure "unexpected content" , testCase "dispatchToolCall executes echo tool" $ do let args = object [ "message" .= ("hello world" :: Text) ] res <- dispatcher "echo" args callToolIsError res @?= Nothing case callToolContent res of [TextContent txt] -> txt @?= "echo: hello world" _ -> assertFailure "unexpected content" , testCase "dispatchToolCall reports error on invalid tool" $ do let args = object [ "foo" .= ("bar" :: Text) ] res <- dispatcher "unknown_tool" args callToolIsError res @?= Just True , testCase "handleJsonRpcMessage responds to initialize" $ do let req = encode $ object [ "jsonrpc" .= ("2.0" :: Text) , "id" .= (1 :: Int) , "method" .= ("initialize" :: Text) ] mResp <- handleJsonRpcMessage serverInfo testTools dispatcher req case mResp of Just respBytes -> do case decode respBytes of Just (JsonRpcResponse "2.0" (Just (Number 1)) (Just resVal) Nothing) -> case resVal of Object _ -> pure () _ -> assertFailure "expected Object result" _ -> assertFailure "unexpected response envelope" Nothing -> assertFailure "expected response" , testCase "handleJsonRpcMessage responds to tools/list" $ do let req = encode $ object [ "jsonrpc" .= ("2.0" :: Text) , "id" .= (2 :: Int) , "method" .= ("tools/list" :: Text) ] mResp <- handleJsonRpcMessage serverInfo testTools dispatcher req case mResp of Just respBytes -> do case decode respBytes of Just (JsonRpcResponse "2.0" (Just (Number 2)) (Just resVal) Nothing) -> case resVal of Object _ -> pure () _ -> assertFailure "expected Object result" _ -> assertFailure "unexpected response envelope" Nothing -> assertFailure "expected response" , testCase "handleJsonRpcMessage executes tools/call over JSON-RPC" $ do let req = encode $ object [ "jsonrpc" .= ("2.0" :: Text) , "id" .= (3 :: Int) , "method" .= ("tools/call" :: Text) , "params" .= object [ "name" .= ("echo" :: Text) , "arguments" .= object [ "message" .= ("ping" :: Text) ] ] ] mResp <- handleJsonRpcMessage serverInfo testTools dispatcher req case mResp of Just respBytes -> do case eitherDecode respBytes of Right (JsonRpcResponse "2.0" (Just (Number 3)) (Just resVal) Nothing) -> do case decode (encode resVal) of Just (CallToolResult [TextContent "echo: ping"] Nothing) -> pure () _ -> assertFailure ("unexpected tool result: " ++ show resVal) Right other -> assertFailure ("unexpected response: " ++ show other) Left err -> assertFailure ("failed decoding response: " ++ err) Nothing -> assertFailure "expected response" ]