Metadata-Version: 2.5
Name: secrets-mcp-server
Version: 0.1.0
Summary: MCP server for storing secrets locally, encrypted at rest, with opaque use (exec/write) so the value never reaches the calling agent
Project-URL: Homepage, https://github.com/KauaLealz/secrets-mcp-server
Project-URL: Repository, https://github.com/KauaLealz/secrets-mcp-server
Project-URL: Issues, https://github.com/KauaLealz/secrets-mcp-server/issues
License-Expression: MIT
License-File: LICENSE
Requires-Python: >=3.11
Requires-Dist: cryptography>=41.0.0
Requires-Dist: mcp<2.0.0,>=1.27.2
Description-Content-Type: text/markdown

# secrets-mcp-server

An MCP server for storing secrets (API keys, tokens, credentials) encrypted
at rest on your own machine, built so an AI agent can manage them **without
the plaintext value ever entering its context**. There is no `get_secret`
tool that returns a value as text — that was a deliberate design decision,
not an oversight. Using a secret is always indirect: run a command with the
value injected into its environment, or write the value straight into a
destination file.

## Why

Giving an agent free-form shell access to your `.env` files means every
secret it touches can end up echoed back into its own transcript, its logs,
or a chat history — anywhere from a debugging session to a support ticket.
This server keeps secrets in one encrypted store and only exposes them
through two narrow, auditable operations: inject-into-subprocess and
write-to-file. The agent can use a secret to authenticate a request or
populate a config file; it can never see, print, or leak the raw value
through a normal tool call.

## Tools

| Category | Tool | Description |
|---|---|---|
| CRUD | `create_secret(name, value, description=None)` | Creates a secret; fails if `name` already exists |
| CRUD | `create_secrets_batch(items)` | Creates several at once (`items: [{name, value, description?}]`). Does not abort on the first error — each item reports its own status (`created`/`skipped_exists`/`error`) |
| CRUD | `import_secrets_from_file(source_path, name_pattern=None, prefix=None, overwrite=False)` | Reads a `.env`-style file (`KEY=VALUE` per line, `export KEY=VALUE` and `#` comments supported) and imports each key as a secret. `name_pattern` (regex) filters which keys get imported; `prefix` is prepended to the name; `overwrite` controls whether existing secrets get updated |
| CRUD | `update_secret(name, value)` | Overwrites the value; fails if the secret doesn't exist |
| CRUD | `delete_secret(name)` | Removes a secret |
| CRUD | `list_secrets()` | Lists name/description/timestamps for every secret. **Never includes the value** |
| Opaque use | `run_with_secret(secret_name, command, env_var_name=None, cwd=None, timeout=None)` | Runs `command` (a list of args, no shell) with the value injected as an environment variable (`env_var_name`, or the secret name upper-cased by default) only inside that subprocess. Returns `exit_code`/`stdout`/`stderr`, with the value redacted if the command happens to print it |
| Opaque use | `apply_secrets_to_file(names, dest_path, key_names=None, format="env")` | Writes one or more secrets straight into `dest_path` (`env`-style `.env`, or `json`), never returning any value as text. `names` is always an explicit list — there is no "export everything" option |

`run_with_secret` never uses a shell (`shell=False`, `command` is a list of
args) — this avoids command injection even if an argument comes from
untrusted text.

`apply_secrets_to_file` only writes inside directories listed in
`SECRETS_MCP_ALLOWED_WRITE_DIRS` — without that configured, every write is
rejected.

`import_secrets_from_file` can read any file the process has OS permission
to read — this is a deliberate choice, with no read-side allowlist (unlike
writes). If you need to restrict that, add a
`SECRETS_MCP_ALLOWED_READ_DIRS` check following the same pattern as
`validate_dest_path` in `security.py`.

## Environment variables

| Variable | Default | Purpose |
|---|---|---|
| `SECRETS_MCP_MASTER_PASSPHRASE` | (required) | Passphrase used to derive the store's encryption key. Without it, the server refuses every operation |
| `SECRETS_MCP_STORE_PATH` | `~/.secrets-mcp/store.enc` | Where the encrypted store file lives |
| `SECRETS_MCP_ALLOWED_WRITE_DIRS` | (empty) | Comma-separated list of directories `apply_secrets_to_file` may write into. Empty means no writes are allowed |
| `SECRETS_MCP_COMMAND_TIMEOUT_SECONDS` | `30` | Timeout for `run_with_secret` (1-300) |

## Security model

- The store is a single file (`salt` + ciphertext) encrypted as a whole
  with Fernet (`cryptography`); the key is derived from the passphrase via
  Scrypt with a fresh random salt on every write. A wrong passphrase or a
  corrupted file fails loudly — there is no silent fallback.
- The store file and any file written by `apply_secrets_to_file` end up
  with `0600` permissions.
- `run_with_secret` never uses `shell=True`; `command` is always a list of
  args, never a shell string.
- `apply_secrets_to_file` resolves the destination path (`realpath`,
  following symlinks) and rejects anything outside
  `SECRETS_MCP_ALLOWED_WRITE_DIRS`.
- No tool logs or returns a raw value. `run_with_secret` does a best-effort
  redaction (`security.redact`) that strips literal occurrences of the
  value from `stdout`/`stderr` in case the command echoes it by accident —
  this is not a guarantee against every leak (e.g. a command that writes
  the value to a file outside this tool's control), but it covers the
  common case.

## Requirements

- Python 3.11+
- [`uv`](https://docs.astral.sh/uv/) to install dependencies and run the
  server

## Installation

From PyPI, no clone needed:

```
uvx secrets-mcp-server
```

or install it as a persistent CLI tool:

```
uv tool install secrets-mcp-server
# or: pipx install secrets-mcp-server
```

From source, for local development:

```
git clone https://github.com/KauaLealz/secrets-mcp-server.git
cd secrets-mcp-server
uv sync
```

## Registering with Claude Code

Using the published package (no clone required):

```
claude mcp add --scope user secrets \
  --env SECRETS_MCP_MASTER_PASSPHRASE=<your-passphrase> \
  --env SECRETS_MCP_ALLOWED_WRITE_DIRS=/path/to/your/projects \
  -- uvx secrets-mcp-server
```

Using a local clone instead:

```
claude mcp add --scope user secrets \
  --env SECRETS_MCP_MASTER_PASSPHRASE=<your-passphrase> \
  --env SECRETS_MCP_ALLOWED_WRITE_DIRS=/path/to/your/projects \
  -- uv run --directory /path/to/secrets-mcp-server secrets-mcp-server
```

Replace `/path/to/your/projects` with whichever directories
`apply_secrets_to_file` should be allowed to write into (comma-separated for
more than one), and `/path/to/secrets-mcp-server` with wherever you cloned
the repo, if using the local-clone form.

`--scope user` makes it available in every Claude Code session. Changing
the passphrase between registrations produces a different store (the
encryption key depends on it) — keep the same passphrase to keep accessing
an existing store, and store it somewhere safe (a password manager). There
is no recovery if you lose it.

A `.env.example` is included as a reference for every variable below — it
is not auto-loaded, it just documents the shape a `.env` for this project
would take if you build tooling around it.

## Registering with other MCP clients

Any MCP client that supports stdio servers can run this the same way:
launch `uvx secrets-mcp-server` (or `uv run --directory /path/to/secrets-mcp-server secrets-mcp-server`
for a local clone) with the environment variables above set in its process
environment. Check your client's documentation for how it declares stdio
MCP servers (e.g. a `mcpServers` entry in its config file).

## Tests

```
uv run pytest
```

## Contributing

Issues and pull requests are welcome. See `AGENTS.md` for the design
constraint this project is built around (no tool ever returns a raw secret
value) — please keep new tools consistent with it, or open an issue to
discuss before changing it.

## License

MIT — see `LICENSE`.
