Описание
An Awesome MCP Server Framework for Haskell
README
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
- ✅ Change Notifications:
listChanged/resource-update pushes, viasubscriptions/listen(2026-07-28) or legacy stdio delivery - ✅ Progress & Logging:
notifications/progressandnotifications/messagescoped 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:
reportProgressemitsnotifications/progressonly when the request carried aprogressToken(progress values must increase call over call).logToClientemitsnotifications/messageonly when the request declaredio.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/cancellednaming 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/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 (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
/mcpfor JSON-RPC messages (GET returns 405 — server-to-client notifications flow over thesubscriptions/listenPOST response stream, not a standalone GET stream) - 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) - 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
- MCP Specification (2026-07-28)
- MCP Specification (2025-11-25, newest legacy revision)
- API Documentation
- Examples
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
Установка Haskell
У этого сервера нет опубликованного пакета — он собирается из исходников. Открой репозиторий и следуй инструкции в README.
▸ github.com/drshade/haskell-mcp-serverFAQ
Haskell MCP бесплатный?
Да, Haskell MCP бесплатный — установка в пару кликов через Unyly без оплаты.
Нужен ли API-ключ для Haskell?
Нет, Haskell работает без API-ключей и переменных окружения.
Haskell — hosted или self-hosted?
Self-hosted: сервер запускается локально на твоей машине командой из раздела установки.
Как установить Haskell в Claude Desktop, Claude Code или Cursor?
Открой Haskell на unyly.org, выбери вкладку своего клиента (Claude Desktop, Claude Code, Cursor) и нажми Install — конфиг сгенерируется автоматически, без правки JSON.
Похожие MCP
GitHub
PRs, issues, code search, CI status
автор: GitHubFilesystem
Secure file operations with configurable access controls.
Memory
Knowledge graph-based persistent memory system.
Template MCP Server
A CLI tool to create a new Model Context Protocol server project with TypeScript support, dual transport options, and an extensible structure
автор: mcpdotdirectCompare Haskell with
Не уверен что выбрать?
Найди свой стек за 60 секунд
Автор?
Embed-бейдж для README
Похожее
Все в категории development
