MCP Gateway
The MCP Gateway is a platform service that brokers tool calls between the Inference-Server and upstream Model Context Protocol providers. The inference server never talks to NetSuite (or any other upstream system) directly — it goes through this gateway.
What it is
A standalone capability server (Capability-Servers/MCP-Gateway) that:
- Holds the registry of provider connections for each tenant — e.g. "this NetSuite account is connected, here are its tools, this is the OAuth token to use."
- Brokers tool calls from the inference server to the appropriate upstream MCP server, injecting the right credentials.
- Manages OAuth to the upstream provider (the NetSuite-side OAuth, not the platform sign-in OAuth).
- Sends heartbeats back to the inference server during tool execution, which is why MCP-routed tools survive client disconnects (see Tools — Heartbeats).
The gateway is the only MCP entry point. MCP_GATEWAY_DOMAIN always
points at this service. The pluggability lives inside the gateway —
it has a provider registry where each provider knows how to talk to its
upstream MCP server. NetSuite is the only registered provider today.
Architecture
┌─────────────────┐ HTTPS ┌──────────────┐ MCP ┌──────────┐
│ Inference-Server│──────────────▶│ MCP Gateway │──────────▶│ NetSuite │
└─────────────────┘ list tools └──────────────┘ (SSE) └──────────┘
│ ▲
│ SQS request │ SQS reply +
│ (tool call) │ heartbeats
└────────────────────────────────┘Two communication channels:
- HTTP for synchronous reads (list providers, fetch tool catalog, OAuth refresh, admin operations).
- SQS request/response for tool execution — async, with the gateway's worker process sending heartbeats back during long-running calls.
Providers and connections
A provider in the gateway is a connection between a tenant account and
an upstream MCP server. It's also referred to as a "connection" —
they're the same thing in the data model (ProviderEntry).
Each provider record carries:
connectionIdstringUUID, the gateway-issued identifier. The first 8 characters double as the tool prefix that disambiguates tools from different connections in the same session.
integrationDiscriminatorstringCombines the platform key (e.g. "netsuite") with an org identifier.
Used to enforce one provider per integration per tenant.
accountDiscriminatorstringNormalized tenant identifier — same value the platform JWT carries. Scopes the provider to its tenant.
labelstringHuman-readable name shown in the Control Panel and in error messages.
usersstring[]Principals (typically emails) allowed to use this provider via the agent.
adminsstring[]Principals allowed to manage the provider configuration.
extensionsRecord<string, unknown>Platform-specific configuration. For NetSuite this carries the SuiteApp scope; for a future Salesforce provider it might carry instance URL, etc.
toolsTool[]Cached tool catalog from the last successful reload. Served to the
inference server via GET /v1/mcp/providers/:connectionId.
Records live in DynamoDB. OAuth tokens for the upstream provider live in a
separate DynamoDB table keyed by (accountDiscriminator, userPrincipal, integrationDiscriminator).
HTTP API surface
All routes require a JWT (Bearer header or Authorization cookie).
Service callers must use a service-identity JWT minted via
mintOnBehalfOf with the gateway as the to claim.
| Method | Path | Purpose |
|---|---|---|
GET | /v1/mcp/providers | List providers accessible to the caller. Paginated; supports account, integrationDiscriminator, deleted filters. |
POST | /v1/mcp/providers | Register a new provider. Body carries integrationDiscriminator, accountDiscriminator, label, users[], admins[], and platform-specific extensions. |
GET | /v1/mcp/providers/:connectionId | Fetch a single provider, including its cached tools[]. This is the call the inference server makes when a session has mcpProviders enabled. |
PATCH | /v1/mcp/providers/:connectionId | Update label, users, admins, or extensions. |
DELETE | /v1/mcp/providers/:connectionId | Soft-delete the provider. |
POST | /v1/mcp/providers/:connectionId/reload | Reconnect to the upstream server, fetch a fresh tool catalog, store it in DynamoDB. Returns the new tools[]. |
GET/POST | /v1/mcp/providers/:connectionId/oauth/refresh | Begin the OAuth dance with the upstream provider; redirects through the platform's OAuth service. |
GET | /v1/mcp/providers/:connectionId/oauth/callback | OAuth callback — exchanges the authorization code for an access token and stores it. |
When tools mysteriously disappear from an agent, the first thing to
check is the cached tools[] on the provider. A
POST .../reload will refetch from the upstream server; if that fails,
the OAuth token has likely expired and the user needs to re-run the
refresh flow.
Upstream OAuth
The gateway does not implement upstream OAuth itself — it delegates to the
platform's central OAuth service (OAUTH_DOMAIN, the same one that
issues the platform JWTs developers carry). The flow:
User triggers a refresh
An admin or end-user opens /v1/mcp/providers/:connectionId/oauth/refresh
(typically via the Control Panel). The gateway deletes
any existing token record for that user + integration + account, then
redirects to the platform OAuth service with scopes[]=mcp plus a
callback URL pointing back to the gateway.
User authorizes upstream
The platform OAuth service walks the user through the upstream provider's authorization screen (for NetSuite, this is the NetSuite login + consent page).
Gateway receives the code
The platform OAuth service redirects back to the gateway's
/v1/mcp/providers/:connectionId/oauth/callback?code=.... The gateway
exchanges the code for an access token and stores it in DynamoDB.
Upstream tokens are not auto-refreshed. When a tool call fails with an auth error, the user must re-run the refresh flow above. The Control Panel surfaces this — see below.
Tool-call brokering
When the model emits a tool call for an MCP-provided tool, here's what happens:
Inference server publishes the call
The inference server writes a request to an SQS queue with the
sessionId, requestId, the prefixed tool name (e.g.
a1b2c3d4_getRecord), the input args, and the calling
userPrincipal + accountDiscriminator.
Gateway worker picks it up
The gateway's SQS worker resolves the provider by the tool prefix (first
8 chars of the name → ToolPrefixIndex lookup), pulls the OAuth token
from DynamoDB, and connects to the upstream MCP server.
Tool runs upstream, gateway heartbeats
The gateway calls the tool via the MCP SDK and, while the call is in flight, sends heartbeats back to the inference server. This is why MCP-routed tools survive client disconnects — the heartbeats originate server-side.
Result returns via SQS
Success comes back as { state: "COMPLETE", output: { toolResult, toolName } };
failure as { success: false, error }. The inference server feeds the
result into the conversation as a tool_result block.
Pluggability
The gateway's provider registry is pluggable in code via
MCPPlatformDefinition:
export type MCPPlatformDefinition = {
platformId: string;
buildUrl: (provider, accountDiscriminator) => URL;
getCredentials: (params) => Promise<{ accessToken: string } | null>;
connect: (ctx) => Promise<MCPClientWrapper>;
mapTools: (toolPrefix, rawTools) => Tool[];
};The NetSuite provider lives at
Capability-Servers/MCP-Gateway/src/platform/netsuite/ and ships these
four functions. The BaseMCPPlatforms enum lists the providers loaded
at boot — today that's just NetSuite.
Adding a new provider is a code change: implement the four functions, register the platform, redeploy. There is no runtime API for end-users or tenants to register an arbitrary MCP server. This is the correction to a common misreading of the platform: "MCP gateway" doesn't mean "any MCP server" — it means "our broker, with whatever providers the platform team has wired up."
Control Panel
The day-to-day UI for managing provider connections is the MCP Gateway
Control Panel (Tools/rfs-ai-mcp-gateway-control-panel), a Next.js
app that talks to the gateway's HTTP API.
It's the admin surface that customers and platform admins use to:
- See all providers their tenant owns, filtered or showing deleted.
- Register a new provider (form-based; runs
POST /v1/mcp/providers). - Drill into a provider to see its cached tools, assigned users, and admins.
- Trigger an OAuth refresh when the upstream token expires.
- Manually reload tools after an upstream catalog change.
- Soft-delete a provider.
Pages
/dashboardStats cards (total providers, connected users, available tools) plus a recent-providers list.
/providerslistPaginated list of every provider the caller has access to, with a filter for soft-deleted entries.
/providers/newformCreate a new provider. After save, you typically click straight through to the OAuth refresh to bind a token.
/providers/[connectionId]detailProvider detail view with tabs for Tools (the cached catalog), Users (who can use it), and Admins (who can manage it). Includes Reload and OAuth Refresh actions.
/providers/[connectionId]/editformEdit label, users, admins, or extensions for an existing provider.
The Control Panel signs in with the platform's OAuth flow — same JWT
the inference API uses. Permission to view or modify a provider is
enforced by the gateway based on the JWT's userPrincipal against the
provider's users[] and admins[].
Operating outside the UI
Everything the Control Panel does is also reachable via the HTTP API documented above — useful for scripted provisioning, CI integration tests, or one-off ops. For example, to register and bind a NetSuite provider end-to-end without the UI:
# 1. Create the provider
curl -X POST "$GATEWAY_URL/v1/mcp/providers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"integrationDiscriminator": "netsuite:acme-1234",
"accountDiscriminator": "acme",
"label": "Acme NetSuite (prod)",
"users": ["user@acme.com"],
"admins": ["admin@acme.com"],
"extensions": { "netsuite": { "suiteapp": "..." } }
}'
# 2. Send the user through OAuth (returns a redirect URL)
curl -i "$GATEWAY_URL/v1/mcp/providers/$CONNECTION_ID/oauth/refresh" \
-H "Authorization: Bearer $TOKEN"
# 3. After the user completes the upstream consent, reload tools
curl -X POST "$GATEWAY_URL/v1/mcp/providers/$CONNECTION_ID/reload" \
-H "Authorization: Bearer $TOKEN"