fix: fall back to legacy config after empty xdg home (#185)
This commit is contained in:
parent
82b19535d8
commit
67e3f5250f
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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 ??
|
||||
|
||||
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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'),
|
||||
]);
|
||||
});
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user