Skip to content

Commit 57b3ba0

Browse files
jancurnclaude
andauthored
Auto-generate session names for connect command (#136)
* Auto-generate session names when @session is omitted from connect command The `mcpc connect <server>` command now auto-generates a session name from the server hostname (e.g. mcp.apify.com → @apify) or config entry name when the @session argument is omitted. If an existing session already matches the same server and auth settings, it is reused instead of creating a duplicate. https://claude.ai/code/session_01SRw2NArnxDKmgZLrNL72c6 * Fix CI: TypeScript exactOptionalPropertyTypes and ESLint non-null assertion - Cast opts.profile and opts.noProfile with explicit types to satisfy exactOptionalPropertyTypes (consistent with existing patterns in file) - Replace labels[0]! with nullish coalescing to satisfy no-non-null-assertion https://claude.ai/code/session_01SRw2NArnxDKmgZLrNL72c6 * Fix exactOptionalPropertyTypes: use globalOpts spread instead of raw opts Use the properly-typed HandlerOptions from getOptionsFromCommand() instead of casting raw Commander opts, consistent with how connectSession receives its options via ...globalOpts spread. https://claude.ai/code/session_01SRw2NArnxDKmgZLrNL72c6 * Fix exactOptionalPropertyTypes: conditionally spread optional fields Build resolveSessionName options with conditional spreads to avoid passing undefined values to optional properties, matching the pattern used by connectSession (e.g. `...(headers && { headers })`). https://claude.ai/code/session_01SRw2NArnxDKmgZLrNL72c6 * Writing * Clarify connect --json output includes _mcpc metadata and tools extension https://claude.ai/code/session_01VizmKUa1PuphPcHMJ21Mys --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent cccf417 commit 57b3ba0

File tree

6 files changed

+342
-31
lines changed

6 files changed

+342
-31
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
99

1010
### Added
1111

12+
- `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.
1213
- `--max-chars <chars>` global option to truncate large tool/prompt/resource output
1314
- `tools-call <tool> --help` shows tool parameter schema (shortcut for `tools-get`)
1415
- "Did you mean?" suggestions for unknown commands, including reversed names (e.g., `list-tools``tools-list`)

README.md

Lines changed: 24 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,8 @@ Usage: mcpc [<@session>] [<command>] [options]
111111
Universal command-line client for the Model Context Protocol (MCP).
112112
113113
Commands:
114-
connect <server> <@session> Connect to an MCP server and start a new named @session
114+
connect <server> [@session] Connect to an MCP server and start a named @session (name
115+
auto-generated if omitted)
115116
close <@session> Close a session
116117
restart <@session> Restart a session (losing all state)
117118
shell <@session> Open interactive shell for a session
@@ -299,7 +300,7 @@ By default, `grep` searches only tools. Use `--resources` or `--prompts` to sear
299300
(combine with `--tools` to include tools too). Sessions that are crashed or unavailable are shown
300301
with their status rather than silently skipped.
301302

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

359360
**Session states:**
360361

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

787788
**Options:**
788789

789-
| Option | Description |
790-
| ------------------- | ---------------------------------------------------------------- |
791-
| `--amount <usd>` | Override the payment amount in USD (e.g. `0.50` for $0.50) |
792-
| `--expiry <seconds>`| Override the payment expiry in seconds from now (e.g. `3600`) |
790+
| Option | Description |
791+
| -------------------- | ------------------------------------------------------------- |
792+
| `--amount <usd>` | Override the payment amount in USD (e.g. `0.50` for $0.50) |
793+
| `--expiry <seconds>` | Override the payment expiry in seconds from now (e.g. `3600`) |
793794

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

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

12831284
**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.
12841285

src/cli/commands/sessions.ts

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import { createServer } from 'net';
66
import {
77
OutputMode,
88
isValidSessionName,
9+
generateSessionName,
10+
normalizeServerUrl,
911
validateProfileName,
1012
isProcessAlive,
1113
getServerHost,
@@ -30,6 +32,7 @@ import {
3032
updateSession,
3133
consolidateSessions,
3234
getSession,
35+
loadSessions,
3336
} from '../../lib/sessions.js';
3437
import {
3538
startBridge,
@@ -80,6 +83,115 @@ async function checkPortAvailable(host: string, port: number): Promise<boolean>
8083
});
8184
}
8285

86+
/**
87+
* Find an existing session that matches the given server target and authentication settings.
88+
* Used when auto-generating session names to reuse existing sessions instead of creating duplicates.
89+
*
90+
* @returns The matching session name (with @ prefix), or undefined if no match found
91+
*/
92+
async function findMatchingSession(
93+
parsed: { type: 'url'; url: string } | { type: 'config'; file: string; entry: string },
94+
options: { profile?: string; headers?: string[]; noProfile?: boolean }
95+
): Promise<string | undefined> {
96+
const storage = await loadSessions();
97+
const sessions = Object.values(storage.sessions);
98+
99+
if (sessions.length === 0) return undefined;
100+
101+
// Determine the effective profile name for comparison
102+
const effectiveProfile = options.noProfile ? undefined : (options.profile ?? 'default');
103+
104+
for (const session of sessions) {
105+
if (!session.server) continue;
106+
107+
// Match server target
108+
if (parsed.type === 'url') {
109+
if (!session.server.url) continue;
110+
// Compare normalized URLs
111+
try {
112+
const existingUrl = normalizeServerUrl(session.server.url);
113+
const newUrl = normalizeServerUrl(parsed.url);
114+
if (existingUrl !== newUrl) continue;
115+
} catch {
116+
continue;
117+
}
118+
} else {
119+
// Config entry: match by command (stdio transport)
120+
// Config entries produce stdio configs with command/args, so we can't easily
121+
// compare them. Instead, just compare generated session names for config targets.
122+
// This is handled by the caller (resolveSessionName) via name-based dedup.
123+
continue;
124+
}
125+
126+
// Match profile
127+
const sessionProfile = session.profileName ?? 'default';
128+
if (effectiveProfile !== sessionProfile) continue;
129+
130+
// Match header keys (values are redacted, so we only compare key sets)
131+
const existingHeaderKeys = Object.keys(session.server.headers || {}).sort();
132+
const newHeaderKeys = (options.headers || [])
133+
.map((h) => h.split(':')[0]?.trim() || '')
134+
.filter(Boolean)
135+
.sort();
136+
if (existingHeaderKeys.join(',') !== newHeaderKeys.join(',')) continue;
137+
138+
// Found a match
139+
return session.name;
140+
}
141+
142+
return undefined;
143+
}
144+
145+
/**
146+
* Resolve the session name when @session is omitted from `mcpc connect`.
147+
* Finds an existing matching session or generates a new unique name.
148+
*
149+
* @returns Session name with @ prefix
150+
*/
151+
export async function resolveSessionName(
152+
parsed: { type: 'url'; url: string } | { type: 'config'; file: string; entry: string },
153+
options: {
154+
outputMode: OutputMode;
155+
profile?: string;
156+
headers?: string[];
157+
noProfile?: boolean;
158+
}
159+
): Promise<string> {
160+
// First, check if an existing session matches this server + auth settings
161+
const existingName = await findMatchingSession(parsed, options);
162+
if (existingName) {
163+
return existingName;
164+
}
165+
166+
// Generate a new session name
167+
const candidateName = generateSessionName(parsed);
168+
169+
// Check if the candidate name is already taken by a different server
170+
const storage = await loadSessions();
171+
if (!(candidateName in storage.sessions)) {
172+
if (options.outputMode === 'human') {
173+
console.log(chalk.cyan(`Using session name: ${candidateName}`));
174+
}
175+
return candidateName;
176+
}
177+
178+
// Name is taken - try suffixed variants
179+
for (let i = 2; i <= 99; i++) {
180+
const suffixed = `${candidateName}-${i}`;
181+
if (isValidSessionName(suffixed) && !(suffixed in storage.sessions)) {
182+
if (options.outputMode === 'human') {
183+
console.log(chalk.cyan(`Using session name: ${suffixed}`));
184+
}
185+
return suffixed;
186+
}
187+
}
188+
189+
throw new ClientError(
190+
`Cannot auto-generate session name: too many sessions for this server.\n` +
191+
`Specify a name explicitly: mcpc connect ${parsed.type === 'url' ? parsed.url : `${parsed.file}:${parsed.entry}`} @my-session`
192+
);
193+
}
194+
83195
/**
84196
* Creates a new session, starts a bridge process, and instructs it to connect an MCP server.
85197
* If session already exists with crashed bridge, reconnects it automatically

src/cli/index.ts

Lines changed: 22 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -432,11 +432,13 @@ Run "mcpc" without arguments to show active sessions and OAuth profiles.
432432
Full docs: ${docsUrl}`
433433
);
434434

435-
// connect command: mcpc connect <server> @<name>
435+
// connect command: mcpc connect <server> [@<name>]
436436
program
437437
.command('connect [server] [@session]')
438-
.usage('<server> <@session>')
439-
.description('Connect to an MCP server and start a new named @session')
438+
.usage('<server> [@session]')
439+
.description(
440+
'Connect to an MCP server and start a named @session (name auto-generated if omitted)'
441+
)
440442
.option('-H, --header <header>', 'HTTP header (can be repeated)')
441443
.option('--profile <name>', 'OAuth profile to use ("default" if skipped)')
442444
.option('--no-profile', 'Skip OAuth profile (connect anonymously)')
@@ -449,6 +451,13 @@ Full docs: ${docsUrl}`
449451
${chalk.bold('Server formats:')}
450452
mcp.apify.com Remote HTTP server (https:// added automatically)
451453
~/.vscode/mcp.json:puppeteer Config file entry (file:entry)
454+
455+
${chalk.bold('Session name:')}
456+
If @session is omitted, a name is auto-generated from the server hostname
457+
(e.g. mcp.apify.com → @apify) or config entry name. If a matching session
458+
already exists (same server URL, OAuth profile, and HTTP header names), it
459+
is reused (restarted if not live). Header values are not compared — they
460+
are stored securely in OS keychain.
452461
${jsonHelp('`InitializeResult` extended with `tools` and `_mcpc` metadata', '`{ protocolVersion, capabilities, serverInfo, instructions?, tools?, _mcpc }`', `${SCHEMA_BASE}#initializeresult`)}`
453462
)
454463
.action(async (server, sessionName, opts, command) => {
@@ -457,11 +466,6 @@ ${jsonHelp('`InitializeResult` extended with `tools` and `_mcpc` metadata', '`{
457466
'Missing required argument: server\n\nExample: mcpc connect mcp.apify.com @myapp'
458467
);
459468
}
460-
if (!sessionName) {
461-
throw new ClientError(
462-
'Missing required argument: @session\n\nExample: mcpc connect mcp.apify.com @myapp'
463-
);
464-
}
465469
const globalOpts = getOptionsFromCommand(command);
466470
const parsed = parseServerArg(server);
467471

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

486+
// Auto-generate session name if not provided
487+
if (!sessionName) {
488+
sessionName = await sessions.resolveSessionName(parsed, {
489+
outputMode: globalOpts.outputMode,
490+
...(globalOpts.profile && { profile: globalOpts.profile }),
491+
...(headers && { headers }),
492+
...(globalOpts.noProfile && { noProfile: globalOpts.noProfile }),
493+
});
494+
}
495+
482496
if (parsed.type === 'config') {
483497
// Config file entry: pass entry name as target with config file path
484498
await sessions.connectSession(parsed.entry, sessionName, {

0 commit comments

Comments
 (0)