MCP Server — the platform as a Model Context Protocol endpoint
Expose curated, permission-gated slices of shop, booking, ghrm and subscription — plus an LLM tool — to any MCP client over one authenticated endpoint, projected from a protocol-neutral capability registry with the agnostic core untouched.
What it is
The MCP Server plugin turns a VBWD instance into a Model Context Protocol server. An external MCP client — Claude Desktop, an autonomous agent, or one of VBWD's own bots — connects to a single authenticated endpoint and can search, read and act on the platform through typed tools and resources.
It is a plugin like any other: the agnostic core was not touched. Everything MCP exposes is reached through a seam the platform already publishes — the search-provider registry, the LLM connection manager, the event bus — or through a peer plugin's own service, never by reaching into another plugin's internals.
404 — indistinguishable from “not installed”. Nothing is exposed until an operator opts in, capability group by capability group.The endpoint
One route carries the whole protocol:
POST /api/v1/mcpIt speaks JSON-RPC 2.0 over the MCP Streamable-HTTP transport. The six methods a client uses are initialize, tools/list, tools/call, resources/list, resources/read and ping. A bearer token in the Authorization header authenticates every request; an absent or unknown token is a flat 404.
Architecture — one registry, one gate, thin protocol
MCP is built so the wire protocol is a detail at the very edge:
POST /api/v1/mcp
-> protocols/mcp.py (JSON-RPC 2.0 adapter — the only code that knows the wire format)
-> CapabilityRegistry (protocol-neutral: a Capability is a plain record, no MCP field)
-> dispatch.invoke() (THE ONE GATE: pack -> user-binding -> scope -> permission -> ownership)
-> adapters/*.py (protocol-blind providers; soft-import their peer plugin)
-> core / peer seams (search registry - llm_client - event bus - the peer's own service)- Protocol-neutral registry. A capability is a plain dataclass with no protocol-specific field, so a second protocol (UCP / A2A / ACP) is a later one-file adapter over the same registry — no capability changes.
- One authorization gate. Every pack / scope / permission / ownership check lives in
dispatch.invoke, so a future protocol adapter physically cannot bypass a check the MCP adapter enforces. - No peer coupling for reads. Catalogue reads ride the same search registry the bot framework uses, so MCP imports none of the vertical plugins and inherits the registry's GDPR block (user / invoice entities can never be registered, so they can never be read).
What it exposes — the capability catalogue
Each capability belongs to a pack an operator toggles, carries a scope (read or action) and a required permission. The v1 catalogue:
| Capability | Scope | Surfaces |
|---|---|---|
platform.search | read | free-text search across every registered vertical at once — shop products, booking resources, ghrm packages and subscription plans |
platform.get | read | re-resolve one hit by its entity_type + key (the detail card) |
platform.entity_types | read | discovery — which entity types the two tools above accept |
booking.create | action | create a reservation on a booking resource, bound to the caller |
booking.cancel | action | cancel one of the caller's own bookings (ownership-guarded) |
subscription.cancel | action | cancel one of the caller's own subscriptions (ownership-guarded) |
llm.ask | read | ask the instance's configured LLM (the same connection the bots use); an optional grounded mode prepends platform.search hits as context |
events.recent | read | a bounded, PII-redacted feed of recent platform events |
Reads that a CMS post would satisfy are on the roadmap: cms joins platform.search/platform.get the moment it registers a search provider, exactly like the verticals above — no MCP change required.
Authentication, scopes & ownership
Authorisation is layered, and every layer is enforced in one place (dispatch.invoke) so no protocol can slip past it:
- Token → principal. A configured bearer token maps to a role, a scope and, optionally, a bound
user_id. No token ⇒404. - Pack gate. A capability is invisible and un-callable unless its pack is enabled.
- Scope. A
readtoken can call onlyreadcapabilities;actioncapabilities need anactiontoken and a bounduser_id(you cannot act on behalf of nobody). - Permission. The mapped role must hold the capability's required permission (the platform's usual RBAC).
- Ownership. Destructive actions (
booking.cancel,subscription.cancel) load the target and refuse unless it belongs to the caller'suser_id— so a token can never cancel a stranger's booking by guessing an id. Admin-scoped principals bypass this last check.
On the wire the outcomes are kept distinct: an authorisation refusal is a JSON-RPC error (-32003); a missing entity or bad input is an MCP isError tool result; a disabled server or bad token is an HTTP 404.
Configuration
Everything is config — no endpoints, tokens or model ids are ever hard-coded. The plugin's config (and the admin Capabilities tab) carry:
enabled— master switch (defaultfalse).tokens— a list of bearer tokens, each a string or an object{token, role, scope, user_id?}. Empty ⇒ the endpoint404s.packs.*— one toggle per capability group (search,booking,subscription,llm,events, …), all off by default.default_scope,max_results,llm_connection_slug(which LLM connectionllm.askuses),events_buffer_size.
Calling it — a worked example
Handshake, discover, then call a tool:
curl -s https://<host>/api/v1/mcp \
-H 'Authorization: Bearer <token>' -H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}'
curl -s … -d '{"jsonrpc":"2.0","id":2,"method":"tools/list","params":{}}'
curl -s … -d '{"jsonrpc":"2.0","id":3,"method":"tools/call",
"params":{"name":"platform.search","arguments":{"query":"widget"}}}'initialize advertises the protocol version and the capabilities the token may see; tools/list reflects only the enabled packs; tools/call runs one through the gate. With no token, or the plugin disabled, the same requests return 404.
How it fits the rest of the platform
MCP is the seam that makes the whole catalogue agent-callable without teaching any one plugin about agents:
- Shop, Booking, GHRM, Subscription — each already contributes a search provider, so all four become searchable and readable through
platform.search/platform.getthe instant MCP is enabled, with zero code in MCP that imports them. - meinchat & Bots — MCP and the bots draw on the same two seams: the search registry for catalogue reach and the LLM connection manager for
llm.ask. An MCP client gets the same product knowledge a chat bot has. - CMS — this very documentation is a CMS page; and once CMS registers a search provider, its posts are reachable through MCP like every other vertical.
- Marketplace — because reads ride the shared registry, vendor-scoped listings surface through the same tools with no MCP-side special-casing.
The rule never changes: MCP projects capabilities the plugins already expose through core seams — it never owns domain logic of its own.
Extending it
Adding a capability is a small, protocol-blind change: register a Capability (name, kind, input schema, handler, scope, required permission, pack) in an adapter, and the MCP projection picks it up automatically. Adding a whole new protocol is one file — a ProtocolAdapter that projects the same registry into a new wire format and routes calls through the same dispatch.invoke gate. That is the entire point of the registry-plus-gate design: capabilities and authorisation are written once and reused by every protocol.
In short
Enable the plugin, add a token, switch on the packs you want, and a VBWD instance becomes a permission-gated MCP server: agents search and read the whole catalogue through the shared search seam, ask the configured LLM, and perform ownership-guarded actions on their own bookings and subscriptions — all behind one authenticated endpoint, with the agnostic core untouched. See the GHRM catalogue to get the plugin itself.