Skip to content

MCP: drop per-note resource; clients read files themselves - #746

Merged
srid merged 7 commits into
masterfrom
mcp-drop-note-read-handler
May 26, 2026
Merged

MCP: drop per-note resource; clients read files themselves#746
srid merged 7 commits into
masterfrom
mcp-drop-note-read-handler

Conversation

@srid

@srid srid commented May 26, 2026

Copy link
Copy Markdown
Owner

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

Surface What it returns
emanote://export/metadata JSON metadata for every note (titles, source paths, parent routes, resolved links) — the discovery surface
find_notes / get_backlinks / resolve_wikilink {path, title, …} per match — path is the notebook-relative source path

No per-note emanote://note/{path} resource and no uri field 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; defaultProcessHandlers already leaves the slot Nothing.
  • refactor(hickey,lowy): derive resource dispatch from catalogCatalog.resources :: [(NotebookResource, Model -> ResourceBody)] is now the single source of truth; both listResources and the new readResource lookup derive from it, so adding a future resource is one tuple in Catalog.hs with no parallel branch in Handlers.hs to 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.

Wire change vs. earlier tips of master: this is amending an unreleased feature (2.0.0.0 (Unreleased)). MCP integrations that were tracking master against commits 214480b / 775e9e8 will need to stop calling resources/read emanote://note/{path} (now returns 400) and stop reading the uri field from tool results (no longer emitted). The metadata filePath and the tool path fields are unchanged — those are the canonical handles.

Try it locally

nix run github:srid/emanote/mcp-drop-note-read-handler -- run --port 9010 --mcp-port 8079 -L docs

Generated by /do on Claude Code (model claude-opus-4-7).

srid added 5 commits May 26, 2026 07:52
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.
@srid

srid commented May 26, 2026

Copy link
Copy Markdown
Owner Author

Hickey/Lowy Analysis

# Lens Finding Disposition
1 Hickey Vestigial listResourceTemplatesHandler hard-coded an empty list Fixed in this PR (commit 64c7a1b5)
2 Hickey NoteMatch.uri removal is a silent wire change Fixed in this PR (documented in PR description)
3 Hickey NotebookResource missing renderer made listResources/readResourceHandler invariant doc-only Cross-validated; fixed via catalog-side dispatch table (commit 65430e0e) — embedding the renderer as a record field was rejected by Lowy CV (would leak Model into pure catalog enumeration)
4 Lowy Catalog/Handlers asymmetric with ToolCatalog/Tools because metadataUri/metadataMime exported from Catalog were consumed by Handlers as wire-routing constants Cross-validated; fixed via the same catalog-side dispatch (commit 65430e0e) — moving the constants into Handlers (Lowy's literal recommendation) was rejected by Hickey CV (would create two sources of truth for the URI string)

Cross-validation outcome

Both 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: Catalog.resources :: [(NotebookResource, Model -> ResourceBody)] as a parallel-pair list (not a record field), with listResources projecting fst and readResource performing URI lookup. This keeps the advertising axis (NotebookResource) and the serving axis (Model -> ResourceBody) separately addressable while co-locating them at the catalog boundary.

Hickey rationale

The diff is mostly a deletion: ResourceKind collapsed from a two-constructor sum to nothing, Uri.hs removed, templateFor/allKindShapes/kindMime deleted. Concept count drops with no new abstractions introduced — the Hickey-correct response to a one-inhabitant sum. The follow-up commits address three remaining complecting concerns the deletion alone didn't reach: a vestigial handler override carrying dead structure, a wire-shape change in NoteMatch that needed an explicit contract note for early integrators, and a parallel declaration of "what's listed" vs. "what's served" that the catalog-side dispatch table reduces to one tuple per resource.

Lowy rationale

Volatility map:

  • Catalog-as-advertising (uri, mime, name, title, description) — changed in this PR (description rewritten after per-note removal). Encapsulated in NotebookResource + the catalog tuple.
  • Catalog-as-serving (rendering, encoder, export schema) — changed across phases 2 → 3. Encapsulated in the Model -> ResourceBody half of the catalog tuple.

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 — listResources consumers don't carry serving functions they never call. The asymmetry with the ToolCatalog/Tools split that initially seemed structurally wrong was actually correct once the dispatch became catalog-derived: Catalog now owns both halves of "resource" the same way ToolCatalog owns "query."

@srid

srid commented May 26, 2026

Copy link
Copy Markdown
Owner Author

/do results

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+lowy dominated 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.
  • police at 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.
  • research took 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.
  • implement at 3m 24s — fine for the scope; nothing to optimize.

Workflow completed at 2026-05-26T08:21:00Z.

srid added 2 commits May 26, 2026 08:37
…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.)
@srid
srid marked this pull request as ready for review May 26, 2026 13:35
@srid
srid merged commit 226cdc1 into master May 26, 2026
11 of 14 checks passed
@srid
srid deleted the mcp-drop-note-read-handler branch May 26, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant