diff --git a/docs/superpowers/plans/2026-08-05-mcp-server-overhaul.md b/docs/superpowers/plans/2026-08-05-mcp-server-overhaul.md new file mode 100644 index 0000000..3da6a85 --- /dev/null +++ b/docs/superpowers/plans/2026-08-05-mcp-server-overhaul.md @@ -0,0 +1,1630 @@ +# MCP Server Overhaul Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Turn Notefix's MCP server into a Markdown-native, group-aware notes API with a full note lifecycle (create/append/update/archive/trash/restore, create/list groups), JSON responses, and a Rust Markdown↔HTML converter that preserves Tiptap task lists. + +**Architecture:** Three layers inside `src-tauri/src/`. (1) A new pure `mdconv` module does Markdown↔HTML conversion with no knowledge of storage or MCP. (2) A redesigned `NoteStore` trait is the test seam: a real `StoreAccess` impl backed by SQLite + a `Fake` in-memory impl for tests. (3) The `call_tool` handler in `mcp.rs` orchestrates conversion, group resolution, status filtering, and JSON building on top of the trait. Content crosses the MCP boundary as Markdown; every list/get/search response is a JSON string in the tool's text block. + +**Tech Stack:** Rust (edition 2021), axum JSON-RPC, rusqlite (SQLite), serde_json. New crates: `comrak` (Markdown→HTML, GFM) and `htmd` (HTML→Markdown). Frontend unchanged (TypeScript/React/Tiptap). + +## Global Constraints + +- Rust edition **2021**; crate is `notefix_lib` under `src-tauri/`. +- Rust tests run with: `cd src-tauri && cargo test`. A single test: `cargo test `. +- Formatting/lint gates: `cd src-tauri && cargo fmt` and `cargo clippy --all-targets -- -D warnings` must pass. +- Frontend gates must stay green: from repo root `npx tsc --noEmit` and `npx vitest run`. +- The MCP server is **desktop-only** and already hidden on mobile — do not add mobile UI or gating; no frontend changes are expected in this plan. +- Notes are stored as **Tiptap HTML** in SQLite. There is **no** `content_type`/`format`/`isMarkdown` column and this plan adds none (no DB migration). +- Tiptap task-list HTML format (canonical round-trip target, matches `src/markdown.ts`): + ``. +- Note status is **derived**, never a column: `deleted_at.is_some()` → `"trashed"`; else `archived` → `"archived"`; else `"active"`. +- `Store::save_note` upserts, but on an **existing** row its `ON CONFLICT` updates only `content`, `updated_at`, `dirty`. To change folder/archived/deleted you MUST use the dedicated methods (`set_folder`, `set_archived`, `trash_note`, `restore_note`). Never try to move/archive/trash a note by mutating a `Note` and calling `save_note`. +- All JSON responses are returned as the tool result's `content[0].text` (a serialized JSON string), preserving the existing `{ "content": [{ "type": "text", "text": … }] }` envelope. Errors keep the existing `isError: true` text-content shape. +- Response field names are **camelCase** (matches serde `rename_all = "camelCase"` and the TS types). + +--- + +## File Structure + +- **Create** `src-tauri/src/mdconv.rs` — pure Markdown↔HTML conversion + helpers. One responsibility: format conversion. No storage/MCP imports. +- **Modify** `src-tauri/src/mcp.rs` — redesigned `NoteStore` trait, rewritten `call_tool` handler, JSON builders, `StoreAccess` impl, `tool_defs`, resources handlers, and `#[cfg(test)]` module (`Fake` + handler tests). +- **Modify** `src-tauri/src/lib.rs` — add `mod mdconv;` (single line; verify `apply(...)` call is unaffected). +- **Modify** `src-tauri/Cargo.toml` — add `comrak` and `htmd` dependencies. +- **Unchanged** `storage.rs`, `folders.rs`, `migrate.rs`, `commands.rs` (we consume their existing APIs), and all of `src/` (frontend). + +--- + +## Task 1: Add Markdown conversion dependencies + +**Files:** +- Modify: `src-tauri/Cargo.toml:20-43` (`[dependencies]`) + +**Interfaces:** +- Produces: `comrak` and `htmd` crates available to the build. + +- [ ] **Step 1: Add the crates** + +Run from `src-tauri/`: + +```bash +cargo add comrak htmd +``` + +This appends latest-compatible versions under `[dependencies]` and updates `Cargo.lock`. + +- [ ] **Step 2: Verify the project still builds** + +Run: `cd src-tauri && cargo build` +Expected: builds successfully (downloads `comrak`/`htmd` and transitive deps). + +- [ ] **Step 3: Commit** + +```bash +git add src-tauri/Cargo.toml src-tauri/Cargo.lock +git commit -m "build: add comrak + htmd for MCP markdown conversion" +``` + +--- + +## Task 2: `mdconv` — core Markdown → HTML + +**Files:** +- Create: `src-tauri/src/mdconv.rs` +- Modify: `src-tauri/src/lib.rs` (add `mod mdconv;` next to the other `mod` declarations) + +**Interfaces:** +- Produces: `pub fn md_to_html(md: &str) -> String` — GFM Markdown → HTML. Task-list post-processing is added in Task 3; this task covers headings, emphasis, lists, code, blockquotes, tables, links, and hard line breaks. + +- [ ] **Step 1: Register the module** + +In `src-tauri/src/lib.rs`, add alongside the existing module declarations: + +```rust +mod mdconv; +``` + +- [ ] **Step 2: Write the failing tests** + +Create `src-tauri/src/mdconv.rs` with: + +```rust +//! Pure Markdown <-> HTML conversion for the MCP boundary. Notes are stored as +//! Tiptap HTML; MCP speaks Markdown. Task-list handling mirrors `src/markdown.ts` +//! so notes created via MCP are indistinguishable from app-created ones. + +use comrak::{markdown_to_html, ComrakOptions}; + +/// Markdown (GFM) -> HTML. Hard line breaks on (matches the frontend's +/// `marked` `breaks: true`), tables/strikethrough/autolinks/task items enabled. +pub fn md_to_html(md: &str) -> String { + let mut opts = ComrakOptions::default(); + opts.extension.table = true; + opts.extension.strikethrough = true; + opts.extension.tasklist = true; + opts.extension.autolink = true; + opts.render.hardbreaks = true; + let html = markdown_to_html(md, &opts); + html.trim().to_string() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn headings_and_emphasis() { + let h = md_to_html("# Title\n\nsome **bold** and *italic*"); + assert!(h.contains("

Title

"), "got: {h}"); + assert!(h.contains("bold"), "got: {h}"); + assert!(h.contains("italic"), "got: {h}"); + } + + #[test] + fn bullet_and_ordered_lists() { + let h = md_to_html("- a\n- b"); + assert!(h.contains("