Octelium documentation · Latest

Octelium as an MCP Gateway

Overview

Octelium provides a unified, self-hosted infrastructure for securing MCP clients and servers. The dedicated MCP Service mode understands MCP JSON-RPC messages instead of treating them as generic HTTP traffic. It provides the following:

  • Secure access to MCP servers running on the internet, in private networks behind NAT or as managed containers.

  • Standard bearer and OAuth2 authentication for MCP clients without a special Octelium SDK.

  • Secretless access to upstream MCP servers protected by bearer tokens, API keys, basic authentication or OAuth2 client credentials.

  • MCP protocol version, method, message and origin validation before a request reaches the server.

  • Identity-aware access control using normalized MCP methods, tool names, prompt names and resource URIs.

  • Protocol-aware guardrails for tool arguments, tool results, resource contents, prompt messages and tool definitions.

  • Dynamic routing to different MCP servers using authenticated identity and normalized MCP request context.

  • MCP-specific access logs for JSON-RPC requests, responses and SSE streams.

  • Lua, ExtProc, request rate limit, JSON Schema and direct response plugins inherited from the HTTP dataplane.

  • GitOps-friendly declarative management (read more here).

Client-based
access over WireGuard / QUIC tunnels
Workloads
OAuth2 / bearer token access

Unified identity for all MCP clients

JSON schema validation of tool-call params

Internal MCP server
directly reachable, streamable HTTP / SSE
Remote MCP server behind NAT
private cloud, on-prem, IoT, laptop
Managed container
deployed & scaled by Octelium
Public SaaS MCP server
secretless access to protected servers

Create an MCP Gateway

Assume that a streamable HTTP MCP server is listening at http://mcp-server.default.svc:8080/mcp. The following Service exposes it through public clientless access (read more about the BeyondCorp mode here):

kind: Service metadata: name: my-mcp spec: mode: MCP isPublic: true config: upstream: url: http://mcp-server.default.svc:8080/mcp mcp: endpoint: /mcp protocol: versions: - "2025-11-25" - "2026-07-28" requireVersion: true limits: maxRequestBytes: 524288 maxStreamEventBytes: 131072

Apply the configuration as follows:

octeliumctl apply /PATH/TO/SERVICE.YAML

endpoint is the path exposed to clients. The path in upstream.url is the path used with the server, so the two can differ. When the upstream URL has no path, Octelium preserves the downstream path.

An MCP request must use POST, carry an application/json JSON-RPC 2.0 request or notification and contain a single message. JSON-RPC batches are not supported. SSE responses are streamed and inspected event by event unless a response guardrail requires the complete response.

Upstream MCP Servers

An upstream can run anywhere an Octelium data-plane node can reach it. It can also run behind NAT and be served by a connected User (read more here):

kind: Service metadata: name: development-mcp spec: mode: MCP isPublic: true config: upstream: url: http://localhost:8080/mcp user: mcp-developer mcp: endpoint: /mcp

Managed Containers

Octelium can deploy, scale and serve a containerized MCP server using the Kubernetes infrastructure of the Cluster (read more here):

kind: Service metadata: name: managed-mcp spec: mode: MCP isPublic: true config: upstream: container: port: 8080 image: ghcr.io/org/my-mcp:1.2.3 replicas: 2 credentials: usernamePassword: username: ghcr-username password: fromSecret: ghcr-token resourceLimit: cpu: millicores: 1000 memory: megabytes: 2000 mcp: endpoint: /mcp

The server must listen on 0.0.0.0, and container.port must match its listening port. Environment variables, custom commands, security contexts, volumes and health probes are described in the managed-container reference.

Secretless Upstream Access

If an internet-facing MCP server requires a credential, store it in an Octelium Secret:

octeliumctl create secret mcp-upstream-token

Inject the credential into authorized upstream requests as follows:

kind: Service metadata: name: external-mcp spec: mode: MCP isPublic: true config: upstream: url: https://mcp.example.com/api/mcp mcp: endpoint: /mcp auth: bearer: fromSecret: mcp-upstream-token

The MCP client authenticates to Octelium, while Octelium authenticates separately to the upstream. The downstream authorization credential is never forwarded. Custom API key headers, basic authentication, OAuth2 client credentials and AWS Signature Version 4 are also supported (read more here).

MCP Clients

MCP clients can access Services privately through the octelium client (read more here) or publicly through the clientless mode. HUMAN Users can authenticate using configured IdentityProviders, while WORKLOAD Users can use authentication tokens, OpenID Connect assertions, OAuth2 client credentials or directly issued access tokens.

For public workload access, create an OAuth2 client credential as illustrated here. The MCP client obtains an Octelium access token from the Cluster's /oauth2/token endpoint and sends it as the bearer token to https://my-mcp.<DOMAIN>/mcp. The client needs no upstream MCP server credential.

note

An access token Credential can also be issued and used directly as a bearer token (read more here).

Protocol and Origin Validation

The protocol.versions field is an allowlist. Leaving it empty accepts any syntactically valid version, which is more forward-compatible. requireVersion rejects a request without a version in the MCP-Protocol-Version header or supported request metadata. If a version is declared in more than one location, the values must match.

Unknown but syntactically valid MCP methods are accepted by default so that protocol extensions and later revisions can reach the upstream. Set rejectUnknownMethods: true for strict validation:

mcp: protocol: versions: - "2026-07-28" requireVersion: true rejectUnknownMethods: true

Browser-based clients send an Origin header. Octelium accepts the Service's own origin, requests without an origin and origins allowed by mcp.cors. For example:

mcp: cors: allowOriginStringMatch: - https://agent-console.example.com allowMethods: POST allowHeaders: Authorization, Content-Type, MCP-Protocol-Version exposeHeaders: Mcp-Session-Id

This protects MCP servers from DNS rebinding while allowing an explicit browser client. disableOriginCheck: true disables validation entirely and is not recommended for a browser-reachable endpoint.

Access Control

MCP request information is normalized under ctx.request.mcp. The following Policy allows all authenticated Users to discover tools and permits members of finance-agents to call a specific tool with a bounded argument:

authorization: inlinePolicies: - spec: rules: - effect: ALLOW condition: match: ctx.request.mcp.method == "tools/list" - effect: ALLOW condition: all: of: - match: ctx.request.mcp.method == "tools/call" - match: ctx.request.mcp.name == "transfer" - match: ctx.request.mcp.http.bodyMap.params.arguments.amount <= 1000 - match: '"finance-agents" in ctx.user.spec.groups'

ctx.request.mcp.method contains methods such as tools/call, prompts/get and resources/read. ctx.request.mcp.name contains the corresponding tool name, prompt name or resource URI. The protocol version, JSON-RPC request identifier, notification status, reported client information, capabilities and MCP transport session identifier are also available.

The MCP client information, capabilities, JSON-RPC identifier and MCP session identifier are self-reported transport data rather than authenticated identity. Use the Octelium User, Session, Device and groups for identity-based authorization.

MCP Guardrails

MCP servers can return untrusted content to an AI agent, while tool arguments can carry secrets or personal information to an external server. Protocol-aware guardrails inspect those semantic parts without hardcoding JSON paths.

The following example denies credentials in tool arguments and redacts email addresses:

mcp: plugins: - name: protect-tool-arguments condition: match: ctx.request.mcp.method == "tools/call" guardrail: leg: REQUEST scopes: - TOOL_ARGUMENTS patterns: - secrets: {} action: DENY - type: EMAIL action: REDACT denyMessage: The tool arguments contain restricted content

A response guardrail can inspect tool results, resource contents, prompt messages and tool definitions for indirect prompt injection or tool poisoning:

mcp: plugins: - name: protect-server-content condition: any: of: - match: ctx.request.mcp.method == "tools/call" - match: ctx.request.mcp.method == "resources/read" - match: ctx.request.mcp.method == "prompts/get" - match: ctx.request.mcp.method == "tools/list" guardrail: leg: RESPONSE scopes: - TOOL_RESULTS - RESOURCE_CONTENTS - PROMPT_MESSAGES - TOOL_DEFINITIONS patterns: - regex: '(?i)ignore (all )?previous instructions' action: DENY denyMessage: The MCP server returned restricted content

Request content supports deny, redact, strip and replace actions. Response content and tool definitions can only be denied. A response guardrail buffers a complete SSE response before releasing it, which guarantees that matched content cannot reach the client but delays the first event until the stream ends.

Guardrails run after access control. They protect content; Policies continue to govern which identity may use a method, tool, prompt or resource. Read more here.

Dynamic Routing

Dynamic configuration can route a request to another MCP server using normalized MCP context and authenticated identity (read more here). The following example isolates a production deployment tool:

kind: Service metadata: name: platform-mcp spec: mode: MCP isPublic: true config: upstream: url: http://general-mcp.default.svc:8080/mcp mcp: endpoint: /mcp dynamicConfig: configs: - name: production upstream: url: https://production-mcp.example.com/mcp mcp: auth: bearer: fromSecret: production-mcp-token rules: - condition: all: of: - match: ctx.request.mcp.method == "tools/call" - match: ctx.request.mcp.name == "production_deploy" - match: '"platform-admins" in ctx.user.spec.groups' configName: production

The endpoint, protocol validation, parsing limits, origin configuration and downstream HTTP/2 listener are global. A dynamic configuration can select the upstream, upstream credential, headers, path manipulation, plugins, upstream HTTP/2 and visibility. It can also deploy and route to different managed containers.

Other Plugins

The MCP mode supports Lua, ExtProc, direct response, request rate limit, JSON Schema and path plugins inherited from HTTP (read more here). For example, the following plugins bound the request rate per User and validate tool-call requests against a JSON Schema:

mcp: plugins: - name: request-rate-limit condition: matchAny: true rateLimit: key: perUser: true limit: 100 window: minutes: 2 - name: validate-transfer condition: all: of: - match: ctx.request.mcp.method == "tools/call" - match: ctx.request.mcp.name == "transfer" jsonSchema: inline: | { "type": "object", "required": ["jsonrpc", "method", "params"], "properties": { "jsonrpc": { "const": "2.0" }, "method": { "const": "tools/call" }, "params": { "type": "object" } } }

Lua and ExtProc can intentionally rewrite MCP requests and responses. If they change a method, target or protocol version that the client also supplied in a reserved MCP header, they must update the header as well. Generic plugins sit outside the MCP guardrail boundary, so a plugin that creates MCP content must inspect that content itself when necessary. The generic HTTP cache is not supported because it does not understand MCP authorization or invalidation semantics.

Visibility

The MCP mode emits identity-aware access logs that include the underlying HTTP exchange and normalized protocol version, method, target name, JSON-RPC identifier, notification status, client information and MCP session identifier. Responses can include the result type, JSON-RPC error and tool error status. SSE streams produce a start entry and a final entry with stream duration, event counts and observed protocol metadata.

Request and response bodies are recorded by default because they are useful for MCP authorization and auditing. Disable them when prompts, arguments or results must not be stored:

mcp: visibility: disableRequestBody: true disableResponseBody: true includeRequestHeaders: - User-Agent includeResponseHeaders: - Content-Type

Body capture is bounded. Sensitive authentication and session headers remain excluded even when all headers are selected. Read more about access logs here and the complete MCP mode here.