Skip to content

Secret persistence without an OS keychain: a file-backed (or explicitly in-memory) SecretStore for containers and unsupported platforms #1950

Description

@cliffhall

Problem

Two fixes in flight make the Inspector survive without an OS keychain, but neither answers what secret persistence should mean on those platforms:

After both, KeyringSecretStore on such a box is a store that silently holds nothing: get returns null, delete no-ops, and set hard-fails with KeychainUnavailableError (a 503 from the routes). That is the correct failure behavior — losing a secret silently would be worse — but it means the affected users have no way to persist an OAuth client secret or a stdio env: secret at all. In the container that is the documented, supported way to run the Inspector (ghcr.io/modelcontextprotocol/inspector), so it is not an edge case.

Raised by @dan-kwiat on #1848:

This restores non-secret flows in a container but doesn't answer what secret persistence should mean there. Since SecretStore is already an interface (with InMemorySecretStore alongside), an explicit file-backed or in-memory store may be a better container story than shipping a keyring daemon.

Why this is cheap to do

SecretStore (core/auth/node/secret-store.ts) is already an interface with two implementations — KeyringSecretStore and InMemorySecretStore — and the remote server factory already accepts a secretStore option (that is how the test suite swaps it out). So the work is a third implementation plus a selection policy, not a refactor.

What needs deciding

The implementation is the easy part; these are the actual questions:

  1. Selection policy. Explicit opt-in (an env var such as MCP_INSPECTOR_SECRET_STORE=file|memory|keyring), or automatic fallback when the keychain is unavailable? Automatic is friendlier but silently downgrades where the secret ends up living, which is exactly the kind of thing a security-adjacent tool should not do quietly.
  2. At rest. A file-backed store means secrets on disk. Plaintext with 0600 and a loud banner, or encrypted with a passphrase / key from the environment? Note Move oauthClientSecret out of mcp.json into the OS keychain #1356 moved oauthClientSecret out of mcp.json and into the keychain precisely to get it off disk — a file-backed store partially reverses that, so the decision should be explicit rather than incidental.
  3. Location. ~/.mcp-inspector/secrets.json is the obvious spot, but in a container that is ephemeral unless the user mounts a volume — which may make in-memory (secrets last for the session, no false promise of durability) the better container default, with file-backed as the opt-in for a mounted-volume setup.
  4. Surfacing it. Whichever store is active, the UI and the launcher banner should say so. A user typing a client secret deserves to know whether it lands in the OS keychain, a file, or RAM.

Suggested scope

  • A FileSecretStore implementing SecretStore, plus wiring to select the active store.
  • Selection surfaced in the web UI and the launcher banner.
  • Docs: a section in the container part of the root README, and the SecretStore doc comment.
  • Tests to the usual ≥90 per-file gate; the contract tests in clients/web/src/test/integration/auth/node/secret-store.test.ts are already written against the interface, so a third implementation can reuse the InMemorySecretStore contract block.

Notes

Filed at maintainer request as the follow-up carved out of #1848 and #1905. Not a blocker for either of those — both are complete on their own and should merge independently.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature requestv2Issues and PRs for v2

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions