From 463167d41fcfd1ed7e82f6fa2da621bd2da4a488 Mon Sep 17 00:00:00 2001 From: Jhin Lee Date: Mon, 1 Jun 2026 02:06:08 -0400 Subject: [PATCH] Prefer nested request metadata --- lib/src/types/json_rpc.dart | 13 +++---------- test/mcp_2025_11_25_test.dart | 20 ++++++++++++++++++++ test/mcp_2026_07_28_test.dart | 25 +++++++++++++++++++++++++ 3 files changed, 48 insertions(+), 10 deletions(-) diff --git a/lib/src/types/json_rpc.dart b/lib/src/types/json_rpc.dart index c64fa063..6cb55fd9 100644 --- a/lib/src/types/json_rpc.dart +++ b/lib/src/types/json_rpc.dart @@ -328,12 +328,12 @@ Map? _parseRequestMeta(Object? value) { return validateRequestMeta(readJsonObject(value, '_meta')); } -/// Extracts request metadata from either top-level or params-nested `_meta`. +/// Extracts request metadata, preferring spec-defined params-nested `_meta`. Map? extractRequestMeta(Map json) { final topLevelMeta = _parseRequestMeta(json['_meta']); final params = json['params']; final paramsMeta = params is Map ? _parseRequestMeta(params['_meta']) : null; - return topLevelMeta ?? paramsMeta; + return paramsMeta ?? topLevelMeta; } /// Base class for all JSON-RPC messages (requests, notifications, responses, errors). @@ -435,14 +435,7 @@ sealed class JsonRpcMessage { _ => JsonRpcNotification( method: method, params: params, - meta: readOptionalJsonObject( - json['_meta'], - 'JsonRpcNotification._meta', - ) ?? - readOptionalJsonObject( - params?['_meta'], - 'JsonRpcNotification._meta', - ), + meta: extractRequestMeta(json), ), }; } diff --git a/test/mcp_2025_11_25_test.dart b/test/mcp_2025_11_25_test.dart index 1a99a461..20250b3b 100644 --- a/test/mcp_2025_11_25_test.dart +++ b/test/mcp_2025_11_25_test.dart @@ -1226,6 +1226,26 @@ void main() { ); }); + test('request parsing prefers params metadata over top-level metadata', + () { + final parsed = JsonRpcMessage.fromJson( + const { + 'jsonrpc': jsonRpcVersion, + 'id': 'tools', + 'method': Method.toolsList, + '_meta': {'progressToken': 'top-level'}, + 'params': { + '_meta': {'progressToken': 'params-nested'}, + }, + }, + ); + + expect(parsed, isA()); + final request = parsed as JsonRpcListToolsRequest; + expect(request.meta, {'progressToken': 'params-nested'}); + expect(request.progressToken, 'params-nested'); + }); + test('server capabilities omit non-stable fields while parsing legacy', () { final capabilities = const ServerCapabilities( diff --git a/test/mcp_2026_07_28_test.dart b/test/mcp_2026_07_28_test.dart index 6efe9c05..ac69cfe4 100644 --- a/test/mcp_2026_07_28_test.dart +++ b/test/mcp_2026_07_28_test.dart @@ -322,6 +322,31 @@ void main() { } }); + test('request parsing does not let top-level metadata override params', () { + final parsed = JsonRpcMessage.fromJson({ + 'jsonrpc': jsonRpcVersion, + 'id': 'tools', + 'method': Method.toolsList, + '_meta': { + McpMetaKey.protocolVersion: latestProtocolVersion, + }, + 'params': { + '_meta': _clientMeta(), + }, + }); + + expect(parsed, isA()); + final request = parsed as JsonRpcListToolsRequest; + expect( + request.meta?[McpMetaKey.protocolVersion], + draftProtocolVersion2026_07_28, + ); + expect(request.meta?[McpMetaKey.clientInfo], { + 'name': 'client', + 'version': '1.0.0', + }); + }); + test('preserves integer request ids and progress tokens', () { final message = JsonRpcMessage.fromJson( const {