Skip to content

Commit 88e5f9b

Browse files
jancurnclaude
andauthored
Add server-discover command for MCP 2026-07-28 connections (#354)
MCP 2026-07-28 removed `ping`, so `mcpc @session ping` silently probes with `server/discover` instead — invisible until it shows up in a server's access log, with no way to see what that request returns. This adds a command that says what it does. - `mcpc @session server-discover` sends `server/discover` and reports the answer: supported protocol versions (negotiated one marked), capabilities, instructions, `_meta`. `--json` prints the `DiscoverResult` verbatim. - On 2025-era connections it refuses (exit 2) and points at `mcpc @session`, whose `initialize` result carries the same data. - `ping` output now names the request it measured; `ping --json` is unchanged. - Capability rendering extracted so both screens share it; `SERVER_INFO_META_KEY` mirrored in the dependency-free `protocol.ts` with an SDK drift test. - New `basic/server-discover.test.sh` e2e suite (one branch per protocol era); README, CHANGELOG and the agent skill updated. Refs #316 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01KHDUfie1HZtvSiqFNTYs17 --- _Generated by [Claude Code](https://claude.ai/code/session_01KHDUfie1HZtvSiqFNTYs17)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2a2643d commit 88e5f9b

16 files changed

Lines changed: 519 additions & 79 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1313
- Support for MCP protocol version 2026-07-28: mcpc now probes each server with `server/discover` and talks the new stateless protocol when supported, falling back to older protocol versions (2025-11-25 down to 2024-10-07) automatically. Resource subscriptions use the new `subscriptions/listen` stream (with automatic re-listen on drops), and `ping` transparently uses `server/discover` on 2026-07-28 servers. mcpc now uses the official TypeScript SDK v2 (`@modelcontextprotocol/client`).
1414
- `mcpc --json` now reports each session's server `capabilities`, plus `hasInstructions` to tell whether the server provided instructions (read them with `mcpc --json @<session>`).
1515
- New `--protocol-version` option for `mcpc connect` to pin the MCP protocol version (e.g. `--protocol-version 2025-11-25`) instead of auto-negotiating; the connection fails if the server does not support the pinned version. Also supported as a `protocolVersion` field in mcp.json config entries.
16+
- New `mcpc @<session> server-discover` command that sends a `server/discover` request and reports what the server advertises right now: every protocol version it supports, its capabilities, instructions, and `_meta`. Requires a 2026-07-28 connection, where `ping` is also translated into `server/discover` — the ping output now says so.
1617
- Server details from 2026-07-28 servers now include everything their `server/discover` result carries: `supportedVersions` (every protocol version the server offers) and the result's `_meta`. Reported by `connect`, `restart` and `mcpc @session` in `--json`, and kept in `sessions.json`.
1718

1819
### Changed

README.md

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,7 @@ MCP session commands (after connecting):
174174
<@session> skills-get <name> [--raw]
175175
<@session> logging-set-level <level>
176176
<@session> ping
177+
<@session> server-discover
177178
<@session> logs [-n N] [--follow] [--since 1h]
178179
179180
Run "mcpc" without arguments to show active sessions and OAuth profiles.
@@ -854,6 +855,7 @@ Where `mcpc` stands on each part of the MCP specification:
854855
| 🔔 [**Notifications**](#list-change-notifications) | ✅ Supported |
855856
| 📄 [**Pagination**](#pagination) | ✅ Supported |
856857
| 🏓 [**Ping**](#ping) | ✅ Supported |
858+
| 🔍 [**Server discovery**](#server-discovery) | ✅ Supported (`server/discover`, 2026-07-28 servers) |
857859
| 📁 **Roots** | ❌ Not planned (deprecated by MCP) |
858860
|**Elicitation** | 🚧 Planned |
859861
| 🔤 **Completion** | 🚧 Planned |
@@ -1127,7 +1129,24 @@ mcpc @apify ping --json
11271129

11281130
Protocol version `2026-07-28` removed the `ping` request, so on servers using it `mcpc`
11291131
sends a `server/discover` probe instead — same round trip, same liveness signal, and the
1130-
command works identically on both protocol versions.
1132+
command works identically on both protocol versions. The human-readable output says so,
1133+
so a `server/discover` entry in the server's access log is not a surprise.
1134+
1135+
#### Server discovery
1136+
1137+
On `2026-07-28` connections you can also send that request yourself and see what the server
1138+
answers right now — every protocol version it supports, its capabilities, its instructions,
1139+
and its `_meta`:
1140+
1141+
```bash
1142+
mcpc @apify server-discover
1143+
mcpc @apify server-discover --json # DiscoverResult, verbatim
1144+
```
1145+
1146+
Unlike [`mcpc @apify`](#server-instructions), which reports what the connection settled on
1147+
when it was created, this is a live request. Protocol `2026-07-28` introduced
1148+
`server/discover`, so the command fails on `2025-11-25` (and older) connections, where the
1149+
`initialize` handshake carries the same information — run `mcpc @apify` there instead.
11311150

11321151
#### Async tasks
11331152

skills/mcpc/SKILL.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,8 @@ mcpc @apify skills-get <name> --raw # print the SKILL.md markdown (pipe to a
251251
mcpc --verbose @apify tools-call <tool> # protocol-level detail (JSON-RPC, transport)
252252
mcpc @apify logs # bridge log; -n <N>, --follow, --since 1h
253253
mcpc @apify ping # round-trip health check
254+
mcpc @apify server-discover # what the server advertises now (2026-07-28 only;
255+
# on older servers use mcpc @apify instead)
254256
mcpc @apify logging-set-level debug # deprecated; 2025-11-25 servers only, will be removed
255257
mcpc clean # tidy stale sessions/logs (also: mcpc clean all)
256258
```

src/bridge/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,6 +1393,10 @@ class BridgeProcess {
13931393
result = await this.client.ping();
13941394
break;
13951395

1396+
case 'discover':
1397+
result = await this.client.discover();
1398+
break;
1399+
13961400
case 'listTools': {
13971401
const cursor = message.params as string | undefined;
13981402
result = await this.client.listTools(cursor);

src/cli/commands/utilities.ts

Lines changed: 43 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,37 @@
11
/**
2-
* Server-level commands (ping, etc.)
2+
* Server-level commands (ping, server-discover, etc.)
33
*/
44

5-
import { formatSuccess, formatOutput } from '../output.js';
5+
import chalk from 'chalk';
6+
import { formatSuccess, formatOutput, formatDiscoverResult } from '../output.js';
67
import { withMcpClient } from '../helpers.js';
8+
import { ServerError } from '../../lib/errors.js';
9+
import { isModernProtocolVersion, discoverUnavailableMessage } from '../../core/protocol.js';
710
import type { CommandOptions } from '../../lib/types.js';
811

912
/**
1013
* Ping the MCP server to check if it's alive
1114
*/
1215
export async function ping(target: string, options: CommandOptions): Promise<void> {
13-
await withMcpClient(target, options, async (client, _context) => {
16+
await withMcpClient(target, options, async (client, context) => {
17+
const { protocolVersion } = context;
18+
const isModern = !!protocolVersion && isModernProtocolVersion(protocolVersion);
19+
1420
const startTime = performance.now();
1521
await client.ping();
1622
const endTime = performance.now();
1723
const durationMillis = Math.round(endTime - startTime);
1824

1925
if (options.outputMode === 'human') {
2026
console.log(formatSuccess(`Ping successful (${durationMillis}ms)`));
27+
// Say which request actually measured the roundtrip: MCP 2026-07-28 has no `ping`,
28+
// so a --verbose log or the server's access log shows server/discover instead.
29+
if (isModern) {
30+
console.log(
31+
chalk.dim(` MCP ${protocolVersion} has no ping request; probed with server/discover.`)
32+
);
33+
console.log(chalk.dim(` ↳ see the full result: mcpc ${target} server-discover`));
34+
}
2135
} else {
2236
console.log(
2337
formatOutput(
@@ -31,3 +45,29 @@ export async function ping(target: string, options: CommandOptions): Promise<voi
3145
}
3246
});
3347
}
48+
49+
/**
50+
* Send a `server/discover` request and report what the server advertises.
51+
*
52+
* 2026-07-28 and later only. The equivalent data on a legacy connection comes from the
53+
* `initialize` handshake, which mcpc already reports via `mcpc @session` — refuse here
54+
* (with that pointer) rather than passing off cached handshake data as a discover result.
55+
*/
56+
export async function serverDiscover(target: string, options: CommandOptions): Promise<void> {
57+
await withMcpClient(target, options, async (client, _context) => {
58+
// Gate on the negotiated version before sending anything, so the reason names this
59+
// session and its version. The core client repeats the check as a backstop.
60+
const details = await client.getServerDetails();
61+
if (details.protocolVersion && !isModernProtocolVersion(details.protocolVersion)) {
62+
throw new ServerError(discoverUnavailableMessage(details.protocolVersion, target));
63+
}
64+
65+
const result = await client.discover();
66+
67+
if (options.outputMode === 'human') {
68+
console.log(formatDiscoverResult(result, target, details.protocolVersion));
69+
} else {
70+
console.log(formatOutput(result, 'json'));
71+
}
72+
});
73+
}

src/cli/helpers.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,12 @@ export interface McpClientContext {
142142
sessionName?: string | undefined;
143143
profileName?: string | undefined;
144144
serverConfig?: ServerConfig | undefined;
145+
/**
146+
* Protocol version negotiated by the session's bridge, as persisted in sessions.json.
147+
* Lets a command adapt its output to the protocol era without an extra IPC round-trip;
148+
* commands that must be authoritative should call `getServerDetails()` instead.
149+
*/
150+
protocolVersion?: string | undefined;
145151
}
146152

147153
/**
@@ -185,6 +191,7 @@ export async function withMcpClient<T>(
185191
sessionName: session?.name,
186192
profileName: session?.profileName,
187193
serverConfig: session?.server,
194+
protocolVersion: session?.protocolVersion,
188195
};
189196

190197
// Log target prefix (unless hidden)

src/cli/index.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -460,6 +460,7 @@ ${chalk.bold('MCP session commands (after connecting):')}
460460
<@session> ${theme.cyan('skills-get')} <name> [--raw]
461461
<@session> ${theme.cyan('logging-set-level')} <level>
462462
<@session> ${theme.cyan('ping')}
463+
<@session> ${theme.cyan('server-discover')}
463464
<@session> ${theme.cyan('logs')} [-n N] [--follow] [--since 1h]
464465
465466
Run "mcpc" without arguments to show active sessions and OAuth profiles.
@@ -1445,13 +1446,38 @@ ${jsonHelp('`{ level: string }`')}`
14451446
`
14461447
${chalk.bold('Notes:')}
14471448
Measures the request roundtrip. MCP 2026-07-28 removed \`ping\`, so on modern
1448-
connections the liveness probe is \`server/discover\` instead.
1449+
connections the liveness probe is \`server/discover\` instead — run
1450+
\`mcpc ${session} server-discover\` to see what that request returns.
14491451
${jsonHelp('`{ success: true, durationMs: number }`')}`
14501452
)
14511453
.action(async (_options, command) => {
14521454
await utilities.ping(session, getOptionsFromCommand(command));
14531455
});
14541456

1457+
program
1458+
.command('server-discover')
1459+
.description('Ask the server what it supports (MCP 2026-07-28+).')
1460+
.addHelpText(
1461+
'after',
1462+
`
1463+
${chalk.bold('Notes:')}
1464+
Sends \`server/discover\` and reports the answer: every protocol version the
1465+
server supports, its capabilities, instructions, and \`_meta\`. Unlike
1466+
\`mcpc ${session}\`, which shows what the connection settled on at connect time,
1467+
this is a live request.
1468+
MCP 2026-07-28 introduced the method, so the command fails on 2025-11-25 (and
1469+
older) connections, where \`initialize\` carries the same data — run
1470+
\`mcpc ${session}\` there instead.
1471+
${jsonHelp(
1472+
'`DiscoverResult` object, verbatim',
1473+
'`{ supportedVersions: [...], capabilities: { ... }, instructions?, _meta? }`',
1474+
`${SCHEMA_BASE}#discoverresult`
1475+
)}`
1476+
)
1477+
.action(async (_options, command) => {
1478+
await utilities.serverDiscover(session, getOptionsFromCommand(command));
1479+
});
1480+
14551481
// Logs command
14561482
program
14571483
.command('logs')

0 commit comments

Comments
 (0)