Skip to content
cccskills
FreePublished on GitHub

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.

The English summary is being prepared. Showing the original description.

See how to install

Files included (1)

  • SKILL.md7.8 KB

SKILL.md (original)

Review the instructions the agent will receive before you install.

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:

# 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

TransportAuthEntry pointNotes
HTTP + JWTservant-auth-servermcpAPI stateVar via serveWithContext (Proxy @MCPAPI) ctxProduction web
Simple HTTPnonesimpleHttpApp stateVar :: ApplicationLocal/loopback only; pair with Warp.run or Warp.runSettings
StdiononeserveStdio stdin stdout initialStateSubprocess 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, listResourceTemplatesHandler
  • listToolsHandler, callToolHandler — don't set manually; use withToolHandlers :: [ToolHandler] -> ProcessHandlers -> ProcessHandlers which wires both based on a list of ToolHandlers.
  • listPromptsHandler, getPromptHandler
  • completeHandler
  • subscribeHandler, 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 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. 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.

Reviews

No reviews yet. Tell others what you thought.

Comments

No login needed. Share how it worked for you, ask a question, or note where it did not run.

No comments yet. Be the first to write one.

Comments are published right away. Do not include personal information such as email addresses or phone numbers. Up to 2 URLs.

0 / 1,000 characters

This form is protected by reCAPTCHA, and the Google Privacy Policy and Terms of Service apply.

More skills from this repository

be

Free

Modern, interactive alternative to `/do` — clarify intent up front, then take a task end-to-end with a serial AI review gauntlet (lens debate (lowy ⇄ hickey) → codex debate → simplify → code-police, each editing the branch in turn) → CI → evidence. ONLY invoke when the user explicitly types `/be` or `$be`; never auto-select from a natural-language request.

The English summary is being prepared. Showing the original description.

srid/emanote965Updated Jul 26, 2026

Run /be's review gauntlet SERIALLY — /lens-debate (lowy ⇄ hickey), then /codex-debate, then /simplify, then code-police, each editing and committing on the live branch in turn. Use from /be §4, or when the user asks to "run the review gauntlet". Requires Claude Code's Skill tool.

The English summary is being prepared. Showing the original description.

srid/emanote965Updated Jul 26, 2026

ci

Free

Reference for the `odu` runner — how to invoke a full pipeline, a single recipe, or a platform-pinned node, and how to attach to a live run, from a project whose CI odu runs. Trigger when the user asks to "run CI", "run the pipeline", "re-run a check", to run named lanes or recipes (e.g. "run fmt and nix", "just the e2e lane", bare selectors like `fmt`/`nix`/`e2e`), or names a recipe by `<recipe>@<platform>`. This skill — not a repo's local `just ci` / `just <recipe>` — is how an odu-run request is served.

The English summary is being prepared. Showing the original description.

srid/emanote965Updated Jul 26, 2026

Review code for quality, simplicity, and common mistakes before declaring work complete.

The English summary is being prepared. Showing the original description.

srid/emanote965Updated Jul 26, 2026

Run an automated codex⇄Claude debate to consensus — no round cap, no deadlock exit. Two explicit subcommands. `review` (also the bare/back-compat default) — codex (reviewer) critiques the current diff and a Claude subagent (author) fixes/disputes, looping until they agree. `answer` — Claude and codex each answer a freeform prompt in parallel, then cross-check until they agree, and a unified answer is returned. Use when the user types `/codex-debate`, asks to "have codex review this", "run the codex debate", "review this PR with codex", "argue this with codex until you agree", or passes a question to "have Claude and codex debate/answer until they agree".

The English summary is being prepared. Showing the original description.

srid/emanote965Updated Jul 26, 2026

do

Free

Do a task end-to-end — implement, PR, CI loop, ship. ONLY invoke when the user explicitly types `/do` or `$do`; never auto-select from a natural-language request, even one that sounds like an end-to-end task.

The English summary is being prepared. Showing the original description.

srid/emanote965Updated Jul 26, 2026

See all skills by srid

Report a problem with this skill