Reference for the dpella/mcp Haskell library — types, handler shape, transports, and the conventions Emanote uses. Load this before adding MCP resources/tools/subscriptions so you don't re-discover the API by WebFetch.
Scanned 9/12/2026
Install to Claude Code
npx -y skills add srid/emanote --skill dpella-mcp --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Dpella Mcp?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/srid-dpella-mcp-accecc13)More formats (shields.io, HTML) on the badges page.
---
name: dpella-mcp
description: Reference for the dpella/mcp Haskell library — types, handler shape, transports, and the conventions Emanote uses. Load this before adding MCP resources/tools/subscriptions so you don't re-discover the API by WebFetch.
user-invocable: false
---
# dpella/mcp reference
Canonical source: <https://github.com/dpella/mcp>. Two Hackage packages: `mcp-types` (pure types, minimal deps) and `mcp` (Servant-based server with HTTP/Stdio transports). MCP protocol version `2025-06-18`. Depends on `jsonrpc` (also a DPella package, not in nixpkgs).
## Versions to pin
Use GitHub sources rather than Hackage when the target GHC's `all-cabal-hashes` snapshot lags behind:
```nix
# flake.nix inputs
dpella-mcp.url = "github:dpella/mcp";
dpella-mcp.flake = false;
dpella-jsonrpc.url = "github:dpella/jsonrpc";
dpella-jsonrpc.flake = false;
```
```nix
# haskell-flake
packages.mcp.source = inputs.dpella-mcp + /mcp-server;
packages.mcp-types.source = inputs.dpella-mcp + /mcp-types;
packages.jsonrpc.source = inputs.dpella-jsonrpc;
```
Supported GHC: 9.6–9.12 (via `base >=4.18 && <4.22`). `servant-auth-server` 0.4.x and `warp` 3.4.x are sibling deps.
## Required type-family instances
Every user of `MCP.Server` must provide both:
```haskell
type instance MCPHandlerState = YourSessionState
type instance MCPHandlerUser = YourUserPayload -- for JWT; use () when using simpleHttpApp
```
Instances must live in a module that gets compiled before `initMCPServerState` is called.
## Three transports
| Transport | Auth | Entry point | Notes |
|---|---|---|---|
| HTTP + JWT | servant-auth-server | `mcpAPI stateVar` via `serveWithContext (Proxy @MCPAPI) ctx` | Production web |
| Simple HTTP | none | `simpleHttpApp stateVar :: Application` | Local/loopback only; pair with `Warp.run` or `Warp.runSettings` |
| Stdio | none | `serveStdio stdin stdout initialState` | Subprocess integrations |
For HTTP use `Warp.setBeforeMainLoop` to log after the socket is bound — do not print before `Warp.run`/`runSettings`, the bind hasn't happened yet and the log lies.
## `initMCPServerState` signature
```haskell
initMCPServerState
:: MCPHandlerState -- initial state
-> Maybe (MCPHandlerUser -> MCPHandlerState -> IO MCPHandlerState) -- onInitialize (JWT only)
-> Maybe (MCPHandlerState -> IO MCPHandlerState) -- onFinalize (after each req)
-> ServerCapabilities -- what to advertise
-> Implementation -- name/version/title
-> Maybe Text -- free-text instructions
-> ProcessHandlers -- the actual handlers
-> MCPServerState
```
Wrap the result in an `MVar` — `simpleHttpApp` / `mcpAPI` both take `MVar MCPServerState`.
## `ServerCapabilities` record
```haskell
ServerCapabilities
{ logging :: Maybe LoggingCapability
, prompts :: Maybe PromptsCapability { listChanged :: Maybe Bool }
, resources :: Maybe ResourcesCapability { listChanged, subscribe :: Maybe Bool }
, tools :: Maybe ToolsCapability { listChanged :: Maybe Bool }
, completions :: Maybe CompletionsCapability
, experimental :: Maybe ...
}
```
Advertising `Just …` without providing a matching handler means clients will call it and hit the library's `method_not_found`. For Phase-1-style stubs, provide an empty-list handler (e.g. `listResourcesHandler = Just (\_ -> pure $ ProcessSuccess ListResourcesResult{..})`).
## `ProcessHandlers` record
Every field is a `Maybe` — set only what the server implements:
- `listResourcesHandler`, `readResourceHandler`, `listResourceTemplatesHandler`
- `listToolsHandler`, `callToolHandler` — **don't set manually**; use `withToolHandlers :: [ToolHandler] -> ProcessHandlers -> ProcessHandlers` which wires both based on a list of `ToolHandler`s.
- `listPromptsHandler`, `getPromptHandler`
- `completeHandler`
- `subscribeHandler`, `unsubscribeHandler`
Start from `defaultProcessHandlers` (all `Nothing`) and override.
## `ToolHandler` construction
```haskell
toolHandler
:: Text -- name
-> Maybe Text -- description
-> InputSchema -- JSON schema for args
-> (Maybe (Map Text Value) -- handler body
-> MCPServerT (ProcessResult CallToolResult))
-> ToolHandler
```
`InputSchema "object" (Just propsMap) (Just ["req1", "req2"])` is the typical shape. `withToolHandlers` validates required args before calling the handler.
Returning results: `toolTextResult [Text]` for plain text; build `CallToolResult` directly when you need structured output (`structuredContent :: Maybe (Map Text Value)`).
## Logging
`mcp_log_level :: Maybe LoggingLevel` on `MCPServerState` — defaults to `Just Warning`. Set to `Just Debug` to get one `[request]` / `[response]` stdout line per JSON-RPC call (see `MCP/Server/HTTP/Internal.hs`). The library does **not** emit anything else on its own; wire your own startup log via Warp's `setBeforeMainLoop`.
Clients can also change the level at runtime via `logging/setLevel` if the server advertises `logging = Just LoggingCapability`.
## `ProcessResult` shape
```haskell
data ProcessResult a
= ProcessSuccess a
| ProcessRPCError Int Text -- JSON-RPC error (400-ish, 404-ish, …)
| ProcessServerError Text -- internal
| ProcessClientInput Text Value cont -- request additional input from client (sampling, elicitation)
```
Return `ProcessRPCError 404 ("resource not found: " <> uri)` for unknown URIs; the library turns this into a well-formed JSON-RPC error.
## `DuplicateRecordFields` is required
MCP types reuse `name`, `_meta`, `title`, etc. across many records (`Implementation`, `Tool`, `Prompt`, `Resource`, …). Enable `{-# LANGUAGE DuplicateRecordFields #-}` at the use site. Field disambiguation sometimes needs a qualified prefix: `MCP.name = …`, `MCP._meta = …` where `import MCP.Server qualified as MCP`.
## Emanote-specific conventions (phase 1)
- MCP runs beside the live server via `UnliftIO.Async.race_` under the `run` subcommand only.
- CLI flag: `emanote run --mcp-port PORT` (top-level `--verbose` flips library log level to `Debug`).
- Startup line: `[mcp] listening on http://localhost:PORT/mcp` via `Warp.setBeforeMainLoop`.
- Transport: `simpleHttpApp` (no auth). Authentication deferred per the issue's open question.
- State types: `MCPHandlerState = ()`, `MCPHandlerUser = ()`.
- Handlers advertise `resources` and `tools` capabilities; empty inventories today.
- See `emanote/src/Emanote/MCP.hs` for the canonical shape.
Rollout tracker: [srid/emanote#645](https://github.com/srid/emanote/issues/645). Phases 2–5 will add notebook-backed resources, query tools, subscriptions, and optional prompts respectively.
## Common pitfalls
1. **Version tax**: Hackage's `mcp` moves faster than nixpkgs' `all-cabal-hashes`. Pin GitHub sources in `flake.nix` inputs when the Hackage-via-nixpkgs path refuses to build.
2. **`newMVar` ambiguity**: importing `Control.Concurrent.MVar` alongside Relude produces an ambiguous `newMVar`. Drop the explicit import — Relude's is fine.
3. **`NamedFieldPuns` with qualified imports**: `ReadResourceParams {uri}` only works if `uri` is unqualified. If you import `MCP.Server` qualified, either enable `NamedFieldPuns` and use the unqualified name, or bind fields positionally.
4. **Ambiguous `_meta`**: field is shared across a dozen records; with `DuplicateRecordFields` on, GHC often still demands a qualified prefix (`MCP._meta`) at construction sites. `fourmolu` will re-add the qualifier if removed.
5. **Don't advertise `subscribe = Just True`** until the server actually implements `subscribeHandler` / `unsubscribeHandler`. The library has no built-in defaults for those.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!