Skip to main content
Glama
Hammad-Tariq007

bd-crm-analytics

README.md
# BD CRM Analytics — MCP server

A **read-only** [MCP](https://modelcontextprotocol.io) server that exposes the Meissasoft
**BD Leads CRM** analytics (win rate, conversion funnel, connects economics, sales velocity,
forecast, and lead lookup) to MCP clients like **Cursor** and **Claude Desktop**.

It talks only to the CRM **public API** (`/api/v1/`) using a Personal Access Token
(`X-Api-Key`). It never writes anything — every tool is a GET. What a token can see is
governed entirely by the CRM: a token only returns analytics if **its owning user is a
workspace admin AND (the workspace owner OR has been granted analytics access)**.

---

## Tools

| Tool | What it returns |
|---|---|
| `list_metadata` | Custom fields (+ their options) and pipeline states — the valid values for filters. Call this first to discover profiles/countries/contract types/states. |
| `get_win_rate` | Win rate. `dimension="bd"` → overall + per-rep; `profile`/`lead_source`/`country`/`contract_type` → win rate sliced by that field. |
| `get_conversion_funnel` | Applied → … → Won funnel with per-step conversion %, drop-off, and biggest-leak stage. |
| `get_connects_economics` | Connects-per-win overall, ROI by segment (spend, connects/win, est. revenue-per-connect, wasted connects), and boosted-vs-not. `dimension="profile"|"country"`. |
| `get_velocity_and_cycle` | Sales velocity ($/day) with its 4 inputs, plus avg cycle length by Profile and Country. |
| `get_forecast` | Weighted pipeline forecast (open leads × stage win-probability × est. deal value) with per-stage breakdown. |
| `list_leads` | Paginated leads with key fields + BD custom fields. Filter by `state`, `profile`, `country`, date range; `limit`/`offset`. |

Analytics tools accept an optional date range: `date_filter` (e.g. `this_month`, `last_month`,
`last_3_months`) **or** an explicit `start_date`/`end_date` (`YYYY-MM-DD`). Omit for all-time.

---

## 1. Prerequisites

- **Node.js 18+** (uses the built-in `fetch`).

## 2. Install & build

```bash
cd bd-crm-mcp
npm install
npm run build      # compiles to dist/
```

## 3. Mint a CRM Personal Access Token

1. Sign in to the CRM (e.g. `https://bd-crm.meissasoft.com`) as a user who can see BD
   Insights — i.e. a **workspace admin** who is the **workspace owner** or has been granted
   **analytics access**. (If your token's user isn't allowed, every tool returns a clear
   `403` — that's expected.)
2. Go to **Profile → Settings → Personal access tokens** (API tokens) and **create a token**.
3. Copy it — it looks like `plane_api_xxxxxxxx…`. Store it as `CRM_API_TOKEN`.

Find the other values:
- `WORKSPACE_SLUG` — the workspace segment in the CRM URL, e.g. `bd-leads` in
  `…/bd-leads/projects/…`.
- `PROJECT_ID` — open the BD Leads project; it's the UUID in the URL:
  `…/projects/<PROJECT_ID>/…`.

## 4. Environment variables

| Var | Example | Notes |
|---|---|---|
| `CRM_BASE_URL` | `https://bd-crm.meissasoft.com` | No trailing slash. |
| `CRM_API_TOKEN` | `plane_api_…` | Sent as `X-Api-Key`. |
| `WORKSPACE_SLUG` | `bd-leads` | |
| `PROJECT_ID` | `99361d89-…` | The BD Leads project UUID. |

For local testing you can copy `.env.example` to `.env`; when wired into a client, set them
in the client config (below) instead.

---

## 5. Configure your MCP client

Use the **absolute path** to the built `dist/index.js`.

### Cursor

Edit `~/.cursor/mcp.json` (global) or `.cursor/mcp.json` in your project:

```json
{
  "mcpServers": {
    "bd-crm-analytics": {
      "command": "node",
      "args": ["/absolute/path/to/bd-crm-mcp/dist/index.js"],
      "env": {
        "CRM_BASE_URL": "https://bd-crm.meissasoft.com",
        "CRM_API_TOKEN": "plane_api_xxxxxxxxxxxxxxxxxxxx",
        "WORKSPACE_SLUG": "bd-leads",
        "PROJECT_ID": "99361d89-81b6-4eee-83a6-24e622182383"
      }
    }
  }
}
```

Reload Cursor; the `bd-crm-analytics` tools appear in the MCP tool list.

### Claude Desktop

Edit the config file:
- **macOS:** `~/Library/Application Support/Claude/claude_desktop_config.json`
- **Windows:** `%APPDATA%\Claude\claude_desktop_config.json`

```json
{
  "mcpServers": {
    "bd-crm-analytics": {
      "command": "node",
      "args": ["/absolute/path/to/bd-crm-mcp/dist/index.js"],
      "env": {
        "CRM_BASE_URL": "https://bd-crm.meissasoft.com",
        "CRM_API_TOKEN": "plane_api_xxxxxxxxxxxxxxxxxxxx",
        "WORKSPACE_SLUG": "bd-leads",
        "PROJECT_ID": "99361d89-81b6-4eee-83a6-24e622182383"
      }
    }
  }
}
```

Fully quit and reopen Claude Desktop. Ask e.g. *"What's our win rate by profile this
quarter?"* or *"Show the conversion funnel and biggest leak."*

> **Windows note:** if `node` isn't on Claude Desktop's PATH, use its full path
> (e.g. `"command": "C:\\Program Files\\nodejs\\node.exe"`) and a double-backslashed
> `args` path.

---

## Hosted service — web chat + remote MCP (`server/`)

Everything above is the **local stdio** server, unchanged. The same repo also ships a
**hosted** service (`server/`) that reuses the same `CrmClient` and the same 7 tools, and
adds three network surfaces behind one URL (`https://bd-crm.meissasoft.com/mcp`):

| Surface | Route | Who it's for | Gate |
|---|---|---|---|
| Web chat UI | `GET /mcp` | People in a browser | Login → httpOnly session cookie |
| Chat backend | `POST /mcp/chat` | (the UI) | Session cookie |
| Remote MCP | `POST /mcp/rpc` (alias `/mcp/sse`) | Claude Desktop | `Authorization: Bearer <your CRM token>` (or `X-Api-Key`) |

**Auth model.** Login is *separate* from data fetching. To sign in, a person presents
**their own** CRM personal token; the service verifies it passes the analytics gate
(workspace admin **and** owner-or-analytics-flag), captures their identity, then **discards
the token**. All data is fetched with a single server-side `CRM_ADMIN_TOKEN` — the browser
never sees it, and the `ANTHROPIC_API_KEY` is server-side only. The remote MCP endpoint uses
the same check on the token sent in its header.

### Environment (hosted only)

| Var | Example | Notes |
|---|---|---|
| `CRM_BASE_URL` | `https://bd-crm.meissasoft.com` | CRM public API base. |
| `CRM_ADMIN_TOKEN` | `plane_api_…` | **Owner-level** token; does all data fetching. Never sent to the browser. |
| `LLM_API_KEY` | `sk-or-v1-…` | Chat backend key — any OpenAI-compatible provider (OpenRouter). Server-side only. Blank ⇒ chat disabled. |
| `LLM_BASE_URL` | `https://openrouter.ai/api/v1` | OpenAI-compatible base URL. Defaults to OpenRouter. |
| `LLM_MODEL` | `anthropic/claude-sonnet-5` | Model slug. Defaults to Claude Sonnet on OpenRouter. |
| `WORKSPACE_SLUG` | `bd-leads` | |
| `PROJECT_ID` | `99361d89-…` | BD Leads project UUID. |
| `SESSION_SECRET` | 32+ random bytes | Signs the session cookie. `node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"` |
| `PORT` | `8787` | Listen port. |
| `NODE_ENV` | `production` | In prod, gives cookies the `Secure` flag. |

The chat backend calls an **OpenAI-compatible** chat/completions API (OpenRouter by default),
so the provider is just `LLM_BASE_URL` + `LLM_API_KEY`. The model is pinned in one place —
`DEFAULT_LLM_MODEL` in `server/config.ts` (currently `anthropic/claude-sonnet-5`) — and any
deploy can override it with `LLM_MODEL`. Only the LLM-call layer is provider-specific; the 7
tools and the `CrmClient` (env admin token) wiring are unchanged.

**Mint `CRM_ADMIN_TOKEN`:** sign in to the CRM as the workspace **owner** (or an admin with
analytics access), go to **Profile → Settings → Personal access tokens**, create one, and set
it as `CRM_ADMIN_TOKEN`. This is the only token stored, and it lives only in the server env.

### Run locally

```bash
npm install
npm run build:server
CRM_BASE_URL=http://localhost:8001 \
CRM_ADMIN_TOKEN=plane_api_… \
WORKSPACE_SLUG=bd-leads \
PROJECT_ID=99361d89-… \
SESSION_SECRET=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))") \
LLM_API_KEY=sk-or-v1-…  \
PORT=8790 npm run start:server
# open http://localhost:8790/mcp   (dev live-reload: npm run dev:server)
```

### Connect Claude Desktop to the remote MCP endpoint

Add this to `claude_desktop_config.json` (macOS: `~/Library/Application Support/Claude/…`,
Windows: `%APPDATA%\Claude\…`). It uses [`mcp-remote`](https://www.npmjs.com/package/mcp-remote)
to bridge stdio ↔ the remote HTTP endpoint, sending **your own** CRM token as a header:

```json
{
  "mcpServers": {
    "bd-crm-analytics": {
      "command": "npx",
      "args": [
        "-y", "mcp-remote",
        "https://bd-crm.meissasoft.com/mcp/rpc",
        "--header", "X-Api-Key:plane_api_xxxxxxxxxxxxxxxxxxxx"
      ]
    }
  }
}
```

`X-Api-Key:<token>` (no space) sidesteps a known `mcp-remote` header-parsing quirk. If you
prefer a bearer token, use `"--header", "Authorization:Bearer plane_api_…"` — but keep it
one token with no space after the header name, or pass it via an env-substituted value. Fully
quit and reopen Claude Desktop; the tools appear. Your token must pass the analytics gate or
every call returns a readable 401/403.

### Deploy (reference — not auto-applied)

Nothing environment-specific is hardcoded — the service reads everything from env, so
**dev and prod differ only by their `.env` file**:

- `.env.local.example` — dev values (`CRM_BASE_URL=http://localhost:8001`, dev `PROJECT_ID`).
- `.env.prod.example` — prod values (`CRM_BASE_URL=https://bd-crm.meissasoft.com`, prod `PROJECT_ID`).

Both carry **placeholders only** for `CRM_ADMIN_TOKEN`, `LLM_API_KEY`, `SESSION_SECRET`.

Build & push the image to GHCR (same flow as the CRM image), then deploy from it:

```bash
export MCP_IMAGE_TAG=$(git rev-parse --short HEAD)      # or a semver, e.g. v1.0.0
echo "$GHCR_PAT" | docker login ghcr.io -u <github-username> --password-stdin
docker build -t ghcr.io/meissasoft/bd-crm-mcp:$MCP_IMAGE_TAG \
             -t ghcr.io/meissasoft/bd-crm-mcp:latest .
docker push ghcr.io/meissasoft/bd-crm-mcp:$MCP_IMAGE_TAG
docker push ghcr.io/meissasoft/bd-crm-mcp:latest
```

On the host:

```bash
cp .env.prod.example .env      # then fill the 3 secrets
docker compose up -d bd-crm-mcp
```

- `Dockerfile` — builds `server/` and runs `server/dist/server/main.js` on `PORT`.
- `deploy/docker-compose.snippet.yml` — isolated service block; **`image:` from GHCR**, every
  value via `${VAR}` (no baked IDs/URLs). Caddy network is a `TODO` to fill after inspecting the stack.
- `deploy/Caddyfile.snippet` — routes `/mcp/*` to the container (`flush_interval -1` for SSE).

Deploy is a **separate, deliberate step**: push the image, set the `.env`, add the container +
Caddy route, and confirm the CRM app is untouched.

---

## Behavior & troubleshooting

- **Read-only.** No tool creates, edits, or deletes anything.
- **`401`** → token missing/invalid/expired: check `CRM_API_TOKEN`.
- **`403`** → the token's user lacks analytics access (needs workspace admin + owner-or-flag).
- **`404`** → check `CRM_BASE_URL` and `WORKSPACE_SLUG`.
- Errors are returned as readable tool results; the server does not crash.
- Estimated figures (deal value, revenue-per-connect, velocity, forecast) come straight from
  the CRM's deal-value proxy and are labelled as estimates there.

## Development

```bash
npm run dev     # run from source with tsx (no build step)
```

TDQS

A4.1/5.0

Scored across 7 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: economics metrics, conversion funnel, forecast, velocity/cycle, win rate, leads listing, and metadata discovery. No overlap in functionality.

Naming Consistency5/5

All tools follow a consistent verb_noun pattern using 'get_' for metrics and 'list_' for data retrieval. The naming is uniform and predictable.

Tool Count5/5

Seven tools is appropriate for a CRM analytics server, covering key analytic dimensions without being overwhelming or too sparse.

Completeness5/5

The tool set covers the primary analytics needs: economics, funnel, forecast, velocity, win rates, plus leads listing and metadata. No obvious gaps for the intended purpose.

Maintenance

ActivityMaintained
ResponsivenessNo issues