Spaces:
Running
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.pyfor 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:
- Phase 1 (Setup) β Phase 2 (Foundational) β Phase 3 (US1) + Phase 4 (US2) in parallel
- Phase 5 (US3) depends on Phase 4 (needs HTTP API routes from US2)
- Phase 6 (US4) depends on Phase 2 (needs auth foundation) and Phase 3 (needs vault/indexer)
- Phase 7 (US5) depends on Phase 3 (needs indexer) and Phase 4 (needs HTTP API)
- 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:
- Week 1: Complete Phase 1-2 (Setup + Foundational)
- Week 2: Complete Phase 3 (US1 - MCP STDIO working)
- Week 3: Complete Phase 4 (US2 - Web UI working, MVP delivered)
- Week 4: Complete Phase 5-6 (US3 editing + US4 multi-tenant)
- Week 5: Complete Phase 7-8 (US5 advanced search + Polish)
Next Steps:
- Review this task breakdown with stakeholders
- Assign initial tasks (Phase 1 Setup) to team
- Create GitHub issues from tasks using
/speckit.taskstoissues - Begin implementation with T001 (project structure)