mcp-server-0.3.0.1: examples/Ask/Main.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
-- | Tools that ask the user for input part-way through (multi round-trip
-- requests, MCP 2026-07-28). Run it, then watch stderr while a client
-- books a table: the handler replays from the top on every round, but each
-- 'step' runs once in a normal booking, its result replayed afterwards.
--
-- 'step' is at-least-once, though: a client that retries a round with an
-- earlier state (say, after losing a reply) runs the steps after that point
-- again. A real 'confirmBooking' must therefore be idempotent, for example
-- by keying the booking on something the retry repeats.
module Main where
import Control.Monad.IO.Class (liftIO)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Time.Clock.POSIX (getPOSIXTime)
import MCP.Server
import MCP.Server.Derive
import System.IO (hPutStrLn, hSetEncoding, stderr,
stdout, utf8)
import Types
-- Returning 'Ask' instead of 'IO' is all it takes to ask questions.
handleTool :: ClientContext -> BookingTool -> Ask Content
handleTool _ (BookTable place) = do
-- Ask for the details. 'elicit' picks the form from the type the
-- answer is used at: BookingDetails.
answer <- elicit ("Booking a table at " <> place <> ". What do you need?")
case answer of
Accepted details -> do
-- Work between questions goes in 'step': it runs once, and
-- its result is replayed on later rounds.
offer <- step (findTable place details)
ok <- elicit ("Book " <> offer <> "?")
case ok of
Accepted (Confirm True) -> do
ref <- step (confirmBooking offer)
pure $ ContentText $ "Booked " <> offer <> ". Your reference is " <> ref <> "."
_ -> pure $ ContentText "No problem, nothing was booked."
Declined -> pure $ ContentText "No problem, nothing was booked."
Cancelled -> pure $ ContentText "Booking cancelled."
-- The client cannot show forms (an older protocol revision, or no
-- elicitation capability): no round trip happens, so say what to do.
Unavailable -> pure $ ContentText $
"I can't ask for booking details in this client. Please call "
<> place <> " to book."
handleTool _ (CancelBooking ref) = do
ok <- elicit ("Cancel booking " <> ref <> "? This cannot be undone.")
case ok of
Accepted (Confirm True) -> do
_ <- step (cancelBooking ref)
pure $ ContentText $ "Booking " <> ref <> " is cancelled."
-- Cancelling without confirmation would be destructive: refuse.
Unavailable -> pure $ ContentText "Cancelling needs confirmation, which this client can't ask for."
_ -> pure $ ContentText $ "Booking " <> ref <> " is unchanged."
-- A tool that never asks finishes in one round; plain IO goes through liftIO.
handleTool _ (OpeningHours place) =
ContentText <$> liftIO (lookupHours place)
-- The "back end". Each logs to stderr so you can see when it runs.
findTable :: Text -> BookingDetails -> IO Text
findTable place details = do
logLine $ "finding a table at " <> place <> " for " <> T.pack (show details)
pure $ "a table for " <> T.pack (show (partySize details)) <> " at " <> place
<> ", " <> slotTime (slot details)
<> (if null (dietary details) then "" else " (kitchen notified: "
<> T.intercalate ", " (map dietLabel (dietary details)) <> ")")
<> (if highChair details == Just True then " with a high chair" else "")
-- Not idempotent: a retried final round would book again under a new
-- reference. Fine for a demo; see the note at the top of the module.
confirmBooking :: Text -> IO Text
confirmBooking offer = do
now <- getPOSIXTime
let ref = "BK-" <> T.pack (show (floor now `mod` (100000 :: Integer)))
logLine $ "booked " <> offer <> " as " <> ref
pure ref
cancelBooking :: Text -> IO ()
cancelBooking ref = logLine $ "cancelled " <> ref
lookupHours :: Text -> IO Text
lookupHours place = pure $ place <> " is open 12:00-15:00 and 17:30-23:00, Tuesday to Sunday."
slotTime :: Slot -> Text
slotTime Lunch = "13:00"
slotTime EarlyDinner = "18:00"
slotTime LateDinner = "20:30"
dietLabel :: Diet -> Text
dietLabel Vegetarian = "vegetarian"
dietLabel Vegan = "vegan"
dietLabel GlutenFree = "gluten-free"
dietLabel NutAllergy = "nut allergy"
logLine :: Text -> IO ()
logLine = hPutStrLn stderr . ("[bookings] " <>) . T.unpack
main :: IO ()
main = do
hSetEncoding stdout utf8
hSetEncoding stderr utf8
runMcpServerStdio
McpServerInfo
{ serverName = "Table Booking Example"
, serverVersion = "0.1.0"
, serverInstructions = "Books restaurant tables, asking the user for the details."
}
noHandlers
{ tools = Just $(deriveToolHandlerWithOptions ''BookingTool 'handleTool
[ ("BookTable", defaultDefinitionOptions
{ optDescription = Just "Book a table at a restaurant; asks the user for the details" })
, ("CancelBooking", defaultDefinitionOptions
{ optDescription = Just "Cancel a booking by its reference, after confirming with the user"
, optToolAnnotations = Just defaultToolAnnotations { toolDestructiveHint = Just True } })
, ("OpeningHours", defaultDefinitionOptions
{ optDescription = Just "A restaurant's opening hours"
, optToolAnnotations = Just defaultToolAnnotations { toolReadOnlyHint = Just True } })
])
}