Skip to content
Closed
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
7 changes: 1 addition & 6 deletions example/client_stdio.dart
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import 'package:mcp_dart/mcp_dart.dart';
/// It demonstrates how to use the MCP client library with standard I/O.
/// It runs the server example from `example/server_stdio.dart`
/// and communicates with it using the StdioClientTransport.
/// The client sends various requests to the server, including ping, tool calls,
/// The client sends various requests to the server, including tool calls,
/// resource reads, and prompt calls.
Future<void> main() async {
// Define the server executable and arguments
Expand Down Expand Up @@ -48,11 +48,6 @@ Future<void> main() async {
await client.connect(transport);
print('Connected to server.');

// Example: Send a ping request
print('Sending ping...');
final pingResult = await client.ping();
print('Ping successful: ${pingResult.toJson()}');

print('Listing tools...');
final tools = await client.listTools();
print('Resources: ${tools.toJson()}');
Expand Down
38 changes: 26 additions & 12 deletions lib/src/client/client.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,24 +15,21 @@ class McpClientOptions extends ProtocolOptions {
/// Capabilities to advertise as being supported by this client.
final ClientCapabilities? capabilities;

/// Preferred protocol version for opt-in `server/discover` negotiation.
///
/// The current default keeps existing clients on the stable initialization
/// flow unless [useServerDiscover] is enabled.
/// Preferred protocol version for `server/discover` negotiation.
final String protocolVersion;

/// Whether [McpClient.connect] should probe with `server/discover` first.
final bool useServerDiscover;

/// Whether a `server/discover` method-not-found response should fall back to
/// the legacy `initialize` handshake.
/// Whether a failed `server/discover` probe should fall back to the legacy
/// `initialize` handshake when the peer looks like a pre-discovery server.
final bool allowLegacyInitializationFallback;

const McpClientOptions({
super.enforceStrictCapabilities,
this.capabilities,
this.protocolVersion = latestDraftProtocolVersion,
this.useServerDiscover = false,
this.useServerDiscover = true,
this.allowLegacyInitializationFallback = true,
});
}
Expand Down Expand Up @@ -173,7 +170,7 @@ class McpClient extends Protocol {
: _capabilities = options?.capabilities ?? const ClientCapabilities(),
_preferredProtocolVersion =
options?.protocolVersion ?? latestDraftProtocolVersion,
_useServerDiscover = options?.useServerDiscover ?? false,
_useServerDiscover = options?.useServerDiscover ?? true,
_allowLegacyInitializationFallback =
options?.allowLegacyInitializationFallback ?? true,
super(options) {
Expand Down Expand Up @@ -471,10 +468,7 @@ class McpClient extends Protocol {
await discoverServer();
return;
} catch (error) {
final canFallback = _allowLegacyInitializationFallback &&
error is McpError &&
error.code == ErrorCode.methodNotFound.value;
if (!canFallback) {
if (!_isLegacyDiscoveryFallbackError(error)) {
rethrow;
}
_logger.debug(
Expand All @@ -494,6 +488,26 @@ class McpClient extends Protocol {
}
}

bool _isLegacyDiscoveryFallbackError(Object error) {
if (!_allowLegacyInitializationFallback || error is! McpError) {
return false;
}
if (error.code == ErrorCode.methodNotFound.value) {
return true;
}

final message = error.message;
if (error.code == 0 &&
message.contains('Error POSTing to endpoint (HTTP 400)')) {
return true;
}

return (error.code == 0 ||
error.code == ErrorCode.connectionClosed.value ||
error.code == ErrorCode.invalidRequest.value) &&
message.contains('Server not initialized');
}

@override
Future<T> request<T extends BaseResultData>(
JsonRpcRequest requestData,
Expand Down
19 changes: 19 additions & 0 deletions packages/mcp_dart_cli/lib/src/conformance_runner.dart
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,25 @@ Future<void> _initializeClient(
final connectFuture = client.connect(transport);
await _settle();

final discoverRequests = transport.sentMessages
.whereType<JsonRpcRequest>()
.where((request) => request.method == _serverDiscoverMethod)
.toList();
for (final discoverRequest in discoverRequests) {
transport.emit(
JsonRpcError(
id: discoverRequest.id,
error: JsonRpcErrorData(
code: ErrorCode.methodNotFound.value,
message: 'Method not found',
),
),
);
}
if (discoverRequests.isNotEmpty) {
await _settle();
}

final initializeRequests = transport.sentMessages
.whereType<JsonRpcRequest>()
.where((request) => request.method == Method.initialize)
Expand Down
13 changes: 12 additions & 1 deletion test/client/client_elicitation_defaults_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,18 @@ class MockTransport extends Transport {
@override
Future<void> send(JsonRpcMessage message, {int? relatedRequestId}) async {
sentMessages.add(message);
if (message is JsonRpcRequest && message.method == Method.initialize) {
if (message is JsonRpcRequest && message.method == Method.serverDiscover) {
_respond(
JsonRpcError(
id: message.id,
error: const JsonRpcErrorData(
code: -32601,
message: 'Method not found',
),
),
);
} else if (message is JsonRpcRequest &&
message.method == Method.initialize) {
_respond(
JsonRpcResponse(
id: message.id,
Expand Down
22 changes: 18 additions & 4 deletions test/client/client_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,10 @@ void main() {
expect(transport.sentMessages.length, greaterThan(0));
expect(transport.sentMessages.first is JsonRpcRequest, isTrue);
expect(
(transport.sentMessages.first as JsonRpcRequest).method,
equals('initialize'),
transport.sentMessages
.whereType<JsonRpcRequest>()
.map((message) => message.method),
containsAllInOrder([Method.serverDiscover, Method.initialize]),
);

// Verify that an initialized notification was sent
Expand Down Expand Up @@ -585,8 +587,20 @@ class MockTransport extends Transport {
Future<void> send(JsonRpcMessage message, {int? relatedRequestId}) async {
sentMessages.add(message);

// If it's an initialize request, respond with the mock response
if (message is JsonRpcRequest &&
// Simulate a legacy peer by rejecting discovery, then respond to initialize.
if (message is JsonRpcRequest && message.method == Method.serverDiscover) {
if (onmessage != null) {
onmessage!(
JsonRpcError(
id: message.id,
error: const JsonRpcErrorData(
code: -32601,
message: 'Method not found',
),
),
);
}
} else if (message is JsonRpcRequest &&
message.method == 'initialize' &&
mockInitializeResponse != null) {
if (onmessage != null) {
Expand Down
13 changes: 12 additions & 1 deletion test/client/client_tool_validation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,18 @@ class MockTransport extends Transport
@override
Future<void> send(JsonRpcMessage message, {int? relatedRequestId}) async {
sentMessages.add(message);
if (message is JsonRpcRequest && message.method == Method.initialize) {
if (message is JsonRpcRequest && message.method == Method.serverDiscover) {
_respond(
JsonRpcError(
id: message.id,
error: const JsonRpcErrorData(
code: -32601,
message: 'Method not found',
),
),
);
} else if (message is JsonRpcRequest &&
message.method == Method.initialize) {
_respond(
JsonRpcResponse(
id: message.id,
Expand Down
7 changes: 5 additions & 2 deletions test/client/streamable_https_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -331,8 +331,11 @@ void main() {

expect(initializeCount, 1);
expect(initializedNotificationCount, 1);
expect(capturedSessionHeaders, isNotEmpty);
expect(capturedSessionHeaders, everyElement(preconfiguredSessionId));
expect(capturedSessionHeaders, [
null,
preconfiguredSessionId,
preconfiguredSessionId,
]);
expect(client.getServerCapabilities()?.logging, isNotNull);
expect(client.getServerVersion()?.name, 'PreconfiguredSessionServer');
expect(
Expand Down
15 changes: 15 additions & 0 deletions test/elicitation_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,21 @@ class MockTransport extends Transport {
Future<void> send(JsonRpcMessage message, {int? relatedRequestId}) async {
sentMessages.add(message);

// Handle discovery probe from default 2026 clients against this legacy
// mock transport.
if (message is JsonRpcRequest && message.method == Method.serverDiscover) {
onmessage?.call(
JsonRpcError(
id: message.id,
error: JsonRpcErrorData(
code: ErrorCode.methodNotFound.value,
message: 'Method not found',
),
),
);
return;
}

// Handle initialize request
if (message is JsonRpcRequest &&
message.method == 'initialize' &&
Expand Down
4 changes: 4 additions & 0 deletions test/lifecycle_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ void main() {
final client = Client(
const Implementation(name: 'client', version: '1.0.0'),
options: const ClientOptions(
useServerDiscover: false,
capabilities: ClientCapabilities(
sampling: ClientCapabilitiesSampling(),
),
Expand Down Expand Up @@ -414,6 +415,7 @@ void main() {
final transport = LifecycleTransport();
final client = Client(
const Implementation(name: 'client', version: '1.0.0'),
options: const ClientOptions(useServerDiscover: false),
);
final errors = <Error>[];
client.onerror = errors.add;
Expand All @@ -436,6 +438,7 @@ void main() {
final client = Client(
const Implementation(name: 'client', version: '1.0.0'),
options: const ClientOptions(
useServerDiscover: false,
capabilities: ClientCapabilities(
sampling: ClientCapabilitiesSampling(),
),
Expand Down Expand Up @@ -484,6 +487,7 @@ void main() {
final transport = FailingInitializedSendTransport();
final client = Client(
const Implementation(name: 'client', version: '1.0.0'),
options: const ClientOptions(useServerDiscover: false),
);

final connectFuture = client.connect(transport);
Expand Down
66 changes: 63 additions & 3 deletions test/mcp_2026_07_28_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,11 @@ class DiscoveringClientTransport extends Transport
class LegacyFallbackTransport extends Transport
implements ProtocolVersionAwareTransport {
LegacyFallbackTransport({
this.discoveryError,
this.toolsListResult = const {'tools': []},
});

final McpError? discoveryError;
final Map<String, dynamic> toolsListResult;
final List<JsonRpcMessage> sentMessages = [];

Expand All @@ -152,6 +154,10 @@ class LegacyFallbackTransport extends Transport
sentMessages.add(message);

if (message is JsonRpcRequest && message.method == Method.serverDiscover) {
final error = discoveryError;
if (error != null) {
throw error;
}
onmessage?.call(
JsonRpcError(
id: message.id,
Expand Down Expand Up @@ -2497,12 +2503,11 @@ void main() {
expect(transport.sentMessages.single, isA<JsonRpcResponse>());
});

test('client can opt in to server/discover and sends stateless metadata',
test('client defaults to server/discover and sends stateless metadata',
() async {
final transport = DiscoveringClientTransport();
final client = McpClient(
const Implementation(name: 'client', version: '1.0.0'),
options: const McpClientOptions(useServerDiscover: true),
);

await client.connect(transport);
Expand All @@ -2529,6 +2534,62 @@ void main() {
expect(listRequest.meta?[McpMetaKey.clientCapabilities], {});
});

test('client can opt out of discovery for legacy initialization', () async {
final transport = LegacyFallbackTransport();
final client = McpClient(
const Implementation(name: 'client', version: '1.0.0'),
options: const McpClientOptions(useServerDiscover: false),
);

await client.connect(transport);

expect(client.getProtocolVersion(), stableProtocolVersion2025_11_25);
expect(transport.protocolVersion, stableProtocolVersion2025_11_25);
expect(
transport.sentMessages
.whereType<JsonRpcRequest>()
.map((message) => message.method),
isNot(contains(Method.serverDiscover)),
);
expect(
transport.sentMessages
.whereType<JsonRpcRequest>()
.map((message) => message.method),
contains(Method.initialize),
);
});

test('client falls back when legacy HTTP rejects discovery before init',
() async {
final errors = [
McpError(
0,
'Error POSTing to endpoint (HTTP 400): '
'{"jsonrpc":"2.0","error":{"code":-32000,'
'"message":"Bad Request: Server not initialized"},"id":null}',
),
McpError(0, 'Error POSTing to endpoint (HTTP 400): '),
];

for (final error in errors) {
final transport = LegacyFallbackTransport(discoveryError: error);
final client = McpClient(
const Implementation(name: 'client', version: '1.0.0'),
);

await client.connect(transport);

expect(client.getProtocolVersion(), stableProtocolVersion2025_11_25);
expect(transport.protocolVersion, stableProtocolVersion2025_11_25);
expect(
transport.sentMessages
.whereType<JsonRpcRequest>()
.map((message) => message.method),
[Method.serverDiscover, Method.initialize],
);
}
});

test('stateless client rejects removed request methods before send',
() async {
final transport = DiscoveringClientTransport();
Expand Down Expand Up @@ -3470,7 +3531,6 @@ void main() {
final transport = LegacyFallbackTransport();
final client = McpClient(
const Implementation(name: 'client', version: '1.0.0'),
options: const McpClientOptions(useServerDiscover: true),
);

await client.connect(transport);
Expand Down