mit einem Klick
dpella-mcp
// 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.
// 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.
| 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 |
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).
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.
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.
| 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 signatureinitMCPServerState
:: 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 recordServerCapabilities
{ 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 recordEvery field is a Maybe — set only what the server implements:
listResourcesHandler, readResourceHandler, listResourceTemplatesHandlerlistToolsHandler, callToolHandler — don't set manually; use withToolHandlers :: [ToolHandler] -> ProcessHandlers -> ProcessHandlers which wires both based on a list of ToolHandlers.listPromptsHandler, getPromptHandlercompleteHandlersubscribeHandler, unsubscribeHandlerStart from defaultProcessHandlers (all Nothing) and override.
ToolHandler constructiontoolHandler
:: 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)).
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 shapedata 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 requiredMCP 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.
UnliftIO.Async.race_ under the run subcommand only.emanote run --mcp-port PORT (top-level --verbose flips library log level to Debug).[mcp] listening on http://localhost:PORT/mcp via Warp.setBeforeMainLoop.simpleHttpApp (no auth). Authentication deferred per the issue's open question.MCPHandlerState = (), MCPHandlerUser = ().resources and tools capabilities; empty inventories today.emanote/src/Emanote/MCP.hs for the canonical shape.Rollout tracker: srid/emanote#645. Phases 2–5 will add notebook-backed resources, query tools, subscriptions, and optional prompts respectively.
mcp moves faster than nixpkgs' all-cabal-hashes. Pin GitHub sources in flake.nix inputs when the Hackage-via-nixpkgs path refuses to build.newMVar ambiguity: importing Control.Concurrent.MVar alongside Relude produces an ambiguous newMVar. Drop the explicit import — Relude's is fine.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._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.subscribe = Just True until the server actually implements subscribeHandler / unsubscribeHandler. The library has no built-in defaults for those.