fix: respect stdio cwd config
This commit is contained in:
parent
e1a35f8d91
commit
7ffbd52bf7
@ -189,6 +189,7 @@ Server definition fields (subset of what `RawEntrySchema` accepts):
|
||||
| `description` | Free-form summary printed by `mcporter list`/`config list`. |
|
||||
| `baseUrl` / `url` / `serverUrl` | HTTPS or HTTP endpoint. `http://` requires `--allow-http` in ad-hoc mode but works in config if you explicitly set it. |
|
||||
| `command` / `args` | Stdio executable definition (string or array). Arrays are preferred because they avoid shell quoting issues. |
|
||||
| `cwd` | Working directory for stdio servers. A leading `~` is expanded to `$HOME`; relative paths resolve against the config file directory. Defaults to the config file directory when omitted. |
|
||||
| `env` | Key/value pairs applied when launching stdio commands. Supports `${VAR}` interpolation and `${VAR:-fallback}` defaults. Existing process env values win over fallbacks. |
|
||||
| `headers` | Request headers for HTTP/SSE transports. Values can reference `$env:VAR` or `${VAR}` placeholders, which must be set at runtime or mcporter aborts with a helpful error. |
|
||||
| `auth` | Currently only `oauth` is recognized. Any other string is ignored (treated as undefined) to avoid stale state from other clients. |
|
||||
|
||||
@ -59,6 +59,10 @@
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"cwd": {
|
||||
"description": "Working directory for stdio servers. A leading ~ is expanded to $HOME; relative paths resolve against the config file directory",
|
||||
"type": "string"
|
||||
},
|
||||
"headers": {
|
||||
"description": "HTTP headers for requests. Supports $VAR and $env:VAR placeholders",
|
||||
"type": "object",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import path from 'node:path';
|
||||
import type { CommandSpec, RawEntry, ServerDefinition, ServerLoggingOptions, ServerSource } from './config-schema.js';
|
||||
import { expandHome } from './env.js';
|
||||
import { resolveLifecycle } from './lifecycle.js';
|
||||
@ -36,7 +37,7 @@ export function normalizeServerEntry(
|
||||
kind: 'stdio',
|
||||
command: stdio.command,
|
||||
args: stdio.args,
|
||||
cwd: baseDir,
|
||||
cwd: resolveCwd(raw.cwd, baseDir),
|
||||
};
|
||||
} else {
|
||||
throw new Error(`Server '${name}' is missing a baseUrl/url or command definition in mcporter.json`);
|
||||
@ -93,6 +94,13 @@ function normalizePath(input: string | undefined): string | undefined {
|
||||
return expandHome(input);
|
||||
}
|
||||
|
||||
function resolveCwd(input: string | undefined, baseDir: string): string {
|
||||
if (!input) {
|
||||
return baseDir;
|
||||
}
|
||||
return path.resolve(baseDir, expandHome(input));
|
||||
}
|
||||
|
||||
function getUrl(raw: RawEntry): string | undefined {
|
||||
return raw.baseUrl ?? raw.base_url ?? raw.url ?? raw.serverUrl ?? raw.server_url ?? undefined;
|
||||
}
|
||||
|
||||
@ -62,6 +62,12 @@ export const RawEntrySchema = z
|
||||
.describe('Command to spawn for stdio transport (string or array of arguments)'),
|
||||
executable: z.string().optional().describe('Executable path for stdio transport'),
|
||||
args: z.array(z.string()).optional().describe('Arguments to pass to the stdio command'),
|
||||
cwd: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe(
|
||||
'Working directory for stdio servers. A leading ~ is expanded to $HOME; relative paths resolve against the config file directory'
|
||||
),
|
||||
headers: z
|
||||
.record(z.string(), z.string())
|
||||
.optional()
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
|
||||
const ENV_DEFAULT_PATTERN = /^\$\{([A-Za-z_][A-Za-z0-9_]*)(?::-|:|-)?([^}]*)\}$/;
|
||||
const ENV_INTERPOLATION_PATTERN = /\\?\$\{([A-Za-z_][A-Za-z0-9_]*)\}/g;
|
||||
@ -13,8 +14,8 @@ export function expandHome(input: string): string {
|
||||
if (input === '~') {
|
||||
return home;
|
||||
}
|
||||
if (input.startsWith('~/')) {
|
||||
return `${home}/${input.slice(2)}`;
|
||||
if (input.startsWith('~/') || input.startsWith('~\\')) {
|
||||
return path.join(home, input.slice(2));
|
||||
}
|
||||
return input;
|
||||
}
|
||||
|
||||
@ -36,6 +36,62 @@ describe('config normalization', () => {
|
||||
expect(headers?.accept?.toLowerCase()).toContain('text/event-stream');
|
||||
});
|
||||
|
||||
it('respects cwd on stdio servers', async () => {
|
||||
await fs.mkdir(TEMP_DIR, { recursive: true });
|
||||
const configPath = path.join(TEMP_DIR, 'mcporter-cwd.json');
|
||||
const absoluteCwd = path.join(os.tmpdir(), 'mcporter-cwd-absolute');
|
||||
await fs.mkdir(absoluteCwd, { recursive: true });
|
||||
await fs.writeFile(
|
||||
configPath,
|
||||
JSON.stringify(
|
||||
{
|
||||
mcpServers: {
|
||||
absolute: {
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
cwd: absoluteCwd,
|
||||
},
|
||||
relative: {
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
cwd: 'packages/foo',
|
||||
},
|
||||
tilde: {
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
cwd: '~/mcporter-cwd-home',
|
||||
},
|
||||
tildeBackslash: {
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
cwd: '~\\mcporter-cwd-home',
|
||||
},
|
||||
defaulted: {
|
||||
command: 'node',
|
||||
args: ['server.js'],
|
||||
},
|
||||
},
|
||||
},
|
||||
null,
|
||||
2
|
||||
),
|
||||
'utf8'
|
||||
);
|
||||
|
||||
const servers = await loadServerDefinitions({ configPath });
|
||||
const byName = new Map(servers.map((entry) => [entry.name, entry]));
|
||||
const cwdFor = (name: string): string | undefined => {
|
||||
const command = byName.get(name)?.command;
|
||||
return command?.kind === 'stdio' ? command.cwd : undefined;
|
||||
};
|
||||
|
||||
expect(cwdFor('absolute')).toBe(absoluteCwd);
|
||||
expect(cwdFor('relative')).toBe(path.resolve(TEMP_DIR, 'packages/foo'));
|
||||
expect(cwdFor('tilde')).toBe(path.join(os.homedir(), 'mcporter-cwd-home'));
|
||||
expect(cwdFor('tildeBackslash')).toBe(path.join(os.homedir(), 'mcporter-cwd-home'));
|
||||
expect(cwdFor('defaulted')).toBe(TEMP_DIR);
|
||||
});
|
||||
|
||||
it('normalizes oauthScope from camelCase and snake_case keys', async () => {
|
||||
await fs.mkdir(TEMP_DIR, { recursive: true });
|
||||
const configPath = path.join(TEMP_DIR, 'mcporter-oauth-scope.json');
|
||||
|
||||
Loading…
Reference in New Issue
Block a user