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

- New [REFERENCE.md](REFERENCE.md) with the full `--help` output of every mcpc command, generated from the CLI itself so it always matches the release.
- `mcpc @session` and `server-discover` now show the description and website URL a server advertises about itself, right below its name.

### Fixed

- `mcpc help tools/list` and other MCP method names now show the command's help instead of failing with "Unknown command" — they already worked as aliases everywhere else.

## [0.6.0] - 2026-08-02

Expand Down
6 changes: 5 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -656,7 +656,11 @@ When implementing features:
- **One line per description** - Every `.description()` and `.option()` description must fit on a single line of help output. Help width is 100 columns and the term column eats 30–40 of them, so keep descriptions under ~55 characters; anything longer wraps and makes the command list unreadable. Caveats, deprecations, protocol-era limits, defaults, and examples belong in a titled `.addHelpText('after', ...)` section (`Notes:`, `Deprecated:`, `Examples:`), or point at one with `(see below)`. Never restate a default Commander already prints: `.option('--scheme <x>', 'desc', 'auto')` appends `(default: "auto")` on its own. Enforced — together with the JSON-output rule below — by `test/e2e/suites/basic/help.test.sh`, which walks the whole help surface; run it after touching help text.
- **Document the JSON output** - Every command that prints JSON must describe its `--json` shape with `jsonHelp()` in its help text, including the session program itself (`mcpc @session --help`, which documents the no-command server-info output). A command whose help has no `JSON output (--json):` section is a bug.
- **Set the help width** - Any new Commander program must `configureOutput({ getOutHelpWidth: () => 100, getErrHelpWidth: () => 100 })` like the existing ones, otherwise it wraps at the default 80 columns.
2. **Next-step hints** - Every command's human-mode output should make it clear what the user or agent might want to do next. After listing items or finishing an action, print a dim hint suggesting the next likely command using the format `chalk.dim(' ↳ <action>: mcpc <command>')` with the `↳` arrow prefix. Examples: after `mcpc` lists sessions, hint how to view details (`↳ view a session: mcpc @sessionname`); after `mcpc connect` skips stdio servers, hint how to include them (`↳ run: mcpc connect --stdio`); after recoverable session states, hint the recovery command (`↳ run: mcpc @sessionname restart`). The goal is that any user or agent can chain commands without consulting `--help`. Do not emit hints in `--json` mode — JSON output stays strictly machine-readable.
2. **Next-step hints** - Every command's human-mode output should make it clear what the user or agent might want to do next. After listing items or finishing an action, print a dim hint suggesting the next likely command, so that any user or agent can chain commands without consulting `--help`. Two shapes, picked by what the hint hangs off:
- **`↳` arrow, for one-line situations only** - when the hint belongs to a single line right above it: a session line in a list, a one-line status, an empty state. Indent it under that line when the line is itself an item in a list (`chalk.dim(' ↳ run: mcpc @sessionname restart')` under a session in `mcpc`), otherwise keep it at the same column as the line it follows (`chalk.dim('↳ save to a file: ...')` under `(binary content not shown)`). The arrow means "this points at the line above" — never use it as a footer for a whole screen, where there is no single line to point at.
- **Plain sentence, for everything else** - a hint closing a multi-line block or a whole screen reads as part of that block when arrowed and indented. Use an unindented dim sentence instead: `chalk.dim('For session logs, run: mcpc @sessionname logs')`, `chalk.dim('To stop syncing (keeps the file), run: ...')`.

Do not emit hints in `--json` mode — JSON output stays strictly machine-readable.
3. **Keep core runtime-agnostic** - Use native APIs, avoid runtime-specific dependencies
4. **Error handling** - Provide clear, actionable error messages; use appropriate exit codes
5. **Retry logic** - Use exponential backoff for network operations (3 attempts for requests, 1s→30s for streams)
Expand Down
9 changes: 2 additions & 7 deletions REFERENCE.md
Original file line number Diff line number Diff line change
Expand Up @@ -958,13 +958,8 @@ Options:
--json Output in JSON format

Notes:
Sends `server/discover` and reports the answer: every protocol version the
server supports, its capabilities, instructions, and `_meta`. Unlike
`mcpc @<session>`, which shows what the connection settled on at connect time,
this is a live request.
MCP 2026-07-28 introduced the method, so the command fails on 2025-11-25 (and
older) connections, where `initialize` carries the same data — run
`mcpc @<session>` there instead.
A live `server/discover` request; `mcpc @<session>` shows the cached connect-time
answer instead — use it on 2025-11-25 (and older) connections, where this fails.

JSON output (--json):
`DiscoverResult` object, verbatim:
Expand Down
2 changes: 1 addition & 1 deletion src/cli/commands/grep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -577,7 +577,7 @@
);
} else {
console.log(chalk.bold('No active sessions.'));
console.log(chalk.dim(' \u21B3 run: mcpc connect mcp.example.com @test'));
console.log(chalk.dim('\u21B3 run: mcpc connect mcp.example.com @test'));
}
return 1;
}
Expand Down Expand Up @@ -629,7 +629,7 @@
} else {
const reason: unknown = outcome.reason;
errors.push({
name: toSessionRef(queryableSessions[i]!.name),

Check warning on line 632 in src/cli/commands/grep.ts

View workflow job for this annotation

GitHub Actions / Node.js 26

Forbidden non-null assertion

Check warning on line 632 in src/cli/commands/grep.ts

View workflow job for this annotation

GitHub Actions / Node.js 24

Forbidden non-null assertion

Check warning on line 632 in src/cli/commands/grep.ts

View workflow job for this annotation

GitHub Actions / Node.js 22

Forbidden non-null assertion
error: reason instanceof Error ? reason.message : String(reason),
});
}
Expand Down
10 changes: 6 additions & 4 deletions src/cli/commands/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -164,11 +164,13 @@ export async function subscribeResource(
if (options.outputMode === 'human') {
const mimeSuffix = result.mimeType ? `, ${result.mimeType}` : '';
console.log(formatSuccess(`Subscribed to resource: ${uri}`));
console.log(` Synced to ${result.file} (${result.bytes} bytes${mimeSuffix})`);
console.log(chalk.dim(' The file is updated automatically while the session is connected.'));
console.log(chalk.dim(`check sync status: mcpc ${target}`));
console.log(`Synced to ${result.file} (${result.bytes} bytes${mimeSuffix})`);
console.log(chalk.dim('The file is updated automatically while the session is connected.'));
console.log(chalk.dim(`To check sync status, run: mcpc ${target}`));
console.log(
chalk.dim(` ↳ stop syncing (keeps the file): mcpc ${target} resources-unsubscribe ${uri}`)
chalk.dim(
`To stop syncing (keeps the file), run: mcpc ${target} resources-unsubscribe ${uri}`
)
);
} else {
console.log(formatOutput({ subscribed: true, ...result }, 'json'));
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/sessions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -161,7 +161,7 @@ export async function listSessionsAndAuthProfiles(options: {
// Display sessions
if (sessions.length === 0) {
console.log(chalk.bold('No active MCP sessions.'));
console.log(chalk.dim(' ↳ run: mcpc connect mcp.example.com @test'));
console.log(chalk.dim('↳ run: mcpc connect mcp.example.com @test'));
} else {
console.log(chalk.bold('MCP sessions:'));
for (const session of sessions) {
Expand Down Expand Up @@ -202,7 +202,7 @@ export async function listSessionsAndAuthProfiles(options: {
console.log('');
if (profiles.length === 0) {
console.log(chalk.bold('No OAuth profiles.'));
console.log(chalk.dim(' ↳ run: mcpc login mcp.example.com'));
console.log(chalk.dim('↳ run: mcpc login mcp.example.com'));
} else {
console.log(chalk.bold('Saved OAuth profiles:'));
for (const profile of profiles) {
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/utilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ export async function ping(target: string, options: CommandOptions): Promise<voi
// so a --verbose log or the server's access log shows server/discover instead.
if (isModern) {
console.log(
chalk.dim(` MCP ${protocolVersion} has no ping request; probed with server/discover.`)
chalk.dim(`MCP ${protocolVersion} has no ping request; probed with server/discover.`)
);
console.log(chalk.dim(` ↳ see the full result: mcpc ${target} server-discover`));
console.log(chalk.dim(`↳ see the full result: mcpc ${target} server-discover`));
}
} else {
console.log(
Expand Down
4 changes: 2 additions & 2 deletions src/cli/commands/x402.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@ async function walletInfo(options: {
} else {
console.log(formatInfo('No wallet configured. Create one with: mcpc x402 init'));
if (options.showUsageHint) {
console.log(chalk.dim(' For usage information, run: mcpc help x402'));
console.log(chalk.dim('For usage information, run: mcpc help x402'));
}
}
return;
Expand Down Expand Up @@ -237,7 +237,7 @@ async function walletInfo(options: {
await printAddressQrCode(wallet.address);
if (options.showUsageHint) {
console.log('');
console.log(chalk.dim(' For usage information, run: mcpc help x402'));
console.log(chalk.dim('For usage information, run: mcpc help x402'));
}
}

Expand Down
20 changes: 13 additions & 7 deletions src/cli/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -929,6 +929,17 @@ ${jsonHelp('`[{ sessionName, tools?: Tool[], resources?: Resource[], prompts?: P
return;
}

// Raw MCP method names ("server/discover") are accepted wherever a command name
// is expected, so `help` must resolve them too — otherwise looking up the alias
// you just used successfully reports it as an unknown command.
const slashAlias = normalizeSlashCommand(cmdName);
if (
slashAlias !== cmdName &&
[...KNOWN_COMMANDS, ...KNOWN_SESSION_COMMANDS].includes(slashAlias)
) {
cmdName = slashAlias;
}

// x402 has its own Commander program with full subcommand help
if (cmdName === 'x402') {
const helpArgs = subcommand ? [subcommand, '--help'] : ['--help'];
Expand Down Expand Up @@ -1463,13 +1474,8 @@ ${jsonHelp('`{ success: true, durationMs: number }`')}`
'after',
`
${chalk.bold('Notes:')}
Sends \`server/discover\` and reports the answer: every protocol version the
server supports, its capabilities, instructions, and \`_meta\`. Unlike
\`mcpc ${session}\`, which shows what the connection settled on at connect time,
this is a live request.
MCP 2026-07-28 introduced the method, so the command fails on 2025-11-25 (and
older) connections, where \`initialize\` carries the same data — run
\`mcpc ${session}\` there instead.
A live \`server/discover\` request; \`mcpc ${session}\` shows the cached connect-time
answer instead — use it on 2025-11-25 (and older) connections, where this fails.
${jsonHelp(
'`DiscoverResult` object, verbatim',
'`{ supportedVersions: [...], capabilities: { ... }, instructions?, _meta? }`',
Expand Down
41 changes: 33 additions & 8 deletions src/cli/output.ts
Original file line number Diff line number Diff line change
Expand Up @@ -897,7 +897,7 @@ export function formatResourceContents(
lines.push(chalk.gray('(binary content not shown)'));
lines.push(
chalk.dim(
` ↳ save to a file: mcpc ${target} resources-read ${item.uri || requestedUri} -o <file>`
`↳ save to a file: mcpc ${target} resources-read ${item.uri || requestedUri} -o <file>`
)
);
} else {
Expand Down Expand Up @@ -1675,6 +1675,33 @@ function formatCapabilityList(
return list;
}

/**
* Format the server identity block: the name/version headline plus the optional
* `description` and `websiteUrl` a server may advertise in its `serverInfo`.
*
* Those two fields are the only human-readable statement of what the server actually is
* and where it is documented, so they belong next to the name instead of staying a
* `--json`-only detail. Both are optional — servers that omit them keep the single line.
* Long descriptions are printed verbatim and left to the terminal to wrap, like
* instructions and tool descriptions elsewhere in this file.
*/
function formatServerIdentity(serverInfo: Implementation): string[] {
const lines = [
chalk.bold('Server:') + ` ${serverInfo.name} (version: ${serverInfo.version || 'N/A'})`,
];

const description = serverInfo.description?.trim();
if (description) {
lines.push(...description.split('\n').map((line) => chalk.gray(line)));
}

if (serverInfo.websiteUrl) {
lines.push(theme.cyan(serverInfo.websiteUrl));
}

return lines;
}

/**
* Format the result of a live `server/discover` request (2026-07-28+).
*
Expand All @@ -1692,9 +1719,7 @@ export function formatDiscoverResult(
const serverInfo = result._meta?.[SERVER_INFO_META_KEY] as Implementation | undefined;

if (serverInfo) {
lines.push(
chalk.bold('Server:') + ` ${serverInfo.name} (version: ${serverInfo.version || 'N/A'})`
);
lines.push(...formatServerIdentity(serverInfo));
lines.push('');
}

Expand Down Expand Up @@ -1730,7 +1755,9 @@ export function formatDiscoverResult(
lines.push('');
}

lines.push(chalk.dim(` ↳ session info and available commands: mcpc ${target}`));
// Plain footer rather than an indented "↳" hint: this block ends with the server's
// instructions, which can run for pages, so an indented arrow reads as part of them.
lines.push(chalk.dim(`For session info and available commands, run: mcpc ${target}`));

return lines.join('\n');
}
Expand Down Expand Up @@ -1769,9 +1796,7 @@ export function formatServerDetails(

// Server info
if (serverInfo) {
lines.push(
chalk.bold('Server:') + ` ${serverInfo.name} (version: ${serverInfo.version || 'N/A'})`
);
lines.push(...formatServerIdentity(serverInfo));
lines.push('');
}

Expand Down
2 changes: 2 additions & 0 deletions test/e2e/server/index-v2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ function createTestServer(): Server {
{
name: 'e2e-test-server',
version: '2.0.0',
description: 'A fake MCP server that exists only to exercise the mcpc CLI.',
websiteUrl: 'https://example.com/e2e-test-server',
},
{
capabilities,
Expand Down
2 changes: 2 additions & 0 deletions test/e2e/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ function createMcpServer(): Server {
{
name: 'e2e-test-server',
version: '1.0.0',
description: 'A fake MCP server that exists only to exercise the mcpc CLI.',
websiteUrl: 'https://example.com/e2e-test-server',
},
{
capabilities,
Expand Down
10 changes: 10 additions & 0 deletions test/e2e/suites/basic/help.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,16 @@ assert_not_contains "$STDOUT" "Mental model"
assert_contains "$STDERR" "takes no command name"
test_pass

# Test: mcpc help accepts raw MCP method names, like the command slot itself does
test_case "help resolves slash-style method names"
run_mcpc help server/discover
assert_success
assert_contains "$STDOUT" "mcpc <@session> server-discover"
run_mcpc help tools/list
assert_success
assert_contains "$STDOUT" "mcpc <@session> tools-list"
test_pass

# Test: --version shows version
test_case "--version shows version"
run_mcpc --version
Expand Down
7 changes: 7 additions & 0 deletions test/e2e/suites/basic/human-output.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,13 @@ assert_success
assert_contains "$STDOUT" "e2e-test-server"
test_pass

test_case "server info contains the advertised description and website"
run_mcpc "$SESSION"
assert_success
assert_contains "$STDOUT" "A fake MCP server that exists only to exercise the mcpc CLI."
assert_contains "$STDOUT" "https://example.com/e2e-test-server"
test_pass

test_case "server info contains capabilities"
run_mcpc "$SESSION"
assert_success
Expand Down
51 changes: 51 additions & 0 deletions test/unit/cli/output.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1061,6 +1061,38 @@ describe('formatServerDetails', () => {
expect(output).toContain('This is the server instructions.');
});

it('shows the description and website URL a server advertises in serverInfo', () => {
const details: ServerDetails = {
protocolVersion: '2025-11-25',
capabilities: { tools: {} },
serverInfo: {
name: 'Notion MCP',
version: '1.2.0',
description: "Notion's official MCP server.\nUse your workspace as a system of record.",
websiteUrl: 'https://developers.notion.com/docs/mcp',
},
};

const output = formatServerDetails(details, '@notion');

expect(output).toContain('Notion MCP (version: 1.2.0)');
expect(output).toContain("Notion's official MCP server.");
expect(output).toContain('Use your workspace as a system of record.');
expect(output).toContain('https://developers.notion.com/docs/mcp');
});

it('keeps the server line alone when no description or website URL is advertised', () => {
const details: ServerDetails = {
protocolVersion: '2025-11-25',
capabilities: { tools: {} },
serverInfo: { name: 'Bare Server', version: '1.0.0' },
};

const output = formatServerDetails(details, '@bare');

expect(output).toContain('Server: Bare Server (version: 1.0.0)\n\nCapabilities:');
});

it('shows the MCP version with the transport and its connection mode', () => {
const details: ServerDetails = {
protocolVersion: '2026-07-28',
Expand Down Expand Up @@ -1438,6 +1470,25 @@ describe('formatDiscoverResult', () => {
expect(output).toContain('mcpc @m');
});

it('shows the description and website URL from the advertised identity', () => {
const described = {
...result,
_meta: {
'io.modelcontextprotocol/serverInfo': {
name: 'Modern Server',
version: '2.0.0',
description: 'A server that describes itself.',
websiteUrl: 'https://example.com/docs',
},
},
} as Parameters<typeof formatDiscoverResult>[0];

const output = formatDiscoverResult(described, '@m', '2026-07-28');

expect(output).toContain('A server that describes itself.');
expect(output).toContain('https://example.com/docs');
});

it('marks the version this session negotiated', () => {
const output = formatDiscoverResult(result, '@m', '2026-07-28');
expect(output).toMatch(/2026-07-28 \(negotiated\)/);
Expand Down
Loading