packages feed

mcp-server 0.1.0.20 → 0.1.0.21

raw patch · 19 files changed

+413/−261 lines, 19 filesPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

API changes (from Hackage documentation)

- MCP.Server.Transport.Http: instance GHC.Classes.Eq MCP.Server.Transport.Http.HttpConfig
- MCP.Server.Transport.Http: instance GHC.Internal.Show.Show MCP.Server.Transport.Http.HttpConfig
+ MCP.Server: [httpAuthorize] :: HttpConfig -> Maybe (Maybe Text -> IO (Maybe Value))
+ MCP.Server.Protocol: supportedVersions :: [Text]
+ MCP.Server.Transport.Http: [httpAuthorize] :: HttpConfig -> Maybe (Maybe Text -> IO (Maybe Value))
+ MCP.Server.Types: ClientContext :: Maybe Text -> Maybe Value -> ClientContext
+ MCP.Server.Types: [clientPrincipal] :: ClientContext -> Maybe Value
+ MCP.Server.Types: [clientToken] :: ClientContext -> Maybe Text
+ MCP.Server.Types: data ClientContext
+ MCP.Server.Types: instance GHC.Classes.Eq MCP.Server.Types.ClientContext
+ MCP.Server.Types: instance GHC.Internal.Show.Show MCP.Server.Types.ClientContext
- MCP.Server: HttpConfig :: Int -> String -> String -> Bool -> HttpConfig
+ MCP.Server: HttpConfig :: Int -> String -> String -> Bool -> Maybe (Maybe Text -> IO (Maybe Value)) -> HttpConfig
- MCP.Server.Handlers: handleInitialize :: MonadIO m => McpServerInfo -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleInitialize :: MonadIO m => McpServerInfo -> McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse
- MCP.Server.Handlers: handleMcpMessage :: MonadIO m => McpServerInfo -> McpServerHandlers m -> JsonRpcMessage -> m (Maybe JsonRpcMessage)
+ MCP.Server.Handlers: handleMcpMessage :: MonadIO m => McpServerInfo -> McpServerHandlers m -> ClientContext -> JsonRpcMessage -> m (Maybe JsonRpcMessage)
- MCP.Server.Handlers: handlePromptsGet :: MonadIO m => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handlePromptsGet :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
- MCP.Server.Handlers: handlePromptsList :: MonadIO m => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handlePromptsList :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
- MCP.Server.Handlers: handleResourcesList :: MonadIO m => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleResourcesList :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
- MCP.Server.Handlers: handleResourcesRead :: MonadIO m => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleResourcesRead :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
- MCP.Server.Handlers: handleToolsCall :: MonadIO m => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleToolsCall :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
- MCP.Server.Handlers: handleToolsList :: MonadIO m => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse
+ MCP.Server.Handlers: handleToolsList :: MonadIO m => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse
- MCP.Server.Transport.Http: HttpConfig :: Int -> String -> String -> Bool -> HttpConfig
+ MCP.Server.Transport.Http: HttpConfig :: Int -> String -> String -> Bool -> Maybe (Maybe Text -> IO (Maybe Value)) -> HttpConfig
- MCP.Server.Types: type PromptGetHandler (m :: Type -> Type) = PromptName -> [(ArgumentName, ArgumentValue)] -> m Either Error Content
+ MCP.Server.Types: type PromptGetHandler (m :: Type -> Type) = ClientContext -> PromptName -> [(ArgumentName, ArgumentValue)] -> m Either Error Content
- MCP.Server.Types: type PromptListHandler (m :: Type -> k) = m [PromptDefinition]
+ MCP.Server.Types: type PromptListHandler (m :: Type -> Type) = ClientContext -> m [PromptDefinition]
- MCP.Server.Types: type ResourceListHandler (m :: Type -> k) = m [ResourceDefinition]
+ MCP.Server.Types: type ResourceListHandler (m :: Type -> Type) = ClientContext -> m [ResourceDefinition]
- MCP.Server.Types: type ResourceReadHandler (m :: Type -> Type) = URI -> m Either Error ResourceContent
+ MCP.Server.Types: type ResourceReadHandler (m :: Type -> Type) = ClientContext -> URI -> m Either Error ResourceContent
- MCP.Server.Types: type ToolCallHandler (m :: Type -> Type) = ToolName -> [(ArgumentName, ArgumentValue)] -> m Either Error Content
+ MCP.Server.Types: type ToolCallHandler (m :: Type -> Type) = ClientContext -> ToolName -> [(ArgumentName, ArgumentValue)] -> m Either Error Content
- MCP.Server.Types: type ToolListHandler (m :: Type -> k) = m [ToolDefinition]
+ MCP.Server.Types: type ToolListHandler (m :: Type -> Type) = ClientContext -> m [ToolDefinition]

Files

CHANGELOG.md view
@@ -1,5 +1,40 @@ # Revision history for mcp-server +## 0.1.0.21 - ???++* **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;+  on HTTP it carries the request's bearer token and the principal returned by+  the authorization callback.+* **BREAKING**: `HttpConfig` gains an `httpAuthorize` field — an optional+  callback that validates the presented `Authorization: Bearer` token and+  returns an application-defined principal (`Nothing` rejects with 401). As it+  now holds a function, `HttpConfig` no longer derives `Show`/`Eq`.+* HTTP transport: accept requests without an `MCP-Protocol-Version` header+  (the spec says to assume `2025-03-26`), exempt `initialize` from the header+  check (it negotiates its version in the body), and keep rejecting a present+  but unsupported header with 400. Previously every request without the header+  was rejected, locking out pre-`2025-06-18` clients.+* `initialize` now advertises only the capabilities that actually have+  handlers, so strict clients no longer drop the server when e.g.+  `prompts/list` answers "not supported".+* CORS: preflight `OPTIONS` requests are exempt from authorization (browsers+  send no credentials on preflight) and `Authorization` is included in+  `Access-Control-Allow-Headers`.+* `http-simple-example` is now built with `-threaded`, which Warp requires;+  previously every request crashed with a `TimerManager` error.++## 0.1.0.20 - ???++* 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+  always responding with the server's own version. Fixes clients (e.g. Claude+  Code) that disconnect when they receive a different version than requested.+* Apply the same negotiation to the HTTP transport's `MCP-Protocol-Version`+  header check, which previously rejected anything other than `2025-06-18`.+* Default/fallback advertised version bumped to `2025-11-25`.+ ## 0.1.0.19 - ???  * Improve handler code generated by TemplateHaskell functions in `MCP.Server.Derive`:
README.md view
@@ -4,11 +4,11 @@  ## Features -- **Complete MCP Implementation**: Supports MCP 2025-06-18 specification+- **Complete MCP Implementation**: Negotiates MCP protocol revisions `2024-11-05` through `2025-11-25` (the shared wire format for tool/resource/prompt operations) - **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-- **Multiple Transports**: STDIO and HTTP Streaming transport (MCP 2025-06-18 Streamable HTTP)+- **Multiple Transports**: STDIO and HTTP Streaming transport (MCP Streamable HTTP)  ## Supported MCP Features @@ -41,18 +41,19 @@ data MyResource = Menu | Specials data MyTool = Search { query :: Text } | Order { item :: Text } --- Implement handlers-handlePrompt :: MyPrompt -> IO Content-handlePrompt (Recipe idea) = pure $ ContentText $ "Recipe for " <> idea-handlePrompt (Shopping items) = pure $ ContentText $ "Shopping list: " <> items+-- Implement handlers. Every handler receives the per-request 'ClientContext'+-- (the caller's bearer token and principal on the HTTP transport) first.+handlePrompt :: ClientContext -> MyPrompt -> IO Content+handlePrompt _ (Recipe idea) = pure $ ContentText $ "Recipe for " <> idea+handlePrompt _ (Shopping items) = pure $ ContentText $ "Shopping list: " <> items -handleResource :: MyResource -> IO Content-handleResource Menu = pure $ ContentText "Today's menu..."-handleResource Specials = pure $ ContentText "Daily specials..."+handleResource :: ClientContext -> URI -> MyResource -> IO ResourceContent+handleResource _ uri Menu = pure $ ResourceText uri "text/plain" "Today's menu..."+handleResource _ uri Specials = pure $ ResourceText uri "text/plain" "Daily specials..." -handleTool :: MyTool -> IO Content-handleTool (Search query) = pure $ ContentText $ "Search results for " <> query-handleTool (Order item) = pure $ ContentText $ "Ordered " <> item+handleTool :: ClientContext -> MyTool -> IO Content+handleTool _ (Search query) = pure $ ContentText $ "Search results for " <> query+handleTool _ (Order item) = pure $ ContentText $ "Ordered " <> item  -- Derive handlers automatically main :: IO ()@@ -163,9 +164,10 @@ ```haskell import MCP.Server --- Manual handler implementation-promptListHandler :: IO [PromptDefinition]-promptGetHandler :: PromptName -> [(ArgumentName, ArgumentValue)] -> IO (Either Error Content)+-- Manual handler implementation. Every handler receives the per-request+-- 'ClientContext' as its first argument.+promptListHandler :: ClientContext -> IO [PromptDefinition]+promptGetHandler :: ClientContext -> PromptName -> [(ArgumentName, ArgumentValue)] -> IO (Either Error Content) -- ... implement your custom logic  main :: IO ()@@ -180,7 +182,8 @@  ## HTTP Transport (NEW!) -The library now supports MCP 2025-06-18 Streamable HTTP transport:+The library supports the MCP Streamable HTTP transport. Compile your+executable with `ghc-options: -threaded` — Warp requires the threaded runtime:  ```haskell import MCP.Server.Transport.Http@@ -195,15 +198,33 @@       { httpPort = 8080       , httpHost = "0.0.0.0"       , httpEndpoint = "/api/mcp"-      , httpVerbose = True  -- Enable detailed logging+      , httpVerbose = True     -- Enable detailed logging+      , httpAuthorize = Nothing -- No authentication (see below)       } ``` +**Bearer-token authentication** (optional): supply an `httpAuthorize` callback+to validate the `Authorization: Bearer` token each request presents. Return+`Just principal` to authorize (the principal — any JSON `Value`, e.g. a role —+reaches your handlers as `clientPrincipal` in the `ClientContext`), or+`Nothing` to reject the request with 401. Token policy lives entirely in your+application; the library only threads the identity through:++```haskell+    customConfig = defaultHttpConfig+      { httpAuthorize = Just $ \mtoken -> case mtoken of+          Just "secret-admin-token" -> pure $ Just (String "admin")+          Just "secret-user-token"  -> pure $ Just (String "user")+          _                         -> pure Nothing+      }+```+ **Features:** - CORS enabled for web clients - GET `/mcp` for server discovery - POST `/mcp` for JSON-RPC messages-- Full MCP 2025-06-18 compliance+- Protocol-version negotiation across supported revisions (`2024-11-05`–`2025-11-25`)+- Optional pluggable bearer-token authentication via `httpAuthorize`  ## Examples @@ -245,7 +266,7 @@  ## Documentation -- [MCP Specification](https://modelcontextprotocol.io/specification/2025-06-18/)+- [MCP Specification](https://modelcontextprotocol.io/specification/2025-11-25/) - [API Documentation](https://hackage.haskell.org/package/mcp-server) - [Examples](examples/) 
examples/Complete/Main.hs view
@@ -10,28 +10,28 @@  -- High-level handler functions -handlePrompt :: MyPrompt -> IO Content-handlePrompt (Recipe idea) =+handlePrompt :: ClientContext -> MyPrompt -> IO Content+handlePrompt _ (Recipe idea) =     pure $ ContentText $ "Recipe prompt for " <> idea <> ": Start by gathering fresh ingredients..."-handlePrompt (Shopping description) =+handlePrompt _ (Shopping description) =     pure $ ContentText $ "Shopping prompt for " <> description <> ": Create a detailed shopping list..." -handleResource :: URI -> MyResource -> IO ResourceContent-handleResource uri ProductCategories =+handleResource :: ClientContext -> URI -> MyResource -> IO ResourceContent+handleResource _ uri ProductCategories =     pure $ ResourceText uri "text/plain" "Fresh Produce, Dairy, Bakery, Meat & Seafood, Frozen Foods"-handleResource uri SaleItems =+handleResource _ uri SaleItems =     pure $ ResourceText uri "text/plain" "Organic Apples $2.99/lb, Free Range Eggs $4.50/dozen, Artisan Bread $3.25/loaf"-handleResource uri HeadlineBannerAd =+handleResource _ uri HeadlineBannerAd =     pure $ ResourceText uri "text/plain" "🛒 Weekly Special: 20% off all organic produce! 🥕🥬🍎" -handleTool :: MyTool -> IO Content-handleTool (SearchForProduct q category) =+handleTool :: ClientContext -> MyTool -> IO Content+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!"-handleTool (ComplexTool field1 field2 field3 field4 field5) =+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!"+handleTool _ (ComplexTool field1 field2 field3 field4 field5) =     pure $ ContentText $ "Complex tool called with: " <> field1 <> ", " <> field2 <>                         maybe "" (", " <>) field3 <> ", " <> field4 <>                         maybe "" (", " <>) field5
examples/HttpSimple/Main.hs view
@@ -16,13 +16,13 @@     -- Create a simple in-memory store     store <- newIORef [] -    let handleTool :: SimpleTool -> IO Content-        handleTool (GetValue k) = do+    let handleTool :: ClientContext -> SimpleTool -> IO Content+        handleTool _ (GetValue k) = do             pairs <- readIORef store             case lookup k pairs of                 Nothing -> pure $ ContentText $ "Key '" <> k <> "' not found"                 Just v  -> pure $ ContentText v-        handleTool (SetValue k v) = do+        handleTool _ (SetValue k v) = do             pairs <- readIORef store             let newPairs = (k, v) : filter ((/= k) . fst) pairs             writeIORef store newPairs
examples/Simple/Main.hs view
@@ -16,13 +16,13 @@     -- Create a simple in-memory store     store <- newIORef [] -    let handleTool :: SimpleTool -> IO Content-        handleTool (GetValue k) = do+    let handleTool :: ClientContext -> SimpleTool -> IO Content+        handleTool _ (GetValue k) = do             pairs <- readIORef store             case lookup k pairs of                 Nothing -> pure $ ContentText $ "Key '" <> k <> "' not found"                 Just v  -> pure $ ContentText v-        handleTool (SetValue k v) = do+        handleTool _ (SetValue k v) = do             pairs <- readIORef store             let newPairs = (k, v) : filter ((/= k) . fst) pairs             writeIORef store newPairs
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.20+version: 0.1.0.21 -- A short (one-line) description of the package. synopsis: Library for building Model Context Protocol (MCP) servers -- A longer description of the package.@@ -156,6 +156,8 @@    -- Directories containing source files.   hs-source-dirs: examples/HttpSimple+  -- Warp requires the threaded RTS (its timeout manager needs it).+  ghc-options: -threaded   -- Base language which the package is written in.   default-language: GHC2021 
src/MCP/Server/Derive.hs view
@@ -106,13 +106,13 @@       promptDefs <- traverse (mkPromptDefWithDescription descriptions) constructors        -- Generate list handler-      listHandlerExp <- [| pure $(return $ ListE promptDefs) |]+      listHandlerExp <- [| \_ctx -> pure $(return $ ListE promptDefs) |]        -- Generate get handler with cases       cases <- traverse (mkDispatchCase handlerName) constructors       defaultCase <- [| pure $ Left $ InvalidPromptName $ "Unknown prompt: " <> name |]       let defaultMatch = Match WildP (NormalB defaultCase) []-      let getHandlerExp = LamE [VarP (mkName "name"), VarP (mkName "args")] $+      let getHandlerExp = LamE [VarP (mkName "ctx"), VarP (mkName "name"), VarP (mkName "args")] $             CaseE (AppE (VarE 'T.unpack) (VarE (mkName "name")))               (map clauseToMatch cases ++ [defaultMatch]) @@ -182,7 +182,7 @@   body <- case con of     NormalC _ [] ->       [| do-          content <- $(varE handlerName) $(conE name)+          content <- $(varE handlerName) $(varE (mkName "ctx")) $(conE name)           pure $ Right content |]     RecC _ fields ->       mkRecordCase name handlerName fields@@ -203,7 +203,7 @@         paramConstructorApp <- buildParameterConstructor paramType fieldVars         let outerConstructorApp = AppE (ConE outerConName) paramConstructorApp         [| do-            content <- $(varE handlerName) $(return outerConstructorApp)+            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) |]@@ -212,14 +212,14 @@ mkRecordCase recConName handlerName fields = do   case fields of     [] -> [| do-        content <- $(varE handlerName) $(conE recConName)+        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) $(return constructorApp)+                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) |]@@ -316,14 +316,14 @@     TyConI (DataD _ _ _ _ constructors _) -> do       -- Generate resource definitions       resourceDefs <- traverse (mkResourceDefWithDescription descriptions) constructors-      listHandlerExp <- [| pure $(return $ ListE resourceDefs) |]+      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 "uri")] $+      let readHandlerExp = LamE [VarP (mkName "ctx"), VarP (mkName "uri")] $             CaseE (AppE (VarE 'show) (VarE (mkName "uri")))               (map clauseToMatch cases ++ [defaultMatch]) @@ -363,7 +363,7 @@   let resourceName = T.pack . snakeName $ name   let resourceURI = "resource://" <> T.unpack resourceName   clause [litP $ stringL resourceURI]-    (normalB [| Right <$> $(varE handlerName) $(varE (mkName "uri")) $(conE name) |])+    (normalB [| Right <$> $(varE handlerName) $(varE (mkName "ctx")) $(varE (mkName "uri")) $(conE name) |])     [] mkResourceCase _ _ = fail "Unsupported constructor type for resources" @@ -383,13 +383,13 @@       -- Generate tool definitions       toolDefs <- traverse (mkToolDefWithDescription descriptions) constructors -      listHandlerExp <- [| pure $(return $ ListE toolDefs) |]+      listHandlerExp <- [| \_ctx -> pure $(return $ ListE toolDefs) |]        -- Generate call handler with cases       cases <- traverse (mkDispatchCase handlerName) constructors       defaultCase <- [| pure $ Left $ UnknownTool $ "Unknown tool: " <> name |]       let defaultMatch = Match WildP (NormalB defaultCase) []-      let callHandlerExp = LamE [VarP (mkName "name"), VarP (mkName "args")] $+      let callHandlerExp = LamE [VarP (mkName "ctx"), VarP (mkName "name"), VarP (mkName "args")] $             CaseE (AppE (VarE 'T.unpack) (VarE (mkName "name")))               (map clauseToMatch cases ++ [defaultMatch]) 
src/MCP/Server/Handlers.hs view
@@ -64,25 +64,26 @@ -- with another protocol version it supports." validateProtocolVersion :: Text -> Either Text Text validateProtocolVersion clientVersion-  | clientVersion == protocolVersion = Right protocolVersion  -- Exact match-  | otherwise = Right protocolVersion  -- Negotiate: return server's supported version+  | 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+                 -> ClientContext                  -> JsonRpcMessage                  -> m (Maybe JsonRpcMessage)-handleMcpMessage serverInfo handlers (JsonRpcMessageRequest req) = do+handleMcpMessage serverInfo handlers ctx (JsonRpcMessageRequest req) = do   response <- case requestMethod req of-    "initialize" -> handleInitialize serverInfo req+    "initialize" -> handleInitialize serverInfo handlers req     "ping" -> handlePing req-    "prompts/list" -> handlePromptsList handlers req-    "prompts/get" -> handlePromptsGet handlers req-    "resources/list" -> handleResourcesList handlers req-    "resources/read" -> handleResourcesRead handlers req-    "tools/list" -> handleToolsList handlers req-    "tools/call" -> handleToolsCall 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+    "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@@ -90,7 +91,7 @@       }   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"@@ -100,12 +101,12 @@       return ()   return Nothing -handleMcpMessage _ _ (JsonRpcMessageResponse _) =+handleMcpMessage _ _ _ (JsonRpcMessageResponse _) =   return Nothing  -- | Handle initialize request-handleInitialize :: (MonadIO m) => McpServerInfo -> JsonRpcRequest -> m JsonRpcResponse-handleInitialize serverInfo req = do+handleInitialize :: (MonadIO m) => McpServerInfo -> McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse+handleInitialize serverInfo handlers req = do   case requestParams req of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError       { errorCode = -32602@@ -130,11 +131,14 @@               }             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 = Just $ PromptCapabilities { promptListChanged = Nothing }-                    , capabilityResources = Just $ ResourceCapabilities { resourceSubscribe = Nothing, resourceListChanged = Nothing }-                    , capabilityTools = Just $ ToolCapabilities { toolListChanged = Nothing }-                    , capabilityLogging = Nothing  -- Not supported yet+                    { 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                     }               let response = InitializeResponse                     { initRespProtocolVersion = negotiatedVersion@@ -148,8 +152,8 @@ handlePing req = return $ makeSuccessResponse (requestId req) (toJSON PongResponse)  -- | Handle prompts/list request-handlePromptsList :: (MonadIO m) => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse-handlePromptsList handlers req =+handlePromptsList :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handlePromptsList handlers ctx req =   case prompts handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError       { errorCode = -32601@@ -157,15 +161,15 @@       , errorData = Nothing       }     Just (listHandler, _) -> do-      promptsList <- listHandler+      promptsList <- listHandler ctx       let response = PromptsListResponse             { promptsListPrompts = promptsList             }       return $ makeSuccessResponse (requestId req) (toJSON response)  -- | Handle prompts/get request-handlePromptsGet :: (MonadIO m) => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse-handlePromptsGet handlers req =+handlePromptsGet :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handlePromptsGet handlers ctx req =   case prompts handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError       { errorCode = -32601@@ -188,7 +192,7 @@               }             Success getReq -> do               let args = maybe [] (map (\(k, v) -> (k, jsonValueToText v)) . Map.toList) (promptsGetArguments getReq)-              result <- getHandler (promptsGetName getReq) args+              result <- getHandler ctx (promptsGetName getReq) args               case result of                 Left err -> return $ makeErrorResponse (requestId req) $ JsonRpcError                   { errorCode = errorCodeFromMcpError err@@ -204,8 +208,8 @@                   return $ makeSuccessResponse (requestId req) (toJSON response)  -- | Handle resources/list request-handleResourcesList :: (MonadIO m) => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse-handleResourcesList handlers req =+handleResourcesList :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handleResourcesList handlers ctx req =   case resources handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError       { errorCode = -32601@@ -213,15 +217,15 @@       , errorData = Nothing       }     Just (listHandler, _) -> do-      resourcesList <- listHandler+      resourcesList <- listHandler ctx       let response = ResourcesListResponse             { resourcesListResources = resourcesList             }       return $ makeSuccessResponse (requestId req) (toJSON response)  -- | Handle resources/read request-handleResourcesRead :: (MonadIO m) => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse-handleResourcesRead handlers req =+handleResourcesRead :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handleResourcesRead handlers ctx req =   case resources handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError       { errorCode = -32601@@ -243,7 +247,7 @@               , errorData = Nothing               }             Success readReq -> do-              result <- readHandler (resourcesReadUri readReq)+              result <- readHandler ctx (resourcesReadUri readReq)               case result of                 Left err -> return $ makeErrorResponse (requestId req) $ JsonRpcError                   { errorCode = errorCodeFromMcpError err@@ -257,8 +261,8 @@                   return $ makeSuccessResponse (requestId req) (toJSON response)  -- | Handle tools/list request-handleToolsList :: (MonadIO m) => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse-handleToolsList handlers req =+handleToolsList :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handleToolsList handlers ctx req =   case tools handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError       { errorCode = -32601@@ -266,15 +270,15 @@       , errorData = Nothing       }     Just (listHandler, _) -> do-      toolsList <- listHandler+      toolsList <- listHandler ctx       let response = ToolsListResponse             { toolsListTools = toolsList             }       return $ makeSuccessResponse (requestId req) (toJSON response)  -- | Handle tools/call request-handleToolsCall :: (MonadIO m) => McpServerHandlers m -> JsonRpcRequest -> m JsonRpcResponse-handleToolsCall handlers req =+handleToolsCall :: (MonadIO m) => McpServerHandlers m -> ClientContext -> JsonRpcRequest -> m JsonRpcResponse+handleToolsCall handlers ctx req =   case tools handlers of     Nothing -> return $ makeErrorResponse (requestId req) $ JsonRpcError       { errorCode = -32601@@ -297,7 +301,7 @@               }             Success callReq -> do               let args = maybe [] (map (\(k, v) -> (k, jsonValueToText v)) . Map.toList) (toolsCallArguments callReq)-              result <- callHandler (toolsCallName callReq) args+              result <- callHandler ctx (toolsCallName callReq) args               case result of                 Left err -> return $ makeErrorResponse (requestId req) $ JsonRpcError                   { errorCode = errorCodeFromMcpError err
src/MCP/Server/Protocol.hs view
@@ -34,6 +34,7 @@      -- * Protocol Functions   , protocolVersion+  , supportedVersions   ) where  import           Data.Aeson@@ -42,8 +43,25 @@ 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. protocolVersion :: Text-protocolVersion = "2025-06-18"+protocolVersion = "2025-11-25"++-- | Date-versioned MCP 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+-- requirement that a supported version be answered with the same version.+--+-- Ordered newest-first.+supportedVersions :: [Text]+supportedVersions =+  [ "2025-11-25"+  , "2025-06-18"+  , "2025-03-26"+  , "2024-11-05"+  ]   -- | Initialize request
src/MCP/Server/Transport/Http.hs view
@@ -10,11 +10,13 @@  import           Control.Monad            (when) import           Data.Aeson+import qualified Data.Aeson.KeyMap        as KM import qualified Data.ByteString.Lazy     as BSL import           Data.String              (IsString (fromString)) import           Data.Text                (Text) import qualified Data.Text                as T import qualified Data.Text.Encoding       as TE+import           Data.Text.Encoding.Error (lenientDecode) import           Network.HTTP.Types import qualified Network.Wai              as Wai import qualified Network.Wai.Handler.Warp as Warp@@ -22,23 +24,37 @@  import           MCP.Server.Handlers import           MCP.Server.JsonRpc+import           MCP.Server.Protocol (protocolVersion, supportedVersions) import           MCP.Server.Types --- | HTTP transport configuration following MCP 2025-06-18 Streamable HTTP specification+-- | HTTP transport configuration following the MCP Streamable HTTP specification+--+-- Note: 'HttpConfig' has no 'Show'/'Eq' instances because 'httpAuthorize' is a+-- function. data HttpConfig = HttpConfig-  { httpPort     :: Int      -- ^ Port to listen on-  , httpHost     :: String   -- ^ Host to bind to (default "localhost")-  , httpEndpoint :: String   -- ^ MCP endpoint path (default "/mcp")-  , httpVerbose  :: Bool     -- ^ Enable verbose logging (default False)-  } deriving (Show, Eq)+  { httpPort      :: Int      -- ^ Port to listen on+  , httpHost      :: String   -- ^ Host to bind to (default "localhost")+  , httpEndpoint  :: String   -- ^ MCP endpoint path (default "/mcp")+  , httpVerbose   :: Bool     -- ^ Enable verbose logging (default False)+  , httpAuthorize :: Maybe (Maybe Text -> IO (Maybe Value))+      -- ^ Optional authorization callback. 'Nothing' disables authentication.+      --   When @'Just' check@, the bearer token presented by each request (or+      --   'Nothing' when absent / not a Bearer credential) is passed to+      --   @check@, which returns the caller's principal: @'Just' principal@+      --   authorizes the request — the principal (e.g. a role) is placed in the+      --   handler 'ClientContext' as 'clientPrincipal' — while 'Nothing' rejects+      --   the request with @401@. Validation and principal assignment are left+      --   entirely to the caller.+  } --- | Default HTTP configuration+-- | Default HTTP configuration (authentication disabled). defaultHttpConfig :: HttpConfig defaultHttpConfig = HttpConfig   { httpPort = 3000   , httpHost = "localhost"   , httpEndpoint = "/mcp"   , httpVerbose = False+  , httpAuthorize = Nothing   }  -- | Helper for conditional logging@@ -62,30 +78,58 @@   -- Log the request   logVerbose config $ "HTTP " ++ show (Wai.requestMethod req) ++ " " ++ T.unpack (TE.decodeUtf8 $ Wai.rawPathInfo req) -  -- Check if this is our MCP endpoint-  if TE.decodeUtf8 (Wai.rawPathInfo req) == T.pack (httpEndpoint config)-    then handleMcpRequest config serverInfo handlers req respond-    else respond $ Wai.responseLBS status404 [("Content-Type", "text/plain")] "Not Found"+  -- 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" +-- | 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.+bearerToken :: Wai.Request -> Maybe Text+bearerToken req = do+  header <- lookup hAuthorization (Wai.requestHeaders req)+  let (scheme, rest) = T.break (== ' ') (TE.decodeUtf8With lenientDecode header)+  if T.toCaseFold scheme == "bearer" && not (T.null rest)+    then Just (T.stripStart rest)+    else Nothing+ -- | Handle MCP requests according to Streamable HTTP specification-handleMcpRequest :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> Wai.Request -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived-handleMcpRequest config serverInfo handlers req respond = do-  -- Check for mandatory MCP-Protocol-Version header (2025-06-18 requirement)-  case lookup "MCP-Protocol-Version" (Wai.requestHeaders req) of-    Nothing -> do-      logVerbose config "Request rejected: Missing MCP-Protocol-Version header"+handleMcpRequest :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> 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.+  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" .= ("Missing required MCP-Protocol-Version header" :: Text)])-    Just headerValue ->-      if TE.decodeUtf8 headerValue /= "2025-06-18" then do-        logVerbose config $ "Request rejected: Invalid protocol version: " ++ show headerValue-        respond $ Wai.responseLBS-          status400-          [("Content-Type", "application/json")]-          (encode $ object ["error" .= ("Unsupported protocol version. Server only supports 2025-06-18" :: Text)])-      else+        (encode $ object ["error" .= ("Unsupported protocol version. Supported versions: " <> T.intercalate ", " supportedVersions)])+    else         case Wai.requestMethod req of           -- GET requests for endpoint discovery           "GET" -> do@@ -93,7 +137,7 @@                   [ "name" .= serverName serverInfo                   , "version" .= serverVersion serverInfo                   , "description" .= serverInstructions serverInfo-                  , "protocolVersion" .= ("2025-06-18" :: Text)+                  , "protocolVersion" .= protocolVersion                   , "capabilities" .= object                       [ "tools" .= object []                       , "prompts" .= object []@@ -108,17 +152,15 @@            -- POST requests for JSON-RPC messages           "POST" -> do-            -- Read request body-            body <- Wai.strictRequestBody req             logVerbose config $ "Received POST body (" ++ show (BSL.length body) ++ " bytes): " ++ take 200 (show body)-            handleJsonRpcRequest config serverInfo handlers body respond+            handleJsonRpcRequest config serverInfo handlers ctx body respond            -- 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, MCP-Protocol-Version")+            , ("Access-Control-Allow-Headers", "Content-Type, Authorization, MCP-Protocol-Version")             ]             "" @@ -128,9 +170,26 @@             [("Content-Type", "text/plain"), ("Allow", "GET, POST, OPTIONS")]             "Method Not Allowed" +-- | 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++-- | 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+ -- | Handle JSON-RPC request from HTTP body-handleJsonRpcRequest :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> BSL.ByteString -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived-handleJsonRpcRequest config serverInfo handlers body respond = do+handleJsonRpcRequest :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> ClientContext -> BSL.ByteString -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived+handleJsonRpcRequest config serverInfo handlers ctx body respond = do   case eitherDecode body of     Left err -> do       hPutStrLn stderr $ "JSON parse error: " ++ err@@ -139,11 +198,11 @@         [("Content-Type", "application/json")]         (encode $ object ["error" .= ("Invalid JSON" :: Text)]) -    Right jsonValue -> handleSingleJsonRpc config serverInfo handlers jsonValue respond+    Right jsonValue -> handleSingleJsonRpc config serverInfo handlers ctx jsonValue respond  -- | Handle a single JSON-RPC message-handleSingleJsonRpc :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> Value -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived-handleSingleJsonRpc config serverInfo handlers jsonValue respond = do+handleSingleJsonRpc :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> ClientContext -> Value -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived+handleSingleJsonRpc config serverInfo handlers ctx jsonValue respond = do   case parseJsonRpcMessage jsonValue of     Left err -> do       hPutStrLn stderr $ "JSON-RPC parse error: " ++ err@@ -154,7 +213,7 @@      Right message -> do       logVerbose config $ "Processing HTTP message: " ++ show (getMessageSummary message)-      maybeResponse <- handleMcpMessage serverInfo handlers message+      maybeResponse <- handleMcpMessage serverInfo handlers ctx message        case maybeResponse of         Just responseMsg -> do
src/MCP/Server/Transport/Stdio.hs view
@@ -43,7 +43,7 @@               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 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))
src/MCP/Server/Types.hs view
@@ -26,6 +26,7 @@     -- * Server Types   , McpServerInfo(..)   , McpServerHandlers(..)+  , ClientContext(..)   , ServerCapabilities(..)   , PromptCapabilities(..)   , ResourceCapabilities(..)@@ -338,15 +339,26 @@     ]  --- | Handler type definitions-type PromptListHandler m = m [PromptDefinition]-type PromptGetHandler m = PromptName -> [(ArgumentName, ArgumentValue)] -> m (Either Error Content)+-- | Per-request context passed to every handler, so handlers can behave+-- differently depending on who is calling.+data ClientContext = ClientContext+  { clientToken     :: Maybe Text   -- ^ Authenticated bearer token, if any.+  , clientPrincipal :: Maybe Value  -- ^ Application-defined principal returned by+                                    --   the transport's authorization callback+                                    --   (e.g. a role). 'Nothing' when+                                    --   authentication is disabled.+  } deriving (Show, Eq) -type ResourceListHandler m = m [ResourceDefinition]-type ResourceReadHandler m = URI -> m (Either Error ResourceContent)+-- | 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) -type ToolListHandler m = m [ToolDefinition]-type ToolCallHandler m = ToolName -> [(ArgumentName, ArgumentValue)] -> m (Either Error Content)+type ResourceListHandler m = ClientContext -> m [ResourceDefinition]+type ResourceReadHandler m = ClientContext -> URI -> m (Either Error ResourceContent)++type ToolListHandler m = ClientContext -> m [ToolDefinition]+type ToolCallHandler m = ClientContext -> ToolName -> [(ArgumentName, ArgumentValue)] -> m (Either Error Content)  -- | Server handlers data McpServerHandlers m = McpServerHandlers
test/Spec/AdvancedDerivation.hs view
@@ -42,7 +42,7 @@  assertToolCallResult :: (ToolCallHandler IO) -> Text -> [(Text, Text)] -> Text -> IO () assertToolCallResult handler toolName args expectedContent = do-  result <- handler toolName args+  result <- handler anonCtx toolName args   case result of     Right (ContentText content) -> content `shouldBe` expectedContent     other -> expectationFailure $ "Expected ContentText but got: " ++ show other@@ -66,7 +66,7 @@   describe "Separate Parameter Types" $ do     it "generates correct schema for separate parameter tools" $ do       let (listHandler, _) = testSeparateParamsToolHandlers-      toolDefs <- listHandler+      toolDefs <- listHandler anonCtx        -- Test GetValue tool schema       let getValueDef = findToolByName "get_value" toolDefs@@ -84,7 +84,7 @@   describe "Recursive Parameter Types" $ do     it "generates correct schema for recursive parameter tools" $ do       let (listHandler, _) = testRecursiveToolHandlers-      toolDefs <- listHandler+      toolDefs <- listHandler anonCtx        let processDataDef = findToolByName "process_data" toolDefs       assertSchemaHasProperties ["_ipName", "_ipAge"] processDataDef@@ -97,7 +97,7 @@   describe "Custom Descriptions with Separate Parameters" $ do     it "applies correct tool descriptions for separate parameter tools" $ do       let (listHandler, _) = testSeparateParamsToolHandlersWithDescriptions-      toolDefs <- listHandler+      toolDefs <- listHandler anonCtx        let getValueDef = findToolByName "get_value" toolDefs       assertToolHasDescription "get_value" "Retrieves a value from the key-value store" getValueDef@@ -107,7 +107,7 @@      it "applies correct field descriptions for separate parameter tools" $ do       let (listHandler, _) = testSeparateParamsToolHandlersWithDescriptions-      toolDefs <- listHandler+      toolDefs <- listHandler anonCtx        let getValueDef = findToolByName "get_value" toolDefs       assertPropertyHasDescription "_gvpKey" "The key to retrieve the value for" getValueDef@@ -119,14 +119,14 @@   describe "Recursive Tool Descriptions" $ do     it "applies correct descriptions for recursive parameter tools" $ do       let (listHandler, _) = testRecursiveToolHandlersWithDescriptions-      toolDefs <- listHandler+      toolDefs <- listHandler anonCtx        let processDataDef = findToolByName "process_data" toolDefs       assertToolHasDescription "process_data" "Processes user data with age validation" processDataDef      it "applies correct field descriptions for recursive parameters" $ do       let (listHandler, _) = testRecursiveToolHandlersWithDescriptions-      toolDefs <- listHandler+      toolDefs <- listHandler anonCtx        let processDataDef = findToolByName "process_data" toolDefs       assertPropertyHasDescription "_ipName" "The person's full name" processDataDef
test/Spec/BasicDerivation.hs view
@@ -40,13 +40,13 @@  testPromptCall :: PromptGetHandler IO -> Text -> [(Text, Text)] -> Text -> IO () testPromptCall handler name args expected =-  shouldReturnContentText (handler name args) expected+  shouldReturnContentText (handler anonCtx name args) expected  testResourceCall :: ResourceReadHandler IO -> String -> Text -> IO () testResourceCall handler uriString expected = do   case parseURI uriString of     Just uri -> do-      result <- handler uri+      result <- handler anonCtx uri       case result of         Right (ResourceText _ _ content) -> content `shouldBe` expected         Right (ResourceBlob _ _ _) -> expectationFailure "Expected ResourceText but got ResourceBlob"@@ -55,7 +55,7 @@  testToolCall :: ToolCallHandler IO -> Text -> [(Text, Text)] -> Text -> IO () testToolCall handler name args expected =-  shouldReturnContentText (handler name args) expected+  shouldReturnContentText (handler anonCtx name args) expected  spec :: Spec spec = describe "Basic Template Haskell Derivation" $ do@@ -76,7 +76,7 @@           Just uri ->             if useSubstringMatch testCase               then do-                result <- readHandler uri+                result <- readHandler anonCtx uri                 case result of                   Right (ResourceText _ _ content) ->                     T.isInfixOf (resourceExpectedContent testCase) content `shouldBe` True
test/Spec/ProtocolVersionNegotiation.hs view
@@ -4,30 +4,25 @@  import Data.Aeson (Value(..), object, (.=)) import qualified Data.Aeson.KeyMap as KM+import Data.Text (Text) import Test.Hspec  import MCP.Server.Handlers (handleInitialize) import MCP.Server.JsonRpc (JsonRpcRequest(..), RequestId(..), JsonRpcResponse(..), JsonRpcError(..))-import MCP.Server.Types (McpServerInfo(..))+import MCP.Server.Protocol (protocolVersion)+import MCP.Server.Types (Error(..), McpServerHandlers(..), McpServerInfo(..)) --- | Test that server performs proper version negotiation according to MCP spec+-- | Test that server performs proper version negotiation according to MCP spec. -- -- From the 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." ----- This means if a client sends protocolVersion "2025-11-25" and the server--- only supports "2025-06-18", the server should respond with:--- {---   "jsonrpc": "2.0",---   "id": 0,---   "result": {---     "protocolVersion": "2025-06-18",---     ...---   }--- }------ NOT with an error like: {"jsonrpc":"2.0","id":0,"error":{"code":-32602,...}}+-- The wire format for the basic tool/resource/prompt operations this library+-- implements is unchanged across the date-versioned revisions 2024-11-05,+-- 2025-03-26, 2025-06-18 and 2025-11-25, so the server echoes back any of+-- these the client proposes. Unknown versions fall back to the server's own+-- 'protocolVersion'. spec :: Spec spec = describe "Protocol Version Negotiation" $ do   let testServerInfo = McpServerInfo@@ -36,79 +31,79 @@         , serverInstructions = "Test server for version negotiation"         } -  it "Server should respond with supported version when client requests unsupported version" $ do-    -- Create an initialize request with a newer protocol version-    -- that the library doesn't support-    let params = object-          [ "protocolVersion" .= String "2025-11-25"  -- Newer than library supports-          , "capabilities" .= object []-          , "clientInfo" .= object-              [ "name" .= String "test-client"-              , "version" .= String "1.0.0"+  -- 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 []+                       , \_ctx name _args -> pure (Left (UnknownTool name))+                       )+        }++  let initialize :: Text -> IO JsonRpcResponse+      initialize clientVersion = do+        let params = object+              [ "protocolVersion" .= String clientVersion+              , "capabilities" .= object []+              , "clientInfo" .= object+                  [ "name" .= String "test-client"+                  , "version" .= String "1.0.0"+                  ]               ]-          ]-        request = JsonRpcRequest-          { requestJsonrpc = "2.0"-          , requestId = RequestIdNumber 0-          , requestMethod = "initialize"-          , requestParams = Just params-          }+            request = JsonRpcRequest+              { requestJsonrpc = "2.0"+              , requestId = RequestIdNumber 0+              , requestMethod = "initialize"+              , requestParams = Just params+              }+        handleInitialize testServerInfo testHandlers request -    -- Call handleInitialize directly (it runs in IO monad)-    response <- handleInitialize testServerInfo request+  -- Issue an initialize request proposing the given protocol version and+  -- return the negotiated version from the server's (non-error) response.+  let negotiate :: Text -> IO Text+      negotiate clientVersion = do+        response <- initialize clientVersion+        case responseError response of+          Just err ->+            error $ "Server returned error instead of negotiating version. "+                 ++ "Per MCP spec, server MUST respond with a supported version, "+                 ++ "not an error. Error was: " ++ show (errorMessage err)+          Nothing -> case responseResult response of+            Nothing -> error "Response has no result"+            Just (Object result) -> case KM.lookup "protocolVersion" result of+              Just (String version) -> pure version+              Just other -> error $ "protocolVersion is not a string: " ++ show other+              Nothing -> error "Response missing protocolVersion"+            Just other -> error $ "Result is not an object: " ++ show other -    -- Check if it's an error response-    case responseError response of-      Just err -> do-        -- This is the bug! Server returned an error instead of negotiating-        expectationFailure $-          "Server returned error instead of negotiating version. " ++-          "Per MCP spec, server MUST respond with a supported version, " ++-          "not an error. Error was: " ++ show (errorMessage err)-      Nothing -> do-        -- Good! Now verify the result has protocolVersion-        case responseResult response of-          Nothing -> expectationFailure "Response has no result"-          Just (Object result) -> do-            case KM.lookup "protocolVersion" result of-              Just (String version) -> do-                -- The server should have responded with its supported version-                -- We expect "2025-06-18" based on the library-                version `shouldBe` "2025-06-18"-              Just other -> expectationFailure $ "protocolVersion is not a string: " ++ show other-              Nothing -> expectationFailure "Response missing protocolVersion"-          Just other -> expectationFailure $ "Result is not an object: " ++ show other+  it "echoes back the newest revision (2025-11-25) when the client proposes it" $+    negotiate "2025-11-25" `shouldReturn` "2025-11-25" -  it "Server should respond with same version when client requests supported version" $ do-    -- This test verifies the happy path: client requests "2025-06-18"-    -- and server responds with "2025-06-18"-    let params = object-          [ "protocolVersion" .= String "2025-06-18"  -- Library's supported version-          , "capabilities" .= object []-          , "clientInfo" .= object-              [ "name" .= String "test-client"-              , "version" .= String "1.0.0"-              ]-          ]-        request = JsonRpcRequest-          { requestJsonrpc = "2.0"-          , requestId = RequestIdNumber 0-          , requestMethod = "initialize"-          , requestParams = Just params-          }+  it "echoes back 2025-06-18 when the client proposes it" $+    negotiate "2025-06-18" `shouldReturn` "2025-06-18" -    response <- handleInitialize testServerInfo request+  it "echoes back 2025-03-26 when the client proposes it" $+    negotiate "2025-03-26" `shouldReturn` "2025-03-26" -    -- Check it's not an error-    case responseError response of-      Just err -> expectationFailure $ "Unexpected error: " ++ show (errorMessage err)-      Nothing -> do-        -- Verify protocolVersion matches-        case responseResult response of-          Nothing -> expectationFailure "Response has no result"-          Just (Object result) -> do-            case KM.lookup "protocolVersion" result of-              Just (String version) -> version `shouldBe` "2025-06-18"-              Just other -> expectationFailure $ "protocolVersion is not a string: " ++ show other-              Nothing -> expectationFailure "Response missing protocolVersion"-          Just other -> expectationFailure $ "Result is not an object: " ++ show other+  -- Regression: Claude Code proposes 2024-11-05 and disconnects if it does not+  -- receive that exact version back, so tools never appear.+  it "echoes back the oldest compatible revision (2024-11-05) when the client proposes it" $+    negotiate "2024-11-05" `shouldReturn` "2024-11-05"++  it "falls back to the server's own version for an unknown/unsupported version" $+    negotiate "1999-01-01" `shouldReturn` protocolVersion++  -- Strict clients drop a server that advertises a capability and then answers+  -- the corresponding list request with an error, so only capabilities with an+  -- actual handler may be advertised.+  it "advertises only the capabilities that have handlers" $ do+    response <- initialize protocolVersion+    caps <- case responseResult response of+      Just (Object result) -> case KM.lookup "capabilities" result of+        Just (Object capsObj) -> pure capsObj+        other -> error $ "capabilities is not an object: " ++ show other+      other -> error $ "Result is not an object: " ++ show other+    KM.member "tools" caps `shouldBe` True+    KM.member "prompts" caps `shouldBe` False+    KM.member "resources" caps `shouldBe` False
test/Spec/SchemaValidation.hs view
@@ -67,7 +67,7 @@      forM_ schemaTestCases $ \testCase -> do       it (T.unpack $ schemaTestDescription testCase) $ do-        toolDefs <- listHandler+        toolDefs <- listHandler anonCtx          assertToolExists (schemaToolName testCase) toolDefs         let toolDef = findTool (schemaToolName testCase) toolDefs@@ -82,7 +82,7 @@   describe "Custom Descriptions" $ do     it "applies correct tool descriptions" $ do       let (listHandler, _) = testToolHandlersWithDescriptions-      toolDefs <- listHandler+      toolDefs <- listHandler anonCtx        assertToolExists "echo" toolDefs       let echoDef = findTool "echo" toolDefs@@ -94,7 +94,7 @@      it "applies correct field descriptions for Calculate tool" $ do       let (listHandler, _) = testToolHandlersWithDescriptions-      toolDefs <- listHandler+      toolDefs <- listHandler anonCtx        assertToolExists "calculate" toolDefs       let calculateDef = findTool "calculate" toolDefs
test/Spec/ToolCallParsing.hs view
@@ -14,7 +14,7 @@ allTypesHandlers = $(deriveToolHandler ''AllTypesTool 'handleAllTypesTool)  callTool :: Text -> [(Text, Text)] -> IO (Either Error Content)-callTool = snd allTypesHandlers+callTool = snd allTypesHandlers anonCtx  shouldBeRight :: IO (Either Error Content) -> Text -> IO () shouldBeRight action expected = do
test/Spec/UnicodeHandling.hs view
@@ -215,24 +215,26 @@               Nothing -> return $ Left $ MissingRequiredParams "expression"             _ -> return $ Left $ UnknownTool name +      -- Handlers above ignore the per-request client context+      let ctx = ClientContext Nothing Nothing       let handlers = McpServerHandlers-            { prompts = Just (promptListHandler, promptGetHandler)-            , resources = Just (resourceListHandler, resourceReadHandler)-            , tools = Just (toolListHandler, toolCallHandler)+            { prompts = Just (const promptListHandler, const promptGetHandler)+            , resources = Just (const resourceListHandler, const resourceReadHandler)+            , tools = Just (const toolListHandler, const toolCallHandler)             }        -- Test that all handlers work with Unicode-      promptList <- fst $ case prompts handlers of Just h -> h; Nothing -> error "No prompts"+      promptList <- ($ ctx) $ fst $ case prompts handlers of Just h -> h; Nothing -> error "No prompts"       promptList `shouldSatisfy` (not . null) -      resourceList <- fst $ case resources handlers of Just h -> h; Nothing -> error "No resources"+      resourceList <- ($ ctx) $ fst $ case resources handlers of Just h -> h; Nothing -> error "No resources"       resourceList `shouldSatisfy` (not . null) -      toolList <- fst $ case tools handlers of Just h -> h; Nothing -> error "No tools"+      toolList <- ($ ctx) $ fst $ case tools handlers of Just h -> h; Nothing -> error "No tools"       toolList `shouldSatisfy` (not . null)        -- Test actual Unicode handling-      promptResult <- snd (case prompts handlers of Just h -> h; Nothing -> error "No prompts") "math_formula" [("formula", "√(x²+y²)")]+      promptResult <- snd (case prompts handlers of Just h -> h; Nothing -> error "No prompts") ctx "math_formula" [("formula", "√(x²+y²)")]       case promptResult of         Right (ContentText txt) -> txt `shouldSatisfy` T.isInfixOf "√"         _ -> expectationFailure "Expected successful prompt result"@@ -240,12 +242,12 @@       uri <- case parseURI "resource://unicode_symbols" of         Just u  -> return u         Nothing -> fail "Invalid URI"-      resourceResult <- snd (case resources handlers of Just h -> h; Nothing -> error "No resources") uri+      resourceResult <- snd (case resources handlers of Just h -> h; Nothing -> error "No resources") ctx uri       case resourceResult of         Right (ResourceText _ _ txt) -> txt `shouldSatisfy` T.isInfixOf "∀∃∈∉"         _ -> expectationFailure "Expected successful resource result" -      toolResult <- snd (case tools handlers of Just h -> h; Nothing -> error "No tools") "calculate" [("expression", "∫₀^∞ e^(-x²) dx = √π/2")]+      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           txt `shouldSatisfy` T.isInfixOf "√"
test/TestTypes.hs view
@@ -4,9 +4,13 @@  import           Data.Text  (Text) import qualified Data.Text  as T-import           MCP.Server (Content(..), ResourceContent(..))+import           MCP.Server (ClientContext(..), Content(..), ResourceContent(..)) import           Network.URI (URI) +-- Context passed to handlers in tests (no transport-level identity)+anonCtx :: ClientContext+anonCtx = ClientContext Nothing Nothing+ -- Test data types for end-to-end testing data TestPrompt     = SimplePrompt { message :: Text }@@ -47,49 +51,49 @@     deriving (Show, Eq)  -- Handler functions-handleTestPrompt :: TestPrompt -> IO Content-handleTestPrompt (SimplePrompt msg) =+handleTestPrompt :: ClientContext -> TestPrompt -> IO Content+handleTestPrompt _ (SimplePrompt msg) =     pure $ ContentText $ "Simple prompt: " <> msg-handleTestPrompt (ComplexPrompt title prio urgent) =+handleTestPrompt _ (ComplexPrompt title prio urgent) =     pure $ ContentText $ "Complex prompt: " <> title <> " (priority=" <> T.pack (show prio) <> ", urgent=" <> T.pack (show urgent) <> ")"-handleTestPrompt (OptionalPrompt req opt) =+handleTestPrompt _ (OptionalPrompt req opt) =     pure $ ContentText $ "Optional prompt: " <> req <> maybe "" ((" optional=" <>) . T.pack . show) opt -handleTestResource :: URI -> TestResource -> IO ResourceContent-handleTestResource uri ConfigFile =+handleTestResource :: ClientContext -> URI -> TestResource -> IO ResourceContent+handleTestResource _ uri ConfigFile =     pure $ ResourceText uri "text/plain" "Config file contents: debug=true, timeout=30"-handleTestResource uri DatabaseConnection =+handleTestResource _ uri DatabaseConnection =     pure $ ResourceText uri "text/plain" "Database at localhost:5432"-handleTestResource uri UserProfile =+handleTestResource _ uri UserProfile =     pure $ ResourceText uri "text/plain" "User profile for ID 123" -handleTestTool :: TestTool -> IO Content-handleTestTool (Echo text) =+handleTestTool :: ClientContext -> TestTool -> IO Content+handleTestTool _ (Echo text) =     pure $ ContentText $ "Echo: " <> text-handleTestTool (Calculate op x y) =+handleTestTool _ (Calculate op x y) =     let result = case op of             "add" -> x + y             "multiply" -> x * y             "subtract" -> x - y             _ -> 0     in pure $ ContentText $ T.pack (show result)-handleTestTool (Toggle flag) =+handleTestTool _ (Toggle flag) =     pure $ ContentText $ "Flag is now: " <> T.pack (show (not flag))-handleTestTool (Search query limit caseSens) =+handleTestTool _ (Search query limit caseSens) =     pure $ ContentText $ "Search results for '" <> query <> "'" <>         maybe "" ((" (limit=" <>) . (<> ")") . T.pack . show) limit <>         maybe "" ((" (case-sensitive=" <>) . (<> ")") . T.pack . show) caseSens  -- Handler for separate params tool-handleSeparateParamsTool :: SeparateParamsTool -> IO Content-handleSeparateParamsTool (GetValue (GetValueParams key)) =+handleSeparateParamsTool :: ClientContext -> SeparateParamsTool -> IO Content+handleSeparateParamsTool _ (GetValue (GetValueParams key)) =     pure $ ContentText $ "Getting value for key: " <> key-handleSeparateParamsTool (SetValue (SetValueParams key value)) =+handleSeparateParamsTool _ (SetValue (SetValueParams key value)) =     pure $ ContentText $ "Setting " <> key <> " = " <> value  -- Handler for recursive tool-handleRecursiveTool :: RecursiveTool -> IO Content-handleRecursiveTool (ProcessData (MiddleParams (InnerParams name age))) =+handleRecursiveTool :: ClientContext -> RecursiveTool -> IO Content+handleRecursiveTool _ (ProcessData (MiddleParams (InnerParams name age))) =     pure $ ContentText $ "Processing data for " <> name <> " (age " <> T.pack (show age) <> ")"  -- Type covering all parseable field types for exhaustive parsing tests@@ -112,8 +116,8 @@         }     deriving (Show, Eq) -handleAllTypesTool :: AllTypesTool -> IO Content-handleAllTypesTool (RequiredFields t i ig d f b) =+handleAllTypesTool :: ClientContext -> AllTypesTool -> IO Content+handleAllTypesTool _ (RequiredFields t i ig d f b) =     pure $ ContentText $ T.intercalate ", "         [ "text=" <> t         , "int=" <> T.pack (show i)@@ -122,7 +126,7 @@         , "float=" <> T.pack (show f)         , "bool=" <> T.pack (show b)         ]-handleAllTypesTool (OptionalFields t i ig d f b) =+handleAllTypesTool _ (OptionalFields t i ig d f b) =     pure $ ContentText $ T.intercalate ", "         [ "text=" <> maybe "Nothing" id t         , "int=" <> maybe "Nothing" (T.pack . show) i