Skip to main content

Hosted MCP server

Devotel Orbit can be exposed as an MCP (Model Context Protocol) server, so an external MCP client — Claude, ChatGPT, or an IDE agent — operates your tenant through a curated catalog of platform operations. This page is the conceptual anchor for that surface: why it exists, what the catalog contains, how scope resolution decides which tools a given API key can see, and how the authorize layer composes with the platform’s normal role and scope gates. Task-level walkthroughs (client setup, protocol handshake) link out at the end; this page defines the model they all reference.

Section 1 — Why Orbit hosts an MCP server

Most integrations talking to Orbit are outbound MCP consumers: your Orbit agents register third-party MCP servers (for example, a CRM or a calendar provider) and call their tools. This surface is the reverse direction. One curated JSON-RPC endpoint — POST /api/v1/mcp (with a GET probe that returns the capability descriptor) — lets an external client drive your tenant through a fixed catalog of Orbit’s own platform operations. The design choice is deliberate: an MCP client gets a catalog, not arbitrary tool injection. Every tool in the catalog dispatches to an already-shipped, already-gated REST route — sender resolution, quota checks, TCPA/quiet-hours gates, provider routing, and audit writes all run through the same code path a normal API call would. The MCP layer is a protocol adapter over the platform, not a second implementation of it. Nothing here depends on a local npx process or a client-side broker — the server is hosted behind the API gateway, and authentication is the platform’s normal API-key bearer (X-API-Key or Authorization: Bearer <key>).

Section 2 — The curated tool catalog

The catalog is intentionally small and read-heavy: one write tool per chargeable surface (send an SMS, place a call) plus read tools for campaigns, numbers, analytics, and CDP profiles. Each tool declares the same OAuth-style scope string the underlying REST route enforces — there is no MCP-specific scope namespace to keep in sync. Two names cover the same voice-verb dispatch (place_call, and the Twilio-parity alias send_voice_call with optional answer_url call-control). Outbound voice still exits only via the Devotel softswitch — these tools call back into the already-gated REST route; they wire nothing new to a carrier. Each tool’s inputSchema is a JSON-Schema subset (type, properties, required) so an MCP client can render argument forms or validate input before it calls. A required-argument check runs before dispatch, so a missing campaign_id on get_campaign errors cleanly instead of silently collapsing onto the list route.

Section 3 — Default-deny scope resolution

Scope resolution answers “which of the catalog’s tools does this key’s scopes array grant access to” at tools/list time, and again at tools/call time:
  • A key with no scopes sees zero tools. An empty or absent scopes array unlocks nothing — tools/list returns an empty tools[], and every tools/call is rejected.
  • The * wildcard scope opens the whole catalog. Full-access keys enumerate every catalogued tool, matching what * means everywhere else on the platform.
  • A scope the catalog doesn’t carry unlocks nothing. Granting voice:read to a key whose tools only accept voice:write doesn’t leak voice tools into the list.
There is no enumeration side-channel: a client only ever sees tools it is entitled to call. An unknown tool name and a known-but-unscoped tool name are indistinguishable to the caller — both surface as tool not found — so a disallowed tool’s mere existence is never leaked.
The legacy “scopeless key = full access” grandfather window that the platform’s general-purpose API routes honour for long-lived keys is deliberately not honoured here. The MCP surface is new, so every caller must opt in with explicit scopes — a brand-new attack surface for external LLM agents gets tightened defaults, not the laxest historical semantics.
Grant or revoke scopes under Dashboard → Settings → API Keys, then re-run tools/list to refresh the catalog for that key.

Section 4 — Transport and handshake

The endpoint is a single JSON-RPC 2.0 service that always replies with a plain JSON body — it never upgrades to an SSE stream. The session flows:
  1. initialize — capability negotiation; returns the protocol version and the server descriptor (name: orbit, plus its version).
  2. tools/list — enumerate the scoped catalog (Section 3).
  3. tools/call — dispatch one tool by name with arguments.
  4. notifications/initialized — acknowledged per JSON-RPC notification semantics (no response body required).
The dashboard probe page (Dashboard → Developer → MCP Server) exercises exactly this lifecycle without any client setup: a “Test connection” button fires initialize, and only on success does the page issue tools/list to render the catalog rows. An empty result renders an explicit “no tools in scope” message rather than hanging on an empty list.

Section 5 — Authorization vs role gates

Two distinct gates stack on this surface:
  • Scope gates (requireScope) decide which catalog entries the key may see and call. The scope claim lives on the API key and is resolved per tool, as in Section 3.
  • Role gates (requireRole) decide whether the caller’s derived role is allowed onto a given class of route at all — for example an analytics:read tool still routes through a handler that may require a minimum dashboard role in addition to the scope.
The dispatch itself re-injects the caller’s own auth header into the underlying REST route, so the inner route’s normal middleware chain re-resolves tenant and re-runs its own scope/role checks. Even if this MCP layer’s pre-check were ever bypassed, the inner route’s gate still applies — defence in depth, not a single point of failure. Tenant isolation is identical to a direct REST call: the injected request re-resolves req.ctx.tenant from the same credential, so an MCP caller can never reach another tenant’s data.

Section 6 — Relationship to the guides

This page is the conceptual anchor for the surface. The task-level walkthroughs build on it:

See also