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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `mcpc connect` now accepts an inline stdio command — quote the whole string (e.g., `mcpc connect "npx -y @modelcontextprotocol/server-filesystem /tmp"`) or use `--` after the session name (e.g., `mcpc connect @stdio -- node dist/stdio.js`). The session name is auto-generated from the binary basename with a numeric suffix (e.g., `@npx-1`, `@node-1`) when omitted; identical re-runs reuse the existing session. Auth flags (`--header`, `--profile`, `--no-profile`, `--x402`) are not allowed with inline commands.
- New `tasks-result <taskId>` command that fetches the final `CallToolResult` payload of an async task via the MCP `tasks/result` method. Blocks until the task reaches a terminal state, then prints the payload using the same renderer as `tools-call` (`--json` returns the raw result).

## [0.2.5] - 2026-04-15
Expand Down
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,10 @@ mcpc --json @test tools-list
# Use a local MCP server package (stdio) referenced from config file
mcpc connect ./.vscode/mcp.json:filesystem @fs
mcpc @fs tools-list

# Or spawn a local stdio server inline (no config file needed)
mcpc connect "npx -y @modelcontextprotocol/server-filesystem ${PWD}"
mcpc @npx-1 tools-list
```

## Usage
Expand Down Expand Up @@ -178,6 +182,7 @@ The `connect`, `login`, and `logout` commands accept a `<server>` argument in th

- **Remote URL** (e.g. `mcp.apify.com` or `https://mcp.apify.com`) — scheme defaults to `https://`
- **Config file entry** (e.g. `~/.vscode/mcp.json:filesystem`) — `file:entry-name` syntax
- **Inline stdio command** (e.g. `"npx -y @modelcontextprotocol/server-filesystem /tmp"`) — quote the whole command, or use `--` after the session name (e.g. `mcpc connect @fs -- node dist/stdio.js`). Your shell handles `${VAR}` expansion; `mcpc` does not substitute env vars in inline commands.

### MCP commands

Expand All @@ -193,6 +198,11 @@ mcpc @apify tools-call search-apify-docs query:="What are Actors?"
mcpc connect ~/.vscode/mcp.json:filesystem @fs
mcpc @fs tools-list
mcpc @fs tools-call list_directory path:=/

# Connect to a local stdio server inline (no config file)
mcpc connect "npx -y @modelcontextprotocol/server-filesystem ${PWD}"
mcpc connect @stdio -- node dist/stdio.js # explicit form via '--'
mcpc @npx-1 tools-list # auto-named from binary basename
```

See [MCP feature support](#mcp-feature-support) for details about all supported MCP features and commands.
Expand Down
103 changes: 36 additions & 67 deletions src/cli/commands/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {
OutputMode,
isValidSessionName,
generateSessionName,
normalizeServerUrl,
validateProfileName,
isProcessAlive,
getServerHost,
redactHeaders,
matchSessionByTarget,
pickAvailableSessionName,
} from '../../lib/index.js';
import { DISCONNECTED_THRESHOLD_MS } from '../../lib/types.js';
import type { ServerConfig, ProxyConfig } from '../../lib/types.js';
Expand Down Expand Up @@ -90,57 +91,15 @@ async function checkPortAvailable(host: string, port: number): Promise<boolean>
*
* @returns The matching session name (with @ prefix), or undefined if no match found
*/
async function findMatchingSession(
parsed: { type: 'url'; url: string } | { type: 'config'; file: string; entry: string },
export async function findMatchingSession(
parsed:
| { type: 'url'; url: string }
| { type: 'config'; file: string; entry: string }
| { type: 'command'; command: string; args: string[]; env?: Record<string, string> },
options: { profile?: string; headers?: string[]; noProfile?: boolean }
): Promise<string | undefined> {
const storage = await loadSessions();
const sessions = Object.values(storage.sessions);

if (sessions.length === 0) return undefined;

// Determine the effective profile name for comparison
const effectiveProfile = options.noProfile ? undefined : (options.profile ?? 'default');

for (const session of sessions) {
if (!session.server) continue;

// Match server target
if (parsed.type === 'url') {
if (!session.server.url) continue;
// Compare normalized URLs
try {
const existingUrl = normalizeServerUrl(session.server.url);
const newUrl = normalizeServerUrl(parsed.url);
if (existingUrl !== newUrl) continue;
} catch {
continue;
}
} else {
// Config entry: match by command (stdio transport)
// Config entries produce stdio configs with command/args, so we can't easily
// compare them. Instead, just compare generated session names for config targets.
// This is handled by the caller (resolveSessionName) via name-based dedup.
continue;
}

// Match profile
const sessionProfile = session.profileName ?? 'default';
if (effectiveProfile !== sessionProfile) continue;

// Match header keys (values are redacted, so we only compare key sets)
const existingHeaderKeys = Object.keys(session.server.headers || {}).sort();
const newHeaderKeys = (options.headers || [])
.map((h) => h.split(':')[0]?.trim() || '')
.filter(Boolean)
.sort();
if (existingHeaderKeys.join(',') !== newHeaderKeys.join(',')) continue;

// Found a match
return session.name;
}

return undefined;
return matchSessionByTarget(storage, parsed, options);
}

/**
Expand All @@ -150,7 +109,10 @@ async function findMatchingSession(
* @returns Session name with @ prefix
*/
export async function resolveSessionName(
parsed: { type: 'url'; url: string } | { type: 'config'; file: string; entry: string },
parsed:
| { type: 'url'; url: string }
| { type: 'config'; file: string; entry: string }
| { type: 'command'; command: string; args: string[]; env?: Record<string, string> },
options: {
outputMode: OutputMode;
profile?: string;
Expand All @@ -167,29 +129,29 @@ export async function resolveSessionName(
// Generate a new session name
const candidateName = generateSessionName(parsed);

// Check if the candidate name is already taken by a different server
// For inline commands, always append a numeric suffix (starting at -1) since the binary
// basename is rarely as distinctive as a hostname or config entry.
// For URL/config targets, try the bare name first, then -2, -3, ...
const storage = await loadSessions();
if (!(candidateName in storage.sessions)) {
const picked = pickAvailableSessionName(storage, candidateName, parsed.type === 'command');
if (picked) {
if (options.outputMode === 'human') {
console.log(chalk.cyan(`Using session name: ${candidateName}`));
console.log(chalk.cyan(`Using session name: ${picked}`));
}
return candidateName;
return picked;
}

// Name is taken - try suffixed variants
for (let i = 2; i <= 99; i++) {
const suffixed = `${candidateName}-${i}`;
if (isValidSessionName(suffixed) && !(suffixed in storage.sessions)) {
if (options.outputMode === 'human') {
console.log(chalk.cyan(`Using session name: ${suffixed}`));
}
return suffixed;
}
let targetDescription: string;
if (parsed.type === 'url') {
targetDescription = parsed.url;
} else if (parsed.type === 'config') {
targetDescription = `${parsed.file}:${parsed.entry}`;
} else {
targetDescription = [parsed.command, ...parsed.args].join(' ');
}

throw new ClientError(
`Cannot auto-generate session name: too many sessions for this server.\n` +
`Specify a name explicitly: mcpc connect ${parsed.type === 'url' ? parsed.url : `${parsed.file}:${parsed.entry}`} @my-session`
`Specify a name explicitly: mcpc connect ${targetDescription} @my-session`
);
}

Expand All @@ -214,6 +176,11 @@ export async function connectSession(
insecure?: boolean;
skipDetails?: boolean;
quiet?: boolean;
/**
* Pre-built ServerConfig (for inline stdio commands). When provided,
* resolveTarget() is skipped and this config is used directly.
*/
inlineServerConfig?: ServerConfig;
}
): Promise<void> {
// Validate session name
Expand Down Expand Up @@ -281,8 +248,10 @@ export async function connectSession(
}
}

// Resolve target to transport config
const serverConfig = await resolveTarget(target, options);
// Resolve target to transport config (or use the pre-built inline config for stdio commands)
const serverConfig = options.inlineServerConfig
? options.inlineServerConfig
: await resolveTarget(target, options);

// Detect conflicting auth flags: --profile and --header "Authorization: ..." are mutually exclusive
const hasExplicitAuthHeader = serverConfig.headers?.Authorization !== undefined;
Expand Down
Loading
Loading