MCP gateway

The open source, self-hosted MCP gateway

Octelium is a free and open source, self-hosted zero trust access platform. Its MCP mode turns a Service into an identity-aware Model Context Protocol gateway: it parses JSON-RPC messages, authorizes each tool call against the identity behind it, injects the upstream credential, and records what was called with which arguments.

  • Free and open source
  • Designed for self-hosting
  • Streamable HTTP MCP servers
mcp.yaml
An MCP ServiceThe MCP mode understands JSON-RPC messages and validates them before the upstream sees them.
An MCP Service. The MCP mode understands JSON-RPC messages and validates them before the upstream sees them.
kind: Service
metadata:
  name: tools-mcp
spec:
  mode: MCP
  isPublic: true
  config:
    upstream:
      url: http://mcp.internal:8080
    mcp:
      endpoint: /mcp
      protocol:
        versions: ["2026-07-28"]
        requireVersion: true
A Policy. Methods, tool names and tool arguments are each authorized on their own.
kind: Policy
metadata:
  name: finance-tools
spec:
  rules:
    - effect: ALLOW
      condition:
        all:
          of:
            - match: ctx.request.mcp.method == "tools/call"
            - match: ctx.request.mcp.name == "transfer"
            - match: '"finance" in ctx.user.spec.groups'
    - effect: ALLOW
      condition:
        match: ctx.request.mcp.method == "tools/list"
A deployed MCP server. The Cluster can deploy, scale and serve the MCP server itself as the Service upstream.
kind: Service
metadata:
  name: tools-mcp
spec:
  mode: MCP
  isPublic: true
  config:
    upstream:
      container:
        port: 8080
        image: ghcr.io/acme/mcp:1.4.0
        replicas: 2
    mcp:
      endpoint: /mcp
mcp-private.yaml
A private MCP serverAn MCP server behind NAT in any environment, reached over the tunnel through a stable route.
A private MCP server. An MCP server behind NAT in any environment, reached over the tunnel through a stable route.
kind: Service
metadata:
  name: internal-mcp
spec:
  mode: MCP
  config:
    upstream:
      url: http://10.0.6.20:8080
    mcp:
      endpoint: /mcp
The same server, clientlessly. One added field publishes the very same Service over HTTPS for agents with no client at all.
kind: Service
metadata:
  name: internal-mcp
spec:
  mode: MCP
  isPublic: true
  config:
    upstream:
      url: http://10.0.6.20:8080
    mcp:
      endpoint: /mcp
Origin validation. Browser-based clients are checked against an allowlist, which protects against DNS rebinding.
kind: Service
metadata:
  name: tools-mcp
spec:
  mode: MCP
  isPublic: true
  config:
    upstream:
      url: https://mcp.example.com
    mcp:
      endpoint: /mcp
      cors:
        allowOriginStringMatch:
          - https://client.example.com
policy-tool.yaml
A single toolThe JSON-RPC method and the tool name are normalized fields, not substrings of a body.
A single tool. The JSON-RPC method and the tool name are normalized fields, not substrings of a body.
kind: Policy
metadata:
  name: search-only
spec:
  rules:
    - effect: ALLOW
      condition:
        all:
          of:
            - match: ctx.request.mcp.method == "tools/call"
            - match: ctx.request.mcp.name == "search_docs"
            - match: '"agents" in ctx.user.spec.groups'
Tool arguments. The parsed JSON-RPC body is part of the request context, so arguments can be bounded.
kind: Policy
metadata:
  name: bounded-transfer
spec:
  rules:
    - effect: ALLOW
      condition:
        match: 'ctx.request.mcp.method == "tools/call" &&
          ctx.request.mcp.name == "transfer" &&
          ctx.request.mcp.http.bodyMap.params.arguments.amount <= 1000'
Protocol validation. Accepted protocol versions are an allowlist, and unknown methods can be rejected outright.
kind: Service
metadata:
  name: tools-mcp
spec:
  mode: MCP
  config:
    upstream:
      url: http://mcp.internal:8080
    mcp:
      endpoint: /mcp
      protocol:
        versions:
          - "2025-11-25"
          - "2026-07-28"
        requireVersion: true
        rejectUnknownMethods: true
mcp-secretless.yaml
An upstream bearer tokenThe upstream token stays in the Cluster and is injected only after authorization.
An upstream bearer token. The upstream token stays in the Cluster and is injected only after authorization.
kind: Service
metadata:
  name: tools-mcp
spec:
  mode: MCP
  isPublic: true
  config:
    upstream:
      url: https://mcp.example.com
    mcp:
      endpoint: /mcp
      auth:
        bearer:
          fromSecret: mcp-upstream-token
A separate server per tool. Sensitive tools can be routed to their own MCP server with their own upstream credential.
kind: Service
metadata:
  name: tools-mcp
spec:
  mode: MCP
  config:
    upstream:
      url: http://general-mcp:8080
    mcp:
      endpoint: /mcp
  dynamicConfig:
    configs:
      - name: sensitive
        upstream:
          url: http://sensitive-mcp:8080
        mcp:
          auth:
            bearer:
              fromSecret: sensitive-mcp-token
    rules:
      - condition:
          match: ctx.request.mcp.name == "deploy"
        configName: sensitive
A private registry. Deployed MCP servers pull from private registries using Cluster Secrets, with resource limits.
kind: Service
metadata:
  name: tools-mcp
spec:
  mode: MCP
  isPublic: true
  config:
    upstream:
      container:
        port: 8080
        image: ghcr.io/acme/mcp:1.4.0
        replicas: 2
        credentials:
          usernamePassword:
            username: ghcr-user
            password:
              fromSecret: ghcr-token
    mcp:
      endpoint: /mcp
idp.yaml
Secretless agent identityAgents and jobs authenticate with OIDC assertions issued by the platform running them.
Secretless agent identity. Agents and jobs authenticate with OIDC assertions issued by the platform running them.
kind: IdentityProvider
metadata:
  name: github-actions
spec:
  oidcIdentityToken:
    issuerURL: https://token.actions.githubusercontent.com
    audience: https://example.com
A workload User. Agents are Users with the same groups, Policies and audit trail as people.
kind: User
metadata:
  name: release-agent
spec:
  type: WORKLOAD
  groups:
    - agents
An LLM gateway. The same Cluster fronts the model an agent talks to, not only the tools it calls.
kind: Service
metadata:
  name: anthropic
spec:
  mode: LLM
  isPublic: true
  config:
    upstream:
      url: https://api.anthropic.com
    llm:
      protocol: ANTHROPIC
      auth:
        custom:
          header: x-api-key
          value:
            fromSecret: anthropic-api-key
Visibility and auditing

Every tool call, with the identity that made it

Every request produces an AccessLog identifying the User, Session and Device alongside the JSON-RPC detail of the message itself, emitted in real time to your OpenTelemetry OTLP receivers.

Read about visibility and auditing
AccessLogOTLP
ALLOWlinus@acme.compg-prod.dbPOSTGRESquery: select id, email from users limit 50
ALLOWci-runnerprod-k8sKUBERNETESverb=list resource=pods namespace=production
DENYagent-07tools-mcpMCPtools/call name=transfer policy=mcp-tools
ALLOWjane@acme.combastionSSHsession recorded, upstream user=deploy
ALLOWagent-07openaiLLMCHAT_COMPLETIONS model=gpt-5-mini tokens=1284
octeliumctl
octeliumctl apply ./cluster
$ octeliumctl apply ./cluster
Secret: ghcr-token Updated
Secret: mcp-upstream-token Updated
IdentityProvider: github-actions Created
Policy: finance-tools Created
Group: agents Created
User: release-agent Created
Service: tools-mcp.ai Created
Service: internal-mcp.ai Created
Service: anthropic.ai Updated
Namespace: ai Created
Cluster Core resources successfully applied
 7 resources created
 3 resources updated

 ClusterConfig updated
The MCP surface

Everything an MCP request carries, and what checks it

An MCP call is not a single decision. These are the parts of a request that matter for access control and auditing, next to the mechanism in an Octelium Cluster that handles each one.

  1. 01

    The caller

    Every request carries an Octelium identity. Agents and workloads authenticate with the OAuth2 client credentials flow, access tokens or federated OIDC assertions, and a browser-based client authenticates through your own identity provider.

    Read more
  2. 02

    The protocol version

    The declared version is read from the MCP-Protocol-Version header, the request metadata or an initialize request, and checked against an optional allowlist. An empty allowlist accepts any valid version, so a Service can accept newer revisions without a Cluster upgrade.

    Read more
  3. 03

    The JSON-RPC method

    The method, such as tools/call, tools/list, prompts/get or resources/read, is a normalized field available to Policies. Unknown but otherwise valid methods are accepted by default and can be rejected explicitly.

    Read more
  4. 04

    The tool being called

    The tool name, prompt name or resource URI is normalized separately from the method, so discovery can be allowed for everyone while individual tools are restricted to the identities that should call them.

    Read more
  5. 05

    The tool arguments

    The parsed JSON-RPC body is part of the request context, including arguments that have no normalized field, so a policy can bound a numeric argument or require a particular target rather than allowing a tool outright.

    Read more
  6. 06

    The browser origin

    Browser-based clients send an Origin header, which is validated by default to protect MCP servers from DNS rebinding attacks. Additional origins are an explicit allowlist, and requests without the header are accepted for non-browser clients.

    Read more
  7. 07

    The upstream credential

    A bearer token, API key, basic authentication or OAuth2 client credential protecting the MCP server is stored as a Secret and injected only after a request is authorized. The downstream credential is never forwarded upstream.

    Read more
  8. 08

    The server itself

    The upstream can be an existing MCP server anywhere, or a container the Cluster deploys, scales and serves itself from a public or private registry with resource limits, volumes and health probes.

    Read more

Client-reported names, capabilities and session identifiers are useful context and useful audit evidence, but they are supplied by the caller. Identity decisions use the Octelium User, Session and Device.

Questions

Frequently asked

What does the MCP mode actually inspect?
An MCP request must use POST, carry an application/json media type and contain a single valid JSON-RPC 2.0 request or notification. The mode normalizes the protocol version, method, target name, request identifier, notification status, reported client information, declared capabilities and transport session identifier, and it also exposes the parsed request body. JSON-RPC batch requests are not supported, and a streamed SSE response is proxied without waiting for the stream to end.
Can access control reach individual tool calls?
Yes. The JSON-RPC method and the tool name are separate normalized fields, so tools/list can be allowed for every authenticated User while tools/call is restricted per tool and per group. Tool arguments are available through the parsed request body, which lets a policy bound a numeric argument or require a specific target rather than allowing a tool unconditionally.
Can the Cluster host the MCP server as well as protect it?
Yes. Managed containers deploy, scale and serve a containerized MCP server as the Service upstream, using the Kubernetes infrastructure of the Cluster itself. The server must listen on 0.0.0.0 and its port must match the configured container port. Environment variables and Secrets, custom commands, replica counts, private registries, resource limits, security contexts, volumes and health probes are all supported.
Is the client information reported by an MCP client trustworthy?
No, and it should not be treated as identity. The client name and version, declared capabilities, JSON-RPC request identifier and MCP session identifier are all controlled by the downstream client. They are useful as request context and audit information, but access control decisions should use the Octelium User, Session, Device and group information instead.
How do agents authenticate without a client or SDK?
Workload Users share the same identity model as human Users. They authenticate with authentication tokens, with the standard OAuth2 client credentials flow against the Cluster's own token endpoint, or secretlessly with federated OpenID Connect assertions issued by the platform hosting them, such as GitHub Actions, a cloud provider or a Kubernetes cluster. No special SDK is required.
Are MCP messages recorded?
Yes. Each request produces an identity-aware AccessLog streamed in real time over OpenTelemetry, including the protocol version, method, target name, request identifier, client information, result type, JSON-RPC error details and tool error status. Unlike the LLM mode, request and response body visibility is enabled by default here because inspecting JSON-RPC messages is commonly required for auditing, and it can be disabled per Service.
Get started

Deploy Octelium on your own infrastructure in minutes

Free and open source. Self-hosted. No vendor lock-in.