Skip to main content
Glama
jnot807

Juicebox MCP

by jnot807
README.md
# Juicebox MCP

A local MCP server that reads your Juicebox sourcing data into Claude — saved
searches and their scored results — using your own logged-in Juicebox session.

Runs entirely on your machine. Your session never leaves it, and every call is
made as you, on your own seat.

**Reads cost no export credits.** Everything the read tools return comes from the
same free surface the search-results page already renders. One tool writes, and
says so: `jb_run_search` creates a real saved search in your workspace.

---

## Install

### Option A — Desktop Extension (easiest)

Download `juicebox-mcp.mcpb` from
[Releases](https://github.com/jnot807/juicebox-mcp/releases), then double-click it,
or drag it into **Claude Desktop → Settings → Extensions**.

There is no API key to paste. After installing, do the one-time browser steps
below.

### Option B — from source

```bash
git clone https://github.com/jnot807/juicebox-mcp.git
cd juicebox-mcp
npm install          # also downloads the Chromium build (see note)
npm run login        # a real browser opens — sign in to Juicebox yourself
npm run check        # proves the session works headless
```

Then register it with Claude Code:

```bash
claude mcp add -s user juicebox -- node "$(pwd)/server.js"
```

`-s user` makes it available in every session; without it the registration is
scoped to whatever directory you happened to run it from.

### The one-time browser download

This drives a real Chromium, and that binary is **not** part of `node_modules` —
it is a one-time download of roughly 500MB into a shared cache
(`~/Library/Caches/ms-playwright` on macOS).

`npm install` fetches it automatically via a postinstall step. **Desktop Extension
users need to run it once by hand**, because an extension bundles `node_modules`
but not that cache:

```bash
npx patchright install chromium
```

If it is missing, the server tells you so in plain language rather than throwing a
stack trace about a missing executable.

### Signing in

Authentication is a real sign-in, not a key. `npm run login` opens a browser
window; sign in to Juicebox as you normally would. The session is then stored in
`session/` (gitignored, `chmod 600`) and reused headlessly.

Sign in again whenever `npm run check` starts failing — sessions expire.

---

## Tools

| Tool | What it does |
|---|---|
| `jb_list_searches(projectId?)` | Saved searches on a project (id + name). |
| `jb_get_results(searchId, limit?, minMatchRate?)` | A search's ranked candidates — name, LinkedIn URL, title, company, location, `matchRate`, per-criterion verdicts, and dated `experience[]` + `education` read off the rendered cards. Up to ~500 per call. |
| `jb_count(queryInput, searchId?)` | Size a filter set **without** running a search — the tuning primitive. `queryInput` is a PATCH over a harvested template; check `noEffect` in the response. |
| `jb_run_search(prompt, need?)` | **WRITES.** Creates and runs a new search from a natural-language prompt, then returns its candidates. Leaves a saved search visible to your whole workspace — confirm before using it. |

`experience[]` is the only way to see **past** employers: the API payload carries
just the current one, so alumni of a target company are invisible without it.

---

## Which project it reads by default

Nothing is hardcoded. At sign-in, a probe loads `/projects`, which redirects into
a project your seat can see, and that id is saved as `defaultProjectId` in
`session/session-meta.json`.

It is written **once and then left alone**. The redirect follows whichever project
the app most recently had open, so trusting it on every run would make a tool call
with no `projectId` read a different project than it did yesterday.

Resolution order:

1. `JUICEBOX_PROJECT_ID` (env — this is what the Desktop Extension's optional
   "default project" field sets)
2. `JUICEBOX_VALIDATOR_PROJECT` (env — also pins the auth check to that project)
3. `defaultProjectId` in `session/session-meta.json`, set by discovery

Every tool also takes an explicit `projectId`, which always wins.

Juicebox project ids are ~20-character keys like `c5PheL2fANnX6uBQVUdo` — the
`/project/<id>/` part of a URL. If you pass a UUID, the server rejects it with an
explanation rather than silently navigating to a project that does not exist.

---

## Two rules the tools carry

- **`verdictFound: false` → `unknown`, never a negative.** "No evidence found"
  and "evidence says no" are different verdicts. Collapsing them scores a
  candidate down for a criterion nobody could actually check.
- **Broad skill terms dilute ranking.** Skills are OR-weighted; a
  population-wide term like "Account Management" on a customer-success search
  inflates the pool by roughly 3.4×. Drop the generic terms and promote the one
  hard requirement to a skill filter.

---

## Running scripts while the server is up

You can't share the browser profile: `session/profile/` is single-writer, and the
MCP server holds it whenever it is running. A second process trying to open it
fails the auth check — which reports itself as "session expired" and sends you
round in circles re-logging-in.

For diagnostics, build a fresh context from the checkpoint instead. No lock, same
session:

```js
const { chromium } = require('patchright');
const browser = await chromium.launch({ headless: true });
const context = await browser.newContext({ storageState: 'session/storage-state.json' });
```

---

## How it works, and the traps

The results page is server-rendered on first load, so `/api/profiles/results` only
fires on *interaction*. The client nudges the pager to make the app issue its own
request, then captures the response — which carries the whole ranked set, not just
the visible page.

Three things that will bite anyone editing `client.js`:

1. **Never use `addInitScript`.** Patchright silently no-ops it as an
   anti-detection measure — no error, the script just never runs. Use
   `page.on('response')`.
2. **The API's `linkedin_url` is encrypted** (`hex:hex`), as are `profiles[].url`
   and `profileDetails.id`. Real URLs come from the rendered cards and are joined
   on normalised `full_name` — measured at 100% on a live search.
3. **The list blanks mid-pagination.** A null pager reading means "still moving",
   not "failed". Gating anything on pager-change detection during a transition is
   how two earlier bugs happened.

---

## When it breaks

This rides Juicebox's **internal** API. There is no stability contract, and it can
change without notice.

- `npm run check` fails → session expired: `npm run login`.
- The server says Chromium is missing → `npx patchright install chromium`.
- `jb_get_results` returns `source: "dom-fallback"` → the API capture broke; you
  lose `matchRate` and criteria. Check `RESULTS_PATH` still matches.
- `jb_get_results` reports `joinedLinkedInUrls: 0` → the card markup changed;
  revisit `harvestCards` / `rewindToFirstPage`.
- Empty search list → the project page markup changed; see `listSavedSearches`.

---

## Requirements

- Node.js 18 or newer
- A Juicebox account you can sign into
- ~500MB free disk for the Chromium download

## Licence

MIT. Not affiliated with or endorsed by Juicebox.

TDQS

A4.6/5.0

Scored across 4 tools

Disambiguation5/5

Each tool has a clearly distinct purpose: list saved searches, fetch ranked results, tune filter counts, and create/run a new search. No two tools overlap in function, making misselection unlikely.

Naming Consistency5/5

All tools follow the consistent pattern of 'jb_' prefix + verb_noun (list_searches, get_results, run_search) with count as a concise verb. This is predictable and uniform.

Tool Count5/5

Four tools is well-scoped for a talent sourcing server. Each tool earns its place and covers the core workflow without redundancy or bloat.

Completeness4/5

The surface covers the essential lifecycle: listing existing searches, retrieving results, tuning filters without execution, and creating new searches. Minor gaps like deleting or updating saved searches exist but are not critical for the primary use case.

Maintenance

ActivityMaintained
ResponsivenessSyncing