An AI-powered lead qualification system with an MCP server for Claude Desktop and a Next.js SaaS frontend — built within 3 days.
All dev tooling runs via Docker. You do not need Go, Node, Bun, goose, or sqlc installed locally.
Developers: see
CLAUDE.mdfor commands, conventions, and test instructions.
Tip
This project demonstrates and acts as a foundation boilerplate for 2-mode operation of an AI MCP as:
- Single-tenant Claude Desktop Go binary (individual use)
- Multi-tenant Next.js SaaS + chat LLM + MCP Go server (hosted environment)
The project architecture includes:
- AI SDK + assistant-ui chat
- Example MCP development
- Shared backend code for both binary and server operation
- Claude Code (and other development agent) usage and best practices
- Docker best practices for development and production
- Database security role/schema best practices (adopted early for scale)
See the full demo with screenshots and flows here:
👉 View Demo Docs
Claude Desktop
└── bin/outreacher-mcp-0.1.0-linux-amd64 (Go stdio binary, WSL)
└── localhost:5432 (standalone postgres, volume: outreacher_standalone_data)
Browser / Next.js frontend
└── app (Next.js, :3000)
└── mcp (:3001, Go SSE server, air hot reload)
└── postgres (internal only, volume: outreacher_postgres-data)
Two runtime modes — standalone binary for Claude Desktop, Docker Compose for development. Important: use separate named volumes for each mode to avoid data directory conflicts.
- Next.js 15 (App Router, Bun)
- Go MCP server (
mcp/) — stdio + SSE transports, pgx/v5, sqlc, goose migrations - PostgreSQL 16 —
appschema, named Docker volume - Docker Compose — dev stack with air hot reload on the Go server
| User | Password | Role |
|---|---|---|
admin |
admin |
Owns schema, runs DDL and migrations |
app |
app |
Next.js runtime — member of app_crud + direct nextauth grants |
mcp |
mcp |
MCP server runtime — member of app_crud |
reporter |
reporter |
Read-only access — member of app_read |
Group roles (no LOGIN) — DEFAULT PRIVILEGES auto-cover new tables/sequences/types:
app_crud— SELECT/INSERT/UPDATE/DELETE on app schemaapp_read— SELECT on app schema
DATABASE_URL → mcp user (MCP server) or app user (Next.js). DATABASE_ADMIN_URL → admin user (goose only).
| Table | Key fields |
|---|---|
organizations |
id, name, slug (unique), is_system |
campaigns |
id, organization_id, name, slug, is_default |
users |
id, email†, slug (unique), name, is_system |
organization_memberships |
organization_id, user_id, role (owner|admin|member) |
campaign_memberships |
campaign_id, user_id, role (admin|member|viewer) |
| Table | Key fields |
|---|---|
companies |
id, campaign_id, name, domain, industry, linkedin_url |
signals |
id, description — global, no campaign scope |
signal_keywords |
signal_id, keyword — global |
company_signals |
company_id, signal_id |
leads |
id, campaign_id, name, email†, linkedin_url†, company_id, title, status, score, location, phone |
notes |
id, lead_id, content, created_at |
contact_identifiers |
campaign_id, type, value — dedup key, PK is (campaign_id, type, value) |
† nullable. Leads are deduplicated by contact_identifiers, not by email.
Lead status flow: new → contacted → qualified → disqualified → converted
| Tool | Description |
|---|---|
search_leads |
Filter by name, email, status, company, campaign_id |
get_lead |
Full lead detail with company and notes |
update_lead_status |
Advance a lead through the status flow |
create_followup_note |
Append a note to a lead |
import_csv |
Import from CSV text (auto-detects Gojiberry, Revli formats) |
search_companies |
Search companies by name or domain |
get_company |
Full company detail with signals |
| Tool | Description |
|---|---|
list_campaigns |
List all campaigns for the current org |
get_campaign |
Get campaign details by id |
create_campaign |
Create a new campaign under the current org |
rename_campaign |
Rename a campaign's display name |
Build the binary:
npm run build:mcp
# outputs bin/outreacher-mcp-0.1.0-linux-amd64 (stdio, WSL)
# bin/outreacher-mcp-0.1.0-darwin-arm64 (stdio, macOS Apple Silicon)
# bin/outreacher-mcp-0.1.0-darwin-amd64 (stdio, macOS Intel)
# bin/outreacher-mcp-0.1.0-windows-amd64.exe
# bin/outreacher-server-0.1.0 (SSE server)Start standalone postgres:
docker run -d --name outreacher-pg --restart always \
-p 5432:5432 \
-e POSTGRES_DB=outreacher \
-e POSTGRES_USER=admin \
-e POSTGRES_PASSWORD=admin \
-v outreacher_standalone_data:/var/lib/postgresql/data \
-v "$(pwd)/postgres/init:/docker-entrypoint-initdb.d" \
postgres:16-alpineRun migrations:
PG_IP=$(docker inspect outreacher-pg --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
docker run --rm \
-v "$(pwd)/mcp:/src" -w /src \
--add-host=postgres:$PG_IP \
golang:1.24-alpine \
sh -c 'go run github.com/pressly/goose/v3/cmd/goose@latest \
-dir migrations postgres \
"postgresql://admin:admin@postgres:5432/outreacher" up'Claude Desktop config (Settings → Developer → Edit Config):
{
"mcpServers": {
"Outreacher": {
"command": "wsl.exe",
"args": [
"~/Code/outreacher/bin/outreacher-mcp-0.1.0-linux-amd64",
"--database-url", "postgresql://mcp:mcp@localhost:5432/outreacher",
"--openai-api-key", "sk-proj-..."
]
}
}
}Replace <user> with your WSL username. ANTHROPIC_API_KEY enables LLM-based CSV extraction for unknown formats; omit to use the built-in parsers only. Restart Claude Desktop after saving — the hammer icon confirms tools are connected.
The binary idempotently bootstraps system_default_org, system_default_user, and the Default campaign on first run.
Copy .env.example to .env and fill in API keys, then:
docker compose up --build -d| Service | Host port | Notes |
|---|---|---|
app |
3000 | Next.js dev server |
mcp |
3001 | Go SSE server, air hot reload |
postgres |
— | Internal only |
First-time migrations:
docker run --rm --network outreacher_default \
-v "$(pwd)/mcp:/src" -w /src \
golang:1.24-alpine \
sh -c 'go run github.com/pressly/goose/v3/cmd/goose@latest \
-dir migrations postgres \
"postgresql://admin:admin@outreacher-postgres-1:5432/outreacher" up'If the mcp container exited while waiting for migrations, restart it:
docker compose restart mcpfixtures/imports/ is gitignored — it contains live personal data used for local development and debugging only. Supported formats: Gojiberry, Revli startup contacts, Revli investor contacts, Revli startup companies, Revli investor companies.
© 2026 Vitruvian Technology, Corp. All rights reserved.
This code is provided under a noncommercial license for evaluation and research purposes only.