diff --git a/CHANGELOG.md b/CHANGELOG.md index ef9de5e8..71fe6a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 0886fff4..25e7118d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ', '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(' ↳ : mcpc ')` 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) diff --git a/REFERENCE.md b/REFERENCE.md index dbe45e1a..6b2b2feb 100644 --- a/REFERENCE.md +++ b/REFERENCE.md @@ -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 @`, 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 @` there instead. + A live `server/discover` request; `mcpc @` 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: diff --git a/src/cli/commands/grep.ts b/src/cli/commands/grep.ts index 29216315..5b1c9362 100644 --- a/src/cli/commands/grep.ts +++ b/src/cli/commands/grep.ts @@ -577,7 +577,7 @@ export async function grepAllSessions(pattern: string, options: GrepOptions): Pr ); } 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; } diff --git a/src/cli/commands/resources.ts b/src/cli/commands/resources.ts index af2b7020..790bb0d4 100644 --- a/src/cli/commands/resources.ts +++ b/src/cli/commands/resources.ts @@ -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')); diff --git a/src/cli/commands/sessions.ts b/src/cli/commands/sessions.ts index 63d451b6..96cd022e 100644 --- a/src/cli/commands/sessions.ts +++ b/src/cli/commands/sessions.ts @@ -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) { @@ -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) { diff --git a/src/cli/commands/utilities.ts b/src/cli/commands/utilities.ts index 9eef7bea..632d7fc9 100644 --- a/src/cli/commands/utilities.ts +++ b/src/cli/commands/utilities.ts @@ -28,9 +28,9 @@ export async function ping(target: string, options: CommandOptions): Promise` + `↳ save to a file: mcpc ${target} resources-read ${item.uri || requestedUri} -o ` ) ); } else { @@ -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+). * @@ -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(''); } @@ -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'); } @@ -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(''); } diff --git a/test/e2e/server/index-v2.ts b/test/e2e/server/index-v2.ts index 141fd942..a2255399 100644 --- a/test/e2e/server/index-v2.ts +++ b/test/e2e/server/index-v2.ts @@ -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, diff --git a/test/e2e/server/index.ts b/test/e2e/server/index.ts index 20a242e3..ad33863d 100644 --- a/test/e2e/server/index.ts +++ b/test/e2e/server/index.ts @@ -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, diff --git a/test/e2e/suites/basic/help.test.sh b/test/e2e/suites/basic/help.test.sh index bee7b1c3..081e5f2b 100755 --- a/test/e2e/suites/basic/help.test.sh +++ b/test/e2e/suites/basic/help.test.sh @@ -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 diff --git a/test/e2e/suites/basic/human-output.test.sh b/test/e2e/suites/basic/human-output.test.sh index b4590f6f..43e7626a 100755 --- a/test/e2e/suites/basic/human-output.test.sh +++ b/test/e2e/suites/basic/human-output.test.sh @@ -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 diff --git a/test/unit/cli/output.test.ts b/test/unit/cli/output.test.ts index 5c268582..dee35e83 100644 --- a/test/unit/cli/output.test.ts +++ b/test/unit/cli/output.test.ts @@ -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', @@ -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[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\)/);