dpella/mcp reference
SkillDocs & knowledgeReference 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.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the dpella/mcp reference skill
What this skill tells your AI
The instructions your AI receives, as published by srid/emanote in .agents/skills/dpella-mcp/SKILL.md and read by ahel’s review.
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:
# flake.nix inputs
dpella-mcp.url = "github:dpella/mcp";
dpella-mcp.flake = false;
dpella-jsonrpc.url = "github:dpella/jsonrpc";
dpella-jsonrpc.flake = false;
# 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:
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
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
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,listResourceTemplatesHandlerlistToolsHandler,callToolHandler— don't set manually; usewithToolHandlers :: [ToolHandler] -> ProcessHandlers -> ProcessHandlerswhich wires both based on a list ofToolHandlers.listPromptsHandler,getPromptHandlercompleteHandlersubscribeHandler,unsubscribeHandler
Start from defaultProcessHandlers (all Nothing) and override.
ToolHandler construction
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
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 therunsubcommand only. - CLI flag:
emanote run --mcp-port PORT(top-level--verboseflips library log level toDebug). - Startup line:
[mcp] listening on http://localhost:PORT/mcpviaWarp.setBeforeMainLoop. - Transport:
simpleHttpApp(no auth). Authentication deferred per the issue's open question. - State types:
MCPHandlerState = (),MCPHandlerUser = (). - Handlers advertise
resourcesandtoolscapabilities; empty inventories today. - See
emanote/src/Emanote/MCP.hsfor the canonical shape.
Rollout tracker: srid/emanote#645. Phases 2–5 will add notebook-backed resources, query tools, subscriptions, and optional prompts respectively.
Common pitfalls
- Version tax: Hackage's
mcpmoves faster than nixpkgs'all-cabal-hashes. Pin GitHub sources inflake.nixinputs when the Hackage-via-nixpkgs path refuses to build. newMVarambiguity: importingControl.Concurrent.MVaralongside Relude produces an ambiguousnewMVar. Drop the explicit import — Relude's is fine.NamedFieldPunswith qualified imports:ReadResourceParams {uri}only works ifuriis unqualified. If you importMCP.Serverqualified, either enableNamedFieldPunsand use the unqualified name, or bind fields positionally.- Ambiguous
_meta: field is shared across a dozen records; withDuplicateRecordFieldson, GHC often still demands a qualified prefix (MCP._meta) at construction sites.fourmoluwill re-add the qualifier if removed. - Don't advertise
subscribe = Just Trueuntil the server actually implementssubscribeHandler/unsubscribeHandler. The library has no built-in defaults for those.
Signals
- GitHub stars
- 959
- Forks
- 79
- Last commit
- Jul 2026
Advanced
- Catalog kind
- skill
- Gateway key
dpella-mcp- Source
- github.com/srid/emanote