Skip to content

technicallypete/outreacher

Repository files navigation

Outreacher

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.md for commands, conventions, and test instructions.

Tip

This project demonstrates and acts as a foundation boilerplate for 2-mode operation of an AI MCP as:

  1. Single-tenant Claude Desktop Go binary (individual use)
  2. 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)

📸 Demo & Walkthrough

See the full demo with screenshots and flows here:
👉 View Demo Docs


Architecture

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.


Tech Stack

  • Next.js 15 (App Router, Bun)
  • Go MCP server (mcp/) — stdio + SSE transports, pgx/v5, sqlc, goose migrations
  • PostgreSQL 16app schema, named Docker volume
  • Docker Compose — dev stack with air hot reload on the Go server

DB Users

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 schema
  • app_read — SELECT on app schema

DATABASE_URLmcp user (MCP server) or app user (Next.js). DATABASE_ADMIN_URLadmin user (goose only).


Schema (app)

Multi-tenancy

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)

Domain (scoped to campaign_id)

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


MCP Tools

Domain tools (all accept optional campaign_id; defaults to startup campaign)

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

Campaign management (stdio binary only)

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

Mode 1 — Claude Desktop (Go binary via WSL)

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-alpine

Run 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.


Mode 2 — Docker Compose (dev stack)

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 mcp

Imports

fixtures/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.

© Copyright

© 2026 Vitruvian Technology, Corp. All rights reserved.

This code is provided under a noncommercial license for evaluation and research purposes only.

About

Boilerplate AI MCP+SaaS LLM foundation with use-case as an AI-powered lead management CRM system with an MCP server for Claude Desktop and a Next.js SaaS frontend.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Languages