Skip to content

Bug: GET api/servers Fails Within Headless Docker Container #1918

Description

@brandonja991

Summary

In any environment without a working OS/D-Bus secret-service (e.g. a headless Docker container — no gnome-keyring, no org.freedesktop.secrets), adding any server — even a plain Streamable HTTP server with no OAuth config — causes every subsequent GET /api/servers to fail with:

Failed to read server list: Couldn't access platform storage: PermissionDenied

Environment

  • @modelcontextprotocol/inspector@2.0.0
  • Reproduced in Docker, run with DANGEROUSLY_OMIT_AUTH=true, DANGEROUSLY_BIND_ALL_INTERFACES=true — no D-Bus/secret-service daemon present, as is typical for minimal/CI containers.

Steps to Reproduce

1. Build a minimal container

Create a Dockerfile:

FROM node:24-alpine

RUN npm install -g @modelcontextprotocol/inspector@2.0.0

ENV HOST=0.0.0.0 \
    DANGEROUSLY_BIND_ALL_INTERFACES=true \
    DANGEROUSLY_OMIT_AUTH=true \
    MCP_AUTO_OPEN_ENABLED=false \
    CLIENT_PORT=6274

EXPOSE 6274

CMD ["mcp-inspector", "--web"]

Build and run it:

docker build -t inspector-repro .
docker run --rm -d -p 6274:6274 --name inspector-repro inspector-repro

Confirm it's up by opening http://localhost:6274 in your browser (HTTP, not HTTPS) — you should see the Inspector UI with an empty server list.

2. Add any server via the UI

  1. Click Add ServerAdd Manually.
  2. Give it any name, e.g. test-server.
  3. Set transport to Streamable HTTP.
  4. Set the URL to any value, e.g. https://example.com/mcp.
  5. Save/submit the server.

Expected: the server list loads normally and shows test-server.

Actual: the server list fails to load. The underlying GET /api/servers request (visible in browser dev tools) returns:

HTTP/1.1 500 Internal Server Error

{"error":"Failed to read server list: Couldn't access platform storage: PermissionDenied\n\nCaused by:\n    PermissionDenied"}

Root Cause

  • expectedSecretFields() unconditionally includes the OAuth-client-secret field for every server, even ones with no OAuth config at all. This means rehydrateConfig()readKeychainEntriesFor()secretStore.get() runs a keychain lookup for every server on every GET /api/servers, regardless of whether that server could possibly have a stored secret.
  • In KeyringSecretStore.get(), new AsyncEntry(SERVICE_NAME, buildAccount(serverId, field)) is constructed outside the surrounding try/catch — only the subsequent await entry.getPassword() is guarded.
  • When there's no secret-service backend available, @napi-rs/keyring's AsyncEntry constructor throws synchronously, so it bypasses the KeychainUnavailableError-specific graceful-degradation path that already exists elsewhere in the codebase (e.g. in migratePlaintextSecrets()), and instead surfaces as a raw, unclassified error → generic 500 in the /api/servers route handler.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingduplicateThis issue or pull request already existskeyringRelated to system keychain accessv2Issues and PRs for v2

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions