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: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@
`resources/list`, `resources/templates/list`, and `resources/read`, including
stateless server defaults for `resultType`, `ttlMs`, and `cacheScope` while
keeping legacy result serialization unchanged unless cache hints are set.
- Rejected core RPCs removed from stateless MCP 2026 requests
(`initialize`, `ping`, `logging/setLevel`, `resources/subscribe`,
`resources/unsubscribe`, `notifications/initialized`, and
`notifications/roots/list_changed`) while preserving legacy session behavior.
- Added request-scoped stateless logging gating via
`io.modelcontextprotocol/logLevel` metadata so 2026 log notifications are
emitted only when the current request opts in.

## 2.2.0

Expand Down
11 changes: 10 additions & 1 deletion lib/src/server/mcp_server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -984,11 +984,20 @@ class McpServer {
bool get isConnected => server.transport != null;

/// Sends a logging message to the client, if connected.
///
/// For stateless MCP requests, pass [requestMeta] from
/// [RequestHandlerExtra.meta] so log notifications honor the request-scoped
/// `io.modelcontextprotocol/logLevel` opt-in.
Future<void> sendLoggingMessage(
LoggingMessageNotification params, {
String? sessionId,
Map<String, dynamic>? requestMeta,
}) async {
return server.sendLoggingMessage(params, sessionId: sessionId);
return server.sendLoggingMessage(
params,
sessionId: sessionId,
requestMeta: requestMeta,
);
}

/// Sets the error handler for the server.
Expand Down
119 changes: 118 additions & 1 deletion lib/src/server/server.dart
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,19 @@ class Server extends Protocol {
LoggingLevel.emergency: 7,
};

static const Set<String> _statelessRemovedRequestMethods = {
Method.initialize,
Method.ping,
Method.loggingSetLevel,
Method.resourcesSubscribe,
Method.resourcesUnsubscribe,
};

static const Set<String> _statelessRemovedNotificationMethods = {
Method.notificationsInitialized,
Method.notificationsRootsListChanged,
};

/// Callback to be notified when the server is fully initialized.
void Function()? oninitialized;

Expand Down Expand Up @@ -181,6 +194,14 @@ class Server extends Protocol {
);
}

final logLevel = meta?[McpMetaKey.logLevel];
if (logLevel != null && _parseLoggingLevel(logLevel) == null) {
return McpError(
ErrorCode.invalidRequest.value,
'Invalid stateless request metadata: ${McpMetaKey.logLevel}',
);
}

return null;
}

Expand Down Expand Up @@ -225,6 +246,82 @@ class Server extends Protocol {
isStatelessProtocolVersion(requestedProtocolVersion);
}

bool _isStatelessNotification(JsonRpcNotification notification) {
final requestedProtocolVersion =
notification.meta?[McpMetaKey.protocolVersion];
return requestedProtocolVersion is String &&
isStatelessProtocolVersion(requestedProtocolVersion);
}

LoggingLevel? _parseLoggingLevel(Object? value) {
if (value is LoggingLevel) {
return value;
}
if (value is String) {
for (final level in LoggingLevel.values) {
if (level.name == value) {
return level;
}
}
}
return null;
}

bool _allowsStatelessLogging(
LoggingLevel messageLevel,
Map<String, dynamic>? requestMeta,
) {
if (!_isStatelessMeta(requestMeta)) {
return true;
}

final requestedLevel = _parseLoggingLevel(
requestMeta?[McpMetaKey.logLevel],
);
if (requestedLevel == null) {
return false;
}

return _logLevelSeverity[messageLevel]! >=
_logLevelSeverity[requestedLevel]!;
}

bool _isStatelessMeta(Map<String, dynamic>? requestMeta) {
final requestedProtocolVersion = requestMeta?[McpMetaKey.protocolVersion];
return requestedProtocolVersion is String &&
isStatelessProtocolVersion(requestedProtocolVersion);
}

McpError? _validateStatelessRemovedRequestMethod(JsonRpcRequest request) {
if (!_isStatelessRequest(request)) {
return null;
}
if (!_statelessRemovedRequestMethods.contains(request.method)) {
return null;
}

return McpError(
ErrorCode.methodNotFound.value,
'${request.method} is not part of MCP stateless protocol versions.',
);
}

McpError? _validateStatelessRemovedNotificationMethod(
JsonRpcNotification notification,
) {
if (!_isStatelessNotification(notification)) {
return null;
}
if (!_statelessRemovedNotificationMethods.contains(notification.method)) {
return null;
}

return McpError(
ErrorCode.methodNotFound.value,
'${notification.method} is not part of MCP stateless protocol versions.',
);
}

McpError? _validateDraftTaskMethods(JsonRpcRequest request) {
if (!_isStatelessRequest(request)) {
return null;
Expand Down Expand Up @@ -336,6 +433,12 @@ class Server extends Protocol {
if (metadataError != null) {
return metadataError;
}
final removedMethodError = _validateStatelessRemovedRequestMethod(
request,
);
if (removedMethodError != null) {
return removedMethodError;
}
return _validateRequestTaskSemantics(request);
}

Expand Down Expand Up @@ -372,6 +475,12 @@ class Server extends Protocol {

@override
McpError? validateIncomingNotification(JsonRpcNotification notification) {
final removedMethodError =
_validateStatelessRemovedNotificationMethod(notification);
if (removedMethodError != null) {
return removedMethodError;
}

switch (notification.method) {
case Method.notificationsCancelled:
case Method.notificationsProgress:
Expand Down Expand Up @@ -1027,12 +1136,20 @@ class Server extends Protocol {
}

/// Sends a `notifications/message` (logging) notification to the client.
///
/// For stateless MCP requests, pass [requestMeta] from
/// [RequestHandlerExtra.meta] so log notifications honor the request-scoped
/// `io.modelcontextprotocol/logLevel` opt-in.
Future<void> sendLoggingMessage(
LoggingMessageNotification params, {
String? sessionId,
Map<String, dynamic>? requestMeta,
}) async {
if (_capabilities.logging != null) {
if (!_isMessageIgnored(params.level, sessionId)) {
final statelessLogContext = _isStatelessMeta(requestMeta);
if (_allowsStatelessLogging(params.level, requestMeta) &&
(statelessLogContext ||
!_isMessageIgnored(params.level, sessionId))) {
final notif = JsonRpcLoggingMessageNotification(logParams: params);
return notification(notif);
}
Expand Down
4 changes: 2 additions & 2 deletions lib/src/types/initialization.dart
Original file line number Diff line number Diff line change
Expand Up @@ -983,11 +983,11 @@ class DiscoverResult implements BaseResultData {

/// Notification sent from the client to the server after initialization is finished.
class JsonRpcInitializedNotification extends JsonRpcNotification {
const JsonRpcInitializedNotification()
const JsonRpcInitializedNotification({super.meta})
: super(method: Method.notificationsInitialized);

factory JsonRpcInitializedNotification.fromJson(Map<String, dynamic> json) =>
const JsonRpcInitializedNotification();
JsonRpcInitializedNotification(meta: extractRequestMeta(json));
}

/// Deprecated alias for [InitializeRequest].
Expand Down
4 changes: 2 additions & 2 deletions lib/src/types/roots.dart
Original file line number Diff line number Diff line change
Expand Up @@ -82,11 +82,11 @@ class ListRootsResult implements BaseResultData {

/// Notification from client indicating the list of roots has changed.
class JsonRpcRootsListChangedNotification extends JsonRpcNotification {
const JsonRpcRootsListChangedNotification()
const JsonRpcRootsListChangedNotification({super.meta})
: super(method: Method.notificationsRootsListChanged);

factory JsonRpcRootsListChangedNotification.fromJson(
Map<String, dynamic> json,
) =>
const JsonRpcRootsListChangedNotification();
JsonRpcRootsListChangedNotification(meta: extractRequestMeta(json));
}
Loading