packages feed

mcp-server 0.1.0.21 → 0.2.0.0

raw patch · 56 files changed

+3494/−670 lines, 56 filesdep +base64-bytestringdep +directorydep +scientificdep ~containersdep ~waiPVP ok

version bump matches the API change (PVP)

Dependencies added: base64-bytestring, directory, scientific, stm

Dependency ranges changed: containers, wai

API changes (from Hackage documentation)

- MCP.Server: jsonValueToText :: Value -> Text
- MCP.Server.Protocol: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Protocol.MessageRole
- MCP.Server.Protocol: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Protocol.PromptMessage
- MCP.Server.Protocol: instance GHC.Classes.Eq MCP.Server.Protocol.MessageRole
- MCP.Server.Protocol: instance GHC.Classes.Eq MCP.Server.Protocol.PromptMessage
- MCP.Server.Protocol: instance GHC.Internal.Generics.Generic MCP.Server.Protocol.MessageRole
- MCP.Server.Protocol: instance GHC.Internal.Generics.Generic MCP.Server.Protocol.PromptMessage
- MCP.Server.Protocol: instance GHC.Internal.Show.Show MCP.Server.Protocol.MessageRole
- MCP.Server.Protocol: instance GHC.Internal.Show.Show MCP.Server.Protocol.PromptMessage
- MCP.Server.Types: ContentResource :: ContentResourceData -> Content
- MCP.Server.Types: ContentResourceData :: URI -> Maybe Text -> ContentResourceData
- MCP.Server.Types: InputSchemaDefinitionObject :: [(Text, InputSchemaDefinitionProperty)] -> [Text] -> InputSchemaDefinition
- MCP.Server.Types: InputSchemaDefinitionProperty :: Text -> Text -> InputSchemaDefinitionProperty
- MCP.Server.Types: [contentResourceMimeType] :: ContentResourceData -> Maybe Text
- MCP.Server.Types: [contentResourceUri] :: ContentResourceData -> URI
- MCP.Server.Types: [properties] :: InputSchemaDefinition -> [(Text, InputSchemaDefinitionProperty)]
- MCP.Server.Types: [propertyDescription] :: InputSchemaDefinitionProperty -> Text
- MCP.Server.Types: [propertyType] :: InputSchemaDefinitionProperty -> Text
- MCP.Server.Types: [required] :: InputSchemaDefinition -> [Text]
- MCP.Server.Types: data ContentResourceData
- MCP.Server.Types: data InputSchemaDefinition
- MCP.Server.Types: data InputSchemaDefinitionProperty
- MCP.Server.Types: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Types.InputSchemaDefinition
- MCP.Server.Types: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Types.InputSchemaDefinitionProperty
- MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.ContentResourceData
- MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.InputSchemaDefinition
- MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.InputSchemaDefinitionProperty
- MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.ContentResourceData
- MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.InputSchemaDefinition
- MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.InputSchemaDefinitionProperty
- MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.ContentResourceData
- MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.InputSchemaDefinition
- MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.InputSchemaDefinitionProperty
+ MCP.Server: McpNotifier :: IO () -> IO () -> IO () -> (URI -> IO ()) -> McpNotifier
+ MCP.Server: StdioConfig :: Bool -> CacheHints -> Maybe NotificationSource -> StdioConfig
+ MCP.Server: [httpAllowedOrigins] :: HttpConfig -> Maybe [Text]
+ MCP.Server: [httpCacheHints] :: HttpConfig -> CacheHints
+ MCP.Server: [httpNotifications] :: HttpConfig -> Maybe NotificationSource
+ MCP.Server: [notifyPromptsListChanged] :: McpNotifier -> IO ()
+ MCP.Server: [notifyResourceUpdated] :: McpNotifier -> URI -> IO ()
+ MCP.Server: [notifyResourcesListChanged] :: McpNotifier -> IO ()
+ MCP.Server: [notifyToolsListChanged] :: McpNotifier -> IO ()
+ MCP.Server: [stdioCacheHints] :: StdioConfig -> CacheHints
+ MCP.Server: [stdioNotifications] :: StdioConfig -> Maybe NotificationSource
+ MCP.Server: [stdioVerbose] :: StdioConfig -> Bool
+ MCP.Server: data McpNotifier
+ MCP.Server: data NotificationSource
+ MCP.Server: data StdioConfig
+ MCP.Server: defaultHttpConfig :: HttpConfig
+ MCP.Server: defaultStdioConfig :: StdioConfig
+ MCP.Server: newMcpNotifier :: IO (McpNotifier, NotificationSource)
+ MCP.Server: runMcpServerStdioWithConfig :: StdioConfig -> McpServerInfo -> McpServerHandlers -> IO ()
+ MCP.Server.Derive: deriveResourceTemplates :: Name -> Q Exp
+ MCP.Server.Derive: deriveResourceTemplatesWithDescription :: Name -> [(String, String)] -> Q Exp
+ MCP.Server.Handlers: handleCompletionComplete :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse
+ MCP.Server.Handlers: handleResourcesTemplatesList :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse
+ MCP.Server.Handlers: handleServerDiscover :: McpServerInfo -> NotificationSupport -> McpServerHandlers -> JsonRpcRequest -> IO JsonRpcResponse
+ MCP.Server.Handlers: metaProtocolVersion :: Maybe Value -> Maybe Text
+ MCP.Server.Handlers: unsupportedVersionError :: Text -> JsonRpcError
+ MCP.Server.Notifications: McpNotifier :: IO () -> IO () -> IO () -> (URI -> IO ()) -> McpNotifier
+ MCP.Server.Notifications: NotificationFilter :: Bool -> Bool -> Bool -> [Text] -> NotificationFilter
+ MCP.Server.Notifications: PromptsListChangedEvent :: ChangeEvent
+ MCP.Server.Notifications: ResourceUpdatedEvent :: Text -> ChangeEvent
+ MCP.Server.Notifications: ResourcesListChangedEvent :: ChangeEvent
+ MCP.Server.Notifications: ToolsListChangedEvent :: ChangeEvent
+ MCP.Server.Notifications: [filterPrompts] :: NotificationFilter -> Bool
+ MCP.Server.Notifications: [filterResourceSubs] :: NotificationFilter -> [Text]
+ MCP.Server.Notifications: [filterResources] :: NotificationFilter -> Bool
+ MCP.Server.Notifications: [filterTools] :: NotificationFilter -> Bool
+ MCP.Server.Notifications: [notifyPromptsListChanged] :: McpNotifier -> IO ()
+ MCP.Server.Notifications: [notifyResourceUpdated] :: McpNotifier -> URI -> IO ()
+ MCP.Server.Notifications: [notifyResourcesListChanged] :: McpNotifier -> IO ()
+ MCP.Server.Notifications: [notifyToolsListChanged] :: McpNotifier -> IO ()
+ MCP.Server.Notifications: acknowledgedNotification :: RequestId -> NotificationFilter -> JsonRpcNotification
+ MCP.Server.Notifications: closureResponse :: McpServerInfo -> RequestId -> JsonRpcResponse
+ MCP.Server.Notifications: data ChangeEvent
+ MCP.Server.Notifications: data McpNotifier
+ MCP.Server.Notifications: data NotificationFilter
+ MCP.Server.Notifications: data NotificationSource
+ MCP.Server.Notifications: eventNotification :: RequestId -> ChangeEvent -> JsonRpcNotification
+ MCP.Server.Notifications: filterAccepts :: NotificationFilter -> ChangeEvent -> Bool
+ MCP.Server.Notifications: filterIsEmpty :: NotificationFilter -> Bool
+ MCP.Server.Notifications: instance GHC.Classes.Eq MCP.Server.Notifications.ChangeEvent
+ MCP.Server.Notifications: instance GHC.Classes.Eq MCP.Server.Notifications.NotificationFilter
+ MCP.Server.Notifications: instance GHC.Internal.Show.Show MCP.Server.Notifications.ChangeEvent
+ MCP.Server.Notifications: instance GHC.Internal.Show.Show MCP.Server.Notifications.NotificationFilter
+ MCP.Server.Notifications: legacyEventNotification :: ChangeEvent -> JsonRpcNotification
+ MCP.Server.Notifications: newMcpNotifier :: IO (McpNotifier, NotificationSource)
+ MCP.Server.Notifications: parseNotificationFilter :: Maybe Value -> NotificationFilter
+ MCP.Server.Notifications: subscribeEvents :: NotificationSource -> STM (TChan ChangeEvent)
+ MCP.Server.Protocol: CompleteRequest :: CompletionRef -> Text -> Text -> Map Text Text -> CompleteRequest
+ MCP.Server.Protocol: CompleteResponse :: CompletionResult -> CompleteResponse
+ MCP.Server.Protocol: ResourcesTemplatesListResponse :: [ResourceTemplateDefinition] -> ResourcesTemplatesListResponse
+ MCP.Server.Protocol: [completeArgumentName] :: CompleteRequest -> Text
+ MCP.Server.Protocol: [completeArgumentValue] :: CompleteRequest -> Text
+ MCP.Server.Protocol: [completeContextArgs] :: CompleteRequest -> Map Text Text
+ MCP.Server.Protocol: [completeRef] :: CompleteRequest -> CompletionRef
+ MCP.Server.Protocol: [completeResult] :: CompleteResponse -> CompletionResult
+ MCP.Server.Protocol: [resourcesTemplatesList] :: ResourcesTemplatesListResponse -> [ResourceTemplateDefinition]
+ MCP.Server.Protocol: [toolsCallStructuredContent] :: ToolsCallResponse -> Maybe Value
+ MCP.Server.Protocol: allVersions :: [Text]
+ MCP.Server.Protocol: data CompleteRequest
+ MCP.Server.Protocol: data CompleteResponse
+ MCP.Server.Protocol: data ResourcesTemplatesListResponse
+ MCP.Server.Protocol: instance Data.Aeson.Types.FromJSON.FromJSON MCP.Server.Protocol.CompleteRequest
+ MCP.Server.Protocol: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Protocol.CompleteResponse
+ MCP.Server.Protocol: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Protocol.ResourcesTemplatesListResponse
+ MCP.Server.Protocol: instance GHC.Classes.Eq MCP.Server.Protocol.CompleteRequest
+ MCP.Server.Protocol: instance GHC.Classes.Eq MCP.Server.Protocol.CompleteResponse
+ MCP.Server.Protocol: instance GHC.Classes.Eq MCP.Server.Protocol.ResourcesTemplatesListResponse
+ MCP.Server.Protocol: instance GHC.Internal.Generics.Generic MCP.Server.Protocol.CompleteRequest
+ MCP.Server.Protocol: instance GHC.Internal.Generics.Generic MCP.Server.Protocol.CompleteResponse
+ MCP.Server.Protocol: instance GHC.Internal.Generics.Generic MCP.Server.Protocol.ResourcesTemplatesListResponse
+ MCP.Server.Protocol: instance GHC.Internal.Show.Show MCP.Server.Protocol.CompleteRequest
+ MCP.Server.Protocol: instance GHC.Internal.Show.Show MCP.Server.Protocol.CompleteResponse
+ MCP.Server.Protocol: instance GHC.Internal.Show.Show MCP.Server.Protocol.ResourcesTemplatesListResponse
+ MCP.Server.Protocol: modernVersions :: [Text]
+ MCP.Server.Transport.Http: BodyPeek :: Maybe Text -> Maybe Text -> Maybe Text -> RequestId -> BodyPeek
+ MCP.Server.Transport.Http: [httpAllowedOrigins] :: HttpConfig -> Maybe [Text]
+ MCP.Server.Transport.Http: [httpCacheHints] :: HttpConfig -> CacheHints
+ MCP.Server.Transport.Http: [httpNotifications] :: HttpConfig -> Maybe NotificationSource
+ MCP.Server.Transport.Http: [peekId] :: BodyPeek -> RequestId
+ MCP.Server.Transport.Http: [peekMetaVersion] :: BodyPeek -> Maybe Text
+ MCP.Server.Transport.Http: [peekMethod] :: BodyPeek -> Maybe Text
+ MCP.Server.Transport.Http: [peekName] :: BodyPeek -> Maybe Text
+ MCP.Server.Transport.Http: data BodyPeek
+ MCP.Server.Transport.Http: decodeSentinel :: Text -> Text
+ MCP.Server.Transport.Http: peekBody :: ByteString -> BodyPeek
+ MCP.Server.Transport.Http: peekIsModern :: BodyPeek -> Bool
+ MCP.Server.Transport.Http: validateRequestHeaders :: Request -> BodyPeek -> Maybe JsonRpcError
+ MCP.Server.Transport.Stdio: StdioConfig :: Bool -> CacheHints -> Maybe NotificationSource -> StdioConfig
+ MCP.Server.Transport.Stdio: [stdioCacheHints] :: StdioConfig -> CacheHints
+ MCP.Server.Transport.Stdio: [stdioNotifications] :: StdioConfig -> Maybe NotificationSource
+ MCP.Server.Transport.Stdio: [stdioVerbose] :: StdioConfig -> Bool
+ MCP.Server.Transport.Stdio: data StdioConfig
+ MCP.Server.Transport.Stdio: defaultStdioConfig :: StdioConfig
+ MCP.Server.Transport.Stdio: transportRunStdioWithConfig :: StdioConfig -> McpServerInfo -> McpServerHandlers -> IO ()
+ MCP.Server.Types: CacheHints :: Int -> Bool -> CacheHints
+ MCP.Server.Types: CompletionCapabilities :: CompletionCapabilities
+ MCP.Server.Types: CompletionRefPrompt :: Text -> CompletionRef
+ MCP.Server.Types: CompletionRefResource :: Text -> CompletionRef
+ MCP.Server.Types: CompletionResult :: [Text] -> Maybe Int -> Maybe Bool -> CompletionResult
+ MCP.Server.Types: ContentAudio :: ContentAudioData -> Content
+ MCP.Server.Types: ContentAudioData :: Text -> Text -> ContentAudioData
+ MCP.Server.Types: ContentEmbeddedResource :: ResourceContent -> Content
+ MCP.Server.Types: ContentResourceLink :: ResourceDefinition -> Content
+ MCP.Server.Types: NotificationSupport :: Bool -> Bool -> NotificationSupport
+ MCP.Server.Types: PromptMessage :: MessageRole -> Content -> PromptMessage
+ MCP.Server.Types: PromptResult :: Maybe Text -> [PromptMessage] -> PromptResult
+ MCP.Server.Types: ResourceTemplateDefinition :: Text -> Text -> Maybe Text -> Maybe Text -> Maybe Text -> ResourceTemplateDefinition
+ MCP.Server.Types: RoleAssistant :: MessageRole
+ MCP.Server.Types: RoleUser :: MessageRole
+ MCP.Server.Types: Schema :: Maybe Text -> SchemaType -> Schema
+ MCP.Server.Types: SchemaArray :: Schema -> SchemaType
+ MCP.Server.Types: SchemaBoolean :: SchemaType
+ MCP.Server.Types: SchemaInteger :: SchemaType
+ MCP.Server.Types: SchemaNumber :: SchemaType
+ MCP.Server.Types: SchemaObject :: [(Text, Schema)] -> [Text] -> SchemaType
+ MCP.Server.Types: SchemaString :: Maybe [Text] -> SchemaType
+ MCP.Server.Types: ToolResult :: [Content] -> Maybe Value -> Bool -> Maybe Value -> ToolResult
+ MCP.Server.Types: [cacheScopePublic] :: CacheHints -> Bool
+ MCP.Server.Types: [cacheTtlMs] :: CacheHints -> Int
+ MCP.Server.Types: [capabilityCompletions] :: ServerCapabilities -> Maybe CompletionCapabilities
+ MCP.Server.Types: [clientCapabilities] :: ClientContext -> Maybe Value
+ MCP.Server.Types: [clientInfo] :: ClientContext -> Maybe Value
+ MCP.Server.Types: [clientProtocolVersion] :: ClientContext -> Maybe Text
+ MCP.Server.Types: [completionHasMore] :: CompletionResult -> Maybe Bool
+ MCP.Server.Types: [completionTotal] :: CompletionResult -> Maybe Int
+ MCP.Server.Types: [completionValues] :: CompletionResult -> [Text]
+ MCP.Server.Types: [completions] :: McpServerHandlers -> Maybe CompletionHandler
+ MCP.Server.Types: [contentAudioData] :: ContentAudioData -> Text
+ MCP.Server.Types: [contentAudioMimeType] :: ContentAudioData -> Text
+ MCP.Server.Types: [promptMessageContent] :: PromptMessage -> Content
+ MCP.Server.Types: [promptMessageRole] :: PromptMessage -> MessageRole
+ MCP.Server.Types: [promptResultDescription] :: PromptResult -> Maybe Text
+ MCP.Server.Types: [promptResultMessages] :: PromptResult -> [PromptMessage]
+ MCP.Server.Types: [resourceTemplateDescription] :: ResourceTemplateDefinition -> Maybe Text
+ MCP.Server.Types: [resourceTemplateMimeType] :: ResourceTemplateDefinition -> Maybe Text
+ MCP.Server.Types: [resourceTemplateName] :: ResourceTemplateDefinition -> Text
+ MCP.Server.Types: [resourceTemplateTitle] :: ResourceTemplateDefinition -> Maybe Text
+ MCP.Server.Types: [resourceTemplateURITemplate] :: ResourceTemplateDefinition -> Text
+ MCP.Server.Types: [resourceTemplates] :: McpServerHandlers -> Maybe ResourceTemplateListHandler
+ MCP.Server.Types: [schemaDescription] :: Schema -> Maybe Text
+ MCP.Server.Types: [schemaShape] :: Schema -> SchemaType
+ MCP.Server.Types: [supportsLegacyPush] :: NotificationSupport -> Bool
+ MCP.Server.Types: [supportsListen] :: NotificationSupport -> Bool
+ MCP.Server.Types: [toolDefinitionOutputSchema] :: ToolDefinition -> Maybe Schema
+ MCP.Server.Types: [toolResultContent] :: ToolResult -> [Content]
+ MCP.Server.Types: [toolResultIsError] :: ToolResult -> Bool
+ MCP.Server.Types: [toolResultMeta] :: ToolResult -> Maybe Value
+ MCP.Server.Types: [toolResultStructured] :: ToolResult -> Maybe Value
+ MCP.Server.Types: anonymousContext :: ClientContext
+ MCP.Server.Types: class ToPromptResult a
+ MCP.Server.Types: class ToToolResult a
+ MCP.Server.Types: completionResult :: [Text] -> CompletionResult
+ MCP.Server.Types: data CacheHints
+ MCP.Server.Types: data CompletionCapabilities
+ MCP.Server.Types: data CompletionRef
+ MCP.Server.Types: data CompletionResult
+ MCP.Server.Types: data ContentAudioData
+ MCP.Server.Types: data MessageRole
+ MCP.Server.Types: data NotificationSupport
+ MCP.Server.Types: data PromptMessage
+ MCP.Server.Types: data PromptResult
+ MCP.Server.Types: data ResourceTemplateDefinition
+ MCP.Server.Types: data Schema
+ MCP.Server.Types: data SchemaType
+ MCP.Server.Types: data ToolResult
+ MCP.Server.Types: defaultCacheHints :: CacheHints
+ MCP.Server.Types: describedSchema :: Text -> SchemaType -> Schema
+ MCP.Server.Types: instance Data.Aeson.Types.FromJSON.FromJSON MCP.Server.Types.ResourceDefinition
+ MCP.Server.Types: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Types.CompletionCapabilities
+ MCP.Server.Types: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Types.MessageRole
+ MCP.Server.Types: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Types.PromptMessage
+ MCP.Server.Types: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Types.ResourceTemplateDefinition
+ MCP.Server.Types: instance Data.Aeson.Types.ToJSON.ToJSON MCP.Server.Types.Schema
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.CacheHints
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.CompletionCapabilities
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.CompletionRef
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.CompletionResult
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.ContentAudioData
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.MessageRole
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.NotificationSupport
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.PromptMessage
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.PromptResult
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.ResourceTemplateDefinition
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.Schema
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.SchemaType
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.ToolResult
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.CompletionCapabilities
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.CompletionRef
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.CompletionResult
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.ContentAudioData
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.MessageRole
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.PromptMessage
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.PromptResult
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.ResourceTemplateDefinition
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.Schema
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.SchemaType
+ MCP.Server.Types: instance GHC.Internal.Generics.Generic MCP.Server.Types.ToolResult
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.CacheHints
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.CompletionCapabilities
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.CompletionRef
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.CompletionResult
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.ContentAudioData
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.MessageRole
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.NotificationSupport
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.PromptMessage
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.PromptResult
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.ResourceTemplateDefinition
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.Schema
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.SchemaType
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.ToolResult
+ MCP.Server.Types: instance MCP.Server.Types.ToPromptResult Data.Text.Internal.Text
+ MCP.Server.Types: instance MCP.Server.Types.ToPromptResult MCP.Server.Types.Content
+ MCP.Server.Types: instance MCP.Server.Types.ToPromptResult MCP.Server.Types.PromptMessage
+ MCP.Server.Types: instance MCP.Server.Types.ToPromptResult MCP.Server.Types.PromptResult
+ MCP.Server.Types: instance MCP.Server.Types.ToPromptResult a => MCP.Server.Types.ToPromptResult [a]
+ MCP.Server.Types: instance MCP.Server.Types.ToToolResult Data.Text.Internal.Text
+ MCP.Server.Types: instance MCP.Server.Types.ToToolResult MCP.Server.Types.Content
+ MCP.Server.Types: instance MCP.Server.Types.ToToolResult MCP.Server.Types.ToolResult
+ MCP.Server.Types: instance MCP.Server.Types.ToToolResult a => MCP.Server.Types.ToToolResult [a]
+ MCP.Server.Types: noHandlers :: McpServerHandlers
+ MCP.Server.Types: noNotificationSupport :: NotificationSupport
+ MCP.Server.Types: schema :: SchemaType -> Schema
+ MCP.Server.Types: toPromptResult :: ToPromptResult a => a -> PromptResult
+ MCP.Server.Types: toToolResult :: ToToolResult a => a -> ToolResult
+ MCP.Server.Types: toolError :: Text -> ToolResult
+ MCP.Server.Types: toolResult :: [Content] -> ToolResult
+ MCP.Server.Types: type CompletionHandler = ClientContext -> CompletionRef -> ArgumentName -> Text -> Map Text Text -> IO Either Error CompletionResult
+ MCP.Server.Types: type ResourceTemplateListHandler = ClientContext -> IO [ResourceTemplateDefinition]
- MCP.Server: HttpConfig :: Int -> String -> String -> Bool -> Maybe (Maybe Text -> IO (Maybe Value)) -> HttpConfig
+ MCP.Server: HttpConfig :: Int -> String -> String -> Bool -> Maybe (Maybe Text -> IO (Maybe Value)) -> Maybe [Text] -> CacheHints -> Maybe NotificationSource -> HttpConfig
- MCP.Server: runMcpServerHttp :: McpServerInfo -> McpServerHandlers IO -> IO ()
+ MCP.Server: runMcpServerHttp :: McpServerInfo -> McpServerHandlers -> IO ()
- MCP.Server: runMcpServerHttpWithConfig :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> IO ()
+ MCP.Server: runMcpServerHttpWithConfig :: HttpConfig -> McpServerInfo -> McpServerHandlers -> IO ()
- MCP.Server: runMcpServerStdio :: McpServerInfo -> McpServerHandlers IO -> IO ()
+ MCP.Server: runMcpServerStdio :: McpServerInfo -> McpServerHandlers -> IO ()
- MCP.Server.Handlers: handleInitialize :: MonadIO m => McpServerInfo -> McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleInitialize :: McpServerInfo -> NotificationSupport -> McpServerHandlers -> JsonRpcRequest -> IO JsonRpcResponse
- MCP.Server.Handlers: handleMcpMessage :: MonadIO m => McpServerInfo -> McpServerHandlers m -> ClientContext -> JsonRpcMessage -> m (Maybe JsonRpcMessage)
+ MCP.Server.Handlers: handleMcpMessage :: McpServerInfo -> CacheHints -> NotificationSupport -> McpServerHandlers -> ClientContext -> JsonRpcMessage -> IO (Maybe JsonRpcMessage)
- MCP.Server.Handlers: handlePing :: MonadIO m => JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handlePing :: JsonRpcRequest -> IO JsonRpcResponse
- MCP.Server.Handlers: handlePromptsGet :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handlePromptsGet :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse
- MCP.Server.Handlers: handlePromptsList :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handlePromptsList :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse
- MCP.Server.Handlers: handleResourcesList :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleResourcesList :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse
- MCP.Server.Handlers: handleResourcesRead :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleResourcesRead :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse
- MCP.Server.Handlers: handleToolsCall :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleToolsCall :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse
- MCP.Server.Handlers: handleToolsList :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleToolsList :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse
- MCP.Server.Protocol: ToolsCallResponse :: [Content] -> Maybe Bool -> Maybe Value -> ToolsCallResponse
+ MCP.Server.Protocol: ToolsCallResponse :: [Content] -> Maybe Bool -> Maybe Value -> Maybe Value -> ToolsCallResponse
- MCP.Server.Transport.Http: HttpConfig :: Int -> String -> String -> Bool -> Maybe (Maybe Text -> IO (Maybe Value)) -> HttpConfig
+ MCP.Server.Transport.Http: HttpConfig :: Int -> String -> String -> Bool -> Maybe (Maybe Text -> IO (Maybe Value)) -> Maybe [Text] -> CacheHints -> Maybe NotificationSource -> HttpConfig
- MCP.Server.Transport.Http: transportRunHttp :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> IO ()
+ MCP.Server.Transport.Http: transportRunHttp :: HttpConfig -> McpServerInfo -> McpServerHandlers -> IO ()
- MCP.Server.Transport.Stdio: transportRunStdio :: MonadIO m => McpServerInfo -> McpServerHandlers m -> m ()
+ MCP.Server.Transport.Stdio: transportRunStdio :: McpServerInfo -> McpServerHandlers -> IO ()
- MCP.Server.Types: ClientContext :: Maybe Text -> Maybe Value -> ClientContext
+ MCP.Server.Types: ClientContext :: Maybe Text -> Maybe Value -> Maybe Text -> Maybe Value -> Maybe Value -> ClientContext
- MCP.Server.Types: McpServerHandlers :: Maybe (PromptListHandler m, PromptGetHandler m) -> Maybe (ResourceListHandler m, ResourceReadHandler m) -> Maybe (ToolListHandler m, ToolCallHandler m) -> McpServerHandlers (m :: Type -> Type)
+ MCP.Server.Types: McpServerHandlers :: Maybe (PromptListHandler, PromptGetHandler) -> Maybe (ResourceListHandler, ResourceReadHandler) -> Maybe ResourceTemplateListHandler -> Maybe (ToolListHandler, ToolCallHandler) -> Maybe CompletionHandler -> McpServerHandlers
- MCP.Server.Types: ServerCapabilities :: Maybe PromptCapabilities -> Maybe ResourceCapabilities -> Maybe ToolCapabilities -> Maybe LoggingCapabilities -> ServerCapabilities
+ MCP.Server.Types: ServerCapabilities :: Maybe PromptCapabilities -> Maybe ResourceCapabilities -> Maybe ToolCapabilities -> Maybe CompletionCapabilities -> Maybe LoggingCapabilities -> ServerCapabilities
- MCP.Server.Types: ToolDefinition :: Text -> Text -> InputSchemaDefinition -> Maybe Text -> ToolDefinition
+ MCP.Server.Types: ToolDefinition :: Text -> Text -> Schema -> Maybe Schema -> Maybe Text -> ToolDefinition
- MCP.Server.Types: [prompts] :: McpServerHandlers (m :: Type -> Type) -> Maybe (PromptListHandler m, PromptGetHandler m)
+ MCP.Server.Types: [prompts] :: McpServerHandlers -> Maybe (PromptListHandler, PromptGetHandler)
- MCP.Server.Types: [resources] :: McpServerHandlers (m :: Type -> Type) -> Maybe (ResourceListHandler m, ResourceReadHandler m)
+ MCP.Server.Types: [resources] :: McpServerHandlers -> Maybe (ResourceListHandler, ResourceReadHandler)
- MCP.Server.Types: [toolDefinitionInputSchema] :: ToolDefinition -> InputSchemaDefinition
+ MCP.Server.Types: [toolDefinitionInputSchema] :: ToolDefinition -> Schema
- MCP.Server.Types: [tools] :: McpServerHandlers (m :: Type -> Type) -> Maybe (ToolListHandler m, ToolCallHandler m)
+ MCP.Server.Types: [tools] :: McpServerHandlers -> Maybe (ToolListHandler, ToolCallHandler)
- MCP.Server.Types: data McpServerHandlers (m :: Type -> Type)
+ MCP.Server.Types: data McpServerHandlers
- MCP.Server.Types: type PromptGetHandler (m :: Type -> Type) = ClientContext -> PromptName -> [(ArgumentName, ArgumentValue)] -> m Either Error Content
+ MCP.Server.Types: type PromptGetHandler = ClientContext -> PromptName -> Map Text Text -> IO Either Error PromptResult
- MCP.Server.Types: type PromptListHandler (m :: Type -> Type) = ClientContext -> m [PromptDefinition]
+ MCP.Server.Types: type PromptListHandler = ClientContext -> IO [PromptDefinition]
- MCP.Server.Types: type ResourceListHandler (m :: Type -> Type) = ClientContext -> m [ResourceDefinition]
+ MCP.Server.Types: type ResourceListHandler = ClientContext -> IO [ResourceDefinition]
- MCP.Server.Types: type ResourceReadHandler (m :: Type -> Type) = ClientContext -> URI -> m Either Error ResourceContent
+ MCP.Server.Types: type ResourceReadHandler = ClientContext -> URI -> IO Either Error ResourceContent
- MCP.Server.Types: type ToolCallHandler (m :: Type -> Type) = ClientContext -> ToolName -> [(ArgumentName, ArgumentValue)] -> m Either Error Content
+ MCP.Server.Types: type ToolCallHandler = ClientContext -> ToolName -> Map Text Value -> IO Either Error ToolResult
- MCP.Server.Types: type ToolListHandler (m :: Type -> Type) = ClientContext -> m [ToolDefinition]
+ MCP.Server.Types: type ToolListHandler = ClientContext -> IO [ToolDefinition]

Files

CHANGELOG.md view
@@ -1,7 +1,138 @@ # Revision history for mcp-server -## 0.1.0.21 - ???+## 0.2.0.0 - 2026-07-31 +A major overhaul of the handler API. The headline changes: the handler+boundary is no longer stringly typed, and the server is dual-era — it speaks+both the legacy initialize-handshake revisions and the stateless+`2026-07-28` revision.++### Dual-era protocol support (2026-07-28)++* Requests that declare a protocol revision in their params `_meta`+  (`io.modelcontextprotocol/protocolVersion`) are served statelessly with+  the modern result envelope: `resultType: "complete"`, the server identity+  in result `_meta`, and — on `tools/list`, `prompts/list`,+  `resources/list`, `resources/read` and `server/discover` — the required+  `ttlMs`/`cacheScope` fields (configurable via `CacheHints` on the+  transport configs; default: no caching, private). Requests without modern+  `_meta` are served byte-identically to before under the revision+  negotiated by `initialize`.+* New `server/discover` method (mandatory in 2026-07-28, and the+  backwards-compatibility probe): supported revisions of both eras,+  handler-gated capabilities, server identity and instructions.+* Declaring an unsupported revision returns+  `UnsupportedProtocolVersionError` (`-32022`) listing the supported set.+* A legacy client proposing `2026-07-28` via `initialize` negotiates down+  to `2025-11-25`: an initializing client is legacy by definition.+* Handlers can read the declared revision, client info and client+  capabilities from the `ClientContext`+  (`clientProtocolVersion`/`clientInfo`/`clientCapabilities`); the new+  `anonymousContext` builds an empty context.+* HTTP: modern requests get the 2026-07-28 request-metadata validation —+  the `MCP-Protocol-Version` header must match the body's declared+  revision, `Mcp-Method` must match the body method, and `Mcp-Name` must+  match `params.name`/`params.uri` for `tools/call`/`resources/read`/+  `prompts/get` (with `=?base64?…?=` sentinel decoding); violations return+  `400` with `HeaderMismatch` (`-32020`). Unknown methods return HTTP 404+  and unsupported revisions HTTP 400, so era-probing clients can+  distinguish them. Legacy requests keep the relaxed pre-2026 rules.++### Change notifications and subscriptions/listen++* New `MCP.Server.Notifications`: create an `McpNotifier` with+  `newMcpNotifier`, hand its `NotificationSource` to a transport+  (`stdioNotifications`/`httpNotifications`), and call+  `notifyToolsListChanged`/`notifyPromptsListChanged`/+  `notifyResourcesListChanged`/`notifyResourceUpdated` when things change.+* `subscriptions/listen` (2026-07-28) is served on both transports: the+  mandatory acknowledgment comes first with the honored filter, every+  message is tagged with the subscription id, only opted-into types are+  delivered, and streams end gracefully (closure responses at stdio EOF;+  closing the SSE stream cancels over HTTP, `notifications/cancelled` over+  stdio). HTTP streams send periodic keep-alive comments and+  `X-Accel-Buffering: no`.+* Legacy stdio clients receive spontaneous untagged notifications after+  `initialize`. Capabilities are era- and transport-aware: `listChanged` is+  advertised only where delivery is possible (stdio legacy push, or+  modern `subscriptions/listen`), and `subscribe` only to modern clients.+* `defaultHttpConfig` is now re-exported from `MCP.Server`.++### Resource templates and completions++* Record constructors of a resource type now derive as resource /templates/+  (`UserProfile { userId :: Text }` →+  `resource://user_profile/{userId}`): the derived read handler matches+  template URIs, percent-decodes the path segments, and parses them into+  the constructor's (typed) fields. `deriveResourceTemplates` derives the+  `resources/templates/list` handler advertising them; the method carries+  the modern cacheability envelope.+* New `completions` handler slot serving `completion/complete` for prompt+  arguments and resource-template parameters (`CompletionRef`,+  `CompletionResult`, capped at 100 values per the spec). The+  `completions` capability is advertised automatically.+* `McpServerHandlers` gains `resourceTemplates` and `completions` fields;+  the new `noHandlers` value lets you construct handler sets by record+  update so future fields don't break your code.++### Typed tool arguments and results (BREAKING)++* Tool arguments arrive as full JSON values (`Map Text Value`). The Template+  Haskell derivation decodes records recursively and now supports **list+  fields**, **enumeration fields** (all-nullary data types, wired as string+  enums), and **nested record fields** in addition to the primitives.+  Primitive parsing is lenient: native JSON types or their string+  representations are both accepted (many clients send numbers/booleans as+  strings). Prompt arguments remain string-valued per the MCP specification.+* `inputSchema` is generated as a real JSON Schema (`Schema`/`SchemaType`+  ADT with `enum`, `items` and nested `object`s), replacing the flat+  `InputSchemaDefinition*` types that silently typed every non-primitive+  field as a string.+* Tool handlers produce a `ToolResult`: multiple content blocks,+  `structuredContent`, `_meta`, and `isError`. Tool *execution* failures+  should be reported via `isError` (see `toolError`) so the model can see+  them — per spec — instead of surfacing as JSON-RPC protocol errors.+  The `ToToolResult` class keeps simple handlers simple: returning+  `Content` or `Text` still works unchanged.+* Prompt handlers produce a `PromptResult` (optional description plus a+  multi-message conversation with user/assistant roles) via the analogous+  `ToPromptResult` class.+* `Content` gains `audio` and `resource_link` variants; embedded resources+  now carry their full contents as the spec requires.+* `ToolDefinition` gains `outputSchema`; `tools/call` responses carry+  `structuredContent`.+* Handler types are fixed to `IO` — the monad parameter was unusable+  through the public API (both transports required `IO`).++### Transport fixes++* stdio: a blank line on stdin no longer terminates the server, EOF shuts+  down cleanly instead of crashing, and malformed input is answered with+  proper JSON-RPC error responses (`-32700`/`-32600`, `id: null`).+* stdio: raw request bodies are no longer logged to stderr by default+  (tool arguments may carry sensitive data) — only message summaries.+  `runMcpServerStdioWithConfig` with `stdioVerbose = True` restores full+  body logging.+* JSON-RPC: messages are classified by shape (method/id presence) instead+  of parse-fallthrough, so a request with a malformed `id` is answered+  with an error rather than silently dropped as a notification. Request+  ids must be integral.+* HTTP: new `httpAllowedOrigins` policy on `HttpConfig` (Origin+  validation / DNS-rebinding protection, a spec MUST); accepted+  notifications return `202` with no body; malformed bodies get JSON-RPC+  error responses; the `Access-Control-Allow-Origin` header is set+  consistently on every response and echoes the validated origin (with+  `Vary: Origin`) when a policy is configured.+* HTTP (BREAKING): the non-spec GET "discovery" endpoint is removed — the+  MCP endpoint now answers GET with `405 Method Not Allowed`, matching the+  spec (no revision defines a GET discovery response, and `2026-07-28`+  requires 405 here).+* Integer tool arguments bound the scientific-notation exponent (1024, the+  same bound aeson uses) so a tiny payload like `1e1000000000` cannot force+  allocation of a gigabyte-sized `Integer`.++## 0.1.0.21 - 2026-07-31+ * **BREAKING**: every handler (prompt/resource/tool; list and get/read/call)   now receives a `ClientContext` as its first argument, so a server can behave   differently depending on who is calling. On stdio the context is anonymous;@@ -25,7 +156,7 @@ * `http-simple-example` is now built with `-threaded`, which Warp requires;   previously every request crashed with a `TimerManager` error. -## 0.1.0.20 - ???+## 0.1.0.20 - 2026-07-31  * Fix protocol version negotiation: echo back any compatible revision the client   proposes (`2024-11-05`, `2025-03-26`, `2025-06-18`, `2025-11-25`) instead of
README.md view
@@ -4,7 +4,7 @@  ## Features -- **Complete MCP Implementation**: Negotiates MCP protocol revisions `2024-11-05` through `2025-11-25` (the shared wire format for tool/resource/prompt operations)+- **Complete MCP Implementation**: Dual-era server — legacy revisions `2024-11-05` through `2025-11-25` via the `initialize` handshake, and the stateless `2026-07-28` revision via per-request `_meta` (including `server/discover`, `resultType`, and cacheability fields) - **Type-Safe API**: Leverage Haskell's type system for robust MCP servers - **Multiple Abstractions**: Both low-level fine-grained control and high-level derived interfaces - **Template Haskell Support**: Automatic handler derivation from data types@@ -14,7 +14,9 @@  - ✅ **Prompts**: User-controlled prompt templates with arguments - ✅ **Resources**: Application-controlled readable resources+- ✅ **Resource Templates**: Parameterized resources via URI templates - ✅ **Tools**: Model-controlled callable functions+- ✅ **Completions**: Argument autocompletion for prompts and templates - ✅ **Initialization Flow**: Complete protocol lifecycle with version negotiation - ✅ **Error Handling**: Comprehensive error types and JSON-RPC error responses @@ -82,19 +84,51 @@ -- Becomes: "get_value", "set_value", "search_items" ``` -#### Automatic Type Conversion+#### Typed Tool Arguments -The derivation system automatically converts Text arguments to appropriate Haskell types:+Tool arguments are decoded from full JSON values, and the generated+`inputSchema` mirrors the field types:  ```haskell-data MyTool = Calculate { number :: Int, factor :: Double, enabled :: Bool }--- Text "42" -> Int 42--- Text "3.14" -> Double 3.14--- Text "true" -> Bool True+data Color = Red | Green | Blue          -- all-nullary type: string enum+data Filters = Filters                   -- record: nested JSON object+  { tags     :: [Text]                   -- list: JSON array+  , maxCount :: Maybe Int                -- Maybe: optional field+  }++data MyTool = Search+  { query   :: Text+  , color   :: Color                     -- "red" | "green" | "blue"+  , filters :: Filters                   -- { "tags": [...], "maxCount": ... }+  , limit   :: Maybe Int+  } ``` -Supported conversions: `Int`, `Integer`, `Double`, `Float`, `Bool`, and `Text` (no conversion).+Primitive fields (`Int`, `Integer`, `Double`, `Float`, `Bool`, `Text`) are+parsed leniently: the native JSON type and its string representation are+both accepted (`42` or `"42"`), since many clients send numbers and+booleans as strings. +Prompt arguments are string-valued per the MCP specification, so prompt+records are limited to primitive and enumeration fields.++#### Tool Results++Simple handlers can return plain `Content` (or `Text`). Return a full+`ToolResult` for multiple content blocks, structured content, or to report+execution failures with `isError` — which the spec prefers over protocol+errors, so the model can see what went wrong and react:++```haskell+handleTool :: ClientContext -> MyTool -> IO ToolResult+handleTool _ (Search q _ _ _)+  | T.null q  = pure $ toolError "query must not be empty"+  | otherwise = pure $ toolResult [ContentText ("Results for " <> q)]+```++Prompt handlers can likewise return a `PromptResult` with a description and+a multi-message conversation (user and assistant roles).+ #### Nested Parameter Types  You can nest parameter types with automatic unwrapping:@@ -122,6 +156,44 @@ -- Generates: "resource://menu", "resource://specials" ``` +#### Resource Templates++Record constructors become parameterized resource *templates* (RFC 6570 URI+templates), with one percent-decoded path segment per field:++```haskell+data MyResource+    = Menu                                            -- static: resource://menu+    | ProductDetail { sku :: Text }                   -- resource://product_detail/{sku}+    | OrderItem { orderId :: Int, itemName :: Text }  -- resource://order_item/{orderId}/{itemName}+```++The read handler derived by `deriveResourceHandler` matches template URIs+(e.g. `resource://product_detail/ABC123`) and decodes the segments into the+constructor's fields — typed fields like `Int` are parsed, and a failing+segment yields an invalid-params error. Advertise the templates via+`resources/templates/list` with:++```haskell+resourceTemplates = Just $(deriveResourceTemplates ''MyResource)+```++#### Argument Completion++Provide a `completions` handler to serve `completion/complete` for prompt+arguments and resource-template parameters:++```haskell+handleComplete :: ClientContext -> CompletionRef -> ArgumentName -> Text -> Map Text Text -> IO (Either Error CompletionResult)+handleComplete _ (CompletionRefPrompt "recipe") "idea" partial _ =+    pure $ Right $ completionResult $+        filter (T.isPrefixOf partial) ["pancakes", "pasta", "pizza"]+handleComplete _ _ _ _ _ = pure $ Right $ completionResult []+```++The `completions` capability is advertised automatically when the handler is+present.+ #### Unsupported Patterns  We do not support positional (unnamed) parameters:@@ -165,9 +237,10 @@ import MCP.Server  -- Manual handler implementation. Every handler receives the per-request--- 'ClientContext' as its first argument.+-- 'ClientContext' as its first argument. Prompt arguments are string-valued+-- (Map Text Text); tool arguments are full JSON values (Map Text Value). promptListHandler :: ClientContext -> IO [PromptDefinition]-promptGetHandler :: ClientContext -> PromptName -> [(ArgumentName, ArgumentValue)] -> IO (Either Error Content)+promptGetHandler :: ClientContext -> PromptName -> Map Text Text -> IO (Either Error PromptResult) -- ... implement your custom logic  main :: IO ()@@ -180,6 +253,35 @@       } ``` +## Change Notifications++Servers whose tool/prompt/resource lists change at runtime can push change+notifications. Create a notifier, hand its source to the transport, and call+the notifier when things change:++```haskell+main :: IO ()+main = do+    (notifier, source) <- newMcpNotifier+    _ <- forkIO $ appLogic notifier   -- calls notifyToolsListChanged etc.+    runMcpServerStdioWithConfig+        defaultStdioConfig { stdioNotifications = Just source }+        serverInfo handlers+```++Delivery is transport- and era-aware, and the `listChanged`/`subscribe`+capabilities are advertised automatically where delivery is actually+possible:++- **Modern clients (2026-07-28)** open a `subscriptions/listen` stream (a+  long-lived SSE response over HTTP) and receive only the notification types+  they opted into, tagged with their subscription id — including+  `notifications/resources/updated` for watched URIs.+- **Legacy stdio clients** receive spontaneous untagged notifications after+  `initialize`.+- **Legacy HTTP clients** have no delivery channel (this library does not+  offer the deprecated GET SSE stream), so nothing is advertised to them.+ ## HTTP Transport (NEW!)  The library supports the MCP Streamable HTTP transport. Compile your@@ -194,12 +296,15 @@ -- Custom configuration main = runMcpServerHttpWithConfig customConfig serverInfo handlers   where-    customConfig = HttpConfig+    customConfig = defaultHttpConfig       { httpPort = 8080       , httpHost = "0.0.0.0"       , httpEndpoint = "/api/mcp"       , httpVerbose = True     -- Enable detailed logging-      , httpAuthorize = Nothing -- No authentication (see below)+      , httpAllowedOrigins = Just ["https://app.example.com"]+          -- Origin validation (DNS-rebinding protection): requests with an+          -- Origin header outside this list are rejected with 403. Nothing+          -- disables the check (only for servers unreachable from browsers).       } ``` @@ -221,10 +326,16 @@  **Features:** - CORS enabled for web clients-- GET `/mcp` for server discovery-- POST `/mcp` for JSON-RPC messages-- Protocol-version negotiation across supported revisions (`2024-11-05`–`2025-11-25`)+- POST `/mcp` for JSON-RPC messages (GET returns 405 — this library does not+  offer server-initiated SSE streams)+- Dual-era protocol support: legacy revisions (`2024-11-05`–`2025-11-25`)+  negotiate via `initialize`; the stateless `2026-07-28` revision declares its+  version per request in `_meta`, with full request-metadata header+  validation (`MCP-Protocol-Version`, `Mcp-Method`, `Mcp-Name` including the+  base64 sentinel encoding) - Optional pluggable bearer-token authentication via `httpAuthorize`+- Origin validation via `httpAllowedOrigins`+- Cacheability hints for modern list/read results via `httpCacheHints`  ## Examples 
examples/Complete/Main.hs view
@@ -3,6 +3,9 @@  module Main where +import           Data.Map  (Map)+import           Data.Text (Text)+import qualified Data.Text as T import           MCP.Server import           MCP.Server.Derive import           System.IO (hSetEncoding, stderr, stdout, utf8)@@ -23,19 +26,40 @@     pure $ ResourceText uri "text/plain" "Organic Apples $2.99/lb, Free Range Eggs $4.50/dozen, Artisan Bread $3.25/loaf" handleResource _ uri HeadlineBannerAd =     pure $ ResourceText uri "text/plain" "🛒 Weekly Special: 20% off all organic produce! 🥕🥬🍎"+handleResource _ uri (ProductDetail sku) =+    pure $ ResourceText uri "text/plain" $ "Details for product " <> sku <> ": in stock, $9.99" -handleTool :: ClientContext -> MyTool -> IO Content+-- Tool handlers return a full ToolResult: execution failures are reported+-- with isError so the model can see them (returning plain Content works too+-- for simple tools — see the other examples).+handleTool :: ClientContext -> MyTool -> IO ToolResult handleTool _ (SearchForProduct q category) =     case category of-        Nothing -> pure $ ContentText $ "Search results for '" <> q <> "': Found 15 products across all categories"-        Just cat -> pure $ ContentText $ "Search results for '" <> q <> "' in " <> cat <> ": Found 8 products"-handleTool _ (AddToCart sku) = pure $ ContentText $ "Added item " <> sku <> " to your cart. Cart total: 3 items"-handleTool _ Checkout = pure $ ContentText "Checkout completed! Order #12345 confirmed. Thank you for shopping with us!"+        Nothing -> pure $ toolResult [ContentText $ "Search results for '" <> q <> "': Found 15 products across all categories"]+        Just cat -> pure $ toolResult [ContentText $ "Search results for '" <> q <> "' in " <> cat <> ": Found 8 products"]+handleTool _ (AddToCart sku quantities)+    | any (<= 0) quantities = pure $ toolError "Quantities must be positive"+    | otherwise = pure $ toolResult+        [ContentText $ "Added " <> T.pack (show (sum quantities)) <> " of item " <> sku <> " to your cart"]+handleTool _ (Checkout speed (Address street city zipCode)) =+    pure $ toolResult+        [ ContentText $ "Checkout completed! Order #12345 will ship "+            <> T.pack (show speed) <> " to " <> street <> ", " <> city+            <> maybe "" (" " <>) zipCode+        ] handleTool _ (ComplexTool field1 field2 field3 field4 field5) =-    pure $ ContentText $ "Complex tool called with: " <> field1 <> ", " <> field2 <>+    pure $ toolResult+        [ContentText $ "Complex tool called with: " <> field1 <> ", " <> field2 <>                         maybe "" (", " <>) field3 <> ", " <> field4 <>-                        maybe "" (", " <>) field5+                        maybe "" (", " <>) field5] +-- Argument autocompletion for the Recipe prompt's idea argument+handleComplete :: ClientContext -> CompletionRef -> ArgumentName -> Text -> Map Text Text -> IO (Either Error CompletionResult)+handleComplete _ (CompletionRefPrompt "recipe") "idea" partial _ =+    pure $ Right $ completionResult $+        filter (T.isPrefixOf (T.toLower partial)) ["pancakes", "pasta", "pizza"]+handleComplete _ _ _ _ _ = pure $ Right $ completionResult []+ main :: IO () main = do     -- Set UTF-8 encoding to handle Unicode characters properly@@ -44,6 +68,7 @@     -- Derive the handlers using Template Haskell     let prompts = $(derivePromptHandler ''MyPrompt 'handlePrompt)         resources = $(deriveResourceHandler ''MyResource 'handleResource)+        templates = $(deriveResourceTemplates ''MyResource)         tools = $(deriveToolHandler ''MyTool 'handleTool)      in runMcpServerStdio         McpServerInfo@@ -51,8 +76,10 @@             , serverVersion = "0.3.0"             , serverInstructions = "An example MCP server that handles prompts, resources, and tools."             }-        McpServerHandlers+        noHandlers             { prompts = Just prompts             , resources = Just resources+            , resourceTemplates = Just templates             , tools = Just tools+            , completions = Just handleComplete             }
examples/Complete/Types.hs view
@@ -15,11 +15,29 @@     = ProductCategories     | SaleItems     | HeadlineBannerAd+    -- A record constructor becomes a resource template:+    -- resource://product_detail/{productSku}+    | ProductDetail { productSku :: Text }     deriving (Show, Eq) +-- An enumeration argument: all-nullary data types become string enums in+-- the generated schema ("standard", "express", "overnight")+data ShippingSpeed+    = Standard+    | Express+    | Overnight+    deriving (Show, Eq)++-- A nested record argument: appears as a JSON object in the schema+data Address = Address+    { street :: Text+    , city   :: Text+    , zipCode :: Maybe Text+    } deriving (Show, Eq)+ data MyTool     = SearchForProduct { q :: Text, category :: Maybe Text }-    | AddToCart { sku :: Text }-    | Checkout+    | AddToCart { sku :: Text, quantities :: [Int] }+    | Checkout { speed :: ShippingSpeed, shipTo :: Address }     | ComplexTool { field1 :: Text, field2 :: Text, field3 :: Maybe Text, field4 :: Text, field5 :: Maybe Text }     deriving (Show, Eq)
examples/HttpSimple/Main.hs view
@@ -36,8 +36,6 @@             , serverVersion = "1.0.0"             , serverInstructions = "A simple HTTP key-value store with GetValue and SetValue tools"             }-        McpServerHandlers-            { prompts = Nothing     -- No prompts in this example-            , resources = Nothing   -- No resources in this example-            , tools = Just tools    -- Only tools in this example+        noHandlers+            { tools = Just tools    -- Only tools in this example             }
examples/Simple/Main.hs view
@@ -36,8 +36,6 @@             , serverVersion = "1.0.0"             , serverInstructions = "A simple key-value store with GetValue and SetValue tools"             }-        McpServerHandlers-            { prompts = Nothing     -- No prompts in this example-            , resources = Nothing   -- No resources in this example-            , tools = Just tools    -- Only tools in this example+        noHandlers+            { tools = Just tools    -- Only tools in this example             }
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.21+version: 0.2.0.0 -- A short (one-line) description of the package. synopsis: Library for building Model Context Protocol (MCP) servers -- A longer description of the package.@@ -49,9 +49,12 @@               || == 9.8.4               || == 9.10.3               || == 9.12.2+              || == 9.14.1  -- Extra source files to be distributed with the package, such as examples, or a tutorial module.--- extra-source-files: SPEC.md+extra-source-files:+  test/golden/legacy/*.json+  test/golden/modern/*.json -- Source repository information source-repository head   type: git@@ -69,6 +72,7 @@     MCP.Server.Derive     MCP.Server.Handlers     MCP.Server.JsonRpc+    MCP.Server.Notifications     MCP.Server.Protocol     MCP.Server.Transport.Http     MCP.Server.Transport.Stdio@@ -85,10 +89,13 @@   build-depends:     aeson >=2 && <3,     base >=4.18 && <4.23,+    base64-bytestring >=1.0 && <1.3,     bytestring >=0.10 && <0.13,     containers >=0.6 && <0.9,     http-types >=0.12 && <1,     network-uri >=2.6 && <2.8,+    scientific >=0.3 && <0.4,+    stm >=2.5 && <2.6,     template-haskell >=2.16 && <2.25,     text >=1.2 && <3,     wai >=3.2 && <4,@@ -131,6 +138,7 @@   -- Other library packages from which modules are imported.   build-depends:     base,+    containers,     mcp-server,     text, @@ -170,12 +178,18 @@   other-modules:     Spec.AdvancedDerivation     Spec.BasicDerivation+    Spec.GoldenWire     Spec.JSONConversion+    Spec.ModernEra     Spec.ProtocolVersionNegotiation     Spec.SchemaValidation+    Spec.Subscriptions+    Spec.TemplatesCompletions     Spec.ToolCallParsing+    Spec.TypedArgs     Spec.UnicodeHandling     TestData+    TestHelpers     TestTypes    -- LANGUAGE extensions used by modules in this package.@@ -192,7 +206,11 @@     aeson,     base,     bytestring,+    containers,+    directory,     hspec,     mcp-server,     network-uri,+    stm,     text,+    wai,
src/MCP/Server.hs view
@@ -4,48 +4,45 @@ module MCP.Server   ( -- * Server Runtime     runMcpServerStdio+  , runMcpServerStdioWithConfig   , runMcpServerHttp   , runMcpServerHttpWithConfig      -- * Transport Configuration+  , StdioConfig(..)+  , defaultStdioConfig   , HttpConfig(..)+  , defaultHttpConfig -    -- * Utility Functions-  , jsonValueToText+    -- * Change Notifications+  , McpNotifier(..)+  , NotificationSource+  , newMcpNotifier      -- * Re-exports   , module MCP.Server.Types   ) where -import           Data.Aeson-import           Data.Text              (Text)-import qualified Data.Text              as T--import           MCP.Server.Transport.Stdio (transportRunStdio)+import           MCP.Server.Notifications (McpNotifier (..), NotificationSource,+                                           newMcpNotifier)+import           MCP.Server.Transport.Stdio (StdioConfig (..), defaultStdioConfig,+                                             transportRunStdio,+                                             transportRunStdioWithConfig) import           MCP.Server.Transport.Http (HttpConfig(..), transportRunHttp, defaultHttpConfig) 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- -- | Run an MCP server using STDIO transport-runMcpServerStdio :: McpServerInfo -> McpServerHandlers IO -> IO ()-runMcpServerStdio serverInfo handlers = transportRunStdio serverInfo handlers+runMcpServerStdio :: McpServerInfo -> McpServerHandlers -> IO ()+runMcpServerStdio = transportRunStdio +-- | Run an MCP server using STDIO transport with custom configuration+runMcpServerStdioWithConfig :: StdioConfig -> McpServerInfo -> McpServerHandlers -> IO ()+runMcpServerStdioWithConfig = transportRunStdioWithConfig+ -- | Run an MCP server using HTTP transport with default configuration-runMcpServerHttp :: McpServerInfo -> McpServerHandlers IO -> IO ()-runMcpServerHttp serverInfo handlers = transportRunHttp defaultHttpConfig serverInfo handlers+runMcpServerHttp :: McpServerInfo -> McpServerHandlers -> IO ()+runMcpServerHttp = transportRunHttp defaultHttpConfig  -- | Run an MCP server using HTTP transport with custom configuration-runMcpServerHttpWithConfig :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> IO ()-runMcpServerHttpWithConfig config serverInfo handlers = transportRunHttp config serverInfo handlers+runMcpServerHttpWithConfig :: HttpConfig -> McpServerInfo -> McpServerHandlers -> IO ()+runMcpServerHttpWithConfig = transportRunHttp
src/MCP/Server/Derive.hs view
@@ -2,17 +2,28 @@ {-# 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   , deriveResourceHandler   , deriveResourceHandlerWithDescription+  , deriveResourceTemplates+  , deriveResourceTemplatesWithDescription   , deriveToolHandler   , deriveToolHandlerWithDescription   ) where -import qualified Data.Map            as Map+import           Data.Aeson          (Value (..))+import           Data.List           (intercalate) import           Data.Maybe          (fromMaybe) import qualified Data.Text           as T import           Language.Haskell.TH@@ -52,20 +63,23 @@ descriptionFor descriptions key fallback =   fromMaybe fallback (lookup key descriptions) --- Map a Haskell type to its JSON schema type string-jsonTypeFor :: Type -> String-jsonTypeFor (ConT n)-  | nameBase n == "Int"     = "integer"-  | nameBase n == "Integer" = "integer"-  | nameBase n == "Double"  = "number"-  | nameBase n == "Float"   = "number"-  | nameBase n == "Bool"    = "boolean"-jsonTypeFor _ = "string"- -- 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)]@@ -89,7 +103,203 @@ 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 icon ifields] -> mkObjectParser n icon ifields+      Just [NormalC icon [(_bang, inner)]] -> [| fmap $(conE icon) . $(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 icon ifields = do+  vVar <- newName "v"+  oVar <- newName "o"+  chain <- foldl (\acc f -> [| $acc <*> $(fieldExtractor oVar f) |])+                 [| pure $(conE icon) |]+                 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++-------------------------------------------------------------------------------+-- Argument decoding for a constructor+-------------------------------------------------------------------------------++data ArgStyle = ToolArgs | 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 icon ifields] -> do+            inner <- mkFieldsDecoder style (conE icon) ifields+            [| $(conE cn) <$> $(return inner) |]+          Just [NormalC icon [(_b, innerTy)]] -> do+            innerCon <- mkConDecoder style (NormalC icon [(_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+        ToolArgs+          | isOptional -> [| optionalArg $fname $(mkValueParser innerType) $(varE argsName) |]+          | otherwise  -> [| requiredArg $fname $(mkValueParser innerType) $(varE argsName) |]+        PromptArgs+          | isOptional -> [| optionalTextArg $fname $(mkTextParser innerType) $(varE argsName) |]+          | otherwise  -> [| requiredTextArg $fname $(mkTextParser 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 ------------------------------------------------------------------------------- @@ -99,9 +309,9 @@ -- > $(derivePromptHandlerWithDescription ''MyPrompt 'handlePrompt [("Constructor", "Description")]) derivePromptHandlerWithDescription :: Name -> Name -> [(String, String)] -> Q Exp derivePromptHandlerWithDescription typeName handlerName descriptions = do-  info <- reify typeName-  case info of-    TyConI (DataD _ _ _ _ constructors _) -> do+  cons <- reifyCons typeName+  case cons of+    Just constructors -> do       -- Generate prompt definitions       promptDefs <- traverse (mkPromptDefWithDescription descriptions) constructors @@ -109,15 +319,15 @@       listHandlerExp <- [| \_ctx -> pure $(return $ ListE promptDefs) |]        -- Generate get handler with cases-      cases <- traverse (mkDispatchCase handlerName) constructors-      defaultCase <- [| pure $ Left $ InvalidPromptName $ "Unknown prompt: " <> name |]+      cases <- traverse (mkDispatchCase PromptArgs handlerName) constructors+      defaultCase <- [| pure $ Left $ InvalidPromptName $ "Unknown prompt: " <> $(varE argNameN) |]       let defaultMatch = Match WildP (NormalB defaultCase) []-      let getHandlerExp = LamE [VarP (mkName "ctx"), VarP (mkName "name"), VarP (mkName "args")] $-            CaseE (AppE (VarE 'T.unpack) (VarE (mkName "name")))+      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]-    _ -> fail $ "derivePromptHandlerWithDescription: " ++ show typeName ++ " is not a data type"+    Nothing -> fail $ "derivePromptHandlerWithDescription: " ++ show typeName ++ " is not a data type"  -- | Derive prompt handlers from a data type. -- Usage:@@ -132,11 +342,8 @@   let name = conName con   let sname = snakeName name   let description = descriptionFor descriptions (nameBase name) ("Handle " ++ nameBase name)-  args <- case con of-    NormalC _ []                   -> pure []-    RecC _ fields                  -> traverse (mkArgDef descriptions) fields-    NormalC _ [(_bang, paramType)] -> extractFieldsFromType descriptions paramType-    _                              -> fail "Unsupported constructor type"+  fields <- getConFields con+  args <- traverse (mkArgDef descriptions) fields   [| PromptDefinition       { promptDefinitionName = $(litE $ stringL sname)       , promptDefinitionDescription = $(litE $ stringL description)@@ -144,22 +351,6 @@       , promptDefinitionTitle = Nothing       } |] --- Extract field definitions from a parameter type recursively-extractFieldsFromType :: [(String, String)] -> Type -> Q [Exp]-extractFieldsFromType descriptions paramType = do-  case paramType of-    ConT typeName -> do-      info <- reify typeName-      case info of-        TyConI (DataD _ _ _ _ [RecC _ fields] _) ->-          -- Parameter type is a record with fields-          traverse (mkArgDef descriptions) fields-        TyConI (DataD _ _ _ _ [NormalC _ [(_bang, innerType)]] _) ->-          -- Parameter type has a single parameter - recurse-          extractFieldsFromType descriptions 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- mkArgDef :: [(String, String)] -> (Name, Bang, Type) -> Q Exp mkArgDef descriptions (fieldName, _, fieldType) = do   let isOptional = fst (unwrapMaybe fieldType)@@ -175,160 +366,61 @@ -- Dispatch case generation (shared by prompt and tool) ------------------------------------------------------------------------------- -mkDispatchCase :: Name -> Con -> Q Clause-mkDispatchCase handlerName con = do-  let name = conName con-  let sname = snakeName name-  body <- case con of-    NormalC _ [] ->-      [| do-          content <- $(varE handlerName) $(varE (mkName "ctx")) $(conE name)-          pure $ Right content |]-    RecC _ fields ->-      mkRecordCase name handlerName fields-    NormalC _ [(_bang, paramType)] ->-      mkSeparateParamsCase name handlerName paramType-    _ -> fail "Unsupported constructor type"+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)) |]+    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)) []  ---------------------------------------------------------------------------------- Field validation----------------------------------------------------------------------------------mkSeparateParamsCase :: Name -> Name -> Type -> Q Exp-mkSeparateParamsCase outerConName handlerName paramType = do-  fields <- extractFieldsFromParamType paramType-  let argMapName = mkName "argMap"-  let mkBaseExp fieldVars = do-        paramConstructorApp <- buildParameterConstructor paramType fieldVars-        let outerConstructorApp = AppE (ConE outerConName) paramConstructorApp-        [| do-            content <- $(varE handlerName) $(varE (mkName "ctx")) $(return outerConstructorApp)-            pure $ Right content |]-  inner <- buildFieldValidation argMapName handlerName mkBaseExp fields 0-  [| let $(varP argMapName) = Map.fromList args in $(return inner) |]--mkRecordCase :: Name -> Name -> [(Name, Bang, Type)] -> Q Exp-mkRecordCase recConName handlerName fields = do-  case fields of-    [] -> [| do-        content <- $(varE handlerName) $(varE (mkName "ctx")) $(conE recConName)-        pure $ Right content |]-    _ -> do-      let argMapName = mkName "argMap"-      let mkBaseExp fieldVars = do-            let constructorApp = foldl AppE (ConE recConName) (map VarE fieldVars)-            [| do-                content <- $(varE handlerName) $(varE (mkName "ctx")) $(return constructorApp)-                pure $ Right content |]-      inner <- buildFieldValidation argMapName handlerName mkBaseExp fields 0-      [| let $(varP argMapName) = Map.fromList args in $(return inner) |]---- Build nested case expressions for field validation, supporting any number of fields.--- The mkBaseExp callback receives field variable names and builds the final expression.-buildFieldValidation :: Name -> Name -> ([Name] -> Q Exp) -> [(Name, Bang, Type)] -> Int -> Q Exp-buildFieldValidation _argMap _handlerName mkBaseExp [] depth = do-  let fieldVars = [mkName ("field" ++ show i) | i <- [0..depth-1]]-  mkBaseExp fieldVars--buildFieldValidation argMap handlerName mkBaseExp ((fieldName, _, fieldType):remainingFields) depth = do-  let fieldStr = nameBase fieldName-  let (isOptional, innerType) = unwrapMaybe fieldType-  let fieldVar = mkName ("field" ++ show depth)--  continuation <- buildFieldValidation argMap handlerName mkBaseExp remainingFields (depth + 1)--  let parseFunc = mkParseFunc innerType--  let fieldPrefix = litE $ stringL $ "field '" <> fieldStr <> "': "--  if isOptional-    then do-      [| case Map.lookup $(litE $ stringL fieldStr) $(varE argMap) of-            Nothing -> do-              let $(varP fieldVar) = Nothing-              $(return continuation)-            Just raw -> case $(parseFunc) raw of-              Left err -> pure $ Left $ InvalidParams ($fieldPrefix <> err)-              Right parsed -> do-                let $(varP fieldVar) = Just parsed-                $(return continuation) |]-    else do-      [| case Map.lookup $(litE $ stringL fieldStr) $(varE argMap) of-            Just raw -> case $(parseFunc) raw of-              Left err -> pure $ Left $ InvalidParams ($fieldPrefix <> err)-              Right $(varP fieldVar) -> $(return continuation)-            Nothing -> pure $ Left $ MissingRequiredParams $(litE $ stringL $ "field '" <> fieldStr <> "' is missing") |]---- Extract field information from parameter type-extractFieldsFromParamType :: Type -> Q [(Name, Bang, Type)]-extractFieldsFromParamType paramType = do-  case paramType of-    ConT typeName -> do-      info <- reify typeName-      case info of-        TyConI (DataD _ _ _ _ [RecC _ fields] _) ->-          return fields-        TyConI (DataD _ _ _ _ [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---- Build the parameter constructor application recursively-buildParameterConstructor :: Type -> [Name] -> Q Exp-buildParameterConstructor paramType fieldVars = do-  case paramType of-    ConT typeName -> do-      info <- reify typeName-      case info of-        TyConI (DataD _ _ _ _ [RecC cn _] _) ->-          -- Record constructor - apply all field variables-          return $ foldl AppE (ConE cn) (map VarE fieldVars)-        TyConI (DataD _ _ _ _ [NormalC cn [(_bang, innerType)]] _) -> do-          -- Single parameter constructor - recurse and wrap-          innerConstructor <- buildParameterConstructor innerType fieldVars-          return $ AppE (ConE cn) innerConstructor-        _ -> 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---- Select the appropriate parse helper for a given type-mkParseFunc :: Type -> Q Exp-mkParseFunc innerType = case innerType of-  ConT n | nameBase n == "Int"     -> [| parseInt |]-  ConT n | nameBase n == "Integer" -> [| parseInteger |]-  ConT n | nameBase n == "Double"  -> [| parseDouble |]-  ConT n | nameBase n == "Float"   -> [| parseFloat |]-  ConT n | nameBase n == "Bool"    -> [| parseBool |]-  _                                -> [| parseText |]--------------------------------------------------------------------------------- -- 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 = do-  info <- reify typeName-  case info of-    TyConI (DataD _ _ _ _ constructors _) -> do-      -- Generate resource definitions-      resourceDefs <- traverse (mkResourceDefWithDescription descriptions) constructors+  cons <- reifyCons typeName+  case cons of+    Just constructors -> do+      -- Static resource definitions: nullary constructors only+      resourceDefs <- traverse (mkResourceDefWithDescription descriptions)+                               (filter isNullary constructors)       listHandlerExp <- [| \_ctx -> pure $(return $ ListE resourceDefs) |] -      -- Generate read handler with cases-      cases <- traverse (mkResourceCase handlerName) constructors-      defaultCase <- [| pure $ Left $ ResourceNotFound $ "Resource not found: " <> T.pack unknown |]-      let defaultMatch = Match (VarP (mkName "unknown")) (NormalB defaultCase) []--      let readHandlerExp = LamE [VarP (mkName "ctx"), VarP (mkName "uri")] $-            CaseE (AppE (VarE 'show) (VarE (mkName "uri")))-              (map clauseToMatch cases ++ [defaultMatch])+      -- 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]-    _ -> fail $ "deriveResourceHandlerWithDescription: " ++ show typeName ++ " is not a data type"+    Nothing -> fail $ "deriveResourceHandlerWithDescription: " ++ show typeName ++ " is not a data type"  -- | Derive resource handlers from a data type. -- Usage:@@ -338,6 +430,50 @@ 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 = do+  cons <- reifyCons typeName+  case cons of+    Just constructors -> do+      templateDefs <- traverse (mkTemplateDefWithDescription descriptions)+                               [c | c@(RecC _ _) <- constructors]+      [| \_ctx -> pure $(return $ ListE templateDefs) |]+    Nothing -> fail $ "deriveResourceTemplatesWithDescription: " ++ 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]++mkTemplateDefWithDescription :: [(String, String)] -> Con -> Q Exp+mkTemplateDefWithDescription _ (RecC name []) =+  fail $ "Resource template constructors need at least one field: " ++ nameBase name+mkTemplateDefWithDescription descriptions (RecC name fields) = do+  let description = descriptionFor descriptions (nameBase name) (nameBase name)+  [| ResourceTemplateDefinition+      { resourceTemplateURITemplate = $(litE $ stringL $ templateURI name fields)+      , resourceTemplateName = $(litE $ stringL $ snakeName name)+      , resourceTemplateDescription = Just $(litE $ stringL description)+      , resourceTemplateMimeType = Just "text/plain"+      , resourceTemplateTitle = Nothing+      } |]+mkTemplateDefWithDescription _ _ = fail "Resource templates require record constructors"+ mkResourceDefWithDescription :: [(String, String)] -> Con -> Q Exp mkResourceDefWithDescription descriptions (NormalC name []) = do   let resourceName = T.pack . snakeName $ name@@ -353,19 +489,53 @@           Just desc -> [| Just $(litE $ stringL desc) |]           Nothing   -> [| Nothing |])       , resourceDefinitionMimeType = Just "text/plain"-      , resourceDefinitionTitle = Nothing  -- 2025-06-18: New title field+      , resourceDefinitionTitle = Nothing       } |] mkResourceDefWithDescription _ _ = 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" -mkResourceCase :: Name -> Con -> Q Clause-mkResourceCase handlerName (NormalC name []) = do-  let resourceName = T.pack . snakeName $ name-  let resourceURI = "resource://" <> T.unpack resourceName-  clause [litP $ stringL resourceURI]-    (normalB [| Right <$> $(varE handlerName) $(varE (mkName "ctx")) $(varE (mkName "uri")) $(conE name) |])-    []-mkResourceCase _ _ = 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@@ -377,24 +547,24 @@ -- > $(deriveToolHandlerWithDescription ''MyTool 'handleTool [("Constructor", "Description")]) deriveToolHandlerWithDescription :: Name -> Name -> [(String, String)] -> Q Exp deriveToolHandlerWithDescription typeName handlerName descriptions = do-  info <- reify typeName-  case info of-    TyConI (DataD _ _ _ _ constructors _) -> do+  cons <- reifyCons typeName+  case cons of+    Just constructors -> do       -- Generate tool definitions       toolDefs <- traverse (mkToolDefWithDescription descriptions) constructors        listHandlerExp <- [| \_ctx -> pure $(return $ ListE toolDefs) |]        -- Generate call handler with cases-      cases <- traverse (mkDispatchCase handlerName) constructors-      defaultCase <- [| pure $ Left $ UnknownTool $ "Unknown tool: " <> name |]+      cases <- traverse (mkDispatchCase ToolArgs handlerName) constructors+      defaultCase <- [| pure $ Left $ UnknownTool $ "Unknown tool: " <> $(varE argNameN) |]       let defaultMatch = Match WildP (NormalB defaultCase) []-      let callHandlerExp = LamE [VarP (mkName "ctx"), VarP (mkName "name"), VarP (mkName "args")] $-            CaseE (AppE (VarE 'T.unpack) (VarE (mkName "name")))+      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]-    _ -> fail $ "deriveToolHandlerWithDescription: " ++ show typeName ++ " is not a data type"+    Nothing -> fail $ "deriveToolHandlerWithDescription: " ++ show typeName ++ " is not a data type"  -- | Derive tool handlers from a data type. -- Usage:@@ -410,25 +580,11 @@   let sname = snakeName name   let description = descriptionFor descriptions (nameBase name) (nameBase name)   fields <- getConFields con-  props <- traverse (mkProperty descriptions) fields-  let required = requiredFieldNames fields+  shapeExp <- mkObjectShape descriptions fields   [| ToolDefinition       { toolDefinitionName = $(litE $ stringL sname)       , toolDefinitionDescription = $(litE $ stringL description)-      , toolDefinitionInputSchema = InputSchemaDefinitionObject-          { properties = $(return $ ListE props)-          , required = $(return $ ListE $ map (LitE . StringL) required)-          }+      , toolDefinitionInputSchema = Schema Nothing $(return shapeExp)+      , toolDefinitionOutputSchema = Nothing       , toolDefinitionTitle = Nothing       } |]--mkProperty :: [(String, String)] -> (Name, Bang, Type) -> Q Exp-mkProperty descriptions (fieldName, _, fieldType) = do-  let fieldStr = nameBase fieldName-  let description = descriptionFor descriptions fieldStr fieldStr-  let (_, innerType) = unwrapMaybe fieldType-  let jsonType = jsonTypeFor innerType-  [| ($(litE $ stringL fieldStr), InputSchemaDefinitionProperty-      { propertyType = $(litE $ stringL jsonType)-      , propertyDescription = $(litE $ stringL description)-      }) |]
src/MCP/Server/Derive/Internal.hs view
@@ -1,18 +1,62 @@ {-# LANGUAGE OverloadedStrings #-} +-- | Runtime support for the code generated by "MCP.Server.Derive".+--+-- Two families of parsers exist because the MCP specification types the two+-- argument channels differently: prompt arguments are always strings, while+-- tool arguments are full JSON values. The Value-based parsers are lenient:+-- they accept the native JSON type and fall back to parsing the string+-- representation, since many clients send numbers and booleans as strings. module MCP.Server.Derive.Internal-  ( parseText+  ( -- * Text-based parsers (prompt arguments)+    parseText   , parseInt   , parseInteger   , parseDouble   , parseFloat   , parseBool+    -- * Value-based parsers (tool arguments)+  , valueText+  , valueInt+  , valueInteger+  , valueDouble+  , valueFloat+  , valueBool+  , valueArray+  , renderValue+    -- * Argument/field extraction+  , requiredArg+  , optionalArg+  , requiredTextArg+  , optionalTextArg+  , requiredField+  , optionalField+    -- * Resource template matching+  , matchTemplateSegments+  , templateField   ) where -import           Data.Text (Text)-import qualified Data.Text as T-import           Text.Read (readMaybe)+import           Data.Aeson           (Value (..), encode)+import qualified Data.Aeson.Key       as Key+import qualified Data.Aeson.KeyMap    as KM+import qualified Data.ByteString.Lazy as BSL+import           Data.Foldable        (toList)+import           Data.Map             (Map)+import qualified Data.Map             as Map+import           Data.Scientific      (base10Exponent, floatingOrInteger,+                                       toBoundedInteger, toRealFloat)+import           Data.Text            (Text)+import qualified Data.Text            as T+import qualified Data.Text.Encoding   as TE+import           Network.URI          (unEscapeString)+import           Text.Read            (readMaybe) +import           MCP.Server.Types     (Error (..))++-- ---------------------------------------------------------------------------+-- Text-based parsers (prompt arguments are strings per the MCP spec)+-- ---------------------------------------------------------------------------+ parseText :: Text -> Either Text Text parseText = Right @@ -41,3 +85,131 @@   "true"  -> Right True   "false" -> Right False   _       -> Left $ "Failed to parse Bool from: " <> t++-- ---------------------------------------------------------------------------+-- Value-based parsers (tool arguments are full JSON values)+-- ---------------------------------------------------------------------------++-- | A short rendering of a JSON value for error messages.+renderValue :: Value -> Text+renderValue (String t) = t+renderValue v          = TE.decodeUtf8 $ BSL.toStrict $ encode v++valueText :: Value -> Either Text Text+valueText (String t) = Right t+valueText v          = Left $ "Failed to parse Text from: " <> renderValue v++valueInt :: Value -> Either Text Int+valueInt (Number n) = case toBoundedInteger n of+  Just i  -> Right i+  Nothing -> Left $ "Failed to parse Int from: " <> renderValue (Number n)+valueInt (String t) = parseInt t+valueInt v          = Left $ "Failed to parse Int from: " <> renderValue v++valueInteger :: Value -> Either Text Integer+valueInteger (Number n)+  -- Bound the exponent before materializing the Integer: aeson keeps huge+  -- exponents compact, so a tiny payload like 1e1000000000 would otherwise+  -- allocate a billion-digit Integer (same bound aeson itself uses for its+  -- FromJSON Integer instance).+  | base10Exponent n > 1024 =+      Left "Failed to parse Integer: exponent too large"+  | otherwise = case floatingOrInteger n :: Either Double Integer of+      Right i -> Right i+      Left _  -> Left $ "Failed to parse Integer from: " <> renderValue (Number n)+valueInteger (String t) = parseInteger t+valueInteger v          = Left $ "Failed to parse Integer from: " <> renderValue v++valueDouble :: Value -> Either Text Double+valueDouble (Number n) = Right (toRealFloat n)+valueDouble (String t) = parseDouble t+valueDouble v          = Left $ "Failed to parse Double from: " <> renderValue v++valueFloat :: Value -> Either Text Float+valueFloat (Number n) = Right (toRealFloat n)+valueFloat (String t) = parseFloat t+valueFloat v          = Left $ "Failed to parse Float from: " <> renderValue v++valueBool :: Value -> Either Text Bool+valueBool (Bool b)   = Right b+valueBool (String t) = parseBool t+valueBool v          = Left $ "Failed to parse Bool from: " <> renderValue v++valueArray :: (Value -> Either Text a) -> Value -> Either Text [a]+valueArray p (Array xs) = traverse p (toList xs)+valueArray _ v          = Left $ "Failed to parse array from: " <> renderValue v++-- ---------------------------------------------------------------------------+-- Argument extraction from the top-level arguments map+-- ---------------------------------------------------------------------------++prefixed :: Text -> Either Text a -> Either Error a+prefixed name = either (Left . InvalidParams . (("field '" <> name <> "': ") <>)) Right++-- | A required tool argument: missing yields 'MissingRequiredParams', a parse+-- failure yields 'InvalidParams' with the field name prefixed.+requiredArg :: Text -> (Value -> Either Text a) -> Map Text Value -> Either Error a+requiredArg name p args = case Map.lookup name args of+  Nothing -> Left $ MissingRequiredParams $ "field '" <> name <> "' is missing"+  Just v  -> prefixed name (p v)++-- | An optional tool argument: absent (or JSON null) becomes 'Nothing'.+optionalArg :: Text -> (Value -> Either Text a) -> Map Text Value -> Either Error (Maybe a)+optionalArg name p args = case Map.lookup name args of+  Nothing   -> Right Nothing+  Just Null -> Right Nothing+  Just v    -> Just <$> prefixed name (p v)++-- | A required prompt argument (string-valued).+requiredTextArg :: Text -> (Text -> Either Text a) -> Map Text Text -> Either Error a+requiredTextArg name p args = case Map.lookup name args of+  Nothing -> Left $ MissingRequiredParams $ "field '" <> name <> "' is missing"+  Just t  -> prefixed name (p t)++-- | An optional prompt argument (string-valued).+optionalTextArg :: Text -> (Text -> Either Text a) -> Map Text Text -> Either Error (Maybe a)+optionalTextArg name p args = case Map.lookup name args of+  Nothing -> Right Nothing+  Just t  -> Just <$> prefixed name (p t)++-- ---------------------------------------------------------------------------+-- Field extraction from nested JSON objects+-- ---------------------------------------------------------------------------++-- | A required field of a nested object argument.+requiredField :: Text -> (Value -> Either Text a) -> KM.KeyMap Value -> Either Text a+requiredField name p o = case KM.lookup (Key.fromText name) o of+  Nothing -> Left $ "field '" <> name <> "' is missing"+  Just v  -> either (Left . (("field '" <> name <> "': ") <>)) Right (p v)++-- | An optional field of a nested object argument: absent or null is 'Nothing'.+optionalField :: Text -> (Value -> Either Text a) -> KM.KeyMap Value -> Either Text (Maybe a)+optionalField name p o = case KM.lookup (Key.fromText name) o of+  Nothing   -> Right Nothing+  Just Null -> Right Nothing+  Just v    -> either (Left . (("field '" <> name <> "': ") <>)) (Right . Just) (p v)++-- ---------------------------------------------------------------------------+-- Resource template matching+-- ---------------------------------------------------------------------------++-- | Match a URI against a template of the form @\<prefix\>{a}\/{b}\/…@: the+-- rendered URI must start with the prefix and continue with exactly @n@+-- non-empty, slash-separated segments, which are returned percent-decoded.+-- Any query or fragment on the URI is ignored — templates declare only path+-- variables (a literal @?@ or @#@ inside a segment value arrives+-- percent-encoded and is unaffected).+matchTemplateSegments :: Text -> Int -> String -> Maybe [Text]+matchTemplateSegments prefix n uriStr = do+  let base = T.takeWhile (\c -> c /= '?' && c /= '#') (T.pack uriStr)+  rest <- T.stripPrefix prefix base+  let segs = T.splitOn "/" rest+  if length segs == n && all (not . T.null) segs+    then Just (map (T.pack . unEscapeString . T.unpack) segs)+    else Nothing++-- | Decode one matched template segment into a field value.+templateField :: Text -> (Text -> Either Text a) -> [Text] -> Int -> Either Error a+templateField name p segs i = case drop i segs of+  (s:_) -> either (Left . InvalidParams . (("template field '" <> name <> "': ") <>)) Right (p s)+  []    -> Left $ InvalidParams $ "missing segment for template field '" <> name <> "'"
src/MCP/Server/Handlers.hs view
@@ -9,25 +9,32 @@     -- * Individual Request Handlers   , handleInitialize   , handlePing+  , handleServerDiscover   , handlePromptsList   , handlePromptsGet   , handleResourcesList   , handleResourcesRead+  , handleResourcesTemplatesList   , handleToolsList   , handleToolsCall+  , handleCompletionComplete      -- * Protocol Support   , validateProtocolVersion   , getMessageSummary+  , metaProtocolVersion+  , unsupportedVersionError      -- * Error Conversion   , errorCodeFromMcpError   , errorMessageFromMcpError   ) where -import           Control.Monad.IO.Class (MonadIO, liftIO) import           Data.Aeson+import qualified Data.Aeson.Key         as Key+import qualified Data.Aeson.KeyMap      as KM import qualified Data.Map               as Map+import           Data.Maybe             (fromMaybe, isJust) import           Data.Text              (Text) import qualified Data.Text              as T import           System.IO              (hPutStrLn, stderr)@@ -62,51 +69,157 @@ -- Per MCP spec: "If the server supports the requested protocol version, -- it MUST respond with the same version. Otherwise, the server MUST respond -- with another protocol version it supports."+--+-- Only /legacy/ revisions are eligible here: a client that negotiates via+-- @initialize@ is legacy by definition, so proposing @2026-07-28@ (or+-- anything unknown) negotiates down to the newest legacy revision rather+-- than promising stateless semantics inside a handshake. validateProtocolVersion :: Text -> Either Text Text validateProtocolVersion clientVersion   | clientVersion `elem` supportedVersions = Right clientVersion  -- Supported: echo the client's own version   | otherwise = Right protocolVersion  -- Unknown: negotiate down to the server's default version --- | Handle an MCP message and return a response if needed-handleMcpMessage :: (MonadIO m)-                 => McpServerInfo-                 -> McpServerHandlers m+-- | Look up an @io.modelcontextprotocol/\<key\>@ entry in a request's+-- params @_meta@ object.+metaLookup :: Text -> Maybe Value -> Maybe Value+metaLookup key params = do+  Object o <- params+  Object m <- KM.lookup "_meta" o+  KM.lookup (Key.fromText ("io.modelcontextprotocol/" <> key)) m++-- | The protocol revision a request declares in its params @_meta@+-- (modern, 2026-07-28+ clients). 'Nothing' for legacy requests.+metaProtocolVersion :: Maybe Value -> Maybe Text+metaProtocolVersion params = case metaLookup "protocolVersion" params of+  Just (String v) -> Just v+  _               -> Nothing++-- | The @UnsupportedProtocolVersionError@ (-32022) for a declared version+-- this library does not implement, listing what it does.+unsupportedVersionError :: Text -> JsonRpcError+unsupportedVersionError requested = JsonRpcError+  { errorCode = -32022+  , errorMessage = "Unsupported protocol version"+  , errorData = Just $ object+      [ "supported" .= allVersions+      , "requested" .= requested+      ]+  }++-- | Methods whose modern results carry the required cacheability fields.+cacheableMethods :: [Text]+cacheableMethods =+  [ "server/discover"+  , "tools/list"+  , "prompts/list"+  , "resources/list"+  , "resources/read"+  , "resources/templates/list"+  ]++-- | Stamp the modern-revision result envelope onto a successful response:+-- @resultType: \"complete\"@, the server's identity in result @_meta@, and+-- (for cacheable methods) @ttlMs@ and @cacheScope@. Error responses and+-- legacy responses pass through untouched.+decorateModern :: McpServerInfo -> CacheHints -> Text -> JsonRpcResponse -> JsonRpcResponse+decorateModern serverInfo hints method resp = case responseResult resp of+  Just (Object o) -> resp { responseResult = Just $ Object $ decorate o }+  _               -> resp+  where+    decorate = insertCache . KM.insert "resultType" (String "complete") . insertMeta+    insertMeta o = KM.insert "_meta" (Object meta) o+      where meta = case KM.lookup "_meta" o of+              Just (Object m) -> KM.insert serverInfoKey serverInfoVal m+              _               -> KM.singleton serverInfoKey serverInfoVal+    serverInfoKey = "io.modelcontextprotocol/serverInfo"+    serverInfoVal = object+      [ "name" .= serverName serverInfo+      , "version" .= serverVersion serverInfo+      ]+    insertCache o+      | method `elem` cacheableMethods =+          KM.insert "ttlMs" (toJSON (cacheTtlMs hints)) $+          KM.insert "cacheScope"+            (String (if cacheScopePublic hints then "public" else "private")) o+      | otherwise = o++-- | Handle an MCP message and return a response if needed.+--+-- The server is dual-era: a request that declares a protocol revision in+-- its params @_meta@ (2026-07-28+) is served statelessly with the modern+-- result envelope; a request that does not is served exactly as before+-- under the revision negotiated by @initialize@.+handleMcpMessage :: McpServerInfo+                 -> CacheHints+                 -> NotificationSupport+                 -> McpServerHandlers                  -> ClientContext                  -> JsonRpcMessage-                 -> m (Maybe JsonRpcMessage)-handleMcpMessage serverInfo handlers ctx (JsonRpcMessageRequest req) = do-  response <- case requestMethod req of-    "initialize" -> handleInitialize serverInfo handlers req-    "ping" -> handlePing req-    "prompts/list" -> handlePromptsList handlers ctx req-    "prompts/get" -> handlePromptsGet handlers ctx req-    "resources/list" -> handleResourcesList handlers ctx req-    "resources/read" -> handleResourcesRead handlers ctx req-    "tools/list" -> handleToolsList handlers ctx req-    "tools/call" -> handleToolsCall handlers ctx req-    method -> return $ makeErrorResponse (requestId req) $ JsonRpcError-      { errorCode = -32601-      , errorMessage = "Method not found: " <> method-      , errorData = Nothing-      }-  return $ Just $ JsonRpcMessageResponse response+                 -> IO (Maybe JsonRpcMessage)+handleMcpMessage serverInfo hints notifSupport handlers ctx0 (JsonRpcMessageRequest req) = do+  let params = requestParams req+      declaredVersion = metaProtocolVersion params+  case declaredVersion of+    Just v | v `notElem` modernVersions ->+      return $ Just $ JsonRpcMessageResponse $+        makeErrorResponse (requestId req) (unsupportedVersionError v)+    _ -> do+      -- server/discover is itself a modern-revision method (and the+      -- backwards-compatibility probe), so it always gets the modern+      -- envelope even if a probing client omitted _meta.+      let modern = isJust declaredVersion || requestMethod req == "server/discover"+          ctx = ctx0+            { clientProtocolVersion = declaredVersion+            , clientInfo = metaLookup "clientInfo" params+            , clientCapabilities = metaLookup "clientCapabilities" params+            }+      response <- case requestMethod req of+        -- Era purity: the modern revision has neither initialize (nothing to+        -- negotiate statelessly) nor ping (removed) — a request declaring a+        -- modern revision must be served "according to this revision", so+        -- these are unknown methods there.+        m | isJust declaredVersion, m `elem` ["initialize", "ping"] ->+              return $ makeErrorResponse (requestId req) $ JsonRpcError+                { errorCode = -32601+                , errorMessage = "Method not found: " <> m+                    <> " (not part of protocol revision " <> fromMaybe "" declaredVersion <> ")"+                , errorData = Nothing+                }+        "initialize" -> handleInitialize serverInfo notifSupport handlers req+        "ping" -> handlePing req+        "server/discover" -> handleServerDiscover serverInfo notifSupport handlers req+        "prompts/list" -> handlePromptsList handlers ctx req+        "prompts/get" -> handlePromptsGet handlers ctx req+        "resources/list" -> handleResourcesList handlers ctx req+        "resources/read" -> handleResourcesRead handlers ctx req+        "resources/templates/list" -> handleResourcesTemplatesList handlers ctx req+        "tools/list" -> handleToolsList handlers ctx req+        "tools/call" -> handleToolsCall handlers ctx req+        "completion/complete" -> handleCompletionComplete handlers ctx req+        method -> return $ makeErrorResponse (requestId req) $ JsonRpcError+          { errorCode = -32601+          , errorMessage = "Method not found: " <> method+          , errorData = Nothing+          }+      let response' = if modern+            then decorateModern serverInfo hints (requestMethod req) response+            else response+      return $ Just $ JsonRpcMessageResponse response' -handleMcpMessage _ _ _ (JsonRpcMessageNotification notif) = do+handleMcpMessage _ _ _ _ _ (JsonRpcMessageNotification notif) = do   case notificationMethod notif of-    "notifications/initialized" -> do-      liftIO $ hPutStrLn stderr "Received initialized notification - server is ready for operation"-      return ()-    _ -> do-      liftIO $ hPutStrLn stderr $ "Received unknown notification: " ++ T.unpack (notificationMethod notif)-      return ()+    "notifications/initialized" ->+      hPutStrLn stderr "Received initialized notification - server is ready for operation"+    _ ->+      hPutStrLn stderr $ "Received unknown notification: " ++ T.unpack (notificationMethod notif)   return Nothing -handleMcpMessage _ _ _ (JsonRpcMessageResponse _) =+handleMcpMessage _ _ _ _ _ (JsonRpcMessageResponse _) =   return Nothing  -- | Handle initialize request-handleInitialize :: (MonadIO m) => McpServerInfo -> McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse-handleInitialize serverInfo handlers req = do+handleInitialize :: McpServerInfo -> NotificationSupport -> McpServerHandlers -> JsonRpcRequest -> IO JsonRpcResponse+handleInitialize serverInfo notifSupport handlers req = do   case requestParams req of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError       { errorCode = -32602@@ -130,29 +243,66 @@               , errorData = Nothing               }             Right negotiatedVersion -> do-              liftIO $ hPutStrLn stderr $ "Client version: " ++ T.unpack clientVersion ++ ", using: " ++ T.unpack negotiatedVersion-              -- Only advertise a capability that actually has a handler.-              -- Advertising e.g. "prompts" while prompts/list returns an error-              -- makes strict clients (e.g. Crush) drop the whole server.-              let capabilities = ServerCapabilities-                    { capabilityPrompts   = PromptCapabilities { promptListChanged = Nothing } <$ prompts handlers-                    , capabilityResources = ResourceCapabilities { resourceSubscribe = Nothing, resourceListChanged = Nothing } <$ resources handlers-                    , capabilityTools     = ToolCapabilities { toolListChanged = Nothing } <$ tools handlers-                    , capabilityLogging   = Nothing  -- Not supported yet-                    }+              hPutStrLn stderr $ "Client version: " ++ T.unpack clientVersion ++ ", using: " ++ T.unpack negotiatedVersion               let response = InitializeResponse                     { initRespProtocolVersion = negotiatedVersion-                    , initRespCapabilities = capabilities+                    -- Legacy clients can only receive pushed notifications+                    -- where the transport supports it (stdio); the modern+                    -- subscribe mechanism is not part of their revision.+                    , initRespCapabilities =+                        handlerCapabilities (supportsLegacyPush notifSupport) False handlers                     , initRespServerInfo = serverInfo                     }               return $ makeSuccessResponse (requestId req) (toJSON response) +-- | Only advertise a capability that actually has a handler.+-- Advertising e.g. "prompts" while prompts/list returns an error+-- makes strict clients (e.g. Crush) drop the whole server.+--+-- The flags say whether change notifications (@listChanged@) and resource+-- update subscriptions (@subscribe@) are deliverable to the requesting+-- client — which depends on transport and era, so callers decide.+handlerCapabilities :: Bool -> Bool -> McpServerHandlers -> ServerCapabilities+handlerCapabilities listChanged subscribe handlers = ServerCapabilities+  { capabilityPrompts   = PromptCapabilities { promptListChanged = flag listChanged } <$ prompts handlers+  , capabilityResources =+      if isJust (resources handlers) || isJust (resourceTemplates handlers)+        then Just ResourceCapabilities+              { resourceSubscribe = flag subscribe+              , resourceListChanged = flag listChanged+              }+        else Nothing+  , capabilityTools     = ToolCapabilities { toolListChanged = flag listChanged } <$ tools handlers+  , capabilityCompletions = CompletionCapabilities <$ completions handlers+  , capabilityLogging   = Nothing  -- Not supported yet+  }+  where+    flag b = if b then Just True else Nothing+ -- | Handle ping request-handlePing :: (MonadIO m) => JsonRpcRequest -> m JsonRpcResponse+handlePing :: JsonRpcRequest -> IO JsonRpcResponse handlePing req = return $ makeSuccessResponse (requestId req) (toJSON PongResponse) +-- | Handle server/discover (2026-07-28+): the server's supported protocol+-- revisions, capabilities and identity, available before any other request.+-- Also serves as the stdio backwards-compatibility probe. The modern result+-- envelope (resultType, serverInfo _meta, cacheability) is stamped on by+-- 'decorateModern'.+handleServerDiscover :: McpServerInfo -> NotificationSupport -> McpServerHandlers -> JsonRpcRequest -> IO JsonRpcResponse+handleServerDiscover serverInfo notifSupport handlers req = do+  -- Modern clients receive notifications via subscriptions/listen, so both+  -- listChanged and subscribe hinge on that being served.+  let listen = supportsListen notifSupport+      result = object $+        [ "supportedVersions" .= allVersions+        , "capabilities" .= handlerCapabilities listen listen handlers+        ] ++ (if T.null (serverInstructions serverInfo)+                then []+                else ["instructions" .= serverInstructions serverInfo])+  return $ makeSuccessResponse (requestId req) result+ -- | Handle prompts/list request-handlePromptsList :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handlePromptsList :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse handlePromptsList handlers ctx req =   case prompts handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError@@ -168,7 +318,7 @@       return $ makeSuccessResponse (requestId req) (toJSON response)  -- | Handle prompts/get request-handlePromptsGet :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handlePromptsGet :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse handlePromptsGet handlers ctx req =   case prompts handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError@@ -191,7 +341,9 @@               , errorData = Nothing               }             Success getReq -> do-              let args = maybe [] (map (\(k, v) -> (k, jsonValueToText v)) . Map.toList) (promptsGetArguments getReq)+              -- Prompt arguments are string-valued per the MCP spec; flatten+              -- any non-string values a lenient client may have sent.+              let args = maybe Map.empty (fmap jsonValueToText) (promptsGetArguments getReq)               result <- getHandler ctx (promptsGetName getReq) args               case result of                 Left err -> return $ makeErrorResponse (requestId req) $ JsonRpcError@@ -199,18 +351,24 @@                   , errorMessage = errorMessageFromMcpError err                   , errorData = Nothing                   }-                Right content -> do+                Right promptRes -> do                   let response = PromptsGetResponse-                        { promptsGetDescription = Nothing-                        , promptsGetMessages = [PromptMessage RoleUser content]-                        , promptsGetMeta = Nothing  -- Can be extended for additional metadata+                        { promptsGetDescription = promptResultDescription promptRes+                        , promptsGetMessages = promptResultMessages promptRes+                        , promptsGetMeta = Nothing                         }                   return $ makeSuccessResponse (requestId req) (toJSON response)  -- | Handle resources/list request-handleResourcesList :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handleResourcesList :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse handleResourcesList handlers ctx req =   case resources handlers of+    -- A templates-only configuration advertises the resources capability,+    -- so resources/list must answer (with an empty list) rather than error:+    -- strict clients drop servers whose advertised capabilities error.+    Nothing | isJust (resourceTemplates handlers) ->+      return $ makeSuccessResponse (requestId req)+        (toJSON (ResourcesListResponse { resourcesListResources = [] }))     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError       { errorCode = -32601       , errorMessage = "Resources not supported"@@ -224,7 +382,7 @@       return $ makeSuccessResponse (requestId req) (toJSON response)  -- | Handle resources/read request-handleResourcesRead :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handleResourcesRead :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse handleResourcesRead handlers ctx req =   case resources handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError@@ -260,8 +418,62 @@                         }                   return $ makeSuccessResponse (requestId req) (toJSON response) +-- | Handle resources/templates/list request+handleResourcesTemplatesList :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse+handleResourcesTemplatesList handlers ctx req =+  case resourceTemplates handlers of+    Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError+      { errorCode = -32601+      , errorMessage = "Resource templates not supported"+      , errorData = Nothing+      }+    Just listHandler -> do+      templates <- listHandler ctx+      let response = ResourcesTemplatesListResponse+            { resourcesTemplatesList = templates+            }+      return $ makeSuccessResponse (requestId req) (toJSON response)++-- | Handle completion/complete request+handleCompletionComplete :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse+handleCompletionComplete handlers ctx req =+  case completions handlers of+    Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError+      { errorCode = -32601+      , errorMessage = "Completions not supported"+      , errorData = Nothing+      }+    Just completeHandler ->+      case requestParams req of+        Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError+          { errorCode = -32602+          , errorMessage = "Missing parameters"+          , errorData = Nothing+          }+        Just params ->+          case fromJSON params of+            Error err -> return $ makeErrorResponse (requestId req) $ JsonRpcError+              { errorCode = -32602+              , errorMessage = "Invalid parameters: " <> T.pack err+              , errorData = Nothing+              }+            Success completeReq -> do+              result <- completeHandler ctx+                (completeRef completeReq)+                (completeArgumentName completeReq)+                (completeArgumentValue completeReq)+                (completeContextArgs completeReq)+              case result of+                Left err -> return $ makeErrorResponse (requestId req) $ JsonRpcError+                  { errorCode = errorCodeFromMcpError err+                  , errorMessage = errorMessageFromMcpError err+                  , errorData = Nothing+                  }+                Right completion ->+                  return $ makeSuccessResponse (requestId req) (toJSON (CompleteResponse completion))+ -- | Handle tools/list request-handleToolsList :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handleToolsList :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse handleToolsList handlers ctx req =   case tools handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError@@ -277,7 +489,7 @@       return $ makeSuccessResponse (requestId req) (toJSON response)  -- | Handle tools/call request-handleToolsCall :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handleToolsCall :: McpServerHandlers -> ClientContext -> JsonRpcRequest -> IO JsonRpcResponse handleToolsCall handlers ctx req =   case tools handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError@@ -300,7 +512,8 @@               , errorData = Nothing               }             Success callReq -> do-              let args = maybe [] (map (\(k, v) -> (k, jsonValueToText v)) . Map.toList) (toolsCallArguments callReq)+              -- Tool arguments are passed through as full JSON values.+              let args = fromMaybe Map.empty (toolsCallArguments callReq)               result <- callHandler ctx (toolsCallName callReq) args               case result of                 Left err -> return $ makeErrorResponse (requestId req) $ JsonRpcError@@ -308,11 +521,12 @@                   , errorMessage = errorMessageFromMcpError err                   , errorData = Nothing                   }-                Right content -> do+                Right toolRes -> do                   let response = ToolsCallResponse-                        { toolsCallContent = [content]-                        , toolsCallIsError = Nothing-                        , toolsCallMeta = Nothing  -- Can be extended for structured output+                        { toolsCallContent = toolResultContent toolRes+                        , toolsCallIsError = if toolResultIsError toolRes then Just True else Nothing+                        , toolsCallStructuredContent = toolResultStructured toolRes+                        , toolsCallMeta = toolResultMeta toolRes                         }                   return $ makeSuccessResponse (requestId req) (toJSON response) 
src/MCP/Server/JsonRpc.hs view
@@ -20,9 +20,10 @@  import Data.Text (Text) import Data.Aeson+import qualified Data.Aeson.KeyMap as KM import Data.Aeson.Types (parseEither)+import Data.Scientific (toBoundedInteger) import GHC.Generics (Generic)-import Control.Applicative ((<|>))  -- | JSON-RPC request ID data RequestId@@ -38,7 +39,9 @@  instance FromJSON RequestId where   parseJSON (String t) = return $ RequestIdText t-  parseJSON (Number n) = return $ RequestIdNumber (round n)+  parseJSON (Number n) = case toBoundedInteger n of+    Just i  -> return $ RequestIdNumber i+    Nothing -> fail "Request id must be an integral number"   parseJSON Null = return RequestIdNull   parseJSON _ = fail "Invalid request ID" @@ -137,12 +140,16 @@   toJSON (JsonRpcMessageResponse resp) = toJSON resp   toJSON (JsonRpcMessageNotification notif) = toJSON notif +-- | Classify by shape instead of parse-fallthrough: a request with a+-- malformed field must fail as a request, not silently succeed as a+-- notification (which has fewer required fields). instance FromJSON JsonRpcMessage where-  parseJSON v = parseRequest v <|> parseResponse v <|> parseNotification v-    where-      parseRequest = fmap JsonRpcMessageRequest . parseJSON-      parseResponse = fmap JsonRpcMessageResponse . parseJSON-      parseNotification = fmap JsonRpcMessageNotification . parseJSON+  parseJSON = withObject "JsonRpcMessage" $ \o ->+    case (KM.member "method" o, KM.member "id" o) of+      (True, True)   -> JsonRpcMessageRequest <$> parseJSON (Object o)+      (True, False)  -> JsonRpcMessageNotification <$> parseJSON (Object o)+      (False, True)  -> JsonRpcMessageResponse <$> parseJSON (Object o)+      (False, False) -> fail "Not a JSON-RPC message: no method and no id"  -- | Create a successful JSON-RPC response makeSuccessResponse :: RequestId -> Value -> JsonRpcResponse
+ src/MCP/Server/Notifications.hs view
@@ -0,0 +1,188 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Server-initiated change notifications.+--+-- Create a notifier with 'newMcpNotifier', hand the 'NotificationSource' to+-- a transport via its configuration ('MCP.Server.Transport.Stdio.StdioConfig'+-- \/ 'MCP.Server.Transport.Http.HttpConfig'), keep the 'McpNotifier', and+-- call its actions whenever your tool\/prompt\/resource lists (or a specific+-- resource) change:+--+-- > (notifier, source) <- newMcpNotifier+-- > let config = defaultStdioConfig { stdioNotifications = Just source }+-- > _ <- forkIO $ appLogic notifier+-- > runMcpServerStdioWithConfig config serverInfo handlers+--+-- Delivery is transport- and era-specific: modern (2026-07-28) clients open+-- a @subscriptions\/listen@ stream and receive only the notification types+-- they opted into, tagged with their subscription id; legacy stdio clients+-- receive untagged notifications spontaneously after @initialize@. Legacy+-- HTTP has no delivery channel (this library dropped the deprecated GET SSE+-- stream), so the @listChanged@ capabilities are not advertised there.+module MCP.Server.Notifications+  ( -- * Notifier+    McpNotifier(..)+  , NotificationSource+  , newMcpNotifier+  , ChangeEvent(..)+  , subscribeEvents++    -- * Subscription wire format (used by transports)+  , NotificationFilter(..)+  , parseNotificationFilter+  , filterAccepts+  , filterIsEmpty+  , acknowledgedNotification+  , eventNotification+  , legacyEventNotification+  , closureResponse+  ) where++import           Control.Concurrent.STM (STM, TChan, atomically,+                                         dupTChan, newBroadcastTChanIO,+                                         writeTChan)+import           Data.Aeson+import qualified Data.Aeson.KeyMap      as KM+import           Data.Aeson.Types       (Pair)+import           Data.Text              (Text)+import qualified Data.Text              as T+import           Network.URI            (URI)++import           MCP.Server.JsonRpc+import           MCP.Server.Types       (McpServerInfo (..))++-- | A change an application can announce.+data ChangeEvent+  = ToolsListChangedEvent+  | PromptsListChangedEvent+  | ResourcesListChangedEvent+  | ResourceUpdatedEvent Text  -- ^ the resource URI, rendered+  deriving (Show, Eq)++-- | The application-facing handle: call these when things change.+data McpNotifier = McpNotifier+  { notifyToolsListChanged     :: IO ()+  , notifyPromptsListChanged   :: IO ()+  , notifyResourcesListChanged :: IO ()+  , notifyResourceUpdated      :: URI -> IO ()+  }++-- | The transport-facing end of a notifier: subscribe with+-- 'subscribeEvents'. Backed by a broadcast 'TChan', so events published+-- while nobody listens are dropped rather than retained.+newtype NotificationSource = NotificationSource (TChan ChangeEvent)++-- | Create a notifier and its source.+newMcpNotifier :: IO (McpNotifier, NotificationSource)+newMcpNotifier = do+  chan <- newBroadcastTChanIO+  let publish = atomically . writeTChan chan+  pure+    ( McpNotifier+        { notifyToolsListChanged     = publish ToolsListChangedEvent+        , notifyPromptsListChanged   = publish PromptsListChangedEvent+        , notifyResourcesListChanged = publish ResourcesListChangedEvent+        , notifyResourceUpdated      = publish . ResourceUpdatedEvent . T.pack . show+        }+    , NotificationSource chan+    )++-- | A private read end delivering every event published after the call.+subscribeEvents :: NotificationSource -> STM (TChan ChangeEvent)+subscribeEvents (NotificationSource chan) = dupTChan chan++-- | Which notification types a @subscriptions\/listen@ request opted into.+data NotificationFilter = NotificationFilter+  { filterTools        :: Bool+  , filterPrompts      :: Bool+  , filterResources    :: Bool+  , filterResourceSubs :: [Text]  -- ^ URIs watched for updates+  } deriving (Show, Eq)++-- | Parse the @notifications@ filter out of a @subscriptions\/listen@+-- request's params. Absent fields mean "not subscribed".+parseNotificationFilter :: Maybe Value -> NotificationFilter+parseNotificationFilter params = NotificationFilter+  { filterTools        = boolField "toolsListChanged"+  , filterPrompts      = boolField "promptsListChanged"+  , filterResources    = boolField "resourcesListChanged"+  , filterResourceSubs = subsField+  }+  where+    notifications = do+      Object o <- params+      KM.lookup "notifications" o+    boolField key = case notifications of+      Just (Object n) | Just (Bool b) <- KM.lookup key n -> b+      _ -> False+    subsField = case notifications of+      Just (Object n) | Just (Array xs) <- KM.lookup "resourceSubscriptions" n ->+        [u | String u <- foldr (:) [] xs]+      _ -> []++-- | Whether a subscription with this filter receives the event.+filterAccepts :: NotificationFilter -> ChangeEvent -> Bool+filterAccepts f ToolsListChangedEvent      = filterTools f+filterAccepts f PromptsListChangedEvent    = filterPrompts f+filterAccepts f ResourcesListChangedEvent  = filterResources f+filterAccepts f (ResourceUpdatedEvent uri) = uri `elem` filterResourceSubs f++-- | True when the filter subscribes to nothing at all.+filterIsEmpty :: NotificationFilter -> Bool+filterIsEmpty f =+  not (filterTools f || filterPrompts f || filterResources f)+    && null (filterResourceSubs f)++-- | The @_meta@ object tagging a message with its subscription id (the+-- JSON-RPC id of the @subscriptions\/listen@ request).+subscriptionMeta :: RequestId -> Pair+subscriptionMeta subId =+  "_meta" .= object ["io.modelcontextprotocol/subscriptionId" .= subId]++-- | The mandatory first message of a subscription stream: the honored+-- filter (this library honors every requested type).+acknowledgedNotification :: RequestId -> NotificationFilter -> JsonRpcNotification+acknowledgedNotification subId f =+  makeNotification "notifications/subscriptions/acknowledged" $ Just $ object+    [ subscriptionMeta subId+    , "notifications" .= object (concat+        [ ["toolsListChanged" .= True | filterTools f]+        , ["promptsListChanged" .= True | filterPrompts f]+        , ["resourcesListChanged" .= True | filterResources f]+        , ["resourceSubscriptions" .= filterResourceSubs f | not (null (filterResourceSubs f))]+        ])+    ]++-- | The method and extra params of an event's notification.+eventBody :: ChangeEvent -> (Text, [Pair])+eventBody ToolsListChangedEvent      = ("notifications/tools/list_changed", [])+eventBody PromptsListChangedEvent    = ("notifications/prompts/list_changed", [])+eventBody ResourcesListChangedEvent  = ("notifications/resources/list_changed", [])+eventBody (ResourceUpdatedEvent uri) = ("notifications/resources/updated", ["uri" .= uri])++-- | A change event as a subscription-tagged notification (modern era).+eventNotification :: RequestId -> ChangeEvent -> JsonRpcNotification+eventNotification subId event =+  let (method, extra) = eventBody event+  in makeNotification method $ Just $ object (subscriptionMeta subId : extra)++-- | A change event as an untagged notification (legacy stdio delivery).+legacyEventNotification :: ChangeEvent -> JsonRpcNotification+legacyEventNotification event =+  let (method, extra) = eventBody event+  in makeNotification method $ if null extra then Nothing else Just (object extra)++-- | The graceful-closure response a server sends when it ends a+-- subscription on its own initiative (e.g. shutdown). Like every modern+-- result it carries the server identity alongside the subscription id.+closureResponse :: McpServerInfo -> RequestId -> JsonRpcResponse+closureResponse serverInfo subId = makeSuccessResponse subId $ object+  [ "resultType" .= ("complete" :: Text)+  , "_meta" .= object+      [ "io.modelcontextprotocol/subscriptionId" .= subId+      , "io.modelcontextprotocol/serverInfo" .= object+          [ "name" .= serverName serverInfo+          , "version" .= serverVersion serverInfo+          ]+      ]+  ]
src/MCP/Server/Protocol.hs view
@@ -22,7 +22,12 @@   , ResourcesListResponse(..)   , ResourcesReadRequest(..)   , ResourcesReadResponse(..)+  , ResourcesTemplatesListResponse(..) +    -- * Completion Protocol+  , CompleteRequest(..)+  , CompleteResponse(..)+     -- * Tools Protocol   , ToolsListRequest(..)   , ToolsListResponse(..)@@ -35,20 +40,27 @@     -- * Protocol Functions   , protocolVersion   , supportedVersions+  , modernVersions+  , allVersions   ) where  import           Data.Aeson+import           Data.Aeson.Types (Parser) import           Data.Map         (Map) import           Data.Text        (Text)+import qualified Data.Text        as T import           GHC.Generics     (Generic) import           MCP.Server.Types --- | The protocol revision the server advertises by default. Used as the--- fallback when a client proposes a version this library does not recognise.+-- | The newest /legacy/ (initialize-handshake) revision the server+-- advertises by default. Used as the fallback when an initializing client+-- proposes a version this library does not recognise — deliberately capped+-- at the newest legacy revision: a client that sends @initialize@ is legacy+-- by definition and must not be promised the stateless 2026-07-28 semantics. protocolVersion :: Text protocolVersion = "2025-11-25" --- | Date-versioned MCP revisions whose wire format for the basic+-- | Legacy (initialize-handshake) revisions whose wire format for the basic -- tool/resource/prompt operations this library implements is identical. -- The server echoes back any of these a client proposes (see -- 'MCP.Server.Handlers.validateProtocolVersion'), satisfying the spec@@ -63,7 +75,21 @@   , "2024-11-05"   ] +-- | Modern (stateless, per-request @_meta@) revisions this library+-- implements. Requests declaring one of these are served with modern+-- semantics: @resultType@, cacheability fields, and @serverInfo@ in result+-- @_meta@.+modernVersions :: [Text]+modernVersions =+  [ "2026-07-28"+  ] +-- | Every revision this library implements, newest first — what+-- @server\/discover@ and 'UnsupportedProtocolVersionError' report.+allVersions :: [Text]+allVersions = modernVersions ++ supportedVersions++ -- | Initialize request data InitializeRequest = InitializeRequest   { initProtocolVersion :: Text@@ -116,26 +142,6 @@ instance ToJSON PongResponse where   toJSON PongResponse = object [] --- | Message role for prompts-data MessageRole = RoleUser | RoleAssistant-  deriving (Show, Eq, Generic)--instance ToJSON MessageRole where-  toJSON RoleUser      = "user"-  toJSON RoleAssistant = "assistant"---- | Prompt message-data PromptMessage = PromptMessage-  { promptMessageRole    :: MessageRole-  , promptMessageContent :: Content-  } deriving (Show, Eq, Generic)--instance ToJSON PromptMessage where-  toJSON msg = object-    [ "role" .= promptMessageRole msg-    , "content" .= promptMessageContent msg-    ]- -- | Prompts list request data PromptsListRequest = PromptsListRequest   deriving (Show, Eq, Generic)@@ -216,6 +222,58 @@     [ "contents" .= resourcesReadContents resp     ] +-- | Resource templates list response+data ResourcesTemplatesListResponse = ResourcesTemplatesListResponse+  { resourcesTemplatesList :: [ResourceTemplateDefinition]+  } deriving (Show, Eq, Generic)++instance ToJSON ResourcesTemplatesListResponse where+  toJSON resp = object+    [ "resourceTemplates" .= resourcesTemplatesList resp+    ]++-- | Completion request (completion/complete)+data CompleteRequest = CompleteRequest+  { completeRef             :: CompletionRef+  , completeArgumentName    :: Text+  , completeArgumentValue   :: Text+  , completeContextArgs     :: Map Text Text+  } deriving (Show, Eq, Generic)++instance FromJSON CompleteRequest where+  parseJSON = withObject "CompleteRequest" $ \o -> do+    refObj <- o .: "ref"+    ref <- withObject "CompletionRef"+      (\r -> do+        refType <- r .: "type" :: Parser Text+        case refType of+          "ref/prompt"   -> CompletionRefPrompt <$> r .: "name"+          "ref/resource" -> CompletionRefResource <$> r .: "uri"+          _              -> fail $ "Unknown completion ref type: " ++ T.unpack refType)+      refObj+    argument <- o .: "argument"+    (name, value) <- withObject "argument"+      (\a -> (,) <$> a .: "name" <*> a .: "value")+      argument+    ctx <- o .:? "context"+    ctxArgs <- case ctx of+      Nothing -> pure mempty+      Just c  -> withObject "context" (\co -> co .:? "arguments" .!= mempty) c+    pure $ CompleteRequest ref name value ctxArgs++-- | Completion response+data CompleteResponse = CompleteResponse+  { completeResult :: CompletionResult+  } deriving (Show, Eq, Generic)++instance ToJSON CompleteResponse where+  toJSON (CompleteResponse res) = object+    [ "completion" .= object+        ([ "values" .= take 100 (completionValues res) ]+          ++ maybe [] (\t -> ["total" .= t]) (completionTotal res)+          ++ maybe [] (\h -> ["hasMore" .= h]) (completionHasMore res))+    ]+ -- | Tools list request data ToolsListRequest = ToolsListRequest   deriving (Show, Eq, Generic)@@ -246,15 +304,17 @@  -- | Tools call response (2025-06-18 enhanced) data ToolsCallResponse = ToolsCallResponse-  { toolsCallContent :: [Content]-  , toolsCallIsError :: Maybe Bool-  , toolsCallMeta :: Maybe Value  -- New _meta field for structured output+  { toolsCallContent           :: [Content]+  , toolsCallIsError           :: Maybe Bool+  , toolsCallStructuredContent :: Maybe Value  -- ^ structuredContent (2025-06-18+)+  , toolsCallMeta              :: Maybe Value   } deriving (Show, Eq, Generic)  instance ToJSON ToolsCallResponse where   toJSON resp = object $     [ "content" .= toolsCallContent resp     ] ++ maybe [] (\e -> ["isError" .= e]) (toolsCallIsError resp)+      ++ maybe [] (\s -> ["structuredContent" .= s]) (toolsCallStructuredContent resp)       ++ maybe [] (\m -> ["_meta" .= m]) (toolsCallMeta resp)  -- | List changed notification
src/MCP/Server/Transport/Http.hs view
@@ -6,12 +6,25 @@     HttpConfig(..)   , transportRunHttp   , defaultHttpConfig++    -- * Request validation (exposed for testing)+  , BodyPeek(..)+  , peekBody+  , peekIsModern+  , validateRequestHeaders+  , decodeSentinel   ) where -import           Control.Monad            (when)+import           Control.Concurrent.STM   (atomically, check, orElse,+                                           readTChan, readTVar, registerDelay)+import           Control.Monad            (forever, when) import           Data.Aeson import qualified Data.Aeson.KeyMap        as KM+import           Data.Aeson.Types         (parseEither)+import qualified Data.ByteString.Base64   as B64+import qualified Data.ByteString.Builder  as B import qualified Data.ByteString.Lazy     as BSL+import           Data.Maybe               (isJust) import           Data.String              (IsString (fromString)) import           Data.Text                (Text) import qualified Data.Text                as T@@ -24,7 +37,9 @@  import           MCP.Server.Handlers import           MCP.Server.JsonRpc-import           MCP.Server.Protocol (protocolVersion, supportedVersions)+import           MCP.Server.Notifications+import           MCP.Server.Protocol (allVersions, modernVersions,+                                      supportedVersions) import           MCP.Server.Types  -- | HTTP transport configuration following the MCP Streamable HTTP specification@@ -45,6 +60,25 @@       --   handler 'ClientContext' as 'clientPrincipal' — while 'Nothing' rejects       --   the request with @401@. Validation and principal assignment are left       --   entirely to the caller.+  , httpAllowedOrigins :: Maybe [Text]+      -- ^ Origin-validation policy (DNS-rebinding protection; the MCP+      --   Streamable HTTP spec requires servers to validate the @Origin@+      --   header). @'Just' origins@ rejects any request whose @Origin@ header+      --   is present but not in the list with @403@, and echoes the allowed+      --   origin in CORS headers instead of @*@. Requests without an @Origin@+      --   header (non-browser clients) are always allowed. 'Nothing' disables+      --   the check — only appropriate when the server is not reachable from+      --   a browser (e.g. bound to localhost for CLI clients).+  , httpCacheHints :: CacheHints+      -- ^ Cacheability hints stamped onto modern (2026-07-28+) list/read+      --   results.+  , httpNotifications :: Maybe NotificationSource+      -- ^ When configured, @subscriptions\/listen@ (2026-07-28) is served as+      --   a long-lived SSE stream delivering the change notifications the+      --   client opted into, and the @listChanged@\/@subscribe@ capabilities+      --   are advertised to modern clients. Legacy HTTP clients have no+      --   delivery channel (the deprecated GET SSE stream is not offered),+      --   so legacy @initialize@ does not advertise them.   }  -- | Default HTTP configuration (authentication disabled).@@ -55,15 +89,22 @@   , httpEndpoint = "/mcp"   , httpVerbose = False   , httpAuthorize = Nothing+  , httpAllowedOrigins = Nothing+  , httpCacheHints = defaultCacheHints+  , httpNotifications = Nothing   }  -- | Helper for conditional logging logVerbose :: HttpConfig -> String -> IO () logVerbose config msg = when (httpVerbose config) $ hPutStrLn stderr msg +-- | Whether a notification source is configured.+httpHasNotifications :: HttpConfig -> Bool+httpHasNotifications = isJust . httpNotifications + -- | Transport-specific implementation for HTTP-transportRunHttp :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> IO ()+transportRunHttp :: HttpConfig -> McpServerInfo -> McpServerHandlers -> IO () transportRunHttp config serverInfo handlers = do   let settings = Warp.setHost (fromString $ httpHost config) $                  Warp.setPort (httpPort config) $@@ -73,34 +114,65 @@   Warp.runSettings settings (mcpApplication config serverInfo handlers)  -- | WAI Application for MCP over HTTP-mcpApplication :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> Wai.Application-mcpApplication config serverInfo handlers req respond = do+mcpApplication :: HttpConfig -> McpServerInfo -> McpServerHandlers -> Wai.Application+mcpApplication config serverInfo handlers req respond0 = do   -- Log the request   logVerbose config $ "HTTP " ++ show (Wai.requestMethod req) ++ " " ++ T.unpack (TE.decodeUtf8 $ Wai.rawPathInfo req) -  -- Authenticate and obtain the caller's principal (if any) before anything-  -- else. CORS preflight requests are exempt: browsers never attach-  -- credentials to an OPTIONS preflight, and the preflight response is what-  -- tells the browser it may send the Authorization header at all.-  decision <- case httpAuthorize config of-    _ | Wai.requestMethod req == "OPTIONS"-               -> pure (Just Nothing)      -- CORS preflight: no credentials-    Nothing    -> pure (Just Nothing)      -- auth disabled: allowed, no principal-    Just check -> fmap (fmap Just) (check (bearerToken req))-  case decision of-    Nothing -> do-      logVerbose config "Request rejected by authorization callback"-      respond $ Wai.responseLBS-        status401-        [("Content-Type", "application/json"), ("WWW-Authenticate", "Bearer")]-        (encode $ object ["error" .= ("Unauthorized" :: Text)])-    Just principal -> do-      let ctx = ClientContext { clientToken = bearerToken req, clientPrincipal = principal }-      -- Check if this is our MCP endpoint-      if TE.decodeUtf8 (Wai.rawPathInfo req) == T.pack (httpEndpoint config)-        then handleMcpRequest config serverInfo handlers ctx req respond-        else respond $ Wai.responseLBS status404 [("Content-Type", "text/plain")] "Not Found"+  let origin = lookup hOrigin (Wai.requestHeaders req)+      -- Every response carries exactly one Access-Control-Allow-Origin header:+      -- the validated request origin when a policy is configured, "*"+      -- otherwise. When the value depends on the request origin, Vary: Origin+      -- keeps shared caches from serving one origin's response to another.+      corsHeaders = case (httpAllowedOrigins config, origin) of+        (Just _, Just o) -> [("Access-Control-Allow-Origin", o), ("Vary", "Origin")]+        _                -> [("Access-Control-Allow-Origin", "*")]+      addCors hs = corsHeaders+                 ++ filter ((/= "Access-Control-Allow-Origin") . fst) hs+      respond = respond0 . Wai.mapResponseHeaders addCors +  -- Origin validation first (DNS-rebinding protection, a MUST in the spec).+  if not (originAllowed config req)+    then do+      logVerbose config $ "Request rejected: disallowed Origin " ++ show origin+      respond0 $ Wai.responseLBS+        status403+        [("Content-Type", "application/json")]+        (encode $ object ["error" .= ("Origin not allowed" :: Text)])+    else do+      -- Authenticate and obtain the caller's principal (if any) before anything+      -- else. CORS preflight requests are exempt: browsers never attach+      -- credentials to an OPTIONS preflight, and the preflight response is what+      -- tells the browser it may send the Authorization header at all.+      decision <- case httpAuthorize config of+        _ | Wai.requestMethod req == "OPTIONS"+                   -> pure (Just Nothing)      -- CORS preflight: no credentials+        Nothing    -> pure (Just Nothing)      -- auth disabled: allowed, no principal+        Just authorize -> fmap (fmap Just) (authorize (bearerToken req))+      case decision of+        Nothing -> do+          logVerbose config "Request rejected by authorization callback"+          respond $ Wai.responseLBS+            status401+            [("Content-Type", "application/json"), ("WWW-Authenticate", "Bearer")]+            (encode $ object ["error" .= ("Unauthorized" :: Text)])+        Just principal -> do+          let ctx = anonymousContext { clientToken = bearerToken req, clientPrincipal = principal }+          -- Check if this is our MCP endpoint+          if TE.decodeUtf8 (Wai.rawPathInfo req) == T.pack (httpEndpoint config)+            then handleMcpRequest config serverInfo handlers ctx req respond+            else respond $ Wai.responseLBS status404 [("Content-Type", "text/plain")] "Not Found"++-- | Whether the request passes the configured Origin policy: with no policy+-- everything passes; with a policy, a present Origin header must be in the+-- allow-list (absent Origin means a non-browser client and always passes).+originAllowed :: HttpConfig -> Wai.Request -> Bool+originAllowed config req = case httpAllowedOrigins config of+  Nothing      -> True+  Just allowed -> case lookup hOrigin (Wai.requestHeaders req) of+    Nothing -> True+    Just o  -> TE.decodeUtf8With lenientDecode o `elem` allowed+ -- | The bearer token presented by a request, if any: the value following -- @Authorization: Bearer @. The scheme is matched case-insensitively per -- RFC 7235, and invalid UTF-8 in the header is replaced rather than thrown.@@ -113,121 +185,259 @@     else Nothing  -- | Handle MCP requests according to Streamable HTTP specification-handleMcpRequest :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> ClientContext -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived+handleMcpRequest :: HttpConfig -> McpServerInfo -> McpServerHandlers -> ClientContext -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived handleMcpRequest config serverInfo handlers ctx req respond = do-  -- Read the POST body up front so we can identify the `initialize` request:-  -- it negotiates the protocol version in its *body*, so (per the Streamable-  -- HTTP spec, which scopes the MCP-Protocol-Version header to "subsequent-  -- requests") it is exempt from the header check. For any other request a-  -- *missing* header is accepted, while a *present but unsupported* one is-  -- rejected with 400.+  -- Read the POST body up front: the request era and header validation both+  -- depend on it. A body declaring a modern protocol revision in _meta gets+  -- the 2026-07-28 header validation (header/body match, Mcp-Method,+  -- Mcp-Name); a legacy body keeps the relaxed pre-2026 rules, where+  -- `initialize` is exempt (it negotiates in the body), a missing+  -- MCP-Protocol-Version header is accepted, and a present-but-unsupported+  -- one is rejected with 400.   body <- if Wai.requestMethod req == "POST" then Wai.strictRequestBody req else pure ""-  if extractMethod body /= Just "initialize" && not (versionHeaderSupported req)-    then do-      logVerbose config "Request rejected: unsupported MCP-Protocol-Version header"-      respond $ Wai.responseLBS-        status400-        [("Content-Type", "application/json")]-        (encode $ object ["error" .= ("Unsupported protocol version. Supported versions: " <> T.intercalate ", " supportedVersions)])-    else-        case Wai.requestMethod req of-          -- GET requests for endpoint discovery-          "GET" -> do-            let discoveryResponse = object-                  [ "name" .= serverName serverInfo-                  , "version" .= serverVersion serverInfo-                  , "description" .= serverInstructions serverInfo-                  , "protocolVersion" .= protocolVersion-                  , "capabilities" .= object-                      [ "tools" .= object []-                      , "prompts" .= object []-                      , "resources" .= object []-                      ]-                  ]-            logVerbose config $ "Sending server discovery response: " ++ show discoveryResponse-            respond $ Wai.responseLBS-              status200-              [("Content-Type", "application/json"), ("Access-Control-Allow-Origin", "*")]-              (encode discoveryResponse)+  let peek = peekBody body+      modern = peekIsModern peek+  case Wai.requestMethod req of+    -- POST requests for JSON-RPC messages+    "POST" ->+      case validateRequestHeaders req peek of+        Just err -> do+          logVerbose config $ "Request rejected: " ++ T.unpack (errorMessage err)+          respond $ Wai.responseLBS+            status400+            [("Content-Type", "application/json")]+            (encode $ makeErrorResponse (peekId peek) err)+        Nothing+          -- subscriptions/listen is transport-level: the response is a+          -- long-lived SSE stream, not a single JSON object+          | peekMethod peek == Just "subscriptions/listen"+          , Just src <- httpNotifications config+          -> handleSubscriptionsListen config src body respond+          | otherwise -> do+              logVerbose config $ "Received POST body (" ++ show (BSL.length body) ++ " bytes): " ++ take 200 (show body)+              handleJsonRpcRequest config serverInfo handlers ctx modern body respond -          -- POST requests for JSON-RPC messages-          "POST" -> do-            logVerbose config $ "Received POST body (" ++ show (BSL.length body) ++ " bytes): " ++ take 200 (show body)-            handleJsonRpcRequest config serverInfo handlers ctx body respond+    -- OPTIONS for CORS preflight+    "OPTIONS" -> respond $ Wai.responseLBS+      status200+      [ ("Access-Control-Allow-Methods", "POST, OPTIONS")+      , ("Access-Control-Allow-Headers", "Content-Type, Authorization, MCP-Protocol-Version, Mcp-Method, Mcp-Name")+      ]+      "" -          -- OPTIONS for CORS preflight-          "OPTIONS" -> respond $ Wai.responseLBS-            status200-            [ ("Access-Control-Allow-Origin", "*")-            , ("Access-Control-Allow-Methods", "GET, POST, OPTIONS")-            , ("Access-Control-Allow-Headers", "Content-Type, Authorization, MCP-Protocol-Version")-            ]-            ""+    -- Everything else, including GET: the MCP endpoint only speaks+    -- JSON-RPC over POST (this library does not offer server-initiated+    -- SSE streams, and revision 2026-07-28 requires 405 for GET).+    _ -> respond $ Wai.responseLBS+      status405+      [("Content-Type", "text/plain"), ("Allow", "POST, OPTIONS")]+      "Method Not Allowed" -          -- Unsupported methods-          _ -> respond $ Wai.responseLBS-            status405-            [("Content-Type", "text/plain"), ("Allow", "GET, POST, OPTIONS")]-            "Method Not Allowed"+-- | The parts of a JSON-RPC body that header validation needs.+data BodyPeek = BodyPeek+  { peekMethod      :: Maybe Text+  , peekMetaVersion :: Maybe Text+  , peekName        :: Maybe Text  -- ^ params.name or params.uri+  , peekId          :: RequestId+  } --- | True unless the request carries a *present but unsupported*--- MCP-Protocol-Version header. A missing header is treated as acceptable, since--- the spec allows the server to assume a default protocol version in that case.-versionHeaderSupported :: Wai.Request -> Bool-versionHeaderSupported req =-  case lookup "MCP-Protocol-Version" (Wai.requestHeaders req) of-    Nothing -> True-    Just hv -> TE.decodeUtf8 hv `elem` supportedVersions+-- | Whether the body declares a modern (2026-07-28+) request.+peekIsModern :: BodyPeek -> Bool+peekIsModern peek =+  peekMetaVersion peek /= Nothing || peekMethod peek == Just "server/discover" --- | Peek at a JSON-RPC message body to read its @method@ (if present).-extractMethod :: BSL.ByteString -> Maybe Text-extractMethod body = case decode body of-  Just (Object o) -> case KM.lookup "method" o of-    Just (String m) -> Just m-    _               -> Nothing-  _ -> Nothing+peekBody :: BSL.ByteString -> BodyPeek+peekBody body = case decode body of+  Just (Object o) -> BodyPeek+    { peekMethod = case KM.lookup "method" o of+        Just (String m) -> Just m+        _               -> Nothing+    , peekMetaVersion = metaProtocolVersion (KM.lookup "params" o)+    , peekName = case KM.lookup "params" o of+        Just (Object p) -> case (KM.lookup "name" p, KM.lookup "uri" p) of+          (Just (String n), _) -> Just n+          (_, Just (String u)) -> Just u+          _                    -> Nothing+        _ -> Nothing+    , peekId = case KM.lookup "id" o of+        Just v  -> either (const RequestIdNull) id (parseEither parseJSON v)+        Nothing -> RequestIdNull+    }+  _ -> BodyPeek Nothing Nothing Nothing RequestIdNull +-- | Validate the request-metadata headers against the body. 'Nothing' means+-- the request may proceed; @'Just' err@ is answered with 400 and the given+-- JSON-RPC error.+--+-- Modern requests (body declares a protocol revision in @_meta@) follow the+-- 2026-07-28 rules: the MCP-Protocol-Version header must match the body's+-- declared revision, Mcp-Method must match the body method, and Mcp-Name+-- must match @params.name@/@params.uri@ for the three named methods+-- (@HeaderMismatch@, -32020); an undeclared revision yields+-- @UnsupportedProtocolVersionError@ (-32022).+--+-- Legacy requests keep the relaxed pre-2026 rules: only a+-- present-but-unsupported MCP-Protocol-Version header is rejected, and+-- @initialize@ is exempt entirely.+validateRequestHeaders :: Wai.Request -> BodyPeek -> Maybe JsonRpcError+validateRequestHeaders req peek = case peekMetaVersion peek of+  Just v+    | v `notElem` modernVersions -> Just (unsupportedVersionError v)+    | headerText "MCP-Protocol-Version" /= Just v ->+        Just $ headerMismatch $ "MCP-Protocol-Version header "+          <> maybe "is missing" (\h -> "value '" <> h <> "'") (headerText "MCP-Protocol-Version")+          <> " and does not match body value '" <> v <> "'"+    | headerText "Mcp-Method" /= peekMethod peek ->+        Just $ headerMismatch $ "Mcp-Method header "+          <> maybe "is missing" (\h -> "value '" <> h <> "'") (headerText "Mcp-Method")+          <> " and does not match body method"+    | needsName+    , (decodeSentinel <$> headerText "Mcp-Name") /= peekName peek ->+        Just $ headerMismatch $ "Mcp-Name header "+          <> maybe "is missing" (\h -> "value '" <> h <> "'") (headerText "Mcp-Name")+          <> " and does not match the body name/uri"+    | otherwise -> Nothing+  Nothing+    | peekMethod peek /= Just "initialize"+    , Just hv <- headerText "MCP-Protocol-Version"+    , hv `notElem` supportedVersions ++ modernVersions ->+        Just $ JsonRpcError+          { errorCode = -32600+          , errorMessage = "Unsupported protocol version. Supported versions: "+              <> T.intercalate ", " allVersions+          , errorData = Nothing+          }+    | otherwise -> Nothing+  where+    headerText name =+      TE.decodeUtf8With lenientDecode <$> lookup name (Wai.requestHeaders req)+    needsName = peekMethod peek `elem` map Just ["tools/call", "resources/read", "prompts/get"]+    headerMismatch msg = JsonRpcError+      { errorCode = -32020+      , errorMessage = "Header mismatch: " <> msg+      , errorData = Nothing+      }++-- | Decode the @=?base64?...?=@ sentinel encoding the 2026-07-28 transport+-- uses for header values that cannot be represented in plain ASCII. Values+-- not wrapped in the sentinel (or with invalid base64) pass through as-is.+decodeSentinel :: Text -> Text+decodeSentinel t+  | Just inner <- T.stripPrefix "=?base64?" t >>= T.stripSuffix "?=" =+      case B64.decode (TE.encodeUtf8 inner) of+        Right bs -> TE.decodeUtf8With lenientDecode bs+        Left _   -> t+  | otherwise = t++-- | Serve a subscriptions/listen request (2026-07-28): a long-lived SSE+-- stream that first acknowledges the subscription and then delivers exactly+-- the notification types the client opted into, tagged with the+-- subscription id. Closing the stream is the client's cancellation signal;+-- periodic SSE comments keep intermediaries from timing the stream out.+handleSubscriptionsListen :: HttpConfig -> NotificationSource -> BSL.ByteString -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived+handleSubscriptionsListen config src body respond =+  case eitherDecode body :: Either String JsonRpcRequest of+    Left err -> do+      hPutStrLn stderr $ "subscriptions/listen parse error: " ++ err+      respond $ Wai.responseLBS+        status400+        [("Content-Type", "application/json")]+        (encode $ makeErrorResponse RequestIdNull $ JsonRpcError+          { errorCode = -32600+          , errorMessage = "Invalid Request"+          , errorData = Nothing+          })+    Right req -> do+      let subId = requestId req+          notifFilter = parseNotificationFilter (requestParams req)+      logVerbose config $ "Opening subscription stream " ++ show subId+      respond $ Wai.responseStream+        status200+        [ ("Content-Type", "text/event-stream")+        , ("Cache-Control", "no-cache")+        , ("X-Accel-Buffering", "no")+        ]+        $ \write flush -> do+          chan <- atomically $ subscribeEvents src+          let sendJson v = do+                write $ B.lazyByteString $ "data: " <> encode v <> "\n\n"+                flush+          sendJson $ toJSON $ acknowledgedNotification subId notifFilter+          forever $ do+            keepAlive <- registerDelay 25000000  -- 25s+            next <- atomically $+              (Just <$> readTChan chan)+                `orElse` (readTVar keepAlive >>= check >> pure Nothing)+            case next of+              Just event+                | filterAccepts notifFilter event ->+                    sendJson $ toJSON $ eventNotification subId event+                | otherwise -> pure ()+              Nothing -> do+                write $ B.byteString ": keep-alive\n\n"+                flush+ -- | Handle JSON-RPC request from HTTP body-handleJsonRpcRequest :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> ClientContext -> BSL.ByteString -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived-handleJsonRpcRequest config serverInfo handlers ctx body respond = do+handleJsonRpcRequest :: HttpConfig -> McpServerInfo -> McpServerHandlers -> ClientContext -> Bool -> BSL.ByteString -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived+handleJsonRpcRequest config serverInfo handlers ctx modern body respond = do   case eitherDecode body of     Left err -> do       hPutStrLn stderr $ "JSON parse error: " ++ err       respond $ Wai.responseLBS         status400         [("Content-Type", "application/json")]-        (encode $ object ["error" .= ("Invalid JSON" :: Text)])+        (encode $ makeErrorResponse RequestIdNull $ JsonRpcError+          { errorCode = -32700+          , errorMessage = "Parse error"+          , errorData = Nothing+          }) -    Right jsonValue -> handleSingleJsonRpc config serverInfo handlers ctx jsonValue respond+    Right jsonValue -> handleSingleJsonRpc config serverInfo handlers ctx modern jsonValue respond  -- | Handle a single JSON-RPC message-handleSingleJsonRpc :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> ClientContext -> Value -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived-handleSingleJsonRpc config serverInfo handlers ctx jsonValue respond = do+handleSingleJsonRpc :: HttpConfig -> McpServerInfo -> McpServerHandlers -> ClientContext -> Bool -> Value -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived+handleSingleJsonRpc config serverInfo handlers ctx modern jsonValue respond = do   case parseJsonRpcMessage jsonValue of     Left err -> do       hPutStrLn stderr $ "JSON-RPC parse error: " ++ err       respond $ Wai.responseLBS         status400         [("Content-Type", "application/json")]-        (encode $ object ["error" .= ("Invalid JSON-RPC" :: Text)])+        (encode $ makeErrorResponse RequestIdNull $ JsonRpcError+          { errorCode = -32600+          , errorMessage = "Invalid Request"+          , errorData = Nothing+          })      Right message -> do       logVerbose config $ "Processing HTTP message: " ++ show (getMessageSummary message)-      maybeResponse <- handleMcpMessage serverInfo handlers ctx message+      let notifSupport = NotificationSupport+            { supportsLegacyPush = False  -- no legacy HTTP delivery channel+            , supportsListen = httpHasNotifications config+            }+      maybeResponse <- handleMcpMessage serverInfo (httpCacheHints config) notifSupport handlers ctx message        case maybeResponse of         Just responseMsg -> do           let responseJson = encode $ encodeJsonRpcMessage responseMsg           logVerbose config $ "Sending HTTP response for: " ++ show (getMessageSummary message)           respond $ Wai.responseLBS-            status200-            [("Content-Type", "application/json"), ("Access-Control-Allow-Origin", "*")]+            (statusForResponse modern responseMsg)+            [("Content-Type", "application/json")]             responseJson          Nothing -> do           logVerbose config $ "No response needed for: " ++ show (getMessageSummary message)-          -- For notifications, return 200 with empty JSON object (per MCP spec)-          respond $ Wai.responseLBS-            status200-            [("Content-Type", "application/json"), ("Access-Control-Allow-Origin", "*")]-            "{}"+          -- Accepted notification: 202 with no body, per the Streamable HTTP spec+          respond $ Wai.responseLBS status202 [] ""++-- | The HTTP status for a JSON-RPC response. Modern (2026-07-28) requests+-- map protocol-level failures onto HTTP statuses — 404 for an unknown RPC+-- method, 400 for an unsupported protocol version — so era-probing clients+-- can distinguish them; legacy requests always get 200 as before.+statusForResponse :: Bool -> JsonRpcMessage -> Status+statusForResponse True (JsonRpcMessageResponse r) = case responseError r of+  Just e | errorCode e == -32601 -> status404+         | errorCode e == -32022 -> status400+  _ -> status200+statusForResponse _ _ = status200
src/MCP/Server/Transport/Stdio.hs view
@@ -4,51 +4,214 @@ module MCP.Server.Transport.Stdio   ( -- * STDIO Transport     transportRunStdio+  , transportRunStdioWithConfig+  , StdioConfig(..)+  , defaultStdioConfig   ) where -import           Control.Monad          (when)-import           Control.Monad.IO.Class (MonadIO, liftIO)+import           Control.Concurrent     (ThreadId, forkIO, killThread)+import           Control.Concurrent.MVar (modifyMVar_, newMVar, readMVar,+                                          withMVar)+import           Control.Concurrent.STM (atomically, readTChan)+import           Control.Monad          (forever, unless, when) import           Data.Aeson+import qualified Data.Aeson.KeyMap      as KM+import           Data.Aeson.Types       (parseEither) import qualified Data.ByteString.Lazy   as BSL+import           Data.IORef             (newIORef, readIORef, writeIORef) import qualified Data.Text              as T import qualified Data.Text.Encoding     as TE import qualified Data.Text.IO           as TIO-import           System.IO              (hFlush, stderr, stdout)+import           System.IO              (hFlush, hIsEOF, hSetEncoding, stderr,+                                         stdin, stdout, utf8)  import           MCP.Server.Handlers import           MCP.Server.JsonRpc+import           MCP.Server.Notifications+import           MCP.Server.Protocol    (modernVersions) import           MCP.Server.Types +-- | STDIO transport configuration+data StdioConfig = StdioConfig+  { stdioVerbose :: Bool+    -- ^ When 'True', raw request bodies are logged to stderr. The default+    -- ('False') logs only message summaries (method and id): tool arguments+    -- may carry sensitive data that does not belong in logs.+  , stdioCacheHints :: CacheHints+    -- ^ Cacheability hints stamped onto modern (2026-07-28+) list/read+    -- results.+  , stdioNotifications :: Maybe NotificationSource+    -- ^ When configured, change notifications are delivered: modern clients+    -- via @subscriptions\/listen@, legacy clients as spontaneous+    -- notifications after @initialize@ — and the corresponding+    -- @listChanged@\/@subscribe@ capabilities are advertised.+  } --- | Transport-specific implementation for STDIO-import           System.IO              (hSetEncoding, utf8)+-- | Default STDIO configuration: summaries only, no raw bodies, no caching,+-- no notifications.+defaultStdioConfig :: StdioConfig+defaultStdioConfig = StdioConfig+  { stdioVerbose = False+  , stdioCacheHints = defaultCacheHints+  , stdioNotifications = Nothing+  } -transportRunStdio :: (MonadIO m) => McpServerInfo -> McpServerHandlers m -> m ()-transportRunStdio serverInfo handlers = do+-- | Run the STDIO transport with the default configuration.+transportRunStdio :: McpServerInfo -> McpServerHandlers -> IO ()+transportRunStdio = transportRunStdioWithConfig defaultStdioConfig++-- | Run the STDIO transport with the given configuration.+transportRunStdioWithConfig :: StdioConfig -> McpServerInfo -> McpServerHandlers -> IO ()+transportRunStdioWithConfig config serverInfo handlers = do   -- Ensure UTF-8 encoding for all handles-  liftIO $ do-    hSetEncoding stderr utf8-    hSetEncoding stdout utf8-  loop-  where+  hSetEncoding stderr utf8+  hSetEncoding stdout utf8++  -- Subscription threads and the main loop share stdout: one line at a time.+  writeLock <- newMVar ()+  -- Active subscriptions_listen streams, by their request id+  subsVar <- newMVar ([] :: [(RequestId, ThreadId)])+  -- Whether a legacy client has completed initialize (gates legacy pushes)+  legacyReady <- newIORef False++  let logLine = TIO.hPutStrLn stderr+      logVerbose msg = when (stdioVerbose config) $ logLine msg++      sendRaw bytes = withMVar writeLock $ \_ -> do+        TIO.putStrLn $ TE.decodeUtf8 $ BSL.toStrict bytes+        hFlush stdout+      sendMessage msg = sendRaw $ encode $ encodeJsonRpcMessage msg+      sendResponse resp = sendRaw $ encode $ toJSON (resp :: JsonRpcResponse)+      sendNotification notif = sendRaw $ encode $ toJSON (notif :: JsonRpcNotification)++      notifSupport = case stdioNotifications config of+        Nothing -> noNotificationSupport+        Just _  -> NotificationSupport { supportsLegacyPush = True, supportsListen = True }++  -- Legacy delivery: push untagged notifications to a client that completed+  -- the initialize handshake (which is when the capability was advertised).+  case stdioNotifications config of+    Nothing  -> pure ()+    Just src -> do+      chan <- atomically $ subscribeEvents src+      _ <- forkIO $ forever $ do+        event <- atomically $ readTChan chan+        ready <- readIORef legacyReady+        when ready $ sendNotification $ legacyEventNotification event+      pure ()++  let+    -- Open a subscriptions/listen stream: acknowledge, then deliver+    -- matching events tagged with the subscription id until cancelled.+    -- A listen reusing an already-open subscription id replaces the old+    -- stream (otherwise the shadowed writer would leak until EOF).+    openSubscription src req = do+      let subId = requestId req+          notifFilter = parseNotificationFilter (requestParams req)+      _ <- cancelSubscription subId+      chan <- atomically $ subscribeEvents src+      sendNotification $ acknowledgedNotification subId notifFilter+      tid <- forkIO $ forever $ do+        event <- atomically $ readTChan chan+        when (filterAccepts notifFilter event) $+          sendNotification $ eventNotification subId event+      modifyMVar_ subsVar (pure . ((subId, tid) :))+      logLine $ "Opened subscription " <> T.pack (show subId)++    cancelSubscription cancelledId = do+      subs <- readMVar subsVar+      case lookup cancelledId subs of+        Nothing  -> pure False+        Just tid -> do+          killThread tid+          modifyMVar_ subsVar (pure . filter ((/= cancelledId) . fst))+          logLine $ "Cancelled subscription " <> T.pack (show cancelledId)+          pure True++    -- Server-side teardown at EOF: graceful closure for every open stream+    closeAllSubscriptions = do+      subs <- readMVar subsVar+      mapM_ (\(subId, tid) -> do+                killThread tid+                sendResponse $ closureResponse serverInfo subId)+            subs++    handleParsed message = case message of+      -- subscriptions/listen is transport-level: the stream outlives the+      -- request. Only intercept when a source is configured and the+      -- declared revision (if any) is one we implement — otherwise fall+      -- through for the ordinary -32601 / -32022 answer.+      JsonRpcMessageRequest req+        | requestMethod req == "subscriptions/listen"+        , Just src <- stdioNotifications config+        , maybe True (`elem` modernVersions) (metaProtocolVersion (requestParams req))+        -> openSubscription src req++      -- notifications/cancelled referencing an open subscription tears it+      -- down (no response, per the cancellation rules)+      JsonRpcMessageNotification notif+        | notificationMethod notif == "notifications/cancelled"+        , Just cancelledId <- cancelledRequestId (notificationParams notif)+        -> do+          wasSub <- cancelSubscription cancelledId+          unless wasSub $+            logLine $ "Ignoring cancellation for unknown request " <> T.pack (show cancelledId)++      _ -> do+        -- The client's initialized notification is the legacy ready signal:+        -- the lifecycle forbids server notifications before it arrives, and+        -- it only arrives after the client accepted a successful handshake+        -- response (so failed initializes never enable pushes).+        case message of+          JsonRpcMessageNotification n+            | notificationMethod n == "notifications/initialized" ->+                writeIORef legacyReady True+          _ -> pure ()+        response <- handleMcpMessage serverInfo (stdioCacheHints config) notifSupport handlers anonymousContext message+        case response of+          Just responseMsg -> do+            logLine $ "Sending response for: " <> T.pack (show (getMessageSummary message))+            sendMessage responseMsg+          Nothing ->+            logLine $ "No response needed for: " <> T.pack (show (getMessageSummary message))+     loop = do-      input <- liftIO TIO.getLine-      when (not $ T.null $ T.strip input) $ do-        -- Use TIO.hPutStrLn for UTF-8 output-        liftIO $ TIO.hPutStrLn stderr $ "Received request: " <> input-        case eitherDecode (BSL.fromStrict $ TE.encodeUtf8 input) of-          Left err -> liftIO $ TIO.hPutStrLn stderr $ "Parse error: " <> T.pack err-          Right jsonValue -> do-            case parseJsonRpcMessage jsonValue of-              Left err -> liftIO $ TIO.hPutStrLn stderr $ "JSON-RPC parse error: " <> T.pack err-              Right message -> do-                liftIO $ TIO.hPutStrLn stderr $ "Processing message: " <> T.pack (show (getMessageSummary message))-                response <- handleMcpMessage serverInfo handlers (ClientContext Nothing Nothing) message-                case response of-                  Just responseMsg -> do-                    liftIO $ TIO.hPutStrLn stderr $ "Sending response for: " <> T.pack (show (getMessageSummary message))-                    let responseText = TE.decodeUtf8 $ BSL.toStrict $ encode $ encodeJsonRpcMessage responseMsg-                    liftIO $ TIO.putStrLn responseText-                    liftIO $ hFlush stdout-                  Nothing -> liftIO $ TIO.hPutStrLn stderr $ "No response needed for: " <> T.pack (show (getMessageSummary message))-        loop+      eof <- hIsEOF stdin+      if eof+        then do+          closeAllSubscriptions+          logLine "stdin closed - shutting down"+        else do+          input <- TIO.getLine+          unless (T.null $ T.strip input) $ do+            logVerbose $ "Received request: " <> input+            case eitherDecode (BSL.fromStrict $ TE.encodeUtf8 input) of+              Left err -> do+                logLine $ "Parse error: " <> T.pack err+                sendResponse $ makeErrorResponse RequestIdNull $ JsonRpcError+                  { errorCode = -32700+                  , errorMessage = "Parse error"+                  , errorData = Nothing+                  }+              Right jsonValue ->+                case parseJsonRpcMessage jsonValue of+                  Left err -> do+                    logLine $ "JSON-RPC parse error: " <> T.pack err+                    sendResponse $ makeErrorResponse RequestIdNull $ JsonRpcError+                      { errorCode = -32600+                      , errorMessage = "Invalid Request"+                      , errorData = Nothing+                      }+                  Right message -> do+                    logLine $ "Processing message: " <> T.pack (show (getMessageSummary message))+                    handleParsed message+          loop++  loop++-- | The request id referenced by a notifications/cancelled notification.+cancelledRequestId :: Maybe Value -> Maybe RequestId+cancelledRequestId params = do+  Object o <- params+  v <- KM.lookup "requestId" o+  either (const Nothing) Just (parseEither parseJSON v)
src/MCP/Server/Types.hs view
@@ -5,41 +5,70 @@   ( -- * Content Types     Content(..)   , ContentImageData(..)-  , ContentResourceData(..)+  , ContentAudioData(..)   , ResourceContent(..) -    -- * URI Utilities  +    -- * Handler Result Types+  , ToolResult(..)+  , toolResult+  , toolError+  , ToToolResult(..)+  , PromptResult(..)+  , ToPromptResult(..)+  , PromptMessage(..)+  , MessageRole(..)++    -- * URI Utilities   , parseURI   , URI      -- * Error Types   , Error(..) +    -- * Schema Types+  , Schema(..)+  , SchemaType(..)+  , schema+  , describedSchema+     -- * Definition Types   , PromptDefinition(..)   , ResourceDefinition(..)+  , ResourceTemplateDefinition(..)   , ToolDefinition(..)   , ArgumentDefinition(..)-  , InputSchemaDefinition(..)-  , InputSchemaDefinitionProperty(..) +    -- * Completion Types+  , CompletionRef(..)+  , CompletionResult(..)+  , completionResult+     -- * Server Types   , McpServerInfo(..)   , McpServerHandlers(..)+  , noHandlers   , ClientContext(..)+  , anonymousContext+  , CacheHints(..)+  , defaultCacheHints+  , NotificationSupport(..)+  , noNotificationSupport   , ServerCapabilities(..)   , PromptCapabilities(..)   , ResourceCapabilities(..)   , ToolCapabilities(..)+  , CompletionCapabilities(..)   , LoggingCapabilities(..) -    -- * Request/Response Types+    -- * Handler Types   , PromptListHandler   , PromptGetHandler   , ResourceListHandler   , ResourceReadHandler+  , ResourceTemplateListHandler   , ToolListHandler   , ToolCallHandler+  , CompletionHandler      -- * Basic Types   , PromptName@@ -50,8 +79,10 @@  import           Data.Aeson import           Data.Aeson.Key   (fromText)-import           Data.Aeson.Types (Parser)-import           Data.Maybe       (catMaybes)+import qualified Data.Aeson.KeyMap as KM+import           Data.Aeson.Types (Pair, Parser)+import           Data.Map         (Map)+import           Data.Maybe       (catMaybes, listToMaybe) import           Data.Text        (Text) import qualified Data.Text        as T import           GHC.Generics     (Generic)@@ -66,7 +97,11 @@ data Content   = ContentText Text   | ContentImage ContentImageData-  | ContentResource ContentResourceData+  | ContentAudio ContentAudioData+  | ContentEmbeddedResource ResourceContent+    -- ^ A resource embedded into the result, carrying its full contents+  | ContentResourceLink ResourceDefinition+    -- ^ A reference to a resource the client can read separately   deriving (Show, Eq, Generic)  instance ToJSON Content where@@ -79,13 +114,19 @@     , "data" .= contentImageData img     , "mimeType" .= contentImageMimeType img     ]-  toJSON (ContentResource res) = object+  toJSON (ContentAudio audio) = object+    [ "type" .= ("audio" :: Text)+    , "data" .= contentAudioData audio+    , "mimeType" .= contentAudioMimeType audio+    ]+  toJSON (ContentEmbeddedResource res) = object     [ "type" .= ("resource" :: Text)-    , "resource" .= object-        [ "uri" .= contentResourceUri res-        , "mimeType" .= contentResourceMimeType res-        ]+    , "resource" .= res     ]+  toJSON (ContentResourceLink def) =+    case toJSON def of+      Object o -> Object (KM.insert "type" (String "resource_link") o)+      other    -> other  instance FromJSON Content where   parseJSON = withObject "Content" $ \o -> do@@ -96,21 +137,22 @@         imgData <- o .: "data"         mimeType <- o .: "mimeType"         return $ ContentImage $ ContentImageData imgData mimeType-      "resource" -> do-        res <- o .: "resource"-        uri <- res .: "uri"-        mimeType <- res .:? "mimeType"-        return $ ContentResource $ ContentResourceData uri mimeType+      "audio" -> do+        audioData <- o .: "data"+        mimeType <- o .: "mimeType"+        return $ ContentAudio $ ContentAudioData audioData mimeType+      "resource" -> ContentEmbeddedResource <$> o .: "resource"+      "resource_link" -> ContentResourceLink <$> parseJSON (Object o)       _ -> fail $ "Unknown content type: " ++ T.unpack contentType  data ContentImageData = ContentImageData-  { contentImageData     :: Text+  { contentImageData     :: Text  -- ^ base64-encoded image data   , contentImageMimeType :: Text   } deriving (Show, Eq, Generic) -data ContentResourceData = ContentResourceData-  { contentResourceUri      :: URI-  , contentResourceMimeType :: Maybe Text+data ContentAudioData = ContentAudioData+  { contentAudioData     :: Text  -- ^ base64-encoded audio data+  , contentAudioMimeType :: Text   } deriving (Show, Eq, Generic)  -- | Resource content compliant with MCP specification@@ -154,6 +196,115 @@           (Nothing, Just blob) -> return $ ResourceBlob uri mimeType blob           _ -> fail "ResourceContent must have either 'text' or 'blob' field" +-- | Message role for prompts+data MessageRole = RoleUser | RoleAssistant+  deriving (Show, Eq, Generic)++instance ToJSON MessageRole where+  toJSON RoleUser      = "user"+  toJSON RoleAssistant = "assistant"++-- | Prompt message+data PromptMessage = PromptMessage+  { promptMessageRole    :: MessageRole+  , promptMessageContent :: Content+  } deriving (Show, Eq, Generic)++instance ToJSON PromptMessage where+  toJSON msg = object+    [ "role" .= promptMessageRole msg+    , "content" .= promptMessageContent msg+    ]++-- | The full result of a tool call.+--+-- Tool /execution/ failures belong in the result with 'toolResultIsError'+-- set (so the model can see what went wrong and react); JSON-RPC errors are+-- reserved for protocol-level failures such as unknown tools or malformed+-- arguments.+data ToolResult = ToolResult+  { toolResultContent    :: [Content]+  , toolResultStructured :: Maybe Value  -- ^ structuredContent (2025-06-18+)+  , toolResultIsError    :: Bool+  , toolResultMeta       :: Maybe Value+  } deriving (Show, Eq, Generic)++-- | A successful tool result with the given content blocks.+toolResult :: [Content] -> ToolResult+toolResult content = ToolResult+  { toolResultContent = content+  , toolResultStructured = Nothing+  , toolResultIsError = False+  , toolResultMeta = Nothing+  }++-- | A failed tool execution: the message is reported to the model with+-- @isError: true@ rather than as a JSON-RPC error.+toolError :: Text -> ToolResult+toolError msg = (toolResult [ContentText msg]) { toolResultIsError = True }++-- | Types a tool handler may return. Returning plain 'Content' (or 'Text')+-- keeps simple handlers simple; return a full 'ToolResult' for multiple+-- content blocks, structured content, or execution errors.+class ToToolResult a where+  toToolResult :: a -> ToolResult++instance ToToolResult ToolResult where+  toToolResult = id++instance ToToolResult Content where+  toToolResult c = toolResult [c]++-- | Concatenates content; a merged result is an error if any element is+-- ('toolResultIsError' is OR-ed), and the first present 'structuredContent'+-- and @_meta@ are kept.+instance ToToolResult a => ToToolResult [a] where+  toToolResult xs = ToolResult+    { toolResultContent = concatMap toolResultContent rs+    , toolResultStructured = firstJust (map toolResultStructured rs)+    , toolResultIsError = any toolResultIsError rs+    , toolResultMeta = firstJust (map toolResultMeta rs)+    }+    where rs = map toToolResult xs++instance ToToolResult Text where+  toToolResult = toToolResult . ContentText++firstJust :: [Maybe a] -> Maybe a+firstJust = listToMaybe . catMaybes++-- | The full result of a prompts/get request: an optional description and+-- a conversation of one or more messages.+data PromptResult = PromptResult+  { promptResultDescription :: Maybe Text+  , promptResultMessages    :: [PromptMessage]+  } deriving (Show, Eq, Generic)++-- | Types a prompt handler may return. Plain 'Content' (or 'Text') becomes a+-- single user message; return 'PromptResult' or @['PromptMessage']@ for+-- multi-message conversations or assistant roles.+class ToPromptResult a where+  toPromptResult :: a -> PromptResult++instance ToPromptResult PromptResult where+  toPromptResult = id++instance ToPromptResult PromptMessage where+  toPromptResult m = PromptResult Nothing [m]++-- | Concatenates messages and keeps the first present description.+instance ToPromptResult a => ToPromptResult [a] where+  toPromptResult xs = PromptResult+    (firstJust (map promptResultDescription rs))+    (concatMap promptResultMessages rs)+    where rs = map toPromptResult xs++instance ToPromptResult Content where+  toPromptResult c = toPromptResult (PromptMessage RoleUser c)++instance ToPromptResult Text where+  toPromptResult = toPromptResult . ContentText+ -- | MCP protocol errors data Error   = InvalidPromptName Text@@ -192,6 +343,53 @@       errorMessage (MethodNotFound msg) = "Method not found: " <> msg       errorMessage (InvalidParams msg) = "Invalid parameters: " <> msg +-- | A JSON Schema fragment: a shape plus an optional description.+data Schema = Schema+  { schemaDescription :: Maybe Text+  , schemaShape       :: SchemaType+  } deriving (Show, Eq, Generic)++-- | The shape of a JSON Schema fragment.+data SchemaType+  = SchemaString (Maybe [Text])+    -- ^ A string, optionally restricted to an enum of allowed values+  | SchemaInteger+  | SchemaNumber+  | SchemaBoolean+  | SchemaArray Schema+  | SchemaObject [(Text, Schema)] [Text]+    -- ^ Properties and the names of the required ones+  deriving (Show, Eq, Generic)++-- | A schema with no description.+schema :: SchemaType -> Schema+schema = Schema Nothing++-- | A schema with a description.+describedSchema :: Text -> SchemaType -> Schema+describedSchema desc = Schema (Just desc)++instance ToJSON Schema where+  toJSON (Schema desc shape) = object $+    typeFields shape ++ maybe [] (\d -> ["description" .= d]) desc+    where+      typeFields :: SchemaType -> [Pair]+      typeFields (SchemaString enumVals) =+        [ "type" .= ("string" :: Text) ]+        ++ maybe [] (\vs -> ["enum" .= vs]) enumVals+      typeFields SchemaInteger = [ "type" .= ("integer" :: Text) ]+      typeFields SchemaNumber  = [ "type" .= ("number" :: Text) ]+      typeFields SchemaBoolean = [ "type" .= ("boolean" :: Text) ]+      typeFields (SchemaArray items) =+        [ "type" .= ("array" :: Text)+        , "items" .= items+        ]+      typeFields (SchemaObject props req) =+        [ "type" .= ("object" :: Text)+        , "properties" .= object (map (\(k, v) -> fromText k .= v) props)+        , "required" .= req+        ]+ -- | Prompt definition (2025-06-18 enhanced) data PromptDefinition = PromptDefinition   { promptDefinitionName        :: Text@@ -225,12 +423,61 @@     maybe [] (\m -> ["mimeType" .= m]) (resourceDefinitionMimeType def) ++     maybe [] (\t -> ["title" .= t]) (resourceDefinitionTitle def) +instance FromJSON ResourceDefinition where+  parseJSON = withObject "ResourceDefinition" $ \o -> ResourceDefinition+    <$> o .: "uri"+    <*> o .: "name"+    <*> o .:? "description"+    <*> o .:? "mimeType"+    <*> o .:? "title"++-- | Resource template definition: a parameterized resource identified by an+-- RFC 6570 URI template.+data ResourceTemplateDefinition = ResourceTemplateDefinition+  { resourceTemplateURITemplate :: Text+  , resourceTemplateName        :: Text+  , resourceTemplateDescription :: Maybe Text+  , resourceTemplateMimeType    :: Maybe Text+  , resourceTemplateTitle       :: Maybe Text+  } deriving (Show, Eq, Generic)++instance ToJSON ResourceTemplateDefinition where+  toJSON def = object $+    [ "uriTemplate" .= resourceTemplateURITemplate def+    , "name" .= resourceTemplateName def+    ] +++    maybe [] (\d -> ["description" .= d]) (resourceTemplateDescription def) +++    maybe [] (\m -> ["mimeType" .= m]) (resourceTemplateMimeType def) +++    maybe [] (\t -> ["title" .= t]) (resourceTemplateTitle def)++-- | What a completion request is completing an argument for.+data CompletionRef+  = CompletionRefPrompt Text    -- ^ @ref/prompt@: a prompt, by name+  | CompletionRefResource Text  -- ^ @ref/resource@: a resource URI or URI template+  deriving (Show, Eq, Generic)++-- | Completion suggestions for an argument value.+data CompletionResult = CompletionResult+  { completionValues  :: [Text]      -- ^ Suggestions ranked by relevance (max 100 are sent)+  , completionTotal   :: Maybe Int   -- ^ Optional total number of matches+  , completionHasMore :: Maybe Bool  -- ^ Whether more results exist beyond 'completionValues'+  } deriving (Show, Eq, Generic)++-- | A completion result with just the given suggestions.+completionResult :: [Text] -> CompletionResult+completionResult vals = CompletionResult+  { completionValues = vals+  , completionTotal = Nothing+  , completionHasMore = Nothing+  }+ -- | Tool definition (2025-06-18 enhanced) data ToolDefinition = ToolDefinition-  { toolDefinitionName        :: Text-  , toolDefinitionDescription :: Text-  , toolDefinitionInputSchema :: InputSchemaDefinition-  , toolDefinitionTitle       :: Maybe Text  -- New title field for human-friendly display+  { toolDefinitionName         :: Text+  , toolDefinitionDescription  :: Text+  , toolDefinitionInputSchema  :: Schema+  , toolDefinitionOutputSchema :: Maybe Schema+  , toolDefinitionTitle        :: Maybe Text  -- New title field for human-friendly display   } deriving (Show, Eq, Generic)  instance ToJSON ToolDefinition where@@ -238,7 +485,8 @@     [ "name" .= toolDefinitionName def     , "description" .= toolDefinitionDescription def     , "inputSchema" .= toolDefinitionInputSchema def-    ] ++ maybe [] (\t -> ["title" .= t]) (toolDefinitionTitle def)+    ] ++ maybe [] (\s -> ["outputSchema" .= s]) (toolDefinitionOutputSchema def)+      ++ maybe [] (\t -> ["title" .= t]) (toolDefinitionTitle def)  -- | Argument definition for prompts data ArgumentDefinition = ArgumentDefinition@@ -254,30 +502,6 @@     , "required" .= argumentDefinitionRequired def     ] --- | Input schema definition for tools-data InputSchemaDefinition = InputSchemaDefinitionObject-  { properties :: [(Text, InputSchemaDefinitionProperty)]-  , required   :: [Text]-  } deriving (Show, Eq, Generic)--instance ToJSON InputSchemaDefinition where-  toJSON (InputSchemaDefinitionObject props req) = object-    [ "type" .= ("object" :: Text)-    , "properties" .= object (map (\(k, v) -> fromText k .= v) props)-    , "required" .= req-    ]--data InputSchemaDefinitionProperty = InputSchemaDefinitionProperty-  { propertyType        :: Text-  , propertyDescription :: Text-  } deriving (Show, Eq, Generic)--instance ToJSON InputSchemaDefinitionProperty where-  toJSON prop = object-    [ "type" .= propertyType prop-    , "description" .= propertyDescription prop-    ]- -- | Server information data McpServerInfo = McpServerInfo   { serverName         :: Text@@ -315,6 +539,13 @@     [ fmap ("listChanged" .=) (toolListChanged caps)     ] +data CompletionCapabilities = CompletionCapabilities+  { -- No sub-capabilities defined+  } deriving (Show, Eq, Generic)++instance ToJSON CompletionCapabilities where+  toJSON _ = object []+ data LoggingCapabilities = LoggingCapabilities   { -- No specific sub-capabilities for logging yet   } deriving (Show, Eq, Generic)@@ -324,10 +555,11 @@  -- | Server capabilities data ServerCapabilities = ServerCapabilities-  { capabilityPrompts   :: Maybe PromptCapabilities-  , capabilityResources :: Maybe ResourceCapabilities-  , capabilityTools     :: Maybe ToolCapabilities-  , capabilityLogging   :: Maybe LoggingCapabilities+  { capabilityPrompts     :: Maybe PromptCapabilities+  , capabilityResources   :: Maybe ResourceCapabilities+  , capabilityTools       :: Maybe ToolCapabilities+  , capabilityCompletions :: Maybe CompletionCapabilities+  , capabilityLogging     :: Maybe LoggingCapabilities   } deriving (Show, Eq, Generic)  instance ToJSON ServerCapabilities where@@ -335,6 +567,7 @@     [ fmap ("prompts" .=) (capabilityPrompts caps)     , fmap ("resources" .=) (capabilityResources caps)     , fmap ("tools" .=) (capabilityTools caps)+    , fmap ("completions" .=) (capabilityCompletions caps)     , fmap ("logging" .=) (capabilityLogging caps)     ] @@ -347,22 +580,108 @@                                     --   the transport's authorization callback                                     --   (e.g. a role). 'Nothing' when                                     --   authentication is disabled.+  , clientProtocolVersion :: Maybe Text+      -- ^ The protocol revision the request declared in its @_meta@+      --   (modern, 2026-07-28+ clients). 'Nothing' for legacy clients that+      --   negotiated via @initialize@.+  , clientInfo :: Maybe Value+      -- ^ The client's self-reported identity from request @_meta@+      --   (modern clients), for display/logging only.+  , clientCapabilities :: Maybe Value+      -- ^ The client's declared capabilities from request @_meta@+      --   (modern clients).   } deriving (Show, Eq) +-- | A context carrying no transport- or request-level information: what+-- handlers see for legacy stdio requests.+anonymousContext :: ClientContext+anonymousContext = ClientContext+  { clientToken = Nothing+  , clientPrincipal = Nothing+  , clientProtocolVersion = Nothing+  , clientInfo = Nothing+  , clientCapabilities = Nothing+  }++-- | Cacheability hints stamped onto modern (2026-07-28+) list/read results,+-- which require @ttlMs@ and @cacheScope@ fields.+data CacheHints = CacheHints+  { cacheTtlMs       :: Int   -- ^ Freshness hint in milliseconds.+  , cacheScopePublic :: Bool  -- ^ 'True' allows shared intermediaries to+                              --   cache the response (@\"public\"@);+                              --   'False' is @\"private\"@.+  } deriving (Show, Eq)++-- | Conservative defaults: no caching (@ttlMs = 0@), private scope.+defaultCacheHints :: CacheHints+defaultCacheHints = CacheHints+  { cacheTtlMs = 0+  , cacheScopePublic = False+  }++-- | What change-notification delivery the serving transport offers, which+-- decides the @listChanged@\/@subscribe@ capabilities each era advertises.+data NotificationSupport = NotificationSupport+  { supportsLegacyPush :: Bool+      -- ^ The transport can push unsolicited notifications to legacy+      --   (initialize-handshake) clients — true for stdio with a configured+      --   notification source; never for HTTP (this library dropped the+      --   deprecated GET SSE stream legacy delivery relied on).+  , supportsListen :: Bool+      -- ^ @subscriptions\/listen@ (2026-07-28) is served.+  } deriving (Show, Eq)++-- | No notification delivery at all: nothing extra is advertised.+noNotificationSupport :: NotificationSupport+noNotificationSupport = NotificationSupport+  { supportsLegacyPush = False+  , supportsListen = False+  }+ -- | Handler type definitions. Every handler receives the request's -- 'ClientContext' as its first argument.-type PromptListHandler m = ClientContext -> m [PromptDefinition]-type PromptGetHandler m = ClientContext -> PromptName -> [(ArgumentName, ArgumentValue)] -> m (Either Error Content)+--+-- Prompt arguments are string-valued per the MCP specification; tool+-- arguments are full JSON values.+type PromptListHandler = ClientContext -> IO [PromptDefinition]+type PromptGetHandler = ClientContext -> PromptName -> Map Text Text -> IO (Either Error PromptResult) -type ResourceListHandler m = ClientContext -> m [ResourceDefinition]-type ResourceReadHandler m = ClientContext -> URI -> m (Either Error ResourceContent)+type ResourceListHandler = ClientContext -> IO [ResourceDefinition]+type ResourceReadHandler = ClientContext -> URI -> IO (Either Error ResourceContent)+type ResourceTemplateListHandler = ClientContext -> IO [ResourceTemplateDefinition] -type ToolListHandler m = ClientContext -> m [ToolDefinition]-type ToolCallHandler m = ClientContext -> ToolName -> [(ArgumentName, ArgumentValue)] -> m (Either Error Content)+type ToolListHandler = ClientContext -> IO [ToolDefinition]+type ToolCallHandler = ClientContext -> ToolName -> Map Text Value -> IO (Either Error ToolResult) +-- | Completion handler: given what is being completed ('CompletionRef'), the+-- argument name, the partial value typed so far, and any already-resolved+-- sibling arguments, produce ranked suggestions.+type CompletionHandler = ClientContext -> CompletionRef -> ArgumentName -> Text -> Map Text Text -> IO (Either Error CompletionResult)+ -- | Server handlers-data McpServerHandlers m = McpServerHandlers-  { prompts   :: Maybe (PromptListHandler m, PromptGetHandler m)-  , resources :: Maybe (ResourceListHandler m, ResourceReadHandler m)-  , tools     :: Maybe (ToolListHandler m, ToolCallHandler m)+data McpServerHandlers = McpServerHandlers+  { prompts           :: Maybe (PromptListHandler, PromptGetHandler)+  , resources         :: Maybe (ResourceListHandler, ResourceReadHandler)+  , resourceTemplates :: Maybe ResourceTemplateListHandler+      -- ^ Parameterized resources (@resources\/templates\/list@). Template+      --   URIs are read through the 'ResourceReadHandler' in 'resources' —+      --   configure both, or template reads have nothing to serve them+      --   (a templates-only configuration still answers @resources\/list@+      --   with an empty list so the advertised capability stays honest).+  , tools             :: Maybe (ToolListHandler, ToolCallHandler)+  , completions       :: Maybe CompletionHandler+      -- ^ Argument autocompletion (@completion\/complete@) for prompts and+      --   resource templates.+  }++-- | Handlers for a server that supports nothing — record-update the features+-- you provide, so adding new handler slots to the library does not break+-- your construction.+noHandlers :: McpServerHandlers+noHandlers = McpServerHandlers+  { prompts = Nothing+  , resources = Nothing+  , resourceTemplates = Nothing+  , tools = Nothing+  , completions = Nothing   }
test/HspecMain.hs view
@@ -8,8 +8,13 @@ import qualified Spec.SchemaValidation import qualified Spec.AdvancedDerivation import qualified Spec.UnicodeHandling+import qualified Spec.GoldenWire+import qualified Spec.ModernEra import qualified Spec.ProtocolVersionNegotiation+import qualified Spec.Subscriptions+import qualified Spec.TemplatesCompletions import qualified Spec.ToolCallParsing+import qualified Spec.TypedArgs  main :: IO () main = hspec $ do@@ -19,5 +24,10 @@     Spec.SchemaValidation.spec     Spec.AdvancedDerivation.spec     Spec.UnicodeHandling.spec+    Spec.GoldenWire.spec+    Spec.ModernEra.spec     Spec.ProtocolVersionNegotiation.spec+    Spec.Subscriptions.spec+    Spec.TemplatesCompletions.spec     Spec.ToolCallParsing.spec+    Spec.TypedArgs.spec
test/Spec/AdvancedDerivation.hs view
@@ -9,20 +9,21 @@ import MCP.Server import MCP.Server.Derive import Test.Hspec+import TestHelpers import TestTypes import TestData  -- Generate advanced handlers for separate parameter types-testSeparateParamsToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)+testSeparateParamsToolHandlers :: (ToolListHandler, ToolCallHandler) testSeparateParamsToolHandlers = $(deriveToolHandler ''SeparateParamsTool 'handleSeparateParamsTool) -testRecursiveToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)+testRecursiveToolHandlers :: (ToolListHandler, ToolCallHandler) testRecursiveToolHandlers = $(deriveToolHandler ''RecursiveTool 'handleRecursiveTool) -testSeparateParamsToolHandlersWithDescriptions :: (ToolListHandler IO, ToolCallHandler IO)+testSeparateParamsToolHandlersWithDescriptions :: (ToolListHandler, ToolCallHandler) testSeparateParamsToolHandlersWithDescriptions = $(deriveToolHandlerWithDescription ''SeparateParamsTool 'handleSeparateParamsTool separateParamsDescriptions) -testRecursiveToolHandlersWithDescriptions :: (ToolListHandler IO, ToolCallHandler IO)+testRecursiveToolHandlersWithDescriptions :: (ToolListHandler, ToolCallHandler) testRecursiveToolHandlersWithDescriptions = $(deriveToolHandlerWithDescription ''RecursiveTool 'handleRecursiveTool separateParamsDescriptions)  -- Helper functions for advanced testing@@ -35,17 +36,13 @@  assertSchemaHasProperties :: [Text] -> ToolDefinition -> IO () assertSchemaHasProperties expectedProps toolDef = do-  case toolDefinitionInputSchema toolDef of-    InputSchemaDefinitionObject props _ ->-      let propNames = map fst props-      in all (`elem` propNames) expectedProps `shouldBe` True+  let propNames = map fst (schemaProps (toolDefinitionInputSchema toolDef))+  all (`elem` propNames) expectedProps `shouldBe` True -assertToolCallResult :: (ToolCallHandler IO) -> Text -> [(Text, Text)] -> Text -> IO ()+assertToolCallResult :: ToolCallHandler -> Text -> [(Text, Text)] -> Text -> IO () assertToolCallResult handler toolName args expectedContent = do-  result <- handler anonCtx toolName args-  case result of-    Right (ContentText content) -> content `shouldBe` expectedContent-    other -> expectationFailure $ "Expected ContentText but got: " ++ show other+  result <- handler anonCtx toolName (stringArgs args)+  result `shouldBeTextResult` expectedContent  assertToolHasDescription :: Text -> Text -> ToolDefinition -> IO () assertToolHasDescription toolName expectedDesc toolDef = do@@ -54,11 +51,9 @@  assertPropertyHasDescription :: Text -> Text -> ToolDefinition -> IO () assertPropertyHasDescription propName expectedDesc toolDef = do-  case toolDefinitionInputSchema toolDef of-    InputSchemaDefinitionObject props _ ->-      case lookup propName props of-        Just prop -> propertyDescription prop `shouldBe` expectedDesc-        Nothing -> expectationFailure $ "Property not found: " ++ T.unpack propName+  case lookupProp propName toolDef of+    Just prop -> schemaDescription prop `shouldBe` Just expectedDesc+    Nothing -> expectationFailure $ "Property not found: " ++ T.unpack propName  spec :: Spec spec = describe "Advanced Template Haskell Derivation" $ do
test/Spec/BasicDerivation.hs view
@@ -9,40 +9,26 @@ import MCP.Server import MCP.Server.Derive import Test.Hspec+import TestHelpers import TestTypes import TestData  -- Generate handlers using Template Haskell-testPromptHandlers :: (PromptListHandler IO, PromptGetHandler IO)+testPromptHandlers :: (PromptListHandler, PromptGetHandler) testPromptHandlers = $(derivePromptHandler ''TestPrompt 'handleTestPrompt) -testResourceHandlers :: (ResourceListHandler IO, ResourceReadHandler IO)+testResourceHandlers :: (ResourceListHandler, ResourceReadHandler) testResourceHandlers = $(deriveResourceHandler ''TestResource 'handleTestResource) -testToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)+testToolHandlers :: (ToolListHandler, ToolCallHandler) testToolHandlers = $(deriveToolHandler ''TestTool 'handleTestTool) --- Helper functions for common test patterns-shouldReturnContentText :: IO (Either Error Content) -> Text -> IO ()-shouldReturnContentText action expected = do-  result <- action-  case result of-    Right (ContentText content) -> content `shouldBe` expected-    other -> expectationFailure $ "Expected ContentText '" ++ T.unpack expected ++ "' but got: " ++ show other--shouldContainText :: IO (Either Error Content) -> Text -> IO ()-shouldContainText action expectedSubstring = do-  result <- action-  case result of-    Right (ContentText content) ->-      T.isInfixOf expectedSubstring content `shouldBe` True-    other -> expectationFailure $ "Expected ContentText containing '" ++ T.unpack expectedSubstring ++ "' but got: " ++ show other--testPromptCall :: PromptGetHandler IO -> Text -> [(Text, Text)] -> Text -> IO ()-testPromptCall handler name args expected =-  shouldReturnContentText (handler anonCtx name args) expected+testPromptCall :: PromptGetHandler -> Text -> [(Text, Text)] -> Text -> IO ()+testPromptCall handler name args expected = do+  result <- handler anonCtx name (promptArgsMap args)+  result `shouldBePromptText` expected -testResourceCall :: ResourceReadHandler IO -> String -> Text -> IO ()+testResourceCall :: ResourceReadHandler -> String -> Text -> IO () testResourceCall handler uriString expected = do   case parseURI uriString of     Just uri -> do@@ -53,9 +39,10 @@         Left err -> expectationFailure $ "Expected success but got error: " ++ show err     Nothing -> expectationFailure $ "Failed to parse URI: " ++ uriString -testToolCall :: ToolCallHandler IO -> Text -> [(Text, Text)] -> Text -> IO ()-testToolCall handler name args expected =-  shouldReturnContentText (handler anonCtx name args) expected+testToolCall :: ToolCallHandler -> Text -> [(Text, Text)] -> Text -> IO ()+testToolCall handler name args expected = do+  result <- handler anonCtx name (stringArgs args)+  result `shouldBeTextResult` expected  spec :: Spec spec = describe "Basic Template Haskell Derivation" $ do
+ test/Spec/GoldenWire.hs view
@@ -0,0 +1,172 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Golden wire-format fixtures: a canned request set is run through+-- 'handleMcpMessage' and each response is compared against a checked-in+-- fixture under @test/golden/@.+--+-- The legacy fixtures were generated from @main@ at v0.2.0 (commit+-- @7bd1bcc@), so they anchor the promise that dual-era support leaves+-- legacy responses unchanged. The modern fixtures pin the 2026-07-28+-- envelope introduced by this revision of the library.+--+-- Responses are compared as parsed 'Value's, not raw bytes: aeson's object+-- key order depends on the aeson\/hashable versions in the build plan, which+-- vary across the CI matrix, while 'Value' equality is stable and still+-- catches every added, removed, renamed or changed field.+--+-- To create a fixture for a new case, run the suite with @GOLDEN_ACCEPT=1@:+-- missing fixture files are then written from the current output. Existing+-- fixtures are never overwritten — delete one first to regenerate it, and+-- never regenerate the legacy fixtures from a branch that intends to keep+-- legacy output unchanged.+module Spec.GoldenWire (spec) where++import Control.Monad (forM_)+import Data.Aeson+import qualified Data.ByteString.Lazy as BSL+import qualified Data.Map as Map+import qualified Data.Text as T+import MCP.Server+import MCP.Server.Handlers (handleMcpMessage)+import MCP.Server.JsonRpc+import System.Directory (doesFileExist)+import System.Environment (lookupEnv)+import Test.Hspec++goldenServerInfo :: McpServerInfo+goldenServerInfo = McpServerInfo+  { serverName = "Golden Server"+  , serverVersion = "1.0.0"+  , serverInstructions = "Golden fixture server"+  }++-- Deterministic manual handlers (no Template Haskell): one prompt, one+-- resource, one happy tool, one failing tool.+goldenHandlers :: McpServerHandlers+goldenHandlers = noHandlers+  { prompts = Just (promptList, promptGet)+  , resources = Just (resourceList, resourceRead)+  , tools = Just (toolList, toolCall)+  }+  where+    promptList _ = pure+      [ PromptDefinition "greet" "Greet someone"+          [ArgumentDefinition "name" "Who to greet" True] Nothing+      ]+    promptGet _ name args = case name of+      "greet" -> pure $ Right $ PromptResult (Just "A greeting")+        [PromptMessage RoleUser (ContentText ("Hello, " <> Map.findWithDefault "?" "name" args <> "!"))]+      _ -> pure $ Left $ InvalidPromptName name++    resourceList _ = pure+      [ ResourceDefinition "resource://info" "info" (Just "Some info") (Just "text/plain") Nothing ]+    resourceRead _ uri+      | show uri == "resource://info" = pure $ Right $ ResourceText uri "text/plain" "The golden info"+      | otherwise = pure $ Left $ ResourceNotFound $ T.pack $ show uri++    toolList _ = pure+      [ ToolDefinition "echo" "Echo the text"+          (Schema Nothing (SchemaObject+            [("text", Schema (Just "The text") (SchemaString Nothing))] ["text"]))+          Nothing Nothing+      ]+    toolCall _ name args = case name of+      "echo" -> case Map.lookup "text" args of+        Just (String t) -> pure $ Right $ toolResult [ContentText ("echo: " <> t)]+        _               -> pure $ Left $ MissingRequiredParams "field 'text' is missing"+      "boom" -> pure $ Right $ toolError "kaboom"+      _      -> pure $ Left $ UnknownTool name++-- | 'goldenHandlers' extended with the handler slots introduced after+-- v0.2.0. Used only for the fixtures of methods that postdate the legacy+-- anchor: extending the main handler set would change the advertised+-- capabilities and perturb the v0.2.0-anchored initialize fixture.+extendedHandlers :: McpServerHandlers+extendedHandlers = goldenHandlers+  { resourceTemplates = Just $ \_ -> pure+      [ ResourceTemplateDefinition "resource://item/{itemId}" "item"+          (Just "An item") (Just "text/plain") Nothing+      ]+  , completions = Just $ \_ _ref _arg partial _ctx -> pure $ Right $+      completionResult (filter (T.isPrefixOf partial) ["alpha", "beta"])+  }++-- | (fixture path, raw request). The raw requests are written out verbatim+-- so the fixtures capture the full request->response wire behavior.+cases :: [(FilePath, BSL.ByteString)]+cases =+  -- Legacy era: no _meta; served under the initialize-negotiated revision.+  [ ("legacy/initialize",         "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"golden-client\",\"version\":\"1.0\"}}}")+  , ("legacy/ping",               "{\"jsonrpc\":\"2.0\",\"id\":2,\"method\":\"ping\"}")+  , ("legacy/tools-list",         "{\"jsonrpc\":\"2.0\",\"id\":3,\"method\":\"tools/list\"}")+  , ("legacy/tools-call-echo",    "{\"jsonrpc\":\"2.0\",\"id\":4,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{\"text\":\"hi\"}}}")+  , ("legacy/tools-call-unknown", "{\"jsonrpc\":\"2.0\",\"id\":5,\"method\":\"tools/call\",\"params\":{\"name\":\"nope\",\"arguments\":{}}}")+  , ("legacy/tools-call-boom",    "{\"jsonrpc\":\"2.0\",\"id\":6,\"method\":\"tools/call\",\"params\":{\"name\":\"boom\",\"arguments\":{}}}")+  , ("legacy/prompts-list",       "{\"jsonrpc\":\"2.0\",\"id\":7,\"method\":\"prompts/list\"}")+  , ("legacy/prompts-get",        "{\"jsonrpc\":\"2.0\",\"id\":8,\"method\":\"prompts/get\",\"params\":{\"name\":\"greet\",\"arguments\":{\"name\":\"World\"}}}")+  , ("legacy/resources-list",     "{\"jsonrpc\":\"2.0\",\"id\":9,\"method\":\"resources/list\"}")+  , ("legacy/resources-read",     "{\"jsonrpc\":\"2.0\",\"id\":10,\"method\":\"resources/read\",\"params\":{\"uri\":\"resource://info\"}}")++  -- Modern era: _meta declares 2026-07-28; responses carry the modern+  -- envelope (resultType, serverInfo _meta, cacheability on list/read).+  , ("modern/server-discover",     "{\"jsonrpc\":\"2.0\",\"id\":21,\"method\":\"server/discover\",\"params\":{" <> meta <> "}}")+  , ("modern/tools-list",          "{\"jsonrpc\":\"2.0\",\"id\":22,\"method\":\"tools/list\",\"params\":{" <> meta <> "}}")+  , ("modern/tools-call-echo",     "{\"jsonrpc\":\"2.0\",\"id\":23,\"method\":\"tools/call\",\"params\":{\"name\":\"echo\",\"arguments\":{\"text\":\"hi\"}," <> meta <> "}}")+  , ("modern/prompts-get",         "{\"jsonrpc\":\"2.0\",\"id\":24,\"method\":\"prompts/get\",\"params\":{\"name\":\"greet\",\"arguments\":{\"name\":\"World\"}," <> meta <> "}}")+  , ("modern/resources-read",      "{\"jsonrpc\":\"2.0\",\"id\":25,\"method\":\"resources/read\",\"params\":{\"uri\":\"resource://info\"," <> meta <> "}}")+  , ("modern/unsupported-version", "{\"jsonrpc\":\"2.0\",\"id\":26,\"method\":\"tools/list\",\"params\":{\"_meta\":{\"io.modelcontextprotocol/protocolVersion\":\"2099-01-01\"}}}")+  , ("modern/initialize",          "{\"jsonrpc\":\"2.0\",\"id\":27,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2026-07-28\",\"capabilities\":{},\"clientInfo\":{\"name\":\"golden-client\",\"version\":\"1.0\"}," <> meta <> "}}")+  , ("modern/ping",                "{\"jsonrpc\":\"2.0\",\"id\":28,\"method\":\"ping\",\"params\":{" <> meta <> "}}")+  ]++-- Methods introduced after v0.2.0: their fixtures originate on the branch+-- that added the method (there is no earlier behavior to anchor to).+extendedCases :: [(FilePath, BSL.ByteString)]+extendedCases =+  [ ("legacy/resources-templates-list", "{\"jsonrpc\":\"2.0\",\"id\":11,\"method\":\"resources/templates/list\"}")+  , ("legacy/completion-complete",      "{\"jsonrpc\":\"2.0\",\"id\":12,\"method\":\"completion/complete\",\"params\":{\"ref\":{\"type\":\"ref/prompt\",\"name\":\"greet\"},\"argument\":{\"name\":\"name\",\"value\":\"al\"}}}")+  , ("modern/resources-templates-list", "{\"jsonrpc\":\"2.0\",\"id\":29,\"method\":\"resources/templates/list\",\"params\":{" <> meta <> "}}")+  , ("modern/completion-complete",      "{\"jsonrpc\":\"2.0\",\"id\":30,\"method\":\"completion/complete\",\"params\":{\"ref\":{\"type\":\"ref/prompt\",\"name\":\"greet\"},\"argument\":{\"name\":\"name\",\"value\":\"al\"}," <> meta <> "}}")+  ]++meta :: BSL.ByteString+meta = "\"_meta\":{\"io.modelcontextprotocol/protocolVersion\":\"2026-07-28\",\"io.modelcontextprotocol/clientInfo\":{\"name\":\"golden-client\",\"version\":\"1.0\"},\"io.modelcontextprotocol/clientCapabilities\":{}}"++-- Capability fixtures for a transport that delivers notifications (stdio+-- with a configured source: legacy push + modern listen)+notifyingCases :: [(FilePath, BSL.ByteString)]+notifyingCases =+  [ ("legacy/initialize-notifying", "{\"jsonrpc\":\"2.0\",\"id\":13,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"2025-06-18\",\"capabilities\":{},\"clientInfo\":{\"name\":\"golden-client\",\"version\":\"1.0\"}}}")+  , ("modern/server-discover-notifying", "{\"jsonrpc\":\"2.0\",\"id\":31,\"method\":\"server/discover\",\"params\":{" <> meta <> "}}")+  ]++runCase :: NotificationSupport -> McpServerHandlers -> BSL.ByteString -> IO Value+runCase support handlers raw = do+  jsonValue <- either (fail . ("request does not parse: " ++)) pure (eitherDecode raw)+  message <- either (fail . ("request is not JSON-RPC: " ++)) pure (parseJsonRpcMessage jsonValue)+  maybeResponse <- handleMcpMessage goldenServerInfo defaultCacheHints support handlers anonymousContext message+  case maybeResponse of+    Just responseMsg -> pure $ encodeJsonRpcMessage responseMsg+    Nothing          -> fail "expected a response"++goldenCase :: NotificationSupport -> McpServerHandlers -> (FilePath, BSL.ByteString) -> Spec+goldenCase support handlers (name, raw) =+  it name $ do+    actual <- runCase support handlers raw+    let path = "test/golden/" ++ name ++ ".json"+    exists <- doesFileExist path+    accept <- lookupEnv "GOLDEN_ACCEPT"+    if not exists && accept /= Nothing+      then BSL.writeFile path (encode actual)+      else do+        fixtureBytes <- BSL.readFile path+        case eitherDecode fixtureBytes :: Either String Value of+          Left err      -> expectationFailure $ "fixture does not parse: " ++ err+          Right fixture -> actual `shouldBe` fixture++spec :: Spec+spec = describe "Golden wire-format fixtures" $ do+  forM_ cases (goldenCase noNotificationSupport goldenHandlers)+  forM_ extendedCases (goldenCase noNotificationSupport extendedHandlers)+  forM_ notifyingCases+    (goldenCase (NotificationSupport { supportsLegacyPush = True, supportsListen = True }) goldenHandlers)
test/Spec/JSONConversion.hs view
@@ -5,7 +5,7 @@ import Data.Aeson (toJSON) import Data.Text (Text) import qualified Data.Text as T-import MCP.Server+import MCP.Server.Handlers (jsonValueToText) import Test.Hspec import Test.QuickCheck import Text.Read (readMaybe)
+ test/Spec/ModernEra.hs view
@@ -0,0 +1,225 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Coverage for dual-era operation: modern (2026-07-28, per-request _meta)+-- requests get server/discover, the modern result envelope and header+-- validation; legacy (initialize-handshake) requests are served byte-+-- identically to before.+module Spec.ModernEra (spec) where++import Data.Aeson+import qualified Data.Aeson.KeyMap as KM+import Data.Aeson.Types (Pair)+import Data.Text (Text)+import MCP.Server+import MCP.Server.Handlers (handleMcpMessage)+import MCP.Server.JsonRpc+import MCP.Server.Transport.Http (BodyPeek (..), decodeSentinel, peekBody,+                                  peekIsModern, validateRequestHeaders)+import qualified Network.Wai as Wai+import Test.Hspec++testServerInfo :: McpServerInfo+testServerInfo = McpServerInfo+  { serverName = "Test Server"+  , serverVersion = "9.9.9"+  , serverInstructions = "Use the tools."+  }++-- A server that only provides tools; the call handler reports the protocol+-- version it saw in the ClientContext.+testHandlers :: McpServerHandlers+testHandlers = noHandlers+  { tools = Just+      ( \_ctx -> pure []+      , \ctx _name _args -> pure $ Right $ toToolResult+          ("version=" <> maybe "none" id (clientProtocolVersion ctx))+      )+  }++run :: JsonRpcMessage -> IO (Maybe JsonRpcMessage)+run = handleMcpMessage testServerInfo defaultCacheHints noNotificationSupport testHandlers anonymousContext++request :: Text -> Maybe Value -> JsonRpcMessage+request method params = JsonRpcMessageRequest $ JsonRpcRequest+  { requestJsonrpc = "2.0"+  , requestId = RequestIdNumber 1+  , requestMethod = method+  , requestParams = params+  }++-- params carrying the standard modern _meta+modernParams :: [Pair] -> Value+modernParams extra = object $ extra +++  [ "_meta" .= object+      [ "io.modelcontextprotocol/protocolVersion" .= ("2026-07-28" :: Text)+      , "io.modelcontextprotocol/clientInfo" .= object ["name" .= ("test-client" :: Text)]+      , "io.modelcontextprotocol/clientCapabilities" .= object []+      ]+  ]++resultObject :: Maybe JsonRpcMessage -> IO Object+resultObject (Just (JsonRpcMessageResponse r)) = case responseResult r of+  Just (Object o) -> pure o+  other -> expectationFailure ("Expected object result, got: " ++ show other) >> pure KM.empty+resultObject other =+  expectationFailure ("Expected a response, got: " ++ show other) >> pure KM.empty++errorOf :: Maybe JsonRpcMessage -> IO JsonRpcError+errorOf (Just (JsonRpcMessageResponse r)) = case responseError r of+  Just e -> pure e+  Nothing -> expectationFailure "Expected an error response" >> pure (JsonRpcError 0 "" Nothing)+errorOf other =+  expectationFailure ("Expected a response, got: " ++ show other) >> pure (JsonRpcError 0 "" Nothing)++spec :: Spec+spec = describe "Dual-era protocol support" $ do++  describe "server/discover" $ do+    it "reports all supported revisions, newest first" $ do+      o <- resultObject =<< run (request "server/discover" (Just (modernParams [])))+      KM.lookup "supportedVersions" o `shouldBe`+        Just (toJSON (["2026-07-28", "2025-11-25", "2025-06-18", "2025-03-26", "2024-11-05"] :: [Text]))++    it "advertises only capabilities with handlers, plus identity and instructions" $ do+      o <- resultObject =<< run (request "server/discover" (Just (modernParams [])))+      case KM.lookup "capabilities" o of+        Just (Object caps) -> do+          KM.member "tools" caps `shouldBe` True+          KM.member "prompts" caps `shouldBe` False+        other -> expectationFailure $ "capabilities not an object: " ++ show other+      KM.lookup "instructions" o `shouldBe` Just (String "Use the tools.")+      case KM.lookup "_meta" o of+        Just (Object m) ->+          KM.lookup "io.modelcontextprotocol/serverInfo" m `shouldBe`+            Just (object ["name" .= ("Test Server" :: Text), "version" .= ("9.9.9" :: Text)])+        other -> expectationFailure $ "_meta not an object: " ++ show other++    it "carries the modern result envelope even without _meta (probe)" $ do+      o <- resultObject =<< run (request "server/discover" Nothing)+      KM.lookup "resultType" o `shouldBe` Just (String "complete")+      KM.member "ttlMs" o `shouldBe` True+      KM.lookup "cacheScope" o `shouldBe` Just (String "private")++  describe "Modern requests" $ do+    it "stamps resultType and cache fields on cacheable results" $ do+      o <- resultObject =<< run (request "tools/list" (Just (modernParams [])))+      KM.lookup "resultType" o `shouldBe` Just (String "complete")+      KM.lookup "ttlMs" o `shouldBe` Just (Number 0)+      KM.lookup "cacheScope" o `shouldBe` Just (String "private")++    it "stamps resultType but not cache fields on tools/call" $ do+      o <- resultObject =<< run (request "tools/call"+        (Just (modernParams ["name" .= ("t" :: Text), "arguments" .= object []])))+      KM.lookup "resultType" o `shouldBe` Just (String "complete")+      KM.member "ttlMs" o `shouldBe` False++    it "exposes the declared protocol version to handlers via ClientContext" $ do+      o <- resultObject =<< run (request "tools/call"+        (Just (modernParams ["name" .= ("t" :: Text), "arguments" .= object []])))+      KM.lookup "content" o `shouldBe`+        Just (toJSON [object ["type" .= ("text" :: Text), "text" .= ("version=2026-07-28" :: Text)]])++    it "serves initialize and ping as unknown methods when a modern revision is declared" $ do+      e1 <- errorOf =<< run (request "initialize" (Just (modernParams+        [ "protocolVersion" .= ("2026-07-28" :: Text)+        , "capabilities" .= object []+        , "clientInfo" .= object ["name" .= ("c" :: Text), "version" .= ("1" :: Text)]+        ])))+      errorCode e1 `shouldBe` (-32601)+      e2 <- errorOf =<< run (request "ping" (Just (modernParams [])))+      errorCode e2 `shouldBe` (-32601)++    it "rejects undeclared revisions with UnsupportedProtocolVersionError" $ do+      e <- errorOf =<< run (request "tools/list" (Just (object+        [ "_meta" .= object ["io.modelcontextprotocol/protocolVersion" .= ("2099-01-01" :: Text)] ])))+      errorCode e `shouldBe` (-32022)+      case errorData e of+        Just (Object d) -> do+          KM.lookup "requested" d `shouldBe` Just (String "2099-01-01")+          case KM.lookup "supported" d of+            Just (Array _) -> pure ()+            other -> expectationFailure $ "supported not a list: " ++ show other+        other -> expectationFailure $ "no error data: " ++ show other++  describe "Legacy requests" $ do+    it "are served without the modern envelope" $ do+      o <- resultObject =<< run (request "tools/list" Nothing)+      KM.member "resultType" o `shouldBe` False+      KM.member "ttlMs" o `shouldBe` False+      KM.member "_meta" o `shouldBe` False++    it "negotiate down to the newest legacy revision when proposing 2026-07-28 via initialize" $ do+      o <- resultObject =<< run (request "initialize" (Just (object+        [ "protocolVersion" .= ("2026-07-28" :: Text)+        , "capabilities" .= object []+        , "clientInfo" .= object ["name" .= ("c" :: Text), "version" .= ("1" :: Text)]+        ])))+      KM.lookup "protocolVersion" o `shouldBe` Just (String "2025-11-25")++  describe "HTTP request metadata validation" $ do+    let modernBody name = encode $ object+          [ "jsonrpc" .= ("2.0" :: Text)+          , "id" .= (7 :: Int)+          , "method" .= ("tools/call" :: Text)+          , "params" .= modernParams ["name" .= name, "arguments" .= object []]+          ]+        reqWith hs = Wai.defaultRequest { Wai.requestHeaders = hs }++    it "peeks method, name, version and id from the body" $ do+      let peek = peekBody (modernBody ("get_weather" :: Text))+      peekMethod peek `shouldBe` Just "tools/call"+      peekName peek `shouldBe` Just "get_weather"+      peekMetaVersion peek `shouldBe` Just "2026-07-28"+      peekId peek `shouldBe` RequestIdNumber 7+      peekIsModern peek `shouldBe` True++    it "accepts a fully consistent modern request" $ do+      let peek = peekBody (modernBody ("get_weather" :: Text))+      validateRequestHeaders (reqWith+        [ ("MCP-Protocol-Version", "2026-07-28")+        , ("Mcp-Method", "tools/call")+        , ("Mcp-Name", "get_weather")+        ]) peek `shouldBe` Nothing++    it "rejects a missing Mcp-Method header with HeaderMismatch" $ do+      let peek = peekBody (modernBody ("get_weather" :: Text))+      fmap errorCode (validateRequestHeaders (reqWith+        [ ("MCP-Protocol-Version", "2026-07-28")+        , ("Mcp-Name", "get_weather")+        ]) peek) `shouldBe` Just (-32020)++    it "rejects a header/body protocol version mismatch with HeaderMismatch" $ do+      let peek = peekBody (modernBody ("get_weather" :: Text))+      fmap errorCode (validateRequestHeaders (reqWith+        [ ("MCP-Protocol-Version", "2025-11-25")+        , ("Mcp-Method", "tools/call")+        , ("Mcp-Name", "get_weather")+        ]) peek) `shouldBe` Just (-32020)++    it "accepts a base64-sentinel Mcp-Name for non-ASCII names" $ do+      let peek = peekBody (modernBody ("Hello, 世界" :: Text))+      validateRequestHeaders (reqWith+        [ ("MCP-Protocol-Version", "2026-07-28")+        , ("Mcp-Method", "tools/call")+        , ("Mcp-Name", "=?base64?SGVsbG8sIOS4lueVjA==?=")+        ]) peek `shouldBe` Nothing++    it "legacy bodies keep the relaxed rules (no headers required)" $ do+      let legacyBody = encode $ object+            [ "jsonrpc" .= ("2.0" :: Text), "id" .= (1 :: Int)+            , "method" .= ("tools/list" :: Text) ]+      validateRequestHeaders (reqWith []) (peekBody legacyBody) `shouldBe` Nothing++    it "legacy bodies with an unsupported version header are rejected" $ do+      let legacyBody = encode $ object+            [ "jsonrpc" .= ("2.0" :: Text), "id" .= (1 :: Int)+            , "method" .= ("tools/list" :: Text) ]+      fmap errorCode (validateRequestHeaders+        (reqWith [("MCP-Protocol-Version", "1999-01-01")])+        (peekBody legacyBody)) `shouldBe` Just (-32600)++  describe "Sentinel decoding" $ do+    it "passes plain values through" $+      decodeSentinel "us-west1" `shouldBe` "us-west1"+    it "decodes base64 sentinel values" $+      decodeSentinel "=?base64?SGVsbG8sIOS4lueVjA==?=" `shouldBe` "Hello, 世界"
test/Spec/ProtocolVersionNegotiation.hs view
@@ -10,7 +10,7 @@ import MCP.Server.Handlers (handleInitialize) import MCP.Server.JsonRpc (JsonRpcRequest(..), RequestId(..), JsonRpcResponse(..), JsonRpcError(..)) import MCP.Server.Protocol (protocolVersion)-import MCP.Server.Types (Error(..), McpServerHandlers(..), McpServerInfo(..))+import MCP.Server.Types (Error(..), McpServerHandlers(..), McpServerInfo(..), noHandlers, noNotificationSupport)  -- | Test that server performs proper version negotiation according to MCP spec. --@@ -33,10 +33,8 @@    -- A server that only provides tools: capabilities in the initialize   -- response should reflect exactly this.-  let testHandlers = McpServerHandlers-        { prompts = Nothing-        , resources = Nothing-        , tools = Just ( \_ctx -> pure []+  let testHandlers = noHandlers+        { tools = Just ( \_ctx -> pure []                        , \_ctx name _args -> pure (Left (UnknownTool name))                        )         }@@ -57,7 +55,7 @@               , requestMethod = "initialize"               , requestParams = Just params               }-        handleInitialize testServerInfo testHandlers request+        handleInitialize testServerInfo noNotificationSupport testHandlers request    -- Issue an initialize request proposing the given protocol version and   -- return the negotiated version from the server's (non-error) response.
test/Spec/SchemaValidation.hs view
@@ -9,14 +9,15 @@ import MCP.Server import MCP.Server.Derive import Test.Hspec+import TestHelpers import TestTypes import TestData  -- Generate handlers for testing-testToolHandlers :: (ToolListHandler IO, ToolCallHandler IO)+testToolHandlers :: (ToolListHandler, ToolCallHandler) testToolHandlers = $(deriveToolHandler ''TestTool 'handleTestTool) -testToolHandlersWithDescriptions :: (ToolListHandler IO, ToolCallHandler IO)+testToolHandlersWithDescriptions :: (ToolListHandler, ToolCallHandler) testToolHandlersWithDescriptions = $(deriveToolHandlerWithDescription ''TestTool 'handleTestTool testDescriptions)  -- Helper functions for schema validation@@ -27,13 +28,11 @@     [] -> error $ "Tool not found: " ++ T.unpack toolName     _ -> error $ "Multiple tools found with name: " ++ T.unpack toolName -getProperty :: Text -> ToolDefinition -> InputSchemaDefinitionProperty+getProperty :: Text -> ToolDefinition -> Schema getProperty propertyName toolDef =-  case toolDefinitionInputSchema toolDef of-    InputSchemaDefinitionObject props _ ->-      case lookup propertyName props of-        Just prop -> prop-        Nothing -> error $ "Property not found: " ++ T.unpack propertyName+  case lookupProp propertyName toolDef of+    Just prop -> prop+    Nothing -> error $ "Property not found: " ++ T.unpack propertyName  assertToolExists :: Text -> [ToolDefinition] -> IO () assertToolExists toolName toolDefs = do@@ -42,18 +41,17 @@ assertHasProperty :: Text -> Text -> ToolDefinition -> IO () assertHasProperty propertyName expectedType toolDef = do   let prop = getProperty propertyName toolDef-  propertyType prop `shouldBe` expectedType+  schemaTypeName prop `shouldBe` expectedType  assertPropertyDescription :: Text -> ToolDefinition -> Text -> IO () assertPropertyDescription propertyName toolDef expectedDesc = do   let prop = getProperty propertyName toolDef-  propertyDescription prop `shouldBe` expectedDesc+  schemaDescription prop `shouldBe` Just expectedDesc  assertRequiredFields :: [Text] -> ToolDefinition -> IO () assertRequiredFields expectedFields toolDef = do-  case toolDefinitionInputSchema toolDef of-    InputSchemaDefinitionObject _ required ->-      all (`elem` required) expectedFields `shouldBe` True+  let required = schemaRequired (toolDefinitionInputSchema toolDef)+  all (`elem` required) expectedFields `shouldBe` True  assertToolDescription :: ToolDefinition -> Text -> IO () assertToolDescription toolDef expectedDesc =
+ test/Spec/Subscriptions.hs view
@@ -0,0 +1,157 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Coverage for the change-notification machinery: the notifier, the+-- subscriptions/listen wire shapes, and era-aware capability advertisement.+module Spec.Subscriptions (spec) where++import Control.Concurrent.STM (atomically, readTChan)+import Data.Aeson+import qualified Data.Aeson.KeyMap as KM+import Data.Text (Text)+import MCP.Server+import MCP.Server.Handlers (handleMcpMessage)+import MCP.Server.JsonRpc+import MCP.Server.Notifications+import Test.Hspec++subId :: RequestId+subId = RequestIdNumber 7++fullFilterParams :: Maybe Value+fullFilterParams = Just $ object+  [ "notifications" .= object+      [ "toolsListChanged" .= True+      , "resourceSubscriptions" .= (["resource://info"] :: [Text])+      ]+  ]++paramsOf :: JsonRpcNotification -> Object+paramsOf n = case notificationParams n of+  Just (Object o) -> o+  other           -> error $ "notification params not an object: " ++ show other++metaSubId :: Object -> Maybe Value+metaSubId o = do+  Object m <- KM.lookup "_meta" o+  KM.lookup "io.modelcontextprotocol/subscriptionId" m++spec :: Spec+spec = describe "Change notifications" $ do++  describe "Filter parsing" $ do+    it "parses the requested notification types" $ do+      let f = parseNotificationFilter fullFilterParams+      filterTools f `shouldBe` True+      filterPrompts f `shouldBe` False+      filterResources f `shouldBe` False+      filterResourceSubs f `shouldBe` ["resource://info"]++    it "treats missing filters as subscribed-to-nothing" $ do+      let f = parseNotificationFilter Nothing+      filterIsEmpty f `shouldBe` True++  describe "Filter semantics" $ do+    it "delivers only opted-in types" $ do+      let f = parseNotificationFilter fullFilterParams+      filterAccepts f ToolsListChangedEvent `shouldBe` True+      filterAccepts f PromptsListChangedEvent `shouldBe` False+      filterAccepts f ResourcesListChangedEvent `shouldBe` False++    it "matches resource updates by watched URI" $ do+      let f = parseNotificationFilter fullFilterParams+      filterAccepts f (ResourceUpdatedEvent "resource://info") `shouldBe` True+      filterAccepts f (ResourceUpdatedEvent "resource://other") `shouldBe` False++  describe "Wire shapes" $ do+    it "acknowledges with the subscription id and the honored subset" $ do+      let ack = acknowledgedNotification subId (parseNotificationFilter fullFilterParams)+      notificationMethod ack `shouldBe` "notifications/subscriptions/acknowledged"+      let o = paramsOf ack+      metaSubId o `shouldBe` Just (Number 7)+      KM.lookup "notifications" o `shouldBe` Just (object+        [ "toolsListChanged" .= True+        , "resourceSubscriptions" .= (["resource://info"] :: [Text])+        ])++    it "tags event notifications with the subscription id" $ do+      let n = eventNotification subId (ResourceUpdatedEvent "resource://info")+      notificationMethod n `shouldBe` "notifications/resources/updated"+      let o = paramsOf n+      metaSubId o `shouldBe` Just (Number 7)+      KM.lookup "uri" o `shouldBe` Just (String "resource://info")++    it "uses the standard list_changed methods" $ do+      notificationMethod (eventNotification subId ToolsListChangedEvent)+        `shouldBe` "notifications/tools/list_changed"+      notificationMethod (eventNotification subId PromptsListChangedEvent)+        `shouldBe` "notifications/prompts/list_changed"+      notificationMethod (eventNotification subId ResourcesListChangedEvent)+        `shouldBe` "notifications/resources/list_changed"++    it "legacy notifications are untagged" $ do+      let n = legacyEventNotification ToolsListChangedEvent+      notificationMethod n `shouldBe` "notifications/tools/list_changed"+      notificationParams n `shouldBe` Nothing++    it "closure responses carry resultType, the subscription id and the server identity" $ do+      let r = closureResponse (McpServerInfo "S" "2.0" "") subId+      responseResult r `shouldBe` Just (object+        [ "resultType" .= ("complete" :: Text)+        , "_meta" .= object+            [ "io.modelcontextprotocol/subscriptionId" .= (7 :: Int)+            , "io.modelcontextprotocol/serverInfo" .= object+                [ "name" .= ("S" :: Text), "version" .= ("2.0" :: Text) ]+            ]+        ])++  describe "Notifier plumbing" $ do+    it "delivers published events to subscribers" $ do+      (notifier, source) <- newMcpNotifier+      chan <- atomically $ subscribeEvents source+      notifyToolsListChanged notifier+      event <- atomically $ readTChan chan+      event `shouldBe` ToolsListChangedEvent++  describe "Era-aware capability advertisement" $ do+    let toolsServer = noHandlers+          { tools = Just (\_ -> pure [], \_ n _ -> pure (Left (UnknownTool n)))+          , resources = Just (\_ -> pure [], \_ _ -> pure (Left (ResourceNotFound "x")))+          }+        run support method params = handleMcpMessage+          (McpServerInfo "T" "1" "")+          defaultCacheHints+          support+          toolsServer+          anonymousContext+          (JsonRpcMessageRequest (JsonRpcRequest "2.0" (RequestIdNumber 1) method params))+        capsOf resp = case resp of+          Just (JsonRpcMessageResponse r)+            | Just (Object o) <- responseResult r+            , Just (Object caps) <- KM.lookup "capabilities" o -> pure caps+          other -> error $ "no capabilities in: " ++ show other+        initParams = Just $ object+          [ "protocolVersion" .= ("2025-11-25" :: Text)+          , "capabilities" .= object []+          , "clientInfo" .= object ["name" .= ("c" :: Text), "version" .= ("1" :: Text)]+          ]++    it "legacy initialize advertises listChanged only with legacy push" $ do+      caps <- capsOf =<< run (NotificationSupport True True) "initialize" initParams+      KM.lookup "tools" caps `shouldBe` Just (object ["listChanged" .= True])+      -- legacy subscribe (the removed resources/subscribe RPC) is never advertised+      KM.lookup "resources" caps `shouldBe` Just (object ["listChanged" .= True])++    it "legacy initialize over a push-less transport advertises neither" $ do+      caps <- capsOf =<< run (NotificationSupport False True) "initialize" initParams+      KM.lookup "tools" caps `shouldBe` Just (object [])+      KM.lookup "resources" caps `shouldBe` Just (object [])++    it "modern discover advertises listChanged and subscribe when listen is served" $ do+      caps <- capsOf =<< run (NotificationSupport False True) "server/discover" Nothing+      KM.lookup "tools" caps `shouldBe` Just (object ["listChanged" .= True])+      KM.lookup "resources" caps `shouldBe`+        Just (object ["subscribe" .= True, "listChanged" .= True])++    it "no support means nothing extra is advertised" $ do+      caps <- capsOf =<< run noNotificationSupport "server/discover" Nothing+      KM.lookup "tools" caps `shouldBe` Just (object [])
+ test/Spec/TemplatesCompletions.hs view
@@ -0,0 +1,177 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++-- | Coverage for resource templates (record constructors → URI templates)+-- and argument completion (completion/complete).+module Spec.TemplatesCompletions (spec) where++import Data.Aeson+import qualified Data.Aeson.KeyMap as KM+import qualified Data.Map as Map+import Data.Text (Text)+import qualified Data.Text as T+import MCP.Server+import MCP.Server.Derive+import MCP.Server.Handlers (handleMcpMessage)+import MCP.Server.JsonRpc+import Test.Hspec+import TestTypes++templResourceHandlers :: (ResourceListHandler, ResourceReadHandler)+templResourceHandlers = $(deriveResourceHandler ''TemplResource 'handleTemplResource)++templTemplates :: ResourceTemplateListHandler+templTemplates = $(deriveResourceTemplatesWithDescription ''TemplResource+  [("MemberProfile", "A member's profile"), ("OrderItem", "One item of an order")])++readUri :: String -> IO (Either Error ResourceContent)+readUri s = case parseURI s of+  Just uri -> snd templResourceHandlers anonCtx uri+  Nothing  -> fail ("bad test URI: " ++ s)++expectText :: Either Error ResourceContent -> Text -> Expectation+expectText (Right (ResourceText _ _ content)) expected = content `shouldBe` expected+expectText other expected =+  expectationFailure $ "Expected ResourceText '" ++ T.unpack expected ++ "' but got: " ++ show other++-- A completion handler that records what it receives in its suggestions+testCompletions :: CompletionHandler+testCompletions _ ref argName partial ctxArgs = pure $ Right $ completionResult $+  case ref of+    CompletionRefPrompt name ->+      [ "prompt:" <> name <> ":" <> argName <> ":" <> partial+        <> ":" <> T.intercalate "," (Map.keys ctxArgs)+      ]+    CompletionRefResource uri -> [ "resource:" <> uri <> ":" <> argName ]++completionServer :: McpServerHandlers+completionServer = noHandlers { completions = Just testCompletions }++runReq :: McpServerHandlers -> Text -> Maybe Value -> IO (Maybe JsonRpcMessage)+runReq handlers method params =+  handleMcpMessage+    (McpServerInfo "T" "1" "")+    defaultCacheHints+    noNotificationSupport+    handlers+    anonymousContext+    (JsonRpcMessageRequest (JsonRpcRequest "2.0" (RequestIdNumber 1) method params))++resultOf :: Maybe JsonRpcMessage -> IO Object+resultOf (Just (JsonRpcMessageResponse r)) = case responseResult r of+  Just (Object o) -> pure o+  other -> expectationFailure ("Expected object result, got: " ++ show other) >> pure KM.empty+resultOf other =+  expectationFailure ("Expected a response, got: " ++ show other) >> pure KM.empty++spec :: Spec+spec = describe "Resource templates and completions" $ do++  describe "Template derivation" $ do+    it "advertises record constructors as URI templates" $ do+      templates <- templTemplates anonCtx+      map resourceTemplateURITemplate templates `shouldBe`+        [ "resource://member_profile/{memberId}"+        , "resource://order_item/{orderId}/{itemName}"+        ]+      map resourceTemplateDescription templates `shouldBe`+        [ Just "A member's profile", Just "One item of an order" ]++    it "excludes record constructors from the static resource list" $ do+      statics <- fst templResourceHandlers anonCtx+      map resourceDefinitionURI statics `shouldBe` ["resource://catalog"]++  describe "Template reads" $ do+    it "still reads static resources" $ do+      result <- readUri "resource://catalog"+      result `expectText` "The catalog"++    it "reads a single-parameter template" $ do+      result <- readUri "resource://member_profile/alice"+      result `expectText` "Profile of alice"++    it "percent-decodes template segments" $ do+      result <- readUri "resource://member_profile/alice%20smith"+      result `expectText` "Profile of alice smith"++    it "ignores query strings and fragments when matching templates" $ do+      result <- readUri "resource://member_profile/alice?x=1"+      result `expectText` "Profile of alice"+      result2 <- readUri "resource://member_profile/alice#frag"+      result2 `expectText` "Profile of alice"++    it "reads a multi-parameter template with typed segments" $ do+      result <- readUri "resource://order_item/42/widget"+      result `expectText` "Order 42 item widget"++    it "rejects segments that fail the field's type" $ do+      result <- readUri "resource://order_item/notanum/widget"+      case result of+        Left (InvalidParams msg) -> msg `shouldSatisfy` T.isInfixOf "template field 'orderId'"+        other -> expectationFailure $ "Expected InvalidParams but got: " ++ show other++    it "falls through to ResourceNotFound on a segment-count mismatch" $ do+      result <- readUri "resource://member_profile/a/b"+      case result of+        Left (ResourceNotFound _) -> pure ()+        other -> expectationFailure $ "Expected ResourceNotFound but got: " ++ show other++  describe "resources/templates/list dispatch" $ do+    it "returns templates and carries the modern cache envelope" $ do+      let handlers = noHandlers { resourceTemplates = Just templTemplates }+      o <- resultOf =<< runReq handlers "resources/templates/list" (Just (object+        [ "_meta" .= object ["io.modelcontextprotocol/protocolVersion" .= ("2026-07-28" :: Text)] ]))+      KM.member "resourceTemplates" o `shouldBe` True+      KM.member "ttlMs" o `shouldBe` True+      KM.lookup "resultType" o `shouldBe` Just (String "complete")++    it "answers -32601 when no template handler is configured" $ do+      resp <- runReq noHandlers "resources/templates/list" Nothing+      case resp of+        Just (JsonRpcMessageResponse r) ->+          fmap errorCode (responseError r) `shouldBe` Just (-32601)+        other -> expectationFailure $ "Expected a response, got: " ++ show other++    it "answers resources/list with an empty list for a templates-only server" $ do+      let handlers = noHandlers { resourceTemplates = Just templTemplates }+      o <- resultOf =<< runReq handlers "resources/list" Nothing+      KM.lookup "resources" o `shouldBe` Just (toJSON ([] :: [ResourceDefinition]))++  describe "completion/complete" $ do+    it "dispatches prompt refs with context arguments" $ do+      o <- resultOf =<< runReq completionServer "completion/complete" (Just (object+        [ "ref" .= object ["type" .= ("ref/prompt" :: Text), "name" .= ("recipe" :: Text)]+        , "argument" .= object ["name" .= ("idea" :: Text), "value" .= ("pa" :: Text)]+        , "context" .= object ["arguments" .= object ["cuisine" .= ("italian" :: Text)]]+        ]))+      KM.lookup "completion" o `shouldBe`+        Just (object ["values" .= (["prompt:recipe:idea:pa:cuisine"] :: [Text])])++    it "dispatches resource template refs" $ do+      o <- resultOf =<< runReq completionServer "completion/complete" (Just (object+        [ "ref" .= object ["type" .= ("ref/resource" :: Text), "uri" .= ("resource://member_profile/{memberId}" :: Text)]+        , "argument" .= object ["name" .= ("memberId" :: Text), "value" .= ("al" :: Text)]+        ]))+      KM.lookup "completion" o `shouldBe`+        Just (object ["values" .= (["resource:resource://member_profile/{memberId}:memberId"] :: [Text])])++    it "answers -32601 when no completion handler is configured" $ do+      resp <- runReq noHandlers "completion/complete" Nothing+      case resp of+        Just (JsonRpcMessageResponse r) ->+          fmap errorCode (responseError r) `shouldBe` Just (-32601)+        other -> expectationFailure $ "Expected a response, got: " ++ show other++  describe "Capabilities" $ do+    it "advertises completions and resources-via-templates when configured" $ do+      let handlers = noHandlers+            { resourceTemplates = Just templTemplates+            , completions = Just testCompletions+            }+      o <- resultOf =<< runReq handlers "server/discover" Nothing+      case KM.lookup "capabilities" o of+        Just (Object caps) -> do+          KM.member "completions" caps `shouldBe` True+          KM.member "resources" caps `shouldBe` True+          KM.member "tools" caps `shouldBe` False+        other -> expectationFailure $ "capabilities not an object: " ++ show other
test/Spec/ToolCallParsing.hs view
@@ -3,27 +3,27 @@  module Spec.ToolCallParsing (spec) where +import Data.Aeson (Value (..)) import Data.Text (Text) import qualified Data.Text as T import MCP.Server import MCP.Server.Derive import Test.Hspec+import TestHelpers import TestTypes -allTypesHandlers :: (ToolListHandler IO, ToolCallHandler IO)+allTypesHandlers :: (ToolListHandler, ToolCallHandler) allTypesHandlers = $(deriveToolHandler ''AllTypesTool 'handleAllTypesTool) -callTool :: Text -> [(Text, Text)] -> IO (Either Error Content)-callTool = snd allTypesHandlers anonCtx+callTool :: Text -> [(Text, Text)] -> IO (Either Error ToolResult)+callTool name args = snd allTypesHandlers anonCtx name (stringArgs args) -shouldBeRight :: IO (Either Error Content) -> Text -> IO ()+shouldBeRight :: IO (Either Error ToolResult) -> Text -> IO () shouldBeRight action expected = do   result <- action-  case result of-    Right (ContentText content) -> content `shouldBe` expected-    other -> expectationFailure $ "Expected ContentText '" ++ T.unpack expected ++ "' but got: " ++ show other+  result `shouldBeTextResult` expected -shouldBeInvalidParams :: IO (Either Error Content) -> Text -> IO ()+shouldBeInvalidParams :: IO (Either Error ToolResult) -> Text -> IO () shouldBeInvalidParams action expectedSubstring = do   result <- action   case result of@@ -31,7 +31,7 @@       T.isInfixOf expectedSubstring msg `shouldBe` True     other -> expectationFailure $ "Expected InvalidParams containing '" ++ T.unpack expectedSubstring ++ "' but got: " ++ show other -shouldBeMissingParams :: IO (Either Error Content) -> Text -> IO ()+shouldBeMissingParams :: IO (Either Error ToolResult) -> Text -> IO () shouldBeMissingParams action expectedSubstring = do   result <- action   case result of@@ -146,3 +146,14 @@       shouldBeMissingParams         (callTool "required_fields" [("rfText", "hello")])         "is missing"++  describe "Integer exponent bound" $ do+    -- Regression: aeson keeps huge exponents compact, so without a bound a+    -- ~13-byte payload like 1e100000 would materialize a 100001-digit Integer.+    it "rejects huge exponents without materializing the Integer" $ do+      result <- snd allTypesHandlers anonCtx "optional_fields"+        (valueArgs [("ofInteger", Number (read "1e100000"))])+      case result of+        Left (InvalidParams msg) ->+          msg `shouldSatisfy` T.isInfixOf "exponent too large"+        other -> expectationFailure $ "Expected InvalidParams but got: " ++ show other
+ test/Spec/TypedArgs.hs view
@@ -0,0 +1,135 @@+{-# 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
test/Spec/UnicodeHandling.hs view
@@ -5,6 +5,7 @@ import           Data.Aeson import qualified Data.Aeson.KeyMap    as KM import qualified Data.ByteString.Lazy as BSL+import qualified Data.Map             as Map import qualified Data.Text            as T import qualified Data.Text.Encoding   as TE import           MCP.Server.Protocol@@ -61,7 +62,8 @@       let response = ToolsCallResponse             { toolsCallContent = [ContentText "Result: √16 = 4, π ≈ 3.14159"]             , toolsCallIsError = Nothing-            , toolsCallMeta = Nothing  -- 2025-06-18: New _meta field+            , toolsCallStructuredContent = Nothing+            , toolsCallMeta = Nothing             }       let encoded = encode response       -- Just verify the JSON can be encoded and contains Unicode@@ -87,7 +89,8 @@       let response = ToolsCallResponse             { toolsCallContent = [ContentText "Mathematical result: √(π²+e²) ≈ 4.53"]             , toolsCallIsError = Nothing-            , toolsCallMeta = Nothing  -- 2025-06-18: New _meta field+            , toolsCallStructuredContent = Nothing+            , toolsCallMeta = Nothing             }       let json = toJSON response       -- Verify the JSON can be encoded and contains Unicode@@ -177,8 +180,8 @@             ]        let promptGetHandler name args = case name of-            "math_formula" -> case lookup "formula" args of-              Just formula -> return $ Right $ ContentText $ "Formula: " <> formula <> " → √(solution)"+            "math_formula" -> case Map.lookup "formula" args of+              Just formula -> return $ Right $ toPromptResult $ ContentText $ "Formula: " <> formula <> " → √(solution)"               Nothing -> return $ Left $ MissingRequiredParams "formula"             _ -> return $ Left $ InvalidPromptName name @@ -201,23 +204,23 @@             ToolDefinition               { toolDefinitionName = "calculate"               , toolDefinitionDescription = "Calculate with Unicode symbols: √∑∏"-              , toolDefinitionInputSchema = InputSchemaDefinitionObject-                  { properties = [("expression", InputSchemaDefinitionProperty "string" "Mathematical expression")]-                  , required = ["expression"]-                  }+              , toolDefinitionInputSchema = schema $ SchemaObject+                  [("expression", describedSchema "Mathematical expression" (SchemaString Nothing))]+                  ["expression"]+              , toolDefinitionOutputSchema = Nothing               , toolDefinitionTitle = Nothing  -- 2025-06-18: New title field               }             ]        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"+            "calculate" -> case Map.lookup "expression" args of+              Just (String expr) -> return $ Right $ toToolResult $ ContentText $ "Result: " <> expr <> " → √answer"+              _ -> return $ Left $ MissingRequiredParams "expression"             _ -> return $ Left $ UnknownTool name        -- Handlers above ignore the per-request client context-      let ctx = ClientContext Nothing Nothing-      let handlers = McpServerHandlers+      let ctx = anonymousContext+      let handlers = noHandlers             { prompts = Just (const promptListHandler, const promptGetHandler)             , resources = Just (const resourceListHandler, const resourceReadHandler)             , tools = Just (const toolListHandler, const toolCallHandler)@@ -234,9 +237,9 @@       toolList `shouldSatisfy` (not . null)        -- Test actual Unicode handling-      promptResult <- snd (case prompts handlers of Just h -> h; Nothing -> error "No prompts") ctx "math_formula" [("formula", "√(x²+y²)")]+      promptResult <- snd (case prompts handlers of Just h -> h; Nothing -> error "No prompts") ctx "math_formula" (Map.fromList [("formula", "√(x²+y²)")])       case promptResult of-        Right (ContentText txt) -> txt `shouldSatisfy` T.isInfixOf "√"+        Right (PromptResult _ [PromptMessage _ (ContentText txt)]) -> txt `shouldSatisfy` T.isInfixOf "√"         _ -> expectationFailure "Expected successful prompt result"        uri <- case parseURI "resource://unicode_symbols" of@@ -247,9 +250,9 @@         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") ctx "calculate" [("expression", "∫₀^∞ e^(-x²) dx = √π/2")]-      case toolResult of-        Right (ContentText txt) -> do+      callResult <- snd (case tools handlers of Just h -> h; Nothing -> error "No tools") ctx "calculate" (Map.fromList [("expression", String "∫₀^∞ e^(-x²) dx = √π/2")])+      case callResult of+        Right tr | [ContentText txt] <- toolResultContent tr -> do           txt `shouldSatisfy` T.isInfixOf "√"           txt `shouldSatisfy` T.isInfixOf "∫"         _ -> expectationFailure "Expected successful tool result"
+ test/TestHelpers.hs view
@@ -0,0 +1,89 @@+{-# LANGUAGE OverloadedStrings #-}++-- | Shared helpers for exercising the 0.2 handler API from tests.+module TestHelpers+  ( -- * Building argument maps+    stringArgs+  , valueArgs+  , promptArgsMap+    -- * Result expectations+  , shouldBeTextResult+  , shouldBeTextResultContaining+  , shouldBePromptText+    -- * Schema access+  , schemaTypeName+  , schemaProps+  , schemaRequired+  , lookupProp+  ) where++import           Data.Aeson (Value (..))+import           Data.Map   (Map)+import qualified Data.Map   as Map+import           Data.Text  (Text)+import qualified Data.Text  as T+import           MCP.Server+import           Test.Hspec++-- | Tool arguments where every value is a JSON string (exercises the lenient+-- string-fallback parsing that pre-0.2 clients rely on).+stringArgs :: [(Text, Text)] -> Map Text Value+stringArgs = Map.fromList . map (fmap String)++-- | Tool arguments with native JSON values.+valueArgs :: [(Text, Value)] -> Map Text Value+valueArgs = Map.fromList++-- | Prompt arguments (string-valued per the MCP spec).+promptArgsMap :: [(Text, Text)] -> Map Text Text+promptArgsMap = Map.fromList++-- | Expect a successful, non-error tool result with exactly one text block.+shouldBeTextResult :: Either Error ToolResult -> Text -> Expectation+shouldBeTextResult (Right tr) expected+  | not (toolResultIsError tr)+  , [ContentText content] <- toolResultContent tr = content `shouldBe` expected+shouldBeTextResult other expected =+  expectationFailure $ "Expected text result '" ++ T.unpack expected ++ "' but got: " ++ show other++-- | Like 'shouldBeTextResult' but with substring matching.+shouldBeTextResultContaining :: Either Error ToolResult -> Text -> Expectation+shouldBeTextResultContaining (Right tr) expected+  | not (toolResultIsError tr)+  , [ContentText content] <- toolResultContent tr =+      content `shouldSatisfy` T.isInfixOf expected+shouldBeTextResultContaining other expected =+  expectationFailure $ "Expected text result containing '" ++ T.unpack expected ++ "' but got: " ++ show other++-- | Expect a prompt result of a single user message with the given text.+shouldBePromptText :: Either Error PromptResult -> Text -> Expectation+shouldBePromptText (Right (PromptResult _ [PromptMessage RoleUser (ContentText content)])) expected =+  content `shouldBe` expected+shouldBePromptText other expected =+  expectationFailure $ "Expected prompt text '" ++ T.unpack expected ++ "' but got: " ++ show other++-- | The JSON Schema type keyword for a schema.+schemaTypeName :: Schema -> Text+schemaTypeName s = case schemaShape s of+  SchemaString _   -> "string"+  SchemaInteger    -> "integer"+  SchemaNumber     -> "number"+  SchemaBoolean    -> "boolean"+  SchemaArray _    -> "array"+  SchemaObject _ _ -> "object"++-- | The properties of an object schema (empty for non-objects).+schemaProps :: Schema -> [(Text, Schema)]+schemaProps s = case schemaShape s of+  SchemaObject props _ -> props+  _                    -> []++-- | The required field names of an object schema.+schemaRequired :: Schema -> [Text]+schemaRequired s = case schemaShape s of+  SchemaObject _ req -> req+  _                  -> []++-- | Look up a property schema in a tool's input schema.+lookupProp :: Text -> ToolDefinition -> Maybe Schema+lookupProp name def = lookup name (schemaProps (toolDefinitionInputSchema def))
test/TestTypes.hs view
@@ -4,12 +4,15 @@  import           Data.Text  (Text) import qualified Data.Text  as T-import           MCP.Server (ClientContext(..), Content(..), ResourceContent(..))+import           MCP.Server (ClientContext, anonymousContext, Content (..), MessageRole (..),+                             PromptMessage (..), PromptResult (..),+                             ResourceContent (..), ToolResult, toolError,+                             toolResult) import           Network.URI (URI)  -- Context passed to handlers in tests (no transport-level identity) anonCtx :: ClientContext-anonCtx = ClientContext Nothing Nothing+anonCtx = anonymousContext  -- Test data types for end-to-end testing data TestPrompt@@ -135,6 +138,59 @@         , "float=" <> maybe "Nothing" (T.pack . show) f         , "bool=" <> maybe "Nothing" (T.pack . show) b         ]++-- Types exercising 0.2 typed arguments: enums, lists, nested records+data Color = Red | Green | Blue+    deriving (Show, Eq)++data Filters = Filters { tags :: [Text], maxCount :: Maybe Int }+    deriving (Show, Eq)++data TypedTool+    = Paint { color :: Color, brightness :: Int }+    | BulkAdd { values :: [Int] }+    | QueryData { filters :: Filters }+    | AlwaysFails { reason :: Text }+    deriving (Show, Eq)++handleTypedTool :: ClientContext -> TypedTool -> IO ToolResult+handleTypedTool _ (Paint c b) =+    pure $ toolResult [ContentText $ "Painting " <> T.pack (show c) <> " at " <> T.pack (show b)]+handleTypedTool _ (BulkAdd vs) =+    pure $ toolResult [ContentText $ "Sum: " <> T.pack (show (sum vs))]+handleTypedTool _ (QueryData (Filters ts mc)) =+    pure $ toolResult [ContentText $ "Query tags=" <> T.intercalate "," ts+                        <> maybe "" ((" max=" <>) . T.pack . show) mc]+handleTypedTool _ (AlwaysFails msg) =+    pure $ toolError msg++-- Resource type mixing static resources and templates (record constructors)+data TemplResource+    = Catalog+    | MemberProfile { memberId :: Text }+    | OrderItem { orderId :: Int, itemName :: Text }+    deriving (Show, Eq)++handleTemplResource :: ClientContext -> URI -> TemplResource -> IO ResourceContent+handleTemplResource _ uri Catalog =+    pure $ ResourceText uri "text/plain" "The catalog"+handleTemplResource _ uri (MemberProfile uid) =+    pure $ ResourceText uri "text/plain" ("Profile of " <> uid)+handleTemplResource _ uri (OrderItem oid item) =+    pure $ ResourceText uri "text/plain" ("Order " <> T.pack (show oid) <> " item " <> item)++-- A prompt whose handler produces a multi-message conversation+data ConvPrompt = Conversation { topic :: Text }+    deriving (Show, Eq)++handleConvPrompt :: ClientContext -> ConvPrompt -> IO PromptResult+handleConvPrompt _ (Conversation t) = pure $ PromptResult+    { promptResultDescription = Just ("About " <> t)+    , promptResultMessages =+        [ PromptMessage RoleUser (ContentText ("Tell me about " <> t))+        , PromptMessage RoleAssistant (ContentText ("Here is what I know about " <> t))+        ]+    }  -- Test descriptions for custom description functionality testDescriptions :: [(String, String)]
+ test/golden/legacy/completion-complete.json view
@@ -0,0 +1,1 @@+{"id":12,"jsonrpc":"2.0","result":{"completion":{"values":["alpha"]}}}
+ test/golden/legacy/initialize-notifying.json view
@@ -0,0 +1,1 @@+{"id":13,"jsonrpc":"2.0","result":{"capabilities":{"prompts":{"listChanged":true},"resources":{"listChanged":true},"tools":{"listChanged":true}},"protocolVersion":"2025-06-18","serverInfo":{"instructions":"Golden fixture server","name":"Golden Server","version":"1.0.0"}}}
+ test/golden/legacy/initialize.json view
@@ -0,0 +1,1 @@+{"id":1,"jsonrpc":"2.0","result":{"capabilities":{"prompts":{},"resources":{},"tools":{}},"protocolVersion":"2025-06-18","serverInfo":{"instructions":"Golden fixture server","name":"Golden Server","version":"1.0.0"}}}
+ test/golden/legacy/ping.json view
@@ -0,0 +1,1 @@+{"id":2,"jsonrpc":"2.0","result":{}}
+ test/golden/legacy/prompts-get.json view
@@ -0,0 +1,1 @@+{"id":8,"jsonrpc":"2.0","result":{"description":"A greeting","messages":[{"content":{"text":"Hello, World!","type":"text"},"role":"user"}]}}
+ test/golden/legacy/prompts-list.json view
@@ -0,0 +1,1 @@+{"id":7,"jsonrpc":"2.0","result":{"prompts":[{"arguments":[{"description":"Who to greet","name":"name","required":true}],"description":"Greet someone","name":"greet"}]}}
+ test/golden/legacy/resources-list.json view
@@ -0,0 +1,1 @@+{"id":9,"jsonrpc":"2.0","result":{"resources":[{"description":"Some info","mimeType":"text/plain","name":"info","uri":"resource://info"}]}}
+ test/golden/legacy/resources-read.json view
@@ -0,0 +1,1 @@+{"id":10,"jsonrpc":"2.0","result":{"contents":[{"mimeType":"text/plain","text":"The golden info","uri":"resource://info"}]}}
+ test/golden/legacy/resources-templates-list.json view
@@ -0,0 +1,1 @@+{"id":11,"jsonrpc":"2.0","result":{"resourceTemplates":[{"description":"An item","mimeType":"text/plain","name":"item","uriTemplate":"resource://item/{itemId}"}]}}
+ test/golden/legacy/tools-call-boom.json view
@@ -0,0 +1,1 @@+{"id":6,"jsonrpc":"2.0","result":{"content":[{"text":"kaboom","type":"text"}],"isError":true}}
+ test/golden/legacy/tools-call-echo.json view
@@ -0,0 +1,1 @@+{"id":4,"jsonrpc":"2.0","result":{"content":[{"text":"echo: hi","type":"text"}]}}
+ test/golden/legacy/tools-call-unknown.json view
@@ -0,0 +1,1 @@+{"error":{"code":-32602,"message":"Unknown tool: nope"},"id":5,"jsonrpc":"2.0"}
+ test/golden/legacy/tools-list.json view
@@ -0,0 +1,1 @@+{"id":3,"jsonrpc":"2.0","result":{"tools":[{"description":"Echo the text","inputSchema":{"properties":{"text":{"description":"The text","type":"string"}},"required":["text"],"type":"object"},"name":"echo"}]}}
+ test/golden/modern/completion-complete.json view
@@ -0,0 +1,1 @@+{"id":30,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"Golden Server","version":"1.0.0"}},"completion":{"values":["alpha"]},"resultType":"complete"}}
+ test/golden/modern/initialize.json view
@@ -0,0 +1,1 @@+{"error":{"code":-32601,"message":"Method not found: initialize (not part of protocol revision 2026-07-28)"},"id":27,"jsonrpc":"2.0"}
+ test/golden/modern/ping.json view
@@ -0,0 +1,1 @@+{"error":{"code":-32601,"message":"Method not found: ping (not part of protocol revision 2026-07-28)"},"id":28,"jsonrpc":"2.0"}
+ test/golden/modern/prompts-get.json view
@@ -0,0 +1,1 @@+{"id":24,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"Golden Server","version":"1.0.0"}},"description":"A greeting","messages":[{"content":{"text":"Hello, World!","type":"text"},"role":"user"}],"resultType":"complete"}}
+ test/golden/modern/resources-read.json view
@@ -0,0 +1,1 @@+{"id":25,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"Golden Server","version":"1.0.0"}},"cacheScope":"private","contents":[{"mimeType":"text/plain","text":"The golden info","uri":"resource://info"}],"resultType":"complete","ttlMs":0}}
+ test/golden/modern/resources-templates-list.json view
@@ -0,0 +1,1 @@+{"id":29,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"Golden Server","version":"1.0.0"}},"cacheScope":"private","resourceTemplates":[{"description":"An item","mimeType":"text/plain","name":"item","uriTemplate":"resource://item/{itemId}"}],"resultType":"complete","ttlMs":0}}
+ test/golden/modern/server-discover-notifying.json view
@@ -0,0 +1,1 @@+{"id":31,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"Golden Server","version":"1.0.0"}},"cacheScope":"private","capabilities":{"prompts":{"listChanged":true},"resources":{"listChanged":true,"subscribe":true},"tools":{"listChanged":true}},"instructions":"Golden fixture server","resultType":"complete","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"ttlMs":0}}
+ test/golden/modern/server-discover.json view
@@ -0,0 +1,1 @@+{"id":21,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"Golden Server","version":"1.0.0"}},"cacheScope":"private","capabilities":{"prompts":{},"resources":{},"tools":{}},"instructions":"Golden fixture server","resultType":"complete","supportedVersions":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"],"ttlMs":0}}
+ test/golden/modern/tools-call-echo.json view
@@ -0,0 +1,1 @@+{"id":23,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"Golden Server","version":"1.0.0"}},"content":[{"text":"echo: hi","type":"text"}],"resultType":"complete"}}
+ test/golden/modern/tools-list.json view
@@ -0,0 +1,1 @@+{"id":22,"jsonrpc":"2.0","result":{"_meta":{"io.modelcontextprotocol/serverInfo":{"name":"Golden Server","version":"1.0.0"}},"cacheScope":"private","resultType":"complete","tools":[{"description":"Echo the text","inputSchema":{"properties":{"text":{"description":"The text","type":"string"}},"required":["text"],"type":"object"},"name":"echo"}],"ttlMs":0}}
+ test/golden/modern/unsupported-version.json view
@@ -0,0 +1,1 @@+{"error":{"code":-32022,"data":{"requested":"2099-01-01","supported":["2026-07-28","2025-11-25","2025-06-18","2025-03-26","2024-11-05"]},"message":"Unsupported protocol version"},"id":26,"jsonrpc":"2.0"}