# Sales Agent

> One sales agent. Your CRM (sqlite/hubspot/close/attio/salesforce) or none. Your channels (gmail, linkedin). Runs on any MCP-capable harness.

- **Type:** MCP server
- **Install:** `agentstack add mcp-dominien-sales-agent`
- **Verified:** Yes — security-reviewed for prompt injection and unsafe behavior
- **Seller:** [Dominien](https://agentstack.voostack.com/s/dominien)
- **Installs:** 0
- **Category:** [Databases](https://agentstack.voostack.com/c/databases)
- **Latest version:** 0.1.0
- **License:** MIT
- **Upstream author:** [Dominien](https://github.com/Dominien)
- **Source:** https://github.com/Dominien/sales-agent

## Install

```sh
agentstack add mcp-dominien-sales-agent
```

Requires the [AgentStack CLI](https://agentstack.voostack.com/docs/cli). Works with Claude Code, Cursor, and any MCP-compatible agent.

## About

# sales-agent

  

  Built for a friend who runs outbound for a living. The agent read his HubSpot, scored 300 contacts,
  sent 120 personalized LinkedIn invites, and surfaced 3 signed deals hiding in his own pipeline.
  Now it's open source. Runs on your laptop. Your data stays yours.

  
  
  
  
  
  
  
  
  

  
  
  
  
  
  

---

## What you get

- **Pick your CRM — or go CRM-less.** SQLite-first mode means you can start
  fresh with zero setup. Add HubSpot / Close / Attio / Salesforce later without
  rewriting anything.
- **Multiple channels.** Email (Gmail — draft-only, human sends) and LinkedIn
  (autonomous with strict rate-limits). Adding Outlook / Slack / SMS is a
  ~150-LOC extension.
- **10 composable skills.** cold-outreach, follow-up-loop, inbox-classifier,
  prospect-research, research-outreach, lead-recovery, compose-reply,
  pipeline-analysis, performance-review, contact-manager.
- **Built-in feedback loop.** Section A/B/C learnings file + deterministic
  analytics → the agent gets better over time with your explicit approval of
  every new rule.
- **Safety rails everywhere.** Per-channel rate limiter enforced before every
  send. Never-invent-details rule. Email is draft-only. LinkedIn caps stay
  below flagging thresholds. Silent rejects + transient click races are
  detected and auto-handled without halting batches.

---

## CRM support matrix

| CRM | Status | Setup | Notes |
|---|---|---|---|
| **SQLite** (no CRM) | Shipped v1 | zero | The tracker IS the CRM — start fresh, always available |
| **HubSpot** | Shipped v1 | harness OAuth | Hosted MCP at `mcp.hubspot.com/anthropic` |
| **Close** | Shipped v1 | harness OAuth | Hosted MCP at `mcp.close.com/mcp` |
| **Attio** | Shipped v1 | harness OAuth | Hosted MCP per `docs.attio.com/mcp` |
| **Salesforce** | Shipped v1 | sfdx + self-hosted MCP | `salesforcecli/mcp` |
| Pipedrive | v1.1 | self-hosted (community) | Community MCP — pin version, flag maintenance risk |
| Folk | v1.1 | self-hosted (community) | Community MCP |
| Notion / Airtable / Monday / Zoho | v1.1 | harness OAuth | First-party hosted MCPs — same pattern as HubSpot |
| **Bring your own** | always | ~150 LOC | Implement `CRMAdapter` — see `src/adapters/README.md` |

## Channel support matrix

| Channel | Status | Outbound | Rate limit |
|---|---|---|---|
| **Email (Gmail)** | Shipped v1 | Draft only (user sends) | 200 drafts/day (soft cap) |
| **LinkedIn** | Shipped v1 | Autonomous with guardrails | 20 invites/day, 80/week, 40 msgs/day, 5 personalized-notes/month |
| **Bring your own** | always | ~150 LOC | Implement `Channel` — see `src/channels/README.md` |

---

## 60-second quickstart (SQLite + email only)

```bash
# 1. Install
git clone /sales-agent && cd sales-agent
npm install

# 2. Wizard — writes agent.config.json + .env skeleton
npx tsx src/init.ts
# Choose: sqlite, email, fill in your name/email/offering, accept defaults.

# 3. Sanity check
npx tsx src/tracker.ts read       # → []  (empty tracker bootstraps clean)
npx tsx src/config.ts              # → prints resolved config

# 4. Invoke a skill from your harness (e.g., Claude Code)
#    Copy a template from prompts/invoke-skill.md into your chat.
```

That's it. Your first cold email campaign: point `cold-outreach` at a list of
email addresses → the agent drafts personalized messages (one per contact)
into Gmail → you review and send.

## 5-minute quickstart (with LinkedIn)

LinkedIn ships in-repo as a TypeScript scraper. No external MCP server, no
`claude mcp add`, no `uvx`. Just two extra commands on top of the email setup:

```bash
# 1. Install Chromium (one-time)
npx playwright install chromium

# 2. Set your LinkedIn display language to English BEFORE logging in.
#    Settings → Account preferences → Display language → English.

# 3. One-time interactive browser login
npx tsx src/linkedin/cli.ts login        # pops a Chromium window; sign in
npx tsx src/linkedin/cli.ts check        # → {"status":"authed"}

# 4. Re-run the wizard to add linkedin to channels
npx tsx src/init.ts
```

After that, every skill that touches LinkedIn shells out to
`npx tsx src/linkedin/cli.ts `. The first call spawns a long-lived
warm-browser daemon (~30s); subsequent calls reuse it over a Unix socket and
return in (Claude Code / Cursor / Continue / Windsurf)"]]
    Skills["skills/*.mdpure instruction — no codereferences CLAUDE.md + program.mdreads config via src/config.ts"]

    subgraph CRMAdapters["CRM adapter layer — pick one"]
        direction LR
        SQLite[("SQLiteno-CRM default")]
        HubSpot[HubSpot]
        Close[Close]
        Attio[Attio]
        Salesforce[Salesforce]
        BYOCRM([BYO adapter])
    end

    subgraph ChannelAdapters["Channel adapter layer — one or more"]
        direction LR
        Gmail["Gmaildraft-only"]
        LinkedIn["LinkedInautonomous + rate caps"]
        BYOCh([BYO: Outlook / Slack / SMS])
    end

    subgraph Core["Local core (src/) — always runs"]
        direction TB
        DB["db.tsunified SQLite schema"]
        Tracker["tracker.tsidentity resolution"]
        RateLimiter["rate-limiter.tsper-channel caps"]
        Scoring["scoring.tsfit × engagement → tier"]
        Performance["performance.tsdeterministic analytics"]
        Learnings["learnings.tsSection A/B/C log"]
        Config["config.tsagent.config.json"]
    end

    TrackerDB[("tracker.dbactivity log+ CRM when crm=sqlite")]

    User --> Harness
    Harness --> Skills
    Skills -->|mcp__crm__*| CRMAdapters
    Skills -->|mcp__channel__*| ChannelAdapters
    Skills -->|npx tsx src/*.ts| Core

    CRMAdapters --> TrackerDB
    ChannelAdapters -.->|rate check/record| RateLimiter
    Core --> TrackerDB

    classDef default fill:#f8f9fb,stroke:#4b5563,stroke-width:1px,color:#111827
    classDef user fill:#dbeafe,stroke:#2563eb,color:#1e3a8a
    classDef skill fill:#fef3c7,stroke:#d97706,color:#78350f
    classDef crm fill:#dcfce7,stroke:#16a34a,color:#14532d
    classDef channel fill:#fce7f3,stroke:#db2777,color:#831843
    classDef core fill:#e0e7ff,stroke:#4f46e5,color:#312e81
    classDef db fill:#fef2f2,stroke:#dc2626,color:#7f1d1d
    class User,Harness user
    class Skills skill
    class SQLite,HubSpot,Close,Attio,Salesforce,BYOCRM crm
    class Gmail,LinkedIn,BYOCh channel
    class DB,Tracker,RateLimiter,Scoring,Performance,Learnings,Config core
    class TrackerDB db
```

**Reading the diagram:**
- Your harness invokes a skill (just a markdown file).
- The skill reads `agent.config.json` to know which **CRM adapter** (exactly one) and which **channels** (one or more) are active.
- External CRM + channel calls go through MCP tools in your harness.
- Tracker / rate-limiter / scoring / performance / learnings always run locally via Node.
- `tracker.db` is the local source of truth for activity — and the CRM itself when `crm=sqlite`.

More detail in [`docs/architecture.md`](docs/architecture.md).

---

## Data model

**One row per contact** in the local `tracker.db`, keyed by UUID with unique
secondary indexes on `email` and `linkedin_url`. External-CRM linkage via
`crm_source` + `crm_id`.

Every channel gets its own columns (`email_last_draft_id`,
`linkedin_connection_status`, etc.) so signals from one channel never
overwrite the other. Replies are tagged with the channel they arrived on.

Full schema: [`src/db.ts`](src/db.ts).

### Why SQLite even when you have an external CRM?

The external CRM owns the canonical contact record. The local tracker is the
**activity log** + **scoring / rate-limit state**. Keeping them separate means:

- You can switch CRMs later without losing your agent's history.
- You can run the agent offline for research & analytics.
- Rate-limiter state doesn't pollute your CRM with agent-internal noise.
- Performance analytics run fast (indexed local queries, not N HubSpot API calls).

---

## Safety rails

- **Email is draft-only.** The agent creates Gmail drafts; you send them after
  review. Never auto-sent.
- **LinkedIn has hard caps.** 20 invites/day, 80/week, 40 messages/day, plus
  a monthly personalized-note budget (default 5/mo, LinkedIn free-tier) —
  all below LinkedIn's flagging thresholds. Per-skill enforcement via
  [`src/rate-limiter.ts`](src/rate-limiter.ts).
- **Never-invent-details rule.** Skills will skip a contact rather than
  fabricate a personalization detail.
- **Silent-reject distinct from real errors.** When LinkedIn closes the
  invite dialog without transitioning to Pending (upsell / throttle), the
  contact is skipped without consuming rate budget or counting toward the
  3-consecutive-error hard-stop.
- **One-shot auto-retry on transient click races** in `connect`; only
  genuine `send_failed` errors advance the consecutive-error counter.
- **3-error hard-stop.** After 3 consecutive `send_failed` errors, the
  skill exits and logs an observation — no silent retries past that.
- **Note-quota auto-fallback.** When the monthly personalized-note budget
  is exhausted (detected from a silent note-drop), skills continue with
  bare invites and queue the drafted note for post-accept DM delivery.
  No mid-batch prompt.
- **Match validation before LinkedIn sends.** Search results are scored on
  surname uniqueness + company + location overlap; ambiguous top hits are
  stashed in `output/research/ambiguous/` instead of shipped.
- **`do_not_contact` is honored everywhere.** Once set (by
  `inbox-classifier` on `BOUNCE`/`NEGATIVE_HARD`, or manually via
  `tracker.ts skip`), no outreach skill will touch the contact on any
  channel.
- **Section C is human-only.** `performance-review` proposes rule blocks; it
  never edits `learnings.md` Section C itself.

See [`docs/rate-limits.md`](docs/rate-limits.md) for the full safety-rail playbook.

---

## Extensibility

### Add your own CRM
```ts
// src/adapters/.ts
import type { CRMAdapter } from './crm.ts';
export function createMyCrmAdapter(): CRMAdapter {
  return {
    name: 'my-crm',
    async searchContacts(q) { /* map to your CRM's API / MCP */ },
    async upsertContact(c) { /* ... */ },
    // ... implement the rest of CRMAdapter
  };
}
```
1. Add `'my-crm'` to the `CRMName` union in `src/adapters/crm.ts`.
2. Add a case in `loadAdapter()`.
3. Document in `docs/crm-adapters.md`.

### Add your own channel
Same pattern — implement `Channel`, register in `channel.ts`, document in
`docs/channels.md`. If the channel has rate limits, add a key in
`src/rate-limiter.ts`.

---

## FAQ

**Do I need a CRM?** No. `sqlite` mode is first-class — the tracker serves
as your CRM. Add an external CRM when you're ready without rewriting.

**Will it send emails without my review?** No. Email is draft-only. The agent
creates drafts in Gmail; you review and send manually.

**Will it send LinkedIn messages without review?** Yes — within strict rate
limits (20 invites/day, 40 messages/day by default). Defaults are conservative.
You can tighten them in `agent.config.json` or switch to draft-only by
overriding the skill's send step.

**Can I switch CRMs later?** Yes. Change `agent.config.json` → `crm`. Existing
tracker rows keep their `crm_source` so you can migrate gradually.

**Is LinkedIn scraping allowed?** It's a grey area. Personal use only — the
in-repo scraper drives a real browser as you, with cookies you logged in
with, so it walks the same line your manual browser does. May still conflict
with LinkedIn's Terms. You assume the risk when you use it.

**What harnesses are supported?** Anything with MCP support. Tested on Claude
Code. See [`AGENTS.md`](AGENTS.md) for the compatibility matrix.

**What if my CRM doesn't have MCP support?** Two options: (a) run `sqlite`
mode and use the local tracker as your CRM; (b) write a ~150-LOC adapter that
calls your CRM's HTTP API directly. Skills don't care which — they talk to the
`CRMAdapter` interface.

---

## Project layout

```
sales-agent/
├── README.md                  # you are here
├── CLAUDE.md                  # shared message rules (cross-CRM, cross-channel)
├── AGENTS.md                  # harness compatibility
├── program.md                 # universal skill constraints
├── agent.config.json          # your configuration (written by init wizard)
├── docs/                      # setup, architecture, channels, crm-adapters, rate-limits, migration
├── src/
│   ├── db.ts tracker.ts config.ts rate-limiter.ts scoring.ts performance.ts learnings.ts init.ts
│   ├── skip-flags.ts          # hard / warm / personal skip-tier classifier
│   ├── cohort-builder.ts      # typed outreach-queue builder + CLI
│   ├── honorifics.ts          # strips Dr./Prof. Dr./Dipl.-Ing. from firstname
│   ├── adapters/              # CRM adapters (sqlite, hubspot, close, attio, salesforce)
│   ├── channels/              # Channel adapters (gmail, linkedin)
│   └── linkedin/
│       ├── match-validator.ts # post-search candidate confidence scorer
│       └── scrape/ …          # Playwright scrapers (connect, search, inbox, …)
├── skills/                    # 10 skill markdown files
├── knowledge/                 # learnings.md, scoring-config.md, research-config.md, crm-field-mapping.md
├── prompts/invoke-skill.md    # ready-to-paste skill invocations
└── output/                    # dossiers, drafts, analysis, performance reports
```

---

## License

MIT. See [`LICENSE`](LICENSE).

## Acknowledgements

Built on top of:
- [Model Context Protocol](https://modelcontextprotocol.io/) — the open standard that makes the CRM and email channels pluggable.
- [Playwright](https://playwright.dev/) (via [rebrowser-playwright](https://github.com/rebrowser/rebrowser-playwright)) — drives the in-repo LinkedIn scraper.
- First-party MCP servers from HubSpot, Close, Attio, Salesforce, and the Gmail MCP team.

Inspired by two progenitor agents (`hubspot-email-agent`, `linkedin-sales-agent`)
that proved out the patterns.

## Source & license

This open-source MCP server is cataloged on AgentStack and links to its original source — we do not rehost the code.

- **Author:** [Dominien](https://github.com/Dominien)
- **Source:** [Dominien/sales-agent](https://github.com/Dominien/sales-agent)
- **License:** MIT

Install and usage instructions live in the source repository linked above.

## Pricing

- **Free** — Free

## Security capabilities

Automated source analysis of v0.1.0 — what this tool can access:

- **Network access:** no
- **Filesystem access:** no
- **Shell / process execution:** no
- **Environment & secrets:** yes
- **Dynamic code execution:** no

*"Yes" means the capability is present in the source — more access means more to trust, not that it is unsafe.*


## Versions

- **0.1.0** — security scan: passed — Imported from the upstream source.

## Links

- Listing page: https://agentstack.voostack.com/l/mcp-dominien-sales-agent
- Seller: https://agentstack.voostack.com/s/dominien
- Browse the marketplace: https://agentstack.voostack.com/browse

---
Listed on AgentStack — the marketplace for AI agent skills and MCP servers. Every listing is security-reviewed. Creators keep 70%.
