Open-source MCP gateway

Secure and govern MCP access for AI agents

Octelium is a self-hosted Model Context Protocol gateway built around authenticated identity and protocol-aware policy. Control which humans, workloads and AI agents can discover and invoke tools, inspect what crosses the client/server boundary, isolate MCP server credentials, route calls dynamically and audit each MCP exchange.

  • Free and open source
  • Designed for self-hosting

Every request from a human, a browser, a CLI, a workload or an AI agent is authenticated against your identity providers, authorized per request at the application layer by the Octelium Service in front of the resource, exported to your SIEM and observability stack whether it is allowed or denied, and only then routed to the upstream HTTP API, database, SSH server, Kubernetes cluster, LLM or MCP server.

Capabilities of the LLM and MCP gateway modes, each shown as the Service configuration that enables it

  • LLM gateway, Prompt instructions: Insert, replace or reject system instructions and messages. openai.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: openai
  • spec:
  • mode: LLM
  • config:
  • upstream:
  • url: https://api.openai.com
  • llm:
  • protocol: OPENAI
  • auth:
  • bearer:
  • fromSecret: openai-api-key
  • plugins:
  • - name: house-rules
  • condition:
  • match: ctx.request.llm.operation == "GENERATE"
  • prompt:
  • system:
  • mode: PREPEND
  • content:
  • value: Follow Acme's security and data-handling rules.
  • - name: caller-identity
  • condition:
  • match: ctx.request.llm.operation == "GENERATE"
  • prompt:
  • message:
  • role: USER
  • position: NEW_BEFORE
  • content:
  • eval: '"The requesting User is " + ctx.user.metadata.name'
  • mode: PREPEND: first instruction
  • eval: '"The requesting User is " + ctx.user.metadata.name': …User is support-bot
  • LLM gateway, Guardrails: Deny secrets and redact personal data before the model sees it. openai.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: openai
  • spec:
  • mode: LLM
  • config:
  • upstream:
  • url: https://api.openai.com
  • llm:
  • protocol: OPENAI
  • auth:
  • bearer:
  • fromSecret: openai-api-key
  • plugins:
  • - name: input-dlp
  • condition:
  • match: ctx.request.llm.operation == "GENERATE"
  • guardrail:
  • leg: REQUEST
  • scopes:
  • - CONTENT
  • - TOOL_RESULTS
  • patterns:
  • - secrets: {}
  • action: DENY
  • - type: EMAIL
  • action: REDACT
  • denyMessage: The request contains restricted content
  • action: DENY: sk-proj-… → 403
  • action: REDACT: jane@acme.com → [REDACTED:EMAIL]
  • LLM gateway, Model and reasoning: Decide the model and how much it may reason, per identity. openai.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: openai
  • spec:
  • mode: LLM
  • config:
  • upstream:
  • url: https://api.openai.com
  • llm:
  • protocol: OPENAI
  • model:
  • eval: '"ai-admins" in ctx.user.spec.groups ? "gpt-5" : "gpt-5-mini"'
  • reasoning:
  • level: MEDIUM
  • auth:
  • bearer:
  • fromSecret: openai-api-key
  • eval: '"ai-admins" in ctx.user.spec.groups ? "gpt-5" : "gpt-5-mini"': support-bot → gpt-5-mini
  • level: MEDIUM: reasoning_effort: medium
  • LLM gateway, Semantic routing and caching: Route by meaning and answer repeats from a per-User cache. openai.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: openai
  • spec:
  • mode: LLM
  • config:
  • upstream:
  • url: https://api.openai.com
  • llm:
  • protocol: OPENAI
  • auth:
  • bearer:
  • fromSecret: openai-api-key
  • embedding:
  • source:
  • currentUpstream: true
  • model: text-embedding-3-small
  • plugins:
  • - name: auto-model
  • condition:
  • match: ctx.request.llm.model == "auto"
  • semanticRouter:
  • fallbackModel: gpt-5-mini
  • routes:
  • - name: engineering
  • examples:
  • - Why is this Go program deadlocking?
  • model: gpt-5
  • - name: user-cache
  • condition:
  • match: ctx.request.llm.operation == "GENERATE"
  • semanticCache:
  • scope:
  • perUser: true
  • minSimilarity: 0.92
  • useXCacheHeader: true
  • - name: engineering: similarity 0.81
  • model: gpt-5: auto → gpt-5
  • minSimilarity: 0.92: 0.97
  • useXCacheHeader: true: X-Cache: HIT
  • LLM gateway, Token rate limiting: Sliding-window token budgets per User, Session or tenant. openai.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: openai
  • spec:
  • mode: LLM
  • config:
  • upstream:
  • url: https://api.openai.com
  • llm:
  • protocol: OPENAI
  • auth:
  • bearer:
  • fromSecret: openai-api-key
  • plugins:
  • - name: user-budget
  • condition:
  • match: ctx.request.llm.operation == "GENERATE"
  • tokenRateLimit:
  • key:
  • perUser: true
  • limit: 100000
  • window:
  • hours: 1
  • defaultOutputTokens: 2048
  • denyMessage: The hourly token budget has been exhausted
  • limit: 100000: 98,912 used
  • defaultOutputTokens: 2048: 2,048 reserved
  • denyMessage: The hourly token budget has been exhausted: 429 Too Many Requests
  • LLM gateway, Protocol translation: Serve the OpenAI API to agents from an Anthropic upstream. claude.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: claude
  • spec:
  • mode: LLM
  • config:
  • upstream:
  • url: https://api.anthropic.com
  • llm:
  • protocol: OPENAI
  • translation:
  • upstreamProtocol: ANTHROPIC
  • defaultMaxOutputTokens: 4096
  • auth:
  • custom:
  • header: x-api-key
  • value:
  • fromSecret: anthropic-api-key
  • protocol: OPENAI: /v1/chat/completions
  • upstreamProtocol: ANTHROPIC: /v1/messages
  • fromSecret: anthropic-api-key: injected upstream
  • MCP gateway, Tool-level access control: Authorize every method, tool and argument per identity. finance-mcp.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: finance-mcp
  • spec:
  • mode: MCP
  • config:
  • upstream:
  • url: http://10.0.5.18:8080/mcp
  • mcp:
  • endpoint: /mcp
  • auth:
  • bearer:
  • fromSecret: mcp-upstream-token
  • 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: '"finance-agents" in ctx.user.spec.groups'
  • - match: ctx.request.mcp.http.bodyMap.params.arguments.amount <= 1000
  • match: ctx.request.mcp.method == "tools/list": tools/list
  • - match: ctx.request.mcp.method == "tools/call": tools/call
  • - match: ctx.request.mcp.name == "transfer": transfer
  • - match: '"finance-agents" in ctx.user.spec.groups': finance-agents
  • - match: ctx.request.mcp.http.bodyMap.params.arguments.amount <= 1000: 25000
  • MCP gateway, Guardrails: Catch leaked secrets in arguments and injections in results. finance-mcp.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: finance-mcp
  • spec:
  • mode: MCP
  • config:
  • upstream:
  • url: http://10.0.5.18:8080/mcp
  • mcp:
  • endpoint: /mcp
  • auth:
  • bearer:
  • fromSecret: mcp-upstream-token
  • plugins:
  • - name: tool-content
  • condition:
  • match: ctx.request.mcp.method == "tools/call"
  • guardrail:
  • leg: BOTH
  • scopes:
  • - TOOL_ARGUMENTS
  • - TOOL_RESULTS
  • patterns:
  • - secrets: {}
  • action: DENY
  • - regex: '(?i)ignore (all )?previous instructions'
  • action: DENY
  • denyMessage: Restricted content was detected
  • - TOOL_ARGUMENTS: agent → server
  • - TOOL_RESULTS: server → agent
  • action: DENY: ghp_… in arguments
  • action: DENY: injection in a result
  • MCP gateway, Lua scripting: Rewrite JSON-RPC messages with the caller's identity. finance-mcp.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: finance-mcp
  • spec:
  • mode: MCP
  • config:
  • upstream:
  • url: http://10.0.5.18:8080/mcp
  • mcp:
  • endpoint: /mcp
  • auth:
  • bearer:
  • fromSecret: mcp-upstream-token
  • plugins:
  • - name: add-user
  • condition:
  • all:
  • of:
  • - match: ctx.request.mcp.method == "tools/call"
  • - match: ctx.request.mcp.name == "lookup"
  • lua:
  • inline: |
  • function onRequest(ctx)
  • local body = json.decode(octelium.req.getRequestBody())
  • body.params.arguments.userUID = ctx.user.metadata.uid
  • octelium.req.setRequestBody(json.encode(body))
  • end
  • body.params.arguments.userUID = ctx.user.metadata.uid: 5f0e8b2a-…
  • octelium.req.setRequestBody(json.encode(body)): re-validated
  • MCP gateway, Rate limiting: Bound tool calls per User and answer with JSON-RPC errors. finance-mcp.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: finance-mcp
  • spec:
  • mode: MCP
  • config:
  • upstream:
  • url: http://10.0.5.18:8080/mcp
  • mcp:
  • endpoint: /mcp
  • auth:
  • bearer:
  • fromSecret: mcp-upstream-token
  • plugins:
  • - name: user-rate
  • condition:
  • matchAny: true
  • rateLimit:
  • key:
  • perUser: true
  • limit: 100
  • window:
  • minutes: 2
  • statusCode: 429
  • body:
  • inline: '{"jsonrpc":"2.0","error":{"code":-32000,"message":"Rate limit exceeded"},"id":null}'
  • limit: 100: 100 used
  • statusCode: 429: 101st call
  • MCP gateway, Managed containers: Deploy and scale the MCP server itself inside the Cluster. finance-mcp.yaml reads as follows.
  • kind: Service
  • metadata:
  • name: finance-mcp
  • spec:
  • mode: MCP
  • config:
  • upstream:
  • container:
  • image: ghcr.io/acme/finance-mcp:1.4.0
  • port: 8080
  • replicas: 3
  • resourceLimit:
  • cpu:
  • millicores: 1000
  • memory:
  • megabytes: 2000
  • mcp:
  • endpoint: /mcp
  • replicas: 3: 3/3 ready
MCP exchangetools/call · transfer
  1. 01Protocol boundaryJSON-RPC · version · origin · size
  2. 02Identity policymethod · target · arguments
  3. 03Request guardrailPII · secrets · custom patterns
  4. 04Extension pluginsLua · ExtProc · schema · limits
  5. 05Dynamic routeupstream · credential · path
  6. 06Response guardrailtools · resources · prompts · results
Identityrelease-agent
Argumentamount ≤ 1000
DecisionEVALUATING
MCP server credential isolation

Keep MCP server credentials out of agents and applications

Clients authenticate to Octelium with their own identity. The gateway separately authenticates to the selected MCP server after access is allowed, so server credentials do not have to be distributed across agent runtimes, developer machines or automation systems.

Read about secretless access
bearer.yaml
Bearer or API keyKeep the MCP server credential in the Cluster and inject it only after access is allowed.
Bearer or API key. Keep the MCP server credential in the Cluster and inject it only after access is allowed.
mcp:
  auth:
    bearer:
      fromSecret: mcp-upstream-token

# Or a provider-specific header
# auth:
#   custom:
#     header: X-Api-Key
#     value:
#       fromSecret: mcp-api-key
OAuth2 client credentials. Acquire and use upstream access tokens without distributing the client secret to agents.
mcp:
  auth:
    oauth2ClientCredentials:
      clientID: octelium-mcp
      clientSecret:
        fromSecret: mcp-oauth-client-secret
      tokenURL: https://identity.example.com/oauth2/token
      scopes:
        - mcp.invoke
AWS Signature Version 4. Sign requests to an AWS-hosted MCP endpoint inside the gateway.
mcp:
  auth:
    sigv4:
      accessKeyID: AKIAEXAMPLE
      secretAccessKey:
        fromSecret: aws-secret-access-key
      region: us-east-1
      service: execute-api
protocol.yaml
Protocol boundaryEnforce accepted MCP revisions, explicit version declarations and known methods at the gateway boundary.
Protocol boundary. Enforce accepted MCP revisions, explicit version declarations and known methods at the gateway boundary.
spec:
  mode: MCP
  config:
    upstream:
      url: https://mcp.example.com/api/v2/mcp
    mcp:
      endpoint: /mcp
      protocol:
        versions:
          - "2025-11-25"
          - "2026-07-28"
        requireVersion: true
        rejectUnknownMethods: true
Origins and limits. Protect browser-reachable MCP endpoints with origin controls and explicit message-size limits.
mcp:
  cors:
    allowOriginStringMatch:
      - https://agent-console.example.com
    allowMethods: POST
    allowHeaders: Authorization, Content-Type, MCP-Protocol-Version
    exposeHeaders: Mcp-Session-Id
    maxAge: "3600"
    allowCredentials: true
  limits:
    maxRequestBytes: 524288
    maxStreamEventBytes: 131072
Headers, path and HTTP/2. Adapt headers, paths and HTTP/2 between clients and upstream MCP servers.
mcp:
  header:
    addRequestHeaders:
      - key: X-User-ID
        eval: ctx.user.metadata.uid
    removeRequestHeaders:
      - X-Untrusted-Header
    forwardedMode: DROP
    authorizationMode: DELETE
  path:
    removePrefix: /mcp
    addPrefix: /internal/mcp
  isUpstreamHTTP2: true
  listenHTTP2: true
MCP observability and audit

Trace MCP activity by identity, tool and outcome

Every MCP exchange can be tied to the authenticated Octelium identity that initiated it rather than to self-reported client metadata. Structured protocol-aware logs make tool use, server responses and failures visible to security and platform teams.

Read about MCP visibility
MCP visibilityLast 24 hours · all Services
UserMethodTool
Exchanges
48.2K31 Users · 9 Services
Tool calls
18.7K126 unique tools
Guardrails
28417 denied · 267 redacted
Errors
0.8%protocol + tool errors
Top callerresearch-agent38%
Top methodtools/call61%
Top targetsearch_docs8.4K
release-agent→tools/callproduction_deployPolicy allowGuardrail passcompleteHTTP2 · 386ms
maya@acme.com→resources/readdocs://runbooksSTREAM_END3 notificationsstream · 14 eventsSSE · 2.8s
finance-agent→tools/calltransferisToolErrorHTTP 200tool errorHTTP2 · 91ms
Identity + policy attachedOpenTelemetry OTLP

Cluster resources applied declaratively from YAML with octeliumctl and managed programmatically with the Go, TypeScript, Python and Rust SDKs

  • GitOps: cluster.yaml in the main branch of the ~/octelium repository describes the desired state of the Cluster. Its diff reads as follows, with + for added, ~ for changed and - for removed lines.
  • + kind: Group
  • + metadata:
  • + name: payments
  • + spec: {}
  • + ---
  • + kind: User
  • + metadata:
  • + name: alice
  • + spec:
  • + type: HUMAN
  • + email: alice@acme.com
  • + groups: ["payments"]
  • ---
  • kind: User
  • metadata:
  • name: bob
  • spec:
  • type: HUMAN
  • ~ groups: ["payments"]
  • ---
  • kind: Service
  • metadata:
  • name: billing
  • spec:
  • mode: HTTP
  • config:
  • upstream:
  • ~ url: http://10.0.3.24:8080
  • - ---
  • - kind: Service
  • - metadata:
  • - name: billing-legacy
  • - spec: ⋯
  • - ---
  • - kind: User
  • - metadata:
  • - name: john
  • - spec: ⋯
  • ~/octelium ❯ octeliumctl apply --prune .
  • Group: payments Created
  • User: alice Created
  • User: bob Updated
  • Service: billing.default Updated
  • Service: billing-legacy.default Deleted
  • User: john Deleted
  • Cluster Core resources successfully applied
  • 2 resources created
  • 2 resources updated
  • 2 resources deleted
  • Go: main.go uses the octelium-go SDK to list the Users of the Cluster and update the email and Policies of linus.
  • client, err := octelium.New(ctx)
  • if err != nil {⋯}
  • conn, err := client.Conn(ctx)
  • if err != nil {⋯}
  • api := corev1.NewMainServiceClient(conn)
  • users, err := api.ListUser(ctx, &corev1.ListUserOptions{})
  • if err != nil {⋯}
  • for _, u := range users.Items {
  • fmt.Println(u.Metadata.Name)
  • }
  • user, err := api.GetUser(ctx, &metav1.GetOptions{Name: "linus"})
  • if err != nil {⋯}
  • user.Spec.Email = "linus@acme.com"
  • user.Spec.Authorization = &corev1.User_Spec_Authorization{
  • Policies: []string{"db-readonly"},
  • }
  • if _, err := api.UpdateUser(ctx, user); err != nil {⋯}
  • fmt.Println("updated", user.Metadata.Name)
  • ~/app ❯ go run .
  • alice
  • bob
  • linus
  • updated linus
  • TypeScript: main.ts uses the @octelium/sdk SDK to list the Users of the Cluster and update the email and Policies of linus.
  • const client = await OcteliumClient.create({
  • domain: "acme.com",
  • auth: {⋯},
  • });
  • const users = await client.coreV1.listUser(ListUserOptions.create());
  • for (const u of users.response.items) {
  • console.log(u.metadata?.name);
  • }
  • const { response: user } = await client.coreV1.getUser(
  • GetOptions.create({ name: "linus" }),
  • );
  • user.spec!.email = "linus@acme.com";
  • user.spec!.authorization = User_Spec_Authorization.create({
  • policies: ["db-readonly"],
  • });
  • await client.coreV1.updateUser(user);
  • console.log("updated", user.metadata?.name);
  • await client.close();
  • ~/app ❯ npx tsx main.ts
  • alice
  • bob
  • linus
  • updated linus
  • Python: main.py uses the octelium-sdk SDK to list the Users of the Cluster and update the email and Policies of linus.
  • async with await OcteliumClient.create() as client:
  • users = await client.core_v1.list_user(ListUserOptions())
  • for u in users.items:
  • print(u.metadata.name)
  • user = await client.core_v1.get_user(GetOptions(name="linus"))
  • user.spec.email = "linus@acme.com"
  • user.spec.authorization.policies = ["db-readonly"]
  • await client.core_v1.update_user(user)
  • print("updated", user.metadata.name)
  • ~/app ❯ python main.py
  • alice
  • bob
  • linus
  • updated linus
  • Rust: main.rs uses the octelium SDK to list the Users of the Cluster and update the email and Policies of linus.
  • let client = Client::from_env().await?;
  • let mut api = client.core_v1();
  • let users = api.list_user(ListUserOptions::default()).await?;
  • for u in users.into_inner().items {
  • println!("{}", u.metadata.unwrap_or_default().name);
  • }
  • let options = GetOptions {
  • name: "linus".into(),
  • ..Default::default()
  • };
  • let mut user = api.get_user(options).await?.into_inner();
  • let spec = user.spec.get_or_insert_default();
  • spec.email = "linus@acme.com".into();
  • spec.authorization.get_or_insert_default().policies = vec!["db-readonly".into()];
  • let user = api.update_user(user).await?.into_inner();
  • println!("updated {}", user.metadata.unwrap_or_default().name);
  • ~/app ❯ cargo run -q
  • alice
  • bob
  • linus
  • updated linus
MCP gateway capabilities

Security and control across the MCP request path

Identity, protocol context, content controls, routing, credentials and audit data are evaluated within the same gateway request path, so each MCP action can be controlled and attributed consistently.

  1. 01

    Identity-aware access control

    Authorize MCP methods, tools, prompts, resources and parsed arguments using authenticated User, Group, Session and Device context with CEL or OPA policy.

    Read more
  2. 02

    Tool-level dynamic routing

    Select the MCP server, credential and request-processing configuration dynamically from the authenticated identity and normalized MCP request.

    Read more
  3. 03

    Bidirectional MCP guardrails

    Inspect tool arguments before they reach a server and inspect tool results, resources, prompts and tool definitions before they reach an agent.

    Read more
  4. 04

    MCP protocol enforcement

    Validate JSON-RPC messages, endpoints and protocol versions, control unknown methods, protect browser origins and enforce request and stream limits.

    Read more
  5. 05

    MCP server credential isolation

    Keep bearer tokens, API keys, basic credentials, OAuth2 client secrets and AWS signing credentials inside the Cluster rather than distributing them to agents.

    Read more
  6. 06

    Programmable request processing

    Add JSON Schema validation, request limits, Lua, Envoy ExtProc, direct responses and conditional path handling around protocol-aware controls.

    Read more
  7. 07

    Identity-aware MCP audit

    Record methods, targets, arguments and results, stream lifecycle, protocol errors and tool errors with authenticated identity and export them through OpenTelemetry.

    Read more
  8. 08

    Managed MCP servers

    Deploy and horizontally scale containerized MCP servers with Secrets, resources, volumes, security contexts and health probes on the same Kubernetes platform.

    Read more
Questions

Frequently asked

What makes Octelium an MCP-aware gateway?
Octelium parses and validates MCP JSON-RPC instead of treating it as opaque HTTP. Policies and dynamic configuration can use the normalized protocol version, method and logical target together with the complete parsed request body. Finite JSON responses and streamed SSE responses are supported.
How granular is MCP access control?
Access can be controlled at the JSON-RPC method, tool, prompt or resource level and can include values inside parsed arguments. For example, a Policy can allow tools/list for discovery while allowing tools/call only for specific identities, tools and argument constraints.
Does Octelium trust MCP client or session metadata as identity?
No. MCP client name, version, capabilities, request IDs and Mcp-Session-Id values are supplied by the downstream client and are treated as protocol and audit context, not authenticated identity. Security decisions use the authenticated Octelium User, Groups, Session and Device context.
Can different tools be routed to different MCP servers?
Yes. Dynamic configuration can choose the upstream MCP server, its credential and the applicable request-processing configuration from authenticated identity and MCP request context. This allows sensitive tools to be isolated behind dedicated servers and credentials while clients continue to use one controlled gateway.
What MCP guardrails are available?
Request guardrails inspect tool arguments for configured patterns, personal information and secrets and can deny, redact, strip or replace matching request content. Response guardrails inspect tool results, resource contents, prompt messages and tool definitions and can block restricted server-supplied content before it reaches the client.
Can Octelium validate or modify MCP traffic?
Yes. MCP Services can use JSON Schema validation, request rate limits, conditional path handling, Lua and Envoy-compatible ExtProc processing. Lua and ExtProc can intentionally rewrite JSON-RPC requests or responses; rewritten requests are parsed and validated again before later MCP controls run.
How are MCP server credentials handled?
The credential used by an agent to access Octelium is separate from the credential used to access the upstream MCP server. Bearer tokens, custom API keys, basic credentials, OAuth2 client credentials and AWS Signature Version 4 credentials can remain inside the Cluster and be applied after authorization. Downstream authorization credentials are not forwarded by default.
What can teams audit about MCP activity?
Identity-aware MCP logs can include the protocol version, JSON-RPC method, tool, prompt or resource target, request and response bodies, stream lifecycle, result type, protocol errors and tool errors together with the authenticated User, Session, Device, Service and Policy decision. Logs can be exported through OpenTelemetry OTLP.
Can Octelium run the MCP servers as well as protect them?
Yes. An MCP Service can proxy an existing server or deploy a containerized MCP server as a managed upstream. Managed servers can be horizontally scaled and configured with Secrets, resource limits, volumes, security contexts and health probes on the Kubernetes infrastructure running the Cluster.
Is Octelium open source and self-hosted?
Yes. Octelium is designed to run on Kubernetes infrastructure you control with no mandatory hosted control plane in the MCP data path. Cluster configuration can be managed declaratively with YAML and GitOps or programmatically through gRPC APIs and the Go, TypeScript, Rust and Python SDKs.
Get started

Run Octelium on your own infrastructure

Install a self-hosted Cluster and start applying identity-aware access control to your Services.