Skip to content

0.10.x: honoring caller's XDG_CONFIG_HOME silently empties server registry when embedder uses XDG for a different downstream tool #184

Description

@ChrisBot2026

Summary

Starting with 0.10.x, mcporter strictly honors the caller process's XDG_CONFIG_HOME for its own config discovery and only looks at $XDG_CONFIG_HOME/mcporter/mcporter.json[c]. This silently breaks any embedder process that legitimately sets XDG_CONFIG_HOME to sandbox a different downstream tool's state — mcporter's server registry comes up empty and every mcporter call <server>.<tool> returns Unknown MCP server '<server>'., even though the user's ~/.mcporter/mcporter.json is fully populated and correct.

The behavior change is documented (Discovery & Precedence in dist/docs-site/config.html and the table noting "Unset, empty, or relative XDG vars fall back to ~/.mcporter for backwards compatibility"), but the failure mode for embedders that do set XDG for unrelated reasons is to silently fall back to whatever the caller's degraded path is — there is no warning that the registry came up empty.

Affected at least one shipping integration: openclaw's qmd-manager sandboxes QMD state per-agent via XDG_CONFIG_HOME=~/.openclaw/agents/<id>/qmd/xdg-config, then spawns mcporter call qmd.query … with that env. Worked fine on mcporter 0.7.3; silently regressed on the 0.10.2 upgrade. Diagnosed today after ~9 days of memory_search silently degrading to a legacy index without any user-visible error.

Versions

  • mcporter: 0.10.2 (installed from npm on macOS, 2026-05-11)
  • node: 25.5.0 (homebrew)
  • Embedder: openclaw 2026.5.18 (no env-related code changes since the May-7 baseline, verified against qmd-manager-D-Df6uBI.js snapshot)
  • mcporter version that last worked for this embedder: 0.7.3

Repro (clean shell)

~/.mcporter/mcporter.json contains a stdio server named qmd (any healthy stdio MCP server reproduces — substitute your own). The embedder sets XDG_CONFIG_HOME to a private dir that has nothing under mcporter/:

EMBEDDER_XDG=/tmp/embedder-private-xdg-$$
mkdir -p "$EMBEDDER_XDG"

# 1) Bare (no env override) — WORKS
mcporter call qmd.query --args '{"searches":[{"type":"lex","query":"x"}],"limit":1}' \
  --output json --timeout 15000
# → {"results":[...]}

# 2) Embedder env (XDG_CONFIG_HOME set to a dir with no mcporter/ under it) — FAILS
XDG_CONFIG_HOME="$EMBEDDER_XDG" \
  mcporter call qmd.query --args '{"searches":[{"type":"lex","query":"x"}],"limit":1}' \
  --output json --timeout 15000
# → {"server":"qmd","tool":"query","error":"Unknown MCP server 'qmd'.", ...}

# 3) Workaround: explicit MCPORTER_CONFIG (also documented) — WORKS again
MCPORTER_CONFIG=$HOME/.mcporter/mcporter.json \
  XDG_CONFIG_HOME="$EMBEDDER_XDG" \
  mcporter call qmd.query --args '{"searches":[{"type":"lex","query":"x"}],"limit":1}' \
  --output json --timeout 15000
# → {"results":[...]}

Code pointers

The strict XDG resolution lives in dist/paths.js mcporterConfigCandidates():

// dist/paths.js, lines 23–26
export function mcporterConfigCandidates() {
    const base = mcporterDir('config');
    return [path.join(base, 'mcporter.json'), path.join(base, 'mcporter.jsonc')];
}

…where mcporterDir('config') resolves to $XDG_CONFIG_HOME/mcporter when XDG_CONFIG_HOME is set:

// dist/paths.js, lines 4–22 (relevant excerpt)
const XDG_HOME_ENV = { config: 'XDG_CONFIG_HOME', ... };
// mcporterDir(kind) returns path.join(process.env[XDG_HOME_ENV[kind]], 'mcporter')
// when the env var is set to an absolute path; else legacyMcporterDir() (~/.mcporter)

dist/config/path-discovery.js then uses those candidates (homeConfigCandidates()mcporterConfigCandidates()) in both listConfigLayerPaths and resolveConfigPath — neither falls through to ~/.mcporter when the XDG-resolved candidate is missing.

Proposed fix (one of)

  1. Preferred: introduce a dedicated MCPORTER_HOME (or MCPORTER_CONFIG_HOME) env var that controls mcporter's own config-dir resolution independently from generic XDG. Caller can still set XDG_CONFIG_HOME for downstream tools without empty-registry'ing mcporter. Document that mcporter intentionally does not consume generic XDG.
  2. Alternative: transparent fall-through. If $XDG_CONFIG_HOME/mcporter/mcporter.json[c] does not exist, also probe ~/.mcporter/mcporter.json[c] before returning an empty registry. Preserves XDG-first semantics for users who actually deploy mcporter via XDG, restores backwards compatibility for everyone else.
  3. Minimum: emit a single warning on stderr when the in-process server registry is empty AND XDG_CONFIG_HOME is set but $XDG_CONFIG_HOME/mcporter/mcporter.json does not exist AND ~/.mcporter/mcporter.json does. Even if the call still fails, the user gets a clear pointer instead of Unknown MCP server 'X' with no breadcrumb.

Workaround (applied locally)

Embedder-side: ensure the env handed to mcporter sets MCPORTER_CONFIG explicitly. In our case, the patch was a 2-line addition to openclaw's qmd-manager dist file:

this.env = {
    ...process.env,
    PATH: buildQmdProcessPath(process.env.PATH),
    XDG_CONFIG_HOME: this.xdgConfigHome,
    QMD_CONFIG_DIR: path.join(this.xdgConfigHome, "qmd"),
    XDG_CACHE_HOME: this.xdgCacheHome,
    MCPORTER_CONFIG: process.env.MCPORTER_CONFIG
        ?? path.join(os.homedir(), ".mcporter", "mcporter.json"),
    NO_COLOR: "1"
};

This restores the pre-0.10 behavior for any embedder that sets a private XDG_CONFIG_HOME — but it'd be much nicer for downstream integrators if mcporter's own discovery decoupled from generic XDG so this kind of patch wasn't needed.

Impact

For our deployment, this silently degraded a memory-search path that routes through mcporter call qmd.query for ~9 days. The fallback path returned no error to the user, just lower-quality search results. The only signal was Unknown MCP server 'qmd'. in the embedder's error log — easy to miss when the embedder's overall error surface is busy.

Happy to test fixes or contribute a PR for option (1) or (2) above.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions