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 @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Added

- `connect` command now auto-generates session name when `@session` is omitted (e.g., `mcpc connect mcp.apify.com` creates `@apify`). If a session for the same server already exists with matching auth settings, it is reused instead of creating a duplicate.
- `--max-chars <chars>` global option to truncate large tool/prompt/resource output
- `tools-call <tool> --help` shows tool parameter schema (shortcut for `tools-get`)
- "Did you mean?" suggestions for unknown commands, including reversed names (e.g., `list-tools` → `tools-list`)
Expand Down
47 changes: 24 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,8 @@ Usage: mcpc [<@session>] [<command>] [options]
Universal command-line client for the Model Context Protocol (MCP).

Commands:
connect <server> <@session> Connect to an MCP server and start a new named @session
connect <server> [@session] Connect to an MCP server and start a named @session (name
auto-generated if omitted)
close <@session> Close a session
restart <@session> Restart a session (losing all state)
shell <@session> Open interactive shell for a session
Expand Down Expand Up @@ -299,7 +300,7 @@ By default, `grep` searches only tools. Use `--resources` or `--prompts` to sear
(combine with `--tools` to include tools too). Sessions that are crashed or unavailable are shown
with their status rather than silently skipped.

The `grep` command is useful for **dynamic tool discovery**,
The `grep` command is useful for **dynamic tool discovery**,
also called [Tool search tool](https://www.anthropic.com/engineering/advanced-tool-use) by Anthropic
or [Dynamic context discovery](https://cursor.com/blog/dynamic-context-discovery) by Cursor.
Rather than loading all tools into AI agent's context, the agent can use `grep` to discover the right tool
Expand Down Expand Up @@ -358,9 +359,9 @@ Still, sessions can fail due to network disconnects, bridge process crash, or se

**Session states:**

| State | Meaning |
| --------------------- | -------------------------------------------------------------------------------------------------- |
| 🟢**`live`** | Bridge process running and server responding |
| State | Meaning |
| -------------------- | -------------------------------------------------------------------------------------------------- |
| 🟢**`live`** | Bridge process running and server responding |
| 🟡**`connecting`** | Initial bridge startup in progress (`mcpc connect`) |
| 🟡**`reconnecting`** | Bridge crashed or lost auth; auto-reconnecting in the background |
| 🟡**`disconnected`** | Bridge process running but server unreachable; auto-recovers when server responds |
Expand Down Expand Up @@ -786,10 +787,10 @@ mcpc x402 sign <base64-payment-required> --amount 1.00 --expiry 3600 --json

**Options:**

| Option | Description |
| ------------------- | ---------------------------------------------------------------- |
| `--amount <usd>` | Override the payment amount in USD (e.g. `0.50` for $0.50) |
| `--expiry <seconds>`| Override the payment expiry in seconds from now (e.g. `3600`) |
| Option | Description |
| -------------------- | ------------------------------------------------------------- |
| `--amount <usd>` | Override the payment amount in USD (e.g. `0.50` for $0.50) |
| `--expiry <seconds>` | Override the payment expiry in seconds from now (e.g. `3600`) |

The command outputs the signed `PAYMENT-SIGNATURE` header value and an MCP config snippet
that can be used directly with other MCP clients.
Expand Down Expand Up @@ -860,7 +861,7 @@ The bridge process manages the full MCP session lifecycle:
| 🔔 [**Notifications**](#list-change-notifications) | ✅ Supported |
| 📄 [**Pagination**](#pagination) | ✅ Supported |
| 🏓 [**Ping**](#ping) | ✅ Supported |
| ⏳ [**Async tasks**](#async-tasks) | ✅ Supported |
| ⏳ [**Async tasks**](#async-tasks) | ✅ Supported |
| 📁 **Roots** | 🚧 Planned |
| ❓ **Elicitation** | 🚧 Planned |
| 🔤 **Completion** | 🚧 Planned |
Expand Down Expand Up @@ -1266,19 +1267,19 @@ See [CONTRIBUTING](./CONTRIBUTING.md) for development setup, architecture overvi
<!-- Stars, contributors, commits, and activity as of March 2026. -->

| Tool | Lang | Stars | Contrib / Commits | Active | Tools | Resources | Prompts | Tasks | Code mode | Sessions | OAuth | Stdio | HTTP | Tool search | x402 | LLM |
| ----------------------------------------------------------------------- | ------ | ----: | -----------------: | ------ | ----- | --------- | ------- | ----- | --------- | -------- | ----- | ----- | ---- | ----------- | ---- | --- |
| **[apify/mcpc](https://github.com/apify/mcpc)** | TS | ~420 | 7 / ~510 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| [steipete/mcporter](https://github.com/steipete/mcporter) | TS | ~3.5k | 24 / ~570 | ✅ | ✅ | — | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | — |
| [IBM/mcp-cli](https://github.com/IBM/mcp-cli) | Python | ~1.9k | 22 / ~790 | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | ✅ |
| [knowsuchagency/mcp2cli](https://github.com/knowsuchagency/mcp2cli) | Python | ~1.8k | 5 / ~76 | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
| [f/mcptools](https://github.com/f/mcptools) | Go | ~1.5k | 15 / ~170 | ⚠️ | ✅ | ✅ | ✅ | — | ✅ | — | — | ✅ | ✅ | — | — | — |
| [philschmid/mcp-cli](https://github.com/philschmid/mcp-cli) | TS | ~1.1k | 2 / ~30 | ✅ | ✅ | — | — | — | ✅ | ✅ | — | ✅ | ✅ | ✅ | — | — |
| [adhikasp/mcp-client-cli](https://github.com/adhikasp/mcp-client-cli) | Python | ~670 | 6 / ~110 | ⚠️ | ✅ | ✅ | ✅ | — | — | — | — | ✅ | — | — | — | ✅ |
| [thellimist/clihub](https://github.com/thellimist/clihub) | Go | ~640 | 1 / ~60 | ✅ | ✅ | — | — | — | — | — | ✅ | ✅ | ✅ | ✅ | — | — |
| [wong2/mcp-cli](https://github.com/wong2/mcp-cli) | JS | ~430 | 4 / ~63 | ⚠️ | ✅ | ✅ | ✅ | — | — | — | ✅ | — | ✅ | — | — | — |
| [mcpshim/mcpshim](https://github.com/mcpshim/mcpshim) | Go | ~54 | 1 / ~13 | ✅ | ✅ | — | — | — | ✅ | ✅ | ✅ | — | ✅ | ✅ | — | — |
| [evantahler/mcpx](https://github.com/evantahler/mcpx) | TS | ~28 | 1 / ~64 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | — | — |
| [EstebanForge/mcp-cli-ent](https://github.com/EstebanForge/mcp-cli-ent) | Go | ~15 | ~2 / ~46 | ✅ | ✅ | — | — | — | ✅ | ✅ | — | ✅ | ✅ | ✅ | — | — |
| ----------------------------------------------------------------------- | ------ | ----: | ----------------: | ------ | ----- | --------- | ------- | ----- | --------- | -------- | ----- | ----- | ---- | ----------- | ---- | --- |
| **[apify/mcpc](https://github.com/apify/mcpc)** | TS | ~420 | 7 / ~510 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — |
| [steipete/mcporter](https://github.com/steipete/mcporter) | TS | ~3.5k | 24 / ~570 | ✅ | ✅ | — | — | — | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | — |
| [IBM/mcp-cli](https://github.com/IBM/mcp-cli) | Python | ~1.9k | 22 / ~790 | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ | — | — | ✅ |
| [knowsuchagency/mcp2cli](https://github.com/knowsuchagency/mcp2cli) | Python | ~1.8k | 5 / ~76 | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | — |
| [f/mcptools](https://github.com/f/mcptools) | Go | ~1.5k | 15 / ~170 | ⚠️ | ✅ | ✅ | ✅ | — | ✅ | — | — | ✅ | ✅ | — | — | — |
| [philschmid/mcp-cli](https://github.com/philschmid/mcp-cli) | TS | ~1.1k | 2 / ~30 | ✅ | ✅ | — | — | — | ✅ | ✅ | — | ✅ | ✅ | ✅ | — | — |
| [adhikasp/mcp-client-cli](https://github.com/adhikasp/mcp-client-cli) | Python | ~670 | 6 / ~110 | ⚠️ | ✅ | ✅ | ✅ | — | — | — | — | ✅ | — | — | — | ✅ |
| [thellimist/clihub](https://github.com/thellimist/clihub) | Go | ~640 | 1 / ~60 | ✅ | ✅ | — | — | — | — | — | ✅ | ✅ | ✅ | ✅ | — | — |
| [wong2/mcp-cli](https://github.com/wong2/mcp-cli) | JS | ~430 | 4 / ~63 | ⚠️ | ✅ | ✅ | ✅ | — | — | — | ✅ | — | ✅ | — | — | — |
| [mcpshim/mcpshim](https://github.com/mcpshim/mcpshim) | Go | ~54 | 1 / ~13 | ✅ | ✅ | — | — | — | ✅ | ✅ | ✅ | — | ✅ | ✅ | — | — |
| [evantahler/mcpx](https://github.com/evantahler/mcpx) | TS | ~28 | 1 / ~64 | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | — | ✅ | ✅ | ✅ | ✅ | — | — |
| [EstebanForge/mcp-cli-ent](https://github.com/EstebanForge/mcp-cli-ent) | Go | ~15 | ~2 / ~46 | ✅ | ✅ | — | — | — | ✅ | ✅ | — | ✅ | ✅ | ✅ | — | — |

**Legend:** ✅ = supported, ⚠️ = stale (no commits in 3+ months), **Contrib / Commits** = contributors / total commits, **Tasks** = [async tasks](https://modelcontextprotocol.io/specification/latest/server/utilities/tasks), **x402** = [x402 payment protocol](https://www.x402.org/) support, **LLM** = requires/uses an LLM.

Expand Down
112 changes: 112 additions & 0 deletions src/cli/commands/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ import { createServer } from 'net';
import {
OutputMode,
isValidSessionName,
generateSessionName,
normalizeServerUrl,
validateProfileName,
isProcessAlive,
getServerHost,
Expand All @@ -30,6 +32,7 @@ import {
updateSession,
consolidateSessions,
getSession,
loadSessions,
} from '../../lib/sessions.js';
import {
startBridge,
Expand Down Expand Up @@ -80,6 +83,115 @@ async function checkPortAvailable(host: string, port: number): Promise<boolean>
});
}

/**
* Find an existing session that matches the given server target and authentication settings.
* Used when auto-generating session names to reuse existing sessions instead of creating duplicates.
*
* @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 },
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;
}

/**
* Resolve the session name when @session is omitted from `mcpc connect`.
* Finds an existing matching session or generates a new unique name.
*
* @returns Session name with @ prefix
*/
export async function resolveSessionName(
parsed: { type: 'url'; url: string } | { type: 'config'; file: string; entry: string },
options: {
outputMode: OutputMode;
profile?: string;
headers?: string[];
noProfile?: boolean;
}
): Promise<string> {
// First, check if an existing session matches this server + auth settings
const existingName = await findMatchingSession(parsed, options);
if (existingName) {
return existingName;
}

// Generate a new session name
const candidateName = generateSessionName(parsed);

// Check if the candidate name is already taken by a different server
const storage = await loadSessions();
if (!(candidateName in storage.sessions)) {
if (options.outputMode === 'human') {
console.log(chalk.cyan(`Using session name: ${candidateName}`));
}
return candidateName;
}

// 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;
}
}

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`
);
}

/**
* Creates a new session, starts a bridge process, and instructs it to connect an MCP server.
* If session already exists with crashed bridge, reconnects it automatically
Expand Down
30 changes: 22 additions & 8 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -432,11 +432,13 @@ Run "mcpc" without arguments to show active sessions and OAuth profiles.
Full docs: ${docsUrl}`
);

// connect command: mcpc connect <server> @<name>
// connect command: mcpc connect <server> [@<name>]
program
.command('connect [server] [@session]')
.usage('<server> <@session>')
.description('Connect to an MCP server and start a new named @session')
.usage('<server> [@session]')
.description(
'Connect to an MCP server and start a named @session (name auto-generated if omitted)'
)
.option('-H, --header <header>', 'HTTP header (can be repeated)')
.option('--profile <name>', 'OAuth profile to use ("default" if skipped)')
.option('--no-profile', 'Skip OAuth profile (connect anonymously)')
Expand All @@ -449,6 +451,13 @@ Full docs: ${docsUrl}`
${chalk.bold('Server formats:')}
mcp.apify.com Remote HTTP server (https:// added automatically)
~/.vscode/mcp.json:puppeteer Config file entry (file:entry)

${chalk.bold('Session name:')}
If @session is omitted, a name is auto-generated from the server hostname
(e.g. mcp.apify.com → @apify) or config entry name. If a matching session
already exists (same server URL, OAuth profile, and HTTP header names), it
is reused (restarted if not live). Header values are not compared — they
are stored securely in OS keychain.
${jsonHelp('`InitializeResult` extended with `tools` and `_mcpc` metadata', '`{ protocolVersion, capabilities, serverInfo, instructions?, tools?, _mcpc }`', `${SCHEMA_BASE}#initializeresult`)}`
)
.action(async (server, sessionName, opts, command) => {
Expand All @@ -457,11 +466,6 @@ ${jsonHelp('`InitializeResult` extended with `tools` and `_mcpc` metadata', '`{
'Missing required argument: server\n\nExample: mcpc connect mcp.apify.com @myapp'
);
}
if (!sessionName) {
throw new ClientError(
'Missing required argument: @session\n\nExample: mcpc connect mcp.apify.com @myapp'
);
}
const globalOpts = getOptionsFromCommand(command);
const parsed = parseServerArg(server);

Expand All @@ -479,6 +483,16 @@ ${jsonHelp('`InitializeResult` extended with `tools` and `_mcpc` metadata', '`{
);
}

// Auto-generate session name if not provided
if (!sessionName) {
sessionName = await sessions.resolveSessionName(parsed, {
outputMode: globalOpts.outputMode,
...(globalOpts.profile && { profile: globalOpts.profile }),
...(headers && { headers }),
...(globalOpts.noProfile && { noProfile: globalOpts.noProfile }),
});
}

if (parsed.type === 'config') {
// Config file entry: pass entry name as target with config file path
await sessions.connectSession(parsed.entry, sessionName, {
Expand Down
Loading
Loading