mcp-server
Library for building Model Context Protocol (MCP) servers
https://github.com/drshade/haskell-mcp-server
| LTS Haskell 24.52: | 0.1.0.20 |
| Stackage Nightly 2026-08-01: | 0.2.0.0 |
| Latest on Hackage: | 0.2.0.0 |
mcp-server-0.2.0.0@sha256:e04b5a634e5c37bcfdf170b206c464c92169022a87418dd1c373472f3af9470d,6616Module documentation for 0.2.0.0
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-05through2025-11-25via theinitializehandshake, and the stateless2026-07-28revision via per-request_meta(includingserver/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
- ✅ 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 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
-- 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"
}
handlers = McpServerHandlers
{ 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).
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.
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 = McpServerHandlers
{ 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 = McpServerHandlers
{ prompts = Just (promptListHandler, promptGetHandler)
, resources = Nothing -- Not supported
, tools = Nothing -- Not supported
}
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/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 after
initialize. - 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 (NEW!)
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
/mcpfor JSON-RPC messages (GET returns 405 — this library does not offer server-initiated SSE streams) - Dual-era protocol support: legacy revisions (
2024-11-05–2025-11-25) negotiate viainitialize; the stateless2026-07-28revision declares its version per request in_meta, with full request-metadata header validation (MCP-Protocol-Version,Mcp-Method,Mcp-Nameincluding the base64 sentinel encoding) - Optional pluggable bearer-token authentication via
httpAuthorize - Origin validation via
httpAllowedOrigins - Cacheability hints for modern list/read results via
httpCacheHints
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, and tools (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.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.