Core Plugin · MCP Server

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.

Off by default. With no token configured (or the plugin disabled) the endpoint returns 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/mcp

It 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:

CapabilityScopeSurfaces
platform.searchreadfree-text search across every registered vertical at once — shop products, booking resources, ghrm packages and subscription plans
platform.getreadre-resolve one hit by its entity_type + key (the detail card)
platform.entity_typesreaddiscovery — which entity types the two tools above accept
booking.createactioncreate a reservation on a booking resource, bound to the caller
booking.cancelactioncancel one of the caller's own bookings (ownership-guarded)
subscription.cancelactioncancel one of the caller's own subscriptions (ownership-guarded)
llm.askreadask the instance's configured LLM (the same connection the bots use); an optional grounded mode prepends platform.search hits as context
events.recentreada 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:

  1. Token → principal. A configured bearer token maps to a role, a scope and, optionally, a bound user_id. No token ⇒ 404.
  2. Pack gate. A capability is invisible and un-callable unless its pack is enabled.
  3. Scope. A read token can call only read capabilities; action capabilities need an action token and a bound user_id (you cannot act on behalf of nobody).
  4. Permission. The mapped role must hold the capability's required permission (the platform's usual RBAC).
  5. Ownership. Destructive actions (booking.cancel, subscription.cancel) load the target and refuse unless it belongs to the caller's user_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 (default false).
  • tokens — a list of bearer tokens, each a string or an object {token, role, scope, user_id?}. Empty ⇒ the endpoint 404s.
  • packs.* — one toggle per capability group (search, booking, subscription, llm, events, …), all off by default.
  • default_scope, max_results, llm_connection_slug (which LLM connection llm.ask uses), 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.get the 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.