mcp-server-0.1.0.11: src/MCP/Server/Transport/Http.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module MCP.Server.Transport.Http
( -- * HTTP Transport
HttpTransport(..)
, HttpConfig(..)
, runMcpServerHttp
, runMcpServerHttpWithConfig
) where
import Control.Monad (when)
import Data.Aeson
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 qualified Data.Vector as V
import Network.HTTP.Types
import qualified Network.Wai as Wai
import qualified Network.Wai.Handler.Warp as Warp
import System.IO (hPutStrLn, stderr)
import MCP.Server.Handlers
import MCP.Server.JsonRpc
import MCP.Server.Types
-- | HTTP transport configuration following MCP 2025-03-26 Streamable HTTP specification
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)
-- | Default HTTP configuration
defaultHttpConfig :: HttpConfig
defaultHttpConfig = HttpConfig
{ httpPort = 3000
, httpHost = "localhost"
, httpEndpoint = "/mcp"
, httpVerbose = False
}
-- | HTTP transport following MCP 2025-03-26 Streamable HTTP specification
data HttpTransport = HttpTransport HttpConfig
deriving (Show, Eq)
-- Note: HTTP transport is constrained to IO only due to Warp requirements
-- For the generic interface, we'll use a more specific function
-- | Helper for conditional logging
logVerbose :: HttpConfig -> String -> IO ()
logVerbose config msg = when (httpVerbose config) $ hPutStrLn stderr msg
-- | Run an MCP server using HTTP transport with default config
runMcpServerHttp :: McpServerInfo -> McpServerHandlers IO -> IO ()
runMcpServerHttp = runMcpServerHttpWithConfig defaultHttpConfig
-- | Run an MCP server using HTTP transport with custom config
runMcpServerHttpWithConfig :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> IO ()
runMcpServerHttpWithConfig config serverInfo handlers = do
let settings = Warp.setHost (fromString $ httpHost config) $
Warp.setPort (httpPort config) $
Warp.defaultSettings
putStrLn $ "Starting MCP HTTP server on " ++ httpHost config ++ ":" ++ show (httpPort config) ++ httpEndpoint config
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
-- 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"
-- | 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
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" .= ("2025-03-26" :: Text)
, "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)
-- 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
-- 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")
]
""
-- Unsupported methods
_ -> respond $ Wai.responseLBS
status405
[("Content-Type", "text/plain"), ("Allow", "GET, POST, OPTIONS")]
"Method Not Allowed"
-- | Handle JSON-RPC request from HTTP body (supports batching)
handleJsonRpcRequest :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> BSL.ByteString -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
handleJsonRpcRequest config serverInfo handlers 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)])
Right jsonValue -> do
-- Try to parse as batch first (array), then as single message
case jsonValue of
Array batch -> handleJsonRpcBatch config serverInfo handlers (V.toList batch) respond
singleValue -> handleSingleJsonRpc config serverInfo handlers singleValue 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
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)])
Right message -> do
logVerbose config $ "Processing HTTP message: " ++ show (getMessageSummary message)
maybeResponse <- handleMcpMessage serverInfo handlers 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", "*")]
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", "*")]
"{}"
-- | Handle JSON-RPC batch request (MCP 2025-03-26 feature)
handleJsonRpcBatch :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> [Value] -> (Wai.Response -> IO Wai.ResponseReceived) -> IO Wai.ResponseReceived
handleJsonRpcBatch config serverInfo handlers batch respond = do
logVerbose config $ "Processing JSON-RPC batch with " ++ show (length batch) ++ " messages"
-- Process each message in the batch
responses <- mapM (processBatchMessage config serverInfo handlers) batch
-- Filter out Nothing responses (notifications)
let validResponses = [r | Just r <- responses]
case validResponses of
[] -> do
logVerbose config "Batch contained only notifications, returning empty JSON array"
respond $ Wai.responseLBS
status200
[("Content-Type", "application/json"), ("Access-Control-Allow-Origin", "*")]
"[]"
_ -> do
let responseJson = encode $ map encodeJsonRpcMessage validResponses
logVerbose config $ "Sending batch response with " ++ show (length validResponses) ++ " responses"
respond $ Wai.responseLBS
status200
[("Content-Type", "application/json"), ("Access-Control-Allow-Origin", "*")]
responseJson
-- | Process a single message from a batch
processBatchMessage :: HttpConfig -> McpServerInfo -> McpServerHandlers IO -> Value -> IO (Maybe JsonRpcMessage)
processBatchMessage config serverInfo handlers jsonValue = do
case parseJsonRpcMessage jsonValue of
Left err -> do
logVerbose config $ "Batch message parse error: " ++ err
return Nothing -- Skip invalid messages in batch
Right message -> do
logVerbose config $ "Processing batch message: " ++ show (getMessageSummary message)
handleMcpMessage serverInfo handlers message