fix: fall back to legacy config after empty xdg home (#185)

This commit is contained in:
Peter Steinberger 2026-05-21 22:15:13 +01:00 committed by GitHub
parent 82b19535d8
commit 67e3f5250f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 61 additions and 7 deletions

View File

@ -2,7 +2,7 @@
## [0.11.3] - Unreleased
- Nothing yet.
- Fall back to `~/.mcporter/mcporter.json[c]` when `XDG_CONFIG_HOME` points at an empty mcporter config directory, preventing embedders from accidentally hiding the user server registry. (Issue #184, thanks @ChrisBot2026)
## [0.11.2] - 2026-05-21

View File

@ -489,7 +489,7 @@ mcporter reads exactly one primary config per run. The lookup order is:
1. The path you pass via `--config` (or programmatic `configPath`).
2. The `MCPORTER_CONFIG` environment variable (set it in your shell to apply everywhere).
3. `<root>/config/mcporter.json` inside the current project.
4. `$XDG_CONFIG_HOME/mcporter/mcporter.json[c]` when `XDG_CONFIG_HOME` is set, otherwise `~/.mcporter/mcporter.json[c]`, if the project file is missing.
4. `$XDG_CONFIG_HOME/mcporter/mcporter.json[c]` when `XDG_CONFIG_HOME` is set, falling back to `~/.mcporter/mcporter.json[c]` when no XDG mcporter config exists and the project file is missing.
All `mcporter config …` mutations write back to whichever file was selected by that order. To manage a system-wide config explicitly, point the CLI at it:
@ -499,7 +499,7 @@ mcporter config --config ~/.mcporter/mcporter.json add global-server https://api
Set `MCPORTER_CONFIG=~/.mcporter/mcporter.json` in your shell profile when you want that file to be the default everywhere (handy for `npx mcporter …` runs).
mcporter honors XDG Base Directory env vars for its own files when those vars are explicitly set: `XDG_CONFIG_HOME` for home configs, `XDG_DATA_HOME` for the OAuth vault, `XDG_CACHE_HOME` for schema caches, and `XDG_STATE_HOME` for daemon/runtime state. If the matching XDG var is unset or relative, mcporter keeps the legacy `~/.mcporter` path. Existing explicit overrides still win.
mcporter honors XDG Base Directory env vars for its own files when those vars are explicitly set: `XDG_CONFIG_HOME` for home configs, `XDG_DATA_HOME` for the OAuth vault, `XDG_CACHE_HOME` for schema caches, and `XDG_STATE_HOME` for daemon/runtime state. If the matching XDG var is unset or relative, mcporter keeps the legacy `~/.mcporter` path. Config discovery is XDG-first but still probes `~/.mcporter/mcporter.json[c]` when no XDG mcporter config exists, which keeps embedders from hiding the user registry when they set `XDG_CONFIG_HOME` for another tool. Existing explicit overrides still win.
### Tool Filtering

View File

@ -67,7 +67,7 @@ mcporter now merges home and project config files by default so global servers s
1. If you pass `--config <file>` (or set `--config` programmatically), only that file is used—no merging.
2. If `MCPORTER_CONFIG` is set, only that file is used—no merging.
3. Otherwise, mcporter loads both of these layers (when present):
- `$XDG_CONFIG_HOME/mcporter/mcporter.json[c]` when `XDG_CONFIG_HOME` is set, otherwise `~/.mcporter/mcporter.json[c]`
- `$XDG_CONFIG_HOME/mcporter/mcporter.json[c]` when `XDG_CONFIG_HOME` is set, falling back to `~/.mcporter/mcporter.json[c]` when no XDG mcporter config exists
- `<root>/config/mcporter.json`
Entries from the project file override entries with the same name from the home file. Each layer still pulls in its own imports before merging.
@ -82,7 +82,7 @@ mcporter honors XDG Base Directory env vars for its own paths when they are expl
| cache | `XDG_CACHE_HOME` | `$XDG_CACHE_HOME/mcporter/<server>/schema.json` | `~/.mcporter/...` |
| state | `XDG_STATE_HOME` | `$XDG_STATE_HOME/mcporter/daemon/...` | `~/.mcporter/daemon` |
Unset, empty, or relative XDG vars fall back to `~/.mcporter` for backwards compatibility. Explicit overrides still win: `--config`/`MCPORTER_CONFIG` for config files, `tokenCacheDir` for per-server OAuth/schema cache directories, and `MCPORTER_DAEMON_DIR` for daemon files.
Unset, empty, or relative XDG vars fall back to `~/.mcporter` for backwards compatibility. For config files only, an absolute `XDG_CONFIG_HOME` is XDG-first but still probes `~/.mcporter/mcporter.json[c]` when no XDG mcporter config exists, so embedders that sandbox unrelated tools with `XDG_CONFIG_HOME` do not accidentally hide the user's registry. Explicit overrides still win: `--config`/`MCPORTER_CONFIG` for config files, `tokenCacheDir` for per-server OAuth/schema cache directories, and `MCPORTER_DAEMON_DIR` for daemon files.
## Discovery & Precedence

View File

@ -53,7 +53,7 @@ export async function handleImportCommand(options: ConfigCliOptions, args: strin
}
}
if (flags.copy) {
const lockPath = resolveConfigPath(options.loadOptions.configPath, rootDir).path;
const lockPath = resolveImportCopyTarget(options.loadOptions.configPath, rootDir);
let configPath = lockPath;
await withFileLock(lockPath, async () => {
const loaded = await loadOrCreateConfig({ ...options.loadOptions, configPath: lockPath });
@ -71,6 +71,13 @@ export async function handleImportCommand(options: ConfigCliOptions, args: strin
}
}
function resolveImportCopyTarget(configPath: string | undefined, rootDir: string): string {
if (configPath || process.env.MCPORTER_CONFIG) {
return resolveConfigPath(configPath, rootDir).path;
}
return path.resolve(rootDir, 'config', 'mcporter.json');
}
function extractImportFlags(args: string[]): ImportFlags {
const flags: ImportFlags = { format: 'text' };
let index = 0;

View File

@ -28,5 +28,10 @@ export function mcporterDir(kind: McporterPathKind): string {
export function mcporterConfigCandidates(): string[] {
const base = mcporterDir('config');
return [path.join(base, 'mcporter.json'), path.join(base, 'mcporter.jsonc')];
const candidates = [path.join(base, 'mcporter.json'), path.join(base, 'mcporter.jsonc')];
if (base !== legacyMcporterDir()) {
const legacy = legacyMcporterDir();
candidates.push(path.join(legacy, 'mcporter.json'), path.join(legacy, 'mcporter.jsonc'));
}
return candidates;
}

View File

@ -145,6 +145,30 @@ describe('loadServerDefinitions with layered configs', () => {
expect(servers.map((server) => server.name)).toEqual(['fromHome']);
});
it('falls back to legacy home config when an embedder sets an unrelated empty XDG_CONFIG_HOME', async () => {
const homeDir =
tempHomeDir ??
(() => {
throw new Error('tempHomeDir missing');
})();
const projectDir =
tempProjectDir ??
(() => {
throw new Error('tempProjectDir missing');
})();
process.env.XDG_CONFIG_HOME = path.join(homeDir, 'embedder-private-xdg');
const legacyConfigDir = path.join(homeDir, '.mcporter');
await fs.mkdir(legacyConfigDir, { recursive: true });
await fs.writeFile(
path.join(legacyConfigDir, 'mcporter.json'),
JSON.stringify({ mcpServers: { qmd: { command: 'node', args: ['qmd-server.js'] } } }, null, 2)
);
const servers = await loadServerDefinitions({ rootDir: projectDir });
expect(servers.map((server) => server.name)).toEqual(['qmd']);
});
it('uses explicit config path without merging when set', async () => {
const homeDir =
tempHomeDir ??

View File

@ -115,4 +115,19 @@ describe('resolveConfigPath', () => {
expect(resolved.path).toBe(xdgConfigPath);
expect(resolved.explicit).toBe(false);
});
it('falls back to the legacy home config when XDG config home has no mcporter config', () => {
const tempRoot = makeTempDir('mcporter-project-empty-xdg-');
const fakeHome = makeTempDir('mcporter-empty-xdg-home-');
tempDirs.push(tempRoot, fakeHome);
const legacyConfigPath = path.join(fakeHome, '.mcporter', 'mcporter.json');
fs.mkdirSync(path.dirname(legacyConfigPath), { recursive: true });
fs.writeFileSync(legacyConfigPath, '{"mcpServers":{}}');
homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue(fakeHome);
process.env.XDG_CONFIG_HOME = path.join(fakeHome, '.empty-xdg-config');
const resolved = resolveConfigPath(undefined, tempRoot);
expect(resolved.path).toBe(legacyConfigPath);
expect(resolved.explicit).toBe(false);
});
});

View File

@ -29,6 +29,7 @@ describe('mcporter path helpers', () => {
});
it('honors absolute XDG homes by kind', () => {
homedirSpy = vi.spyOn(os, 'homedir').mockReturnValue('/home/tester');
process.env.XDG_CONFIG_HOME = '/xdg/config';
process.env.XDG_DATA_HOME = '/xdg/data';
process.env.XDG_STATE_HOME = '/xdg/state';
@ -41,6 +42,8 @@ describe('mcporter path helpers', () => {
expect(mcporterConfigCandidates()).toEqual([
path.join('/xdg/config', 'mcporter', 'mcporter.json'),
path.join('/xdg/config', 'mcporter', 'mcporter.jsonc'),
path.join('/home/tester', '.mcporter', 'mcporter.json'),
path.join('/home/tester', '.mcporter', 'mcporter.jsonc'),
]);
});