mcp-server 0.1.0.8 → 0.1.0.9
raw patch · 8 files changed
+639/−490 lines, 8 filesdep +hspecdep ~QuickCheckdep ~aesondep ~basePVP ok
version bump matches the API change (PVP)
Dependencies added: hspec
Dependency ranges changed: QuickCheck, aeson, base, containers, network-uri, template-haskell, text
API changes (from Hackage documentation)
Files
- mcp-server.cabal +28/−20
- test/HspecMain.hs +17/−0
- test/Main.hs +0/−470
- test/Spec/AdvancedDerivation.hs +135/−0
- test/Spec/BasicDerivation.hs +83/−0
- test/Spec/JSONConversion.hs +82/−0
- test/Spec/SchemaValidation.hs +106/−0
- test/TestData.hs +188/−0
mcp-server.cabal view
@@ -15,7 +15,7 @@ -- PVP summary: +-+------- breaking API changes -- | | +----- non-breaking API additions -- | | | +--- code changes with no API change-version: 0.1.0.8+version: 0.1.0.9 -- A short (one-line) description of the package. synopsis: Library for building Model Context Protocol (MCP) servers -- A longer description of the package.@@ -74,11 +74,11 @@ -- Other library packages from which modules are imported. build-depends: aeson >=2.0 && <3.0,- base ^>=4.20.0.0,+ base >=4.20.0 && <4.22, bytestring >=0.10 && <0.13, containers >=0.6 && <0.8, network-uri >=2.6 && <2.8,- template-haskell >=2.16 && <2.23,+ template-haskell >=2.16 && <2.24, text >=1.2 && <3.0, -- Directories containing source files.@@ -97,11 +97,11 @@ -- other-extensions: -- Other library packages from which modules are imported. build-depends:- base ^>=4.20.0.0,- containers >=0.6 && <0.8,+ base,+ containers, mcp-server,- network-uri >=2.6 && <2.8,- text >=1.2 && <3.0,+ network-uri,+ text, -- Directories containing source files. hs-source-dirs: app@@ -119,9 +119,9 @@ -- other-extensions: -- Other library packages from which modules are imported. build-depends:- base ^>=4.20.0.0,+ base, mcp-server,- text >=1.2 && <3.0,+ text, -- Directories containing source files. hs-source-dirs: examples/Simple@@ -139,9 +139,9 @@ -- other-extensions: -- Other library packages from which modules are imported. build-depends:- base ^>=4.20.0.0,+ base, mcp-server,- text >=1.2 && <3.0,+ text, -- Directories containing source files. hs-source-dirs: examples/Complete@@ -154,7 +154,14 @@ -- Base language which the package is written in. default-language: GHC2024 -- Modules included in this executable, other than Main.- other-modules: TestTypes+ other-modules:+ Spec.AdvancedDerivation+ Spec.BasicDerivation+ Spec.JSONConversion+ Spec.SchemaValidation+ TestData+ TestTypes+ -- LANGUAGE extensions used by modules in this package. -- other-extensions: -- The interface type and version of the test suite.@@ -162,14 +169,15 @@ -- Directories containing source files. hs-source-dirs: test -- The entrypoint to the test suite.- main-is: Main.hs+ main-is: HspecMain.hs -- Test dependencies. build-depends:- QuickCheck >=2.14 && <2.16,- aeson >=2.0 && <3.0,- base ^>=4.20.0.0,- containers >=0.6 && <0.8,+ QuickCheck,+ aeson,+ base,+ containers,+ hspec, mcp-server,- network-uri >=2.6 && <2.8,- template-haskell >=2.16 && <2.23,- text >=1.2 && <3.0,+ network-uri,+ template-haskell,+ text,
+ test/HspecMain.hs view
@@ -0,0 +1,17 @@+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Test.Hspec+import qualified Spec.JSONConversion+import qualified Spec.BasicDerivation+import qualified Spec.SchemaValidation+import qualified Spec.AdvancedDerivation++main :: IO ()+main = hspec $ do+ describe "MCP Server" $ do+ Spec.JSONConversion.spec+ Spec.BasicDerivation.spec+ Spec.SchemaValidation.spec+ Spec.AdvancedDerivation.spec
− test/Main.hs
@@ -1,470 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}--module Main (main) where--import Data.Aeson (Value(..), toJSON)-import Data.Text (Text)-import qualified Data.Text as T-import MCP.Server-import MCP.Server.Derive-import Network.URI (parseURI)-import Test.QuickCheck-import TestTypes-import Text.Read (readMaybe)---- =============================================================================--- JSON Type Conversion Tests--- =============================================================================---- Test conversion from JSON Value to Text and back-prop_intRoundTrip :: Int -> Bool-prop_intRoundTrip i = - let jsonVal = toJSON i- textVal = jsonValueToText jsonVal- parsed = case readMaybe (T.unpack textVal) of- Just result -> result- Nothing -> error $ "Failed to parse Int from: " <> T.unpack textVal- in parsed == i--prop_boolRoundTrip :: Bool -> Bool-prop_boolRoundTrip b = - let jsonVal = toJSON b- textVal = jsonValueToText jsonVal- parsed = case T.toLower textVal of- "true" -> True- "false" -> False- _ -> error $ "Failed to parse Bool from: " <> T.unpack textVal- in parsed == b--prop_textRoundTrip :: Text -> Bool-prop_textRoundTrip t = - let jsonVal = toJSON t- textVal = jsonValueToText jsonVal- in textVal == t---- Test the specific conversion functions we use in the derivation-testIntConversion :: IO Bool-testIntConversion = do- let testCases = [0, 42, -17, 999999] :: [Int]- results <- mapM (\i -> do- let jsonVal = toJSON i- textVal = jsonValueToText jsonVal- parsed = case readMaybe (T.unpack textVal) of- Just result -> result- Nothing -> error $ "Failed to parse: " <> T.unpack textVal- return (parsed == (i :: Int))- ) testCases- return (all id results)--testBoolConversion :: IO Bool-testBoolConversion = do- let testCases = [True, False]- results <- mapM (\b -> do- let jsonVal = toJSON b- textVal = jsonValueToText jsonVal- parsed = case T.toLower textVal of- "true" -> True- "false" -> False- _ -> error $ "Failed to parse: " <> T.unpack textVal- return (parsed == b)- ) testCases- return (all id results)--testTextConversion :: IO Bool-testTextConversion = do- let testCases = ["hello", "world", "", "123", "true", "false"]- results <- mapM (\t -> do- let jsonVal = toJSON t- textVal = jsonValueToText jsonVal- return (textVal == t)- ) testCases- return (all id results)---- Test JSON Value to Text conversion specifically-testJsonValueToText :: IO Bool-testJsonValueToText = do- let tests = - [ (Number 42, "42") -- Whole numbers become integers- , (Number 42.5, "42.5") -- Decimals stay as decimals- , (String "hello", "hello")- , (Bool True, "true")- , (Bool False, "false")- , (Null, "")- ]- - let results = map (\(input, expected) -> jsonValueToText input == expected) tests- return (all id results)--runJsonTypesTests :: IO Bool-runJsonTypesTests = do- putStrLn "\n=== JSON Type Conversion Tests ==="- - -- QuickCheck property tests- putStr "Testing Int round-trip property: "- quickCheck prop_intRoundTrip- - putStr "Testing Bool round-trip property: "- quickCheck prop_boolRoundTrip- - -- Manual conversion tests- putStr "Testing Int conversion: "- intResult <- testIntConversion- putStrLn $ if intResult then "PASS" else "FAIL"- - putStr "Testing Bool conversion: "- boolResult <- testBoolConversion- putStrLn $ if boolResult then "PASS" else "FAIL"- - putStr "Testing Text conversion: "- textResult <- testTextConversion- putStrLn $ if textResult then "PASS" else "FAIL"- - putStr "Testing jsonValueToText function: "- jsonResult <- testJsonValueToText- putStrLn $ if jsonResult then "PASS" else "FAIL"- - let allPassed = intResult && boolResult && textResult && jsonResult- putStrLn $ "JSON Types result: " ++ if allPassed then "PASS" else "FAIL"- return allPassed---- =============================================================================--- End-to-End Derivation Tests--- =============================================================================---- Generate handlers using Template Haskell (importing types from TestTypes module)-testPromptHandlers :: (PromptListHandler IO, PromptGetHandler IO)-testPromptHandlers = $(derivePromptHandler ''TestPrompt 'handleTestPrompt)--testResourceHandlers :: (ResourceListHandler IO, ResourceReadHandler IO)-testResourceHandlers = $(deriveResourceHandler ''TestResource 'handleTestResource)--testToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)-testToolHandlers = $(deriveToolHandler ''TestTool 'handleTestTool)---- Test handlers with custom descriptions-testToolHandlersWithDescriptions :: (ToolListHandler IO, ToolCallHandler IO)-testToolHandlersWithDescriptions = $(deriveToolHandlerWithDescription ''TestTool 'handleTestTool testDescriptions)---- Test case for separate parameter types approach (should fail with current implementation)-testSeparateParamsToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)-testSeparateParamsToolHandlers = $(deriveToolHandler ''SeparateParamsTool 'handleSeparateParamsTool)---- Test case for recursive parameter types-testRecursiveToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)-testRecursiveToolHandlers = $(deriveToolHandler ''RecursiveTool 'handleRecursiveTool)---- Test case for separate parameter types with descriptions-testSeparateParamsToolHandlersWithDescriptions :: (ToolListHandler IO, ToolCallHandler IO)-testSeparateParamsToolHandlersWithDescriptions = $(deriveToolHandlerWithDescription ''SeparateParamsTool 'handleSeparateParamsTool separateParamsDescriptions)---- Test case for recursive parameter types with descriptions-testRecursiveToolHandlersWithDescriptions :: (ToolListHandler IO, ToolCallHandler IO)-testRecursiveToolHandlersWithDescriptions = $(deriveToolHandlerWithDescription ''RecursiveTool 'handleRecursiveTool separateParamsDescriptions)---- End-to-end test functions-testPromptDerivation :: IO Bool-testPromptDerivation = do- let (_, getHandler) = testPromptHandlers- - -- Test simple prompt- result1 <- getHandler "simple_prompt" [("message", "hello")]- let test1 = case result1 of- Right (ContentText content) -> content == "Simple prompt: hello"- _ -> False- - -- Test complex prompt with multiple types- result2 <- getHandler "complex_prompt" [("title", "urgent task"), ("priority", "5"), ("urgent", "true")]- let test2 = case result2 of- Right (ContentText content) -> content == "Complex prompt: urgent task (priority=5, urgent=True)"- _ -> False- - -- Test optional prompt with missing optional field- result3 <- getHandler "optional_prompt" [("required", "test")]- let test3 = case result3 of- Right (ContentText content) -> content == "Optional prompt: test"- _ -> False- - -- Test optional prompt with optional field present- result4 <- getHandler "optional_prompt" [("required", "test"), ("optional", "42")]- let test4 = case result4 of- Right (ContentText content) -> content == "Optional prompt: test optional=42"- _ -> False- - return (test1 && test2 && test3 && test4)--testSeparateParamsDerivation :: IO Bool-testSeparateParamsDerivation = do- putStrLn "Testing separate parameter types derivation..."- - -- Test the separate parameter types approach- let (listHandler, callHandler) = testSeparateParamsToolHandlers- - -- First test that tool definitions are generated correctly- toolDefs <- listHandler- let getValueDef = filter (\def -> toolDefinitionName def == "get_value") toolDefs- let setValueDef = filter (\def -> toolDefinitionName def == "set_value") toolDefs- - let schemaTest = case (getValueDef, setValueDef) of- ([getDef], [setDef]) -> - let getProps = case toolDefinitionInputSchema getDef of- InputSchemaDefinitionObject props _ -> map fst props- setProps = case toolDefinitionInputSchema setDef of - InputSchemaDefinitionObject props _ -> map fst props- in "_gvpKey" `elem` getProps && "_svpKey" `elem` setProps && "_svpValue" `elem` setProps- _ -> False- - putStrLn $ " Schema generation: " ++ (if schemaTest then "PASS" else "FAIL")- - -- Test GetValue with separate params- result1 <- callHandler "get_value" [("_gvpKey", "mykey")]- let test1 = case result1 of- Right (ContentText content) -> content == "Getting value for key: mykey"- _ -> False- - putStrLn $ " GetValue execution: " ++ (if test1 then "PASS" else "FAIL")- - -- Test SetValue with separate params - result2 <- callHandler "set_value" [("_svpKey", "mykey"), ("_svpValue", "myvalue")]- let test2 = case result2 of- Right (ContentText content) -> content == "Setting mykey = myvalue"- _ -> False- - putStrLn $ " SetValue execution: " ++ (if test2 then "PASS" else "FAIL")- - -- Test recursive parameter types- let (recursiveListHandler, recursiveCallHandler) = testRecursiveToolHandlers- recursiveToolDefs <- recursiveListHandler- let processDataDef = filter (\def -> toolDefinitionName def == "process_data") recursiveToolDefs- - let recursiveSchemaTest = case processDataDef of- [def] -> case toolDefinitionInputSchema def of- InputSchemaDefinitionObject props _ -> - let propNames = map fst props- in "_ipName" `elem` propNames && "_ipAge" `elem` propNames- _ -> False- - putStrLn $ " Recursive schema generation: " ++ (if recursiveSchemaTest then "PASS" else "FAIL")- - -- Test recursive execution- result3 <- recursiveCallHandler "process_data" [("_ipName", "Alice"), ("_ipAge", "30")]- let test3 = case result3 of- Right (ContentText content) -> content == "Processing data for Alice (age 30)"- _ -> False- - putStrLn $ " Recursive execution: " ++ (if test3 then "PASS" else "FAIL")- - -- Test descriptions with separate parameter types- let (descListHandler, _) = testSeparateParamsToolHandlersWithDescriptions- descToolDefs <- descListHandler- let getValueDefWithDesc = filter (\def -> toolDefinitionName def == "get_value") descToolDefs- let setValueDefWithDesc = filter (\def -> toolDefinitionName def == "set_value") descToolDefs- - let descriptionTest = case (getValueDefWithDesc, setValueDefWithDesc) of- ([getDef], [setDef]) -> - let getDefDescCorrect = toolDefinitionDescription getDef == "Retrieves a value from the key-value store"- setDefDescCorrect = toolDefinitionDescription setDef == "Sets a value in the key-value store"- getFieldDescsCorrect = case toolDefinitionInputSchema getDef of- InputSchemaDefinitionObject props _ -> - case lookup "_gvpKey" props of- Just prop -> propertyDescription prop == "The key to retrieve the value for"- Nothing -> False- setFieldDescsCorrect = case toolDefinitionInputSchema setDef of- InputSchemaDefinitionObject props _ ->- let keyDesc = case lookup "_svpKey" props of- Just prop -> propertyDescription prop == "The key to set the value for"- Nothing -> False- valueDesc = case lookup "_svpValue" props of- Just prop -> propertyDescription prop == "The value to store"- Nothing -> False- in keyDesc && valueDesc- in getDefDescCorrect && setDefDescCorrect && getFieldDescsCorrect && setFieldDescsCorrect- _ -> False- - putStrLn $ " Description support: " ++ (if descriptionTest then "PASS" else "FAIL")- - -- Test recursive descriptions- let (recursiveDescListHandler, _) = testRecursiveToolHandlersWithDescriptions- recursiveDescToolDefs <- recursiveDescListHandler- let processDataDefWithDesc = filter (\def -> toolDefinitionName def == "process_data") recursiveDescToolDefs- - let recursiveDescTest = case processDataDefWithDesc of- [procDef] -> - let procDefDescCorrect = toolDefinitionDescription procDef == "Processes user data with age validation"- procFieldDescsCorrect = case toolDefinitionInputSchema procDef of- InputSchemaDefinitionObject props _ ->- let nameDesc = case lookup "_ipName" props of- Just prop -> propertyDescription prop == "The person's full name"- Nothing -> False- ageDesc = case lookup "_ipAge" props of- Just prop -> propertyDescription prop == "The person's age in years"- Nothing -> False- in nameDesc && ageDesc- in procDefDescCorrect && procFieldDescsCorrect- _ -> False- - putStrLn $ " Recursive description support: " ++ (if recursiveDescTest then "PASS" else "FAIL")- - return (schemaTest && test1 && test2 && recursiveSchemaTest && test3 && descriptionTest && recursiveDescTest)--testResourceDerivation :: IO Bool-testResourceDerivation = do- let (_, readHandler) = testResourceHandlers- - -- Test simple resource- case parseURI "resource://config_file" of- Just uri1 -> do- result1 <- readHandler uri1- let test1 = case result1 of- Right (ContentText content) -> T.isInfixOf "Config file contents" content- _ -> False- - -- Test parameterized resource (this would need to match actual schema)- case parseURI "resource://database_connection" of- Just uri2 -> do- result2 <- readHandler uri2- let test2 = case result2 of- Right (ContentText content) -> T.isInfixOf "Database at" content- _ -> False- return (test1 && test2)- Nothing -> return False- Nothing -> return False--testToolDerivation :: IO Bool-testToolDerivation = do- let (_, callHandler) = testToolHandlers- - -- Test simple tool- result1 <- callHandler "echo" [("text", "hello world")]- let test1 = case result1 of- Right (ContentText content) -> content == "Echo: hello world"- _ -> False- - -- Test tool with multiple typed parameters- result2 <- callHandler "calculate" [("operation", "add"), ("x", "10"), ("y", "5")]- let test2 = case result2 of- Right (ContentText content) -> content == "15"- _ -> False- - -- Test tool with boolean parameter- result3 <- callHandler "toggle" [("flag", "true")]- let test3 = case result3 of- Right (ContentText content) -> content == "Flag is now: False"- _ -> False- - -- Test tool with optional parameters- result4 <- callHandler "search" [("query", "test"), ("limit", "10")]- let test4 = case result4 of- Right (ContentText content) -> T.isInfixOf "Search results for 'test'" content && T.isInfixOf "(limit=10)" content- _ -> False- - return (test1 && test2 && test3 && test4)--testCustomDescriptions :: IO Bool-testCustomDescriptions = do- let (toolListHandler, _) = testToolHandlersWithDescriptions- - toolDefs <- toolListHandler- - -- Find the Echo tool definition and check its description- let echoDef = filter (\def -> toolDefinitionName def == "echo") toolDefs- let echoDescTest = case echoDef of- [def] -> toolDefinitionDescription def == "Echoes the input text back to the user"- _ -> False- - -- Find the Calculate tool definition and check its description and field descriptions- let calculateDef = filter (\def -> toolDefinitionName def == "calculate") toolDefs- let calculateTest = case calculateDef of- [def] -> - let correctToolDesc = toolDefinitionDescription def == "Performs mathematical calculations"- correctFieldDescs = case toolDefinitionInputSchema def of- InputSchemaDefinitionObject props _ ->- let textDesc = case lookup "text" props of- Just prop -> propertyDescription prop == "The text to echo back"- Nothing -> True -- text field doesn't exist in Calculate, that's fine- operationDesc = case lookup "operation" props of- Just prop -> propertyDescription prop == "The mathematical operation to perform"- Nothing -> False- xDesc = case lookup "x" props of- Just prop -> propertyDescription prop == "The first number"- Nothing -> False- yDesc = case lookup "y" props of- Just prop -> propertyDescription prop == "The second number"- Nothing -> False- in operationDesc && xDesc && yDesc- in correctToolDesc && correctFieldDescs- _ -> False- - return (echoDescTest && calculateTest)--testSchemaGeneration :: IO Bool-testSchemaGeneration = do- let (toolListHandler, _) = testToolHandlers- - toolDefs <- toolListHandler- - -- Find the Calculate tool definition- let calculateDef = filter (\def -> toolDefinitionName def == "calculate") toolDefs- case calculateDef of- [def] -> case toolDefinitionInputSchema def of- InputSchemaDefinitionObject props required ->- let hasXInt = case lookup "x" props of- Just prop -> propertyType prop == "integer"- Nothing -> False- hasYInt = case lookup "y" props of- Just prop -> propertyType prop == "integer"- Nothing -> False- hasOpString = case lookup "operation" props of- Just prop -> propertyType prop == "string"- Nothing -> False- hasRequiredFields = all (`elem` required) ["operation", "x", "y"]- in return (hasXInt && hasYInt && hasOpString && hasRequiredFields)- _ -> return False--runEndToEndTests :: IO Bool-runEndToEndTests = do- putStrLn "\n=== End-to-End Derivation Tests ==="- - putStr "Testing prompt derivation: "- promptResult <- testPromptDerivation- putStrLn $ if promptResult then "PASS" else "FAIL"- - putStr "Testing resource derivation: "- resourceResult <- testResourceDerivation- putStrLn $ if resourceResult then "PASS" else "FAIL"- - putStr "Testing tool derivation: "- toolResult <- testToolDerivation- putStrLn $ if toolResult then "PASS" else "FAIL"- - putStr "Testing schema generation: "- schemaResult <- testSchemaGeneration- putStrLn $ if schemaResult then "PASS" else "FAIL"- - putStr "Testing custom descriptions: "- descriptionResult <- testCustomDescriptions- putStrLn $ if descriptionResult then "PASS" else "FAIL"- - putStr "Testing separate parameter types: "- separateParamsResult <- testSeparateParamsDerivation- putStrLn $ if separateParamsResult then "PASS" else "FAIL"- - let allPassed = promptResult && resourceResult && toolResult && schemaResult && descriptionResult && separateParamsResult- putStrLn $ "End-to-End result: " ++ if allPassed then "PASS" else "FAIL"- return allPassed---- =============================================================================--- Main Test Runner--- =============================================================================--main :: IO ()-main = do- putStrLn "MCP Server Test Suite"- putStrLn "===================="- - jsonResult <- runJsonTypesTests- endToEndResult <- runEndToEndTests- - let overallResult = jsonResult && endToEndResult- putStrLn $ "\n===================="- putStrLn $ "Overall result: " ++ if overallResult then "ALL TESTS PASSED" else "SOME TESTS FAILED"
+ test/Spec/AdvancedDerivation.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Spec.AdvancedDerivation (spec) where++import Control.Monad (forM_)+import Data.Text (Text)+import qualified Data.Text as T+import MCP.Server+import MCP.Server.Derive+import Test.Hspec+import TestTypes+import TestData++-- Generate advanced handlers for separate parameter types+testSeparateParamsToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)+testSeparateParamsToolHandlers = $(deriveToolHandler ''SeparateParamsTool 'handleSeparateParamsTool)++testRecursiveToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)+testRecursiveToolHandlers = $(deriveToolHandler ''RecursiveTool 'handleRecursiveTool)++testSeparateParamsToolHandlersWithDescriptions :: (ToolListHandler IO, ToolCallHandler IO)+testSeparateParamsToolHandlersWithDescriptions = $(deriveToolHandlerWithDescription ''SeparateParamsTool 'handleSeparateParamsTool separateParamsDescriptions)++testRecursiveToolHandlersWithDescriptions :: (ToolListHandler IO, ToolCallHandler IO)+testRecursiveToolHandlersWithDescriptions = $(deriveToolHandlerWithDescription ''RecursiveTool 'handleRecursiveTool separateParamsDescriptions)++-- Helper functions for advanced testing+findToolByName :: Text -> [ToolDefinition] -> ToolDefinition+findToolByName toolName toolDefs = + case filter (\def -> toolDefinitionName def == toolName) toolDefs of+ [def] -> def+ [] -> error $ "Tool not found: " ++ T.unpack toolName+ _ -> error $ "Multiple tools found with name: " ++ T.unpack toolName++assertSchemaHasProperties :: [Text] -> ToolDefinition -> IO ()+assertSchemaHasProperties expectedProps toolDef = do+ case toolDefinitionInputSchema toolDef of+ InputSchemaDefinitionObject props _ -> + let propNames = map fst props+ in all (`elem` propNames) expectedProps `shouldBe` True+ other -> expectationFailure $ "Expected InputSchemaDefinitionObject but got: " ++ show other++assertToolCallResult :: (ToolCallHandler IO) -> Text -> [(Text, Text)] -> Text -> IO ()+assertToolCallResult handler toolName args expectedContent = do+ result <- handler toolName args+ case result of+ Right (ContentText content) -> content `shouldBe` expectedContent+ other -> expectationFailure $ "Expected ContentText but got: " ++ show other++assertToolHasDescription :: Text -> Text -> ToolDefinition -> IO ()+assertToolHasDescription toolName expectedDesc toolDef = do+ toolDefinitionName toolDef `shouldBe` toolName+ toolDefinitionDescription toolDef `shouldBe` expectedDesc++assertPropertyHasDescription :: Text -> Text -> ToolDefinition -> IO ()+assertPropertyHasDescription propName expectedDesc toolDef = do+ case toolDefinitionInputSchema toolDef of+ InputSchemaDefinitionObject props _ -> + case lookup propName props of+ Just prop -> propertyDescription prop `shouldBe` expectedDesc+ Nothing -> expectationFailure $ "Property not found: " ++ T.unpack propName+ other -> expectationFailure $ "Expected InputSchemaDefinitionObject but got: " ++ show other++spec :: Spec+spec = describe "Advanced Template Haskell Derivation" $ do+ + describe "Separate Parameter Types" $ do+ it "generates correct schema for separate parameter tools" $ do+ let (listHandler, _) = testSeparateParamsToolHandlers+ toolDefs <- listHandler+ + -- Test GetValue tool schema+ let getValueDef = findToolByName "get_value" toolDefs+ assertSchemaHasProperties ["_gvpKey"] getValueDef+ + -- Test SetValue tool schema + let setValueDef = findToolByName "set_value" toolDefs+ assertSchemaHasProperties ["_svpKey", "_svpValue"] setValueDef+ + forM_ separateParamsTestCases $ \testCase ->+ it (T.unpack $ sepTestDescription testCase) $ do+ let (_, callHandler) = testSeparateParamsToolHandlers+ assertToolCallResult callHandler (sepToolName testCase) (sepArgs testCase) (sepExpectedResult testCase)++ describe "Recursive Parameter Types" $ do+ it "generates correct schema for recursive parameter tools" $ do+ let (listHandler, _) = testRecursiveToolHandlers+ toolDefs <- listHandler+ + let processDataDef = findToolByName "process_data" toolDefs+ assertSchemaHasProperties ["_ipName", "_ipAge"] processDataDef+ + forM_ recursiveParamsTestCases $ \testCase ->+ it (T.unpack $ recTestDescription testCase) $ do+ let (_, callHandler) = testRecursiveToolHandlers+ assertToolCallResult callHandler (recToolName testCase) (recArgs testCase) (recExpectedResult testCase)++ describe "Custom Descriptions with Separate Parameters" $ do+ it "applies correct tool descriptions for separate parameter tools" $ do+ let (listHandler, _) = testSeparateParamsToolHandlersWithDescriptions+ toolDefs <- listHandler+ + let getValueDef = findToolByName "get_value" toolDefs+ assertToolHasDescription "get_value" "Retrieves a value from the key-value store" getValueDef+ + let setValueDef = findToolByName "set_value" toolDefs+ assertToolHasDescription "set_value" "Sets a value in the key-value store" setValueDef+ + it "applies correct field descriptions for separate parameter tools" $ do+ let (listHandler, _) = testSeparateParamsToolHandlersWithDescriptions+ toolDefs <- listHandler+ + let getValueDef = findToolByName "get_value" toolDefs+ assertPropertyHasDescription "_gvpKey" "The key to retrieve the value for" getValueDef+ + let setValueDef = findToolByName "set_value" toolDefs+ assertPropertyHasDescription "_svpKey" "The key to set the value for" setValueDef+ assertPropertyHasDescription "_svpValue" "The value to store" setValueDef++ describe "Recursive Tool Descriptions" $ do+ it "applies correct descriptions for recursive parameter tools" $ do+ let (listHandler, _) = testRecursiveToolHandlersWithDescriptions+ toolDefs <- listHandler+ + let processDataDef = findToolByName "process_data" toolDefs+ assertToolHasDescription "process_data" "Processes user data with age validation" processDataDef+ + it "applies correct field descriptions for recursive parameters" $ do+ let (listHandler, _) = testRecursiveToolHandlersWithDescriptions+ toolDefs <- listHandler+ + let processDataDef = findToolByName "process_data" toolDefs+ assertPropertyHasDescription "_ipName" "The person's full name" processDataDef+ assertPropertyHasDescription "_ipAge" "The person's age in years" processDataDef
+ test/Spec/BasicDerivation.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Spec.BasicDerivation (spec) where++import Control.Monad (forM_)+import Data.Text (Text)+import qualified Data.Text as T+import MCP.Server+import MCP.Server.Derive+import Network.URI (parseURI)+import Test.Hspec+import TestTypes+import TestData++-- Generate handlers using Template Haskell+testPromptHandlers :: (PromptListHandler IO, PromptGetHandler IO)+testPromptHandlers = $(derivePromptHandler ''TestPrompt 'handleTestPrompt)++testResourceHandlers :: (ResourceListHandler IO, ResourceReadHandler IO)+testResourceHandlers = $(deriveResourceHandler ''TestResource 'handleTestResource)++testToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)+testToolHandlers = $(deriveToolHandler ''TestTool 'handleTestTool)++-- Helper functions for common test patterns+shouldReturnContentText :: IO (Either Error Content) -> Text -> IO ()+shouldReturnContentText action expected = do+ result <- action+ case result of+ Right (ContentText content) -> content `shouldBe` expected+ other -> expectationFailure $ "Expected ContentText '" ++ T.unpack expected ++ "' but got: " ++ show other++shouldContainText :: IO (Either Error Content) -> Text -> IO ()+shouldContainText action expectedSubstring = do+ result <- action+ case result of+ Right (ContentText content) -> + T.isInfixOf expectedSubstring content `shouldBe` True+ other -> expectationFailure $ "Expected ContentText containing '" ++ T.unpack expectedSubstring ++ "' but got: " ++ show other++testPromptCall :: PromptGetHandler IO -> Text -> [(Text, Text)] -> Text -> IO ()+testPromptCall handler name args expected = + shouldReturnContentText (handler name args) expected++testResourceCall :: ResourceReadHandler IO -> String -> Text -> IO ()+testResourceCall handler uriString expected = do+ case parseURI uriString of+ Just uri -> shouldReturnContentText (handler uri) expected+ Nothing -> expectationFailure $ "Failed to parse URI: " ++ uriString++testToolCall :: ToolCallHandler IO -> Text -> [(Text, Text)] -> Text -> IO ()+testToolCall handler name args expected = + shouldReturnContentText (handler name args) expected++spec :: Spec+spec = describe "Basic Template Haskell Derivation" $ do+ + describe "Prompt derivation" $ do+ let (_, getHandler) = testPromptHandlers+ + forM_ promptTestCases $ \testCase ->+ it (T.unpack $ testDescription testCase) $ + testPromptCall getHandler (promptName testCase) (promptArgs testCase) (expectedResult testCase)++ describe "Resource derivation" $ do+ let (_, readHandler) = testResourceHandlers+ + forM_ resourceTestCases $ \testCase ->+ it (T.unpack $ resourceTestDescription testCase) $ do+ case parseURI (T.unpack $ resourceUri testCase) of+ Just uri -> + if useSubstringMatch testCase+ then shouldContainText (readHandler uri) (resourceExpectedContent testCase)+ else testResourceCall readHandler (T.unpack $ resourceUri testCase) (resourceExpectedContent testCase)+ Nothing -> expectationFailure $ "Failed to parse URI: " ++ T.unpack (resourceUri testCase)++ describe "Tool derivation" $ do+ let (_, callHandler) = testToolHandlers+ + forM_ toolTestCases $ \testCase ->+ it (T.unpack $ toolTestDescription testCase) $ + testToolCall callHandler (toolName testCase) (toolArgs testCase) (toolExpectedResult testCase)
+ test/Spec/JSONConversion.hs view
@@ -0,0 +1,82 @@+{-# LANGUAGE OverloadedStrings #-}++module Spec.JSONConversion (spec) where++import Data.Aeson (Value(..), toJSON)+import Data.Text (Text)+import qualified Data.Text as T+import MCP.Server+import Test.Hspec+import Test.QuickCheck+import Text.Read (readMaybe)++-- Arbitrary instance for Text (needed for QuickCheck)+instance Arbitrary Text where+ arbitrary = T.pack <$> arbitrary++spec :: Spec+spec = describe "JSON Type Conversion" $ do+ describe "Property-based tests" $ do+ it "round-trips integers correctly" $ property prop_intRoundTrip+ it "round-trips booleans correctly" $ property prop_boolRoundTrip + it "round-trips text correctly" $ property prop_textRoundTrip++ describe "Manual conversion tests" $ do+ it "converts integers manually" testIntConversion+ it "converts booleans manually" testBoolConversion++-- Fixed property-based tests (no more 'error' usage)+prop_intRoundTrip :: Int -> Bool+prop_intRoundTrip i = + let jsonVal = toJSON i+ textVal = jsonValueToText jsonVal+ parsed = readMaybe (T.unpack textVal)+ in parsed == Just i++prop_boolRoundTrip :: Bool -> Bool+prop_boolRoundTrip b = + let jsonVal = toJSON b+ textVal = jsonValueToText jsonVal+ parsed = case T.toLower textVal of+ "true" -> Just True+ "false" -> Just False+ _ -> Nothing+ in parsed == Just b++prop_textRoundTrip :: Text -> Bool+prop_textRoundTrip t = + let jsonVal = toJSON t+ textVal = jsonValueToText jsonVal+ in textVal == t++-- Manual test functions converted to Hspec style+testIntConversion :: IO ()+testIntConversion = do+ let testCases = [0, 42, -17, 999999] :: [Int]+ results <- mapM testSingleInt testCases+ all id results `shouldBe` True+ where+ testSingleInt i = do+ let jsonVal = toJSON i+ textVal = jsonValueToText jsonVal+ case readMaybe (T.unpack textVal) of+ Just parsed -> return (parsed == i)+ Nothing -> do+ expectationFailure $ "Failed to parse Int from: " <> T.unpack textVal+ return False++testBoolConversion :: IO ()+testBoolConversion = do+ let testCases = [True, False]+ results <- mapM testSingleBool testCases+ all id results `shouldBe` True+ where+ testSingleBool b = do+ let jsonVal = toJSON b+ textVal = jsonValueToText jsonVal+ expected = if b then "true" else "false"+ case T.toLower textVal of+ result | result == expected -> return True+ result -> do+ expectationFailure $ "Expected " <> T.unpack expected <> " but got " <> T.unpack result+ return False
+ test/Spec/SchemaValidation.hs view
@@ -0,0 +1,106 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}++module Spec.SchemaValidation (spec) where++import Control.Monad (forM_)+import Data.Text (Text)+import qualified Data.Text as T+import MCP.Server+import MCP.Server.Derive+import Test.Hspec+import TestTypes+import TestData++-- Generate handlers for testing+testToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)+testToolHandlers = $(deriveToolHandler ''TestTool 'handleTestTool)++testToolHandlersWithDescriptions :: (ToolListHandler IO, ToolCallHandler IO)+testToolHandlersWithDescriptions = $(deriveToolHandlerWithDescription ''TestTool 'handleTestTool testDescriptions)++-- Helper functions for schema validation+findTool :: Text -> [ToolDefinition] -> ToolDefinition+findTool toolName toolDefs = + case filter (\def -> toolDefinitionName def == toolName) toolDefs of+ [def] -> def+ [] -> error $ "Tool not found: " ++ T.unpack toolName+ _ -> error $ "Multiple tools found with name: " ++ T.unpack toolName++getProperty :: Text -> ToolDefinition -> InputSchemaDefinitionProperty+getProperty propertyName toolDef = + case toolDefinitionInputSchema toolDef of+ InputSchemaDefinitionObject props _ -> + case lookup propertyName props of+ Just prop -> prop+ Nothing -> error $ "Property not found: " ++ T.unpack propertyName+ other -> error $ "Expected InputSchemaDefinitionObject but got: " ++ show other++assertToolExists :: Text -> [ToolDefinition] -> IO ()+assertToolExists toolName toolDefs = do+ toolDefs `shouldSatisfy` any (\def -> toolDefinitionName def == toolName)++assertHasProperty :: Text -> Text -> ToolDefinition -> IO ()+assertHasProperty propertyName expectedType toolDef = do+ let prop = getProperty propertyName toolDef+ propertyType prop `shouldBe` expectedType++assertPropertyDescription :: Text -> ToolDefinition -> Text -> IO ()+assertPropertyDescription propertyName toolDef expectedDesc = do+ let prop = getProperty propertyName toolDef+ propertyDescription prop `shouldBe` expectedDesc++assertRequiredFields :: [Text] -> ToolDefinition -> IO ()+assertRequiredFields expectedFields toolDef = do+ case toolDefinitionInputSchema toolDef of+ InputSchemaDefinitionObject _ required -> + all (`elem` required) expectedFields `shouldBe` True+ other -> expectationFailure $ "Expected InputSchemaDefinitionObject but got: " ++ show other++assertToolDescription :: ToolDefinition -> Text -> IO ()+assertToolDescription toolDef expectedDesc = + toolDefinitionDescription toolDef `shouldBe` expectedDesc++spec :: Spec+spec = describe "Schema Validation and Custom Descriptions" $ do+ + describe "Schema Generation" $ do+ let (listHandler, _) = testToolHandlers+ + forM_ schemaTestCases $ \testCase -> do+ it (T.unpack $ schemaTestDescription testCase) $ do+ toolDefs <- listHandler+ + assertToolExists (schemaToolName testCase) toolDefs+ let toolDef = findTool (schemaToolName testCase) toolDefs+ + -- Validate property types+ forM_ (expectedProperties testCase) $ \(propName, expectedType) ->+ assertHasProperty propName expectedType toolDef+ + -- Validate required fields+ assertRequiredFields (requiredFields testCase) toolDef++ describe "Custom Descriptions" $ do+ it "applies correct tool descriptions" $ do+ let (listHandler, _) = testToolHandlersWithDescriptions+ toolDefs <- listHandler+ + assertToolExists "echo" toolDefs+ let echoDef = findTool "echo" toolDefs+ assertToolDescription echoDef "Echoes the input text back to the user"+ + assertToolExists "calculate" toolDefs+ let calculateDef = findTool "calculate" toolDefs+ assertToolDescription calculateDef "Performs mathematical calculations"+ + it "applies correct field descriptions for Calculate tool" $ do+ let (listHandler, _) = testToolHandlersWithDescriptions+ toolDefs <- listHandler+ + assertToolExists "calculate" toolDefs+ let calculateDef = findTool "calculate" toolDefs+ + assertPropertyDescription "operation" calculateDef "The mathematical operation to perform"+ assertPropertyDescription "x" calculateDef "The first number" + assertPropertyDescription "y" calculateDef "The second number"
+ test/TestData.hs view
@@ -0,0 +1,188 @@+{-# LANGUAGE OverloadedStrings #-}++module TestData + ( -- * Prompt Test Data+ promptTestCases+ , PromptTestCase(..)+ + -- * Resource Test Data + , resourceTestCases+ , ResourceTestCase(..)+ + -- * Tool Test Data+ , toolTestCases+ , ToolTestCase(..)+ + -- * Schema Test Data+ , schemaTestCases+ , SchemaTestCase(..)+ + -- * Advanced Derivation Test Data+ , separateParamsTestCases+ , recursiveParamsTestCases+ , SeparateParamsTestCase(..)+ , RecursiveParamsTestCase(..)+ ) where++import Data.Text (Text)++-- | Test case for prompt derivation+data PromptTestCase = PromptTestCase+ { promptName :: Text+ , promptArgs :: [(Text, Text)]+ , expectedResult :: Text+ , testDescription :: Text+ } deriving (Show, Eq)++-- | Test case for resource derivation +data ResourceTestCase = ResourceTestCase+ { resourceUri :: Text+ , resourceExpectedContent :: Text+ , useSubstringMatch :: Bool -- True for substring match, False for exact match+ , resourceTestDescription :: Text+ } deriving (Show, Eq)++-- | Test case for tool derivation+data ToolTestCase = ToolTestCase+ { toolName :: Text+ , toolArgs :: [(Text, Text)]+ , toolExpectedResult :: Text+ , toolTestDescription :: Text+ } deriving (Show, Eq)++-- | Test case for schema validation+data SchemaTestCase = SchemaTestCase+ { schemaToolName :: Text+ , expectedProperties :: [(Text, Text)] -- (property name, expected type)+ , requiredFields :: [Text]+ , schemaTestDescription :: Text+ } deriving (Show, Eq)++-- | Test case for separate parameter types+data SeparateParamsTestCase = SeparateParamsTestCase+ { sepToolName :: Text+ , sepArgs :: [(Text, Text)]+ , sepExpectedResult :: Text+ , sepExpectedProperties :: [Text]+ , sepTestDescription :: Text+ } deriving (Show, Eq)++-- | Test case for recursive parameter types+data RecursiveParamsTestCase = RecursiveParamsTestCase+ { recToolName :: Text+ , recArgs :: [(Text, Text)]+ , recExpectedResult :: Text+ , recExpectedProperties :: [Text]+ , recTestDescription :: Text+ } deriving (Show, Eq)++-- | Prompt test cases+promptTestCases :: [PromptTestCase]+promptTestCases =+ [ PromptTestCase+ { promptName = "simple_prompt"+ , promptArgs = [("message", "hello")]+ , expectedResult = "Simple prompt: hello"+ , testDescription = "handles simple prompts correctly"+ }+ , PromptTestCase+ { promptName = "complex_prompt"+ , promptArgs = [("title", "urgent task"), ("priority", "5"), ("urgent", "true")]+ , expectedResult = "Complex prompt: urgent task (priority=5, urgent=True)"+ , testDescription = "handles complex prompts with multiple types"+ }+ , PromptTestCase+ { promptName = "optional_prompt"+ , promptArgs = [("required", "test")]+ , expectedResult = "Optional prompt: test"+ , testDescription = "handles optional prompts with missing optional field"+ }+ , PromptTestCase+ { promptName = "optional_prompt"+ , promptArgs = [("required", "test"), ("optional", "42")]+ , expectedResult = "Optional prompt: test optional=42"+ , testDescription = "handles optional prompts with optional field present"+ }+ ]++-- | Resource test cases+resourceTestCases :: [ResourceTestCase]+resourceTestCases =+ [ ResourceTestCase+ { resourceUri = "resource://config_file"+ , resourceExpectedContent = "Config file contents"+ , useSubstringMatch = True+ , resourceTestDescription = "handles simple resources correctly"+ }+ , ResourceTestCase+ { resourceUri = "resource://database_connection"+ , resourceExpectedContent = "Database at localhost:5432"+ , useSubstringMatch = False+ , resourceTestDescription = "handles parameterized resources correctly"+ }+ ]++-- | Tool test cases+toolTestCases :: [ToolTestCase]+toolTestCases =+ [ ToolTestCase+ { toolName = "calculate"+ , toolArgs = [("operation", "add"), ("x", "10"), ("y", "5")]+ , toolExpectedResult = "15"+ , toolTestDescription = "handles simple tool calls correctly"+ }+ , ToolTestCase+ { toolName = "calculate"+ , toolArgs = [("operation", "multiply"), ("x", "7"), ("y", "6")]+ , toolExpectedResult = "42"+ , toolTestDescription = "handles tool calls with different operations"+ }+ ]++-- | Schema test cases+schemaTestCases :: [SchemaTestCase]+schemaTestCases =+ [ SchemaTestCase+ { schemaToolName = "calculate"+ , expectedProperties = [("x", "integer"), ("y", "integer"), ("operation", "string")]+ , requiredFields = ["operation", "x", "y"]+ , schemaTestDescription = "generates correct schema for Calculate tool"+ }+ , SchemaTestCase+ { schemaToolName = "echo"+ , expectedProperties = [("text", "string")]+ , requiredFields = ["text"]+ , schemaTestDescription = "generates correct schema for Echo tool"+ }+ ]++-- | Separate parameter types test cases+separateParamsTestCases :: [SeparateParamsTestCase]+separateParamsTestCases =+ [ SeparateParamsTestCase+ { sepToolName = "get_value"+ , sepArgs = [("_gvpKey", "mykey")]+ , sepExpectedResult = "Getting value for key: mykey"+ , sepExpectedProperties = ["_gvpKey"]+ , sepTestDescription = "executes GetValue with separate parameters correctly"+ }+ , SeparateParamsTestCase+ { sepToolName = "set_value"+ , sepArgs = [("_svpKey", "mykey"), ("_svpValue", "myvalue")]+ , sepExpectedResult = "Setting mykey = myvalue"+ , sepExpectedProperties = ["_svpKey", "_svpValue"]+ , sepTestDescription = "executes SetValue with separate parameters correctly"+ }+ ]++-- | Recursive parameter types test cases+recursiveParamsTestCases :: [RecursiveParamsTestCase]+recursiveParamsTestCases =+ [ RecursiveParamsTestCase+ { recToolName = "process_data"+ , recArgs = [("_ipName", "Alice"), ("_ipAge", "30")]+ , recExpectedResult = "Processing data for Alice (age 30)"+ , recExpectedProperties = ["_ipName", "_ipAge"]+ , recTestDescription = "executes recursive parameter tools correctly"+ }+ ]