Mindlap
← All projects

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

markdown

Mindlap 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

AreaChoice
MCP transportSeparate stdio MCP server, imports backend service layer directly
RealtimeFastAPI WebSocket channel + in-process pub/sub; cross-process fanout via internal HTTP
Notes editorTipTap, store ProseMirror JSON + derived plaintext column for search
DB / migrationsStay on SQLite, keep Base.metadata.create_all (no Alembic)
TenancySingle-user POC — assigned_to / lead_owner are free-text strings, not FKs
ChartsRecharts
SeedDemo 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).

  • usersid, email UNIQUE, password_hash, full_name, timestamps. Single user in POC; signup endpoint refuses a second registration unless an env override is set.
  • pipeline_stagesid, 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.
  • leadsid, full_name, company_name, email, phone, source, current_stage_id FK→pipeline_stages, next_followup_date, assigned_to TEXT, tags JSON (string array), priority ENUM(low/medium/high), linkedin_url, website, deal_size NUMERIC, industry, lead_owner TEXT, location, timestamps, deleted_at. Indexes on current_stage_id, email, created_at, next_followup_date.
  • lead_stage_historyid, lead_id FK, from_stage_id FK NULLABLE, to_stage_id FK, changed_by FK→users NULLABLE, changed_at. Append-only.
  • lead_notesid, lead_id FK UNIQUE, content_json JSON (TipTap doc), content_plain TEXT (derived on save for LIKE search), updated_by FK→users, timestamps. One running document per lead.
  • lead_followupsid, lead_id FK, due_date, note TEXT, status ENUM(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.pyhash_password, verify_password (passlib bcrypt); create_access_token, decode_token (PyJWT, HS256, JWT_SECRET env).
  • app/deps.pyget_current_user dep that reads Authorization: Bearer, decodes, loads user.
  • app/realtime/bus.pyEventBus with subscribe() returning an asyncio queue, publish(event) fanning out to subscribers. Process-local. Singleton on app.state.bus.
  • app/realtime/events.py — typed events: LeadCreated, LeadUpdated, LeadDeleted, LeadStageChanged, NoteUpdated, FollowupCreated, FollowupUpdated, StagesChanged.
  • app/services/*.py — service functions take an AsyncSession + a Publisher and call publisher.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 supports q, 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 via due_before, status, lead_id.
  • GET /api/dashboard/summary — metric cards + chart series in one payload.
  • WS /api/ws?token=<jwt> — subscribes the connection to app.state.bus and streams events as JSON.
  • POST /_internal/events — gated by INTERNAL_SHARED_SECRET header; 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 the mcp Python SDK with stdio transport. Its own pyproject.toml, with a path dep on ../backend so it can from app.services import leads as leads_service.
  • mindlap_crm_mcp/server.py registers tools and runs Server.run_stdio().
  • mindlap_crm_mcp/auth.py reads MINDLAP_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.py implements the same Publisher interface as the in-process one, but POSTs each event to MINDLAP_CRM_BACKEND_URL/_internal/events with 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, opens AsyncSession, 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 via darkMode: 'class' on the Tailwind config; useDarkMode hook toggles <html class="dark">.
  • src/components/layout/AppShell with 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), LeadDrawer or LeadFormDialog, LeadDetailHeader.
  • src/components/kanban/KanbanBoard, KanbanColumn, KanbanCard, StageEditor modal. Use @dnd-kit/core + @dnd-kit/sortable. Optimistic update on drop, rollback on error.
  • src/components/notes/NotesEditor wrapping TipTap with StarterKit + 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 consume useDashboardSummary().
  • src/pages/Login, Dashboard, Leads, LeadDetail (tabs: Overview / Notes / Followups / History), Kanban.
  • src/routes/ProtectedRoute.tsx — redirects to /login if no token.
  • src/hooks/useAuth (token in localStorage; exposes user, login, logout), useRealtime (opens WS on mount, invalidates queries per event type), useDarkMode.
  • src/api/runtime-config.ts — extend with axios request interceptor that attaches Authorization: Bearer <token>.
  • src/realtime/socket.ts — single shared WebSocket; event router maps LeadUpdated → invalidate ['leads', id], StagesChanged → invalidate ['stages'], etc.
  • src/main.tsx — wrap App with AuthProvider, ThemeProvider, mount useRealtime() inside App when 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:

  1. Tool calls leads_service.move_stage(session, lead_id, stage_id, publisher=http_publisher).
  2. Service updates leads.current_stage_id, inserts lead_stage_history row, commits.
  3. Service calls publisher.publish(LeadStageChanged(lead_id, from, to)).
  4. HTTP publisher POSTs the event to backend's /_internal/events.
  5. Backend's internal route forwards to app.state.bus.publish(event).
  6. All connected WS clients receive the event.
  7. Frontend useRealtime invalidates ['leads'], ['leads', X], ['dashboard'], ['stages-history', X].
  8. 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 localStorage as mlc.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, and next_followup_date distribution 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_followups covering 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.

  1. Phase 1 — Auth (signup/login/me/mcp-token), Leads CRUD + table + detail, Notes (TipTap + GET/PUT), seed.
  2. Phase 2 — Pipeline stages CRUD + reorder, Kanban board + DnD + stage history, Followups CRUD.
  3. Phase 3 — Dashboard summary endpoint + metric cards + 5 charts + Followup Action Center.
  4. Phase 4 — Realtime bus + WS endpoint + frontend WS hook; MCP server + 8 tools + internal events route.
  5. 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, init app.state.bus.
  • backend/app/core/security.py — new.
  • backend/app/deps.py — new.
  • backend/app/db/base.py — import every model under db/models/ so create_all sees 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 call publisher.publish(...).
  • backend/app/routes/{auth,leads,stages,notes,followups,dashboard,ws,internal}.py — new.
  • backend/app/db/seed.py — new.
  • backend/pyproject.toml — add passlib[bcrypt], pyjwt, python-multipart.
  • mcp/ — entire package, new.
  • frontend/src/api/{auth,leads,stages,notes,followups,dashboard}.ts — TanStack hooks, follow health.ts reference 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 inline Home in App.tsx).
  • frontend/src/App.tsx — switch to multi-route layout with ProtectedRoute.
  • frontend/tailwind.config.jsdarkMode: '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 — add JWT_SECRET, INTERNAL_SHARED_SECRET, MCP_TOKEN_TTL_DAYS.

Existing skeleton conventions kept:

  • Health endpoint, hey-api SDK + pnpm regen workflow, axios throwOnError, FastAPI HTTPException JSON shape, init_db lifespan 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) — extend tests/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 / buildpnpm typecheck && pnpm build in frontend/. SDK regen: backend DEBUG=true, run pnpm regen, revert.
  • Seed sanitypython -m app.db.seed against a fresh poc.db; hit GET /api/dashboard/summary and confirm non-trivial numbers.
  • End-to-end realtime demo — start backend, start frontend, start MCP via python -m mindlap_crm_mcp from a second terminal, run create_lead and move_lead_stage tools, 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/events rejects requests missing the shared secret.

Laps & stories

mindlap
AboutFAQPrivacyTerms

© 2026 mindlap