Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed

- OAuth callback ports for the hosted CIMD changed from the contiguous range 13316–13325 to 6 non-contiguous ports (13316, 13163, 31316, 31613, 16133, 16313) so one unrelated process is less likely to claim all of them. `localhost` variants dropped from the CIMD's `redirect_uris` in favor of `127.0.0.1` only (per RFC 8252 §8.3, which recommends the IP literal to avoid DNS resolution ambiguity).
- Stdio (command-based) config entries are now skipped by default when connecting from a config file (`mcpc connect <file>`). Pass `--stdio` to include them. Single-entry connects (`mcpc connect file:entry @session`) are not affected.
- `restart` success message now notes that previous session state (resource subscriptions, pending notifications, async tasks) was lost, since explicit restart always creates a fresh MCP session
- `tasks-list` now shows a hint on how to start a new task (`mcpc @session tools-call <name> [args] --task`) when there are no active tasks
- `mcpc help <command>` now shows a "Did you mean?" suggestion when the command is unknown (e.g., `mcpc help tasks-gfet` → suggests `tasks-get`)
Expand Down
59 changes: 56 additions & 3 deletions src/cli/commands/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ import { getWallet } from '../../lib/wallets.js';
import chalk from 'chalk';
import { createLogger } from '../../lib/logger.js';
import { parseProxyArg } from '../parser.js';
import { loadConfig, listServers } from '../../lib/config.js';
import { loadConfig, listServers, isStdioEntry } from '../../lib/config.js';

const logger = createLogger('sessions');

Expand Down Expand Up @@ -911,23 +911,69 @@ export async function connectAllFromConfig(
noProfile?: boolean;
proxy?: string;
proxyBearerToken?: string;
stdio?: boolean;
x402?: boolean;
insecure?: boolean;
}
): Promise<void> {
const config = loadConfig(configFile);
const serverNames = listServers(config);
const allNames = listServers(config);

if (serverNames.length === 0) {
if (allNames.length === 0) {
throw new ClientError(`No servers found in config file: ${configFile}`);
}

// Filter out stdio entries unless --stdio is passed. Stdio entries execute
// arbitrary local commands via child_process.spawn(), so bulk-connect
// operations default to skipping them to mitigate supply-chain risk from
// malicious config files.
const stdioSkipped: string[] = [];
const serverNames = allNames.filter((name) => {
if (!options.stdio && isStdioEntry(config, name)) {
stdioSkipped.push(name);
return false;
}
return true;
});

if (serverNames.length === 0) {
if (options.outputMode === 'json') {
console.log(
formatOutput(
{
configFile,
results: [],
skipped: stdioSkipped.map((entry) => ({
entry,
sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
reason: 'stdio',
})),
},
'json'
)
);
return;
}
throw new ClientError(
`All ${allNames.length} server${allNames.length === 1 ? '' : 's'} in ${configFile} use stdio transport.\n` +
`Pass --stdio to include them: mcpc connect ${configFile} --stdio`
);
}

if (options.outputMode === 'human') {
console.log(
chalk.cyan(
`Connecting ${serverNames.length} server${serverNames.length === 1 ? '' : 's'} from ${configFile}...`
)
);
if (stdioSkipped.length > 0) {
console.log(
chalk.dim(
` skipping ${stdioSkipped.length} stdio server${stdioSkipped.length === 1 ? '' : 's'} ` +
`(${stdioSkipped.join(', ')}), pass --stdio to include`
)
);
}
}

// Prepare entries with deterministic session names derived from entry names.
Expand Down Expand Up @@ -1019,6 +1065,13 @@ export async function connectAllFromConfig(
status: r.status,
...(r.error && { error: r.error }),
})),
...(stdioSkipped.length > 0 && {
skipped: stdioSkipped.map((entry) => ({
entry,
sessionName: generateSessionName({ type: 'config', file: configFile, entry }),
reason: 'stdio',
})),
}),
},
'json'
)
Expand Down
6 changes: 6 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -441,6 +441,7 @@ Full docs: ${docsUrl}`
.option('--no-profile', 'Skip OAuth profile (connect anonymously)')
.option('--proxy <[host:]port>', 'Start proxy MCP server for session')
.option('--proxy-bearer-token <token>', 'Require authentication for access to proxy server')
.option('--stdio', 'Include stdio (command-based) servers when connecting from config files')
.option('--x402', 'Enable x402 auto-payment using the configured wallet')
.addHelpText(
'after',
Expand All @@ -462,6 +463,10 @@ ${chalk.bold('Stdio servers:')}
servers inherit a minimal env whitelist; forward extras (e.g.
NODE_EXTRA_CA_CERTS, HTTPS_PROXY) via the "env" block. Server stderr is
logged to ~/.mcpc/logs/bridge-<session>.log.

Bulk connects (\`mcpc connect <config-file>\`) skip stdio entries by
default; pass --stdio to include them. Single-entry connects are
unaffected.
${jsonHelp('`InitializeResult` object extended with `toolNames` and `_mcpc` metadata', '`{ protocolVersion, capabilities, serverInfo, instructions?, toolNames?, _mcpc }`', `${SCHEMA_BASE}#initializeresult`)}`
)
.action(async (server, sessionName, opts, command) => {
Expand Down Expand Up @@ -500,6 +505,7 @@ ${jsonHelp('`InitializeResult` object extended with `toolNames` and `_mcpc` meta
...(headers && { headers }),
...(opts.proxy && { proxy: opts.proxy as string }),
...(opts.proxyBearerToken && { proxyBearerToken: opts.proxyBearerToken as string }),
...(opts.stdio && { stdio: true }),
...(opts.x402 && { x402: opts.x402 as boolean }),
...(globalOpts.insecure && { insecure: true }),
});
Expand Down
8 changes: 8 additions & 0 deletions src/lib/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,3 +228,11 @@ export function validateServerConfig(config: ServerConfig): boolean {

return true;
}

/**
* Check whether a named entry in an MCP config uses the stdio transport
* (i.e. has a `command` field rather than a `url`).
*/
export function isStdioEntry(config: McpConfig, entryName: string): boolean {
return config.mcpServers[entryName]?.command !== undefined;
}
41 changes: 41 additions & 0 deletions test/unit/lib/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
getServerConfig,
validateServerConfig,
listServers,
isStdioEntry,
} from '../../../src/lib/config.js';
import { ClientError } from '../../../src/lib/errors.js';

Expand Down Expand Up @@ -170,6 +171,46 @@ describe('validateServerConfig', () => {
});
});

describe('isStdioEntry', () => {
it('returns true for entries with a command field', () => {
const config = {
mcpServers: {
local: { command: 'node', args: ['server.js'] },
},
};
expect(isStdioEntry(config, 'local')).toBe(true);
});

it('returns false for entries with a url field', () => {
const config = {
mcpServers: {
remote: { url: 'https://example.com' },
},
};
expect(isStdioEntry(config, 'remote')).toBe(false);
});

it('returns false for non-existent entries', () => {
const config = {
mcpServers: {
remote: { url: 'https://example.com' },
},
};
expect(isStdioEntry(config, 'missing')).toBe(false);
});

it('correctly distinguishes entries in a mixed config', () => {
const config = {
mcpServers: {
http: { url: 'https://example.com' },
stdio: { command: 'npx', args: ['-y', 'mcp-server'] },
},
};
expect(isStdioEntry(config, 'http')).toBe(false);
expect(isStdioEntry(config, 'stdio')).toBe(true);
});
});

describe('listServers', () => {
it('should list all server names', () => {
const config = {
Expand Down
Loading