@besales/mcp
v0.47.1
Published
Model Context Protocol server for Animaly / Besales
Readme
@besales/mcp
Model Context Protocol server for Animaly / Besales.
Status: public npm package candidate.
Install
Requirements:
- Node.js 20+
- BeSales account with an active workspace
- Claude Desktop, Claude Code, Codex CLI, or another MCP-capable host
No global install is required. Production users can run the package with npx:
npx -y @besales/mcp connect
npx -y @besales/mcp status
npx -y @besales/mcpconnect opens the browser consent flow and stores an MCP API key plus the
selected workspace in the local keychain. With one connection or an explicit session pin,
workspace-level tools resolve it automatically. With several unpinned connections they refuse
the call as ambiguous; inspect besales_workspace_current and select one deliberately.
For production, do not set BESALES_API_BASE_URL or
BESALES_OAUTH_AUTHORIZE_URL. They default to:
BESALES_API_BASE_URL→https://core.besales.ai/api/v2(backend API host — used for MCP tool calls)BESALES_OAUTH_AUTHORIZE_URL→https://app.besales.ai/settings/mcp/consent(frontend bridge page — opened in the browser duringconnect; it mints a one-time setup ticket and forwards to the backend consent page. After approvalconnectreceives a one-timecodeon the loopback callback and exchanges it with the PKCEcode_verifierat/api/v2/auth/mcp-token— the API key never travels in a URL)
For staging or local development override them per command:
BESALES_API_BASE_URL=http://localhost:3000/api/v2 \
BESALES_OAUTH_AUTHORIZE_URL=http://localhost:5173/settings/mcp/consent \
npx -y @besales/mcp connectConfigure Your MCP Host
Step-by-step guide in Russian (Claude Code, Codex, several workspaces, updating): docs/connect.ru.md. Release order for maintainers: RELEASING.md.
After connect, add the server to your MCP host and restart the host.
Claude Desktop config:
{
"mcpServers": {
"besales": {
"command": "npx",
"args": ["-y", "@besales/mcp"]
}
}
}Claude Code:
claude mcp add --scope user besales -- npx -y @besales/mcpCodex CLI:
codex mcp add besales -- npx -y @besales/mcpEquivalent Codex config:
[mcp_servers.besales]
command = "npx"
args = ["-y", "@besales/mcp"]For staging/local backends, include both backend and frontend bridge URLs in the host env. For example:
[mcp_servers.besales.env]
BESALES_API_BASE_URL = "http://localhost:3000/api/v2"
BESALES_OAUTH_AUTHORIZE_URL = "http://localhost:5173/settings/mcp/consent"Multiple Workspaces
besales-mcp can hold keys for several workspaces at once — even across
different accounts. Connect each one once (switch the account/workspace in
the browser before each connect); keys are cached in your OS keychain and
switching afterwards needs no browser.
npx -y @besales/mcp connect # workspace A (browser logged into account A)
npx -y @besales/mcp connect # workspace B (re-login in the browser first)
npx -y @besales/mcp connections # list all connections, marks the active oneconnect stores the workspace name and a masked account label such as
o***@example.com; the raw email is never stored in the MCP index. These labels
are refreshed by a lightweight scoped endpoint (at most once per day) and never
require a full workspace overview. besales_workspace_current reports
current, cached, stale, or unknown plus a safe refresh reason. Reconnect
also refreshes a renamed workspace; legacy connections migrate on first read.
Bind a host session to a specific workspace with BESALES_WORKSPACE_ID in that
server's env block — the cleanest "one session = one workspace" setup (one
project/profile per workspace):
{
"mcpServers": {
"besales": {
"command": "npx",
"args": ["-y", "@besales/mcp"],
"env": { "BESALES_WORKSPACE_ID": "<workspace-uuid>" }
}
}
}[mcp_servers.besales.env]
BESALES_WORKSPACE_ID = "<workspace-uuid>"Workspace selection precedence, per tool call:
BESALES_WORKSPACE_ID(env) — authoritative; an explicit, mismatchingworkspace_idargument is refused.- an explicit
workspace_idargument (only when the env var is not set). - the session binding set by the
besales_workspace_usetool (in-memory, this session/process only — see below). - the only connected workspace.
- otherwise the call is refused — several workspaces are connected and none was chosen.
The global active workspace is not an implicit fallback. It is set on the
very first connect and never revisited, so using it silently would send calls
to whichever workspace you happened to connect first — returning another
workspace's data with no error at all. When several workspaces are connected and
nothing above selects one, the call fails closed; the error names the current
active so you can pass it explicitly if that is what you meant. With exactly
one connection nothing changes. besales_workspace_current reports
source: "ambiguous" and workspace_id: null in the same situation, so the
diagnostic never claims a binding the real call would not use.
BESALES_WORKSPACE_ID always overrides everything. besales_workspace_current
lists names, safe account labels, UUIDs, the selection source, and label freshness.
The assistant shows those names to the user and passes the chosen UUID itself.
GUI hosts without per-session env (e.g. Claude Desktop)
When the host has no place to set BESALES_WORKSPACE_ID per session (you just
click "new session"), there are two ways to keep several workspaces apart.
Recommended — one pinned named server per workspace (survives reconnects). Run once per workspace:
yarn install:claude-desktop --name besales-acme --workspace <uuid-A>
yarn install:claude-desktop --name besales-globex --workspace <uuid-B>This adds env-pinned mcpServers entries (with a timestamped config backup);
restart Claude Desktop. Their tools appear namespaced —
mcp__besales-acme__besales_* (always workspace A) and
mcp__besales-globex__besales_* (always B). Because the pin lives in the host
config, it survives MCP reconnects and never silently drifts to the wrong
workspace. Both toolsets are visible in every session; you just address the one
you mean.
Quick / in-session — besales_workspace_use (does NOT survive a reconnect).
Ask the assistant to call besales_workspace_use <workspaceId> to bind the
current session in-memory. Each host session runs its own besales-mcp process,
so it is per-session and touches neither the global active nor other sessions.
Caveat: the binding is in-memory only — when the host respawns the server
(reconnect, which GUI hosts do often), it is lost and falls back to the global
active. Use it for a stable stretch, not as a durable lock; for that use the
pinned named servers above. besales_workspace_current shows the current binding
- source and lists connected workspaces; it is refused under a
BESALES_WORKSPACE_IDpin (env wins).
BESALES_WORKSPACE_IDis read once at process start — exporting it after a session is already open has no effect. Restart the session to re-pin via env.
First Smoke
Start a fresh host session after changing MCP config, then ask a natural business request:
Use besales MCP to create an ICP for SaaS B2B sales automation.The assistant should discover and call the relevant besales_* tools itself. The user can ask in
ordinary business language. If several workspaces are connected without a pin, the assistant must
show the choices from besales_workspace_current; the user does not need to invent an ID.
Expected quick checks:
npx -y @besales/mcp statusshows connected credentials.- The host shows the
besalesMCP server as connected. - Direct tools such as
besales_icp_creatework. - External flows use
*_get_instructions, execute stages locally in the host, then call the matching*_submittool.
If the host does not expose MCP resources to the model, ask the same request in ordinary words.
The model can call besales_help_search and then besales_help_get; both tools read the same
packaged documents as besales://... resources. besales_help_get returns a compact heading index
by default, one section with section, or the complete document with full=true. See
docs/help-navigation.md.
Hosts with MCP prompts may also show five optional task entries: configure an agent, improve an agent, audit dialogues, build a campaign report, and diagnose a failure. They expand to the same ordinary instructions and tool navigation; a host without prompts loses no operation.
To switch workspace, connect each one once (see Multiple Workspaces)
and either set BESALES_WORKSPACE_ID per host session or run
npx -y @besales/mcp use <workspaceId>. No browser re-auth is needed once a
workspace is connected.
If a call asks for workspace_id, first run besales_workspace_current: the cause may be a real
multi-workspace ambiguity, an env pin conflict, a missing key, or an old schema. Restart the host
with @besales/mcp@latest only when the schema/version is actually stale; reconnect only when the
credential or granted scopes are the problem.
besales_workspace_current reports effective_scopes from the exact selected credential and
marks their source. It never unions permissions from other MCP keys in the same workspace.
Development
yarn install
yarn types:gen:api
yarn types:gen:mcp
yarn buildThe generated files in src/types/ and src/schemas/ are committed because
the source contracts live outside this independent repository.
Run a lightweight local mock API for MCP Inspector tool-call smoke tests:
Terminal 1:
yarn dev:mockTerminal 2:
yarn dev:mock:seed
BESALES_API_BASE_URL=http://127.0.0.1:3100/api/v2 node bin/besales-mcp.jsyarn dev:mock:seed and yarn dev:mock:clear use the same keytar entries as
real besales-mcp connect credentials. They can overwrite or clear a local real
connection for the current macOS user.
The mock server only verifies request wiring. Live tool E2E depends on the
matching ai-aniomaly /api/v2/* endpoints.
Remove seeded mock credentials after smoke testing:
yarn dev:mock:clearCLI
node bin/besales-mcp.js connect # connect (or refresh) a workspace
node bin/besales-mcp.js connections # list connected workspaces (* = active)
node bin/besales-mcp.js use <workspaceId> # set the active (default) workspace
node bin/besales-mcp.js status # show the active workspace
node bin/besales-mcp.js disconnect # disconnect the active workspace only
node bin/besales-mcp.js disconnect <id> # disconnect one workspace
node bin/besales-mcp.js disconnect --all # disconnect every workspace
node bin/besales-mcp.js # start the MCP serverBESALES_API_BASE_URL defaults to https://core.besales.ai/api/v2 (backend).
BESALES_OAUTH_AUTHORIZE_URL defaults to
https://app.besales.ai/settings/mcp/consent (frontend bridge). For local
development point them at your local backend and frontend respectively.
BESALES_MCP_RESULT_ENVELOPE=on turns on the response size ceiling: oversized
answers are shaped or refused instead of flooding the caller's context. It is
off by default for one release because the package is normally run unpinned,
so a change in response semantics would land mid-session. See
docs/response-size-budget.md for the measured
numbers and the per-tool classes.
BESALES_MCP_TOOLSETS limits the session to named toolsets, comma separated —
readonly (every tool that only reads), or a domain group derived from the tool
name (knowledge, crm, agent, prompt, …). Unset or empty means the full
surface: a setting nobody filled in must never narrow access silently. The filter
applies to both tools/list and tools/call, and an unknown name fails loudly
with the list of valid ones. See docs/tool-surface.md
for why the surface gets profiles rather than fewer, larger tools.
Tool calls require stored credentials. Run node bin/besales-mcp.js connect
against a backend that implements the MCP OAuth endpoints
(/api/v2/auth/mcp-connect* + /api/v2/auth/mcp-token), or run
yarn dev:mock:seed for mock-only smoke tests.
For production users, do not set BESALES_API_BASE_URL or
BESALES_OAUTH_AUTHORIZE_URL. Run:
npx -y @besales/mcp connectThe browser consent flow stores the connected workspace key in the local
keychain. Workspace-level tools default to the session's workspace, so users
should not provide workspace_id manually. To work with several workspaces,
connect each once and pin sessions via BESALES_WORKSPACE_ID (or switch the
global default with besales-mcp use <id>) — see
Multiple Workspaces.
Available tools (253 tools by category)
Итог и категории строятся из активных имён src/schemas/mcp-tools.json. Тест сверяет этот блок,
summary.tools_by_category и сумму категорий с tools.length.
| Category | Tools | Name prefixes |
|---|---:|---|
| Workspace, help, tasks and audit | 18 | workspace_*, help_*, task_*, audit_* |
| Agents, behavior and routing | 14 | agent_*, behavior_*, facts_*, pipeline_*, routing_*, router_* |
| Prompts, sandbox and evaluation | 33 | prompt_*, qa_*, sandbox_*, simulation_*, test_*, model_* |
| ICP | 9 | icp_* |
| Knowledge, workbooks, files and variables | 50 | knowledge_*, workbook_*, feedback_*, variable_*, file_* |
| CRM and data sources | 27 | crm_*, datasource_* |
| Channels and external API credentials | 14 | platform_*, external_* |
| Dialogues and contacts | 16 | dialogue_*, dialogues_*, dialog_*, contact_* |
| Triggers | 5 | trigger_* |
| Outbound, follow-up and analytics | 67 | broadcast_*, getcourse_*, wazzup_*, followup_*, scheduled_*, tracked_*, link_*, inbound_*, funnel_*, ai_*, custom_* |
Recent capabilities:
- Intent navigation —
besales_workspace_navigatecombines the requested task, bot type, selected objects and live workspace overview into one next tool call, required user input and a result check. It does not perform the recommended mutation. - Complete async lifecycle — every background start has a read-only status tool; see
docs/async-operation-lifecycle.md. Contact imports and a
single cold-welcome launch use
besales_getcourse_contact_import_status; anchor refresh usesbesales_getcourse_anchor_refresh_statusand reports a lost worker asSTALLED. - Funnel test outside the web UI —
besales_followup_funnel_test_planfreezes expanded texts and dynamic generations under onerunId;startsends exactly that snapshot to the live contact and requires bothexplicit_confirmation=trueandconfirmation_reason. Status/cancel wrap the durable 15-second-gap run. All four tools require the dedicatedmcp:funnel-testscope, which is not covered bymcp:*. - Prompt lint diagnostics — a rejected
besales_prompt_patch_commitnow carries the backend'slintIssues(up to 3, e.g. the exact character count over the 80000 hard limit) instead of a bare 400.besales_prompt_headroomanswers the same question before a draft is built;besales_prompt_diffcompares two agents' prompts without pulling both full texts into context. - Fleet prompt rollout —
besales_prompt_batch_startapplies one set of patch operations across many agents as a background run (33 agents × 3 calls do not fit the 60s client timeout). Every agent is prepared and fully linted first; only those that pass are committed. Pollbesales_prompt_batch_status; roll forward the failures withbesales_prompt_batch_resume— never by starting a second run. - OAuth PKCE provisioning —
besales_platform_create_oauth_initopens the authorization flow in the browser for Instagram/Avito so the OAuth code never passes through the LLM. - CRM connection via browser-secrets —
besales_crm_create_initprovisions AmoCRM/Bitrix24 (and GetCourse/Telegram) using the Setup URL pattern, where the user pastes credentials in the browser instead of through the chat. - External API credential handoff — channel create/key rotation/secret rotation
return
revealUrland its expiry. The signed-in user opens that page to receive credentials once; keys and webhook secrets never enter MCP content or host logs. - Setup URL pattern —
*_initreturns a one-time URL; the user completes sensitive input in the browser and the host polls*_statusuntil ready. - External execution pairs — schemas returned by
*_get_instructionsuse canonical camelCase fields. Matching*_submittools accept those fields directly and retain legacy snake_case aliases, then send one canonical camelCase body to the API. - File upload — two-step flow (
besales_file_upload_request→besales_file_upload_status) so binaries are uploaded directly, not via MCP. - Platform provisioning & clone — create channels and clone an existing
platform configuration (
besales_platform_clone_preview/_execute). - Knowledge ingestion — full namespace/document/Q&A/website/table CRUD plus
workbook ingestion from xlsx and Google Sheets.
besales_workbook_inspectinventories sheets and returns a bounded sample;besales_workbook_sheet_readpages through late rows and columns with source identity and a page fingerprint. Metadata/classification and archival are available through dedicated workbook tools. Namespace/table reindex returns a job ID; poll the matching_reindex_statustool to a terminal state instead of reading worker logs. - GetCourse wave-run batches — queue 1–10 status-mode segments sequentially
through
besales_getcourse_wave_run_batch_start; use one_batch_statuspoll, reuse a generatedbatch_keyon retry, and resume only after resolving two consecutive rejected segments.UNKNOWNи небезопасныйFAILEDжёстко блокируются между волнами; repeat mode не является обходом неопределённого исхода. Контакты import/run/batch принимаютemail,phone,first_name,last_nameи передают их backend-у без потери snake_case→camelCase. - Broadcast mailing variants —
besales_broadcast_mailing_variant_list/create/update/delete; архивирование через delete илиupdate(archived=true)требуетexplicit_confirmation=trueуправляют workspace-каталогом «Вариантов рассылки». Передавай выбранный UUID какmailing_variant_idв direct, durable или batch GetCourse wave; analytics принимает тот же фильтр. Удаление использованного варианта архивирует его и сохраняет историю когорт. - GetCourse school bindings —
besales_platform_settings_updateпринимаетgetcourse_settings.wave_field_bindings: ролиlaunch_type/product/segment/funnel/channelsсвязываются с numeric custom-field ID сделки;product_value_sourceвыбирает код или название продукта, аnullочищает настройку школы.
Resources
The package exposes 38 MCP resources. The dialogue-audit workflow is:
besales://workflows/audit-dialogues
The seven base concept resources are:
besales://concepts/icpbesales://concepts/triggersbesales://concepts/sandboxbesales://concepts/handoffbesales://concepts/external-executionbesales://concepts/feedback-sheetsbesales://concepts/workbook-classification
Inspect them locally:
yarn dlx @modelcontextprotocol/inspector node bin/besales-mcp.jsThe Inspector should show 253 tools, 38 resources, and 5 optional prompts.
Codex Dialogue Audit Skill
Install the packaged read-only audit skill on Anton's machine:
npx -y @besales/mcp install-skill besales-dialogue-auditRestart Codex after installation. To update an existing copy:
npx -y @besales/mcp install-skill besales-dialogue-audit --forceThe skill uses only besales_dialogues_query and besales_dialogue_transcript,
follows besales://workflows/audit-dialogues, and never mutates agents or CRM data.
Claude Desktop
For local development before the package is published, install a Claude Desktop config entry with:
yarn install:claude-desktopThe installer creates a timestamped backup of the existing config and writes only
the mcpServers.besales entry. It uses the absolute Node executable path because
GUI apps may not inherit the shell PATH.
Host Setup
Host-specific local setup and smoke steps for MCP Inspector, Claude Desktop, Claude Code, and Codex CLI are documented in docs/host-setup.md.
Scope
- Claude Desktop / Claude Code / Codex call this package through MCP.
- This package calls Animaly only through
ai-aniomaly/api/v2/*. - Direct calls to
prompt-servicesare intentionally out of scope.
The package exposes 253 active MCP tools from src/schemas/mcp-tools.json.
Token scopes
besales-mcp connect requests one canonical set, declared as MCP_OAUTH_SCOPES
in src/auth/oauth-client.ts:
| Scope | Covers |
|---|---|
| mcp:* | composite base — reads, PromptHub, ICP, sandbox, knowledge, dialogue export, analytics aggregates |
| mcp:platform-setup | setup mutations: knowledge ingestion, variables, CRM pipelines, platform settings/provisioning, clone, the Platform Setup Bridge, waves |
| mcp:funnel-test | frozen follow-up funnel test against a live person |
| read:campaigns | cold-welcome preview, broadcast reads and their message texts |
| launch:campaigns | cold-welcome and broadcast launches |
| read:contacts | contact search, cards, and safe diagnostics |
| write:operators | CAS-protected workspace-operator assignment; no message send or CRM mutation |
None of these is implied by another: a plain mcp:* key authorizes neither setup
mutations nor campaigns, and mcp:platform-setup does not imply the right to send
to real people. Existing connections are never widened silently — after upgrading,
reconnect once to be granted a newly added scope.
⚠️ Invariant — the scope set only ever GROWS. A new scope is appended to
MCP_OAUTH_SCOPES; existing entries are never removed, renamed or reordered. Replacing an entry silently narrows every key issued from then on, andconnectgives no sign of it: consent opens, the key is issued, tools load, and the loss only surfaces much later as a 403 from one endpoint. That is exactly howread:campaigns+launch:campaignswere lost whenmcp:funnel-testwas added (2026-08-19) — the set used to be one flat string, and the string was rewritten. The same rule applies to the backend'sMCP_OPTIONAL_CONNECT_SCOPES, the consent-page allowlistOPTIONAL_OAUTH_SCOPES, and thescopeenum in the OpenAPI contract: add a line, never swap one.
connect prints the granted set, and warns on stderr when the new key carries fewer
permissions than the one it replaces:
Connected to workspace Animaly [de86534d-…] (key: mcp_a1b2…, account: o***@besales.ai).
Scopes: mcp:* mcp:platform-setup mcp:funnel-test read:campaigns launch:campaigns read:contacts write:operators
Active workspace: de86534d-….
WARNING: this key has FEWER permissions than the one it replaced.
Lost: read:campaigns, launch:campaigns
Tools needing them will fail with 403 "API key does not have required scopes".
Update the client (npx -y @besales/mcp@latest) and run `besales-mcp connect` again.The comparison is made by the backend (dropped_scopes in the /auth/mcp-token
response) against the newest live MCP key of that workspace — the client cannot read
an old key's scopes on its own.
