Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 126 additions & 1 deletion mixpanel-plugin/commands/auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,21 @@ arguments, run `session`.

### "login"

Two routing paths — same-machine vs headless. Detect the environment
first:

- **Same-machine** (you can launch the user's browser from the terminal
the user is in — typical Claude Code CLI setup): run `mp login`
directly and let the user complete the flow in their browser.
- **Headless** (Claude Cowork sandbox, devcontainer, browserless SSH —
`$DISPLAY` unset, `$BROWSER` unset, or you know you're in Cowork): use
the two-shot `--start` / `--finish` flow that emits machine-parseable
JSON envelopes. The CLI process can't reach the user's host browser
via loopback, so the flow runs in two CLI invocations bridged by the
user pasting the redirect URL back into chat.

#### Same-machine path

For first-time setup, the frictionless one-shot path is `mp login`. It
runs the right auth flow for the environment, derives the account name
from `/me`, and pins a default project. Tell the user to run:
Expand All @@ -49,11 +64,121 @@ Optional flags they may want:
- `--project ID` — skip the project picker
- `--service-account` — force the SA path (requires `MP_USERNAME` + `MP_SECRET` in env)
- `--token-env VAR` — force the static-bearer path (reads token from `$VAR`)
- `--no-browser` — print the authorization URL instead of launching a browser
- `--no-browser` — print the authorization URL instead of launching a browser (still requires a TTY for paste-back)

After the user confirms they ran it, verify with `account test`:
`python3 ${CLAUDE_PLUGIN_ROOT}/skills/mixpanelyst/scripts/auth_manager.py account test`

#### Headless path (Claude Cowork, devcontainers, browserless remote)

The two-shot `mp login --start` / `--finish` flow exists for environments
where the loopback OAuth callback can't reach the user's host browser.
You drive the dance directly via `Bash` calls and `AskUserQuestion`:

1. **Start.** Run:
```
! mp login --start
```
For EU / India users, append `--region eu` or `--region in`. The CLI
prints a single-line JSON envelope to stdout. Parse it as
`json.loads(stdout)`.

Expected shape:
```json
{
"schema_version": 1,
"state": "ok",
"authorize_url": "https://mixpanel.com/oauth/authorize/?...",
"redirect_uri": "http://localhost:19284/callback",
"expires_at": <unix-ts>,
"region": "us",
"inflight_path": "/home/.../inflight.json"
}
```

2. **Present the URL.** Show `authorize_url` to the user with this
exact framing:

> Open this URL in your browser, complete login, then copy the URL
> from your browser's address bar back here. The page will fail to
> load with "site can't be reached" — that's expected; copy the URL
> anyway. Tested on Chrome, Firefox, and Safari.

Use `AskUserQuestion` to collect the pasted URL.

3. **Finish.** Pass the pasted URL verbatim to `--finish`. Quote it so
shell metacharacters in the query string don't break the call:
```
! mp login --finish '<pasted URL>'
```

Expected `state: ok` envelope:
```json
{
"schema_version": 1,
"state": "ok",
"account": {"name": "...", "type": "oauth_browser", "region": "us"},
"user": {"email": "..."},
"project": {"id": "...", "name": "..."},
"project_pick": {
"auto_picked": true,
"method": "primary_org_lowest_id",
"primary_org_name": "...",
"primary_org_survivor_count": <int>,
"accessible_project_count": <int>,
...
},
"next": [
{"command": "mp project list", "label": "See all accessible projects"},
{"command": "mp project use <id>", "label": "Switch to a different project"}
]
}
```

Render based on `project_pick.method`:
- `"explicit"` → "✓ Logged in to project `<id>` as requested."
- `"sole_survivor"` → "✓ Logged in to project `<name>` (your only
active project)."
- `"sole_survivor_filtered"` → "✓ Logged in to project `<name>` —
the only non-demo, integrated project among your
`<region_compatible_count>` projects in this region. The others
are demos or have never received events. Run `mp project list` if
you want to pick a different one."
- `"primary_org_lowest_id"` → "✓ Logged in to project `<name>` —
auto-picked from `<primary_org_name>` (your most-active org;
`<primary_org_survivor_count>` projects there). Want a different
one?" If yes, run `mp project list` then `mp project use <id>`.
- `"fallback_with_unintegrated"` → "✓ Logged in to project `<name>`
— note: this project hasn't received events. Verify before running
queries."
- `"fallback_with_demos"` → "✓ Logged in to project `<name>` — note:
all your projects are demos. Pass `--project ID` next time if you
want a specific one."

4. **Handle the `cross_region_only` error.** If `--finish` exits 6 with
`state: error` and `error.code: NEEDS_REGION_SWITCH`, the user
authenticated against the wrong region. Show:

> Your account doesn't have any projects in `<auth_region>`. The
> `error.details.cross_region_projects` list shows projects in other
> regions. Want to retry with one of those?

Then offer to re-run `mp login --start --region eu` (or `--region in`)
based on the cross-region list.

5. **Recovery.** If `--finish` fails AFTER token exchange (e.g., `/me`
timed out, name collision), the CLI leaves a `.tmp-*` placeholder dir
in `~/.mp/accounts/`. Re-run with `--resume <PATH>` to retry the
publish without re-running PKCE. The error message will include the
placeholder path.

After the dance completes, verify with `account test`:
`python3 ${CLAUDE_PLUGIN_ROOT}/skills/mixpanelyst/scripts/auth_manager.py account test`

**Inflight TTL is 10 minutes.** If the user takes longer than that to
complete browser auth and paste back, `--finish` returns
`OAUTH_INFLIGHT_EXPIRED`. Re-run `mp login --start` to begin again.

### No arguments or "session"

Run: `python3 ${CLAUDE_PLUGIN_ROOT}/skills/mixpanelyst/scripts/auth_manager.py session`
Expand Down
137 changes: 99 additions & 38 deletions mixpanel-plugin/docs/quickstart-claude-cowork.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,78 @@

Get your Mixpanel credentials working inside [Claude Cowork](https://docs.anthropic.com/en/docs/claude-code/cowork) sessions so Claude agents can query your analytics data autonomously.

Cowork runs Claude agents in sandboxed virtual machines. These VMs don't have access to your local config files or browser, so credentials need to be exported from your machine into a "bridge file" that Cowork can read.
Cowork runs Claude agents in sandboxed virtual machines. The CLI has two ways to authenticate inside Cowork:

1. **Two-shot login (`mp login --start` / `--finish`)** — the simplest path if you don't have `mp` installed locally. Runs entirely inside Cowork; you paste a URL into your host browser, then paste the redirect URL back. No host-side setup.
2. **Bridge file** — if you already have credentials configured on your laptop (e.g., from regular `mp` use), export them into a "bridge file" Cowork can read. One-time setup, lasts across Cowork sessions.

Both paths work. Pick the one that fits your situation.

---

## What You'll Need

- **Claude Desktop** with Cowork access
- **A Mixpanel account** with access to a project
- **One of the following** for authentication:
- A **service account** (username + secret) from your Mixpanel project settings, OR
- A browser for **OAuth login** (interactive — your project is auto-discovered)
- **A browser** on your laptop for OAuth login (works for both paths)

---

## Path A: Two-shot login inside Cowork (recommended for first-time users)

This path requires nothing on your laptop. Everything happens inside Cowork.

### Step A1: Start a Cowork session and run setup

Open a Cowork session and run:

```
/mixpanel-headless:setup
```

This installs the `mp` CLI in the Cowork VM.

### Step A2: Run the login slash command

```
/mixpanel-headless:auth login
```

Claude detects the headless environment and runs the two-shot flow:

1. Claude runs `mp login --start` and shows you a URL to open.
2. You open the URL in your laptop's browser, complete Mixpanel login.
3. Your browser redirects to `localhost:19284/callback?...` and shows "site can't be reached" — **that's expected**.
4. You copy the URL from your browser's address bar and paste it back into chat.
5. Claude runs `mp login --finish '<URL>'`. The CLI exchanges the code for tokens, fetches `/me`, auto-picks a project, and writes the account to `~/.mp/accounts/` inside the VM.

The 10-minute inflight TTL is generous — switch tabs, complete login, come back, paste. If you take longer, just run `/mixpanel-headless:auth login` again to begin a fresh attempt.

### Step A3: Start asking questions

```
How many signups did we get last week?

What's our funnel conversion rate from signup to purchase?
```

### When this path is best

- You don't have `mp` installed on your laptop yet.
- You only need access from this one Cowork session (or a few — the VM persists tokens between sessions, but if Cowork rebuilds the VM you'll re-login).
- You're OK with the manual paste-back per fresh VM (one-time cost; auto-refresh handles token rotation thereafter).

### Tested browsers

The "site can't be reached" page must preserve `?code=...&state=...` in the address bar so you can copy it. Verified working on Chrome, Firefox, and Safari (default settings).

---

## Step 1: Install the CLI and Set Up Credentials
## Path B: Credential bridge from your laptop

If you already have `mp` configured on your laptop, this path is faster: one host-side export, then every Cowork session auto-discovers your credentials.

### Step B1: Install the CLI and set up credentials

On your **local machine** (not inside Cowork), install the `mp` command-line tool:

Expand Down Expand Up @@ -56,7 +113,7 @@ You should see a success message confirming the connection.

---

## Step 2: Export Credentials for Cowork
### Step B2: Export credentials for Cowork

On your **local machine**, export the active account into a v2 bridge file at the default Cowork-readable path:

Expand All @@ -66,52 +123,38 @@ mp account export-bridge --to ~/.claude/mixpanel/auth.json

This writes a v2 `auth.json` bridge file embedding your full `Account` record (and any `oauth_browser` tokens). The Cowork VM auto-discovers it on session start. Override the location with the `MP_AUTH_FILE` env var if you need a custom path.

You'll see a brief confirmation, then the bridge is ready:
The CLI prints:

```
Wrote bridge: ~/.claude/mixpanel/auth.json
Account: personal (oauth_browser, us)
Tokens: included (refresh-capable)
Wrote bridge file to ~/.claude/mixpanel/auth.json
```

### Options

```bash
# Export a specific named account (defaults to the active account)
mp account export-bridge --to ~/.claude/mixpanel/auth.json --account production
mp account export-bridge --to ~/.claude/mixpanel/auth.json --account YOUR_ACCOUNT_NAME

# Pin a project ID into the bridge (overrides the account's default_project)
mp account export-bridge --to ~/.claude/mixpanel/auth.json --project 12345
mp account export-bridge --to ~/.claude/mixpanel/auth.json --project YOUR_PROJECT_ID

# Pin a workspace ID into the bridge (needed for dashboard/entity management)
mp account export-bridge --to ~/.claude/mixpanel/auth.json --workspace 3448413
mp account export-bridge --to ~/.claude/mixpanel/auth.json --workspace YOUR_WORKSPACE_ID
```

---

## Step 3: Start a Cowork Session and Run Setup
### Step B3: Start a Cowork session and run setup

Open a Cowork session and run the setup skill:

```
/mixpanel-headless:setup
```

The setup script automatically detects the Cowork environment and reads credentials from the bridge file. You'll see output like:

```
Cowork environment detected.
✓ Auth bridge file found: ~/.claude/mixpanel/auth.json
Account: personal (oauth_browser, us)
Project: 12345
Tokens: included (refresh-capable)
```

No additional configuration is needed inside Cowork.

---
The setup script automatically detects the Cowork environment and reads credentials from the bridge file. No additional configuration is needed inside Cowork.

## Step 4: Start Asking Questions
### Step B4: Start asking questions

You're ready. Ask Claude questions in natural language, just like in regular Claude Code:

Expand Down Expand Up @@ -185,9 +228,9 @@ The credential bridge is a v2 JSON file that maps your local credentials into a
Your machine Cowork VM
┌─────────────────────────┐ ┌─────────────────────────┐
│ ~/.mp/config.toml │ │ ~/.claude/mixpanel/ │
~/.mp/accounts/<name>/ │──account export-bridge──▶│ auth.json │
(account + tokens) │ --to <path> │ (v2 bridge: full │
│ Account + tokens) │
(account records) │ │ auth.json │
~/.mp/accounts/<name>/ │──account export-bridge──▶│ (v2 bridge: full │
(tokens + me cache)--to <path> │ Account + tokens) │
└─────────────────────────┘ └─────────────────────────┘
resolve_session() reads
Expand Down Expand Up @@ -253,20 +296,38 @@ mp --version # verify

### Setup says "Cowork environment detected" but no credentials

**Cause**: You're inside Cowork but the bridge file is missing.
**Cause**: You're inside Cowork without a bridge file AND haven't done the two-shot login.

**Fix**: Two options:
- Use Path A (two-shot login from inside Cowork): run `/mixpanel-headless:auth login` inside Cowork.
- Use Path B (bridge from your laptop): run `mp account export-bridge --to ~/.claude/mixpanel/auth.json` on your local machine, then start a new Cowork session.

### Two-shot login: "site can't be reached" page

**Cause**: When the OAuth provider redirects your browser to `localhost:19284/callback`, no server is listening (the loopback is the Cowork VM, not your laptop). The browser shows "site can't be reached."

**Fix**: This is the expected behavior. Copy the URL from your browser's address bar (it contains `?code=...&state=...`) and paste it back into chat. Verified to work on Chrome, Firefox, and Safari with default settings.

### Two-shot login: inflight expired

**Cause**: You took longer than 10 minutes between `mp login --start` and pasting the redirect URL.

**Fix**: Re-run `/mixpanel-headless:auth login`. The CLI clobbers the prior inflight and starts fresh.

### Two-shot login: post-publish failure

**Cause**: `mp login --finish` succeeded at token exchange but failed at publish (e.g., `/me` timed out, name collision, network blip).

**Fix**: You cannot configure credentials from inside Cowork (no browser, no host terminal). Exit Cowork, run `mp account export-bridge --to ~/.claude/mixpanel/auth.json` on your local machine, then start a new Cowork session.
**Fix**: The CLI leaves a `.tmp-*` placeholder dir in `~/.mp/accounts/`. Re-run `mp login --resume <PATH>` to retry the publish without re-running PKCE. The error message includes the placeholder path.

### Important: What Doesn't Work Inside Cowork
### Bridge path: commands that need to run on your laptop

These commands require a browser or host terminal and **should be run on your local machine**, not inside Cowork:
These bridge-management commands require host-side resources and **must run on your local machine**:

- `mp login` and `mp account login <name>` (need a browser for the PKCE OAuth flow)
- `mp account add` for `service_account` (prompts for secret interactively by default; `--secret-stdin` and `MP_SECRET` env var work non-interactively, but the credential bridge is the recommended approach for Cowork)
- `mp account export-bridge --to <path>` (reads host credentials, writes the bridge file)
- `mp account remove-bridge [--at <path>]` (removes the bridge file from the host)

Always run these on your **local machine** before starting a Cowork session.
Note: `mp login` itself works **inside** Cowork via the two-shot flow (Path A). Only the bridge-export commands above are host-only.

---

Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ dependencies = [
"tomli-w>=1.0",
"pandas>=2.0",
"pyarrow>=17.0; python_version >= '3.11'",
"httpx>=0.27",
"httpx[socks]>=0.27",
"typer>=0.12",
"rich>=13.0",
"jq>=1.9.0",
Expand Down
12 changes: 12 additions & 0 deletions src/mixpanel_headless/_internal/auth/client_registration.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@
"in": "https://in.mixpanel.com/oauth/",
}

#: Canonical project ``domain`` host string keyed by region.
#: Mirrors ``OAUTH_BASE_URLS`` keys so the auto-pick filter can map a
#: region back to the host string ``MeProjectInfo.domain`` reports.
#: Co-located here as the single source of region truth — both the auth
#: endpoint config and the auto-pick region filter resolve through this
#: file.
DOMAIN_FOR_REGION: dict[str, str] = {
"us": "mixpanel.com",
"eu": "eu.mixpanel.com",
"in": "in.mixpanel.com",
}

#: Scopes sent in the DCR request body for server-side validation.
#: Advisory only — DCR does NOT store these on the application model.
#: The created app has an empty scope field, meaning all scopes are allowed.
Expand Down
Loading
Loading