Skip to main content
Glama

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-inbox will 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.


Related MCP server: Knock MCP Server

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

list_api_endpoints({ domain?, q? })

Returns compact rows — endpointId, summary, risk, whether it writes. Search with q.

call_api({ endpointId, path?, query?, body?, headers?, fields?, where?, slice?, confirm?, describe? })

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

@vybeflow/mcp-inbox

tickets and conversations

7

@vybeflow/mcp-campaign

blast campaigns

7

@vybeflow/mcp-contact

customer records

6

@vybeflow/mcp-channel

WhatsApp, Telegram, Instagram

5

@vybeflow/mcp-product

catalogue

5

@vybeflow/mcp-billing

invoices, quota, usage

5

@vybeflow/mcp-tenant

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/campaigns

Machine-readable documentation

URL

What

/openapi.json

OpenAPI 3.1, every public endpoint

/openapi/<domain>.json

one domain at a time

/llms.txt

short index, llmstxt.org convention

/llms-full.txt

every endpoint with its permission and risk

/.well-known/mcp.json

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 end

A 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 documentation

Status

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.

Tool Schema Changelog

Recent tool additions, removals, and schema changes observed during successful MCP inspections. Dates show when Glama detected each change.

No tool schema history has been recorded yet.

Maintenance

ActivityMaintained
ResponsivenessNo issues

Resources

Unclaimed servers have limited discoverability.

Looking for Admin?

If you are the server author, to access and configure the admin panel.

Related MCP Connectors

Related MCP Servers

Latest Blog Posts

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