mcp-server-0.1.0.13: test/Spec/UnicodeHandling.hs
{-# LANGUAGE OverloadedStrings #-}
module Spec.UnicodeHandling (spec) where
import Data.Aeson
import qualified Data.Aeson.KeyMap as KM
import qualified Data.ByteString.Lazy as BSL
import qualified Data.Text as T
import qualified Data.Text.Encoding as TE
import MCP.Server.Protocol
import MCP.Server.Types
import System.IO (hSetEncoding, stderr, stdout, utf8)
import Test.Hspec
spec :: Spec
spec = describe "Unicode Handling" $ do
describe "Unicode in Content Types" $ do
it "handles Unicode in ContentText" $ do
-- Set UTF-8 encoding for this test
hSetEncoding stdout utf8
hSetEncoding stderr utf8
let content = ContentText "Test with Unicode: √π∞≈∑"
let json = toJSON content
case json of
Object obj -> case KM.lookup "text" obj of
Just (String txt) -> "√π∞≈∑" `T.isInfixOf` txt `shouldBe` True
_ -> expectationFailure "Expected String in text field"
_ -> expectationFailure "Expected Object"
it "handles Unicode in ResourceContent" $ do
uri <- case parseURI "test://unicode" of
Just u -> return u
Nothing -> fail "Invalid URI"
let resource = ResourceText uri "text/plain" "Unicode content: ∀x∈ℝ: √x²=|x|"
let json = toJSON resource
case json of
Object obj -> case KM.lookup "text" obj of
Just (String txt) -> "∀x∈ℝ: √x²=|x|" `T.isInfixOf` txt `shouldBe` True
_ -> expectationFailure "Expected String in text field"
_ -> expectationFailure "Expected Object"
it "handles square root symbol specifically" $ do
let content = ContentText "Square root: √16 = 4"
let json = toJSON content
case json of
Object obj -> case KM.lookup "text" obj of
Just (String txt) -> txt `shouldBe` "Square root: √16 = 4"
_ -> expectationFailure "Expected String in text field"
_ -> expectationFailure "Expected Object"
describe "JSON Encoding/Decoding with Unicode" $ do
it "properly encodes and decodes Unicode content" $ do
let originalContent = ContentText "Mathematical symbols: ∀∃∈∉∪∩⊆⊇√∑∏∞≤≥≠≈π∅Ω∆∇"
let encoded = encode originalContent
let decoded = eitherDecode encoded
case decoded of
Right decodedContent -> decodedContent `shouldBe` originalContent
Left err -> expectationFailure $ "Failed to decode: " ++ err
it "handles Unicode in JSON-RPC messages" $ do
let response = ToolsCallResponse
{ toolsCallContent = [ContentText "Result: √16 = 4, π ≈ 3.14159"]
, toolsCallIsError = Nothing
}
let encoded = encode response
-- Just verify the JSON can be encoded and contains Unicode
BSL.length encoded `shouldSatisfy` (> 0)
describe "UTF-8 Text Encoding" $ do
it "properly handles UTF-8 byte sequences" $ do
let unicodeText = "Unicode: √∞≈π"
let utf8Bytes = TE.encodeUtf8 unicodeText
let decodedText = TE.decodeUtf8 utf8Bytes
decodedText `shouldBe` unicodeText
it "handles multibyte Unicode characters" $ do
let characters = ["√", "∞", "π", "∑", "∏", "∀", "∃", "∈", "∉"]
mapM_ (\char -> do
let encoded = TE.encodeUtf8 char
let decoded = TE.decodeUtf8 encoded
decoded `shouldBe` char
) characters
describe "Protocol Messages with Unicode" $ do
it "handles Unicode in tool responses" $ do
let response = ToolsCallResponse
{ toolsCallContent = [ContentText "Mathematical result: √(π²+e²) ≈ 4.53"]
, toolsCallIsError = Nothing
}
let json = toJSON response
-- Verify the JSON can be encoded and contains Unicode
let encoded = encode json
BSL.length encoded `shouldSatisfy` (> 0)
it "handles Unicode in prompt responses" $ do
let message = PromptMessage RoleUser (ContentText "Formula: E=mc², √(x²+y²)")
let response = PromptsGetResponse
{ promptsGetDescription = Just "Mathematical formulas with symbols: ∀∃"
, promptsGetMessages = [message]
}
let json = toJSON response
let encoded = encode json
BSL.length encoded `shouldSatisfy` (> 0)
it "handles Unicode in error messages" $ do
let mcpError = InvalidParams "Invalid formula: expected √x not ∛x"
let json = toJSON mcpError
case json of
Object obj -> case KM.lookup "message" obj of
Just (String msg) -> "√" `T.isInfixOf` msg `shouldBe` True
_ -> expectationFailure "Expected message field"
_ -> expectationFailure "Expected Object"
describe "Manual Handler Tests with Unicode" $ do
it "tests Unicode content through manual prompt handler" $ do
let promptHandler :: PromptName -> [(ArgumentName, ArgumentValue)] -> IO (Either Error Content)
promptHandler "unicode_test" args = do
case lookup "formula" args of
Just formula -> return $ Right $ ContentText $ "Formula result: " <> formula <> " → √solution"
Nothing -> return $ Left $ MissingRequiredParams "formula"
promptHandler name _ = return $ Left $ InvalidPromptName name
result <- promptHandler "unicode_test" [("formula", "x² + y² = z²")]
case result of
Right (ContentText txt) -> do
txt `shouldSatisfy` T.isInfixOf "√"
txt `shouldSatisfy` T.isInfixOf "²"
_ -> expectationFailure "Expected successful ContentText with Unicode"
it "tests Unicode content through manual tool handler" $ do
let toolHandler :: ToolName -> [(ArgumentName, ArgumentValue)] -> IO (Either Error Content)
toolHandler "calculate" args = do
case lookup "expression" args of
Just expr -> return $ Right $ ContentText $ "Calculation: " <> expr <> " = √result"
Nothing -> return $ Left $ MissingRequiredParams "expression"
toolHandler name _ = return $ Left $ UnknownTool name
result <- toolHandler "calculate" [("expression", "∫₀^∞ e^(-x²) dx")]
case result of
Right (ContentText txt) -> do
txt `shouldSatisfy` T.isInfixOf "√"
txt `shouldSatisfy` T.isInfixOf "∫"
_ -> expectationFailure "Expected successful ContentText with Unicode"
it "tests Unicode content through manual resource handler" $ do
let resourceHandler :: URI -> IO (Either Error ResourceContent)
resourceHandler uri = do
let uriStr = show uri
if "unicode" `T.isInfixOf` T.pack uriStr
then return $ Right $ ResourceText uri "text/plain" "Unicode symbols: ∀∃∈∉√∑∏∞≤≥≠≈π"
else return $ Left $ ResourceNotFound $ T.pack uriStr
uri <- case parseURI "resource://unicode_test" of
Just u -> return u
Nothing -> fail "Invalid URI"
result <- resourceHandler uri
case result of
Right (ResourceText _ _ txt) -> do
txt `shouldSatisfy` T.isInfixOf "∀∃∈∉"
txt `shouldSatisfy` T.isInfixOf "√∑∏∞"
_ -> expectationFailure "Expected successful ResourceText with Unicode"
describe "Integration test simulating MCP workflow" $ do
it "handles complete Unicode workflow without Template Haskell" $ do
-- Create manual handlers with Unicode content
let promptListHandler = return [
PromptDefinition
{ promptDefinitionName = "math_formula"
, promptDefinitionDescription = "Generate mathematical formulas with Unicode: ∀∃∈√"
, promptDefinitionArguments = [ArgumentDefinition "formula" "Mathematical expression" True]
}
]
let promptGetHandler name args = case name of
"math_formula" -> case lookup "formula" args of
Just formula -> return $ Right $ ContentText $ "Formula: " <> formula <> " → √(solution)"
Nothing -> return $ Left $ MissingRequiredParams "formula"
_ -> return $ Left $ InvalidPromptName name
let resourceListHandler = return [
ResourceDefinition
{ resourceDefinitionURI = "resource://unicode_symbols"
, resourceDefinitionName = "unicode_symbols"
, resourceDefinitionDescription = Just "Unicode mathematical symbols: ∀∃∈∉√∑"
, resourceDefinitionMimeType = Just "text/plain"
}
]
let resourceReadHandler uri =
if show uri == "resource://unicode_symbols"
then return $ Right $ ResourceText uri "text/plain" "Symbols: ∀∃∈∉∪∩⊆⊇√∑∏∞≤≥≠≈π∅"
else return $ Left $ ResourceNotFound $ T.pack $ show uri
let toolListHandler = return [
ToolDefinition
{ toolDefinitionName = "calculate"
, toolDefinitionDescription = "Calculate with Unicode symbols: √∑∏"
, toolDefinitionInputSchema = InputSchemaDefinitionObject
{ properties = [("expression", InputSchemaDefinitionProperty "string" "Mathematical expression")]
, required = ["expression"]
}
}
]
let toolCallHandler name args = case name of
"calculate" -> case lookup "expression" args of
Just expr -> return $ Right $ ContentText $ "Result: " <> expr <> " → √answer"
Nothing -> return $ Left $ MissingRequiredParams "expression"
_ -> return $ Left $ UnknownTool name
let handlers = McpServerHandlers
{ prompts = Just (promptListHandler, promptGetHandler)
, resources = Just (resourceListHandler, resourceReadHandler)
, tools = Just (toolListHandler, toolCallHandler)
}
-- Test that all handlers work with Unicode
promptList <- fst $ case prompts handlers of Just h -> h; Nothing -> error "No prompts"
promptList `shouldSatisfy` (not . null)
resourceList <- fst $ case resources handlers of Just h -> h; Nothing -> error "No resources"
resourceList `shouldSatisfy` (not . null)
toolList <- fst $ case tools handlers of Just h -> h; Nothing -> error "No tools"
toolList `shouldSatisfy` (not . null)
-- Test actual Unicode handling
promptResult <- snd (case prompts handlers of Just h -> h; Nothing -> error "No prompts") "math_formula" [("formula", "√(x²+y²)")]
case promptResult of
Right (ContentText txt) -> txt `shouldSatisfy` T.isInfixOf "√"
_ -> expectationFailure "Expected successful prompt result"
uri <- case parseURI "resource://unicode_symbols" of
Just u -> return u
Nothing -> fail "Invalid URI"
resourceResult <- snd (case resources handlers of Just h -> h; Nothing -> error "No resources") uri
case resourceResult of
Right (ResourceText _ _ txt) -> txt `shouldSatisfy` T.isInfixOf "∀∃∈∉"
_ -> expectationFailure "Expected successful resource result"
toolResult <- snd (case tools handlers of Just h -> h; Nothing -> error "No tools") "calculate" [("expression", "∫₀^∞ e^(-x²) dx = √π/2")]
case toolResult of
Right (ContentText txt) -> do
txt `shouldSatisfy` T.isInfixOf "√"
txt `shouldSatisfy` T.isInfixOf "∫"
_ -> expectationFailure "Expected successful tool result"