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)
- 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.
- 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.
- 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.
Summary
Starting with
0.10.x, mcporter strictly honors the caller process'sXDG_CONFIG_HOMEfor its own config discovery and only looks at$XDG_CONFIG_HOME/mcporter/mcporter.json[c]. This silently breaks any embedder process that legitimately setsXDG_CONFIG_HOMEto sandbox a different downstream tool's state — mcporter's server registry comes up empty and everymcporter call <server>.<tool>returnsUnknown MCP server '<server>'., even though the user's~/.mcporter/mcporter.jsonis fully populated and correct.The behavior change is documented (
Discovery & Precedenceindist/docs-site/config.htmland the table noting "Unset, empty, or relative XDG vars fall back to~/.mcporterfor 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-managersandboxes QMD state per-agent viaXDG_CONFIG_HOME=~/.openclaw/agents/<id>/qmd/xdg-config, then spawnsmcporter call qmd.query …with that env. Worked fine onmcporter 0.7.3; silently regressed on the0.10.2upgrade. Diagnosed today after ~9 days ofmemory_searchsilently degrading to a legacy index without any user-visible error.Versions
0.10.2(installed from npm on macOS, 2026-05-11)25.5.0(homebrew)openclaw 2026.5.18(no env-related code changes since the May-7 baseline, verified againstqmd-manager-D-Df6uBI.jssnapshot)0.7.3Repro (clean shell)
~/.mcporter/mcporter.jsoncontains a stdio server namedqmd(any healthy stdio MCP server reproduces — substitute your own). The embedder setsXDG_CONFIG_HOMEto a private dir that has nothing undermcporter/:Code pointers
The strict XDG resolution lives in
dist/paths.jsmcporterConfigCandidates():…where
mcporterDir('config')resolves to$XDG_CONFIG_HOME/mcporterwhenXDG_CONFIG_HOMEis set:dist/config/path-discovery.jsthen uses those candidates (homeConfigCandidates()→mcporterConfigCandidates()) in bothlistConfigLayerPathsandresolveConfigPath— neither falls through to~/.mcporterwhen the XDG-resolved candidate is missing.Proposed fix (one of)
MCPORTER_HOME(orMCPORTER_CONFIG_HOME) env var that controls mcporter's own config-dir resolution independently from generic XDG. Caller can still setXDG_CONFIG_HOMEfor downstream tools without empty-registry'ing mcporter. Document that mcporter intentionally does not consume generic XDG.$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.XDG_CONFIG_HOMEis set but$XDG_CONFIG_HOME/mcporter/mcporter.jsondoes not exist AND~/.mcporter/mcporter.jsondoes. Even if the call still fails, the user gets a clear pointer instead ofUnknown MCP server 'X'with no breadcrumb.Workaround (applied locally)
Embedder-side: ensure the env handed to mcporter sets
MCPORTER_CONFIGexplicitly. In our case, the patch was a 2-line addition to openclaw'sqmd-managerdist file: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.queryfor ~9 days. The fallback path returned no error to the user, just lower-quality search results. The only signal wasUnknown 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.