Skip to main content
Glama
Yormede
by Yormede
README.md
# safe-key-mcp

> **An MCP server for safe secret access by AI agents.**
> Secrets are stored encrypted. AI agents see only names and descriptions, never values.
> All actions are executed server-side. Outputs are scrubbed before being returned.

---

## Context — Why this project exists

Today, AI coding agents (Claude, GPT, Codex, etc.) increasingly need to interact with authenticated services: GitHub APIs, cloud providers, databases, SSH servers, email, etc.

The current solutions are problematic:

| Approach | Problem |
|----------|---------|
| Paste tokens in the chat | Token ends up in conversation history, logs, and LLM provider servers |
| Environment variables | AI can `echo $TOKEN` and see the value |
| `.env` files | AI can `cat .env` |
| 1Password MCP | Proprietary, paid, limited to 1Password users |

**The fundamental issue:** every existing approach eventually exposes the secret value to the AI as plaintext text — which gets sent to inference servers, stored in logs, and leaked in conversation history.

### The real threat model

The AI is not the enemy. The real risks are:

- **Accidental leaks** in logs, debug output, error messages
- **Prompt injection** from a malicious website extracting secrets
- **Conversation history** readable by developers or stored on LLM servers
- **Verbose tool output** that echoes back headers, URLs, or environment variables

We don't need military-grade sandboxing. We need **secrets to never appear in the AI's context window**.

---

## How it works

```
┌─────────────────────────────────────┐
│        safe-key-mcp server          │
│                                     │
│  vault.enc (AES-256-GCM encrypted) │
│  ┌─────────────────────────────┐    │
│  │ github_token = ghp_xxx      │    │
│  │ api_key      = sk-xxxxx     │    │
│  │ ssh_key_ovh  = -----BEGIN…  │    │
│  └─────────────────────────────┘    │
│                                     │
│  MCP Tools:                         │
│  • list_secrets() → names only      │
│  • http_request(secret, url, …)     │
│  • shell_exec(secret, template)     │
│  • ssh_exec(secret, host, cmd)      │
│                                     │
│  Output sanitizer:                  │
│  Scrubs all known secret values     │
│  from every response before sending │
└──────────────┬──────────────────────┘
               │
         [names only ↑]  [scrubbed output ↓]
               │
┌──────────────┴──────────────────────┐
│            AI Agent                  │
│  "Use github_token to GET /repos…"  │
│                                      │
│  Never sees: ghp_xxx                 │
│  Never sees: sk-xxxxx                │
│  Never sees: -----BEGIN…             │
└──────────────────────────────────────┘
```

### Encryption

- **AES-256-GCM** authenticated encryption
- Master password derived via **PBKDF2-HMAC-SHA256** (600,000 iterations)
- Each save uses a fresh random salt (16 bytes) and nonce (12 bytes)
- Vault file is a single binary blob — not human-readable

### Sanitizer

The output sanitizer is a **defence-in-depth** measure. It replaces any occurrence of a known secret value with `[REDACTED]` in all tool responses. This catches:

- Error messages that echo back a URL containing a token
- Verbose curl/HTTP output that includes headers
- Shell command output that accidentally prints environment variables

It is **not** a security boundary against a determined attacker with shell access.

---

## MCP Tools

| Tool | What the AI sends | What the AI receives |
|------|-------------------|---------------------|
| `list_secrets` | nothing | names + descriptions (no values) |
| `http_request` | `secret_name`, url, method, auth_style | HTTP response with values scrubbed |
| `shell_exec` | `secret_name`, command template with `{SECRET}` | stdout/stderr with values scrubbed |
| `ssh_exec` | `secret_name`, host, username, command | stdout/stderr with values scrubbed |

### `http_request` auth styles

- `bearer` — `Authorization: Bearer <value>`
- `basic_user` — HTTP Basic Auth (secret as username)
- `basic_password` — HTTP Basic Auth (secret as password)
- `header` — Custom header (specify `header_name`)
- `query_param` — URL query parameter (specify `param_name`)

### `shell_exec` template

The command must contain exactly one `{SECRET}` placeholder:

```
git clone https://user:{SECRET}@github.com/org/repo.git
```

The server replaces `{SECRET}` with the actual value, runs the command, and scrubs the output.

---

## Quick start

### Prerequisites

- Python 3.10+
- `pip install -e ".[dev]"`

### Add secrets (CLI)

```bash
export SAFE_KEY_MASTER_PASSWORD="your-strong-password"

# Interactive (password prompt, value never shown)
python -m safe_key_mcp add github_token -d "GitHub Personal Access Token" -t "ci,github"
python -m safe_key_mcp add openai_key -d "OpenAI API key" -t "llm"

# List (values never shown)
python -m safe_key_mcp list
```

### Run the server

```bash
export SAFE_KEY_MASTER_PASSWORD="your-strong-password"
python -m safe_key_mcp serve --host 127.0.0.1 --port 8500
```

### Docker

```bash
cp .env.example .env
# Edit SAFE_KEY_MASTER_PASSWORD in .env

docker compose up -d
# Server available at http://localhost:8500/sse
```

### Connect to your AI agent

Add to your MCP client config (e.g. OpenCode, Claude Desktop):

```json
{
  "mcpServers": {
    "safe-key": {
      "url": "http://localhost:8500/sse"
    }
  }
}
```

---

## Project structure

```
src/safe_key_mcp/
├── __init__.py       # Package docstring
├── __main__.py       # CLI: serve / add / list / delete
├── config.py         # Configuration via environment variables
├── vault.py          # AES-256-GCM encrypted storage
├── sanitizer.py      # Output scrubbing
└── server.py         # MCP server with 4 tools
tests/
├── conftest.py
├── test_vault.py     # 8 tests — encryption, persistence, auth
├── test_sanitizer.py # 6 tests — scrubbing, edge cases
└── test_server.py    # 3 tests — tool integration
```

---

## Environment variables

| Variable | Default | Description |
|----------|---------|-------------|
| `SAFE_KEY_MASTER_PASSWORD` | *(required)* | Master password for vault encryption |
| `SAFE_KEY_VAULT_PATH` | `./data/vault.enc` | Path to the encrypted vault file |
| `SAFE_KEY_HOST` | `0.0.0.0` | SSE server host |
| `SAFE_KEY_PORT` | `8500` | SSE server port |

---

## Status

> **This project is under active development and has not been tested in production.**

- Core vault encryption: working (17/17 tests passing)
- MCP tools: implemented, not yet tested with real services
- Docker deployment: ready, not yet deployed
- Sanitizer: functional, basic pattern matching

### What's next — evolution toward a Service Gateway

During development, we realized that this "vault + secret names" approach still has a conceptual flaw: **the AI knows that secrets exist**. It manipulates secret names, chooses auth styles, and builds authenticated requests — it's just one abstraction layer away from the values.

The next evolution of this project will be a **Service Gateway** where:

- The AI doesn't know secrets exist at all
- It just calls services: `service_call("github", "GET", "/repos/owner/repo")`
- Auth is resolved entirely server-side by config mapping
- The AI's mental model is "use this service", not "use this secret"

This is a fundamentally different approach — closer to how a browser handles cookies than how a developer handles API keys. That work will happen in a separate repository.

---

## License

MIT

Maintenance

ActivitySlowing
ResponsivenessNo issues