External-facing user-scoped JSON-RPC proxy for MCP servers and MCP hosts.
- Clients authenticate with
external-rest-apifirst. - Clients request RPC access tokens via
external-rest-api(POST /api/v1/rpc/token). control-apiissues/signs the RPC JWTs via internal endpoints.rpc-proxyvalidates issuer, audience, signature, scopes, and expiry.- Access is enforced by:
- user-to-context mapping from
control-api(userId -> contextIds) - allowed MCP servers returned by
control-apifor each user - allowed MCP hosts returned by
control-apifor each user - scope checks (
mcp:servers:list,mcp:server:invoke,host:message:invoke,host:status:read,host:health:read,host:activity:read) - upstream server URLs provided by
control-api
- user-to-context mapping from
rpc-proxy resolves access and MCP connectivity through control-api:
- uses JWT
subasuserId - calls
control-apiforcontextIdsand allowed MCP servers (including URLs) - caches user access/server lookups in-memory for a short TTL
- treats
teamIdin the token as informational; authorization is user/context based
GET /healthGET /api/v1/rpc/servers(requiresmcp:servers:list)POST /api/v1/rpc/:serverName(requiresmcp:server:invoke)POST /api/v1/rpc/hosts/:hostRef/messages(requireshost:message:invoke)GET /api/v1/rpc/hosts/:hostRef/activity(requireshost:activity:read)GET /api/v1/rpc/hosts/:hostRef/activity/stream(requireshost:activity:read)GET /api/v1/rpc/hosts/:hostRef/status(requireshost:status:read)GET /api/v1/rpc/hosts/:hostRef/status/stream(requireshost:status:read)GET /api/v1/rpc/hosts/:hostRef/health(requireshost:health:read)
Host runtime routes in rpc-proxy are REST-oriented and scope-specific. The status stream is a read-only telemetry channel.
| Endpoint | Method | Required Scope | Access Type | Transport |
|---|---|---|---|---|
/api/v1/rpc/hosts/:hostRef/messages |
POST |
host:message:invoke |
Write | REST |
/api/v1/rpc/hosts/:hostRef/activity |
GET |
host:activity:read |
Read | REST |
/api/v1/rpc/hosts/:hostRef/activity/stream |
GET |
host:activity:read |
Read-only | SSE |
/api/v1/rpc/hosts/:hostRef/status |
GET |
host:status:read |
Read | REST |
/api/v1/rpc/hosts/:hostRef/health |
GET |
host:health:read |
Read | REST |
/api/v1/rpc/hosts/:hostRef/status/stream |
GET |
host:status:read |
Read-only | SSE |
Notes:
status/streamdoes not accept request bodies and must not be used for message submission.activity/streamis read-only telemetry and never accepts message submission payloads.activityandactivity/streamnever include chain-of-thought/internal reasoning text.- Message submission is only via
POST /api/v1/rpc/hosts/:hostRef/messages.
POST /api/v1/rpc/:serverName expects a JSON-RPC 2.0 request body:
{
"jsonrpc": "2.0",
"id": "1",
"method": "tools/list",
"params": {}
}POST /api/v1/rpc/hosts/:hostRef/messages expects a REST request body:
{
"content": "hello from desktop",
"channelType": "rpc",
"sender": "desktop-app"
}sequenceDiagram
participant Desktop as desktop-app
participant Proxy as rpc-proxy
participant Gateway as control-api-rpc-gateway
participant Control as control-api
participant Host as mcp-host/<hostRef>
Desktop->>Proxy: POST /api/v1/rpc/hosts/:hostRef/messages (Bearer RPC JWT)
Proxy->>Proxy: Verify RS256 JWT + iss/aud + typ=user + exp + scope + hostRefs
Proxy->>Gateway: GET /api/v1/rpc/access/users/:userId/mcp-hosts/:hostRef
Gateway->>Control: Forward allowlisted path
Control->>Control: Validate internal caller + x-rpc-access-token + claim binding
Control-->>Proxy: { userId, hostRef, url } or 403
Proxy->>Host: POST /v1/runtime/messages
Host-->>Proxy: runtime response
Proxy-->>Desktop: REST response body
Required JWT claims for host message path:
typ=usersub=<userId>teamId=<teamId>scopesincludeshost:message:invokehostRefsincludes target:hostRef(wildcard*is rejected)iss,aud,iat,exp,jtivalid
401 Unauthorized: token malformed/expired/wrong issuer-audience/signature.403 Forbidden: missing scope: token does not include the required route scope.403 Forbidden: user cannot access this host: host not in tokenhostRefsand/or denied bycontrol-apiRPC-access policy.504 Gateway Timeout: upstream MCP target unreachable (check network policy and service reachability).
See .env.example.
Critical variables:
RPC_PROXY_JWT_PUBLIC_KEYRPC_PROXY_JWT_ISSUERRPC_PROXY_JWT_AUDIENCERPC_PROXY_CONTROL_API_BASE_URLRPC_PROXY_CONTROL_API_CACHE_TTL_MS
Required in production:
RPC_PROXY_CONTROL_API_SERVICE_TOKEN
RPC_PROXY_JWT_PUBLIC_KEY must match the public key for CONTROL_API_RPC_JWT_PRIVATE_KEY.
cd rpc-proxy
npm install
npm run devThe deploy/ manifests include:
- non-root container +
RuntimeDefaultseccomp - dropped Linux capabilities
- read-only root filesystem with writable
/tmp - resource requests/limits and health probes
- ingress TLS and basic rate limiting annotations
- restrictive
NetworkPolicyandPodDisruptionBudget
Before deploying, create a real secret from deploy/example.secret.yaml values:
kubectl create secret generic rpc-proxy-secrets -n rpc-proxy \
--from-literal=RPC_PROXY_JWT_PUBLIC_KEY='-----BEGIN PUBLIC KEY-----\nreplace-with-rs256-public-key\n-----END PUBLIC KEY-----' \
--from-literal=RPC_PROXY_CONTROL_API_SERVICE_TOKEN='replace-with-rpc-proxy-token'Then deploy:
cd rpc-proxy
make deployUpdate deploy/ingress.yaml host/TLS secret and deploy/networkpolicy.yaml selectors/ports for your cluster.