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
22 changes: 19 additions & 3 deletions lib/src/server/streamable_https.dart
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,18 @@ class StreamableHTTPServerTransport
return null;
}

String? _nestedMetadataProtocolVersion(Map<String, dynamic> messageJson) {
final params = messageJson['params'];
if (params is Map) {
final meta = params['_meta'];
if (meta is Map) {
final version = meta[McpMetaKey.protocolVersion];
return version is String ? version : null;
}
}
return null;
}

bool _usesStatelessHttpValidation(
HttpRequest req,
List<JsonRpcMessage> messages,
Expand Down Expand Up @@ -756,6 +768,7 @@ class StreamableHTTPServerTransport
Future<bool> _validateStatelessHttpHeaders(
HttpRequest req,
List<JsonRpcMessage> messages,
List<Map<String, dynamic>> messageJsons,
) async {
if (!_usesStatelessHttpValidation(req, messages)) {
return true;
Expand All @@ -773,6 +786,7 @@ class StreamableHTTPServerTransport
}

final message = messages.single;
final messageJson = messageJsons.single;
final protocolHeader = req.headers.value('mcp-protocol-version')?.trim();
if (protocolHeader == null || protocolHeader.isEmpty) {
await _writeHeaderMismatchResponse(
Expand All @@ -791,12 +805,12 @@ class StreamableHTTPServerTransport
return false;
}

final metadataVersion = _metadataProtocolVersion(message);
final metadataVersion = _nestedMetadataProtocolVersion(messageJson);
if (metadataVersion == null) {
await _writeHeaderMismatchResponse(
req.response,
message,
'MCP-Protocol-Version header has no matching request _meta protocol version',
'MCP-Protocol-Version header has no matching request _meta protocol version in params._meta',
);
return false;
}
Expand Down Expand Up @@ -1336,6 +1350,7 @@ class StreamableHTTPServerTransport
}

final List<JsonRpcMessage> messages = [];
final List<Map<String, dynamic>> messageJsons = [];
if (rawMessages.isEmpty) {
await _writeJsonRpcErrorResponse(
req.response,
Expand All @@ -1362,6 +1377,7 @@ class StreamableHTTPServerTransport
final messageJson = rawItem is Map<String, dynamic>
? rawItem
: rawItem.cast<String, dynamic>();
messageJsons.add(messageJson);
messages.add(JsonRpcMessage.fromJson(messageJson));
} catch (e) {
await _writeJsonRpcErrorResponse(
Expand All @@ -1376,7 +1392,7 @@ class StreamableHTTPServerTransport
}
}

if (!await _validateStatelessHttpHeaders(req, messages)) {
if (!await _validateStatelessHttpHeaders(req, messages, messageJsons)) {
return;
}

Expand Down
12 changes: 12 additions & 0 deletions test/server/streamable_https_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2730,6 +2730,18 @@ void main() {
contains('no matching request _meta protocol version'),
);

final topLevelMetaOnly = const JsonRpcListToolsRequest(id: 20).toJson()
..['_meta'] = _statelessMeta();
body = await postJson(
topLevelMetaOnly,
headers: {
'MCP-Protocol-Version': draftProtocolVersion2026_07_28,
'Mcp-Method': Method.toolsList,
},
);
expect(body['id'], 20);
expect(body['error']['message'], contains('params._meta'));

body = await postJson(
JsonRpcListToolsRequest(id: 5, meta: _statelessMeta()).toJson(),
headers: {
Expand Down