Summary
mcporter resolves its config home and OAuth credential vault path through os.homedir() + '/.mcporter/' with no env-var or CLI override. This makes mcporter hard to deploy in containers and other environments where $HOME is shared across tenants or is on an ephemeral filesystem — the vault must live on a persisted/per-tenant volume, but mcporter doesn't expose a hook to point it there.
Proposal: honor the freedesktop.org XDG Base Directory Spec for config, data, cache, and state paths — read from $XDG_CONFIG_HOME/mcporter/, $XDG_DATA_HOME/mcporter/, $XDG_CACHE_HOME/mcporter/, and $XDG_STATE_HOME/mcporter/ when those env vars are set, fall back to today's ~/.mcporter/ when unset.
What's actually written under ~/.mcporter/
A source audit of main @ 324fb7a found seven distinct files written from five code paths, and eight separate occurrences of the literal os.homedir() + '.mcporter' with no shared helper. Categorical XDG kinds (per the spec) determine the right env var for each:
This codebase already has the right patterns — just not applied here
Two existing patterns make the proposed change consistent with current code, not foreign to it:
-
mcporter already honors XDG_CONFIG_HOME — for importing third-party MCP server configs at src/config/imports/paths.ts:39-40,84-85,107-114. Cursor and opencode imports check process.env.XDG_CONFIG_HOME before falling back to ~/.config/. The proposal is to extend that same pattern from third-party imports to mcporter's own state.
-
Category-specific override env vars already exist — src/daemon/paths.ts:6-11 accepts MCPORTER_DAEMON_DIR to redirect the daemon directory specifically. The XDG approach generalizes this pattern across all categories using standard env vars instead of one bespoke override per kind.
Why the existing knobs don't cover the use case
The config doc lists three redirection mechanisms (docs/config.md); none redirect the vault:
| Knob |
What it redirects |
Vault? |
MCPORTER_CONFIG env var |
Config file only ("only that file is used—no merging") |
No |
--config <path> flag |
Same as above |
No |
--root <dir> flag |
Project root for import discovery |
No |
tokenCacheDir per-server config field |
Token cache for that one server |
Partial — must be set per server, doesn't move the centralized vault |
MCPORTER_DAEMON_DIR env var |
Daemon socket / metadata / log dir |
No — daemon files only |
There's no process.env.HOME indirection that would let an operator set HOME for the mcporter process specifically — HOME belongs to the parent agent (in our case, OpenClaw, which spawns mcporter as a subprocess).
Why XDG over a bespoke MCPORTER_HOME
-
Standardized. Every Linux operator, container image, and packager already knows what XDG_CONFIG_HOME / XDG_DATA_HOME / XDG_STATE_HOME mean — no new tool-specific env var to discover and document.
-
Categorical separation. XDG splits state by kind, not by tool:
| Spec var |
Default |
What goes there |
mcporter use |
XDG_CONFIG_HOME |
~/.config |
User config files |
mcporter.json[c] |
XDG_DATA_HOME |
~/.local/share |
Durable user data |
credentials.json (OAuth vault) |
XDG_STATE_HOME |
~/.local/state |
Logs, history, transient state |
Daemon socket / metadata / log |
XDG_CACHE_HOME |
~/.cache |
Regenerable |
Per-server schema caches |
This matters in deployments: operators routinely place cache on tmpfs/local-SSD, data on backed-up volumes, and config under version control. XDG keeps each on its appropriate medium; a single tool-specific override would collapse them all into one bucket.
-
Composes with multi-tool environments. mcporter isn't the only Node-based CLI in many container images. If the operator already sets XDG vars at the container level for other tools, mcporter joining the convention means zero additional config — the same env vars cover everything.
-
Better defaults. Today's ~/.mcporter/ is a non-standard "dotfile in $HOME" that's increasingly discouraged on Linux (clutters $HOME, doesn't separate by kind). Moving to ~/.config/mcporter/ + ~/.local/share/mcporter/ aligns with where Linux users expect to find tool state.
Backwards compatibility
The standard pattern (adopted by gh, pnpm, direnv, yt-dlp, and many other Node/Go/Python CLIs) is:
- Honor XDG when explicitly set. If
$XDG_DATA_HOME (or $XDG_CONFIG_HOME, etc.) is non-empty, use $XDG_DATA_HOME/mcporter/ (etc.).
- Fall back to legacy
~/.mcporter/ when XDG vars are unset, so existing users see no change.
- (Optional, follow-up release) auto-detect an existing
~/.mcporter/ even when XDG vars are set, and either migrate-on-first-run or read-from-legacy-write-to-XDG, with a one-line stderr notice.
Step (1) alone is enough to unblock containerized/multi-tenant deployments, and is a fully backwards-compatible change — single-user dev environments are unaffected because XDG vars aren't set by default.
The vault file format ({ version: 1, entries: Record<VaultKey, VaultEntry> } at src/oauth-vault.ts:21-23) doesn't need to change — only the path it lives at.
MCPORTER_DAEMON_DIR should keep working as-is (it's already category-specific and operators may rely on it); when both are set, an explicit MCPORTER_DAEMON_DIR should win over XDG_STATE_HOME.
Use case
We pre-install mcporter in a container image that hosts a long-running OpenClaw agent on Cloudflare Sandbox Containers. The container image is shared across multiple tenant instances; per-tenant state (including OAuth tokens for MCP servers the agent has authenticated against) is persisted to a per-instance R2 bucket via an rclone sync loop scoped to a single state directory. The ~/.mcporter/ path lives outside that state directory, so without XDG support:
- Tokens persisted by mcporter on instance A are lost on the next container replacement (container filesystem is ephemeral).
- If
HOME were shared, tokens from instance A would leak into instance B (cross-tenant credential leak).
A filesystem symlink ($HOME/.mcporter → /persisted/state/mcporter) works as a workaround today (verified safe — expandHome at src/env.ts:8-20 and src/config/imports/paths-utils.ts:11-19 uses pure string substitution, no realpath), but it's fragile and breaks if mcporter ever switches to realpath-style resolution. With XDG support we'd set XDG_DATA_HOME=/persisted/state once at the container level and the vault would land there automatically — no symlink, no per-tool override, the same env vars covering every other XDG-aware tool in the image.
Minimal implementation sketch
Replace the eight os.homedir() + '.mcporter' literals with a single helper:
// src/paths.ts (new)
import os from 'node:os';
import path from 'node:path';
export type XdgKind = 'config' | 'data' | 'state' | 'cache';
const XDG_VARS: Record<XdgKind, string> = {
config: 'XDG_CONFIG_HOME',
data: 'XDG_DATA_HOME',
state: 'XDG_STATE_HOME',
cache: 'XDG_CACHE_HOME',
};
// Returns the directory mcporter writes for a given XDG kind. When the
// matching XDG_*_HOME var is set and non-empty, returns $XDG/mcporter; otherwise
// falls back to the legacy ~/.mcporter/ (preserves single-user dev behavior).
export function mcporterDir(kind: XdgKind): string {
const value = process.env[XDG_VARS[kind]];
if (value && value.trim() !== '') {
return path.join(value, 'mcporter');
}
return path.join(os.homedir(), '.mcporter');
}
Then at each existing call site:
// src/oauth-vault.ts:27
return path.join(mcporterDir('data'), 'credentials.json');
// src/schema-cache.ts:15
return definition.tokenCacheDir ?? path.join(mcporterDir('cache'), definition.name);
// src/oauth-persistence.ts:242,277 (legacy fallback — keep using os.homedir())
const legacyDir = path.join(os.homedir(), '.mcporter', definition.name);
// src/cli/config/add.ts:92
return path.join(mcporterDir('config'), 'mcporter.json');
// src/config/path-discovery.ts:58-59 and src/cli/config/shared.ts:130-131
const base = mcporterDir('config');
// src/daemon/paths.ts:10 (preserve MCPORTER_DAEMON_DIR override precedence)
return path.join(mcporterDir('state'), 'daemon');
Note: the legacy migration code in oauth-persistence.ts (the legacyDir = path.join(os.homedir(), '.mcporter', definition.name) lookup) keeps using os.homedir() directly — ~/.mcporter/ remains the right place to find legacy files even after the new default moves to XDG paths.
Related issues
- #139 — also reports headless/server deployment friction (can't see OAuth URL); same broader category of "mcporter assumes interactive single-user environment."
- #58 — long-running daemons; another use case where stable, redirectable on-disk state matters.
Summary
mcporter resolves its config home and OAuth credential vault path through
os.homedir() + '/.mcporter/'with no env-var or CLI override. This makes mcporter hard to deploy in containers and other environments where$HOMEis shared across tenants or is on an ephemeral filesystem — the vault must live on a persisted/per-tenant volume, but mcporter doesn't expose a hook to point it there.Proposal: honor the freedesktop.org XDG Base Directory Spec for config, data, cache, and state paths — read from
$XDG_CONFIG_HOME/mcporter/,$XDG_DATA_HOME/mcporter/,$XDG_CACHE_HOME/mcporter/, and$XDG_STATE_HOME/mcporter/when those env vars are set, fall back to today's~/.mcporter/when unset.What's actually written under
~/.mcporter/A source audit of
main @ 324fb7afound seven distinct files written from five code paths, and eight separate occurrences of the literalos.homedir() + '.mcporter'with no shared helper. Categorical XDG kinds (per the spec) determine the right env var for each:credentials.json(OAuth vault)src/oauth-vault.ts:27$XDG_DATA_HOME/mcporter/credentials.json<server>/schema.json(per-server schema cache)src/schema-cache.ts:15$XDG_CACHE_HOME/mcporter/<server>/schema.json<server>/...(legacy per-server token caches, read-only fallback for compat)src/oauth-persistence.ts:242,277$XDG_DATA_HOME/mcporter/<server>/mcporter.json[c](add --scope homewrite target)src/cli/config/add.ts:92$XDG_CONFIG_HOME/mcporter/mcporter.jsonmcporter.json[c](home discovery candidates)src/config/path-discovery.ts:58-60,src/cli/config/shared.ts:130-131$XDG_CONFIG_HOME/mcporter/mcporter.json[c]daemon/daemon-<key>.{sock,json,log}src/daemon/paths.ts:10$XDG_STATE_HOME/mcporter/daemon/...This codebase already has the right patterns — just not applied here
Two existing patterns make the proposed change consistent with current code, not foreign to it:
mcporter already honors
XDG_CONFIG_HOME— for importing third-party MCP server configs atsrc/config/imports/paths.ts:39-40,84-85,107-114. Cursor and opencode imports checkprocess.env.XDG_CONFIG_HOMEbefore falling back to~/.config/. The proposal is to extend that same pattern from third-party imports to mcporter's own state.Category-specific override env vars already exist —
src/daemon/paths.ts:6-11acceptsMCPORTER_DAEMON_DIRto redirect the daemon directory specifically. The XDG approach generalizes this pattern across all categories using standard env vars instead of one bespoke override per kind.Why the existing knobs don't cover the use case
The config doc lists three redirection mechanisms (docs/config.md); none redirect the vault:
MCPORTER_CONFIGenv var--config <path>flag--root <dir>flagtokenCacheDirper-server config fieldMCPORTER_DAEMON_DIRenv varThere's no
process.env.HOMEindirection that would let an operator setHOMEfor the mcporter process specifically —HOMEbelongs to the parent agent (in our case, OpenClaw, which spawns mcporter as a subprocess).Why XDG over a bespoke
MCPORTER_HOMEStandardized. Every Linux operator, container image, and packager already knows what
XDG_CONFIG_HOME/XDG_DATA_HOME/XDG_STATE_HOMEmean — no new tool-specific env var to discover and document.Categorical separation. XDG splits state by kind, not by tool:
XDG_CONFIG_HOME~/.configmcporter.json[c]XDG_DATA_HOME~/.local/sharecredentials.json(OAuth vault)XDG_STATE_HOME~/.local/stateXDG_CACHE_HOME~/.cacheThis matters in deployments: operators routinely place cache on tmpfs/local-SSD, data on backed-up volumes, and config under version control. XDG keeps each on its appropriate medium; a single tool-specific override would collapse them all into one bucket.
Composes with multi-tool environments. mcporter isn't the only Node-based CLI in many container images. If the operator already sets XDG vars at the container level for other tools, mcporter joining the convention means zero additional config — the same env vars cover everything.
Better defaults. Today's
~/.mcporter/is a non-standard "dotfile in$HOME" that's increasingly discouraged on Linux (clutters$HOME, doesn't separate by kind). Moving to~/.config/mcporter/+~/.local/share/mcporter/aligns with where Linux users expect to find tool state.Backwards compatibility
The standard pattern (adopted by
gh,pnpm,direnv,yt-dlp, and many other Node/Go/Python CLIs) is:$XDG_DATA_HOME(or$XDG_CONFIG_HOME, etc.) is non-empty, use$XDG_DATA_HOME/mcporter/(etc.).~/.mcporter/when XDG vars are unset, so existing users see no change.~/.mcporter/even when XDG vars are set, and either migrate-on-first-run or read-from-legacy-write-to-XDG, with a one-line stderr notice.Step (1) alone is enough to unblock containerized/multi-tenant deployments, and is a fully backwards-compatible change — single-user dev environments are unaffected because XDG vars aren't set by default.
The vault file format (
{ version: 1, entries: Record<VaultKey, VaultEntry> }atsrc/oauth-vault.ts:21-23) doesn't need to change — only the path it lives at.MCPORTER_DAEMON_DIRshould keep working as-is (it's already category-specific and operators may rely on it); when both are set, an explicitMCPORTER_DAEMON_DIRshould win overXDG_STATE_HOME.Use case
We pre-install mcporter in a container image that hosts a long-running OpenClaw agent on Cloudflare Sandbox Containers. The container image is shared across multiple tenant instances; per-tenant state (including OAuth tokens for MCP servers the agent has authenticated against) is persisted to a per-instance R2 bucket via an rclone sync loop scoped to a single state directory. The
~/.mcporter/path lives outside that state directory, so without XDG support:HOMEwere shared, tokens from instance A would leak into instance B (cross-tenant credential leak).A filesystem symlink (
$HOME/.mcporter → /persisted/state/mcporter) works as a workaround today (verified safe —expandHomeatsrc/env.ts:8-20andsrc/config/imports/paths-utils.ts:11-19uses pure string substitution, norealpath), but it's fragile and breaks if mcporter ever switches torealpath-style resolution. With XDG support we'd setXDG_DATA_HOME=/persisted/stateonce at the container level and the vault would land there automatically — no symlink, no per-tool override, the same env vars covering every other XDG-aware tool in the image.Minimal implementation sketch
Replace the eight
os.homedir() + '.mcporter'literals with a single helper:Then at each existing call site:
Note: the legacy migration code in
oauth-persistence.ts(thelegacyDir = path.join(os.homedir(), '.mcporter', definition.name)lookup) keeps usingos.homedir()directly —~/.mcporter/remains the right place to find legacy files even after the new default moves to XDG paths.Related issues