Skip to content

feat(mcp): Markdown-native, group-aware MCP server overhaul - #33

Merged
DGINXREAL merged 18 commits into
mainfrom
feat/mcp-server-overhaul
Aug 6, 2026
Merged

feat(mcp): Markdown-native, group-aware MCP server overhaul#33
DGINXREAL merged 18 commits into
mainfrom
feat/mcp-server-overhaul

Conversation

@DGINXREAL

Copy link
Copy Markdown
Member

Summary

Overhauls the Notefix MCP server so local AI clients (e.g. Claude Desktop) can work with notes properly. Notes are stored as Tiptap HTML; the MCP boundary now speaks Markdown in both directions, every list/get/search response is JSON, and groups (folders) are first-class. Adds a full note lifecycle.

Delivers the four original asks:

  • Notes now return their group and contentType.
  • create_note accepts a content type (markdown/text) and converts Markdown → the app's rich format, so formatting is no longer escaped/mangled.
  • Notes can be created inside a group (by id or name).

Plus lifecycle/discovery tools brainstormed alongside.

What changed

New module src-tauri/src/mdconv.rs — pure Markdown↔HTML conversion:

  • md_to_html via comrak's AST with a custom formatter → Tiptap task-list structure (<ul data-type="taskList">…), correct at arbitrary nesting depth.
  • html_to_md via htmd with a DOM element handler for task items → - [ ] / - [x], nesting-safe.
  • title_from_html (strips only genuine leading Markdown markers, unescapes) and wrap_plaintext.

src-tauri/src/mcp.rs — data-oriented NoteStore trait; JSON responses; 11 tools:

  • Reads: list_notes (status active|archived|trashed|all + groupId filters), get_note (format markdown|html|text), search_notes (+ snippet), list_groups.
  • Writes (gated by allow_write): create_note, append_note, update_note (full replace + move), create_group, archive_note, delete_note (soft-delete), restore_note.
  • Every note carries group {id,name,path}, contentType, status, updatedAt. Group targeting resolves by groupId or case-insensitive groupName (ambiguity errors). tool_defs advertises all 11 tools with explicit "content is Markdown (GFM)" descriptions. resources/list/read return Markdown and exclude trashed.

Security decision: the format:"html" write path was removed (writes accept markdown/text only) to avoid storing unsanitized HTML that reaches innerHTML/dangerouslySetInnerHTML render sinks; get_note can still return html on read.

Notes

  • No DB migration (status is derived from existing fields; no content_type column).
  • Desktop-only MCP surface unchanged; mobile gating untouched.
  • Mutations route through the correct Store methods (set_folder/set_archived/trash_note/restore_note), never the limited save_note ON CONFLICT.

Testing

  • cargo test — 151 lib tests pass; cargo fmt --check clean; cargo clippy --all-targets -- -D warnings clean.
  • npx tsc --noEmit clean; npx vitest run — 231 tests pass.
  • Built test-first task-by-task with per-task + whole-branch review; task-list/nested round-trips and group/status/write-gating covered.

Design & plan: docs/superpowers/specs/2026-08-05-mcp-server-overhaul-design.md, docs/superpowers/plans/2026-08-05-mcp-server-overhaul.md.

🤖 Generated with Claude Code

DGINXREAL and others added 18 commits August 5, 2026 20:24
Markdown-native, group-aware notes API with full note lifecycle
(create/append/update/archive/trash/restore, create/list groups),
JSON responses, and a Rust Markdown<->HTML converter.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
12 TDD tasks: mdconv module (markdown<->html with Tiptap task-list
fidelity), NoteStore trait redesign, JSON read tools, markdown-aware
write tools, group + lifecycle tools, tool_defs rewrite, verification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The regex-based rewrite from the previous commit broke on two realistic
shapes: mixed lists (plain items alongside task items in one <ul> left
the <ul> untagged) and nested task lists (a lazy regex match swallowed
the nested <ul> and left its raw <input> unstripped). Replace the
string post-process with a comrak create_formatter! override for
NodeValue::List/TaskItem, which handles arbitrary nesting for free
since each level is a separate, independently-computed AST subtree.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Adds html_to_md (Tiptap HTML -> GFM Markdown) via htmd, the inverse of
md_to_html. htmd 0.5.5 has no <input> element handler and escapes
literal [ ]/[x] text, so task items round-trip through placeholder
tokens (pre_tasks) swapped for real checkbox markers after conversion.
Replace the regex-based pre_tasks/sentinel-token hack with a custom
htmd `<li>` element handler registered via HtmlToMarkdownBuilder. The
regex approach's lazy `<li ...>(.*?)</li>` match couldn't tell a task
item's own closing tag from a nested task item's, silently dropping
the nested item's checked state and demoting it to a plain bullet.
The DOM-based handler runs once per <li> as htmd walks the parsed
tree, so nesting depth no longer matters, and drops the sentinel
tokens entirely as a side effect.
Adds title_from_html and wrap_plaintext boundary helpers to mdconv,
plus a round-trip test verifying md_to_html -> html_to_md preserves
headings and task-list structure.
Only strip a leading #/>/-/*/+ when it's followed by whitespace (real
block syntax), and unescape any backslash htmd introduced for
ambiguous literal text, so titles like "#hashtag" or "-1 lemons"
survive intact instead of losing their leading character or carrying
a stray backslash.
Replace the text-only NoteAccess trait with NoteStore, which returns
Note/Folder structs and exposes the dedicated mutation primitives
(save/set_folder/set_archived/trash/untrash/list_folders/create_folder)
that later tasks' tool handlers need. StoreAccess now maps directly onto
Store methods; the test Fake mirrors save_note's update-only-content
semantics. call_tool/handle_rpc are temporarily adapted to compile
(list_notes returns id-only JSON; resources/list and resources/read now
use title_from_html/html_to_md). Full tool logic lands in Tasks 7-11.
html_to_md and title_from_html gained real non-test callers when
mcp.rs::handle_rpc wired resources/list and resources/read to them.
Implement list_notes, get_note, search_notes, list_groups in call_tool
using the Task 7 helpers, plus a snippet helper for search results.
Adds create_note, append_note, and update_note write arms to call_tool
plus a shared to_html helper, per Task 9 of the MCP server overhaul.
update_note keeps content-replace (save) and group-move (set_folder)
as independent paths so a move never touches content and a replace
never touches folder_id.
- update_note: resolve groupId/groupName before saving content, matching
  create_note's ordering, so an invalid group leaves the note untouched
  instead of persisting content while returning an error.
- Drop the format:"html" write override (create_note/append_note/
  update_note): a write can no longer store caller-supplied HTML verbatim,
  per product/security decision. get_note is unaffected.
- Clamp snippet()'s start index to chars.len() to avoid a panic when
  to_lowercase() expands a character's char count (e.g. 'İ').
- Remove stale #[allow(dead_code)] from md_to_html/wrap_plaintext now that
  both have real callers via mcp::to_html.
- resources/read now treats a trashed note as not-found, consistent with
  resources/list's existing filter (the get_note tool is unaffected).

Adds/updates covering tests for each finding (151 lib tests passing, up
from 146). cargo fmt --check and cargo clippy --all-targets -- -D warnings
stay clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@DGINXREAL
DGINXREAL merged commit 2a895d9 into main Aug 6, 2026
2 checks passed
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