@finchatbot/opus-mcp
v0.4.0
Published
MCP server exposing tenant-scoped tools over the FCB OPUS API
Readme
@finchatbot/opus-mcp
MCP (Model Context Protocol) server that gives an MCP client — Claude Desktop, Claude Code, Managed Agents — tenant-scoped access to the FCB OPUS API.
With it, Claude can list and configure chatbots, stage and publish widget styling, promote a chatbot's config between environments, trigger n8n project deploys, assign n8n credentials, and provision clients — all pinned to a single tenant it cannot change.
Requires Node 18 or newer. Nothing is installed globally; npx fetches it on
demand.
Quick start
Add a server to your MCP client's config, one per OPUS environment:
"opus-dev": {
"command": "npx",
"args": ["-y", "@finchatbot/opus-mcp"],
"env": {
"OPUS_API_URL": "https://dashboard-api-dev.finchatbot.com",
"OPUS_API_KEY": "fcb_…",
"OPUS_TENANT_ID": "…"
}
}Restart the client afterwards — the tool list is read once at startup, so a new version's tools stay invisible until you do.
Two things that account for most setup failures:
- Claude Desktop does not expand
${VARS}. A"${OPUS_TENANT_ID}"reaches the server as that literal string and fails with400 Tenant not found. Use literal values. - Keys are per-tenant and per-environment. A dev key against the QA URL
is a
401, not a permissions problem.
Name the server after the environment it points at (opus-dev, opus-qa,
opus-uat, opus-prod). With a tenant key, switching tenants means a different
server entry with a different key — never a tool argument. A master key is the
one exception; see Master keys below.
Environment variables
| Variable | Required | Description |
| ---------------- | -------- | ------------------------------------------------------------ |
| OPUS_API_URL | yes | Base URL of the OPUS API for one environment |
| OPUS_API_KEY | yes | OPUS API key, fcb_ followed by 64 hex characters |
| OPUS_TENANT_ID | tenant keys only | Tenant UUID this server instance is scoped to |
OPUS_API_URL and OPUS_API_KEY are always required. OPUS_TENANT_ID is
required for a tenant (fcb_) key and must be omitted for a master (fcbm_)
key. The server exits at startup if a required one is missing, which the client
reports as Server disconnected.
Mint keys at Admin → Claude (MCP) in the OPUS dashboard. Keep real values in your client's own config or your shell — never in a committed file.
Tenant safety model
The tenant is fixed at startup from OPUS_TENANT_ID and sent as x-tenant-id
on every upstream request. No tool accepts a tenant argument — inputs are
parsed with a strict schema, so a smuggled tenantId is dropped before any
call is made.
That is the client-side half. The server side is what actually holds: the OPUS API resolves the key within the tenant named in the header, so a key that does not exist in that tenant fails authentication. Pointing this server at another tenant's UUID does not grant access to it.
Master keys
An FCB master super user can mint a master key (fcbm_…) at
Admin → Claude (MCP) with no tenant selected. It reaches every tenant and
carries every permission scope, including creating new clients.
Configure it by leaving OPUS_TENANT_ID out entirely. The server then runs in
cross-tenant mode, which changes two things:
- Every tenant-scoped tool takes an extra
tenantIdargument naming the tenant to act on. Omitting it on a tool that needs one is an error, not a default. - A
list_tenantstool appears, so Claude can discover the ids to pass.
The tenant-key guarantees above are unchanged: a session started with a fcb_
key stays pinned, has no tenantId argument on any tool, and drops one that is
smuggled in. Prefer a tenant key whenever one will do — a master key is a
production credential with no blast radius limit.
Because the package is public, treat the tool list and endpoint paths below as visible. Nothing here grants access on its own — every call needs a scoped API key.
Tools
Chatbots — read
| Tool | Does | Scope |
| --- | --- | --- |
| list_chatbots | Lists chatbots as summaries (id, name, domain, status, webhook, timestamps). includeArchived adds archived ones; verbose returns whole records. | chatbots:read |
| get_chatbot | One chatbot in full, including any pending style draft. | chatbots:read |
| get_chatbot_stats | Aggregate stats for the tenant. | chatbots:read |
| get_chatbot_webhook_recipe | How to wire an n8n workflow to this chatbot: the exact signed-callback URL, the reply payload shape, the signing rules, and an importable five-node workflow. | chatbots:read |
Chatbots — create and configure
| Tool | Does | Scope |
| --- | --- | --- |
| create_chatbot | Creates a chatbot. Only name and domain are required; the domain is private by default. | chatbots:write |
| update_chatbot_config | Content and behaviour — welcome message, initial prompt, language, feedback and sound toggles. | chatbots:write |
| update_chatbot_identity | Identity and lifecycle — name, webhook URL, active/archived state. Confirmation-gated; refuses non-HTTPS webhooks. | chatbots:write |
| import_chatbot_config | Promotes a chatbot's config from the environment upstream of this one. Confirmation-gated. | chatbots:write |
Styling — staged, so nothing reaches the live widget until you publish.
| Tool | Does | Scope |
| --- | --- | --- |
| update_chatbot_style | Stages theme, colours, fonts, bubbles, buttons, images, position and animation into a preview draft. Repeated calls merge. | chatbots:write |
| publish_chatbot_style | Applies the staged draft to the live config and clears it. Fails if no draft is pending. | chatbots:write |
| discard_chatbot_style | Drops the draft, leaving the live config untouched. | chatbots:write |
Media
| Tool | Does | Scope |
| --- | --- | --- |
| upload_media | Uploads a local file (≤10 MB) and returns a URL for avatars, logos and banners. | media:write |
| list_media | Lists uploaded media, with category, page and limit. | media:read |
Deployments and n8n
| Tool | Does | Scope |
| --- | --- | --- |
| list_deployments | Lists deployments, optionally filtered by status. | deployments:read |
| deploy_n8n_project | Deploys an n8n project to the next environment. Checks promotability first. Confirmation-gated. | deployments:trigger |
| assign_n8n_credential | Assigns an n8n credential to one of your tenant's projects. | integrations:write |
Clients (tenants)
| Tool | Does | Scope |
| --- | --- | --- |
| create_tenant | Provisions a client — a dedicated database, its credentials, and a migration run. country and serverLocation are required. Confirmation-gated, and there is no undo. | tenants:write |
| update_tenant | Corrects a client's details. Cannot activate or deactivate a tenant. Confirmation-gated. | tenants:write |
Confirmation-gated tools
create_tenant, update_tenant, deploy_n8n_project, import_chatbot_config
and update_chatbot_identity each require a confirm argument echoing the
exact target — a business name, a tenant id, a project id. Claude has to state
what it is about to act on before it can act, so an ambiguous instruction stops
rather than guesses.
Note what create_tenant really does: it provisions infrastructure, and OPUS
has no delete-tenant endpoint. A mistake there is not undoable from Claude.
A typical styling pass
upload_media → take the returned URL → update_chatbot_style (stages a
draft) → open the chat app with ?preview=1 to watch it render → then
publish_chatbot_style to go live, or discard_chatbot_style to revert.
Linking a chatbot to an n8n workflow
Call get_chatbot_webhook_recipe first. It returns the wiring filled in for
that specific chatbot, which is where hand-built workflows go wrong — the
callback URL embeds the chatbot's domain and id, and those differ per
environment.
The shape it returns, proven end to end:
Chat Webhook ──┬── Ack (respond immediately)
└── Build Payload ── Sign Payload ── Reply to OPUSThree rules make or break it:
- OPUS ignores what the webhook returns. The reply is a separate POST to
/webhooks/chatbots/{domain}/{chatbotId}/messages. - Serialize the reply exactly once, sign that string, and send that same string as a raw body. The signature is checked against the raw bytes received, so re-serializing fails even when the JSON is equivalent.
- The workflow must be active, or its webhook path 404s and the link looks correct while doing nothing.
The HMAC secret lives in an n8n credential (Crypto Signing Key Opus) and
never passes through this server.
Request behaviour
- Errors carry the upstream body. A failure reads as
OPUS API 403 on POST /tenants — {"message":"missing required scope: tenants:write"}, so the actionable part is visible. The body is capped at 500 characters and the API key is redacted from it. - Every request has a deadline — 30s, or 120s for
upload_media. A hung API surfaces asOPUS API timed out after 30000ms on GET /chatbotsinstead of an open-ended wait. - Results are capped at 25,000 characters and say so when truncated, rather than cutting off silently.
Audit trail
Every request this server makes carries two headers so the API can attribute it:
x-mcp-version— the client and its version, e.g.@finchatbot/[email protected]x-mcp-tool— the tool that caused the request, e.g.update_chatbot_identity
The tool header matters because several tools share a route:
update_chatbot_identity and update_chatbot_config are both
PATCH /chatbots/:id, and without attribution the trail cannot tell a reworded
greeting from a retargeted webhook.
Both are hints for tracing only. They are client-supplied, so the API records them and never uses them for access decisions.
The API stores one metadata row per API-key request — tool, route, status, duration and the acting key. Request and response bodies are never recorded, so no audit row can carry customer content.
Scopes
Beyond the chatbot and media scopes above, three unlock specific tools:
tenants:write for client provisioning, deployments:trigger for n8n deploys,
and integrations:write for credential assignment. A missing one surfaces as
403 … missing required scope: <name>, which is the quickest way to see what a
key lacks.
Grant only what the work needs. A key used for day-to-day chatbot configuration
has no reason to carry tenants:write.
Changelog
0.2.0 — Errors now include the upstream response body, redacted and capped,
so a missing scope names itself instead of surfacing as a bare 403. Every
request carries a timeout (30s, 120s for uploads), so a hung API reports as one
rather than waiting forever. list_chatbots returns summaries by default
(verbose: true for full records) and every result is capped at 25,000
characters with an explicit truncation notice. Requests now carry x-mcp-tool
and x-mcp-version so the API can attribute each one in its audit trail. The
reported server version tracks the package version.
0.1.0 — Initial release.
For the FinChatBot team
Everything below needs a checkout of the monorepo and is not relevant to using the published package.
Generating a client config. Prefer this to hand-writing JSON — it never prints a real credential, so its output is safe to paste into a ticket:
npm run mcp:config # opus-dev
npm run mcp:config -- opus-qa https://dashboard-api-qa.finchatbot.com
npm run mcp:config -- --local # this checkoutUse --local only when developing this server itself — it emits an absolute
path to your working tree, which will not exist on anyone else's machine.
Standing up a new client end to end — tenant → chatbot → n8n project → signed workflow → test — is covered by the Playbook in this package's directory in the monorepo, and by the Claude (MCP) Playbook page in the OPUS dashboard under Experimental → Docs. Both are internal.
Develop and test:
npm install
npm test # jest unit suite
npm start # run over stdio (requires env vars)Protocol-level debugging without a Claude client:
npx @modelcontextprotocol/inspector npx tsx src/index.tsThe repo-root .mcp.json registers this server as opus-local for Claude
Code; restart the session after code changes to pick them up. stdout carries
the MCP protocol, so any diagnostics must go to stderr.
Publishing. prepublishOnly builds and runs the tests, so a broken build
cannot ship.
npm version patch # or minor / major — versions are immutable
npm publish --otp=123456 # code from your authenticator- Bump first. A published version can never be reused, even after
npm unpublish. Publishing without a bump fails with403 cannot publish over previously published version. --otpis not optional. The account runs 2FA atauth-and-writes, and a web-login session does not prompt — it fails with a bare403 … Two-factor authentication … is required. Codes expire in ~30s, so a stale one looks identical to no code at all.- Ship from a clean tree.
prepublishOnlybuilds your working files, so uncommitted changes go out in the tarball. - The README on npm only updates when you publish. Editing it in the repo changes nothing on the package page until the next release.
For CI, use a granular access token scoped to @finchatbot/* with Bypass
2FA, stored as a CI secret. Don't weaken the account's 2FA setting to avoid
the prompt.
Keep the @finchatbot/ scope. An unscoped opus-mcp is unclaimed on the
public registry — anyone could publish to that name, and it would run with
OPUS_API_KEY in its environment.
