mcp-server-0.2.0.1: src/MCP/Server/Derive.hs
{-# LANGUAGE DeriveLift #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
-- | Template Haskell utilities for deriving MCP handlers from data types.
--
-- Tool arguments are decoded from full JSON values: records may contain
-- primitive fields ('Data.Text.Text', 'Int', 'Integer', 'Double', 'Float',
-- 'Bool'), optional fields ('Maybe'), lists, enumerations (data types whose
-- constructors are all nullary), and nested records — the generated
-- @inputSchema@ mirrors the same structure. Prompt arguments are
-- string-valued per the MCP specification, so prompt records are limited to
-- primitive and enumeration fields.
module MCP.Server.Derive
( -- * Template Haskell Derivation
derivePromptHandler
, derivePromptHandlerWithDescription
, derivePromptHandlerWithOptions
, deriveResourceHandler
, deriveResourceHandlerWithDescription
, deriveResourceHandlerWithOptions
, deriveResourceTemplates
, deriveResourceTemplatesWithDescription
, deriveResourceTemplatesWithOptions
, deriveToolHandler
, deriveToolHandlerWithDescription
, deriveToolHandlerWithOptions
, deriveToolHandlerWithOutput
, deriveToolHandlerWithOutputDescription
, deriveToolHandlerWithOutputOptions
-- * Per-constructor customization
, DefinitionOptions(..)
, defaultDefinitionOptions
) where
import Control.Monad (zipWithM)
import Data.Aeson (Value (..))
import Data.List (intercalate)
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Language.Haskell.TH
import Language.Haskell.TH.Syntax (Lift, lift)
import qualified Data.Char as Char
import MCP.Server.Derive.Internal
import MCP.Server.Types
-- | Per-constructor customization for the @WithOptions@ derivations: a
-- single mechanism for everything a definition can carry beyond its shape.
-- Entries are looked up by constructor name; for output-typed tool
-- derivations, an entry keyed by the /output type's/ name supplies its
-- field descriptions.
data DefinitionOptions = DefinitionOptions
{ optDescription :: Maybe Text
-- ^ The definition's description (falls back to the constructor name)
, optTitle :: Maybe Text
, optIcons :: [Icon]
, optToolAnnotations :: Maybe ToolAnnotations
-- ^ Behavioral hints; meaningful for tools, ignored elsewhere
, optFieldDescriptions :: [(Text, Text)]
-- ^ Field\/argument descriptions, scoped to this constructor — two
-- constructors may describe a same-named field differently
} deriving (Show, Eq, Lift)
-- | Nothing customized; record-update what you need.
defaultDefinitionOptions :: DefinitionOptions
defaultDefinitionOptions = DefinitionOptions
{ optDescription = Nothing
, optTitle = Nothing
, optIcons = []
, optToolAnnotations = Nothing
, optFieldDescriptions = []
}
-- | Look up a constructor's options.
optionsFor :: [(String, DefinitionOptions)] -> String -> DefinitionOptions
optionsFor opts name = fromMaybe defaultDefinitionOptions (lookup name opts)
-- | Adapt the legacy flat description list to per-constructor options:
-- constructor entries become 'optDescription'; the whole list also serves
-- as the (unscoped) field-description namespace, preserving the old
-- behavior exactly.
optionsFromDescriptions :: [(String, String)] -> String -> DefinitionOptions
optionsFromDescriptions descriptions name = defaultDefinitionOptions
{ optDescription = T.pack <$> lookup name descriptions
}
-- The merged field-description namespace for one constructor: its scoped
-- entries shadow the legacy global list.
fieldDescsFor :: DefinitionOptions -> [(String, String)] -> [(String, String)]
fieldDescsFor opts globalDescs =
[(T.unpack k, T.unpack v) | (k, v) <- optFieldDescriptions opts] ++ globalDescs
-------------------------------------------------------------------------------
-- Small helpers
-------------------------------------------------------------------------------
-- Helper function to convert PascalCase/camelCase to snake_case
--
-- >>> toSnakeCase "GetValue"
-- "get_value"
toSnakeCase :: String -> String
toSnakeCase [] = []
toSnakeCase (x:xs) = Char.toLower x : go xs
where
go [] = []
go (c:cs)
| Char.isUpper c = '_' : Char.toLower c : go cs
| otherwise = c : go cs
-- Snake-cased name from a TH Name
snakeName :: Name -> String
snakeName = toSnakeCase . nameBase
-- Check if a type is Maybe, and unwrap it
unwrapMaybe :: Type -> (Bool, Type)
unwrapMaybe (AppT (ConT n) inner) | nameBase n == "Maybe" = (True, inner)
unwrapMaybe other = (False, other)
-- Look up description with a default
descriptionFor :: [(String, String)] -> String -> String -> String
descriptionFor descriptions key fallback =
fromMaybe fallback (lookup key descriptions)
-- Helper function to convert Clause to Match
clauseToMatch :: Clause -> Match
clauseToMatch (Clause ps b ds) = Match (case ps of [p] -> p; _ -> TupP ps) b ds
-- The constructors of a named data (or newtype) declaration, if it is one
reifyCons :: Name -> Q (Maybe [Con])
reifyCons n = do
info <- reify n
pure $ case info of
TyConI (DataD _ _ _ _ cs _) -> Just cs
TyConI (NewtypeD _ _ _ _ c _) -> Just [c]
_ -> Nothing
isNullary :: Con -> Bool
isNullary (NormalC _ []) = True
isNullary _ = False
-- Get fields from a constructor: [] for nullary, record fields for RecC,
-- extracted fields for single-param NormalC
getConFields :: Con -> Q [(Name, Bang, Type)]
getConFields (NormalC _ []) = pure []
getConFields (RecC _ fields) = pure fields
getConFields (NormalC _ [(_bang, paramType)]) = extractFieldsFromParamType paramType
getConFields _ = fail "Unsupported constructor type"
-- Get the constructor name from a Con
conName :: Con -> Name
conName (NormalC n _) = n
conName (RecC n _) = n
conName (InfixC _ n _) = n
conName (ForallC _ _ c) = conName c
conName (GadtC (n:_) _ _) = n
conName (RecGadtC (n:_) _ _) = n
conName _ = error "conName: empty GADT constructor list"
-- Compute required field names (non-Maybe fields)
requiredFieldNames :: [(Name, Bang, Type)] -> [String]
requiredFieldNames fields =
[ nameBase fn | (fn, _, ft) <- fields, not (fst (unwrapMaybe ft)) ]
-- Extract field information from a (possibly wrapped) parameter record type
extractFieldsFromParamType :: Type -> Q [(Name, Bang, Type)]
extractFieldsFromParamType paramType = do
case paramType of
ConT typeName -> do
cons <- reifyCons typeName
case cons of
Just [RecC _ fields] -> return fields
Just [NormalC _ [(_bang, innerType)]] -> extractFieldsFromParamType innerType
_ -> fail $ "Parameter type " ++ show typeName ++ " must be a record type or single-parameter constructor"
_ -> fail $ "Parameter type must be a concrete type, got: " ++ show paramType
-- Names used to communicate between the generated lambda and its cases
ctxName, argNameN, argsName, uriName :: Name
ctxName = mkName "ctx"
argNameN = mkName "name"
argsName = mkName "args"
uriName = mkName "uri"
-------------------------------------------------------------------------------
-- Value parsers (tool arguments)
-------------------------------------------------------------------------------
-- Build a parser expression of type @Value -> Either Text a@ for a field type
mkValueParser :: Type -> Q Exp
mkValueParser ty = case ty of
ConT n | nameBase n == "Int" -> [| valueInt |]
| nameBase n == "Integer" -> [| valueInteger |]
| nameBase n == "Double" -> [| valueDouble |]
| nameBase n == "Float" -> [| valueFloat |]
| nameBase n == "Bool" -> [| valueBool |]
| nameBase n == "Text" -> [| valueText |]
AppT ListT inner -> [| valueArray $(mkValueParser inner) |]
ConT n -> do
cons <- reifyCons n
case cons of
Just cs | not (null cs), all isNullary cs -> mkEnumValueParser n cs
Just [RecC icn ifields] -> mkObjectParser n icn ifields
Just [NormalC icn [(_bang, inner)]] -> [| fmap $(conE icn) . $(mkValueParser inner) |]
_ -> fail $ "Unsupported tool argument type: " ++ show n
_ -> fail $ "Unsupported tool argument type: " ++ show ty
-- Parser for an enumeration: a JSON string matching a snake_cased constructor
mkEnumValueParser :: Name -> [Con] -> Q Exp
mkEnumValueParser tyName cons = do
textParser <- mkEnumTextParser tyName cons
[| \v -> case v of
String s -> $(return textParser) s
_ -> Left ("Failed to parse " <> $(litE $ stringL $ nameBase tyName) <> " from: " <> renderValue v) |]
-- Parser of type @Text -> Either Text a@ for an enumeration
mkEnumTextParser :: Name -> [Con] -> Q Exp
mkEnumTextParser tyName cons = do
let expected = intercalate ", " [snakeName (conName c) | c <- cons]
sVar <- newName "s"
matches <- mapM (enumMatch . conName) cons
fallback <- [| Left ("Failed to parse " <> $(litE $ stringL $ nameBase tyName)
<> " from: " <> $(varE sVar)
<> " (expected one of: " <> $(litE $ stringL expected) <> ")") |]
let defaultMatch = Match WildP (NormalB fallback) []
return $ LamE [VarP sVar] $
CaseE (AppE (VarE 'T.unpack) (VarE sVar)) (matches ++ [defaultMatch])
where
enumMatch cn = do
body <- [| Right $(conE cn) |]
return $ Match (LitP $ StringL $ snakeName cn) (NormalB body) []
-- Parser for a nested record: a JSON object decoded field by field
mkObjectParser :: Name -> Name -> [(Name, Bang, Type)] -> Q Exp
mkObjectParser tyName icn ifields = do
vVar <- newName "v"
oVar <- newName "o"
chain <- foldl (\acc f -> [| $acc <*> $(fieldExtractor oVar f) |])
[| pure $(conE icn) |]
ifields
fallback <- [| Left ("Failed to parse " <> $(litE $ stringL $ nameBase tyName)
<> " object from: " <> renderValue $(varE vVar)) |]
objP <- conP 'Object [varP oVar]
return $ LamE [VarP vVar] $
CaseE (VarE vVar)
[ Match objP (NormalB chain) []
, Match WildP (NormalB fallback) []
]
where
fieldExtractor oVar (fieldName, _, fieldType) = do
let (isOptional, innerType) = unwrapMaybe fieldType
let fname = litE $ stringL $ nameBase fieldName
if isOptional
then [| optionalField $fname $(mkValueParser innerType) $(varE oVar) |]
else [| requiredField $fname $(mkValueParser innerType) $(varE oVar) |]
-------------------------------------------------------------------------------
-- Text parsers (prompt arguments)
-------------------------------------------------------------------------------
-- Build a parser expression of type @Text -> Either Text a@ for a prompt field
mkTextParser :: Type -> Q Exp
mkTextParser ty = case ty of
ConT n | nameBase n == "Int" -> [| parseInt |]
| nameBase n == "Integer" -> [| parseInteger |]
| nameBase n == "Double" -> [| parseDouble |]
| nameBase n == "Float" -> [| parseFloat |]
| nameBase n == "Bool" -> [| parseBool |]
| nameBase n == "Text" -> [| parseText |]
ConT n -> do
cons <- reifyCons n
case cons of
Just cs | not (null cs), all isNullary cs -> mkEnumTextParser n cs
_ -> fail $ "Unsupported prompt argument type (prompt arguments are strings): " ++ show n
_ -> fail $ "Unsupported prompt argument type (prompt arguments are strings): " ++ show ty
-------------------------------------------------------------------------------
-- Value serializers (structured output)
-------------------------------------------------------------------------------
-- Build a serializer expression of type @a -> Value@ that mirrors
-- 'mkSchemaShape' exactly: what the schema promises is what the value
-- contains (snake_cased enums, 'Maybe' fields omitted when 'Nothing').
mkValueBuilder :: Type -> Q Exp
mkValueBuilder ty = case ty of
ConT n | nameBase n `elem` ["Int", "Integer", "Double", "Float", "Bool", "Text"] ->
[| toJSON |]
AppT ListT inner ->
[| \xs -> toJSON (map $(mkValueBuilder inner) xs) |]
ConT n -> do
cons <- reifyCons n
case cons of
Just cs | not (null cs), all isNullary cs -> do
vVar <- newName "v"
matches <- mapM enumMatch cs
return $ LamE [VarP vVar] $ CaseE (VarE vVar) matches
Just [RecC icn ifields] -> mkRecordBuilder icn ifields
Just [NormalC icn [(_bang, inner)]] -> do
xVar <- newName "x"
wrapped <- conP icn [varP xVar]
body <- [| $(mkValueBuilder inner) $(varE xVar) |]
return $ LamE [wrapped] body
_ -> fail $ "Unsupported output field type: " ++ show n
_ -> fail $ "Unsupported output field type: " ++ show ty
where
enumMatch c = do
let cn = conName c
body <- [| String $(litE $ stringL $ snakeName cn) |]
pat <- conP cn []
return $ Match pat (NormalB body) []
-- Serializer for a record: an object with one entry per field, omitting
-- 'Maybe' fields that are 'Nothing'. Fields are bound by pattern-matching
-- the constructor — never by accessor application, which would be ambiguous
-- for users with DuplicateRecordFields enabled.
mkRecordBuilder :: Name -> [(Name, Bang, Type)] -> Q Exp
mkRecordBuilder con fields = do
vars <- mapM (const $ newName "f") fields
pat <- conP con (map varP vars)
fieldExps <- zipWithM fieldPairs vars fields
body <- [| object (concat $(return $ ListE fieldExps)) |]
return $ LamE [pat] body
where
fieldPairs var (fieldName, _, fieldType) = do
let (isOptional, innerType) = unwrapMaybe fieldType
let fname = litE $ stringL $ nameBase fieldName
if isOptional
then [| omitNothing $fname $(mkValueBuilder innerType) $(varE var) |]
else [| [ $fname .= $(mkValueBuilder innerType) $(varE var) ] |]
-- The output type must (possibly through single-constructor wrappers)
-- resolve to a record: outputSchema is an object schema per the spec.
requireOutputRecord :: Name -> Q ()
requireOutputRecord n = do
cons <- reifyCons n
case cons of
Just [RecC _ (_:_)] -> pure ()
Just [NormalC _ [(_bang, ConT inner)]] -> requireOutputRecord inner
_ -> fail $ "Output type " ++ show n
++ " must be a record (outputSchema is an object schema)"
-------------------------------------------------------------------------------
-- Argument decoding for a constructor
-------------------------------------------------------------------------------
data ArgStyle = ToolArgs | ToolArgsOutput Exp | PromptArgs
-- Build a decoder expression of type @Either Error <ConType>@ reading from
-- the in-scope arguments map bound to 'argsName'.
mkConDecoder :: ArgStyle -> Con -> Q Exp
mkConDecoder _ (NormalC cn []) = [| Right $(conE cn) |]
mkConDecoder style (RecC cn fields) = mkFieldsDecoder style (conE cn) fields
mkConDecoder style (NormalC cn [(_bang, paramType)]) =
wrapParam paramType
where
wrapParam ty = case ty of
ConT typeName -> do
cons <- reifyCons typeName
case cons of
Just [RecC icn ifields] -> do
inner <- mkFieldsDecoder style (conE icn) ifields
[| $(conE cn) <$> $(return inner) |]
Just [NormalC icn [(_b, innerTy)]] -> do
innerCon <- mkConDecoder style (NormalC icn [(_b, innerTy)])
[| $(conE cn) <$> $(return innerCon) |]
_ -> fail $ "Parameter type " ++ show typeName ++ " must be a record type or single-parameter constructor"
_ -> fail $ "Parameter type must be a concrete type, got: " ++ show ty
mkConDecoder _ con = fail $ "Unsupported constructor type: " ++ show (conName con)
-- Applicative chain over the constructor's fields, reading from 'argsName'
mkFieldsDecoder :: ArgStyle -> Q Exp -> [(Name, Bang, Type)] -> Q Exp
mkFieldsDecoder style con fields =
foldl (\acc f -> [| $acc <*> $(argExtractor f) |]) [| pure $con |] fields
where
argExtractor (fieldName, _, fieldType) = do
let (isOptional, innerType) = unwrapMaybe fieldType
let fname = litE $ stringL $ nameBase fieldName
case style of
PromptArgs
| isOptional -> [| optionalTextArg $fname $(mkTextParser innerType) $(varE argsName) |]
| otherwise -> [| requiredTextArg $fname $(mkTextParser innerType) $(varE argsName) |]
_ -- tool styles share the Value-based argument decoding
| isOptional -> [| optionalArg $fname $(mkValueParser innerType) $(varE argsName) |]
| otherwise -> [| requiredArg $fname $(mkValueParser innerType) $(varE argsName) |]
-------------------------------------------------------------------------------
-- Schema generation
-------------------------------------------------------------------------------
-- Build a 'SchemaType' expression for a field type
mkSchemaShape :: [(String, String)] -> Type -> Q Exp
mkSchemaShape descriptions ty = case ty of
ConT n | nameBase n == "Int" -> [| SchemaInteger |]
| nameBase n == "Integer" -> [| SchemaInteger |]
| nameBase n == "Double" -> [| SchemaNumber |]
| nameBase n == "Float" -> [| SchemaNumber |]
| nameBase n == "Bool" -> [| SchemaBoolean |]
| nameBase n == "Text" -> [| SchemaString Nothing |]
AppT ListT inner ->
[| SchemaArray (Schema Nothing $(mkSchemaShape descriptions inner)) |]
ConT n -> do
cons <- reifyCons n
case cons of
Just cs | not (null cs), all isNullary cs -> do
let vals = listE [litE $ stringL $ snakeName (conName c) | c <- cs]
[| SchemaString (Just $vals) |]
Just [RecC _ ifields] -> mkObjectShape descriptions ifields
Just [NormalC _ [(_bang, inner)]] -> mkSchemaShape descriptions inner
_ -> fail $ "Unsupported tool argument type: " ++ show n
_ -> fail $ "Unsupported tool argument type: " ++ show ty
-- Build a 'SchemaType' object expression from record fields
mkObjectShape :: [(String, String)] -> [(Name, Bang, Type)] -> Q Exp
mkObjectShape descriptions fields = do
props <- mapM prop fields
let reqd = listE [litE $ stringL fn | fn <- requiredFieldNames fields]
[| SchemaObject $(return $ ListE props) $reqd |]
where
prop (fieldName, _, fieldType) = do
let fieldStr = nameBase fieldName
let description = descriptionFor descriptions fieldStr fieldStr
let (_, innerType) = unwrapMaybe fieldType
[| ( $(litE $ stringL fieldStr)
, Schema (Just $(litE $ stringL description)) $(mkSchemaShape descriptions innerType)
) |]
-------------------------------------------------------------------------------
-- Prompt derivation
-------------------------------------------------------------------------------
-- | Derive prompt handlers from a data type with custom descriptions.
-- Usage:
--
-- > $(derivePromptHandlerWithDescription ''MyPrompt 'handlePrompt [("Constructor", "Description")])
derivePromptHandlerWithDescription :: Name -> Name -> [(String, String)] -> Q Exp
derivePromptHandlerWithDescription typeName handlerName descriptions =
derivePromptHandlerGeneric typeName handlerName (optionsFromDescriptions descriptions) descriptions
-- | Derive prompt handlers with per-constructor 'DefinitionOptions'
-- (title, icons, scoped argument descriptions). Usage:
--
-- > $(derivePromptHandlerWithOptions ''MyPrompt 'handlePrompt
-- > [("Recipe", defaultDefinitionOptions { optDescription = Just "…" })])
derivePromptHandlerWithOptions :: Name -> Name -> [(String, DefinitionOptions)] -> Q Exp
derivePromptHandlerWithOptions typeName handlerName opts =
derivePromptHandlerGeneric typeName handlerName (optionsFor opts) []
derivePromptHandlerGeneric :: Name -> Name -> (String -> DefinitionOptions) -> [(String, String)] -> Q Exp
derivePromptHandlerGeneric typeName handlerName conOpts globalDescs = do
cons <- reifyCons typeName
case cons of
Just constructors -> do
-- Generate prompt definitions
promptDefs <- traverse (mkPromptDef conOpts globalDescs) constructors
-- Generate list handler
listHandlerExp <- [| \_ctx -> pure $(return $ ListE promptDefs) |]
-- Generate get handler with cases
cases <- traverse (mkDispatchCase PromptArgs handlerName) constructors
defaultCase <- [| pure $ Left $ InvalidPromptName $ "Unknown prompt: " <> $(varE argNameN) |]
let defaultMatch = Match WildP (NormalB defaultCase) []
let getHandlerExp = LamE [VarP ctxName, VarP argNameN, VarP argsName] $
CaseE (AppE (VarE 'T.unpack) (VarE argNameN))
(map clauseToMatch cases ++ [defaultMatch])
return $ TupE [Just listHandlerExp, Just getHandlerExp]
Nothing -> fail $ "derivePromptHandler: " ++ show typeName ++ " is not a data type"
-- | Derive prompt handlers from a data type.
-- Usage:
--
-- > $(derivePromptHandler ''MyPrompt 'handlePrompt)
derivePromptHandler :: Name -> Name -> Q Exp
derivePromptHandler typeName handlerName =
derivePromptHandlerWithDescription typeName handlerName []
mkPromptDef :: (String -> DefinitionOptions) -> [(String, String)] -> Con -> Q Exp
mkPromptDef conOpts globalDescs con = do
let name = conName con
let sname = snakeName name
let opts = conOpts (nameBase name)
let description = fromMaybe (T.pack ("Handle " ++ nameBase name)) (optDescription opts)
fields <- getConFields con
args <- traverse (mkArgDef (fieldDescsFor opts globalDescs)) fields
[| PromptDefinition
{ promptDefinitionName = $(litE $ stringL sname)
, promptDefinitionDescription = $(lift description)
, promptDefinitionArguments = $(return $ ListE args)
, promptDefinitionTitle = $(lift (optTitle opts))
, promptDefinitionIcons = $(lift (optIcons opts))
} |]
mkArgDef :: [(String, String)] -> (Name, Bang, Type) -> Q Exp
mkArgDef descriptions (fieldName, _, fieldType) = do
let isOptional = fst (unwrapMaybe fieldType)
let fieldNameStr = nameBase fieldName
let description = descriptionFor descriptions fieldNameStr fieldNameStr
[| ArgumentDefinition
{ argumentDefinitionName = $(litE $ stringL fieldNameStr)
, argumentDefinitionDescription = $(litE $ stringL description)
, argumentDefinitionRequired = $(if isOptional then [| False |] else [| True |])
} |]
-------------------------------------------------------------------------------
-- Dispatch case generation (shared by prompt and tool)
-------------------------------------------------------------------------------
mkDispatchCase :: ArgStyle -> Name -> Con -> Q Clause
mkDispatchCase style handlerName con = do
let sname = snakeName (conName con)
decoder <- mkConDecoder style con
body <- case style of
ToolArgs ->
[| case $(return decoder) of
Left err -> pure (Left err)
Right v -> do
r <- $(varE handlerName) $(varE ctxName) v
pure (Right (toToolResult r)) |]
ToolArgsOutput builder ->
[| case $(return decoder) of
Left err -> pure (Left err)
Right v -> do
r <- $(varE handlerName) $(varE ctxName) v
pure (Right (resolveToolOutput $(return builder) r)) |]
PromptArgs ->
[| case $(return decoder) of
Left err -> pure (Left err)
Right v -> do
r <- $(varE handlerName) $(varE ctxName) v
pure (Right (toPromptResult r)) |]
clause [litP $ stringL sname] (normalB (return body)) []
-------------------------------------------------------------------------------
-- Resource derivation
-------------------------------------------------------------------------------
-- | Derive resource handlers from a data type with custom descriptions.
--
-- Nullary constructors become static resources
-- (@Menu -> resource:\/\/menu@). Record constructors become resource
-- /templates/ (@UserProfile { userId :: Text } ->
-- resource:\/\/user_profile\/{userId}@): they are excluded from
-- @resources\/list@ (advertise them with 'deriveResourceTemplates'), and the
-- read handler matches their URIs by splitting the path into percent-decoded
-- segments, one per field, in declaration order.
--
-- Usage:
--
-- > $(deriveResourceHandlerWithDescription ''MyResource 'handleResource [("Constructor", "Description")])
deriveResourceHandlerWithDescription :: Name -> Name -> [(String, String)] -> Q Exp
deriveResourceHandlerWithDescription typeName handlerName descriptions =
deriveResourceHandlerGeneric typeName handlerName (optionsFromDescriptions descriptions)
-- | Derive resource handlers with per-constructor 'DefinitionOptions'
-- (description, title, icons).
deriveResourceHandlerWithOptions :: Name -> Name -> [(String, DefinitionOptions)] -> Q Exp
deriveResourceHandlerWithOptions typeName handlerName opts =
deriveResourceHandlerGeneric typeName handlerName (optionsFor opts)
deriveResourceHandlerGeneric :: Name -> Name -> (String -> DefinitionOptions) -> Q Exp
deriveResourceHandlerGeneric typeName handlerName conOpts = do
cons <- reifyCons typeName
case cons of
Just constructors -> do
-- Static resource definitions: nullary constructors only
resourceDefs <- traverse (mkResourceDef conOpts)
(filter isNullary constructors)
listHandlerExp <- [| \_ctx -> pure $(return $ ListE resourceDefs) |]
-- Read handler: a chain of alternatives over every constructor,
-- falling through to ResourceNotFound
let fallbackQ = [| pure $ Left $ ResourceNotFound $
"Resource not found: " <> T.pack (show $(varE uriName)) |]
readBody <- foldr (mkResourceAlt handlerName) fallbackQ constructors
let readHandlerExp = LamE [VarP ctxName, VarP uriName] readBody
return $ TupE [Just listHandlerExp, Just readHandlerExp]
Nothing -> fail $ "deriveResourceHandler: " ++ show typeName ++ " is not a data type"
-- | Derive resource handlers from a data type.
-- Usage:
--
-- > $(deriveResourceHandler ''MyResource 'handleResource)
deriveResourceHandler :: Name -> Name -> Q Exp
deriveResourceHandler typeName handlerName =
deriveResourceHandlerWithDescription typeName handlerName []
-- | Derive a 'ResourceTemplateListHandler' advertising the record
-- constructors of a resource type as URI templates, with custom
-- descriptions. Usage:
--
-- > $(deriveResourceTemplatesWithDescription ''MyResource [("Constructor", "Description")])
deriveResourceTemplatesWithDescription :: Name -> [(String, String)] -> Q Exp
deriveResourceTemplatesWithDescription typeName descriptions =
deriveResourceTemplatesGeneric typeName (optionsFromDescriptions descriptions)
-- | Derive a 'ResourceTemplateListHandler' with per-constructor
-- 'DefinitionOptions' (description, title, icons).
deriveResourceTemplatesWithOptions :: Name -> [(String, DefinitionOptions)] -> Q Exp
deriveResourceTemplatesWithOptions typeName opts =
deriveResourceTemplatesGeneric typeName (optionsFor opts)
deriveResourceTemplatesGeneric :: Name -> (String -> DefinitionOptions) -> Q Exp
deriveResourceTemplatesGeneric typeName conOpts = do
cons <- reifyCons typeName
case cons of
Just constructors -> do
templateDefs <- traverse (mkTemplateDef conOpts)
[c | c@(RecC _ _) <- constructors]
[| \_ctx -> pure $(return $ ListE templateDefs) |]
Nothing -> fail $ "deriveResourceTemplates: " ++ show typeName ++ " is not a data type"
-- | Derive a 'ResourceTemplateListHandler' from a resource type's record
-- constructors. Usage:
--
-- > $(deriveResourceTemplates ''MyResource)
deriveResourceTemplates :: Name -> Q Exp
deriveResourceTemplates typeName =
deriveResourceTemplatesWithDescription typeName []
-- The URI-template string for a record constructor:
-- resource://user_profile/{userId}/{...}
templateURI :: Name -> [(Name, Bang, Type)] -> String
templateURI name fields =
"resource://" <> snakeName name
<> concat ["/{" <> nameBase fn <> "}" | (fn, _, _) <- fields]
mkTemplateDef :: (String -> DefinitionOptions) -> Con -> Q Exp
mkTemplateDef _ (RecC name []) =
fail $ "Resource template constructors need at least one field: " ++ nameBase name
mkTemplateDef conOpts (RecC name fields) = do
let opts = conOpts (nameBase name)
let description = fromMaybe (T.pack (nameBase name)) (optDescription opts)
[| ResourceTemplateDefinition
{ resourceTemplateURITemplate = $(litE $ stringL $ templateURI name fields)
, resourceTemplateName = $(litE $ stringL $ snakeName name)
, resourceTemplateDescription = Just $(lift description)
, resourceTemplateMimeType = Just "text/plain"
, resourceTemplateTitle = $(lift (optTitle opts))
, resourceTemplateIcons = $(lift (optIcons opts))
} |]
mkTemplateDef _ _ = fail "Resource templates require record constructors"
mkResourceDef :: (String -> DefinitionOptions) -> Con -> Q Exp
mkResourceDef conOpts (NormalC name []) = do
let resourceName = T.pack . snakeName $ name
let resourceURI = "resource://" <> T.unpack resourceName
let opts = conOpts (nameBase name)
let description = fromMaybe (T.pack (nameBase name)) (optDescription opts)
[| ResourceDefinition
{ resourceDefinitionURI = $(litE $ stringL resourceURI)
, resourceDefinitionName = $(litE $ stringL $ T.unpack resourceName)
, resourceDefinitionDescription = Just $(lift description)
, resourceDefinitionMimeType = Just "text/plain"
, resourceDefinitionTitle = $(lift (optTitle opts))
, resourceDefinitionIcons = $(lift (optIcons opts))
} |]
mkResourceDef _ _ = fail "Unsupported constructor type for resources"
-- One alternative of the read handler: try this constructor, else fall
-- through to the rest of the chain.
mkResourceAlt :: Name -> Con -> Q Exp -> Q Exp
mkResourceAlt handlerName con restQ = case con of
NormalC name [] -> do
let resourceURI = "resource://" <> snakeName name
[| if show $(varE uriName) == $(litE $ stringL resourceURI)
then Right <$> $(varE handlerName) $(varE ctxName) $(varE uriName) $(conE name)
else $restQ |]
RecC name [] ->
fail $ "Resource template constructors need at least one field: " ++ nameBase name
RecC name fields -> do
let prefix = "resource://" <> snakeName name <> "/"
segsName = mkName "segs"
decoder <- mkSegmentsDecoder segsName name fields
handled <- [| case $(return decoder) of
Left err -> pure (Left err)
Right v -> Right <$> $(varE handlerName) $(varE ctxName) $(varE uriName) v |]
rest <- restQ
matchExp <- [| matchTemplateSegments $(litE $ stringL prefix)
$(litE $ integerL $ fromIntegral $ length fields)
(show $(varE uriName)) |]
justP <- conP 'Just [varP segsName]
nothingP <- conP 'Nothing []
return $ CaseE matchExp
[ Match justP (NormalB handled) []
, Match nothingP (NormalB rest) []
]
_ -> fail "Unsupported constructor type for resources"
-- Applicative decode of the matched template segments into the constructor
mkSegmentsDecoder :: Name -> Name -> [(Name, Bang, Type)] -> Q Exp
mkSegmentsDecoder segsName con fields =
foldl step [| pure $(conE con) |] (zip [0 :: Integer ..] fields)
where
step acc (i, (fieldName, _, fieldType)) = do
let (isOptional, innerType) = unwrapMaybe fieldType
if isOptional
then fail $ "Maybe fields are not supported in resource templates: " ++ nameBase fieldName
else [| $acc <*> templateField $(litE $ stringL $ nameBase fieldName)
$(mkTextParser innerType)
$(varE segsName)
$(litE $ integerL i) |]
-------------------------------------------------------------------------------
-- Tool derivation
-------------------------------------------------------------------------------
-- | Derive tool handlers from a data type with custom descriptions.
-- Usage:
--
-- > $(deriveToolHandlerWithDescription ''MyTool 'handleTool [("Constructor", "Description")])
deriveToolHandlerWithDescription :: Name -> Name -> [(String, String)] -> Q Exp
deriveToolHandlerWithDescription typeName handlerName descriptions =
deriveToolHandlerGeneric typeName handlerName (optionsFromDescriptions descriptions) descriptions Nothing
-- | Derive tool handlers with per-constructor 'DefinitionOptions'
-- (description, title, icons, behavioral annotations, scoped argument
-- descriptions). Usage:
--
-- > $(deriveToolHandlerWithOptions ''MyTool 'handleTool
-- > [ ("Search", defaultDefinitionOptions
-- > { optDescription = Just "Search the catalog"
-- > , optToolAnnotations = Just defaultToolAnnotations { toolReadOnlyHint = Just True }
-- > })
-- > ])
deriveToolHandlerWithOptions :: Name -> Name -> [(String, DefinitionOptions)] -> Q Exp
deriveToolHandlerWithOptions typeName handlerName opts =
deriveToolHandlerGeneric typeName handlerName (optionsFor opts) [] Nothing
-- | Derive tool handlers from a data type.
-- Usage:
--
-- > $(deriveToolHandler ''MyTool 'handleTool)
deriveToolHandler :: Name -> Name -> Q Exp
deriveToolHandler typeName handlerName =
deriveToolHandlerWithDescription typeName handlerName []
-- | Derive tool handlers with typed, structured output: the third name is a
-- record type describing the tools' results. Its shape becomes the derived
-- @outputSchema@ (same field rules as input derivation: primitives,
-- 'Maybe', lists, all-nullary enums, nested records), and the handler
-- returns @'ToolOutput' \<output\>@, whose typed values the derivation
-- serializes into @structuredContent@ — guaranteed to match the schema.
--
-- > handleTool :: ClientContext -> MyTool -> IO (ToolOutput WeatherReport)
-- > $(deriveToolHandlerWithOutput ''MyTool 'handleTool ''WeatherReport)
deriveToolHandlerWithOutput :: Name -> Name -> Name -> Q Exp
deriveToolHandlerWithOutput typeName handlerName outputName =
deriveToolHandlerWithOutputDescription typeName handlerName outputName []
-- | 'deriveToolHandlerWithOutput' with custom descriptions. The
-- description list is shared with the output type: entries matching output
-- field names describe the @outputSchema@ properties.
deriveToolHandlerWithOutputDescription :: Name -> Name -> Name -> [(String, String)] -> Q Exp
deriveToolHandlerWithOutputDescription typeName handlerName outputName descriptions =
deriveToolHandlerGeneric typeName handlerName (optionsFromDescriptions descriptions) descriptions (Just outputName)
-- | 'deriveToolHandlerWithOutput' with per-constructor
-- 'DefinitionOptions'. An entry keyed by the output type's name supplies
-- the @outputSchema@ field descriptions via 'optFieldDescriptions'.
deriveToolHandlerWithOutputOptions :: Name -> Name -> Name -> [(String, DefinitionOptions)] -> Q Exp
deriveToolHandlerWithOutputOptions typeName handlerName outputName opts =
deriveToolHandlerGeneric typeName handlerName (optionsFor opts) [] (Just outputName)
deriveToolHandlerGeneric :: Name -> Name -> (String -> DefinitionOptions) -> [(String, String)] -> Maybe Name -> Q Exp
deriveToolHandlerGeneric typeName handlerName conOpts globalDescs outputName = do
cons <- reifyCons typeName
case cons of
Just constructors -> do
-- The output type's schema and its matching serializer, when typed
-- output is requested. Its field descriptions come from an options
-- entry keyed by the output type's own name, merged with the legacy
-- global list.
output <- traverse
(\o -> do
requireOutputRecord o
let outDescs = fieldDescsFor (conOpts (nameBase o)) globalDescs
(,) <$> mkSchemaShape outDescs (ConT o)
<*> mkValueBuilder (ConT o))
outputName
-- Generate tool definitions
toolDefs <- traverse (mkToolDef conOpts globalDescs (fst <$> output)) constructors
listHandlerExp <- [| \_ctx -> pure $(return $ ListE toolDefs) |]
-- Generate call handler with cases
let style = maybe ToolArgs (ToolArgsOutput . snd) output
cases <- traverse (mkDispatchCase style handlerName) constructors
defaultCase <- [| pure $ Left $ UnknownTool $ "Unknown tool: " <> $(varE argNameN) |]
let defaultMatch = Match WildP (NormalB defaultCase) []
let callHandlerExp = LamE [VarP ctxName, VarP argNameN, VarP argsName] $
CaseE (AppE (VarE 'T.unpack) (VarE argNameN))
(map clauseToMatch cases ++ [defaultMatch])
return $ TupE [Just listHandlerExp, Just callHandlerExp]
Nothing -> fail $ "deriveToolHandler: " ++ show typeName ++ " is not a data type"
mkToolDef :: (String -> DefinitionOptions) -> [(String, String)] -> Maybe Exp -> Con -> Q Exp
mkToolDef conOpts globalDescs outputShape con = do
let name = conName con
let sname = snakeName name
let opts = conOpts (nameBase name)
let description = fromMaybe (T.pack (nameBase name)) (optDescription opts)
fields <- getConFields con
shapeExp <- mkObjectShape (fieldDescsFor opts globalDescs) fields
[| ToolDefinition
{ toolDefinitionName = $(litE $ stringL sname)
, toolDefinitionDescription = $(lift description)
, toolDefinitionInputSchema = Schema Nothing $(return shapeExp)
, toolDefinitionOutputSchema = $(case outputShape of
Just shape -> [| Just (Schema Nothing $(return shape)) |]
Nothing -> [| Nothing |])
, toolDefinitionTitle = $(lift (optTitle opts))
, toolDefinitionAnnotations = $(lift (optToolAnnotations opts))
, toolDefinitionIcons = $(lift (optIcons opts))
} |]