mcp-server

Build Model Context Protocol servers in
Haskell from plain data types. Declare your tools, prompts and resources as
ADTs, write one handler per type, and the library derives the JSON schemas,
argument decoding, validation and wire protocol for you — then serves it over
stdio or Streamable HTTP to Claude Code, Codex, Claude Desktop, Cursor and any
other MCP client.
{-# LANGUAGE OverloadedStrings, TemplateHaskell #-}
import Data.Text (Text)
import MCP.Server
import MCP.Server.Derive
data Units = Celsius | Fahrenheit
data WeatherTool
= CurrentWeather { city :: Text, units :: Maybe Units }
| Forecast { city :: Text, days :: Int }
handleTool :: ClientContext -> WeatherTool -> IO Content
handleTool _ (CurrentWeather c _) = pure $ ContentText $ "Sunny in " <> c
handleTool _ (Forecast c n) = pure $ ContentText $ "Forecast for " <> c
$(pure []) -- end the declaration group so the splice below can see the types
main :: IO ()
main = runMcpServerStdio serverInfo noHandlers
{ tools = Just $(deriveToolHandler ''WeatherTool 'handleTool) }
where
serverInfo = McpServerInfo
{ serverName = "weather", serverVersion = "1.0.0"
, serverInstructions = "Weather lookups" }
That is a complete, working MCP server exposing two tools, current_weather
and forecast, each with a JSON schema derived from its constructor's fields.
What you get for free
The derivation reads your types, so the schema on the wire always matches the
handler that receives the arguments. Given this constructor from
examples/Complete:
data ShippingSpeed = Standard | Express | Overnight
data Address = Address
{ street :: Text
, city :: Text
, zipCode :: Maybe Text
}
data MyTool
= Checkout { speed :: ShippingSpeed, shipTo :: Address }
| ...
tools/list returns exactly this (captured from the running example; a
field without a description is described by its name):
{
"name": "checkout",
"description": "Checkout",
"annotations": { "destructiveHint": true },
"inputSchema": {
"type": "object",
"required": ["speed", "shipTo"],
"properties": {
"speed": { "type": "string", "enum": ["standard", "express", "overnight"],
"description": "speed" },
"shipTo": {
"type": "object",
"description": "shipTo",
"required": ["street", "city"],
"properties": {
"street": { "type": "string", "description": "street" },
"city": { "type": "string", "description": "city" },
"zipCode": { "type": "string", "description": "zipCode" }
}
}
}
}
}
and a tools/call with matching arguments arrives in your handler as a fully
decoded Checkout Express (Address "1 Main St" "Springfield" Nothing).
Malformed arguments never reach you; the library answers with the appropriate
JSON-RPC error. The same machinery works in reverse for typed results (see
Structured output).
Beyond the derivation, the library handles:
- Both protocol eras. Legacy revisions
2024-11-05 through 2025-11-25
negotiated via initialize, and the stateless 2026-07-28 revision
declared per request in _meta, from one server binary.
- Two transports. stdio, and Streamable HTTP with bearer auth, Origin
validation, per-request SSE and a plain WAI application you can embed.
- Long-running tools. Progress notifications, per-request client logging,
and cancellation of in-flight requests on both transports.
- Live servers.
listChanged and resource-update pushes over
subscriptions/listen.
- Conformance fixtures. A language-agnostic corpus of request/response
pairs the test suite replays against every protocol era.
Installation
Add mcp-server to your build-depends:
build-depends:
base, text, mcp-server
Tested against GHC 9.6 through 9.14 in CI. The HTTP transport requires
ghc-options: -threaded (Warp needs the threaded runtime).
Connecting to a client
Build your server, then register it with your client of choice. The examples
below use the simple-example executable from this repository; substitute
your own.
Claude Code
# stdio: everything after -- is the command Claude Code spawns
claude mcp add my-server -- "$(cabal list-bin exe:simple-example)"
# pass environment variables with --env
claude mcp add my-server --env API_KEY=secret -- /path/to/my-server
# Streamable HTTP
claude mcp add --transport http my-server http://localhost:3000/mcp
Verify with claude mcp list or /mcp inside a session. Add
--scope project to write a .mcp.json you can commit for your team:
{
"mcpServers": {
"my-server": {
"type": "stdio",
"command": "/path/to/my-server",
"env": { "API_KEY": "${API_KEY}" }
}
}
}
Cursor and several other clients read the same mcpServers shape.
Codex
codex mcp add my-server --env API_KEY=secret -- /path/to/my-server
Or in ~/.codex/config.toml, which is also where HTTP servers go:
[mcp_servers.my-server]
command = "/path/to/my-server"
[mcp_servers.my-http-server]
url = "http://localhost:3000/mcp"
Claude Desktop
Claude Desktop launches stdio servers from claude_desktop_config.json. A
Docker image is a convenient way to ship a Haskell binary to it — the
repository's Dockerfile builds all the examples:
docker build -t haskell-mcp-server .
{
"mcpServers": {
"simple-example": {
"command": "docker",
"args": ["run", "-i", "--entrypoint=/usr/local/bin/simple-example", "haskell-mcp-server"]
}
}
}
Keep stdout clean
On stdio, stdout carries only JSON-RPC. The library writes nothing else
there, and your handlers must not either: log to stderr.
Naming and arguments
Constructor names become snake_case tool names; record fields become named
arguments. The generated inputSchema mirrors the field types:
data Color = Red | Green | Blue -- all-nullary type: string enum
data Filters = Filters -- record: nested JSON object
{ tags :: [Text] -- list: JSON array
, maxCount :: Maybe Int -- Maybe: optional field
}
data MyTool
= SearchItems -- "search_items"
{ query :: Text
, color :: Color -- "red" | "green" | "blue"
, filters :: Filters -- { "tags": [...], "maxCount": ... }
, limit :: Maybe Int
}
Primitive fields (Int, Integer, Double, Float, Bool, Text) are
parsed leniently: 42 and "42" are both accepted, since many clients send
numbers and booleans as strings.
A constructor may also wrap a single record type, which is unwrapped
recursively until a record is found:
data SetValueParams = SetValueParams { setKey :: Text, setValue :: Text }
data SimpleTool
= GetValue { key :: Text }
| SetValue SetValueParams -- fields of SetValueParams are the arguments
Positional (unnamed) fields are not supported, because they have no names to
put in the schema:
data SimpleTool = GetValue Int | SetValue Int Text -- ❌ rejected
Results and errors
Simple handlers return Content (or Text). Return a ToolResult for
multiple content blocks or to report an execution failure with isError,
which the spec prefers over a protocol error so the model can see what went
wrong and react:
handleTool :: ClientContext -> MyTool -> IO ToolResult
handleTool _ (SearchItems q _ _ _)
| T.null q = pure $ toolError "query must not be empty"
| otherwise = pure $ toolResult [ContentText ("Results for " <> q)]
Content blocks can carry annotations (audience, priority,
lastModified) via the ContentAnnotated wrapper:
ContentAnnotated defaultAnnotations { annotationsPriority = Just 0.9 }
(ContentText "important result")
Structured output
Give the derivation a result type and it derives the tool's outputSchema
(same field rules as inputs) and serializes your value into
structuredContent, guaranteed to match. Per the spec's recommendation the
JSON is also returned as a text block for clients that predate structured
output:
data WeatherReport = WeatherReport
{ temperature :: Int
, sky :: Sky -- enum
, alerts :: [Text]
, humidity :: Maybe Int -- omitted when Nothing
}
handleTool :: ClientContext -> MyTool -> IO (ToolOutput WeatherReport)
handleTool _ (GetWeather city) = pure $ ToolOutput (lookupWeather city)
handleTool _ (BrokenTool _) = pure $ ToolOutputError "sensor offline"
tools = Just $(deriveToolHandlerWithOutput ''MyTool 'handleTool ''WeatherReport)
ToolOutputWith supplies custom content blocks alongside the structured
value; ToolOutputRaw is the escape hatch back to a plain ToolResult.
Descriptions, annotations and icons
Every derive*Handler and deriveResourceTemplates function has a WithDescription variant taking a flat list of
constructor and field descriptions, and a WithOptions variant taking
per-constructor DefinitionOptions: description, title, icons, behavioral
annotations (which drive client permission UX, such as auto-approving
read-only tools) and argument descriptions scoped to that constructor.
descriptions =
[ ("SearchItems", "Search the catalog") -- constructor
, ("query", "Search terms") -- field
]
tools = Just $(deriveToolHandlerWithDescription ''MyTool 'handleTool descriptions)
tools = Just $(deriveToolHandlerWithOptions ''MyTool 'handleTool
[ ("SearchItems", defaultDefinitionOptions
{ optDescription = Just "Search the catalog"
, optToolAnnotations = Just defaultToolAnnotations
{ toolReadOnlyHint = Just True, toolIdempotentHint = Just True }
, optIcons = [icon "https://example.com/search.png"]
, optFieldDescriptions = [("query", "Search terms")]
})
])
Defining prompts
Prompts derive the same way. Arguments are string-valued per the spec, so
prompt records are limited to primitive and enumeration fields:
data MyPrompt = Recipe { idea :: Text } | Shopping { items :: Text }
handlePrompt :: ClientContext -> MyPrompt -> IO Content
handlePrompt _ (Recipe idea) = pure $ ContentText $ "Recipe for " <> idea
handlePrompt _ (Shopping items) = pure $ ContentText $ "Shopping list: " <> items
prompts = Just $(derivePromptHandler ''MyPrompt 'handlePrompt)
Return a PromptResult instead of Content for a description and a
multi-message conversation with user and assistant roles.
Defining resources
Nullary constructors become static resources with resource:// URIs; record
constructors become resource templates (RFC 6570), one percent-decoded path
segment per field:
data MyResource
= Menu -- resource://menu
| ProductDetail { sku :: Text } -- resource://product_detail/{sku}
| OrderItem { orderId :: Int, itemName :: Text } -- resource://order_item/{orderId}/{itemName}
handleResource :: ClientContext -> URI -> MyResource -> IO ResourceContent
handleResource _ uri Menu = pure $ ResourceText uri "text/plain" "Today's menu..."
handleResource _ uri (ProductDetail sku) = pure $ ResourceText uri "text/plain" ("Details for " <> sku)
handleResource _ uri (OrderItem o i) = ...
resources = Just $(deriveResourceHandler ''MyResource 'handleResource)
resourceTemplates = Just $(deriveResourceTemplates ''MyResource)
The read handler matches template URIs such as
resource://product_detail/ABC123 and decodes the segments into the
constructor's fields; typed fields like Int are parsed, and a failing
segment yields an invalid-params error.
Argument completion
Provide a completions handler to serve completion/complete for prompt
arguments and resource-template parameters. The capability is advertised
automatically when the handler is present:
handleComplete :: ClientContext -> CompletionRef -> ArgumentName -> Text -> Map Text Text
-> IO (Either Error CompletionResult)
handleComplete _ (CompletionRefPrompt "recipe") "idea" partial _ =
pure $ Right $ completionResult $
filter (T.isPrefixOf partial) ["pancakes", "pasta", "pizza"]
handleComplete _ _ _ _ _ = pure $ Right $ completionResult []
handlers = noHandlers { completions = Just handleComplete, ... }
Assembling the server
Start from noHandlers and record-update the features you provide.
Constructing McpServerHandlers directly is discouraged: the library grows
new handler slots over time, and a missed field fails at runtime rather than
compile time.
handlers = noHandlers
{ prompts = Just $(derivePromptHandler ''MyPrompt 'handlePrompt)
, resources = Just $(deriveResourceHandler ''MyResource 'handleResource)
, resourceTemplates = Just $(deriveResourceTemplates ''MyResource)
, tools = Just $(deriveToolHandler ''MyTool 'handleTool)
, completions = Just handleComplete
}
Two Template Haskell details to know:
- A
derive* splice can only see types declared in an earlier declaration
group. Either put the types in their own module (as the
examples do) or end the group with an empty $(pure [])
splice before the main that uses them.
- Every handler receives the per-request
ClientContext first. It carries the
caller's bearer token and principal on HTTP, the protocol revision and
client identity for modern clients, and the reportProgress and
logToClient actions described below.
Manual handlers
The derived handlers are ordinary values, so for full control you can supply
your own instead. Prompt arguments arrive as Map Text Text, tool arguments
as Map Text Value:
promptListHandler :: ClientContext -> IO [PromptDefinition]
promptGetHandler :: ClientContext -> PromptName -> Map Text Text -> IO (Either Error (Outcome PromptResult))
handlers = noHandlers { prompts = Just (promptListHandler, promptGetHandler) }
tools/call, prompts/get and resources/read handlers return an
Outcome: Complete result, or a request for input (see
Asking the user for input).
Tool schemas built by hand can use SchemaRaw for anything the typed
shapes cannot express — null, anyOf, $defs and $ref, numeric bounds
— and it is serialized as given:
import Data.Aeson (object, (.=))
import qualified Data.Aeson.KeyMap as KeyMap
import Data.Text (Text)
nullableCount :: Schema
nullableCount = schema $ SchemaRaw $ KeyMap.fromList
[ "anyOf" .= [object ["type" .= ("null" :: Text)], object ["type" .= ("integer" :: Text), "minimum" .= (0 :: Int)]] ]
A $ref such as #/$defs/node resolves against the root of the whole
input schema, not the raw fragment it appears in, so a recursive schema is
best written raw from the root down.
Transports
stdio
runMcpServerStdio serverInfo handlers serves JSON-RPC over stdin and
stdout. runMcpServerStdioWithConfig takes a StdioConfig for verbose
request logging on stderr, cacheability hints for modern clients, and a
change-notification source.
Streamable HTTP
import MCP.Server
main = runMcpServerHttp serverInfo handlers -- localhost:3000/mcp
main = runMcpServerHttpWithConfig defaultHttpConfig
{ httpPort = 8080
, httpHost = "0.0.0.0"
, httpEndpoint = "/api/mcp"
, httpVerbose = True -- request/response logging on stderr
, httpAllowedOrigins = Just ["https://app.example.com"]
} serverInfo handlers
httpAllowedOrigins is DNS-rebinding protection: requests carrying an
Origin outside the list get 403. Nothing disables the check and is only
appropriate for servers unreachable from browsers.
Bearer-token authentication is a callback. Return Just principal (any
JSON Value, such as a role) to admit the request, or Nothing for 401. The
principal reaches handlers as clientPrincipal in the ClientContext; token
policy lives entirely in your application.
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
}
The endpoint accepts POST only. Server-to-client notifications flow over the
subscriptions/listen POST response stream rather than the deprecated
standalone GET stream, and CORS is enabled for web clients.
Embedding in an existing WAI stack
The MCP endpoint is a plain WAI
application, exported as mcpApplication, so it can be mounted inside your
own Warp settings, TLS, middleware or router:
import MCP.Server (mcpApplication, defaultHttpConfig)
import qualified Network.Wai.Handler.Warp as Warp
main = do
mcp <- mcpApplication defaultHttpConfig serverInfo handlers
Warp.runSettings mySettings $ \req respond ->
-- route /mcp to the MCP endpoint, everything else to your app
mcp req respond
Build the application once: it holds the key that protects multi
round-trip state. httpPort and httpHost are ignored when embedding; the
endpoint path, Origin validation, bearer auth and streaming all apply as
usual.
Progress and logging
Handlers report progress and send log messages to the calling client through
actions on the ClientContext. Both are safe to call unconditionally:
handleTool ctx (ImportData file) = do
reportProgress ctx 0.0 (Just 1.0) (Just "starting import")
logToClient ctx LogInfo (String "opening file")
...
reportProgress ctx 1.0 (Just 1.0) Nothing
reportProgress emits notifications/progress only when the request
carried a progressToken. Progress values must increase call over call.
logToClient emits notifications/message only when the request declared
io.modelcontextprotocol/logLevel, as the spec requires, and drops
messages below the declared level.
On stdio the notifications interleave before the response. On HTTP, a request
that opted in is answered with an SSE stream carrying the notifications
followed by the final response; other requests keep the single-JSON response.
Cancellation
In-flight requests can be cancelled, after which the server stops work as soon
as practical and sends nothing further for that request:
- stdio: each request runs in its own task, and a
notifications/cancelled
naming its id cancels that task. Unknown or completed ids are ignored.
- HTTP: closing the response stream is the cancellation signal. SSE
responses detect the disconnect within one keep-alive interval. Single-JSON
responses only detect it at the final write, so clients wanting cancellable
calls should opt into streaming via a
progressToken.
Cancellation is delivered as an asynchronous exception, the standard GHC
mechanism used by timeout and cancel. Handlers are interruptible wherever
they block in IO, and one that acquires resources should release them with
bracket or finally:
handleTool ctx (ImportData file) =
bracket (openFile file ReadMode) hClose $ \h -> do
...
Critical sections can be shielded with mask, but keep them short: cancellation
waits for them.
Concurrency
Requests are served concurrently on both transports. Handlers touching shared
mutable state must synchronize with MVar, STM or similar.
A tool, prompt or resource handler can stop part-way through and ask the
user something: a form to fill in, or a URL to visit. This uses the
2026-07-28 multi round-trip requests. Give the handler the type Ask
instead of IO, derive forms from the records you want back, and write
the questions in order, as if the handler could wait for the answers:
data Environment = Staging | Production
data DeployTarget = DeployTarget
{ environment :: Environment -- a choice: staging, production
, reason :: Text
, notify :: Maybe Bool -- optional
}
newtype Confirm = Confirm { proceed :: Bool }
data OpsTool = Deploy { service :: Text } | Status { service :: Text }
$(deriveFormWithDescription ''DeployTarget [("reason", "Why deploy now?")])
$(deriveForm ''Confirm)
handleTool :: ClientContext -> OpsTool -> Ask Content
handleTool _ (Deploy svc) = do
target <- elicit ("Where should " <> svc <> " deploy to?")
case target of
Accepted t -> do
plan <- step (planDeploy svc (environment t)) -- runs once
ok <- elicit ("Apply " <> describePlan plan <> "?")
case ok of
Accepted (Confirm True) -> ContentText <$> step (applyPlan plan)
_ -> pure (ContentText "Deployment cancelled")
Unavailable -> pure (ContentText "This client cannot ask where to deploy")
_ -> pure (ContentText "No target chosen")
handleTool _ (Status svc) = ContentText <$> liftIO (currentStatus svc)
tools = Just $(deriveToolHandler ''OpsTool 'handleTool)
elicit picks the form from the type the code expects back, so
Accepted (Confirm True) is all it takes to ask the Confirm form. The
answer is Accepted with the submitted record, Declined or Cancelled,
or Unavailable, without a round trip, when the client cannot ask: it
speaks an older protocol revision or did not declare form elicitation.
elicitUrl sends the user to a URL instead.
The derive*Handler functions take IO and Ask handlers alike, so a
handler switches by changing its type. A tool that never asks, like
Status, finishes in one round; plain IO runs in Ask through liftIO.
Structured output works the same way: return Ask (ToolOutput a).
deriveForm builds the form's schema and its parser from a record.
Elicitation forms are flat, so the fields can be Text, Int, Integer,
Double, Float, Bool, enumerations (offered as snake_cased choices),
lists of enumerations (several choices) and Maybe of these for optional
fields. Anything else, such as a nested record, is a compile-time error
naming the field. deriveFormWithDescription adds help text per field, and
deriveFormWithOptions also sets labels and starting values:
$(deriveFormWithOptions ''DeployTarget
[ ("environment", defaultFormFieldOptions
{ formFieldTitle = Just "Environment", formFieldDefault = Just "staging" })
, ("reason", defaultFormFieldOptions { formFieldDescription = Just "Why deploy now?" })
])
A default that does not fit its field, or an option for a field the record
does not have, is also a compile-time error. deriveForm is a declaration splice, and
Template Haskell lets later splices see only the types declared above the
last declaration splice, so declare the tool, prompt and resource types
before the deriveForm lines (or in another module). For a form a record cannot
describe, write the Form instance by hand: a schema and a parser.
How it works, and the rules that follow
The handler does not pause between rounds. Each round ends the request:
the client asks the user, then sends the request again with the answer,
possibly to another server instance. So the handler runs again from the
start, and the library replays a journal of what earlier rounds produced,
kept in the state the client carries: elicit returns the recorded
answer, or ends the round with its question; step returns its recorded
result instead of running the action again.
- Ask the same questions and run the same steps in the same order on
every round. A handler that strays from its journal fails with an
internal error naming the position, rather than receiving the answer to
a different question.
step is at-least-once. A client that retries from an earlier state
runs later steps again. Make effects that must happen once idempotent.
step results travel to the client and back, encrypted, on every
later round: return an id rather than the whole object. Plain liftIO is
not journaled and runs on every round; use it for reads.
Protecting the state
The library protects the state the client carries between rounds: it is
encrypted, and bound to the caller, the request's arguments and an expiry
(60 minutes by default), and a retry presenting tampered, expired or
misdirected state is rejected before the handler runs. By default the key
is random per process, which suits stdio and a single HTTP instance. HTTP
deployments with several instances must share a key:
main = do
secret <- readSecret -- 32 bytes, shared by every instance
key <- maybe (fail "state key must be 32 bytes") pure (stateKey "2026-10" secret)
runMcpServerHttpWithConfig defaultHttpConfig
{ httpStateCodec = Just (aeadStateCodec key []) } -- to rotate: aeadStateCodec new [old]
serverInfo handlers
Low-level handlers
Hand-written call handlers return an Outcome: Complete with the result,
or InputRequired with requests keyed by ids of the handler's choosing and
some state of its own. On the retry the handler sees the answers in
clientInputResponses and its state in clientRequestState:
deploy :: ToolCallHandler
deploy ctx _ _ = pure $ Right $
case Map.lookup "confirm" (clientInputResponses ctx) of
Nothing | clientSupportsInput ctx question ->
InputRequired (Map.singleton "confirm" question) (Just "step-1")
Just (ElicitResponse ElicitAccept (Just answer))
| Map.lookup "ok" answer == Just (Bool True) ->
Complete (toolResult [ContentText "Deployed"])
_ -> Complete (toolError "Not confirmed")
where
question = ElicitForm "Deploy to production?"
(schema (SchemaObject [("ok", schema SchemaBoolean)] ["ok"]))
clientSupportsInput says whether the client can answer: it must speak
2026-07-28 and have declared the elicitation mode (or sampling, or roots)
in question. Asking a client that cannot answer is reported as an internal
error, so check first and fall back. A low-level handler can also run an
Ask computation with runAsk, which is what the derived handlers do.
Change notifications
Servers whose tool, prompt or resource lists change at runtime can push
change notifications. Create a notifier, hand its source to the transport, and
call the notifier when things change:
main = do
(notifier, source) <- newMcpNotifier
_ <- forkIO $ appLogic notifier -- calls notifyToolsListChanged etc.
runMcpServerStdioWithConfig
defaultStdioConfig { stdioNotifications = Just source }
serverInfo handlers
Delivery is transport- and era-aware, and the listChanged and subscribe
capabilities are advertised only where delivery is possible:
- Modern clients (2026-07-28) open a
subscriptions/listen stream (a
long-lived SSE response over HTTP) and receive only the notification types
they opted into, tagged with their subscription id, including
notifications/resources/updated for watched URIs.
- Legacy stdio clients receive spontaneous untagged notifications once
their
notifications/initialized arrives.
- Legacy HTTP clients have no delivery channel, so nothing is advertised.
Protocol support
| Feature |
Legacy (2024-11-05 to 2025-11-25) |
Modern (2026-07-28) |
| Version selection |
initialize handshake |
per-request _meta, server/discover |
| Prompts, resources, resource templates, tools |
✅ |
✅ |
| Argument completion |
✅ |
✅ |
| Tool annotations, icons, structured output |
✅ |
✅ |
| Progress and per-request logging |
✅ |
✅ |
| Cancellation |
✅ |
✅ |
| Change notifications |
stdio only |
subscriptions/listen (stdio and HTTP) |
| Asking for input (multi round-trip requests) |
— |
Ask handlers and derived forms, protected requestState |
| Result typing and cacheability hints |
— |
resultType, httpCacheHints |
| HTTP request-metadata headers |
— |
MCP-Protocol-Version, Mcp-Method, Mcp-Name validated |
Design decisions and planned work (OAuth resource metadata, pagination,
the tasks extension) live as ADRs under
specs/, ordered by specs/ROADMAP.md.
Examples
examples/Simple/: a key-value store with two tools over stdio.
examples/Complete/: prompts, resources, a resource
template, tools with enum, nested and list arguments, isError,
annotations, progress and completions.
examples/Ask/: tools that ask the user for input
part-way through: a table booking with a derived form, a confirmation
and a fallback for clients that cannot ask.
examples/HttpSimple/: the key-value store over
Streamable HTTP.
cabal run simple-example # stdio; type JSON-RPC on stdin
cabal run ask-example # stdio; asks for input (2026-07-28 clients)
cabal run http-simple-example # http://localhost:3000/mcp
The wire-format fixtures under test/golden/ are a
language-agnostic MCP conformance corpus: each case is a raw JSON-RPC
.request.json and the exact .response.json a reference server answers,
per protocol era, enumerated by a manifest.json. Any MCP implementation
that reproduces the small reference server described there can replay the
requests and diff the responses. Contributions of new cases are welcome.
Documentation
Contributing
Contributions are welcome. See the issue tracker for open issues and feature
requests, and RELEASING.md for how versions reach Hackage.
AI assistance
Much of this library was written with Claude, working from a specification I
wrote and iterating together until I was happy with the result. I review and
maintain all of it, but parts such as the Template Haskell derivation sit
outside what I would have written unaided, and I expect to keep refactoring
them.
License
BSD-3-Clause