BSD-3-Clause licensed by Tom Wells
Maintained by [email protected]
This version can be pinned in stack with:mcp-server-0.2.0.1@sha256:7b09ac83c868f7a5b453aefd68c769e9cac5c5b60a0bac3cc418df1ba9587945,6795

mcp-server

A fully-featured Haskell library for building Model Context Protocol (MCP) servers.

Features

  • Complete MCP Implementation: Dual-era server — legacy revisions 2024-11-05 through 2025-11-25 via the initialize handshake, and the stateless 2026-07-28 revision via per-request _meta (including server/discover, resultType, and cacheability fields)
  • Type-Safe API: Leverage Haskell’s type system for robust MCP servers
  • Multiple Abstractions: Both low-level fine-grained control and high-level derived interfaces
  • Template Haskell Support: Automatic handler derivation from data types
  • Multiple Transports: STDIO and HTTP Streaming transport (MCP Streamable HTTP)

Supported MCP Features

  • Prompts: User-controlled prompt templates with arguments
  • Resources: Application-controlled readable resources
  • Resource Templates: Parameterized resources via URI templates
  • Tools: Model-controlled callable functions
  • Completions: Argument autocompletion for prompts and templates
  • Change Notifications: listChanged/resource-update pushes, via subscriptions/listen (2026-07-28) or legacy stdio delivery
  • Progress & Logging: notifications/progress and notifications/message scoped to the requesting client
  • Cancellation: notifications/cancelled (stdio) and stream closure (HTTP) interrupt in-flight handlers
  • Initialization Flow: Complete protocol lifecycle with version negotiation
  • Error Handling: Comprehensive error types and JSON-RPC error responses

Quick Start

Add the library mcp-server to your cabal file:

build-depends:
  mcp-server

Create a simple module, such as this example below:

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}

import Data.Text (Text)
import MCP.Server
import MCP.Server.Derive

-- Define your data types
data MyPrompt = Recipe { idea :: Text } | Shopping { items :: Text }
data MyResource = Menu | Specials
data MyTool = Search { query :: Text } | Order { item :: Text }

-- Implement handlers. Every handler receives the per-request 'ClientContext'
-- (the caller's bearer token and principal on the HTTP transport) first.
handlePrompt :: ClientContext -> MyPrompt -> IO Content
handlePrompt _ (Recipe idea) = pure $ ContentText $ "Recipe for " <> idea
handlePrompt _ (Shopping items) = pure $ ContentText $ "Shopping list: " <> items

handleResource :: ClientContext -> URI -> MyResource -> IO ResourceContent
handleResource _ uri Menu = pure $ ResourceText uri "text/plain" "Today's menu..."
handleResource _ uri Specials = pure $ ResourceText uri "text/plain" "Daily specials..."

handleTool :: ClientContext -> MyTool -> IO Content
handleTool _ (Search query) = pure $ ContentText $ "Search results for " <> query
handleTool _ (Order item) = pure $ ContentText $ "Ordered " <> item

-- Template Haskell staging: this empty splice ends the declaration group,
-- so the derive splices below can see the types above. (Alternatively,
-- declare the types in a separate module, as the examples/ do.)
$(pure [])

-- Derive handlers automatically
main :: IO ()
main = runMcpServerStdio serverInfo handlers
  where
    serverInfo = McpServerInfo
      { serverName = "My MCP Server"
      , serverVersion = "1.0.0"
      , serverInstructions = "A sample MCP server"
      }
    -- Start from 'noHandlers' and record-update the features you provide:
    -- constructing McpServerHandlers directly breaks (at runtime!) when a
    -- field is missed, and the library grows new handler slots over time.
    handlers = noHandlers
      { prompts = Just $(derivePromptHandler ''MyPrompt 'handlePrompt)
      , resources = Just $(deriveResourceHandler ''MyResource 'handleResource)
      , tools = Just $(deriveToolHandler ''MyTool 'handleTool)
      }

Advanced Template Haskell Features

Automatic Naming Conventions

Constructor names are automatically converted to snake_case for MCP names:

data MyTool = GetValue | SetValue | SearchItems
-- Becomes: "get_value", "set_value", "search_items"

Typed Tool Arguments

Tool arguments are decoded from full JSON values, and 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 = Search
  { 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: the native JSON type and its string representation are both accepted (42 or "42"), since many clients send numbers and booleans as strings.

Prompt arguments are string-valued per the MCP specification, so prompt records are limited to primitive and enumeration fields.

Tool Results

Simple handlers can return plain Content (or Text). Return a full ToolResult for multiple content blocks, structured content, or to report execution failures with isError — which the spec prefers over protocol errors, so the model can see what went wrong and react:

handleTool :: ClientContext -> MyTool -> IO ToolResult
handleTool _ (Search q _ _ _)
  | T.null q  = pure $ toolError "query must not be empty"
  | otherwise = pure $ toolResult [ContentText ("Results for " <> q)]

Prompt handlers can likewise return a PromptResult with a description and a multi-message conversation (user and assistant roles).

Derived Output Schemas

Tools can be typed on the way out too: give the derivation a result record and it derives the tool’s outputSchema (same field rules as inputs — primitives, Maybe, lists, enums, nested records) and serializes your typed values into structuredContent, guaranteed to match the schema. Per the spec’s recommendation, the JSON is also returned as a text content block for clients that predate structured output:

data WeatherReport = WeatherReport
    { temperature :: Int
    , sky         :: Sky          -- enum
    , alerts      :: [Text]
    , humidity    :: Maybe Int    -- optional, 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.

Nested Parameter Types

You can nest parameter types with automatic unwrapping:

-- Parameter record types
data GetValueParams = GetValueParams { _gvpKey :: Text }
data SetValueParams = SetValueParams { _svpKey :: Text, _svpValue :: Text }

-- Main tool type
data SimpleTool
    = GetValue GetValueParams
    | SetValue SetValueParams
    deriving (Show, Eq)

The Template Haskell derivation recursively unwraps single-parameter constructors until it reaches a record type, then extracts all fields for the MCP schema.

Resource URI Generation

Resources automatically get resource:// URIs based on constructor names:

data MyResource = Menu | Specials
-- Generates: "resource://menu", "resource://specials"

Resource Templates

Record constructors become parameterized resource templates (RFC 6570 URI templates), with one percent-decoded path segment per field:

data MyResource
    = Menu                                            -- static: resource://menu
    | ProductDetail { sku :: Text }                   -- resource://product_detail/{sku}
    | OrderItem { orderId :: Int, itemName :: Text }  -- resource://order_item/{orderId}/{itemName}

The read handler derived by deriveResourceHandler matches template URIs (e.g. 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. Advertise the templates via resources/templates/list with:

resourceTemplates = Just $(deriveResourceTemplates ''MyResource)

Argument Completion

Provide a completions handler to serve completion/complete for prompt arguments and resource-template parameters:

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 []

The completions capability is advertised automatically when the handler is present.

Unsupported Patterns

We do not support positional (unnamed) parameters:

-- ❌ This won't work - no field names
data SimpleTool
    = GetValue Int
    | SetValue Int Text

All parameter types must ultimately resolve to records with named fields to generate proper MCP schemas.

Tool Annotations, Icons and Titles

The WithOptions derivation variants take per-constructor DefinitionOptions — description, title, icons, behavioral annotations (which drive client permission UX, e.g. auto-approving read-only tools), and argument descriptions scoped to the constructor:

tools = Just $(deriveToolHandlerWithOptions ''MyTool 'handleTool
  [ ("Search", defaultDefinitionOptions
      { optDescription = Just "Search the catalog"
      , optToolAnnotations = Just defaultToolAnnotations
          { toolReadOnlyHint = Just True, toolIdempotentHint = Just True }
      , optIcons = [icon "https://example.com/search.png"]
      , optFieldDescriptions = [("q", "Search terms")]
      })
  ])

Content blocks can carry annotations too (audience, priority, lastModified), attached with the ContentAnnotated wrapper:

ContentAnnotated defaultAnnotations { annotationsPriority = Just 0.9 }
                 (ContentText "important result")

Custom Descriptions

You can provide custom descriptions for constructors and fields using the *WithDescription variants:

-- Define descriptions for constructors and fields
descriptions :: [(String, String)]
descriptions =
  [ ("Recipe", "Generate a recipe for a specific dish")     -- Constructor description
  , ("Search", "Search our menu database")                  -- Constructor description
  , ("idea", "The dish you want a recipe for")              -- Field description
  , ("query", "Search terms to find menu items")            -- Field description
  ]

-- Use in derivation
handlers = noHandlers
  { prompts = Just $(derivePromptHandlerWithDescription ''MyPrompt 'handlePrompt descriptions)
  , tools = Just $(deriveToolHandlerWithDescription ''MyTool 'handleTool descriptions)
  , resources = Just $(deriveResourceHandlerWithDescription ''MyResource 'handleResource descriptions)
  }

Manual Handler Implementation

For fine-grained control, implement handlers manually:

import MCP.Server

-- Manual handler implementation. Every handler receives the per-request
-- 'ClientContext' as its first argument. Prompt arguments are string-valued
-- (Map Text Text); tool arguments are full JSON values (Map Text Value).
promptListHandler :: ClientContext -> IO [PromptDefinition]
promptGetHandler :: ClientContext -> PromptName -> Map Text Text -> IO (Either Error PromptResult)
-- ... implement your custom logic

main :: IO ()
main = runMcpServerStdio serverInfo handlers
  where
    handlers = noHandlers
      { prompts = Just (promptListHandler, promptGetHandler)
      }

Progress and Per-Request Logging

Handlers can report progress on long-running work and send log messages to the calling client through actions on the ClientContext:

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

Both are safe to call unconditionally:

  • 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 — the spec forbids it otherwise — and drops messages below the declared level.

Delivery is transport-appropriate: on stdio the notifications interleave before the response; on HTTP, a request that opted in is answered with an SSE response stream carrying the notifications followed by the final response (requests that didn’t opt in keep the single-JSON response).

Cancellation

In-flight requests can be cancelled, and per the spec the server then stops work as soon as practical and sends nothing further for that request:

  • stdio: each request runs in its own task; a notifications/cancelled naming its id cancels the task (cancellations for unknown or completed ids are ignored, as required).
  • HTTP: closing the response stream is the cancellation signal. For SSE responses the handler is cancelled as soon as the disconnect is detected (within one keep-alive interval). For single-JSON responses a disconnect is only detected at the final write — the handler runs to completion first — so mid-handler cancellation applies to streaming requests: clients wanting cancellable calls should opt into streaming via a progressToken.

A consequence of cancellable requests: requests are now served concurrently on both transports (stdio previously processed them strictly sequentially). Handlers touching shared mutable state must synchronize (MVar, STM, …) — as was already required for HTTP servers.

Cancellation is delivered to handler code as an asynchronous exception (the standard GHC mechanism, as used by timeout and cancel). Handlers are interruptible wherever they block in IO; a handler that acquires resources must release them with bracket/finally so cancellation cannot leak them:

handleTool ctx (ImportData file) =
    bracket (openFile file ReadMode) hClose $ \h -> do
        ...

Handlers that must not be interrupted mid-operation can shield critical sections with mask, but should keep them short — cancellation waits for them.

Change Notifications

Servers whose tool/prompt/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 :: IO ()
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/subscribe capabilities are advertised automatically where delivery is actually 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 (the lifecycle’s ready signal).
  • Legacy HTTP clients have no delivery channel (this library does not offer the deprecated GET SSE stream), so nothing is advertised to them.

HTTP Transport

The library supports the MCP Streamable HTTP transport. Compile your executable with ghc-options: -threaded — Warp requires the threaded runtime:

import MCP.Server.Transport.Http

-- Simple HTTP server (localhost:3000/mcp)
main = runMcpServerHttp serverInfo handlers

-- Custom configuration
main = runMcpServerHttpWithConfig customConfig serverInfo handlers
  where
    customConfig = defaultHttpConfig
      { httpPort = 8080
      , httpHost = "0.0.0.0"
      , httpEndpoint = "/api/mcp"
      , httpVerbose = True     -- Enable detailed logging
      , httpAllowedOrigins = Just ["https://app.example.com"]
          -- Origin validation (DNS-rebinding protection): requests with an
          -- Origin header outside this list are rejected with 403. Nothing
          -- disables the check (only for servers unreachable from browsers).
      }

Bearer-token authentication (optional): supply an httpAuthorize callback to validate the Authorization: Bearer token each request presents. Return Just principal to authorize (the principal — any JSON Value, e.g. a role — reaches your handlers as clientPrincipal in the ClientContext), or Nothing to reject the request with 401. Token policy lives entirely in your application; the library only threads the identity through:

    customConfig = 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
      }

Features:

  • CORS enabled for web clients
  • POST /mcp for JSON-RPC messages (GET returns 405 — server-to-client notifications flow over the subscriptions/listen POST response stream, not a standalone GET stream)
  • Dual-era protocol support: legacy revisions (2024-11-052025-11-25) negotiate via initialize; the stateless 2026-07-28 revision declares its version per request in _meta, with full request-metadata header validation (MCP-Protocol-Version, Mcp-Method, Mcp-Name including the base64 sentinel encoding)
  • Change notifications as long-lived SSE streams via httpNotifications (see Change Notifications)
  • Optional pluggable bearer-token authentication via httpAuthorize
  • Origin validation via httpAllowedOrigins
  • Cacheability hints for modern list/read results via httpCacheHints

Embedding in an existing WAI stack

runMcpServerHttp starts its own Warp server, but the MCP endpoint is a plain WAI application underneath, and it is exported — so you can mount it inside whatever you already run (your own Warp settings, TLS, middleware, or a larger router):

import MCP.Server (mcpApplication, defaultHttpConfig)
import qualified Network.Wai.Handler.Warp as Warp

main :: IO ()
main = Warp.runSettings mySettings $ \req respond ->
    -- route /mcp to the MCP endpoint, everything else to your app
    mcpApplication defaultHttpConfig serverInfo handlers req respond

httpPort/httpHost are ignored when embedding (they only configure the server runMcpServerHttp starts); the endpoint path, Origin validation, bearer auth and subscriptions/listen streaming all apply as usual.

Conformance corpus

The wire-format fixtures under test/golden/ double as an API-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 (legacy initialize-negotiated revisions and the stateless 2026-07-28 revision), enumerated by a manifest.json. Nothing in the corpus is Haskell-specific — any MCP server implementation that reproduces the small reference server described in the corpus README can replay the requests and diff the responses. Contributions of new cases are welcome.

Roadmap

Design decisions and planned work live as ADRs under specs/, ordered by specs/ROADMAP.md.

Examples

The library includes several examples:

  • examples/Simple/: Basic key-value store using Template Haskell derivation (STDIO)
  • examples/Complete/: Full-featured example with prompts, resources, a resource template, tools (enum/nested/list arguments, isError), and completions (STDIO)
  • examples/HttpSimple/: HTTP version of the simple key-value store

Docker Usage

I like to build and publish my MCP servers to Docker - which means that it’s much easier to configure assistants such as Claude Desktop to run them.

# Build the image
docker build -t haskell-mcp-server .

# Run different examples
docker run -i --entrypoint="/usr/local/bin/simple-example" haskell-mcp-server

And then configure Claude by editing claude_desktop_config.json:

{
    "mcpServers": {
       "simple-example": {
            "command": "docker",
            "args": [
                "run",
                "-i",
                "--entrypoint=/usr/local/bin/simple-example",
                "haskell-mcp-server"
            ]
        }
    }
}

Documentation

Contributing

Contributions are welcome! Please see the issue tracker for open issues and feature requests.

Disclaimer - AI Assistance

I am not sure whether there is any stigma associated with this but Claude helped me write a lot of this library. I started with a very specific specification of what I wanted to achieve and worked shoulder-to-shoulder with Claude to implement and refactor the library until I was happy with it. A few of the features such as the Derive functions are a little out of my comfort zone to have manually written, so I appreciated having an expert guide me here - however I do suspect that this implementation may be sub-par and I do intend to refactor and rewrite large pieces of this through regular maintenance.

License

BSD-3-Clause

Changes

Revision history for mcp-server

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/cancelled cancels 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 a progressToken. Cancellation is delivered as an asynchronous exception, so handlers acquiring resources should use bracket — 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 reportProgress and logToClient on the ClientContext — both safe unconditionally. reportProgress emits notifications/progress only when the request carried a progressToken; logToClient emits notifications/message only when the request declared io.modelcontextprotocol/logLevel (per spec MUST NOT otherwise), filtered to the declared threshold (new LogLevel type, 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: ClientContext carries the two actions and loses its Show/Eq instances; MCP.Server.Handlers.handleMcpMessage takes the transport’s notification sink.

  • Definition metadata (ADR 0006):

    • ToolAnnotationsreadOnlyHint/destructiveHint/idempotentHint/ openWorldHint behavioral hints (2025-03-26+) plus a title, all unset by default (defaultToolAnnotations), carried on ToolDefinition and driving client permission UX.
    • Icon lists (2025-11-25+) on tool, prompt, resource and resource-template definitions.
    • Content Annotations (audience/priority/lastModified, 2025-03-26+) attached via the new ContentAnnotated wrapper, whose annotations merge into the inner block’s JSON (and parse back out).
  • New WithOptions derivations for all five derive families, taking per-constructor DefinitionOptions (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, ResourceDefinition and ResourceTemplateDefinition gain fields, and Content gains the ContentAnnotated constructor. 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’ outputSchema from it (same field rules as input derivation: primitives, Maybe, lists, all-nullary enums, nested records), and serialize the handler’s typed values into structuredContent — the generated serializer mirrors the generated schema, so the two cannot drift. Handlers return the new ToolOutput type: ToolOutput (structured value; the JSON is also returned as a text content block per the spec’s recommendation), ToolOutputWith (custom content blocks), ToolOutputError (isError), or ToolOutputRaw (plain ToolResult escape hatch). Existing ToToolResult handlers are untouched. The conformance corpus gains an echo_structured reference tool with cases in both eras.

  • The HTTP transport’s WAI application is now exported (mcpApplication, re-exported from MCP.Server), so the MCP endpoint can be embedded into an existing WAI stack — your own Warp settings, TLS, middleware or router — instead of transportRunHttp running its own server. httpPort/ httpHost are ignored when embedding; everything else (endpoint path, Origin validation, bearer auth, subscriptions/listen streaming) 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.json pair on disk, enumerated by manifest.json, with the reference server documented in test/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 — on tools/list, prompts/list, resources/list, resources/read and server/discover — the required ttlMs/cacheScope fields (configurable via CacheHints on the transport configs; default: no caching, private). Requests without modern _meta are served byte-identically to before under the revision negotiated by initialize.
  • New server/discover method (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-28 via initialize negotiates down to 2025-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 new anonymousContext builds an empty context.
  • HTTP: modern requests get the 2026-07-28 request-metadata validation — the MCP-Protocol-Version header must match the body’s declared revision, Mcp-Method must match the body method, and Mcp-Name must match params.name/params.uri for tools/call/resources/read/ prompts/get (with =?base64?…?= sentinel decoding); violations return 400 with HeaderMismatch (-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 an McpNotifier with newMcpNotifier, hand its NotificationSource to a transport (stdioNotifications/httpNotifications), and call notifyToolsListChanged/notifyPromptsListChanged/ notifyResourcesListChanged/notifyResourceUpdated when 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/cancelled over stdio). HTTP streams send periodic keep-alive comments and X-Accel-Buffering: no.
  • Legacy stdio clients receive spontaneous untagged notifications after initialize. Capabilities are era- and transport-aware: listChanged is advertised only where delivery is possible (stdio legacy push, or modern subscriptions/listen), and subscribe only to modern clients.
  • defaultHttpConfig is now re-exported from MCP.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. deriveResourceTemplates derives the resources/templates/list handler advertising them; the method carries the modern cacheability envelope.
  • New completions handler slot serving completion/complete for prompt arguments and resource-template parameters (CompletionRef, CompletionResult, capped at 100 values per the spec). The completions capability is advertised automatically.
  • McpServerHandlers gains resourceTemplates and completions fields; the new noHandlers value 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.
  • inputSchema is generated as a real JSON Schema (Schema/SchemaType ADT with enum, items and nested objects), replacing the flat InputSchemaDefinition* types that silently typed every non-primitive field as a string.
  • Tool handlers produce a ToolResult: multiple content blocks, structuredContent, _meta, and isError. Tool execution failures should be reported via isError (see toolError) so the model can see them — per spec — instead of surfacing as JSON-RPC protocol errors. The ToToolResult class keeps simple handlers simple: returning Content or Text still works unchanged.
  • Prompt handlers produce a PromptResult (optional description plus a multi-message conversation with user/assistant roles) via the analogous ToPromptResult class.
  • Content gains audio and resource_link variants; embedded resources now carry their full contents as the spec requires.
  • ToolDefinition gains outputSchema; tools/call responses carry structuredContent.
  • Handler types are fixed to IO — the monad parameter was unusable through the public API (both transports required IO).

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. runMcpServerStdioWithConfig with stdioVerbose = True restores full body logging.
  • JSON-RPC: messages are classified by shape (method/id presence) instead of parse-fallthrough, so a request with a malformed id is answered with an error rather than silently dropped as a notification. Request ids must be integral.
  • HTTP: new httpAllowedOrigins policy on HttpConfig (Origin validation / DNS-rebinding protection, a spec MUST); accepted notifications return 202 with no body; malformed bodies get JSON-RPC error responses; the Access-Control-Allow-Origin header is set consistently on every response and echoes the validated origin (with Vary: 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, and 2026-07-28 requires 405 here).
  • Integer tool arguments bound the scientific-notation exponent (1024, the same bound aeson uses) so a tiny payload like 1e1000000000 cannot force allocation of a gigabyte-sized Integer.

0.1.0.21 - 2026-07-31

  • BREAKING: every handler (prompt/resource/tool; list and get/read/call) now receives a ClientContext as 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: HttpConfig gains an httpAuthorize field — an optional callback that validates the presented Authorization: Bearer token and returns an application-defined principal (Nothing rejects with 401). As it now holds a function, HttpConfig no longer derives Show/Eq.
  • HTTP transport: accept requests without an MCP-Protocol-Version header (the spec says to assume 2025-03-26), exempt initialize from 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-18 clients.
  • initialize now advertises only the capabilities that actually have handlers, so strict clients no longer drop the server when e.g. prompts/list answers “not supported”.
  • CORS: preflight OPTIONS requests are exempt from authorization (browsers send no credentials on preflight) and Authorization is included in Access-Control-Allow-Headers.
  • http-simple-example is now built with -threaded, which Warp requires; previously every request crashed with a TimerManager error.

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-Version header check, which previously rejected anything other than 2025-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.fromList for each argument in map lookup
    • Properly handle argument parse errors (Return InvalidParams error instead of crashing mcp server with error)

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.Handlers module
  • Add MCP.Server.Transport.Http and MCP.Server.Transport.Stdio modules

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.