bigwolfe
logging and polish
a5a2c66

Implementation Tasks: Multi-Tenant Obsidian-Like Docs Viewer

Feature Branch: 001-obsidian-docs-viewer Created: 2025-11-15 Status: Ready for Implementation

Implementation Strategy

MVP = User Story 1 (AI Agent Writes) + User Story 2 (Human Reads UI)

The MVP delivers immediate value:

  • AI agents (via MCP STDIO) can create and maintain documentation
  • Humans can browse, search, and read documentation in the web UI
  • Full-text search, wikilinks, tags, and backlinks work end-to-end

Post-MVP enhancements:

  • User Story 3: Human editing with version conflict detection
  • User Story 4: Multi-tenant HF OAuth for production deployment
  • User Story 5: Advanced search ranking and index health monitoring

Progress Update (2025-11-16):

  • Phase 1 setup complete except T017 (create data/vaults/) and T021 (.env.example); T024 remains as an outstanding run-step (schema init script prepared but not executed).
  • Phase 2 foundational backend/models/types complete.
  • Phase 3 MCP backend + prompt tightening complete; see backend/src/mcp/server.py for the updated tool contracts.

Phase 1: Setup

Goal: Initialize project structure, dependencies, and database schema.

  • [T001] Create project directory structure at /home/wolfe/Projects/Document-MCP
  • [T002] [P] Create backend/src/models/ directory and init.py
  • [T003] [P] Create backend/src/services/ directory and init.py
  • [T004] [P] Create backend/src/api/routes/ directory and init.py
  • [T005] [P] Create backend/src/api/middleware/ directory and init.py
  • [T006] [P] Create backend/src/mcp/ directory and init.py
  • [T007] [P] Create backend/tests/unit/ directory and init.py
  • [T008] [P] Create backend/tests/integration/ directory and init.py
  • [T009] [P] Create backend/tests/contract/ directory and init.py
  • [T010] [P] Create frontend/src/components/ui/ directory
  • [T011] [P] Create frontend/src/pages/ directory
  • [T012] [P] Create frontend/src/services/ directory
  • [T013] [P] Create frontend/src/lib/ directory
  • [T014] [P] Create frontend/src/types/ directory
  • [T015] [P] Create frontend/tests/unit/ directory
  • [T016] [P] Create frontend/tests/e2e/ directory
  • [T017] [P] Create data/vaults/ directory for runtime vault storage
  • [T018] Create backend/pyproject.toml with dependencies: fastapi, fastmcp, python-frontmatter, pyjwt, huggingface_hub, uvicorn
  • [T019] Create frontend/package.json with dependencies: react, vite, typescript, shadcn/ui, react-markdown
  • [T020] Create frontend/vite.config.ts with proxy to backend API
  • [T021] Create .env.example with JWT_SECRET_KEY, HF_OAUTH_CLIENT_ID, HF_OAUTH_CLIENT_SECRET, VAULT_BASE_PATH
  • [T022] Create .gitignore to exclude data/, .env, node_modules/, pycache, dist/
  • [T023] Create backend/src/services/database.py with SQLite initialization DDL from data-model.md
  • [T024] Execute SQLite schema initialization (note_metadata, note_fts, note_tags, note_links, index_health tables)

Phase 2: Foundational

Goal: Build core infrastructure required by all user stories.

  • [T025] Create backend/src/services/config.py to load env vars: JWT_SECRET_KEY, VAULT_BASE_PATH, HF_OAUTH_CLIENT_ID, HF_OAUTH_CLIENT_SECRET
  • [T026] [P] Create backend/src/models/user.py with User and HFProfile Pydantic models from data-model.md
  • [T027] [P] Create backend/src/models/note.py with Note, NoteMetadata, NoteCreate, NoteUpdate, NoteSummary Pydantic models from data-model.md
  • [T028] [P] Create backend/src/models/index.py with Wikilink, Tag, IndexHealth Pydantic models from data-model.md
  • [T029] [P] Create backend/src/models/search.py with SearchResult, SearchRequest Pydantic models from data-model.md
  • [T030] [P] Create backend/src/models/auth.py with TokenResponse, JWTPayload Pydantic models from data-model.md
  • [T031] [P] Create frontend/src/types/user.ts with User and HFProfile TypeScript types from data-model.md
  • [T032] [P] Create frontend/src/types/note.ts with Note, NoteMetadata, NoteSummary, NoteCreateRequest, NoteUpdateRequest TypeScript types from data-model.md
  • [T033] [P] Create frontend/src/types/search.ts with SearchResult, Tag, IndexHealth TypeScript types from data-model.md
  • [T034] [P] Create frontend/src/types/auth.ts with TokenResponse, APIError TypeScript types from data-model.md
  • [T035] Create backend/src/services/vault.py with VaultService class: path validation, sanitization (sanitize_path function from data-model.md), vault directory initialization
  • [T036] Create backend/src/services/auth.py with AuthService class: JWT creation (create_jwt), validation (validate_jwt), placeholder for HF OAuth
  • [T037] Create backend/src/api/middleware/auth_middleware.py with extract_user_id_from_jwt function to validate Authorization: Bearer header
  • [T038] Create backend/src/api/middleware/error_handlers.py with FastAPI exception handlers for 400, 401, 403, 404, 409, 413, 500 from http-api.yaml

Phase 3: User Story 1 - AI Agent Writes (P1)

Goal: Enable AI agents to write/update docs via MCP STDIO, with automatic indexing.

  • [T039] [US1] Create backend/src/services/vault.py VaultService.read_note method: read file, parse frontmatter with python-frontmatter, extract title (priority: frontmatter > H1 > filename stem)
  • [T040] [US1] Create backend/src/services/vault.py VaultService.write_note method: validate path/content, create parent dirs, write frontmatter + body, return absolute path
  • [T041] [US1] Create backend/src/services/vault.py VaultService.delete_note method: validate path, remove file, handle FileNotFoundError
  • [T042] [US1] Create backend/src/services/vault.py VaultService.list_notes method: walk vault tree, filter by folder param, return paths and titles
  • [T043] [US1] Create backend/src/services/indexer.py IndexerService class with db connection management
  • [T044] [US1] Create backend/src/services/indexer.py IndexerService.index_note method: delete old rows for (user_id, note_path), insert into note_metadata, note_fts, note_tags, note_links
  • [T045] [US1] Create backend/src/services/indexer.py IndexerService.extract_wikilinks method: regex pattern [[([^]]+)]] to extract link_text from body
  • [T046] [US1] Create backend/src/services/indexer.py IndexerService.resolve_wikilinks method: normalize slug (data-model.md algorithm), match against normalized_title_slug and normalized_path_slug, prefer same-folder, update is_resolved
  • [T047] [US1] Create backend/src/services/indexer.py IndexerService.increment_version method: get current version or default to 1, increment, return new version
  • [T048] [US1] Create backend/src/services/indexer.py IndexerService.update_index_health method: update note_count, last_incremental_update timestamp
  • [T049] [US1] Create backend/src/services/indexer.py IndexerService.delete_note_index method: delete rows from all index tables, update backlinks to set is_resolved=false
  • [T050] [US1] Create backend/src/mcp/server.py FastMCP server initialization with name="obsidian-docs-viewer"
  • [T051] [US1] Create backend/src/mcp/server.py list_notes MCP tool: call VaultService.list_notes, return [{path, title, last_modified}]
  • [T052] [US1] Create backend/src/mcp/server.py read_note MCP tool: call VaultService.read_note, return {path, title, metadata, body}
  • [T053] [US1] Create backend/src/mcp/server.py write_note MCP tool: call VaultService.write_note, then IndexerService.index_note, return {status: "ok", path}
  • [T054] [US1] Create backend/src/mcp/server.py delete_note MCP tool: call VaultService.delete_note, then IndexerService.delete_note_index, return {status: "ok"}
  • [T055] [US1] Create backend/src/mcp/server.py search_notes MCP tool: query note_fts with bm25 ranking (3.0 title weight, 1.0 body weight), add recency bonus, return [{path, title, snippet}]
  • [T056] [US1] Create backend/src/mcp/server.py get_backlinks MCP tool: query note_links WHERE target_path=?, join note_metadata, return [{path, title}]
  • [T057] [US1] Create backend/src/mcp/server.py get_tags MCP tool: query note_tags GROUP BY tag, return [{tag, count}]
  • [T058] [US1] Create backend/src/mcp/server.py STDIO transport mode: if name == "main", run FastMCP with stdio transport for local development
  • [T059] [US1] Add recency bonus calculation to search_notes: +1.0 for updated in last 7 days, +0.5 for last 30 days, 0 otherwise

Phase 4: User Story 2 - Human Reads UI (P1)

Goal: Web UI for browsing, searching, and reading notes with wikilinks and backlinks.

  • [T060] [US2] Create backend/src/api/routes/notes.py with GET /api/notes endpoint: call VaultService.list_notes, return NoteSummary[] from http-api.yaml
  • [T061] [US2] Create backend/src/api/routes/notes.py with GET /api/notes/{path} endpoint: URL-decode path, call VaultService.read_note, return Note from http-api.yaml
  • [T062] [US2] Create backend/src/api/routes/search.py with GET /api/search endpoint: call IndexerService search with query param, return SearchResult[] from http-api.yaml
  • [T063] [US2] Create backend/src/api/routes/search.py with GET /api/backlinks/{path} endpoint: URL-decode path, query note_links, return BacklinkResult[] from http-api.yaml
  • [T064] [US2] Create backend/src/api/routes/search.py with GET /api/tags endpoint: query note_tags, return Tag[] from http-api.yaml
  • [T065] [US2] Create backend/src/api/main.py FastAPI app with CORS middleware, mount routes, include error handlers
  • [T066] [US2] Create frontend/src/services/api.ts API client with fetch wrapper: add Authorization: Bearer header, handle JSON responses, throw APIError on non-200
  • [T067] [US2] Create frontend/src/services/api.ts listNotes function: GET /api/notes?folder=, return NoteSummary[]
  • [T068] [US2] Create frontend/src/services/api.ts getNote function: GET /api/notes/{encodeURIComponent(path)}, return Note
  • [T069] [US2] Create frontend/src/services/api.ts searchNotes function: GET /api/search?q=, return SearchResult[]
  • [T070] [US2] Create frontend/src/services/api.ts getBacklinks function: GET /api/backlinks/{encodeURIComponent(path)}, return BacklinkResult[]
  • [T071] [US2] Create frontend/src/services/api.ts getTags function: GET /api/tags, return Tag[]
  • [T072] [US2] Create frontend/src/lib/wikilink.ts with extractWikilinks function: regex /[[([^]]+)]]/g
  • [T073] [US2] Create frontend/src/lib/wikilink.ts with normalizeSlug function: lowercase, replace spaces/underscores with dash, strip non-alphanumeric
  • [T074] [US2] Create frontend/src/lib/markdown.tsx with react-markdown config: code highlighting, wikilink custom renderer
  • [T075] [US2] Initialize shadcn/ui in frontend/: run npx shadcn@latest init, select default theme
  • [T076] [US2] Install shadcn/ui components: ScrollArea, Button, Input, Card, Badge, Resizable, Collapsible, Dialog, Alert, Textarea, Dropdown-Menu, Avatar, Command, Tooltip, Popover
  • [T077] [US2] Create frontend/src/components/DirectoryTree.tsx: recursive tree view with collapsible folders, leaf items for notes, onClick handler to load note
  • [T078] [US2] Create frontend/src/components/NoteViewer.tsx: render note title, metadata (tags as badges, timestamps), react-markdown body with wikilink links, backlinks section in footer
  • [T079] [US2] Create frontend/src/components/SearchBar.tsx: Input with debounced onChange (300ms), dropdown results with onClick to navigate to note
  • [T080] [US2] Create frontend/src/pages/MainApp.tsx: two-pane layout (left: DirectoryTree + SearchBar in ScrollArea, right: NoteViewer), state management for selected note path
  • [T081] [US2] Add wikilink click handler in NoteViewer: onClick [[link]] β†’ normalizeSlug β†’ API lookup β†’ navigate to resolved note
  • [T082] [US2] Add broken wikilink styling in NoteViewer: render unresolved [[links]] with distinct color/style
  • [T083] [US2] Create frontend/src/pages/MainApp.tsx useEffect to load directory tree on mount: call listNotes()
  • [T084] [US2] Create frontend/src/pages/MainApp.tsx useEffect to load note when path changes: call getNote(path) and getBacklinks(path)

Phase 5: User Story 3 - Human Edits UI (P2)

Goal: Split-pane editor with optimistic concurrency protection.

  • [T085] [US3] Create backend/src/api/routes/notes.py with PUT /api/notes/{path} endpoint: URL-decode path, validate request body (NoteUpdate), check if_version, call VaultService.write_note, return NoteResponse from http-api.yaml
  • [T086] [US3] Add optimistic concurrency check in IndexerService.increment_version: if if_version provided and != current version, raise ConflictError
  • [T087] [US3] Create ConflictError exception in backend/src/models/errors.py, map to 409 Conflict in error_handlers.py
  • [T088] [US3] Create frontend/src/services/api.ts updateNote function: PUT /api/notes/{encodeURIComponent(path)} with {title?, metadata?, body, if_version?}, handle 409 response
  • [T089] [US3] Create frontend/src/components/NoteEditor.tsx: split-pane layout (left: textarea for markdown source, right: live preview with react-markdown)
  • [T090] [US3] Create frontend/src/components/NoteEditor.tsx with Save button: onClick β†’ call updateNote with if_version from initial note load, handle success β†’ switch to read mode
  • [T091] [US3] Create frontend/src/components/NoteEditor.tsx with Cancel button: onClick β†’ discard changes, switch to read mode
  • [T092] [US3] Add 409 Conflict error handling in NoteEditor: display alert "Note changed since you opened it, please reload before saving"
  • [T093] [US3] Add Edit button to NoteViewer: onClick β†’ switch main pane to NoteEditor mode, pass current note version
  • [T094] [US3] Update frontend/src/pages/MainApp.tsx to toggle between NoteViewer and NoteEditor based on edit mode state

Phase 6: User Story 4 - Multi-Tenant OAuth (P2)

Goal: HF OAuth login, per-user vaults, JWT tokens for API and MCP HTTP.

  • [T095] [US4] Create backend/src/services/auth.py HF OAuth integration: use huggingface_hub.attach_huggingface_oauth and parse_huggingface_oauth helpers
  • [T096] [US4] Create backend/src/api/routes/auth.py with GET /auth/login endpoint: redirect to HF OAuth authorize URL
  • [T097] [US4] Create backend/src/api/routes/auth.py with GET /auth/callback endpoint: parse_huggingface_oauth, map HF username to user_id, create vault if new user, set session cookie
  • [T098] [US4] Create backend/src/api/routes/auth.py with POST /api/tokens endpoint: validate authenticated user, call AuthService.create_jwt, return TokenResponse from http-api.yaml
  • [T099] [US4] Create backend/src/api/routes/auth.py with GET /api/me endpoint: validate Bearer token, return User from http-api.yaml
  • [T100] [US4] Update backend/src/api/middleware/auth_middleware.py to extract user_id from JWT sub claim, attach to request.state.user_id
  • [T101] [US4] Update backend/src/services/vault.py to scope all operations by user_id: vault path = VAULT_BASE_PATH / user_id
  • [T102] [US4] Update backend/src/services/indexer.py to scope all queries by user_id: WHERE user_id = ?
  • [T103] [US4] Initialize vault and index on first user login: create vault dir, insert initial index_health row
  • [T104] [US4] Create backend/src/mcp/server.py HTTP transport mode: FastMCP with http transport, BearerAuth validation, extract user_id from JWT
  • [T105] [US4] Create frontend/src/services/auth.ts with login function: redirect to /auth/login
  • [T106] [US4] Create frontend/src/services/auth.ts with getCurrentUser function: GET /api/me, return User
  • [T107] [US4] Create frontend/src/services/auth.ts with getToken function: POST /api/tokens, return TokenResponse, store token in memory
  • [T108] [US4] Create frontend/src/pages/Login.tsx: "Sign in with Hugging Face" button β†’ onClick call auth.login()
  • [T109] [US4] Create frontend/src/pages/Settings.tsx: display user profile (user_id, HF avatar), API token with copy button for MCP config
  • [T110] [US4] Update frontend/src/pages/App.tsx to call getCurrentUser on mount, redirect to Login if 401
  • [T111] [US4] Update frontend/src/services/api.ts to include token from localStorage in Authorization header

Phase 7: User Story 5 - Advanced Search (P3)

Goal: Enhanced search ranking and index health monitoring.

  • [T112] [US5] Update backend/src/services/indexer.py search_notes to calculate recency bonus: +1.0 for updated in last 7 days, +0.5 for last 30 days, 0 otherwise
  • [T113] [US5] Update backend/src/services/indexer.py search_notes to calculate final score: (3 * title_bm25) + (1 * body_bm25) + recency_bonus
  • [T114] [US5] Create backend/src/api/routes/index.py with GET /api/index/health endpoint: query index_health, return IndexHealth from http-api.yaml
  • [T115] [US5] Create backend/src/api/routes/index.py with POST /api/index/rebuild endpoint: call IndexerService.rebuild_index, return RebuildResponse from http-api.yaml
  • [T116] [US5] Create backend/src/services/indexer.py IndexerService.rebuild_index method: delete all user rows, walk vault, parse all notes, re-insert into all index tables, update index_health
  • [T117] [US5] Create frontend/src/services/api.ts getIndexHealth function: GET /api/index/health, return IndexHealth
  • [T118] [US5] Create frontend/src/services/api.ts rebuildIndex function: POST /api/index/rebuild, return RebuildResponse
  • [T119] [US5] Add index health indicator to frontend/src/pages/MainApp.tsx: display note count and last updated timestamp in footer
  • [T120] [US5] Add "Rebuild Index" button to frontend/src/pages/Settings.tsx: onClick β†’ call rebuildIndex, show progress/completion message

Phase 8: Polish & Cross-Cutting

Goal: Documentation, configuration, logging, error handling improvements.

  • [T121] Create README.md with project overview, tech stack, local setup instructions (backend venv + npm install)
  • [T122] Add README.md section: "Running Backend" with uvicorn command for HTTP API and python -m backend.src.mcp.server for MCP STDIO
  • [T123] Add README.md section: "Running Frontend" with npm run dev command
  • [T124] Add README.md section: "MCP Client Configuration" with Claude Code/Desktop STDIO example from mcp-tools.json
  • [T125] Add README.md section: "Deploying to Hugging Face Space" with environment variables and OAuth setup
  • [T126] Update .env.example with all variables: JWT_SECRET_KEY, VAULT_BASE_PATH, HF_OAUTH_CLIENT_ID, HF_OAUTH_CLIENT_SECRET, DATABASE_PATH
  • [T127] Add structured logging to backend/src/services/vault.py: log file operations with user_id, note_path, operation type
  • [T128] Add structured logging to backend/src/services/indexer.py: log index updates with user_id, note_path, duration_ms
  • [T129] Add structured logging to backend/src/mcp/server.py: log MCP tool calls with tool_name, user_id, duration_ms
  • [T130] Improve error messages in backend/src/api/middleware/error_handlers.py: include detail objects with field names and reasons
  • [T131] Add input validation to all HTTP API routes: validate path format, content size, required fields
  • [T132] Add input validation to all MCP tools: validate path format, content size via Pydantic models
  • [T133] Add rate limiting consideration to README.md: note potential need for per-user rate limits in production
  • [T134] Add performance optimization notes to README.md: FTS5 prefix indexes, SQLite WAL mode for concurrency

Dependencies

Story Completion Order

Must complete in this order:

  1. Phase 1 (Setup) β†’ Phase 2 (Foundational) β†’ Phase 3 (US1) + Phase 4 (US2) in parallel
  2. Phase 5 (US3) depends on Phase 4 (needs HTTP API routes from US2)
  3. Phase 6 (US4) depends on Phase 2 (needs auth foundation) and Phase 3 (needs vault/indexer)
  4. Phase 7 (US5) depends on Phase 3 (needs indexer) and Phase 4 (needs HTTP API)
  5. Phase 8 (Polish) can run anytime after Phase 4 (MVP complete)

Task Dependencies

Critical path (must be sequential):

  • T023 (SQLite schema) β†’ T043 (IndexerService) β†’ T044 (index_note)
  • T035 (VaultService foundation) β†’ T039 (read_note) β†’ T040 (write_note)
  • T050 (FastMCP init) β†’ T051-T057 (MCP tools)
  • T065 (FastAPI app) β†’ T060-T064 (HTTP routes)

Parallelizable within phases (marked with [P]):

  • All directory creation tasks (T002-T017)
  • All Pydantic model tasks (T026-T030)
  • All TypeScript type tasks (T031-T034)
  • All frontend component tasks within US2 (T077-T079)

Parallel Execution Examples

User Story 1 (AI Agent Writes) - Parallel Work

Team A: VaultService implementation (T039-T042) Team B: IndexerService implementation (T043-T049) Team C: MCP tools implementation (T051-T057)

After T050 (FastMCP init) completes, Team C can implement all 7 MCP tools in parallel since they're independent endpoints.

User Story 2 (Human Reads UI) - Parallel Work

Team A: Backend HTTP API routes (T060-T064) Team B: Frontend API client (T066-T071) Team C: Frontend components (T077-T079)

After T065 (FastAPI app) and T075-T076 (shadcn/ui setup) complete, all three teams can work in parallel.

User Story 4 (Multi-Tenant OAuth) - Parallel Work

Team A: Backend OAuth integration (T095-T104) Team B: Frontend auth flow (T105-T111)

Both teams can work in parallel after Phase 2 (Foundational) completes.


Summary

Total Tasks: 134

  • Phase 1 (Setup): 24 tasks
  • Phase 2 (Foundational): 14 tasks
  • Phase 3 (US1 - AI Agent Writes): 21 tasks
  • Phase 4 (US2 - Human Reads UI): 25 tasks
  • Phase 5 (US3 - Human Edits UI): 10 tasks
  • Phase 6 (US4 - Multi-Tenant OAuth): 17 tasks
  • Phase 7 (US5 - Advanced Search): 9 tasks
  • Phase 8 (Polish): 14 tasks

MVP Tasks (US1 + US2): 84 tasks (Phases 1-4) Post-MVP Tasks (US3 + US4 + US5): 36 tasks (Phases 5-7) Polish Tasks: 14 tasks (Phase 8)

Estimated Effort:

  • MVP (US1 + US2): ~2-3 weeks (1-2 developers)
  • Post-MVP (US3 + US4 + US5): ~1-2 weeks
  • Polish: ~3-5 days
  • Total: ~4-6 weeks for complete implementation

Key Milestones:

  1. Week 1: Complete Phase 1-2 (Setup + Foundational)
  2. Week 2: Complete Phase 3 (US1 - MCP STDIO working)
  3. Week 3: Complete Phase 4 (US2 - Web UI working, MVP delivered)
  4. Week 4: Complete Phase 5-6 (US3 editing + US4 multi-tenant)
  5. Week 5: Complete Phase 7-8 (US5 advanced search + Polish)

Next Steps:

  1. Review this task breakdown with stakeholders
  2. Assign initial tasks (Phase 1 Setup) to team
  3. Create GitHub issues from tasks using /speckit.taskstoissues
  4. Begin implementation with T001 (project structure)