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).
Info

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:

connectionIdstring

UUID, the gateway-issued identifier. The first 8 characters double as the tool prefix that disambiguates tools from different connections in the same session.

integrationDiscriminatorstring

Combines the platform key (e.g. "netsuite") with an org identifier. Used to enforce one provider per integration per tenant.

accountDiscriminatorstring

Normalized tenant identifier — same value the platform JWT carries. Scopes the provider to its tenant.

labelstring

Human-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.

MethodPathPurpose
GET/v1/mcp/providersList providers accessible to the caller. Paginated; supports account, integrationDiscriminator, deleted filters.
POST/v1/mcp/providersRegister a new provider. Body carries integrationDiscriminator, accountDiscriminator, label, users[], admins[], and platform-specific extensions.
GET/v1/mcp/providers/:connectionIdFetch 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/:connectionIdUpdate label, users, admins, or extensions.
DELETE/v1/mcp/providers/:connectionIdSoft-delete the provider.
POST/v1/mcp/providers/:connectionId/reloadReconnect to the upstream server, fetch a fresh tool catalog, store it in DynamoDB. Returns the new tools[].
GET/POST/v1/mcp/providers/:connectionId/oauth/refreshBegin the OAuth dance with the upstream provider; redirects through the platform's OAuth service.
GET/v1/mcp/providers/:connectionId/oauth/callbackOAuth callback — exchanges the authorization code for an access token and stores it.
Tip

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.

Warning

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

/dashboard

Stats cards (total providers, connected users, available tools) plus a recent-providers list.

/providerslist

Paginated list of every provider the caller has access to, with a filter for soft-deleted entries.

/providers/newform

Create a new provider. After save, you typically click straight through to the OAuth refresh to bind a token.

/providers/[connectionId]detail

Provider 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]/editform

Edit label, users, admins, or extensions for an existing provider.

Info

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"