mcp-server 0.1.0.1 → 0.1.0.2
raw patch · 5 files changed
+453/−31 lines, 5 filesdep +QuickCheckPVP: minor bump suggested
API additions: PVP suggests at least a minor version bump
Dependencies added: QuickCheck
API changes (from Hackage documentation)
+ MCP.Server: jsonValueToText :: Value -> Text
Files
- mcp-server.cabal +8/−2
- src/MCP/Server.hs +18/−2
- src/MCP/Server/Derive.hs +77/−26
- test/Main.hs +289/−1
- test/TestTypes.hs +61/−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.1+version: 0.1.0.2 -- A short (one-line) description of the package. synopsis: Library for building Model Context Protocol (MCP) servers -- A longer description of the package.@@ -157,7 +157,7 @@ -- Base language which the package is written in. default-language: GHC2024 -- Modules included in this executable, other than Main.- -- other-modules:+ other-modules: TestTypes -- LANGUAGE extensions used by modules in this package. -- other-extensions: -- The interface type and version of the test suite.@@ -168,5 +168,11 @@ main-is: Main.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, mcp-server,+ network-uri >=2.6 && <2.8,+ template-haskell >=2.16 && <2.23,+ text >=1.2 && <3.0,
src/MCP/Server.hs view
@@ -7,6 +7,9 @@ , runMcpServerStdIn , handleMcpMessage + -- * Utility Functions+ , jsonValueToText+ -- * Re-exports , module MCP.Server.Types ) where@@ -26,6 +29,19 @@ import MCP.Server.Protocol import MCP.Server.Types +-- | Convert JSON Value to Text representation suitable for handlers+jsonValueToText :: Value -> Text+jsonValueToText (String t) = t+jsonValueToText (Number n) = + -- Check if it's a whole number, if so format as integer+ if fromInteger (round n) == n+ then T.pack $ show (round n :: Integer)+ else T.pack $ show n+jsonValueToText (Bool True) = "true"+jsonValueToText (Bool False) = "false"+jsonValueToText Null = ""+jsonValueToText v = T.pack $ show v+ -- | Extract a brief summary of a JSON-RPC message for logging getMessageSummary :: JsonRpcMessage -> String getMessageSummary (JsonRpcMessageRequest req) = @@ -207,7 +223,7 @@ , errorData = Nothing } Success getReq -> do- let args = maybe [] (map (\(k, v) -> (k, T.pack $ show v)) . Map.toList) (promptsGetArguments getReq)+ let args = maybe [] (map (\(k, v) -> (k, jsonValueToText v)) . Map.toList) (promptsGetArguments getReq) result <- getHandler (promptsGetName getReq) args case result of Left err -> return $ makeErrorResponse (requestId req) $ JsonRpcError@@ -327,7 +343,7 @@ , errorData = Nothing } Success callReq -> do- let args = maybe [] (map (\(k, v) -> (k, T.pack $ show v)) . Map.toList) (toolsCallArguments callReq)+ let args = maybe [] (map (\(k, v) -> (k, jsonValueToText v)) . Map.toList) (toolsCallArguments callReq) result <- callHandler (toolsCallName callReq) args case result of Left err -> return $ makeErrorResponse (requestId req) $ JsonRpcError
src/MCP/Server/Derive.hs view
@@ -11,6 +11,7 @@ import qualified Data.Map as Map import qualified Data.Text as T import Language.Haskell.TH+import Text.Read (readMaybe) import MCP.Server.Types @@ -27,22 +28,22 @@ TyConI (DataD _ _ _ _ constructors _) -> do -- Generate prompt definitions promptDefs <- sequence $ map mkPromptDef constructors- + -- Generate list handler listHandlerExp <- [| \_cursor -> pure $ PaginatedResult { paginatedItems = $(return $ ListE promptDefs) , paginatedNextCursor = Nothing } |]- + -- Generate get handler with cases cases <- sequence $ map (mkPromptCase handlerName) constructors defaultCase <- [| pure $ Left $ InvalidPromptName $ "Unknown prompt: " <> name |] let defaultMatch = Match WildP (NormalB defaultCase) []- + getHandlerExp <- return $ LamE [VarP (mkName "name"), VarP (mkName "args")] $- CaseE (AppE (VarE 'T.unpack) (VarE (mkName "name"))) + CaseE (AppE (VarE 'T.unpack) (VarE (mkName "name"))) (map clauseToMatch cases ++ [defaultMatch])- + return $ TupE [Just listHandlerExp, Just getHandlerExp] _ -> fail $ "derivePromptHandler: " ++ show typeName ++ " is not a data type" @@ -68,7 +69,7 @@ mkArgDef (fieldName, _, fieldType) = do let isOptional = case fieldType of AppT (ConT n) _ -> nameBase n == "Maybe"- _ -> False+ _ -> False [| ArgumentDefinition { argumentDefinitionName = $(litE $ stringL $ nameBase fieldName) , argumentDefinitionDescription = $(litE $ stringL $ nameBase fieldName)@@ -111,20 +112,55 @@ buildNestedFieldValidation conName handlerName ((fieldName, _, fieldType):remainingFields) depth = do let fieldStr = nameBase fieldName- let isOptional = case fieldType of- AppT (ConT n) _ -> nameBase n == "Maybe"- _ -> False+ let (isOptional, innerType) = case fieldType of+ AppT (ConT n) inner | nameBase n == "Maybe" -> (True, inner)+ other -> (False, other) let fieldVar = mkName ("field" ++ show depth)- + continuation <- buildNestedFieldValidation conName handlerName remainingFields (depth + 1)- ++ -- Generate conversion expression based on type+ let convertExpr rawVar = case innerType of+ ConT n | nameBase n == "Int" ->+ [| case readMaybe (T.unpack $(varE rawVar)) of+ Just parsed -> parsed+ Nothing -> error $ "Failed to parse Int from: " <> T.unpack $(varE rawVar) |]+ ConT n | nameBase n == "Integer" ->+ [| case readMaybe (T.unpack $(varE rawVar)) of+ Just parsed -> parsed+ Nothing -> error $ "Failed to parse Integer from: " <> T.unpack $(varE rawVar) |]+ ConT n | nameBase n == "Double" ->+ [| case readMaybe (T.unpack $(varE rawVar)) of+ Just parsed -> parsed+ Nothing -> error $ "Failed to parse Double from: " <> T.unpack $(varE rawVar) |]+ ConT n | nameBase n == "Float" ->+ [| case readMaybe (T.unpack $(varE rawVar)) of+ Just parsed -> parsed+ Nothing -> error $ "Failed to parse Float from: " <> T.unpack $(varE rawVar) |]+ ConT n | nameBase n == "Bool" ->+ [| case T.toLower $(varE rawVar) of+ "true" -> True+ "false" -> False+ _ -> error $ "Failed to parse Bool from: " <> T.unpack $(varE rawVar) |]+ _ -> varE rawVar -- Text or other types, use as-is+ if isOptional- then [| do- let $(varP fieldVar) = Map.lookup $(litE $ stringL fieldStr) (Map.fromList args)- $(return continuation) |]- else [| case Map.lookup $(litE $ stringL fieldStr) (Map.fromList args) of- Just $(varP fieldVar) -> $(return continuation)- Nothing -> pure $ Left $ MissingRequiredParams $ "field '" <> $(litE $ stringL fieldStr) <> "' is missing" |]+ then do+ rawFieldVar <- newName ("raw" ++ show depth)+ convertedExpr <- convertExpr rawFieldVar+ [| do+ let $(varP fieldVar) = case Map.lookup $(litE $ stringL fieldStr) (Map.fromList args) of+ Nothing -> Nothing+ Just $(varP rawFieldVar) -> Just $(return convertedExpr)+ $(return continuation) |]+ else do+ rawFieldVar <- newName ("raw" ++ show depth)+ convertedExpr <- convertExpr rawFieldVar+ [| case Map.lookup $(litE $ stringL fieldStr) (Map.fromList args) of+ Just $(varP rawFieldVar) -> do+ let $(varP fieldVar) = $(return convertedExpr)+ $(return continuation)+ Nothing -> pure $ Left $ MissingRequiredParams $ "field '" <> $(litE $ stringL fieldStr) <> "' is missing" |] -- | Derive resource handlers from a data type -- Usage: $(deriveResourceHandler ''MyResource 'handleResource)@@ -135,7 +171,7 @@ TyConI (DataD _ _ _ _ constructors _) -> do -- Generate resource definitions resourceDefs <- sequence $ map mkResourceDef constructors- + listHandlerExp <- [| \_cursor -> pure $ PaginatedResult { paginatedItems = $(return $ ListE resourceDefs) , paginatedNextCursor = Nothing@@ -145,9 +181,9 @@ cases <- sequence $ map (mkResourceCase handlerName) constructors defaultCase <- [| pure $ Left $ ResourceNotFound $ "Resource not found: " <> T.pack unknown |] let defaultMatch = Match (VarP (mkName "unknown")) (NormalB defaultCase) []- + readHandlerExp <- return $ LamE [VarP (mkName "uri")] $- CaseE (AppE (VarE 'show) (VarE (mkName "uri"))) + CaseE (AppE (VarE 'show) (VarE (mkName "uri"))) (map clauseToMatch cases ++ [defaultMatch]) return $ TupE [Just listHandlerExp, Just readHandlerExp]@@ -183,7 +219,7 @@ TyConI (DataD _ _ _ _ constructors _) -> do -- Generate tool definitions toolDefs <- sequence $ map mkToolDef constructors- + listHandlerExp <- [| \_cursor -> pure $ PaginatedResult { paginatedItems = $(return $ ListE toolDefs) , paginatedNextCursor = Nothing@@ -193,9 +229,9 @@ cases <- sequence $ map (mkToolCase handlerName) constructors defaultCase <- [| pure $ Left $ UnknownTool $ "Unknown tool: " <> name |] let defaultMatch = Match WildP (NormalB defaultCase) []- + callHandlerExp <- return $ LamE [VarP (mkName "name"), VarP (mkName "args")] $- CaseE (AppE (VarE 'T.unpack) (VarE (mkName "name"))) + CaseE (AppE (VarE 'T.unpack) (VarE (mkName "name"))) (map clauseToMatch cases ++ [defaultMatch]) return $ TupE [Just listHandlerExp, Just callHandlerExp]@@ -218,7 +254,7 @@ requiredFields <- return $ map (\(fieldName, _, fieldType) -> let isOptional = case fieldType of AppT (ConT n) _ -> nameBase n == "Maybe"- _ -> False+ _ -> False in if isOptional then Nothing else Just (nameBase fieldName) ) fields let required = [f | Just f <- requiredFields]@@ -233,10 +269,25 @@ mkToolDef _ = fail "Unsupported constructor type for tools" mkProperty :: (Name, Bang, Type) -> Q Exp-mkProperty (fieldName, _, _) = do+mkProperty (fieldName, _, fieldType) = do let fieldStr = nameBase fieldName+ let jsonType = case fieldType of+ ConT n | nameBase n == "Int" -> "integer"+ ConT n | nameBase n == "Integer" -> "integer"+ ConT n | nameBase n == "Double" -> "number"+ ConT n | nameBase n == "Float" -> "number"+ ConT n | nameBase n == "Bool" -> "boolean"+ AppT (ConT maybeType) innerType | nameBase maybeType == "Maybe" ->+ case innerType of+ ConT n | nameBase n == "Int" -> "integer"+ ConT n | nameBase n == "Integer" -> "integer"+ ConT n | nameBase n == "Double" -> "number"+ ConT n | nameBase n == "Float" -> "number"+ ConT n | nameBase n == "Bool" -> "boolean"+ _ -> "string"+ _ -> "string" [| ($(litE $ stringL fieldStr), InputSchemaDefinitionProperty- { propertyType = "string"+ { propertyType = $(litE $ stringL jsonType) , propertyDescription = $(litE $ stringL fieldStr) }) |]
test/Main.hs view
@@ -1,4 +1,292 @@+{-# 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)++-- End-to-end test functions+testPromptDerivation :: IO Bool+testPromptDerivation = do+ let (_, getHandler) = testPromptHandlers+ + -- Test simple prompt+ result1 <- getHandler "simpleprompt" [("message", "hello")]+ let test1 = case result1 of+ Right (ContentText content) -> content == "Simple prompt: hello"+ _ -> False+ + -- Test complex prompt with multiple types+ result2 <- getHandler "complexprompt" [("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 "optionalprompt" [("required", "test")]+ let test3 = case result3 of+ Right (ContentText content) -> content == "Optional prompt: test"+ _ -> False+ + -- Test optional prompt with optional field present+ result4 <- getHandler "optionalprompt" [("required", "test"), ("optional", "42")]+ let test4 = case result4 of+ Right (ContentText content) -> content == "Optional prompt: test optional=42"+ _ -> False+ + return (test1 && test2 && test3 && test4)++testResourceDerivation :: IO Bool+testResourceDerivation = do+ let (_, readHandler) = testResourceHandlers+ + -- Test simple resource+ case parseURI "resource://configfile" 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://databaseconnection" 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)++testSchemaGeneration :: IO Bool+testSchemaGeneration = do+ let (toolListHandler, _) = testToolHandlers+ + paginatedResult <- toolListHandler Nothing+ let toolDefs = paginatedItems paginatedResult+ + -- 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"+ + let allPassed = promptResult && resourceResult && toolResult && schemaResult+ putStrLn $ "End-to-End result: " ++ if allPassed then "PASS" else "FAIL"+ return allPassed++-- =============================================================================+-- Main Test Runner+-- =============================================================================+ main :: IO ()-main = putStrLn "Test suite not yet implemented."+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/TestTypes.hs view
@@ -0,0 +1,61 @@+{-# LANGUAGE OverloadedStrings #-}++module TestTypes where++import Data.Text (Text)+import qualified Data.Text as T+import MCP.Server (Content(..))++-- Test data types for end-to-end testing+data TestPrompt+ = SimplePrompt { message :: Text }+ | ComplexPrompt { title :: Text, priority :: Int, urgent :: Bool }+ | OptionalPrompt { required :: Text, optional :: Maybe Int }+ deriving (Show, Eq)++data TestResource+ = ConfigFile+ | DatabaseConnection+ | UserProfile+ deriving (Show, Eq)++data TestTool+ = Echo { text :: Text }+ | Calculate { operation :: Text, x :: Int, y :: Int }+ | Toggle { flag :: Bool }+ | Search { query :: Text, limit :: Maybe Int, caseSensitive :: Maybe Bool }+ deriving (Show, Eq)++-- Handler functions+handleTestPrompt :: TestPrompt -> IO Content+handleTestPrompt (SimplePrompt msg) = + pure $ ContentText $ "Simple prompt: " <> msg+handleTestPrompt (ComplexPrompt title prio urgent) = + pure $ ContentText $ "Complex prompt: " <> title <> " (priority=" <> T.pack (show prio) <> ", urgent=" <> T.pack (show urgent) <> ")"+handleTestPrompt (OptionalPrompt req opt) = + pure $ ContentText $ "Optional prompt: " <> req <> maybe "" ((" optional=" <>) . T.pack . show) opt++handleTestResource :: TestResource -> IO Content+handleTestResource ConfigFile = + pure $ ContentText "Config file contents: debug=true, timeout=30"+handleTestResource DatabaseConnection = + pure $ ContentText "Database at localhost:5432"+handleTestResource UserProfile = + pure $ ContentText "User profile for ID 123"++handleTestTool :: TestTool -> IO Content+handleTestTool (Echo text) = + pure $ ContentText $ "Echo: " <> text+handleTestTool (Calculate op x y) = + let result = case op of+ "add" -> x + y+ "multiply" -> x * y+ "subtract" -> x - y+ _ -> 0+ in pure $ ContentText $ T.pack (show result)+handleTestTool (Toggle flag) = + pure $ ContentText $ "Flag is now: " <> T.pack (show (not flag))+handleTestTool (Search query limit caseSens) = + pure $ ContentText $ "Search results for '" <> query <> "'" <>+ maybe "" ((" (limit=" <>) . (<> ")") . T.pack . show) limit <>+ maybe "" ((" (case-sensitive=" <>) . (<> ")") . T.pack . show) caseSens