README.md
Language Markdown8.4 KB172 lines
# Bitrix24 MCP ServerA local MCP server that lets Claude Desktop **read and write** a company'sBitrix24 CRM, tasks, telephony, and website chats over an inbound webhook.**The company is fictional.** This server was built for, and used every day at,a real print and display shop. Here it serves **Meridian Display Co.**, theinvented company behind[meridian-ops-board](https://github.com/mattrguerra/meridian-ops-board): thisis the CRM side of the same system. Funnel names, field labels and examples areMeridian's; the engineering is the real server's.> **This server performs writes.** Lead writes, funnel-locked deal writes> (including `create_deal`), and contact/company create/update are **enabled by> default**. Set `BITRIX_WRITES=off` for a fully read-only server.## Deployment modelLocal, per-machine, stdio: Claude Desktop launches `server.py` as a subprocesson each staff member's machine. There is **no network listener**, so thesecurity boundary is the **webhook secret** in `.env` and the scopes you grantit. See "Security & opsec" below before distributing.## What it does30 tools. Reads are unrestricted; writes are gated by `BITRIX_WRITES` (and dealwrites are additionally locked to specific funnels).### Reads| Tool | Purpose || ---- | ------- || `search_crm` | Find leads/contacts/companies/deals by name, email, or phone || `get_record` | Full detail on a record, incl. custom fields + an order summary || `get_timeline` | Activity history (calls, emails, comments) + website chats || `get_chat_transcript` | Website live-chat (Open Channel) conversations on a record || `list_tasks` | Your open/overdue/completed tasks || `pipeline_summary` | Exact per-stage counts (batched), with stage names || `list_records` | List records by assignee, stage, field, funnel, or date || `get_fields` / `find_field` | Field schema / search fields by label across entities || `get_products` | Line items on a record (product, qty, price, total) || `get_stage_history` | Stage movement with time-in-stage || `get_call_history` | Recent telephony calls (with CALL_IDs and recording flags) || `get_call_recording` | Recording URL/file + bound record for one call || `list_pipelines` | Deal funnels (with stages) + smart processes |### Writes (gated by `BITRIX_WRITES`)| Tool | Scope of write || ---- | -------------- || `add_lead_comment`, `rename_lead`, `set_lead_fields` | Leads || `add_deal_comment`, `set_deal_title`, `set_deal_stage`, `set_deal_fields`, `assign_deal`, `link_deal_contact`, `link_deal_company`, `create_deal` | Deals, **write-enabled funnels only** || `create_contact`, `update_contact`, `create_company`, `update_company` | Parties, gated also by `BITRIX_WRITE_PARTIES`; create dup-checks first || `transcribe_call` | Local transcription, opt-in via `BITRIX_TRANSCRIBE` |`set_lead_fields` / `set_deal_fields` take a `{label: value}` object andtranslate enum options, yes/no booleans, and dates automatically, e.g.`{"Follow-up Status": "Quote Sent", "Rush Job": "yes"}`.## Webhook permissions (required)Create one **Inbound webhook** (Applications → Developer resources → Other →Inbound webhook) and enable exactly these scopes:| Scope | Powers || ----- | ------ || `crm` | all CRM reads + writes || `user_brief` | assignee / author names (upgrade to `user` if names look thin) || `task` | `list_tasks` || `telephony` | `get_call_history`, `get_call_recording`, transcription || `imopenlines` + `im` | website-chat read (commercial plan required) |Without `imopenlines`/`im`, chat tools degrade gracefully (timeline still works).Without `telephony`, the call tools return a clear "missing scope" message.> **CoPilot note.** Bitrix CoPilot's generated transcripts/summaries are **not**> readable over REST, and a call's `TRANSCRIPT_ID` has no getter. "CoPilot-style"> value here comes from **Claude** summarizing the data these tools expose> (chat transcripts, recordings, timeline), not from reading CoPilot itself.## Security & opsec (read before distributing)- **The webhook URL is a password.** Anyone with it has the webhook user's fullAPI access from anywhere (Bitrix cloud webhooks can't be IP-restricted).- Choose a webhook-owner model and stick to it:- **Per-user** (what the Windows install uses): each person's webhook lives in*their own* Bitrix account, so actions are attributable to them. Never copyone person's `.env` to another machine.- **Dedicated service user**: one non-admin "CRM Automation" account scoped toonly what's needed: simplest least-privilege, but loses per-user audit.- Enable **2FA** on whichever account owns the webhook.- `chmod 600 .env` on every machine. Never commit `.env` (it's gitignored).- Have a **rotation plan**: if a machine is lost, delete + recreate the webhookin Bitrix and update `.env`.- **Token never logged.** httpx request logging is silenced and all error pathsredact the webhook URL.- **Logs are PII-scrubbed by default.** `logs/calls.jsonl` records tool callsfor migration analysis; `BITRIX_LOG_ARGS=redact` (default) masks emails/phonesand blanks customer names/free text. Use `full` only on a trusted solo machine;`off` to log no arguments. The log self-rotates past `BITRIX_LOG_MAX_BYTES`.- **Untrusted content is fenced.** Customer/visitor free text (record fields,email/chat bodies) is wrapped as untrusted data so the model treats it as data,never instructions. Keep per-write confirmation on in Claude Desktop.## Setup```bashcd meridian-crm-mcppython3 -m venv .venvsource .venv/bin/activate # Windows: .venv\Scripts\activatepip install -r requirements.txtcp .env.example .env# paste your webhook URL and adjust settingschmod 600 .env # macOS/Linux```Smoke test: `python server.py` should log a startup line and wait silently.Ctrl+C to stop. Then wire it into Claude Desktop's `claude_desktop_config.json`under `mcpServers` with absolute paths to the venv python and `server.py`, andrestart Claude Desktop.## Checks```bashpip install -r requirements.txt pytest ruffruff check .python -m py_compile server.pypytestBITRIX_WEBHOOK_URL=https://example.invalid/rest/1/x BITRIX_DATA_DIR=$(mktemp -d) \python server.py # starts on stdio; Ctrl+C to stopcd mcpb && ./build.sh # mcpb validate + pack```The tests need no webhook and make no network calls: `tests/conftest.py` setsthe environment before importing `server.py` and swaps `bitrix_call` for arecording fake, and `bitrix_call` itself runs against `httpx.MockTransport`.`tests/tool_api.json` records the 30 tools' names, docstrings and parameterschemas, so any change to what the model sees fails the suite until thecontract file is updated on purpose. CI runs lint, compile and tests onPython 3.10 and 3.12.## ConfigurationAll settings live in `.env` (see `.env.example` for the annotated list):`BITRIX_WEBHOOK_URL`, `BITRIX_LOCAL_TZ`, `BITRIX_WRITES`, `BITRIX_WRITE_FUNNELS`,`BITRIX_WRITE_PARTIES`, `BITRIX_LOG_ARGS`, `BITRIX_LOG_MAX_BYTES`,`BITRIX_TRANSCRIBE`.## Architecture notes- **Universal API.** Reads use the `crm.item.*` family (entityTypeId: lead 1,deal 2, contact 3, company 4). The one exception is **party create/update**,which uses legacy `crm.contact.*` / `crm.company.*` because `crm.item.add`silently drops phone/email multifields.- **Batched counts.** `pipeline_summary` runs all per-stage counts in one`rest.batch` round-trip (chunked at 50). One shared `httpx.AsyncClient` poolsconnections across calls.- **Semantic tool names** (`search_crm`, not `bitrix_crm_search`) so theinterface survives a future CRM migration; only the implementation swaps.- **Schema drift tracking.** Field schemas snapshot to `./schema/` (one datedfile per entity per day) so form-builder changes are diffable over time.The snapshots are gitignored: they hold the live CRM's own field definitions.## Troubleshooting- **`BITRIX_WEBHOOK_URL not set`**: `.env` missing/empty.- **`insufficient_scope`**: add the missing scope to the webhook (see table).- **Chat tools say insufficient_scope**: add `imopenlines` + `im` (commercialplan required).- **A deal write is "Refused… not a write-enabled funnel"**: the deal isn't in`BITRIX_WRITE_FUNNELS`; that's the funnel lock working as intended.- **Custom field shows as `UF_CRM_…`**: it's a custom field; `get_record` showsits label, and `get_fields` / `find_field` dump the schema.