feat(mcp): Markdown-native, group-aware MCP server overhaul - #33
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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:
create_noteaccepts a content type (markdown/text) and converts Markdown → the app's rich format, so formatting is no longer escaped/mangled.Plus lifecycle/discovery tools brainstormed alongside.
What changed
New module
src-tauri/src/mdconv.rs— pure Markdown↔HTML conversion:md_to_htmlvia comrak's AST with a custom formatter → Tiptap task-list structure (<ul data-type="taskList">…), correct at arbitrary nesting depth.html_to_mdvia htmd with a DOM element handler for task items →- [ ] / - [x], nesting-safe.title_from_html(strips only genuine leading Markdown markers, unescapes) andwrap_plaintext.src-tauri/src/mcp.rs— data-orientedNoteStoretrait; JSON responses; 11 tools:list_notes(statusactive|archived|trashed|all+groupIdfilters),get_note(formatmarkdown|html|text),search_notes(+ snippet),list_groups.allow_write):create_note,append_note,update_note(full replace + move),create_group,archive_note,delete_note(soft-delete),restore_note.group {id,name,path},contentType,status,updatedAt. Group targeting resolves bygroupIdor case-insensitivegroupName(ambiguity errors).tool_defsadvertises all 11 tools with explicit "content is Markdown (GFM)" descriptions.resources/list/readreturn Markdown and exclude trashed.Security decision: the
format:"html"write path was removed (writes accept markdown/text only) to avoid storing unsanitized HTML that reachesinnerHTML/dangerouslySetInnerHTMLrender sinks;get_notecan still return html on read.Notes
content_typecolumn).Storemethods (set_folder/set_archived/trash_note/restore_note), never the limitedsave_noteON CONFLICT.Testing
cargo test— 151 lib tests pass;cargo fmt --checkclean;cargo clippy --all-targets -- -D warningsclean.npx tsc --noEmitclean;npx vitest run— 231 tests pass.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