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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,8 @@
`params._meta` metadata for direct JSON-RPC transport sends.
- Allowed Streamable MCP server CORS preflights for 2026 stateless routing and
tool parameter headers, including requested `Mcp-Param-*` headers.
- Serialized MRTR `ElicitResult` and `ListRootsResult` input responses with the
MCP 2026 embedded client-result shapes that omit common Result `_meta`.

## 2.2.0

Expand Down
58 changes: 45 additions & 13 deletions lib/src/types/json_rpc.dart
Original file line number Diff line number Diff line change
Expand Up @@ -770,17 +770,13 @@ class InputResponse {

/// Creates an input response from a typed MCP result.
factory InputResponse.fromResult(BaseResultData result) {
return InputResponse.raw(result.toJson());
return InputResponse.raw(_inputResponseJsonForResult(result));
}

factory InputResponse.fromJson(Map<String, dynamic> json) {
if (!_isValidInputResponse(json)) {
throw const FormatException(
'InputResponse must be a CreateMessageResult, ListRootsResult, '
'or ElicitResult',
);
}
return InputResponse.raw(Map<String, dynamic>.from(json));
final value = Map<String, dynamic>.from(json);
_validateInputResponse(value);
return InputResponse.raw(value);
}

/// Parses an input response map.
Expand All @@ -804,13 +800,49 @@ class InputResponse {
);
}

Map<String, dynamic> toJson() => readJsonObject(value, 'InputResponse');
Map<String, dynamic> toJson() {
final json = readJsonObject(value, 'InputResponse');
_validateInputResponse(json);
return json;
}
}

Map<String, dynamic> _inputResponseJsonForResult(BaseResultData result) {
final json = Map<String, dynamic>.from(result.toJson());
if (result is ElicitResult || result is ListRootsResult) {
json.remove('_meta');
}
_validateInputResponse(json);
return json;
}

void _validateInputResponse(Map<String, dynamic> json) {
if (_canParseInputResponse(CreateMessageResult.fromJson, json)) {
return;
}

if (_canParseInputResponse(ListRootsResult.fromJson, json)) {
_rejectInputResponseMeta(json, 'ListRootsResult');
return;
}

if (_canParseInputResponse(ElicitResult.fromJson, json)) {
_rejectInputResponseMeta(json, 'ElicitResult');
return;
}

throw const FormatException(
'InputResponse must be a CreateMessageResult, ListRootsResult, '
'or ElicitResult',
);
}

bool _isValidInputResponse(Map<String, dynamic> json) {
return _canParseInputResponse(CreateMessageResult.fromJson, json) ||
_canParseInputResponse(ListRootsResult.fromJson, json) ||
_canParseInputResponse(ElicitResult.fromJson, json);
void _rejectInputResponseMeta(Map<String, dynamic> json, String resultName) {
if (json.containsKey('_meta')) {
throw FormatException(
'InputResponse $resultName must not include _meta in MCP 2026',
);
}
}

bool _canParseInputResponse(
Expand Down
43 changes: 42 additions & 1 deletion test/mcp_2026_07_28_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -657,10 +657,22 @@ void main() {
const ElicitResult(
action: 'accept',
content: {'name': 'octocat'},
meta: {'stable': true},
),
),
'roots': InputResponse.fromResult(
ListRootsResult(roots: [Root(uri: 'file:///repo')]),
ListRootsResult(
roots: [Root(uri: 'file:///repo')],
meta: const {'stable': true},
),
),
'capital_of_france': InputResponse.fromResult(
const CreateMessageResult(
model: 'model',
role: SamplingMessageRole.assistant,
content: SamplingTextContent(text: 'Paris'),
meta: {'preserved': true},
),
),
};

Expand All @@ -672,6 +684,14 @@ void main() {
);
final toolJson = toolRequest.toJson();
expect(toolJson['inputResponses']['github_login']['action'], 'accept');
expect(
toolJson['inputResponses']['github_login'],
isNot(contains('_meta')),
);
expect(toolJson['inputResponses']['roots'], isNot(contains('_meta')));
expect(toolJson['inputResponses']['capital_of_france']['_meta'], {
'preserved': true,
});
expect(toolJson['requestState'], 'opaque-state');

final parsedToolRequest = CallToolRequest.fromJson(toolJson);
Expand Down Expand Up @@ -801,6 +821,27 @@ void main() {
),
throwsFormatException,
);
expect(
() => ReadResourceRequest.fromJson(
const {
'uri': 'file:///repo/README.md',
'inputResponses': {
'roots': {
'roots': [],
'_meta': {'trace': 'not-in-draft-client-result'},
},
},
},
),
throwsFormatException,
);
expect(
() => const InputResponse.raw({
'action': 'accept',
'_meta': {'trace': 'not-in-draft-client-result'},
}).toJson(),
throwsFormatException,
);
});

test('server acknowledges subscriptions/listen with subscription id',
Expand Down