vybeflow-mcp
vybeflow MCP
Model Context Protocol servers for vybeflow — a multi-channel messaging platform (WhatsApp, Telegram, Instagram, email, web chat) with an inbox, campaigns, contacts, a product catalogue and billing.
Point Claude, Cursor, or any MCP-capable assistant at your vybeflow tenant and let it read your inbox, look up a contact, check a campaign's progress, or send a message — inside permissions you choose, with a key that expires.
Not on npm yet. The packages run, but they are unpublished, so
npx -y @vybeflow/mcp-inboxwill not resolve. Clone this repo,npm install, and point your client at the bin directly. Every@vybeflow/…name below is the published name it will have.
// claude_desktop_config.json
{
"mcpServers": {
"vybeflow-inbox": {
"command": "node",
"args": ["/path/to/vybeflow-mcp/packages/inbox/bin/cli.js"],
"env": { "VYBEFLOW_API_KEY": "vf_live_..." }
}
}
}Getting a key
In the vybeflow console: Pengaturan → API Key → Buat API Key.
You choose two things, and both matter:
What it may do. Scopes come from the same permission catalogue that governs staff accounts, so a key can never do something a role could not. You cannot grant a scope you do not hold yourself, and
iam.*— the permissions that mint roles and other keys — can never be given to a key at all.When it stops. An expiry is required. 90 days by default, 365 maximum. A bearer credential with no end date outlives the integration it was made for.
The key is shown once. The server stores only a salted SHA-256 digest of it; nobody, including us, can recover it afterwards. Revoking is instant in the console and takes effect across all services within about a minute.
The design: two tools, not two hundred
This is the part worth reading before you write your own MCP server.
vybeflow registers 249 routes. The obvious design — one MCP tool per endpoint — costs roughly 225 tokens per serialised tool definition, so arming all of them would burn about 56,000 tokens of context on every single turn, including the turn where you say "hi". It is resident cost: it is paid before the system prompt, before your message, and again on the next message, forever.
So these servers expose two tools, always:
Tool | What it does |
| Returns compact rows — |
| The one door. |
Discovery is a tool result, not a tool definition. A result can be dropped from the conversation once it has been used; a tool definition sits in every request forever. Two tools cost the same whether the registry holds thirty endpoints or five hundred.
call_api with describe: true returns the endpoint's specification — schema, headers, the
permission it needs — without executing anything. That used to be a third tool; folding it in
saved a definition on every turn for something a model needs once per endpoint and never
again.
The flow: domain → endpoint → call
1. domain which server you installed, e.g. @vybeflow/mcp-campaign
2. discover list_api_endpoints() -> 7 rows: endpointId, summary, risk
3. inspect call_api({ endpointId, describe: true })
-> schema, path params, required scope
4. call call_api({ endpointId, ... })
risk: safe -> executes
risk: confirm -> awaiting_confirmation + actionId
risk: destructive -> awaiting_confirmation + a warning, then
call_api({ ..., confirm: actionId })The domain is chosen before anything else, and it is a real boundary rather than a
convention: a campaign server asked for inbox.ticket.list answers wrong_server and names
the package to install instead. Step 3 is optional for an endpoint that needs no arguments,
and unavoidable for one that does — a bare endpointId on a parameterised endpoint returns
the specification rather than a broken request.
Run it yourself: node scripts/demo-flow.mjs.
One server per domain
Seven packages, each carrying only its own slice of the registry:
Package | Domain | Endpoints |
| tickets and conversations | 7 |
| blast campaigns | 7 |
| customer records | 6 |
| WhatsApp, Telegram, Instagram | 5 |
| catalogue | 5 |
| invoices, quota, usage | 5 |
| company profile | 2 |
Install only what you need. An assistant that manages campaigns has no business listing your invoices, and the cheapest way to enforce that is not to load the tool at all.
The guardrails, and why each exists
Most of this file is about refusing well. A refusal that carries a plan is worth more than an error.
Risk is declared, never inferred from the HTTP verb. The verb lies. POST /tickets/:id/close
is a tidy-up; POST /notification/send/whatsapp messages a real customer and spends quota.
Both are POSTs. Anything not marked safe returns awaiting_confirmation with an actionId
and executes only on a second call carrying confirm: actionId. That is not a failure —
it is the confirmation step, and the tool description says so, because a model that reads it
as an error will retry instead of confirming.
Oversized answers are refused, not truncated. A model handed 600KB of rows will chunk it,
read one chunk, and confidently invent the rest — we have watched it happen with a product
list, complete with invented prices. So call_api refuses, and the refusal names the row
count, the filters that exist, and a histogram of the values actually present, so the next
call can narrow instead of guess.
where has ten typed operators and no regex. Two reasons. A model-supplied regular
expression is uncancellable once V8 starts matching, so one catastrophically backtracking
pattern hangs the process. And a regex invites matching the wrong thing: match: "terlambat"
against a field whose value is the English "late" returns zero rows, and zero rows reads as
"there are none" — a confident, specific, wrong answer.
Fields can only narrow. Every endpoint declares a project allow-list of the fields it
returns, and fields can subset that but never widen it. On some endpoints the allow-list is
a secret guard: the company-profile record also holds a Midtrans server key, and the allow-list
is what keeps it out of a conversation.
Loop breakers. Two identical calls is a retry; three is a loop. The third gets
status: "repeating" — but the refusal still carries the endpointIds, so it is not a dead end.
A guessed domain is answered, not rejected. Ask for sales and you get a search across
every domain rather than a rejection, because a model that guessed a domain name still wants
something specific.
403 is terminal. These servers hold exactly one credential and have no second identity to retry as, so a 403 means the key lacks that scope. The refusal names the missing permission and says to widen the key — explicitly not to look for another endpoint with the same effect.
Security posture
What this server does and does not defend against, stated plainly so you can decide whether it is safe to point at your tenant.
The credential is never argument-controlled. Headers are an allow-list drawn from the
registry, and the API key is applied last and unconditionally. An earlier version spread
caller-supplied headers over the defaults, which let a tool argument replace the server's own
key and inject x-active-tenant — the request went out carrying the wrong credential. Undeclared
query parameters are refused the same way: the registry is the contract.
Confirmation handles are unguessable and expire. actionId is 16 bytes of CSPRNG, valid
for five minutes, and the pending set is bounded. Possession of a handle is never treated as
authentication. This matters most for the planned hosted transport, where one process would
serve many users.
Returned data is labelled as data. Rows include customer-written text — an inbox ticket is
literally a message from a member of the public — and it reaches the model through the same
channel as your instructions. Every data-bearing result carries a provenance note saying so.
This is mitigation, not immunity. Prompt injection through third-party content is an
unsolved problem; keep tool approval on, and do not let a model act unattended on what a row
says.
The key only goes where you point it. VYBEFLOW_BASE_URL must be HTTPS (loopback excepted),
and a non-default host is refused unless you set VYBEFLOW_ALLOW_CUSTOM_HOST=true. An MCP
config is a snippet people paste from the internet; one altered line should not silently
forward a live key somewhere else.
Scope is the real boundary. The server enforces nothing itself — vybeflow does. A key carries a permission list, checked by the same middleware that checks a logged-in employee, so the worst a compromised MCP server can do is what its key already allowed. Mint narrow keys with short expiries.
Requests are rate limited, per key and per tenant. A token bucket: bursts are allowed up
to the bucket size, then the rate settles. Writes cost three tokens to a read's one. Every
response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset; a refusal is
429 with Retry-After, and call_api surfaces that as rate_limited with the server's own
number rather than a generic error — so the model waits and repeats the same call instead of
casting around for another endpoint.
The tenant ceiling is the part that matters. A per-key limit alone is bypassed by minting a second key; since a tenant may hold twenty live keys, a key-only limit is a 20× limit wearing a disguise.
Not covered. Limits are per service process, so a key doing 120/min gets 120/min at each
service, not across the platform — the goal is that no one service can be hammered, and a
shared counter would put a network round-trip on every request in the estate. No audit of
which MCP client used a key; lastUsedAt is accurate to about a minute. Revoking takes
effect within roughly a minute, not instantly. stdio only, so the server runs with your user's
privileges like any other local MCP server.
Authentication
Everything goes over HTTPS to https://vybeflow.vyber.id with your key in x-api-key
(Authorization: Bearer vf_live_… also works).
A key resolves, server-side, to a tenant and a permission list, and is then checked by the same middleware that checks a logged-in employee. The enforcement layer never learns it is talking to a machine — which is exactly why a key cannot exceed its scopes.
curl -H "x-api-key: vf_live_..." \
https://vybeflow.vyber.id/api/campaign/campaignsMachine-readable documentation
URL | What |
OpenAPI 3.1, every public endpoint | |
| one domain at a time |
short index, llmstxt.org convention | |
every endpoint with its permission and risk | |
server discovery |
Every operation carries x-vybeflow-scope (the permission your key needs) and
x-vybeflow-risk. All of it is generated from packages/core/src/registry/ — the same file
the servers run on, so the documentation cannot drift from the behaviour.
Repository layout
packages/core/src/registry/ the API described as data, one file per domain
packages/core/src/RegistryExecutor.js the runtime and every guardrail above
packages/core/src/ServiceBases.js service id -> public URL
packages/<domain>/bin/cli.js ~10 lines: pick a domain, speak MCP over stdio
scripts/check-registry-contract.mjs proves the registry matches the real routes
scripts/export-docs.mjs renders OpenAPI, llms.txt and the docs page
scripts/test-executor.mjs guardrails, incl. the hardening above
scripts/test-mcp-handshake.mjs drives a real client over stdio
scripts/demo-flow.mjs the four-step flow, end to endA registry entry is data about somebody else's code, in another repository, with no import
between them — nothing stops a route being renamed and this file staying confidently wrong. So
check:registry walks the real route tables and compares method, path and the literal
permission code, and fails when they disagree. It is what makes the published documentation a
claim rather than a hope.
npm test # executor guardrails + registry contract
npm run check:registry
npm run export:docs # regenerate the published documentationStatus
Early. stdio transport today; a hosted endpoint at vybeflow.vyber.id/mcp/<domain> is
planned, so no local install is needed. Not yet published to npm — run from source. Instagram
has no package — the service is not directly reachable and most of its routes are Meta
webhooks or media bytes.
Licence
MIT.
Latest Blog Posts
- Who's Calling? MCP Hosts Are an Identity Blind Spot (And the Spec Knows It)By Om-Shree-0709 on .mcpAgent IdentityOAuth 2.1
- Your AI Chatbot Just Exposed Your CEO's Salary to an InternBy Om-Shree-0709 on .Agent IdentityMCP SecurityOAuth Delegation
- Why MCP Servers Need Execution Sandboxing (And Why Your Current Stack Isn't Enough)By Om-Shree-0709 on .Agentic AiPrompt InjectionWebAssembly
MCP directory API
We provide all the information about MCP servers via our MCP API.
curl -X GET 'https://glama.ai/api/mcp/v1/servers/never00miss/vybeflow-mcp'
If you have feedback or need assistance with the MCP directory API, please join our Discord server