# Octelium as an MCP Gateway

> Octelium documentation. Canonical page: <https://octelium.com/docs/octelium/latest/management/guide/service/ai/self-hosted-mcp>.

## 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](https://octelium.com/docs/octelium/latest/management/core/overview.md)).

*Diagram: OcteliumMCPGateway.* [View the diagram on the canonical HTML page](https://octelium.com/docs/octelium/latest/management/guide/service/ai/self-hosted-mcp).

## 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](https://octelium.com/docs/octelium/latest/management/core/service/clientless.md)):

```yaml
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:

```bash
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](https://octelium.com/docs/octelium/latest/management/core/service/overview.md#remotely-via-a-connected-user)):

```yaml
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](https://octelium.com/docs/octelium/latest/management/core/service/managed-containers.md)):

```yaml
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*:

```bash
octeliumctl create secret mcp-upstream-token
```

Inject the credential into authorized upstream requests as follows:

```yaml
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](https://octelium.com/docs/octelium/latest/management/core/service/secretless.md)).

## MCP Clients

MCP clients can access *Services* privately through the `octelium` client (read more [here](https://octelium.com/docs/octelium/latest/user/cli/connect.md)) 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](https://octelium.com/docs/octelium/latest/management/core/credential.md#oauth2-client-credentials). 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](https://octelium.com/docs/octelium/latest/management/core/credential.md#access-tokens)).

## 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:

```yaml
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:

```yaml
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:

```yaml
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:

```yaml
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:

```yaml
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](https://octelium.com/docs/octelium/latest/management/core/service/mcp/plugins.md#guardrail).

## Dynamic Routing

Dynamic configuration can route a request to another MCP server using normalized MCP context and authenticated identity (read more [here](https://octelium.com/docs/octelium/latest/management/core/service/dynamic-config.md)). The following example isolates a production deployment tool:

```yaml
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](https://octelium.com/docs/octelium/latest/management/core/service/http-plugins.md)). For example, the following plugins bound the request rate per *User* and validate tool-call requests against a JSON Schema:

```yaml
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:

```yaml
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](https://octelium.com/docs/octelium/latest/management/core/visibility.md) and the complete `MCP` mode [here](https://octelium.com/docs/octelium/latest/management/core/service/mcp/overview.md).
