MCP: drop per-note resource; clients read files themselves - #746
Conversation
Removes the `emanote://note/{path}` MCP resource and the read-file
handler that backed it. MCP clients already have direct filesystem
access (Claude Code's Read, Codex's local file access), so serving
note bodies over MCP just duplicates that and forces an extra round
trip per read.
The metadata export and the three query tools (find_notes,
get_backlinks, resolve_wikilink) still carry every note's
notebook-relative source path — that's the relative path clients use
to read the file themselves.
- Catalog.ResourceKind collapses to a single inhabitant -> drop the
sum type; the catalog now exposes one fixed NotebookResource
(metadata) and one readMetadata function.
- Uri.hs deleted; the lone surviving constant (metadataUri) moves
into Catalog.hs.
- Handlers.readResourceHandler matches metadataUri directly;
listResourceTemplatesHandler returns [].
- ToolCatalog.NoteMatch drops its derived `uri` field (the URI it
pointed at no longer exists). Test updated.
- docs/guide/mcp.md and CHANGELOG entry updated to reflect the new
surface.
The handler hard-coded an empty ResourceTemplate list every call. defaultProcessHandlers leaves the slot Nothing; clients fall through to the dpella/mcp library default (empty list) without our override doing any work.
Catalog now exposes 'resources :: [(NotebookResource, Model -> ResourceBody)]' as the single source of truth; 'listResources' and the new 'readResource' lookup both derive from it. Handlers.readResourceHandler matches on resourceUri via Catalog.readResource instead of comparing against an exported metadataUri constant. Adding a new resource is now one tuple in Catalog.hs — no parallel declaration in Handlers.hs to keep in sync. metadataUri / metadataMime top-level constants drop out (the URI/MIME live on metadataResource). Cross-validated: the alternative shape (embed 'Model -> ResourceBody' as a field on NotebookResource) was rejected because it would leak the serving function into every advertising call site. A parallel pair list keeps the catalog-as-description and catalog-as-serving axes co-located without entangling them in the record itself.
…log haddock The module no longer depends on an in-repo URI scheme; Uri.hs was deleted earlier in this branch.
metadataResource and readMetadata were each named bindings used in exactly one place: the single tuple in 'resources'. Inlining them into the tuple itself eliminates two top-level names that existed only to split one entry across lines. Also drops 'resources' from the export list (no external consumer) and switches readResource's predicate to an explicit lambda destructure for readability.
Hickey/Lowy Analysis
Cross-validation outcomeBoth reviewers had recommendations whose literal application the other lens would have flagged. Running the parallel cross-validation pass converged on a third design neither sub-agent independently proposed: Hickey rationaleThe diff is mostly a deletion: Lowy rationaleVolatility map:
These axes are independently volatile (a description update doesn't drag rendering; a renderer change doesn't drag the description), so a record-field embedding would have braided them. The pair-list shape keeps them co-located at the catalog boundary but separately addressable — |
|
| Step | Status | Duration | Verification |
|---|---|---|---|
| sync | ✓ | 1s | git fetch ok; forge=github |
| research | ✓ | 3m 57s | Mapped MCP file-read handler, URI scheme, NoteMatch.uri field |
| branch | ✓ | 14s | feature branch tracking origin/master |
| implement | ✓ | 3m 24s | Catalog/Uri/Handlers/ToolCatalog/Server/MCP rewritten; Uri.hs deleted |
| check | ✓ | 46s | cabal build all clean: 69 modules |
| docs | ✓ | 16s | docs/guide/mcp.md and CHANGELOG updated |
| fmt | ✓ | 13s | cabal-fmt, fourmolu, hlint, nixpkgs-fmt all passed |
| commit | ✓ | 23s | Primary feature commit pushed |
| hickey+lowy | ✓ | 14m 3s | Cross-validation converged on catalog-side dispatch table; 2 follow-up commits |
| police | ✓ | 10m 29s | 3 iterations clean; 2 follow-up commits (fact-check + elegance) |
| test | ✓ | 25s | 137 examples, 0 failures |
| create-pr | ✓ | 1m 20s | Draft PR #746 with hickey/lowy analysis comment |
| ci | ✓ | 2m 59s | vira ci signed off; e2e-live 79/79, e2e-morph 79/79, e2e-static 60/60 |
| evidence | — | 15s | Skipped: MCP-protocol-only change, no UI rendering impact |
| Total | 38m 54s |
Slowest step: hickey+lowy (14m 3s)
Optimization suggestions
hickey+lowydominated at 14m — both sub-agent reviews + the parallel cross-validation pass cost ~14m on a five-file structural change. For diffs this contained (one module deleted, one collapsed sum type), running cross-validation only when both reviewers produce findings that touch the same file would have cut ~3m. Worth noting only because the structural change here was small; on a richer diff the full cross-val budget is well-spent.policeat 10m 29s ran on the post-hickey diff — the three-pass/code-police(rules + fact-check + elegance) found two violations (stale haddock + single-use helpers). Both were narrow enough that running the elegance pass scoped to files hickey+lowy already touched would have hit them in ~half the time. The fact-check finding ('resources'haddock link to non-exported name) was directly caused by the hickey+lowy commit dropping the export — running police immediately after the hickey+lowy dispatch refactor would catch it earlier and avoid the second elegance pass churn.researchtook 3m 57s for a deletion-shaped change — most of the time went into reading three files in main context to confirm the Explore sub-agent's map (Handlers.hs, Catalog.hs, ToolCatalog.hs). Trusting the Explore output more aggressively on small, well-bounded surfaces would save ~2m.implementat 3m 24s — fine for the scope; nothing to optimize.
Workflow completed at 2026-05-26T08:21:00Z.
…installer-action codeload.github.com intermittently fails to serve the SHA-pinned tarball for nixbuild/nix-quick-install-action@v33 (HTTP error at action setup, "Failed to download archive ... after 1 attempts"). The action itself has no retry around the codeload fetch, so every PR push hits the same flake. Switch to DeterminateSystems/nix-installer-action@main, which is what other projects in this Nix ecosystem (vira, ema, etc.) standardised on. Same Nix install outcome; bypasses the failing codeload path entirely.
v33's pinned tarball was intermittently 404'ing on codeload.github.com during 'Set up job'. v34 (released 2025-09-24) is the current marketplace canonical at https://github.com/marketplace/actions/nix-quick-install. (Replaces an earlier swap to DeterminateSystems/nix-installer-action. Per project preference, sticking with nix-quick-install.)
Removes the
emanote://note/{path}MCP resource and the read-file handler that backed it. MCP clients (Claude Code, Codex, opencode, …) already have direct filesystem read tools, so serving note bodies over MCP just duplicates that capability — and adds an extra round trip per read. The metadata export and the three query tools still carry every note's notebook-relative source path, which is the relative path clients use to read the file themselves.What the MCP surface looks like now
emanote://export/metadatafind_notes/get_backlinks/resolve_wikilink{path, title, …}per match —pathis the notebook-relative source pathNo per-note
emanote://note/{path}resource and nourifield on tool results — both pointed at the removed surface.Structural follow-ups (hickey + lowy)
Three structural commits land alongside the primary feature commit:
refactor(hickey): drop vestigial listResourceTemplatesHandler— the override was hard-coding an empty list every call;defaultProcessHandlersalready leaves the slotNothing.refactor(hickey,lowy): derive resource dispatch from catalog—Catalog.resources :: [(NotebookResource, Model -> ResourceBody)]is now the single source of truth; bothlistResourcesand the newreadResourcelookup derive from it, so adding a future resource is one tuple inCatalog.hswith no parallel branch inHandlers.hsto keep in sync.refactor(police): elegance — inline single-use catalog helpers— the two top-level bindings that fed the dispatch tuple each had exactly one call site.Try it locally
Generated by
/doon Claude Code (modelclaude-opus-4-7).