v1 · MCP over Streamable HTTP · Go · MIT

Your agent gets the capability.
Never the credential.

Aegis is an MCP server that performs authenticated HTTP requests on behalf of an LLM — and keeps the API keys entirely to itself. Deny by default on the way out, redaction on the way back.

The problem

The model does not need your API key

It needs the result of an authenticated request. Handing over the key is a habit, not a requirement — and every place the key travels is a place it can leak.

Without Key in the agent

  • A prompt injection can exfiltrate it — the agent holds the key and can make requests
  • It ends up in transcripts, logs, screenshots and bug reports
  • All or nothing: an agent that should read invoices can delete them
  • A key on three machines is a key you will not rotate
  • No record of what was actually called in your name

With aegis Key in the container

  • The agent cannot send what it never had
  • Nothing to leak into a transcript — the credential never enters the context
  • Per target: allowed hosts, methods, path patterns and a rate limit
  • Rotate in one file, reload without a restart
  • One structured audit line per request, on stdout

How it works

A mediator, not a key ring

The agent describes the request it wants. Aegis decides whether it is permitted, attaches the credentials itself, and returns a response with every trace of them removed.

LLM agent Claude, any MCP client aegis :2019 match · rate limit inject · redact audit Your API Lexware, GitHub, ... config.yaml users · targets · secrets http_request redacted result + credentials response secrets never cross this line

Deny by default

A request goes out only if it matches a target: host, method and path pattern. Everything else is a 403, and nothing leaves the container.

Injection you declare once

The target says which header, query parameter or JSON field carries the secret. The model cannot influence it and never sees the result.

Reflection caught

Responses are scanned for every one of your secret values and replaced with [REDACTED:name]. An API that echoes your token cannot leak it.

OAuth 2.1 with a login

Clients register themselves, you sign in, and the token is the identity — it decides which targets are reachable and which secrets get attached.

Nothing on disk

Clients, codes and tokens live in memory only. A container full of credentials that writes nothing leaves nothing behind — run it read-only.

An audit line per request

Who, which target, which method, which status — and the names of the secrets that were attached. Never a value, at any log level.

In practice

One file to configure, two tools to call

Users are the top-level unit: each brings its own secrets and its own targets, so the security-relevant question — what exactly can this user do? — is answered by reading one block.

config.yaml
users:
  - name: andreas
    password: "bcrypt:$2a$12$..."

    secrets:
      github_pat: "env:GITHUB_PAT"

    targets:
      - id: github
        description: "GitHub REST API, read-only"
        base_url: "https://api.github.com"
        methods: [GET]
        paths:
          - "/repos/andreaskasper/**"
          - "/user"
        rate_limit: "120/m"
        inject:
          headers:
            Authorization: "Bearer ${github_pat}"
what the model sends and gets
-> http_request
{
  "url": "https://api.github.com/user"
}

<- result
{
  "status": 200,
  "target": "github",
  "body": "{\"login\":\"andreaskasper\"}",
  "truncated": false,
  "duration_ms": 143
}

# The Authorization header was added by aegis.
# The model never saw it, and cannot set it.
The other tool is list_targets. It tells the model which hosts, paths and methods are available to it — without revealing a single injection rule or secret name. Two tools, deliberately: a small surface is the point.

Quick start

Running in three steps

Write a config

Copy config.example.yaml, set a password hash and one target. Validate it before it ever runs.

shell
docker run --rm -it ghcr.io/andreaskasper/aegis hashpw
docker run --rm -v "$PWD/config.yaml:/etc/aegis/config.yaml:ro" \
  ghcr.io/andreaskasper/aegis validate /etc/aegis/config.yaml

Start the container

Port 2019, config mounted read-only. Aegis writes nothing, so lock the container down.

shell
docker run -d --name aegis -p 2019:2019 \
  -v "$PWD/config.yaml:/etc/aegis/config.yaml:ro" \
  -e AEGIS_PUBLIC_URL=https://aegis.example.com \
  --read-only --cap-drop ALL \
  ghcr.io/andreaskasper/aegis:latest

Point a client at it

Add https://aegis.example.com/mcp as an MCP server. The client discovers the OAuth endpoints, registers itself and opens the login page. Sign in, and the agent can call list_targets.

Put it behind TLS. Aegis speaks plain HTTP and does not terminate TLS — that belongs in front of the container. The deployment guide has working Compose setups for Traefik with Let's Encrypt, for a Cloudflare Tunnel, and for Cloudflare's proxy in front of Traefik.