packages feed

mcp-server-0.2.0.1: README.md

# mcp-server

A fully-featured Haskell library for building [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) 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:

```cabal
build-depends:
  mcp-server
```

Create a simple module, such as this example below:

```haskell
{-# 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:

```haskell
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:

```haskell
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:

```haskell
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:

```haskell
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:

```haskell
-- 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:

```haskell
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:

```haskell
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:

```haskell
resourceTemplates = Just $(deriveResourceTemplates ''MyResource)
```

#### Argument Completion

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

```haskell
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:

```haskell
-- ❌ 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:

```haskell
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:

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

## Custom Descriptions

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

```haskell
-- 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:

```haskell
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`:

```haskell
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:

```haskell
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:

```haskell
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:

```haskell
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:

```haskell
    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-05`–`2025-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](#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](https://hackage.haskell.org/package/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):

```haskell
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/`](test/golden/README.md) 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/`](specs/README.md), ordered by [`specs/ROADMAP.md`](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.

```bash
# 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`:

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

## Documentation

- [MCP Specification (2026-07-28)](https://modelcontextprotocol.io/specification/2026-07-28/)
- [MCP Specification (2025-11-25, newest legacy revision)](https://modelcontextprotocol.io/specification/2025-11-25/)
- [API Documentation](https://hackage.haskell.org/package/mcp-server)
- [Examples](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