Skip to content

Commit 26cf1e8

Browse files
authored
Accept integer-valued numbers in MCP fields (#3003)
Signed-off-by: HarshPopat23 <musichk61@gmail.com>
1 parent 24d1250 commit 26cf1e8

6 files changed

Lines changed: 233 additions & 3 deletions

File tree

‎src/core/mcp/results.cc‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -982,7 +982,7 @@ auto mcp_request_subscription_id(const MCPProtocolVersion version,
982982

983983
const auto *sub_id{meta->try_at("io.modelcontextprotocol/subscriptionId",
984984
MCP_HASH_META_SUBSCRIPTION_ID)};
985-
if (sub_id == nullptr || (!sub_id->is_string() && !sub_id->is_integer())) {
985+
if (sub_id == nullptr || !internal::valid_id(*sub_id)) {
986986
return nullptr;
987987
}
988988
return sub_id;

‎src/core/mcp/validation.h‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ inline auto require(const bool condition, const char *message) -> void {
2525
}
2626

2727
inline auto valid_id(const JSON &identifier) noexcept -> bool {
28-
return identifier.is_string() || identifier.is_integer();
28+
return identifier.is_string() || identifier.is_integral();
2929
}
3030

3131
inline auto valid_implementation(MCPProtocolVersion version, const JSON &value,
@@ -917,7 +917,7 @@ inline auto valid_resource(const MCPProtocolVersion version, const JSON &value,
917917
validation::field(value, "size", sourcemeta::core::MCP_HASH_SIZE)};
918918
return validation::base(value, received) &&
919919
validation::annotations(value, received) &&
920-
((size == nullptr) || (size->is_integer() && size->to_integer() >= 0));
920+
((size == nullptr) || (size->is_integral() && *size >= JSON{0}));
921921
}
922922

923923
inline auto valid_content(const MCPProtocolVersion version, const JSON &value,

‎test/mcp/mcp_error_test.cc‎

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,58 @@ TEST(make_error_with_integer_identifier) {
5353
})JSON"));
5454
}
5555

56+
TEST(make_error_preserves_integral_real_identifier_2025_03_26) {
57+
const sourcemeta::core::JSON identifier{42.0};
58+
const auto error{sourcemeta::core::mcp_make_error(
59+
sourcemeta::core::MCPProtocolVersion::V_2025_03_26, &identifier,
60+
sourcemeta::core::JSONRPC_CODE_INVALID_PARAMS, "Invalid params")};
61+
EXPECT_EQ(error, sourcemeta::core::parse_json(R"JSON({
62+
"jsonrpc": "2.0",
63+
"id": 42.0,
64+
"error": { "code": -32602, "message": "Invalid params" }
65+
})JSON"));
66+
EXPECT_TRUE(error.at("id").is_real());
67+
}
68+
69+
TEST(make_error_preserves_integral_real_identifier_2025_06_18) {
70+
const sourcemeta::core::JSON identifier{42.0};
71+
const auto error{sourcemeta::core::mcp_make_error(
72+
sourcemeta::core::MCPProtocolVersion::V_2025_06_18, &identifier,
73+
sourcemeta::core::JSONRPC_CODE_INVALID_PARAMS, "Invalid params")};
74+
EXPECT_EQ(error, sourcemeta::core::parse_json(R"JSON({
75+
"jsonrpc": "2.0",
76+
"id": 42.0,
77+
"error": { "code": -32602, "message": "Invalid params" }
78+
})JSON"));
79+
EXPECT_TRUE(error.at("id").is_real());
80+
}
81+
82+
TEST(make_error_preserves_integral_real_identifier_2025_11_25) {
83+
const sourcemeta::core::JSON identifier{42.0};
84+
const auto error{sourcemeta::core::mcp_make_error(
85+
sourcemeta::core::MCPProtocolVersion::V_2025_11_25, &identifier,
86+
sourcemeta::core::JSONRPC_CODE_INVALID_PARAMS, "Invalid params")};
87+
EXPECT_EQ(error, sourcemeta::core::parse_json(R"JSON({
88+
"jsonrpc": "2.0",
89+
"id": 42.0,
90+
"error": { "code": -32602, "message": "Invalid params" }
91+
})JSON"));
92+
EXPECT_TRUE(error.at("id").is_real());
93+
}
94+
95+
TEST(make_error_preserves_integral_real_identifier_2026_07_28) {
96+
const sourcemeta::core::JSON identifier{42.0};
97+
const auto error{sourcemeta::core::mcp_make_error(
98+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28, &identifier,
99+
sourcemeta::core::JSONRPC_CODE_INVALID_PARAMS, "Invalid params")};
100+
EXPECT_EQ(error, sourcemeta::core::parse_json(R"JSON({
101+
"jsonrpc": "2.0",
102+
"id": 42.0,
103+
"error": { "code": -32602, "message": "Invalid params" }
104+
})JSON"));
105+
EXPECT_TRUE(error.at("id").is_real());
106+
}
107+
56108
TEST(make_error_with_string_identifier) {
57109
const sourcemeta::core::JSON identifier{"abc"};
58110
EXPECT_EQ(sourcemeta::core::mcp_make_error(

‎test/mcp/mcp_headers_test.cc‎

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,60 @@ TEST(request_meta_validation_valid_minimum) {
3636
EXPECT_EQ(meta->progress_token, nullptr);
3737
}
3838

39+
// Integer-valued numbers are integers regardless of their JSON representation
40+
// https://json-schema.org/understanding-json-schema/reference/numeric#integer
41+
TEST(request_meta_validation_accepts_integral_real_identifier) {
42+
const auto envelope{sourcemeta::core::parse_json(R"JSON({
43+
"jsonrpc": "2.0", "id": 42.0, "method": "tools/list",
44+
"params": { "_meta": {
45+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
46+
"io.modelcontextprotocol/clientCapabilities": {}
47+
} }
48+
})JSON")};
49+
EXPECT_TRUE(envelope.at("id").is_real());
50+
EXPECT_EQ(sourcemeta::core::mcp_validate_request_meta(envelope).first,
51+
sourcemeta::core::MCPRequestMetaStatus::Valid);
52+
}
53+
54+
TEST(request_meta_validation_accepts_integral_exponent_identifier) {
55+
const auto envelope{sourcemeta::core::parse_json(R"JSON({
56+
"jsonrpc": "2.0", "id": 4.2e1, "method": "tools/list",
57+
"params": { "_meta": {
58+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
59+
"io.modelcontextprotocol/clientCapabilities": {}
60+
} }
61+
})JSON")};
62+
EXPECT_EQ(envelope.at("id"), sourcemeta::core::JSON{42});
63+
EXPECT_EQ(sourcemeta::core::mcp_validate_request_meta(envelope).first,
64+
sourcemeta::core::MCPRequestMetaStatus::Valid);
65+
}
66+
67+
TEST(request_meta_validation_accepts_integral_decimal_identifier) {
68+
const auto envelope{sourcemeta::core::parse_json(R"JSON({
69+
"jsonrpc": "2.0", "id": 18446744073709551617, "method": "tools/list",
70+
"params": { "_meta": {
71+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
72+
"io.modelcontextprotocol/clientCapabilities": {}
73+
} }
74+
})JSON")};
75+
EXPECT_TRUE(envelope.at("id").is_decimal());
76+
EXPECT_EQ(sourcemeta::core::mcp_validate_request_meta(envelope).first,
77+
sourcemeta::core::MCPRequestMetaStatus::Valid);
78+
}
79+
80+
TEST(request_meta_validation_rejects_fractional_decimal_identifier) {
81+
const auto envelope{sourcemeta::core::parse_json(R"JSON({
82+
"jsonrpc": "2.0", "id": 42.00000000000000000001, "method": "tools/list",
83+
"params": { "_meta": {
84+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
85+
"io.modelcontextprotocol/clientCapabilities": {}
86+
} }
87+
})JSON")};
88+
EXPECT_TRUE(envelope.at("id").is_decimal());
89+
EXPECT_EQ(sourcemeta::core::mcp_validate_request_meta(envelope).first,
90+
sourcemeta::core::MCPRequestMetaStatus::InvalidEnvelope);
91+
}
92+
3993
TEST(request_meta_validation_valid_with_client_info) {
4094
const auto envelope{sourcemeta::core::parse_json(R"JSON({
4195
"jsonrpc": "2.0",
@@ -1763,6 +1817,36 @@ TEST(request_client_info_rejects_icon_src_number) {
17631817
.second.has_value());
17641818
}
17651819

1820+
TEST(request_metadata_accepts_integral_real_progress_token) {
1821+
const auto parameters{sourcemeta::core::parse_json(R"JSON({
1822+
"_meta": {
1823+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
1824+
"io.modelcontextprotocol/clientCapabilities": {},
1825+
"progressToken": 7.0
1826+
}
1827+
})JSON")};
1828+
const auto [status, meta]{
1829+
sourcemeta::core::mcp_validate_request_parameters(parameters)};
1830+
EXPECT_EQ(status, sourcemeta::core::MCPRequestMetaStatus::Valid);
1831+
EXPECT_TRUE(meta.has_value());
1832+
EXPECT_EQ(meta->progress_token, &parameters.at("_meta").at("progressToken"));
1833+
}
1834+
1835+
TEST(request_metadata_accepts_integral_decimal_progress_token) {
1836+
const auto parameters{sourcemeta::core::parse_json(R"JSON({
1837+
"_meta": {
1838+
"io.modelcontextprotocol/protocolVersion": "2026-07-28",
1839+
"io.modelcontextprotocol/clientCapabilities": {},
1840+
"progressToken": 18446744073709551617
1841+
}
1842+
})JSON")};
1843+
const auto [status, meta]{
1844+
sourcemeta::core::mcp_validate_request_parameters(parameters)};
1845+
EXPECT_EQ(status, sourcemeta::core::MCPRequestMetaStatus::Valid);
1846+
EXPECT_TRUE(meta.has_value());
1847+
EXPECT_EQ(meta->progress_token, &parameters.at("_meta").at("progressToken"));
1848+
}
1849+
17661850
TEST(request_metadata_rejects_fractional_progress_token) {
17671851
auto value{sourcemeta::core::parse_json(R"JSON({
17681852
"_meta": {

‎test/mcp/mcp_notifications_test.cc‎

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,19 @@ TEST(make_subscription_acknowledged_notification_with_integer_identifier) {
324324
})JSON"));
325325
}
326326

327+
TEST(
328+
make_subscription_acknowledged_notification_with_integral_real_identifier) {
329+
sourcemeta::core::MCPSubscriptionFilter filter;
330+
const sourcemeta::core::JSON identifier{42.0};
331+
const auto notification{
332+
sourcemeta::core::mcp_make_subscription_acknowledged_notification(
333+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28, identifier,
334+
filter, filter)};
335+
EXPECT_EQ(notification.at("params").at("_meta").at(
336+
"io.modelcontextprotocol/subscriptionId"),
337+
identifier);
338+
}
339+
327340
TEST(make_subscription_acknowledged_notification_with_every_event) {
328341
sourcemeta::core::MCPSubscriptionFilter filter;
329342
filter.tools_list_changed = true;
@@ -518,6 +531,38 @@ TEST(request_subscription_id_integer) {
518531
EXPECT_EQ(*identifier, sourcemeta::core::JSON{42});
519532
}
520533

534+
TEST(request_subscription_id_integral_real) {
535+
const auto notification{sourcemeta::core::parse_json(R"JSON({
536+
"params": { "_meta": { "io.modelcontextprotocol/subscriptionId": 42.0 } }
537+
})JSON")};
538+
EXPECT_EQ(
539+
sourcemeta::core::mcp_request_subscription_id(
540+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28, notification),
541+
&notification.at("params").at("_meta").at(
542+
"io.modelcontextprotocol/subscriptionId"));
543+
}
544+
545+
TEST(request_subscription_id_integral_decimal) {
546+
const auto response{sourcemeta::core::parse_json(R"JSON({
547+
"result": { "_meta": {
548+
"io.modelcontextprotocol/subscriptionId": 18446744073709551617
549+
} }
550+
})JSON")};
551+
EXPECT_EQ(sourcemeta::core::mcp_request_subscription_id(
552+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28, response),
553+
&response.at("result").at("_meta").at(
554+
"io.modelcontextprotocol/subscriptionId"));
555+
}
556+
557+
TEST(request_subscription_id_rejects_fractional_decimal) {
558+
const auto metadata{sourcemeta::core::parse_json(R"JSON({
559+
"_meta": { "io.modelcontextprotocol/subscriptionId": 42.00000000000000000001 }
560+
})JSON")};
561+
EXPECT_EQ(sourcemeta::core::mcp_request_subscription_id(
562+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28, metadata),
563+
nullptr);
564+
}
565+
521566
TEST(request_subscription_id_from_bare_metadata) {
522567
const auto metadata{sourcemeta::core::parse_json(
523568
R"JSON({

‎test/mcp/mcp_results_test.cc‎

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,15 @@ TEST(tool_success_with_object_result) {
9696
EXPECT_EQ(envelope, expected);
9797
}
9898

99+
TEST(tool_success_preserves_integral_decimal_identifier) {
100+
const auto identifier{sourcemeta::core::parse_json("18446744073709551617")};
101+
const auto envelope{sourcemeta::core::mcp_make_tool_success(
102+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28, identifier,
103+
sourcemeta::core::JSON::make_object())};
104+
EXPECT_EQ(envelope.at("id"), identifier);
105+
EXPECT_TRUE(envelope.at("id").is_decimal());
106+
}
107+
99108
TEST(tool_success_with_array_result) {
100109
const auto identifier{sourcemeta::core::JSON{"abc"}};
101110
const auto envelope{sourcemeta::core::mcp_make_tool_success(
@@ -2659,6 +2668,46 @@ TEST(tool_descriptor_rejects_a_non_object_schema_2026_07_28) {
26592668
"Invalid MCP tool descriptor");
26602669
}
26612670

2671+
TEST(resources_list_result_accepts_integral_real_size) {
2672+
const auto entries{sourcemeta::core::parse_json(R"JSON([
2673+
{ "name": "x", "uri": "file:///x", "size": 42.0 }
2674+
])JSON")};
2675+
const auto result{sourcemeta::core::mcp_make_resources_list_result(
2676+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28, entries, std::nullopt,
2677+
{})};
2678+
EXPECT_EQ(result.at("resources"), entries);
2679+
}
2680+
2681+
TEST(resources_list_result_accepts_integral_decimal_size) {
2682+
const auto entries{sourcemeta::core::parse_json(R"JSON([
2683+
{ "name": "x", "uri": "file:///x", "size": 18446744073709551617 }
2684+
])JSON")};
2685+
const auto result{sourcemeta::core::mcp_make_resources_list_result(
2686+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28, entries, std::nullopt,
2687+
{})};
2688+
EXPECT_EQ(result.at("resources"), entries);
2689+
}
2690+
2691+
TEST(resources_list_result_rejects_negative_integral_real_size) {
2692+
EXPECT_MCP_REJECTS(sourcemeta::core::mcp_make_resources_list_result(
2693+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28,
2694+
sourcemeta::core::parse_json(R"JSON([
2695+
{ "name": "x", "uri": "file:///x", "size": -42.0 }
2696+
])JSON"),
2697+
std::nullopt, {}),
2698+
"Invalid MCP resource descriptor entry");
2699+
}
2700+
2701+
TEST(resources_list_result_rejects_fractional_decimal_size) {
2702+
EXPECT_MCP_REJECTS(sourcemeta::core::mcp_make_resources_list_result(
2703+
sourcemeta::core::MCPProtocolVersion::V_2026_07_28,
2704+
sourcemeta::core::parse_json(R"JSON([
2705+
{ "name": "x", "uri": "file:///x", "size": 42.00000000000000000001 }
2706+
])JSON"),
2707+
std::nullopt, {}),
2708+
"Invalid MCP resource descriptor entry");
2709+
}
2710+
26622711
TEST(resources_list_result_rejects_a_nameless_resource_2025_03_26) {
26632712
EXPECT_MCP_REJECTS(
26642713
sourcemeta::core::mcp_make_resources_list_result(

0 commit comments

Comments
 (0)