mcp-server
Library for building Model Context Protocol (MCP) servers
https://github.com/drshade/haskell-mcp-server
| LTS Haskell 24.58: | 0.1.0.21 |
| Stackage Nightly 2026-09-09: | 0.2.0.2 |
| Latest on Hackage: | 0.2.0.2 |
mcp-server-0.2.0.2@sha256:754c6cfb689f4ffa56519f288679b7702d163093a3043b31daf159f2b815d533,6861Module documentation for 0.2.0.2
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):
{
"name": "checkout",
"description": "Checkout",
"annotations": { "destructiveHint": true },
"inputSchema": {
"type": "object",
"required": ["speed", "shipTo"],
"properties": {
"speed": { "type": "string", "enum": ["standard", "express", "overnight"] },
"shipTo": {
"type": "object",
"required": ["street", "city"],
"properties": {
"street": { "type": "string" },
"city": { "type": "string" },
"zipCode": { "type": "string" }
}
}
}
}
}
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-05through2025-11-25negotiated viainitialize, and the stateless2026-07-28revision 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.
listChangedand resource-update pushes oversubscriptions/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 three 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.
Defining tools
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 { key :: Text, value :: 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* 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 themainthat uses them. - Every handler receives the per-request
ClientContextfirst. It carries the caller’s bearer token and principal on HTTP, the protocol revision and client identity for modern clients, and thereportProgressandlogToClientactions 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 PromptResult)
handlers = noHandlers { prompts = Just (promptListHandler, promptGetHandler) }
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.Transport.Http
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 = Warp.runSettings mySettings $ \req respond ->
-- route /mcp to the MCP endpoint, everything else to your app
mcpApplication defaultHttpConfig serverInfo handlers req respond
httpPort and httpHost are ignored when embedding; the endpoint path,
Origin validation, bearer auth and streaming all apply as usual.
Long-running tools
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
reportProgressemitsnotifications/progressonly when the request carried aprogressToken. Progress values must increase call over call.logToClientemitsnotifications/messageonly when the request declaredio.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/cancellednaming 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.
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/listenstream (a long-lived SSE response over HTTP) and receive only the notification types they opted into, tagged with their subscription id, includingnotifications/resources/updatedfor watched URIs. - Legacy stdio clients receive spontaneous untagged notifications once
their
notifications/initializedarrives. - 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) |
| Result typing and cacheability hints | — | resultType, httpCacheHints |
| HTTP request-metadata headers | — | MCP-Protocol-Version, Mcp-Method, Mcp-Name validated |
Design decisions and planned work (input-required results, 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/HttpSimple/: the key-value store over Streamable HTTP.
cabal run simple-example # stdio; type JSON-RPC on stdin
cabal run http-simple-example # http://localhost:3000/mcp
Conformance corpus
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
- API documentation on Hackage
- MCP Specification (2026-07-28)
- MCP Specification (2025-11-25, newest legacy revision)
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
Changes
Revision history for mcp-server
0.2.0.2 - 2026-09-08
- stdio: in-flight requests are drained at stdin EOF instead of being
cancelled. Since 0.2.0.1 made stdio requests concurrent, a client that
wrote its requests and closed stdin straight away (scripts,
echo ... | server, conformance replays) lost the responses to whatever was still running when EOF arrived — most visibly the last request in the batch. Per the lifecycle spec the client closes stdin and then waits for the server to exit, so the server now finishes outstanding work and writes those responses before shutting down. Subscription streams are still closed at EOF as before, andnotifications/cancelledstill interrupts a running request. - A handler that throws an exception (rather than returning an error
value such as
toolError) now yields a-32603internal-error response for its request id, on both transports and in both protocol eras. Previously the exception escaped the transport: on stdio the request’s task died silently and the client waited forever for that id; on HTTP Warp answered a bare text/plain 500 (single-JSON responses) or dropped the connection with an empty body (SSE responses). The exception’s first line is the error message; the full rendering (including any call stack) is logged to stderr. Asynchronous exceptions are rethrown untouched, so cancellation is unaffected.
0.2.0.1 - 2026-08-01
(Supersedes 0.2.0.0, which is deprecated on Hackage: it was published hours before this line landed and was never adopted, so rather than burning a major version on a release nobody used, 0.2.0.1 replaces it — including changes that would ordinarily demand a major bump. Anyone explicitly pinning the deprecated 0.2.0.0 should move here. The unreleased 0.2.1.0 line below is folded in as well.)
-
Request cancellation (ADR 0008): in-flight requests can now actually be interrupted, per the spec’s “stop work as soon as practical, send nothing further for that request”. On stdio every request runs in its own task and
notifications/cancelledcancels the referenced one (unknown or completed ids are ignored); on HTTP, closing an SSE response stream cancels the running handler (detected within one keep-alive interval, now 5s). Single-JSON HTTP responses only detect a disconnect at the final write, so clients wanting cancellable calls should opt into streaming via aprogressToken. Cancellation is delivered as an asynchronous exception, so handlers acquiring resources should usebracket— documented in the README. BREAKING (behavioral): stdio requests are now served concurrently rather than strictly sequentially — handlers touching shared mutable state must synchronize, as was already required with the HTTP transport. New dependency:async. -
Progress notifications and per-request SSE (ADR 0007): handlers can call
reportProgressandlogToClienton theClientContext— both safe unconditionally.reportProgressemitsnotifications/progressonly when the request carried aprogressToken;logToClientemitsnotifications/messageonly when the request declaredio.modelcontextprotocol/logLevel(per spec MUST NOT otherwise), filtered to the declared threshold (newLogLeveltype, RFC 5424 ordering). On stdio the notifications interleave before the response; on HTTP a request that opted in is answered with an SSE response stream (notifications, then the final response), while other requests keep the single-JSON response. BREAKING:ClientContextcarries the two actions and loses itsShow/Eqinstances;MCP.Server.Handlers.handleMcpMessagetakes the transport’s notification sink. -
Definition metadata (ADR 0006):
ToolAnnotations—readOnlyHint/destructiveHint/idempotentHint/openWorldHintbehavioral hints (2025-03-26+) plus a title, all unset by default (defaultToolAnnotations), carried onToolDefinitionand driving client permission UX.Iconlists (2025-11-25+) on tool, prompt, resource and resource-template definitions.- Content
Annotations(audience/priority/lastModified, 2025-03-26+) attached via the newContentAnnotatedwrapper, whose annotations merge into the inner block’s JSON (and parse back out).
-
New
WithOptionsderivations for all five derive families, taking per-constructorDefinitionOptions(description, title, icons, tool annotations, and constructor-scoped field descriptions — two constructors can now describe a same-named field differently, fixing the global-namespace wart of the flat description list, which remains supported unchanged). -
BREAKING:
ToolDefinition,PromptDefinition,ResourceDefinitionandResourceTemplateDefinitiongain fields, andContentgains theContentAnnotatedconstructor. New smart constructors (mkToolDefinition,mkPromptDefinition,mkResourceDefinition,mkResourceTemplateDefinition) build definitions from required fields only — construct through them and record-update, so future optional fields stop breaking your code. All new JSON fields are omitted when unset, so wire output for existing servers is unchanged. -
Derived output schemas and structured content (ADR 0005): the new
deriveToolHandlerWithOutput(and...WithOutputDescription) take a result record type, derive the tools’outputSchemafrom it (same field rules as input derivation: primitives,Maybe, lists, all-nullary enums, nested records), and serialize the handler’s typed values intostructuredContent— the generated serializer mirrors the generated schema, so the two cannot drift. Handlers return the newToolOutputtype:ToolOutput(structured value; the JSON is also returned as a text content block per the spec’s recommendation),ToolOutputWith(custom content blocks),ToolOutputError(isError), orToolOutputRaw(plainToolResultescape hatch). ExistingToToolResulthandlers are untouched. The conformance corpus gains anecho_structuredreference tool with cases in both eras. -
The HTTP transport’s WAI application is now exported (
mcpApplication, re-exported fromMCP.Server), so the MCP endpoint can be embedded into an existing WAI stack — your own Warp settings, TLS, middleware or router — instead oftransportRunHttprunning its own server.httpPort/httpHostare ignored when embedding; everything else (endpoint path, Origin validation, bearer auth,subscriptions/listenstreaming) applies as usual. -
The golden wire fixtures are now a self-describing, API-agnostic conformance corpus: each case under
test/golden/is a.request.json/.response.jsonpair on disk, enumerated bymanifest.json, with the reference server documented intest/golden/README.md. Other MCP implementations can replay the requests and diff the responses without touching any Haskell; the fixtures themselves are unchanged.
0.2.0.0 - 2026-07-31
A major overhaul of the handler API. The headline changes: the handler
boundary is no longer stringly typed, and the server is dual-era — it speaks
both the legacy initialize-handshake revisions and the stateless
2026-07-28 revision.
Dual-era protocol support (2026-07-28)
- Requests that declare a protocol revision in their params
_meta(io.modelcontextprotocol/protocolVersion) are served statelessly with the modern result envelope:resultType: "complete", the server identity in result_meta, and — ontools/list,prompts/list,resources/list,resources/readandserver/discover— the requiredttlMs/cacheScopefields (configurable viaCacheHintson the transport configs; default: no caching, private). Requests without modern_metaare served byte-identically to before under the revision negotiated byinitialize. - New
server/discovermethod (mandatory in 2026-07-28, and the backwards-compatibility probe): supported revisions of both eras, handler-gated capabilities, server identity and instructions. - Declaring an unsupported revision returns
UnsupportedProtocolVersionError(-32022) listing the supported set. - A legacy client proposing
2026-07-28viainitializenegotiates down to2025-11-25: an initializing client is legacy by definition. - Handlers can read the declared revision, client info and client
capabilities from the
ClientContext(clientProtocolVersion/clientInfo/clientCapabilities); the newanonymousContextbuilds an empty context. - HTTP: modern requests get the 2026-07-28 request-metadata validation —
the
MCP-Protocol-Versionheader must match the body’s declared revision,Mcp-Methodmust match the body method, andMcp-Namemust matchparams.name/params.urifortools/call/resources/read/prompts/get(with=?base64?…?=sentinel decoding); violations return400withHeaderMismatch(-32020). Unknown methods return HTTP 404 and unsupported revisions HTTP 400, so era-probing clients can distinguish them. Legacy requests keep the relaxed pre-2026 rules.
Change notifications and subscriptions/listen
- New
MCP.Server.Notifications: create anMcpNotifierwithnewMcpNotifier, hand itsNotificationSourceto a transport (stdioNotifications/httpNotifications), and callnotifyToolsListChanged/notifyPromptsListChanged/notifyResourcesListChanged/notifyResourceUpdatedwhen things change. subscriptions/listen(2026-07-28) is served on both transports: the mandatory acknowledgment comes first with the honored filter, every message is tagged with the subscription id, only opted-into types are delivered, and streams end gracefully (closure responses at stdio EOF; closing the SSE stream cancels over HTTP,notifications/cancelledover stdio). HTTP streams send periodic keep-alive comments andX-Accel-Buffering: no.- Legacy stdio clients receive spontaneous untagged notifications after
initialize. Capabilities are era- and transport-aware:listChangedis advertised only where delivery is possible (stdio legacy push, or modernsubscriptions/listen), andsubscribeonly to modern clients. defaultHttpConfigis now re-exported fromMCP.Server.
Resource templates and completions
- Record constructors of a resource type now derive as resource /templates/
(
UserProfile { userId :: Text }→resource://user_profile/{userId}): the derived read handler matches template URIs, percent-decodes the path segments, and parses them into the constructor’s (typed) fields.deriveResourceTemplatesderives theresources/templates/listhandler advertising them; the method carries the modern cacheability envelope. - New
completionshandler slot servingcompletion/completefor prompt arguments and resource-template parameters (CompletionRef,CompletionResult, capped at 100 values per the spec). Thecompletionscapability is advertised automatically. McpServerHandlersgainsresourceTemplatesandcompletionsfields; the newnoHandlersvalue lets you construct handler sets by record update so future fields don’t break your code.
Typed tool arguments and results (BREAKING)
- Tool arguments arrive as full JSON values (
Map Text Value). The Template Haskell derivation decodes records recursively and now supports list fields, enumeration fields (all-nullary data types, wired as string enums), and nested record fields in addition to the primitives. Primitive parsing is lenient: native JSON types or their string representations are both accepted (many clients send numbers/booleans as strings). Prompt arguments remain string-valued per the MCP specification. inputSchemais generated as a real JSON Schema (Schema/SchemaTypeADT withenum,itemsand nestedobjects), replacing the flatInputSchemaDefinition*types that silently typed every non-primitive field as a string.- Tool handlers produce a
ToolResult: multiple content blocks,structuredContent,_meta, andisError. Tool execution failures should be reported viaisError(seetoolError) so the model can see them — per spec — instead of surfacing as JSON-RPC protocol errors. TheToToolResultclass keeps simple handlers simple: returningContentorTextstill works unchanged. - Prompt handlers produce a
PromptResult(optional description plus a multi-message conversation with user/assistant roles) via the analogousToPromptResultclass. Contentgainsaudioandresource_linkvariants; embedded resources now carry their full contents as the spec requires.ToolDefinitiongainsoutputSchema;tools/callresponses carrystructuredContent.- Handler types are fixed to
IO— the monad parameter was unusable through the public API (both transports requiredIO).
Transport fixes
- stdio: a blank line on stdin no longer terminates the server, EOF shuts
down cleanly instead of crashing, and malformed input is answered with
proper JSON-RPC error responses (
-32700/-32600,id: null). - stdio: raw request bodies are no longer logged to stderr by default
(tool arguments may carry sensitive data) — only message summaries.
runMcpServerStdioWithConfigwithstdioVerbose = Truerestores full body logging. - JSON-RPC: messages are classified by shape (method/id presence) instead
of parse-fallthrough, so a request with a malformed
idis answered with an error rather than silently dropped as a notification. Request ids must be integral. - HTTP: new
httpAllowedOriginspolicy onHttpConfig(Origin validation / DNS-rebinding protection, a spec MUST); accepted notifications return202with no body; malformed bodies get JSON-RPC error responses; theAccess-Control-Allow-Originheader is set consistently on every response and echoes the validated origin (withVary: Origin) when a policy is configured. - HTTP (BREAKING): the non-spec GET “discovery” endpoint is removed — the
MCP endpoint now answers GET with
405 Method Not Allowed, matching the spec (no revision defines a GET discovery response, and2026-07-28requires 405 here). - Integer tool arguments bound the scientific-notation exponent (1024, the
same bound aeson uses) so a tiny payload like
1e1000000000cannot force allocation of a gigabyte-sizedInteger.
0.1.0.21 - 2026-07-31
- BREAKING: every handler (prompt/resource/tool; list and get/read/call)
now receives a
ClientContextas its first argument, so a server can behave differently depending on who is calling. On stdio the context is anonymous; on HTTP it carries the request’s bearer token and the principal returned by the authorization callback. - BREAKING:
HttpConfiggains anhttpAuthorizefield — an optional callback that validates the presentedAuthorization: Bearertoken and returns an application-defined principal (Nothingrejects with 401). As it now holds a function,HttpConfigno longer derivesShow/Eq. - HTTP transport: accept requests without an
MCP-Protocol-Versionheader (the spec says to assume2025-03-26), exemptinitializefrom the header check (it negotiates its version in the body), and keep rejecting a present but unsupported header with 400. Previously every request without the header was rejected, locking out pre-2025-06-18clients. initializenow advertises only the capabilities that actually have handlers, so strict clients no longer drop the server when e.g.prompts/listanswers “not supported”.- CORS: preflight
OPTIONSrequests are exempt from authorization (browsers send no credentials on preflight) andAuthorizationis included inAccess-Control-Allow-Headers. http-simple-exampleis now built with-threaded, which Warp requires; previously every request crashed with aTimerManagererror.
0.1.0.20 - 2026-07-31
- Fix protocol version negotiation: echo back any compatible revision the client
proposes (
2024-11-05,2025-03-26,2025-06-18,2025-11-25) instead of always responding with the server’s own version. Fixes clients (e.g. Claude Code) that disconnect when they receive a different version than requested. - Apply the same negotiation to the HTTP transport’s
MCP-Protocol-Versionheader check, which previously rejected anything other than2025-06-18. - Default/fallback advertised version bumped to
2025-11-25.
0.1.0.19 - ???
- Improve handler code generated by TemplateHaskell functions in
MCP.Server.Derive:- Don’t repeat
Map.fromListfor each argument in map lookup - Properly handle argument parse errors (Return
InvalidParamserror instead of crashing mcp server witherror)
- Don’t repeat
0.1.0.18 - 2026-02-09
- Switch default-language to GHC2021 to support broader range of GHC versions (9.6 - 9.12)
0.1.0.17 – 2026-01-28
- Implement protocol version negotiation according to spec
- Remove unused dependencies, fix GHC warnings
- Add tested-with and haskell-ci generated GitHub Actions config
0.1.0.16 – 2026-01-19
- Bump template-haskell dependency upper bound
0.1.0.15 – 2025-08-13
- Update to MCP spec 2025-06-18
0.1.0.14 – 2025-06-26
- Bump version bounds before adding to Stackage
- Remove support for JSON-RPC batching
0.1.0.13 – 2025-06-17
- Better handling of UTF-8 in logs
0.1.0.12 – 2025-06-17
- Fix unicode handling
- Refactor transports to remove unneeded functions
- Add unicode handling tests
0.1.0.11 – 2025-06-17
- Refactor transports and add HTTP streaming support
- Add
MCP.Server.Handlersmodule - Add
MCP.Server.Transport.HttpandMCP.Server.Transport.Stdiomodules
0.1.0.10 – 2025-06-13
- Fix resources handling
0.1.0.9 – 2025-06-13
- Bump versions of dependencies
- Port tests to hspec
0.1.0.8 – 2025-06-12
- Support for nestable data types
0.1.0.7 – 2025-06-09
- Documentation updates
0.1.0.6 – 2025-06-09
- Remove pagination support
0.1.0.5 – 2025-06-09
- Add descriptions to constructors and fields
0.1.0.4 – 2025-06-09
- Clean up build configuration
0.1.0.3 – 2025-06-09
- Refactor example modules
- Fix JSON to Haskell type conversion
0.1.0.0 – 2025-06-05
- First version. Released on an unsuspecting world.