packages feed

langchain-hs-mcp-0.0.6.2: test/Test/Langchain/MCP/McpSpec.hs

{-# LANGUAGE OverloadedStrings #-}

module Test.Langchain.MCP.McpSpec (tests) where

import Control.Monad.Except (runExceptT)
import qualified Data.Aeson as Aeson
import Data.Conduit (fuseUpstream, runConduit)
import qualified Data.Conduit.List as CL
import Data.IORef (modifyIORef', newIORef, readIORef)
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import Test.Tasty
import Test.Tasty.HUnit

import Langchain.MCP.Client
import Langchain.Model.Types (Role (..), extractMessageText, messageRole)
import Langchain.Tool (StreamingTool (..))
import Langchain.Tool.Core (Tool (..))

tests :: TestTree
tests =
  testGroup
    "Langchain.MCP.McpSpec"
    [ testCase "newStdioMcpClient initializes transport and server name" $ do
        let client = newStdioMcpClient "test-mcp" "npx" ["-y", "@modelcontextprotocol/server-everything"]
        serverName client @?= "test-mcp"
        clientTransport client @?= StdioTransport "npx" ["-y", "@modelcontextprotocol/server-everything"]
    , testCase "mcpToolToLangchainTool converts remote tool to callable local Tool" $ do
        let client = newStdioMcpClient "test-server" "echo" []
            toolInfo =
              McpToolInfo
                { mcpToolName = "echo_tool"
                , mcpToolDescription = "Echoes inputs"
                , mcpToolInputSchema = Aeson.object []
                }
            langchainTool = mcpToolToLangchainTool client toolInfo
        toolName langchainTool @?= "echo_tool"
        toolDescription langchainTool @?= "Echoes inputs"
        res <- toolExecute langchainTool (Aeson.object [])
        case res of
          Left err -> assertFailure ("Tool execution failed: " ++ show err)
          Right out -> assertBool "Executed stdio tool" ("Executed MCP tool" `T.isInfixOf` out)
    , testCase "mcpToolToStreamingTool yields output chunks and returns final text" $ do
        let client = newStdioMcpClient "test-server" "echo" []
            toolInfo =
              McpToolInfo
                { mcpToolName = "stream_tool"
                , mcpToolDescription = "Streaming MCP tool"
                , mcpToolInputSchema = Aeson.object []
                }
            sTool = mcpToolToStreamingTool client toolInfo
        toolName (streamingToolDef sTool) @?= "stream_tool"
        chunksRef <- newIORef ([] :: [T.Text])
        eRes <-
          runConduit
            ( streamingToolExecute sTool (Aeson.object [])
                `fuseUpstream` CL.mapM_ (\c -> modifyIORef' chunksRef (++ [c]))
            )
        case eRes of
          Left err -> assertFailure ("Streaming tool failed: " ++ show err)
          Right out -> do
            assertBool "Got final text" ("Executed MCP tool" `T.isInfixOf` out)
            chunks <- readIORef chunksRef
            case chunks of
              [chunk] -> assertBool "Yielded chunk" ("Executed MCP tool" `T.isInfixOf` chunk)
              _ -> assertFailure ("Expected 1 chunk, got: " ++ show (length chunks))
    , testCase "listMcpResources returns server resources" $ do
        let client = newStdioMcpClient "test-server" "echo" []
        eRes <- runExceptT $ listMcpResources client
        case eRes of
          Left err -> assertFailure ("listMcpResources failed: " ++ show err)
          Right [r] -> do
            mcpResourceUri r @?= "mock://resource/1"
            mcpResourceName r @?= "mock_resource"
            mcpResourceMimeType r @?= Just "text/plain"
          Right other -> assertFailure ("Expected 1 resource, got: " ++ show (length other))
    , testCase "readMcpResource reads resource content by uri" $ do
        let client = newStdioMcpClient "test-server" "echo" []
        eRes <- runExceptT $ readMcpResource client "mock://resource/1"
        case eRes of
          Left err -> assertFailure ("readMcpResource failed: " ++ show err)
          Right content ->
            assertBool "Got resource content" ("Mock content for resource" `T.isInfixOf` content)
    , testCase "listMcpPrompts returns prompt descriptors and arguments" $ do
        let client = newStdioMcpClient "test-server" "echo" []
        eRes <- runExceptT $ listMcpPrompts client
        case eRes of
          Left err -> assertFailure ("listMcpPrompts failed: " ++ show err)
          Right [p] -> do
            mcpPromptName p @?= "mock_prompt"
            mcpPromptDescription p @?= Just "Mock prompt template"
            case mcpPromptArguments p of
              [arg] -> do
                promptArgName arg @?= "topic"
                promptArgRequired arg @?= Just True
              args -> assertFailure ("Expected 1 argument, got: " ++ show (length args))
          Right other -> assertFailure ("Expected 1 prompt, got: " ++ show (length other))
    , testCase "getMcpPrompt maps prompt template messages to Langchain Messages" $ do
        let client = newStdioMcpClient "test-server" "echo" []
            args = Map.fromList [("topic", "Haskell")]
        eRes <- runExceptT $ getMcpPrompt client "explain" (Just args)
        case eRes of
          Left err -> assertFailure ("getMcpPrompt failed: " ++ show err)
          Right [m] -> do
            messageRole m @?= User
            assertBool "Contains prompt topic" ("Haskell" `T.isInfixOf` extractMessageText m)
          Right other -> assertFailure ("Expected 1 message, got: " ++ show (length other))
    ]