Skip to content
Open
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
2 changes: 2 additions & 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

- `--auto-restart` help text now clarifies that restarting loses session state
- **Breaking:** CLI syntax redesigned to command-first style. All commands now start with a verb; MCP operations require a named session.

| Before | After |
Expand Down Expand Up @@ -51,6 +52,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- `mcpc @session` now shows available tools list from bridge cache (no extra server call)
- Task capability and `execution.taskSupport` displayed in `tools-get` and server info
- x402 payments are now also sent via the MCP `_meta["x402/payment"]` field on `tools/call` requests, in addition to the existing HTTP header
- `--auto-restart` option for `connect` command to automatically restart expired sessions (server rejected session ID) instead of requiring manual `mcpc @session restart`

### Fixed

Expand Down
92 changes: 22 additions & 70 deletions src/cli/commands/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,12 @@ import {
consolidateSessions,
getSession,
} from '../../lib/sessions.js';
import { startBridge, StartBridgeOptions, stopBridge } from '../../lib/bridge-manager.js';
import {
startBridge,
StartBridgeOptions,
stopBridge,
restartBridge,
} from '../../lib/bridge-manager.js';
import {
storeKeychainSessionHeaders,
storeKeychainProxyBearerToken,
Expand Down Expand Up @@ -88,6 +93,7 @@ export async function connectSession(
proxyBearerToken?: string;
x402?: boolean;
insecure?: boolean;
autoRestart?: boolean;
}
): Promise<void> {
// Validate session name
Expand Down Expand Up @@ -233,6 +239,7 @@ export async function connectSession(
...(proxyConfig && { proxy: proxyConfig }),
...(options.x402 && { x402: true }),
...(options.insecure && { insecure: true }),
...(options.autoRestart && { autoRestart: true }),
// Clear any previous error status (unauthorized, expired) when reconnecting
...(isReconnect && { status: 'active' }),
};
Expand Down Expand Up @@ -595,86 +602,31 @@ export async function restartSession(
options: { outputMode: OutputMode; verbose?: boolean }
): Promise<void> {
try {
// Get existing session
// Verify session exists
const session = await getSession(name);

if (!session) {
throw new ClientError(`Session not found: ${name}`);
}

if (options.outputMode === 'human') {
console.log(chalk.yellow(`Restarting session ${name}...`));
}

// Stop the bridge (even if it's alive)
try {
await stopBridge(name);
} catch {
// Bridge may already be stopped
}

// Get server config from session
const serverConfig = session.server;
if (!serverConfig) {
if (!session.server) {
throw new ClientError(`Session ${name} has no server configuration`);
}

// Load headers from keychain if present
const { readKeychainSessionHeaders } = await import('../../lib/auth/keychain.js');
const headers = await readKeychainSessionHeaders(name);

// Start bridge process
const bridgeOptions: StartBridgeOptions = {
sessionName: name,
serverConfig: { ...serverConfig, ...(headers && { headers }) },
verbose: options.verbose || false,
};

if (headers) {
bridgeOptions.headers = headers;
}

// Resolve auth profile: use stored profile, or auto-detect a "default" profile.
// This handles the case where user creates a session without auth, then later runs
// `mcpc login <server>` to create a default profile, and restarts the session.
const hasExplicitAuthHeader = headers?.Authorization !== undefined;
let profileName = session.profileName;
if (!profileName && serverConfig.url && !hasExplicitAuthHeader) {
profileName = await resolveAuthProfile(serverConfig.url, serverConfig.url, undefined, {
sessionName: name,
});
if (profileName) {
logger.debug(`Discovered auth profile "${profileName}" for session ${name}`);
await updateSession(name, { profileName });
}
}

if (profileName) {
bridgeOptions.profileName = profileName;
}

if (session.proxy) {
bridgeOptions.proxyConfig = session.proxy;
}

if (session.x402) {
bridgeOptions.x402 = session.x402;
}

if (session.insecure) {
bridgeOptions.insecure = session.insecure;
if (options.outputMode === 'human') {
console.log(chalk.yellow(`Restarting session ${name}...`));
}

// NOTE: Do NOT pass mcpSessionId on explicit restart.
// Explicit restart should create a fresh session, not try to resume the old one.
// Session resumption is only attempted on automatic bridge restart (when bridge crashes
// and CLI detects it). If server rejects the session ID, session is marked as expired.

const { pid } = await startBridge(bridgeOptions);
// Delegate to restartBridge with freshSession=true to create a clean MCP session
// and re-discover auth profiles (handles the case where user ran `mcpc login` after connect)
await restartBridge(name, {
freshSession: true,
verbose: options.verbose || false,
resolveProfile: async (serverUrl, sessionName) => {
return resolveAuthProfile(serverUrl, serverUrl, undefined, { sessionName });
},
});

// Update session with new bridge PID and clear any expired/crashed status
await updateSession(name, { pid, status: 'active' });
logger.debug(`Session ${name} restarted with bridge PID: ${pid}`);
logger.debug(`Session ${name} restarted successfully`);

// Success message
if (options.outputMode === 'human') {
Expand Down
3 changes: 3 additions & 0 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -375,6 +375,7 @@ Full docs: ${docsUrl}`
.option('--proxy <[host:]port>', 'Start proxy MCP server for session')
.option('--proxy-bearer-token <token>', 'Require authentication for access to proxy server')
.option('--x402', 'Enable x402 auto-payment using the configured wallet')
.option('--auto-restart', 'Automatically restart session when it expires (loses state)')
.addHelpText(
'after',
`
Expand Down Expand Up @@ -421,6 +422,7 @@ ${chalk.bold('Server formats:')}
proxyBearerToken: opts.proxyBearerToken,
x402: opts.x402,
...(globalOpts.insecure && { insecure: true }),
...(opts.autoRestart && { autoRestart: true }),
});
} else {
await sessions.connectSession(server, sessionName, {
Expand All @@ -430,6 +432,7 @@ ${chalk.bold('Server formats:')}
proxyBearerToken: opts.proxyBearerToken,
x402: opts.x402,
...(globalOpts.insecure && { insecure: true }),
...(opts.autoRestart && { autoRestart: true }),
});
}
});
Expand Down
Loading
Loading