You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
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:
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.
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 oauthClientSecretout ofmcp.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.
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.
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.
Problem
Two fixes in flight make the Inspector survive without an OS keychain, but neither answers what secret persistence should mean on those platforms:
AsyncEntry::newthrows when no platform store is reachable (the published container has no D-Bus session). Now degrades instead of 500ingGET /api/servers.@napi-rs/keyringhas no prebuilt binary for Android / Termux, so the import itself threw. Now loaded lazily so startup survives.After both,
KeyringSecretStoreon such a box is a store that silently holds nothing:getreturnsnull,deleteno-ops, andsethard-fails withKeychainUnavailableError(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 stdioenv: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:
Why this is cheap to do
SecretStore(core/auth/node/secret-store.ts) is already an interface with two implementations —KeyringSecretStoreandInMemorySecretStore— and the remote server factory already accepts asecretStoreoption (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:
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.0600and a loud banner, or encrypted with a passphrase / key from the environment? Note Move oauthClientSecret out of mcp.json into the OS keychain #1356 movedoauthClientSecretout ofmcp.jsonand 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.~/.mcp-inspector/secrets.jsonis 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.Suggested scope
FileSecretStoreimplementingSecretStore, plus wiring to select the active store.SecretStoredoc comment.clients/web/src/test/integration/auth/node/secret-store.test.tsare already written against the interface, so a third implementation can reuse theInMemorySecretStorecontract 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.