packages feed

mcp-server-0.2.0.1: test/Spec/DefinitionMetadata.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell   #-}

-- | Coverage for definition metadata (ADR 0006): tool annotations, icons,
-- content annotations, and the options-based derive customization.
module Spec.DefinitionMetadata (spec) where

import Data.Aeson
import Data.Text (Text)
import MCP.Server
import MCP.Server.Derive
import Test.Hspec
import TestTypes

annotatedToolHandlers :: (ToolListHandler, ToolCallHandler)
annotatedToolHandlers = $(deriveToolHandlerWithOptions ''TestTool 'handleTestTool
  [ ("Echo", defaultDefinitionOptions
      { optDescription = Just "Echoes the input"
      , optTitle = Just "Echo"
      , optIcons = [icon "https://example.com/echo.png"]
      , optToolAnnotations = Just defaultToolAnnotations
          { toolReadOnlyHint = Just True
          , toolIdempotentHint = Just True
          }
      , optFieldDescriptions = [("text", "What to echo")]
      })
  , ("Calculate", defaultDefinitionOptions
      { optFieldDescriptions = [("x", "Calculate's first operand")]
      })
  ])

toolNamed :: Text -> [ToolDefinition] -> ToolDefinition
toolNamed n defs = case [d | d <- defs, toolDefinitionName d == n] of
  (d:_) -> d
  []    -> error $ "tool not found: " ++ show n

propDescription :: Text -> ToolDefinition -> Maybe Text
propDescription name def = case schemaShape (toolDefinitionInputSchema def) of
  SchemaObject props _ -> lookup name props >>= schemaDescription
  _                    -> Nothing

spec :: Spec
spec = describe "Definition metadata" $ do

  describe "JSON shapes" $ do
    it "serializes tool annotations with only the set hints" $ do
      toJSON defaultToolAnnotations { toolReadOnlyHint = Just True } `shouldBe`
        object ["readOnlyHint" .= True]

    it "serializes icons, omitting empty optional fields" $ do
      toJSON (icon "https://example.com/i.png") `shouldBe`
        object ["src" .= ("https://example.com/i.png" :: Text)]
      toJSON (Icon "u" (Just "image/png") ["48x48"]) `shouldBe`
        object ["src" .= ("u" :: Text), "mimeType" .= ("image/png" :: Text), "sizes" .= (["48x48"] :: [Text])]

    it "merges content annotations into the inner block" $ do
      let anns = defaultAnnotations { annotationsAudience = [RoleUser], annotationsPriority = Just 0.8 }
      toJSON (ContentAnnotated anns (ContentText "hi")) `shouldBe`
        object [ "type" .= ("text" :: Text), "text" .= ("hi" :: Text)
               , "annotations" .= object ["audience" .= (["user"] :: [Text]), "priority" .= (0.8 :: Double)]
               ]

    it "round-trips annotated content through FromJSON" $ do
      let anns = defaultAnnotations { annotationsAudience = [RoleAssistant] }
          c = ContentAnnotated anns (ContentText "hello")
      decode (encode c) `shouldBe` Just c

    it "round-trips resource links carrying icons" $ do
      let def = (mkResourceDefinition "resource://x" "x")
            { resourceDefinitionIcons = [Icon "https://example.com/x.png" (Just "image/png") ["48x48"]] }
          c = ContentResourceLink def
      decode (encode c) `shouldBe` Just c

  describe "Options-based derivation" $ do
    it "carries description, title, icons and annotations on the definition" $ do
      defs <- fst annotatedToolHandlers anonCtx
      let echoDef = toolNamed "echo" defs
      toolDefinitionDescription echoDef `shouldBe` "Echoes the input"
      toolDefinitionTitle echoDef `shouldBe` Just "Echo"
      toolDefinitionIcons echoDef `shouldBe` [icon "https://example.com/echo.png"]
      toolDefinitionAnnotations echoDef `shouldBe` Just defaultToolAnnotations
        { toolReadOnlyHint = Just True
        , toolIdempotentHint = Just True
        }

    it "leaves uncustomized constructors bare (constructor-name description)" $ do
      defs <- fst annotatedToolHandlers anonCtx
      let toggleDef = toolNamed "toggle" defs
      toolDefinitionDescription toggleDef `shouldBe` "Toggle"
      toolDefinitionAnnotations toggleDef `shouldBe` Nothing
      toolDefinitionIcons toggleDef `shouldBe` []

    it "scopes field descriptions to their constructor" $ do
      defs <- fst annotatedToolHandlers anonCtx
      -- 'text' described only on Echo; 'x' described only on Calculate
      propDescription "text" (toolNamed "echo" defs) `shouldBe` Just "What to echo"
      propDescription "x" (toolNamed "calculate" defs) `shouldBe` Just "Calculate's first operand"
      -- Echo's options must not leak onto Calculate's same-named args
      propDescription "operation" (toolNamed "calculate" defs) `shouldBe` Just "operation"

    it "still dispatches calls unchanged" $ do
      result <- snd annotatedToolHandlers anonCtx "echo" mempty
      case result of
        Left (MissingRequiredParams msg) -> show msg `shouldContain` "text"
        other -> expectationFailure $ "expected missing-params error, got: " ++ show other