From bdcca6766a77bb129b6b9db6490791a4b7aff6cd Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Mon, 1 Jun 2026 02:32:56 -0400 Subject: [PATCH] Require nested metadata for stateless HTTP --- lib/src/server/streamable_https.dart | 22 +++++++++++++++++++--- test/server/streamable_https_test.dart | 12 ++++++++++++ 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/lib/src/server/streamable_https.dart b/lib/src/server/streamable_https.dart index 5f540921..b0994a79 100644 --- a/lib/src/server/streamable_https.dart +++ b/lib/src/server/streamable_https.dart @@ -439,6 +439,18 @@ class StreamableHTTPServerTransport return null; } + String? _nestedMetadataProtocolVersion(Map 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 messages, @@ -756,6 +768,7 @@ class StreamableHTTPServerTransport Future _validateStatelessHttpHeaders( HttpRequest req, List messages, + List> messageJsons, ) async { if (!_usesStatelessHttpValidation(req, messages)) { return true; @@ -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( @@ -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; } @@ -1336,6 +1350,7 @@ class StreamableHTTPServerTransport } final List messages = []; + final List> messageJsons = []; if (rawMessages.isEmpty) { await _writeJsonRpcErrorResponse( req.response, @@ -1362,6 +1377,7 @@ class StreamableHTTPServerTransport final messageJson = rawItem is Map ? rawItem : rawItem.cast(); + messageJsons.add(messageJson); messages.add(JsonRpcMessage.fromJson(messageJson)); } catch (e) { await _writeJsonRpcErrorResponse( @@ -1376,7 +1392,7 @@ class StreamableHTTPServerTransport } } - if (!await _validateStatelessHttpHeaders(req, messages)) { + if (!await _validateStatelessHttpHeaders(req, messages, messageJsons)) { return; } diff --git a/test/server/streamable_https_test.dart b/test/server/streamable_https_test.dart index 8c288071..6038152b 100644 --- a/test/server/streamable_https_test.dart +++ b/test/server/streamable_https_test.dart @@ -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: {