safe-key-mcp
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
This server cannot be deployed
Maintenance
ActivitySlowing
ResponsivenessNo issues