mcp-0.3.1.0: src/MCP/Server/HTTP.hs
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TypeOperators #-}
{- |
Module: MCP.Server.HTTP
License: MPL-2.0
Maintainer: <matti@dpella.io>, <lobo@dpella.io>
Servant-based HTTP transports for the MCP server.
This module provides two HTTP transports:
* 'MCPAPI' / 'mcpAPI' — a JWT-authenticated transport using @servant-auth@.
Use this when your server is exposed to the network and you need
per-request identity.
* 'SimpleHTTPAPI' / 'simpleHttpApp' — an unauthenticated transport that
accepts all requests unconditionally. This should only be used on
@localhost@ for local development, or behind a reverse proxy (e.g.
Nginx, Envoy, Cloudflare Access) that handles authentication before
requests reach the application. Do __not__ expose it directly to the
public internet.
Both transports accept JSON-RPC requests via POST and return responses as
SSE streams at the @\/mcp@ endpoint.
-}
module MCP.Server.HTTP (
-- * JWT-authenticated API
MCPAPI,
mcpAPI,
handleMCPRequest,
handleMCPEvents,
-- * Simple (unauthenticated) API
SimpleHTTPAPI,
simpleHttpApp,
) where
import Control.Concurrent.MVar
import Data.Aeson (encode, object, (.=))
import Data.Text (Text)
import MCP.Server.Common
import MCP.Server.HTTP.Internal
import Servant
import Servant.Auth.Server (Auth, AuthResult (..), JWT)
import Servant.Auth.Server qualified as AuthServer
import Servant.Types.SourceT
-- ---------------------------------------------------------------------------
-- JWT-authenticated API
-- ---------------------------------------------------------------------------
{- | Servant API type for the JWT-authenticated MCP endpoint.
Accepts JSON-RPC requests with JWT authentication via servant-auth.
All MCP methods are multiplexed through this single endpoint.
-}
type MCPAPI =
"mcp"
:> Auth '[JWT] MCPHandlerUser
-- \^ JWT authentication using servant-auth
:> ReqBody '[JSON] JSONRPCMessage
-- \^ JSON-RPC request
:> StreamPost JSONRPCFrame JSONRPCEvent (SourceIO JSONRPCMessage)
-- \^ Stream of JSON-RPC responses
:<|> "mcp"
:> Auth '[JWT] MCPHandlerUser
:> StreamGet JSONRPCFrame JSONRPCEvent (SourceIO JSONRPCMessage)
-- | Type of the MCP API
mcpAPI :: MVar MCPServerState -> Server MCPAPI
mcpAPI state_var =
handleMCPRequest state_var
:<|> handleMCPEvents
-- | This handles the Get /mcp requests. Does nothing for now except authenticate.
handleMCPEvents ::
AuthResult MCPHandlerUser ->
Handler (SourceIO JSONRPCMessage)
handleMCPEvents auth_result = do
case auth_result of
AuthServer.NoSuchUser -> throwError err401{errBody = "Invalid authentication credentials"}
AuthServer.BadPassword -> throwError err401{errBody = "Authentication failed"}
AuthServer.Indefinite -> throwError err401{errBody = "Authentication error"}
AuthServer.Authenticated _ -> return $ fromStepT $ Stop
{- | Handle incoming MCP JSON-RPC requests.
Validates JWT authentication, then delegates to the shared
'handleMCPRequestCore' from "MCP.Server.HTTP.Internal".
-}
handleMCPRequest ::
MVar MCPServerState ->
AuthResult MCPHandlerUser ->
JSONRPCMessage ->
Handler (SourceIO JSONRPCMessage)
handleMCPRequest state_var auth_result request_value =
case auth_result of
AuthServer.NoSuchUser -> mcpAuthError "Invalid authentication credentials"
AuthServer.BadPassword -> mcpAuthError "Authentication failed"
AuthServer.Indefinite -> mcpAuthError "Authentication error"
AuthServer.Authenticated auth_user ->
handleMCPRequestCore state_var (Just auth_user) request_value
where
mcpAuthError :: Text -> Handler (SourceIO JSONRPCMessage)
mcpAuthError err = throwError err401{errBody = encode $ object ["error" .= err]}
-- ---------------------------------------------------------------------------
-- Simple (unauthenticated) API
-- ---------------------------------------------------------------------------
{- | Servant API type for the unauthenticated MCP endpoint.
Unlike 'MCPAPI', this does not use @servant-auth@ and performs no
authentication. See the module documentation for guidance on when it is
safe to use this transport.
-}
type SimpleHTTPAPI =
"mcp"
:> ReqBody '[JSON] JSONRPCMessage
:> StreamPost JSONRPCFrame JSONRPCEvent (SourceIO JSONRPCMessage)
:<|> "mcp"
:> StreamGet JSONRPCFrame JSONRPCEvent (SourceIO JSONRPCMessage)
{- | Build a WAI 'Application' for the unauthenticated HTTP transport.
Every request is accepted without authentication. This should only be
used on @localhost@ or behind an authenticating reverse proxy.
-}
simpleHttpApp :: MVar MCPServerState -> Application
simpleHttpApp state_var =
serve (Proxy @SimpleHTTPAPI) $
handleSimpleHTTPRequest state_var
:<|> handleSimpleHTTPEvents
-- | Handle GET \/mcp requests (unauthenticated).
handleSimpleHTTPEvents :: Handler (SourceIO JSONRPCMessage)
handleSimpleHTTPEvents = return $ fromStepT Stop
{- | Handle incoming MCP JSON-RPC requests (unauthenticated).
Delegates directly to 'handleMCPRequestCore'. No user type is available,
so @mcp_handler_init@ is not called.
-}
handleSimpleHTTPRequest ::
MVar MCPServerState ->
JSONRPCMessage ->
Handler (SourceIO JSONRPCMessage)
handleSimpleHTTPRequest state_var = handleMCPRequestCore state_var Nothing