Skip to main content
Glama
abipsha

vivid-odoo

by abipsha
README.md
# Vivid Odoo MCP server

Gives Claude direct read/write access to the Vivid Windows Odoo CRM over the
Odoo external API, so the Vendo → CRM lead sync runs without a browser tab or
the Chrome extension.

Built because the current sync drives Odoo through a logged-in browser tab: it
stalls whenever the extension disconnects, it can't run unattended, and every
PDF attachment round-trips a ~500 KB base64 blob through the conversation.

## What it does differently

| | Browser route | This server |
|---|---|---|
| Needs Chrome extension connected | yes | no |
| Runs on a schedule unattended | no | yes |
| PDF attachment | base64 through the model context | fetched server-side, never touches the conversation |
| Lead lookup | page loads, 6–10 s renders | one RPC call |
| Credentials | none (rides your session) | Odoo API key, held server-side |

## Tools

**Guarded CRUD** — `odoo_search_read`, `odoo_read`, `odoo_write`, `odoo_create`,
`odoo_call`

**Workflow helpers** — these encode the conventions that are easy to get wrong,
so they don't have to be remembered at call time:

- `find_leads_by_vendo_id` — matches on the join key, includes archived records,
  and flags each lead `needs_processing` (active **and** in `Leads`). Archived
  leads come back with a warning, because a cancelled job is archived, not deleted.
- `find_duplicate_leads` — widens past the Vendo ID to name, email and street,
  then groups by street so separate jobs for one household are distinguishable
  from true duplicates.
- `merge_leads` — chunks around Odoo's five-record cap and folds the survivor
  forward. Verifies which record actually survived rather than assuming.
- `set_lead_stage` — resolves the stage by name and reports which automations
  fired.
- `attach_vendo_document` — fetches from Vendo storage and writes the binary,
  filename companion and source URL together.
- `lookup_sales_team_for_canvasser`, `lookup_user`, `get_lead_history`
- `odoo_ping` — health check.

## Guardrails

These are enforced in code, not left to caller discipline:

- **Model allowlist.** Readable: `crm.lead`, `crm.stage`, `crm.tag`, `crm.team`,
  `hr.employee`, `res.users`, `res.partner`, `mail.message`, `mail.activity`.
  **Writable: `crm.lead` only.** This server can never edit an employee record
  or a user account.
- **Method allowlist** on `odoo_call` — only the merge wizard. No `unlink`.
- **The `zip` guard.** A broken server action on `crm.lead` evaluates a
  `res.city.zip` field that doesn't exist, so any write touching `zip` dies with
  `ValueError: Invalid field res.city.zip`. The server refuses it with a message
  pointing at `x_studio_zip_code`. *This is a real defect in your Odoo
  automation and is worth fixing at the source rather than routing around
  forever.*
- **Document host allowlist** — `attach_vendo_document` only fetches from
  `storage.paradigmvendo.com`, verifies a `%PDF` header, and caps at 25 MB.
- **Binaries stripped from results** so a read never dumps a PDF into context.
- **Bearer auth** — when `MCP_BEARER_TOKEN` is set, unauthenticated requests get
  a 401. The server logs a loud warning if you start it without one.

## Setup

### 1. Generate an Odoo API key

In Odoo: avatar → **Preferences** → **Account Security** → **New API Key**.

Create it on a **dedicated integration user**, not a personal login — one whose
CRM permissions are scoped to what the sync needs. Keys inherit the user's
rights, so a key on an admin account is an admin key. The key is shown once.

`ODOO_DB` is usually the subdomain (`myvivid`); confirm at
`https://myvivid.odoo.com/web/database/selector` if unsure.

### 2. Configure

```bash
cp .env.example .env      # then fill it in
openssl rand -hex 32      # value for MCP_BEARER_TOKEN
```

Never commit `.env`. Use your host's secret store in production.

### 3. Deploy

Anywhere that gives you a public HTTPS URL and can reach `myvivid.odoo.com` —
Fly.io, Railway, Render, Cloud Run, or your own box behind a TLS proxy.

```bash
docker build -t vivid-odoo-mcp .
docker run -p 8080:8080 --env-file .env vivid-odoo-mcp
```

Or without Docker:

```bash
pip install -r requirements.txt
uvicorn server:app --host 0.0.0.0 --port 8080
```

Verify before connecting:

```bash
curl -s -X POST https://your-host/mcp \
  -H "Authorization: Bearer $MCP_BEARER_TOKEN" \
  -H 'Content-Type: application/json' \
  -H 'Accept: application/json, text/event-stream' \
  -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-06-18","capabilities":{},"clientInfo":{"name":"t","version":"1"}}}'
```

You should get a handshake naming `vivid-odoo`. Without the header you should
get a 401 — check that too.

### 4. Connect it to Claude

claude.ai → **Settings → Connectors → Add custom connector**, MCP endpoint
`https://your-host/mcp`.

### 5. Point the skill at it

Once connected, `vendo-crm-lead-sync` should call these tools instead of running
`odoo()` in a browser tab. Vendo and Terros still need the browser — only the
Odoo half becomes headless. Worth updating the skill's "When the browser will
not cooperate" section accordingly: an Odoo outage is no longer a reason to
defer a run, but a Vendo or Terros outage still is.

## A note on auth

Bearer-token auth is the minimum bar. If your host supports it, put the server
behind OAuth or restrict inbound traffic by IP — a bearer token in a connector
config is a long-lived credential granting write access to your CRM pipeline.

Rotate the Odoo API key from Odoo's Account Security page if it's ever exposed;
revoking there kills the server's access immediately.

## Testing status

Verified in the build environment: all 14 tools register, the 8 guardrail cases
(model allowlist, write allowlist, zip guard, method allowlist, binary
stripping, document host allowlist, empty-search rejection, normal write) behave
correctly, and the full HTTP path works — 401 without a token, 401 with a wrong
token, MCP handshake with the right one. End-to-end RPC (auth, uid caching, lead
lookup, stage change) was exercised against a stub Odoo.

**Not yet tested against the live instance.** The build sandbox's egress policy
blocks `myvivid.odoo.com`, so the first real run needs `odoo_ping` against
production to confirm credentials, database name and field access.