Web + MCP
Mindlap-CRM
A lead-and-followup CRM that keeps every deal's pipeline stage, notes, and next action in one live workspace.
Want the codebase? Get the Git repo emailed to you.
About this project
Mindlap CRM is an AI-native take on the traditional sales CRM, a lightweight lead-management system built so you can run your pipeline from wherever you already work: the web app, your AI assistant, or your inbox. Leads move through customizable pipeline stages on a drag-and-drop Kanban board, each carrying rich-text notes, follow-up tasks, and a full activity history, with a dashboard that rolls everything up into metrics and charts. Its standout move is meeting people where they are: a one-click Gmail extension turns the contact you're reading into a CRM lead, optionally attaching the whole email thread as a note, and a Model Context Protocol server lets Claude or Codex create, search, and update leads directly through tool calls, so the CRM is driven as much by agents as by clicks.
Demo
At a glance
49h
Agent hours
236M
Tokens
3
Laps
82
Stories
236M
tokens
Others · 100%
Tokens by stage
Tech Spec
20M
Implement
57M
Code Review
65M
Fix
54M
Merge
41M
Pipeline
Stage
Runs
Tokens
Duration
Tech Spec
80
20M
6.7h
Implement
96
57M
10.1h
Code Review
196
65M
16.8h
Fix
95
54M
8.5h
Merge
93
41M
7h
Engines used
Others
Tokens
236M
Runs
560
Agent hours
49h
Success
95.7%
Agent team
developer
282 runs · 25.6h
271 passed · 11 failed
engineering_manager
274 runs · 23.5h
265 passed · 9 failed
product_manager
1 runs · 0.1h
1 passed · 0 failed
Artifacts
Feature Spec
markdownMindlap CRM — POC Feature Spec
Context
Mindlap needs a custom CRM POC that demos a modern sales OS plus first-class MCP control of the same data, so a salesperson (or an agent in their terminal) can create, move, and annotate leads and have the change appear instantly in the web UI. This repo already ships a skeleton (FastAPI + async SQLAlchemy + SQLite backend; React 18 + Vite + Tailwind + TanStack Query frontend with a hey-api-generated SDK and a single /healthz route). This spec extends that skeleton — no rewrites, no architecture swap — and adds an mcp/ sibling that shares the backend's service layer over stdio.
Locked decisions
| Area | Choice |
|---|---|
| MCP transport | Separate stdio MCP server, imports backend service layer directly |
| Realtime | FastAPI WebSocket channel + in-process pub/sub; cross-process fanout via internal HTTP |
| Notes editor | TipTap, store ProseMirror JSON + derived plaintext column for search |
| DB / migrations | Stay on SQLite, keep Base.metadata.create_all (no Alembic) |
| Tenancy | Single-user POC — assigned_to / lead_owner are free-text strings, not FKs |
| Charts | Recharts |
| Seed | Demo user + ~20 leads spread across stages + sample notes/followups |
High-level architecture
mindlap-crm/
backend/ existing FastAPI app, extended
app/
core/ +security.py (jwt, password hashing); config keeps env vars
db/ models split per file under db/models/
routes/ auth, leads, stages, notes, followups, dashboard, ws, internal
services/ business logic, called by routes AND by MCP
realtime/ bus.py (asyncio fanout) + events.py (typed events)
deps.py get_current_user, get_session, get_publisher
frontend/ existing React app, extended with auth, pages, components
mcp/ NEW — stdio MCP server
pyproject.toml depends on the backend package (path dep) for services
mindlap_crm_mcp/
server.py stdio entrypoint
tools/ one file per tool
auth.py reads MINDLAP_CRM_API_KEY env
publisher.py posts realtime events to backend's /_internal/events
Both the web app (via REST) and the MCP server (via service-layer import) mutate through the same app.services.* functions. Services emit events to app.realtime.bus. When MCP triggers from a different process, its publisher.py POSTs the event to /_internal/events on the running backend, which fans out to all connected WebSocket clients. The frontend's WS hook translates events into queryClient.invalidateQueries(...), so TanStack Query refetches the affected lists/details automatically.
Data model (SQLAlchemy, create_all)
All tables get created_at, updated_at. Soft-deletable tables also get deleted_at (nullable; queries filter deleted_at IS NULL by default).
- users —
id,email UNIQUE,password_hash,full_name, timestamps. Single user in POC; signup endpoint refuses a second registration unless an env override is set. - pipeline_stages —
id,name,color(hex),order_index INT,is_terminal BOOL(Won/Lost),is_default BOOL, timestamps,deleted_at. Seeded with the 8 defaults from the brief. - leads —
id,full_name,company_name,email,phone,source,current_stage_id FK→pipeline_stages,next_followup_date,assigned_to TEXT,tags JSON(string array),priorityENUM(low/medium/high),linkedin_url,website,deal_size NUMERIC,industry,lead_owner TEXT,location, timestamps,deleted_at. Indexes oncurrent_stage_id,email,created_at,next_followup_date. - lead_stage_history —
id,lead_id FK,from_stage_id FK NULLABLE,to_stage_id FK,changed_by FK→users NULLABLE,changed_at. Append-only. - lead_notes —
id,lead_id FK UNIQUE,content_json JSON(TipTap doc),content_plain TEXT(derived on save forLIKEsearch),updated_by FK→users, timestamps. One running document per lead. - lead_followups —
id,lead_id FK,due_date,note TEXT,statusENUM(pending/done/canceled),completed_at NULLABLE,created_by FK→users, timestamps,deleted_at.
Models live under backend/app/db/models/*.py and are imported in db/base.py (the existing import hook in init_db) so metadata.create_all sees them.
Backend layout & endpoints
New packages: app/services/, app/realtime/, app/db/models/.
app/core/security.py—hash_password,verify_password(passlib bcrypt);create_access_token,decode_token(PyJWT, HS256,JWT_SECRETenv).app/deps.py—get_current_userdep that readsAuthorization: Bearer, decodes, loads user.app/realtime/bus.py—EventBuswithsubscribe()returning an asyncio queue,publish(event)fanning out to subscribers. Process-local. Singleton onapp.state.bus.app/realtime/events.py— typed events:LeadCreated,LeadUpdated,LeadDeleted,LeadStageChanged,NoteUpdated,FollowupCreated,FollowupUpdated,StagesChanged.app/services/*.py— service functions take anAsyncSession+ aPublisherand callpublisher.publish(...)after mutations. Routes inject the in-process publisher; MCP wires the HTTP publisher.
REST routes (all under /api, all require auth except auth endpoints):
POST /api/auth/signup,POST /api/auth/login,GET /api/auth/me,POST /api/auth/mcp-token— mints a long-lived JWT to paste into MCP's env.GET/POST /api/leads,GET/PATCH/DELETE /api/leads/{id},POST /api/leads/{id}/move-stage. List supportsq,stage_id,priority,assigned_to,tag,sort,page,page_size.GET/POST /api/stages,PATCH/DELETE /api/stages/{id},POST /api/stages/reorder(atomic).GET/PUT /api/leads/{id}/notes.GET/POST /api/followups,PATCH/DELETE /api/followups/{id}. Dashboard filters viadue_before,status,lead_id.GET /api/dashboard/summary— metric cards + chart series in one payload.WS /api/ws?token=<jwt>— subscribes the connection toapp.state.busand streams events as JSON.POST /_internal/events— gated byINTERNAL_SHARED_SECRETheader; lets the MCP process inject events.
Register all routers in app/main.py (extend the existing include_router block).
MCP server
- New
mcp/package using themcpPython SDK with stdio transport. Its ownpyproject.toml, with a path dep on../backendso it canfrom app.services import leads as leads_service. mindlap_crm_mcp/server.pyregisters tools and runsServer.run_stdio().mindlap_crm_mcp/auth.pyreadsMINDLAP_CRM_API_KEY(the long-lived JWT issued by/api/auth/mcp-token); decodes it locally to resolve the user; all tools run as that user.mindlap_crm_mcp/publisher.pyimplements the samePublisherinterface as the in-process one, but POSTs each event toMINDLAP_CRM_BACKEND_URL/_internal/eventswith the shared secret. Falls back to no-op if the backend isn't running (mutation still lands in DB).mindlap_crm_mcp/tools/:create_lead.py,update_lead.py,move_lead_stage.py,update_lead_notes.py,get_lead_notes.py,add_followup.py,get_dashboard_summary.py,search_leads.py. Each tool declares a JSON schema, validates input with Pydantic, opensAsyncSession, calls the matching service function with the HTTP publisher, and returns a structured JSON result.
Local session DB path is shared via DATABASE_URL env (same SQLite file as backend).
Frontend layout
Reuse the existing src/api/health.ts pattern — every endpoint gets a hand-written TanStack Query hook that calls into the regenerated src/client/. New folders:
src/components/ui/—Button,Input,Textarea,Select,Dialog,DropdownMenu,Badge,Skeleton,Toast,Tooltip,Tabs. Headless (Radix) + Tailwind. Dark mode viadarkMode: 'class'on the Tailwind config;useDarkModehook toggles<html class="dark">.src/components/layout/—AppShellwith sidebar (Dashboard / Leads / Kanban / Settings) and topbar (search, theme toggle, user menu).src/components/leads/—LeadTable(sticky header, search, filters, sort, pagination, status pills, row actions menu),LeadDrawerorLeadFormDialog,LeadDetailHeader.src/components/kanban/—KanbanBoard,KanbanColumn,KanbanCard,StageEditormodal. Use@dnd-kit/core+@dnd-kit/sortable. Optimistic update on drop, rollback on error.src/components/notes/—NotesEditorwrapping TipTap withStarterKit+Placeholder. Debounced auto-save (PUT every ~1.5s after last edit) plus explicit save button.src/components/followups/—FollowupForm,FollowupList,FollowupActionCenter. this is a part of the dashboard so no seperate component for this is needed.src/components/dashboard/—MetricCard,ChartLeadsByStage(Bar),ChartLeadsBySource(Pie),ChartWeeklyGrowth(Line),ChartFunnel(custom Bar),ChartFollowupTrends(Line). All consumeuseDashboardSummary().src/pages/—Login,Dashboard,Leads,LeadDetail(tabs: Overview / Notes / Followups / History),Kanban.src/routes/ProtectedRoute.tsx— redirects to/loginif no token.src/hooks/—useAuth(token inlocalStorage; exposesuser,login,logout),useRealtime(opens WS on mount, invalidates queries per event type),useDarkMode.src/api/runtime-config.ts— extend with axios request interceptor that attachesAuthorization: Bearer <token>.src/realtime/socket.ts— single shared WebSocket; event router mapsLeadUpdated → invalidate ['leads', id],StagesChanged → invalidate ['stages'], etc.src/main.tsx— wrapAppwithAuthProvider,ThemeProvider, mountuseRealtime()insideAppwhen authed.
New runtime deps to add to frontend/package.json: @tiptap/react, @tiptap/starter-kit, @tiptap/extension-placeholder, @dnd-kit/core, @dnd-kit/sortable, recharts, @radix-ui/react-* (dialog, dropdown-menu, tabs, tooltip, toast), clsx, tailwind-merge, date-fns, lucide-react.
Realtime flow (worked example)
MCP runs move_lead_stage for lead X:
- Tool calls
leads_service.move_stage(session, lead_id, stage_id, publisher=http_publisher). - Service updates
leads.current_stage_id, insertslead_stage_historyrow, commits. - Service calls
publisher.publish(LeadStageChanged(lead_id, from, to)). - HTTP publisher POSTs the event to backend's
/_internal/events. - Backend's internal route forwards to
app.state.bus.publish(event). - All connected WS clients receive the event.
- Frontend
useRealtimeinvalidates['leads'],['leads', X],['dashboard'],['stages-history', X]. - TanStack Query refetches; UI updates with no manual refresh.
Auth flow
- Signup creates the one allowed user, returns JWT (24h) + a separately-issued long-lived MCP JWT (365d). UI shows the MCP token once on a "connect MCP" screen with copy button + env-var snippet.
- Login returns the same short JWT. Stored in
localStorageasmlc.token. - Axios interceptor + WS query string both carry the JWT.
- MCP env:
MINDLAP_CRM_API_KEY(long-lived JWT),MINDLAP_CRM_BACKEND_URL,DATABASE_URL,INTERNAL_SHARED_SECRET.
Seed data
backend/app/db/seed.py invoked by python -m app.db.seed:
- Creates demo user
demo@mindlap.dev/mindlap123. - Inserts 8 default pipeline stages with colors.
- Inserts ~20 leads spread realistically across stages (more in New / Contacted, fewer in Won / Lost), with sample tags (
enterprise,inbound,referral,cold-outbound), varied sources, deal sizes, andnext_followup_datedistribution covering today / overdue / upcoming so the dashboard renders meaningfully. - For ~10 of those leads, inserts a sample TipTap note (one of the three examples from the brief, lightly varied).
- Inserts 6–8
lead_followupscovering today / overdue / next-week buckets. - Inserts stage history rows for moved leads.
README.md updated with a Seed section explaining the command.
Phased build order
Matches the brief's Phase 1–5 priority. Each phase is a working slice — backend models + routes + frontend pages + seed adjustments — so the app boots and demos at each milestone.
- Phase 1 — Auth (signup/login/me/mcp-token), Leads CRUD + table + detail, Notes (TipTap + GET/PUT), seed.
- Phase 2 — Pipeline stages CRUD + reorder, Kanban board + DnD + stage history, Followups CRUD.
- Phase 3 — Dashboard summary endpoint + metric cards + 5 charts + Followup Action Center.
- Phase 4 — Realtime bus + WS endpoint + frontend WS hook; MCP server + 8 tools + internal events route.
- Phase 5 — Dark mode polish, skeletons, toasts, empty states, perf passes (query keys, list virtualization if needed).
Critical files to add or modify
backend/app/main.py— register routers, mount WS, initapp.state.bus.backend/app/core/security.py— new.backend/app/deps.py— new.backend/app/db/base.py— import every model underdb/models/socreate_allsees them.backend/app/db/models/{user,pipeline_stage,lead,lead_stage_history,lead_note,lead_followup}.py— new.backend/app/realtime/{bus,events}.py— new.backend/app/services/{auth,leads,stages,notes,followups,dashboard}.py— new; all mutations callpublisher.publish(...).backend/app/routes/{auth,leads,stages,notes,followups,dashboard,ws,internal}.py— new.backend/app/db/seed.py— new.backend/pyproject.toml— addpasslib[bcrypt],pyjwt,python-multipart.mcp/— entire package, new.frontend/src/api/{auth,leads,stages,notes,followups,dashboard}.ts— TanStack hooks, followhealth.tsreference pattern.frontend/src/api/runtime-config.ts— add auth interceptor.frontend/src/realtime/socket.ts,src/hooks/useRealtime.ts— new.frontend/src/components/{ui,layout,leads,kanban,notes,followups,dashboard}/— new.frontend/src/pages/*— new (replaces the inlineHomeinApp.tsx).frontend/src/App.tsx— switch to multi-route layout withProtectedRoute.frontend/tailwind.config.js—darkMode: 'class', design tokens.frontend/package.json— add TipTap, dnd-kit, recharts, radix-ui, lucide, clsx, tailwind-merge, date-fns.README.md— add MCP setup, seed command, env vars, dark mode, regen reminder..env.example— addJWT_SECRET,INTERNAL_SHARED_SECRET,MCP_TOKEN_TTL_DAYS.
Existing skeleton conventions kept:
- Health endpoint, hey-api SDK +
pnpm regenworkflow, axiosthrowOnError, FastAPIHTTPExceptionJSON shape,init_dblifespan hook. - The reference comment in
src/api/health.ts(“every backend route gets a thin TanStack Query wrapper here”) becomes the actual pattern.
Verification
- Backend tests (
pytest) — extendtests/routes/with auth happy-path, leads CRUD round-trip, stage reorder atomicity, note save, followup lifecycle, dashboard summary shape, internal events shared-secret check. - Type check / build —
pnpm typecheck && pnpm buildinfrontend/. SDK regen: backendDEBUG=true, runpnpm regen, revert. - Seed sanity —
python -m app.db.seedagainst a freshpoc.db; hitGET /api/dashboard/summaryand confirm non-trivial numbers. - End-to-end realtime demo — start backend, start frontend, start MCP via
python -m mindlap_crm_mcpfrom a second terminal, runcreate_leadandmove_lead_stagetools, watch the table and Kanban update in the browser without refresh. - MCP standalone — invoke each of the 8 tools through the MCP SDK's inspector / a Claude Desktop client; verify structured JSON results match the schemas.
- Auth boundary — confirm
/api/*returns 401 without a Bearer token; confirm WS rejects bad/missing token; confirm/_internal/eventsrejects requests missing the shared secret.