- Python 99.2%
- Shell 0.8%
Addresses all 4 issues from Dziadek's (ollama, DeepSeek-v4-pro) wide-review 2026-05-18: #1 - README stale vs code: README rewritten to match v0.2.0+ REST API architecture. Removed `brew install infisical` as a hard dependency (only needed for the user-login fallback path), removed `subprocess.run` narrative from § Security (single subprocess call left is the keychain lookup, documented as such), removed the phantom `infisical-mcp-discover-project-id` reference. #2 - per-cousin machine identity token path: new INFISICAL_MCP_COUSIN env var (default `claude`) selects which `<cousin>-client-secret` file under ~/.platformctl-runtime/infisical/ to read. Backward- compatible: when COUSIN=claude, the keychain account default stays `p@durlej.me` to match v0.2.0; for any other cousin it defaults to `p+<cousin>@durlej.me` per the operator-cousin email pattern in agent-souls' canonical runbook. Each cousin's MCP registration sets its own INFISICAL_MCP_COUSIN, so once per-cousin client-secret files exist the audit log distinguishes them automatically (the seam for follow-up #1 in infisical-machine-identity.md § Open follow-ups). #3 - project_id auto-discovery: `_resolve_project` now falls through to a `list_projects()` call (30s cached) when neither the tool arg nor INFISICAL_MCP_DEFAULT_PROJECT_ID is set. If exactly one workspace is visible, it's used automatically; any other count surfaces a helpful error pointing the agent at `list_projects`. `list_projects` itself now returns a `hint` field guiding the operator to set the env var when default is missing, with the exact UUID baked in for one-workspace setups. #4 - keychain user-login fallback warning: every tool response that used the user-login path now carries an `auth_warning` field naming both the operator account being audited as the actor AND the cousin identity that should have been used, plus the bootstrap path that would switch to machine identity. Models reading the response see this in-band so they can't accidentally assume audit isolation when none is in effect. Other: - Bumped version 0.2.0 -> 0.3.0 - Added COUSIN and INFISICAL_ACCOUNT to public __all__ - New env var INFISICAL_MCP_COUSIN documented in README + roadmap § per-cousin Universal Auth updated to reflect that the seam now exists. End-to-end verified (all three modes against home-platform workspace): - machine_identity (cousin=claude, default): no warning, no hint - auto-discovery (no DEFAULT_PROJECT_ID): list_projects emits the exact env-var-set hint; get_secret resolves the single workspace automatically and returns the right value - user_login fallback (forced via INFISICAL_MCP_RUNTIME_TOKEN=/nonexistent + INFISICAL_MCP_COUSIN=ollama): auth=user_login with explicit auth_warning naming "operator identity (p@durlej.me) ... NOT cousin identity (ollama)" Closes #1, #2, #3, #4. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com> |
||
|---|---|---|
| .gitignore | ||
| infisical-mcp | ||
| infisical_mcp.py | ||
| LICENSE | ||
| pyproject.toml | ||
| README.md | ||
infisical-mcp
Read-only MCP wrapper around Infisical's REST API so agents can pull secrets from a self-hosted Infisical instance instead of asking the operator for the same API keys every session.
What it gives you
Three read-only tools, defaulted to safe behavior:
| Tool | What it returns |
|---|---|
list_projects() |
Workspaces (projects) the current credentials can see. Useful for one-time INFISICAL_MCP_DEFAULT_PROJECT_ID discovery. |
list_secrets(project_id, env, path, recursive, mask_values=True) |
Secret names in a folder. Values are masked (***(42 chars)) by default — set mask_values=False only if you need to compare values en masse. |
get_secret(name, project_id, env, path) |
One secret's value. The only tool that returns plaintext — call it once you know which key you want. |
No write tools (set_secret / delete_secret) are exposed. The risk
profile of letting a long-running agent rotate or wipe production secrets
is too high for the convenience it would buy; if you need it, write it
explicitly in a separate utility, not through an MCP tool surface.
Every tool response includes an auth field with value
"machine_identity" or "user_login" naming which auth method was used,
so the model and operator can see at a glance whether the canonical path
is engaged or the workstation fallback kicked in.
Auth model (v0.2.0+)
Two methods with explicit precedence. The MCP calls Infisical's REST API
directly with urllib.request — there is no infisical CLI
subprocess anywhere on the hot path. The CLI is only relevant if you
want to bootstrap the user-login fallback (infisical login).
1. Machine identity Token Auth (preferred, canonical)
Reads the access token from
~/.platformctl-runtime/infisical/<cousin>-client-secret (default
cousin: claude; override via INFISICAL_MCP_COUSIN env var or the
full path via INFISICAL_MCP_RUNTIME_TOKEN) and uses it as a direct
Bearer per the runbook in
pdurlej/agent-souls:practices/infisical-machine-identity.md.
Audit log at Infisical shows the cousin's machine identity (currently shared "Instance Admin Identity" in 0.1 MVP; per-cousin Universal Auth is the documented follow-up). This is the right path for any identity-isolation-sensitive call.
Bootstrap the file once per workstation per cousin via the runbook
above (operator action, mode 0600). The MCP picks it up automatically
on the next call.
2. User-login JWT (fallback, identity-leaky)
Reads the JTWToken out of the macOS Keychain entry that infisical login maintains. Audit log shows the operator's email
(INFISICAL_MCP_ACCOUNT, default p@durlej.me), NOT the cousin.
This is honest for an interactive operator session but explicitly
forbidden by Rule 2 of the runbook for runtime agent work — so the MCP
only falls back here when the machine identity file is missing, and
attaches a warning field to the response so the model can see "you
are reading as the operator, not as the cousin".
To refresh the user JWT when it expires:
infisical login --domain=https://infisical.pdurlej.com/api
When neither is available
Every tool returns a structured error pointing at both bootstrap paths:
{
"error": "no infisical credentials available",
"hint": "Either run `infisical login ...` to refresh the user JWT, OR bootstrap the machine identity client secret file per pdurlej/agent-souls:practices/infisical-machine-identity.md.",
"checked": {
"machine_identity_path": "/Users/.../claude-client-secret",
"machine_identity_exists": false,
"cousin": "claude",
"keyring_account": "p@durlej.me"
}
}
The server never prompts interactively — that would hang stdio.
Token reads are not cached in process
Tokens are read from disk / Keychain on every API call. Rotations are picked up automatically, no MCP restart needed. (Result data — project list, secret list — IS cached for 30 seconds to avoid hammering Infisical, but credentials are not.)
Install
Requires:
- Network access to a self-hosted Infisical instance (the REST API at
INFISICAL_MCP_DOMAIN, defaulthttps://infisical.pdurlej.com/api). - Python 3.11+ with the
mcppackage on the import path. - macOS keychain access — only used if the user-login fallback path is
active. If you only use the machine identity path, the
infisicalCLI is not required at all.
git clone https://git.pdurlej.com/pdurlej/infisical-mcp.git ~/.local/share/infisical-mcp
ln -sf ~/.local/share/infisical-mcp/infisical-mcp ~/.local/bin/infisical-mcp
chmod +x ~/.local/bin/infisical-mcp
Configure
Set at minimum the default project (one-time):
# Discover the project ID with the MCP itself (after registration):
# call list_projects() once; the response lists your workspaces.
# Or grep the Infisical CLI's backup directory if you have it:
ls ~/.infisical/secrets-backup/ 2>/dev/null | head -1
# project ID is the UUID embedded in the backup file names, between
# "project_secrets_" and the next underscore.
export INFISICAL_MCP_DEFAULT_PROJECT_ID=<your-project-uuid>
Env vars (all optional):
| Var | Default | Purpose |
|---|---|---|
INFISICAL_MCP_DOMAIN |
https://infisical.pdurlej.com/api |
Self-hosted instance URL |
INFISICAL_MCP_DEFAULT_PROJECT_ID |
(empty) | Project UUID used when tool callers don't pass project_id |
INFISICAL_MCP_DEFAULT_ENV |
prod |
Environment slug |
INFISICAL_MCP_COUSIN |
claude |
Cousin identity — selects which <cousin>-client-secret file is read |
INFISICAL_MCP_RUNTIME_TOKEN |
~/.platformctl-runtime/infisical/<cousin>-client-secret |
Full override of the machine identity token path |
INFISICAL_MCP_ACCOUNT |
p+<cousin>@durlej.me (or p@durlej.me for cousin=claude to match v0.2.0) |
Keychain account for user-login fallback |
Register
Claude Code (user scope)
claude mcp add -s user infisical \
-e INFISICAL_MCP_DEFAULT_PROJECT_ID=<your-project-uuid> \
-e INFISICAL_MCP_COUSIN=claude \
-- ~/.local/bin/infisical-mcp
claude mcp list | grep infisical # → ✓ Connected
Codex CLI
codex mcp add infisical \
--env INFISICAL_MCP_DEFAULT_PROJECT_ID=<your-project-uuid> \
--env INFISICAL_MCP_COUSIN=codex \
~/.local/bin/infisical-mcp
Each cousin's MCP registration sets its own INFISICAL_MCP_COUSIN, so
the audit log at Infisical distinguishes them once per-cousin client
secret files are bootstrapped. Until then they all share the same
claude-client-secret (the runbook's 0.1 MVP), and INFISICAL_MCP_COUSIN
just controls the keychain-fallback email.
Usage from the model side
Typical flow when the agent needs a secret it doesn't have in context:
1. list_secrets(path="/home-platform/providers", mask_values=True)
→ sees ANTHROPIC_API_KEY, OPENAI_API_KEY, ... (names only)
2. get_secret(name="ANTHROPIC_API_KEY", path="/home-platform/providers")
→ gets the actual value
3. uses it; doesn't re-ask the operator next session.
Models can rely on INFISICAL_MCP_DEFAULT_PROJECT_ID for the project
and INFISICAL_MCP_DEFAULT_ENV (typically "prod") for the
environment, so typical calls collapse to:
get_secret(name="ANTHROPIC_API_KEY", path="/home-platform/providers")
Security
- Read-only tool surface. No mutation tools exist.
list_secretsmasks values by default; the model's transcript doesn't accumulate plaintext values from listing operations.- Tokens (machine identity file OR keychain JWT) are read fresh on
every API call — never cached in process memory, never written
anywhere. Token rotation by re-bootstrap or
infisical loginis picked up immediately. - API calls go through
urllib.requestwith parameterized URLs (urlencoded). Noshell=True, no string interpolation of user-controlled input into URLs. The only subprocess call is tosecurity find-generic-passwordfor the keychain fallback, with an explicitargvlist. - Project ID is validated as a non-empty string before being passed to the REST API.
- 30-second in-memory cache on
list_projectsandlist_secretsresults — short enough that secret rotations propagate quickly, long enough to avoid hammering Infisical when an agent loops. - When the user-login fallback is active, the response includes an
explicit
warningfield so the model knows it is reading as the operator, not as the cousin. Theauthfield always names the path taken.
Roadmap
- Per-cousin Universal Auth machine identities — tracked upstream
in
pdurlej/agent-souls:practices/infisical-machine-identity.md§ Open follow-ups § 1. TheINFISICAL_MCP_COUSINenv var is already the seam — once per-cousin client-secret files exist, set the env var per cousin's MCP registration and the audit log will distinguishclaudefromcodexfromollamaautomatically. - Universal-auth refresh — use the RefreshToken from Keychain to
auto-refresh the user JWT in-process when machine identity is
unavailable, so a brief expiry doesn't require manual
infisical login. Currently we just surface the hint. - Path completion — surface the folder tree via
list_folders()so agents can discover the structure without trial-and-error onpath. - Multi-environment search —
find_secret(name)that searches acrossdev/staging/prodand returns where the key lives.
License
MIT. See LICENSE.