mcp-server-0.2.0.0: test/Spec/TypedArgs.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
-- | Coverage for the 0.2 typed-argument features: native JSON values,
-- enumerations, lists, nested records, isError results and multi-message
-- prompts.
module Spec.TypedArgs (spec) where
import Data.Aeson (Value (..), object, toJSON, (.=))
import Data.Text (Text)
import qualified Data.Text as T
import MCP.Server
import MCP.Server.Derive
import Test.Hspec
import TestHelpers
import TestTypes
typedHandlers :: (ToolListHandler, ToolCallHandler)
typedHandlers = $(deriveToolHandler ''TypedTool 'handleTypedTool)
convHandlers :: (PromptListHandler, PromptGetHandler)
convHandlers = $(derivePromptHandler ''ConvPrompt 'handleConvPrompt)
callTyped :: Text -> [(Text, Value)] -> IO (Either Error ToolResult)
callTyped name args = snd typedHandlers anonCtx name (valueArgs args)
toolNamed :: Text -> [ToolDefinition] -> ToolDefinition
toolNamed n defs = case [d | d <- defs, toolDefinitionName d == n] of
(d:_) -> d
[] -> error $ "tool not found: " ++ T.unpack n
expectInvalidParams :: Either Error ToolResult -> Text -> Expectation
expectInvalidParams (Left (InvalidParams msg)) expectedSubstring =
msg `shouldSatisfy` T.isInfixOf expectedSubstring
expectInvalidParams other expectedSubstring =
expectationFailure $ "Expected InvalidParams containing '" ++ T.unpack expectedSubstring ++ "' but got: " ++ show other
spec :: Spec
spec = describe "Typed tool arguments" $ do
describe "Enumerations" $ do
it "decodes enum arguments from strings" $ do
result <- callTyped "paint" [("color", String "green"), ("brightness", Number 5)]
result `shouldBeTextResult` "Painting Green at 5"
it "rejects unknown enum values with the allowed set" $ do
result <- callTyped "paint" [("color", String "purple"), ("brightness", Number 5)]
expectInvalidParams result "expected one of: red, green, blue"
it "generates a string schema with enum values" $ do
toolDefs <- fst typedHandlers anonCtx
let paintDef = toolNamed "paint" toolDefs
case lookupProp "color" paintDef of
Just prop -> schemaShape prop `shouldBe` SchemaString (Just ["red", "green", "blue"])
Nothing -> expectationFailure "color property missing"
describe "Native JSON values" $ do
it "accepts native numbers" $ do
result <- callTyped "paint" [("color", String "red"), ("brightness", Number 7)]
result `shouldBeTextResult` "Painting Red at 7"
it "still accepts numbers sent as strings (lenient)" $ do
result <- callTyped "paint" [("color", String "red"), ("brightness", String "7")]
result `shouldBeTextResult` "Painting Red at 7"
it "rejects non-integral numbers for Int fields" $ do
result <- callTyped "paint" [("color", String "red"), ("brightness", Number 7.5)]
expectInvalidParams result "field 'brightness'"
describe "Lists" $ do
it "decodes arrays of integers" $ do
result <- callTyped "bulk_add" [("values", toJSON [1 :: Int, 2, 3])]
result `shouldBeTextResult` "Sum: 6"
it "rejects mistyped array elements" $ do
result <- callTyped "bulk_add" [("values", toJSON [Number 1, String "x"])]
expectInvalidParams result "Failed to parse Int"
it "generates an array schema" $ do
toolDefs <- fst typedHandlers anonCtx
let bulkDef = toolNamed "bulk_add" toolDefs
fmap schemaTypeName (lookupProp "values" bulkDef) `shouldBe` Just "array"
describe "Nested records" $ do
it "decodes nested objects" $ do
result <- callTyped "query_data"
[("filters", object ["tags" .= (["a", "b"] :: [Text]), "maxCount" .= (7 :: Int)])]
result `shouldBeTextResult` "Query tags=a,b max=7"
it "treats missing optional nested fields as Nothing" $ do
result <- callTyped "query_data"
[("filters", object ["tags" .= (["a"] :: [Text])])]
result `shouldBeTextResult` "Query tags=a"
it "reports missing required nested fields with their path" $ do
result <- callTyped "query_data" [("filters", object [])]
expectInvalidParams result "field 'tags' is missing"
it "rejects non-object values for record fields" $ do
result <- callTyped "query_data" [("filters", String "nope")]
expectInvalidParams result "field 'filters'"
it "generates a nested object schema" $ do
toolDefs <- fst typedHandlers anonCtx
let queryDef = toolNamed "query_data" toolDefs
case lookupProp "filters" queryDef of
Just prop -> do
schemaTypeName prop `shouldBe` "object"
map fst (schemaProps prop) `shouldBe` ["tags", "maxCount"]
schemaRequired prop `shouldBe` ["tags"]
Nothing -> expectationFailure "filters property missing"
describe "Tool execution errors" $ do
it "reports handler failures as isError results, not protocol errors" $ do
result <- callTyped "always_fails" [("reason", String "boom")]
case result of
Right tr -> do
toolResultIsError tr `shouldBe` True
toolResultContent tr `shouldBe` [ContentText "boom"]
Left err -> expectationFailure $ "Expected isError result but got protocol error: " ++ show err
describe "ToToolResult list semantics" $ do
it "propagates isError when merging a list of results" $ do
let merged = toToolResult [toolError "boom", toolResult [ContentText "ok"]]
toolResultIsError merged `shouldBe` True
toolResultContent merged `shouldBe` [ContentText "boom", ContentText "ok"]
describe "Multi-message prompts" $ do
it "returns a described multi-message conversation with roles" $ do
result <- snd convHandlers anonCtx "conversation" (promptArgsMap [("topic", "haskell")])
case result of
Right (PromptResult desc msgs) -> do
desc `shouldBe` Just "About haskell"
map promptMessageRole msgs `shouldBe` [RoleUser, RoleAssistant]
Left err -> expectationFailure $ "Expected prompt result but got: " ++ show err