packages feed

mcp-0.2.0.0: examples/http-server.hs

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}

{- |
Example HTTP MCP Server

This example demonstrates how to run the MCP server over HTTP transport.
The server will expose the MCP API at POST /mcp

To test:
1. Compile: cabal build mcp-http
2. Run: cabal run mcp-http
3. Send JSON-RPC requests to: http://localhost:<port>/mcp

Example request:
curl -X POST http://localhost:8080/mcp \
  -H "Content-Type: application/json" \
  -d '{"jsonrpc":"2.0","id":1,"method":"ping"}'

Command line options:
cabal run mcp-http -- --port 8080 --log
-}
module Main where

import Control.Monad (when)
import Control.Monad.IO.Class (liftIO)
import Data.Text qualified as T
import Data.Time (defaultTimeLocale, formatTime, getCurrentTime)
import Options.Applicative

import MCP.Protocol hiding (CompletionResult)
import MCP.Protocol qualified as Protocol
import MCP.Server
import MCP.Server.Auth
import MCP.Server.HTTP
import MCP.Types

-- | Command line options
data Options = Options
    { optPort :: Int
    , optEnableLogging :: Bool
    , optEnableOAuth :: Bool
    }
    deriving (Show)

-- | Parser for command line options
optionsParser :: Parser Options
optionsParser =
    Options
        <$> option
            auto
            ( long "port"
                <> short 'p'
                <> metavar "PORT"
                <> Options.Applicative.value 8080
                <> help "Port to run the HTTP server on (default: 8080)"
            )
        <*> switch
            ( long "log"
                <> short 'l'
                <> help "Enable request/response logging"
            )
        <*> switch
            ( long "oauth"
                <> short 'o'
                <> help "Enable OAuth authentication (demo mode)"
            )

-- | Full parser with help
opts :: ParserInfo Options
opts =
    info
        (optionsParser <**> helper)
        ( fullDesc
            <> progDesc "Run an MCP server over HTTP transport"
            <> header "mcp-http - HTTP MCP Server Example"
        )

-- | Example MCP Server implementation (copied from Main.hs)
instance MCPServer MCPServerM where
    handleListResources _params = do
        return $ ListResourcesResult{resources = [], nextCursor = Nothing, _meta = Nothing}

    handleReadResource _params = do
        let textContent = TextResourceContents{uri = "example://hello", text = "Hello from MCP Haskell HTTP server!", mimeType = Just "text/plain"}
        let content = TextResource textContent
        return $ ReadResourceResult{contents = [content], _meta = Nothing}

    handleListResourceTemplates _params = do
        return $ ListResourceTemplatesResult{resourceTemplates = [], nextCursor = Nothing, _meta = Nothing}

    handleListPrompts _params = do
        return $ ListPromptsResult{prompts = [], nextCursor = Nothing, _meta = Nothing}

    handleGetPrompt _params = do
        let textContent = TextContent{text = "Hello HTTP prompt!", textType = "text", annotations = Nothing}
        let content = TextContentType textContent
        let message = PromptMessage{role = User, content = content}
        return $ GetPromptResult{messages = [message], description = Nothing, _meta = Nothing}

    handleListTools _params = do
        let getCurrentDateTool =
                Tool
                    { name = "getCurrentDate"
                    , description = Just "Get the current date and time via HTTP"
                    , inputSchema = InputSchema "object" Nothing Nothing
                    , annotations = Nothing
                    }
        return $ ListToolsResult{tools = [getCurrentDateTool], nextCursor = Nothing, _meta = Nothing}

    handleCallTool CallToolParams{name = toolName} = do
        case toolName of
            "getCurrentDate" -> do
                currentTime <- liftIO getCurrentTime
                let dateStr = formatTime defaultTimeLocale "%Y-%m-%d %H:%M:%S UTC (via HTTP)" currentTime
                let textContent = TextContent{text = T.pack dateStr, textType = "text", annotations = Nothing}
                let content = TextContentType textContent
                return $ CallToolResult{content = [content], isError = Nothing, _meta = Nothing}
            _ -> do
                let textContent = TextContent{text = "Tool not found", textType = "text", annotations = Nothing}
                let content = TextContentType textContent
                return $ CallToolResult{content = [content], isError = Just True, _meta = Nothing}

    handleComplete _params = do
        let completionResult = Protocol.CompletionResult{values = [], total = Nothing, hasMore = Just True}
        return $ CompleteResult{completion = completionResult, _meta = Nothing}

    handleSetLevel _params = do
        liftIO $ putStrLn "Log level set via HTTP"

main :: IO ()
main = do
    Options{..} <- execParser opts

    putStrLn "Starting MCP Haskell HTTP Server..."
    putStrLn $ "Port: " ++ show optPort
    when optEnableLogging $ putStrLn "Request/Response logging: enabled"

    let serverInfo =
            Implementation
                { name = "mcp-haskell-http-example"
                , version = "0.1.0"
                }

    let resourcesCap =
            ResourcesCapability
                { subscribe = Just False
                , listChanged = Just False
                }
    let promptsCap =
            PromptsCapability
                { listChanged = Just False
                }
    let toolsCap =
            ToolsCapability
                { listChanged = Just False
                }

    let capabilities =
            ServerCapabilities
                { resources = Just resourcesCap
                , prompts = Just promptsCap
                , tools = Just toolsCap
                , completions = Nothing
                , logging = Nothing
                , experimental = Nothing
                }

    let baseUrl = T.pack $ "http://localhost:" ++ show optPort
        oauthConfig =
            if optEnableOAuth
                then
                    Just $
                        defaultDemoOAuthConfig
                            { oauthProviders =
                                [ OAuthProvider
                                    { providerName = "demo"
                                    , clientId = "demo-client"
                                    , clientSecret = Just "demo-secret"
                                    , authorizationEndpoint = baseUrl <> "/authorize"
                                    , tokenEndpoint = baseUrl <> "/token"
                                    , userInfoEndpoint = Nothing
                                    , scopes = ["mcp:read", "mcp:write"]
                                    , grantTypes = [AuthorizationCode]
                                    , requiresPKCE = True -- MCP requires PKCE
                                    , metadataEndpoint = Nothing
                                    }
                                ]
                            , -- Override demo defaults for example
                              authCodeExpirySeconds = 600 -- 10 minutes
                            , accessTokenExpirySeconds = 3600 -- 1 hour
                            , demoUserIdTemplate = Just "demo-user-{clientId}"
                            , demoEmailDomain = "demo.example.com"
                            , demoUserName = "Demo User"
                            , authorizationSuccessTemplate =
                                Just $
                                    "Demo Authorization Successful!\n\n"
                                        <> "Redirect to: {redirectUri}?code={code}{state}\n\n"
                                        <> "This is a demo server. In production, this would redirect automatically."
                            }
                else Nothing

    let config =
            HTTPServerConfig
                { httpPort = optPort
                , httpBaseUrl = baseUrl -- Configurable base URL
                , httpServerInfo = serverInfo
                , httpCapabilities = capabilities
                , httpEnableLogging = optEnableLogging
                , httpOAuthConfig = oauthConfig
                , httpJWK = Nothing -- Will be auto-generated
                , httpProtocolVersion = "2024-11-05" -- Configurable protocol version
                }

    putStrLn $ "HTTP server configured, starting on port " ++ show optPort ++ "..."
    putStrLn $ "MCP endpoint available at: POST " ++ T.unpack baseUrl ++ "/mcp"

    if optEnableOAuth
        then do
            putStrLn ""
            putStrLn "OAuth Demo Flow:"
            putStrLn "1. Generate PKCE code verifier and challenge"
            putStrLn "2. Open authorization URL in browser:"
            putStrLn $ "   " ++ T.unpack baseUrl ++ "/authorize?response_type=code&client_id=demo-client&redirect_uri=http://localhost:3000/callback&code_challenge=YOUR_CHALLENGE&code_challenge_method=S256&scope=mcp:read%20mcp:write"
            putStrLn "3. Exchange authorization code for token:"
            putStrLn $ "   curl -X POST " ++ T.unpack baseUrl ++ "/token \\"
            putStrLn "     -H \"Content-Type: application/x-www-form-urlencoded\" \\"
            putStrLn "     -d \"grant_type=authorization_code&code=AUTH_CODE&code_verifier=YOUR_VERIFIER\""
            putStrLn "4. Use access token for MCP requests:"
            putStrLn $ "   curl -X POST " ++ T.unpack baseUrl ++ "/mcp \\"
            putStrLn "     -H \"Authorization: Bearer ACCESS_TOKEN\" \\"
            putStrLn "     -H \"Content-Type: application/json\" \\"
            putStrLn "     -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}'"
        else do
            putStrLn ""
            putStrLn "Example test command:"
            putStrLn $ "curl -X POST " ++ T.unpack baseUrl ++ "/mcp \\"
            putStrLn "  -H \"Content-Type: application/json\" \\"
            putStrLn "  -d '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"ping\"}'"

    putStrLn ""

    runServerHTTP config