Skip to content
Open
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
19 changes: 19 additions & 0 deletions src/core/mcp/include/sourcemeta/core/mcp_results.h
Original file line number Diff line number Diff line change
Expand Up @@ -944,6 +944,25 @@ auto mcp_write_result(
std::optional<MCPCachePolicy> cache_policy,
std::optional<MCPImplementation> server_info = std::nullopt) -> void;

/// @ingroup mcp
/// Serialize a complete MCP response for one of the six cacheable operations
/// using move semantics, decorating the provided result in-place.
/// A cache policy is mandatory for 2026-07-28. Pass `std::nullopt` for legacy
/// responses. A precomputed discovery result is held to the same rules as the
/// builder that produces one, so its advertised revisions must be non-empty,
/// must include the revision being served, and must each be one this library
/// models.
/// This writes JSON only. Stdio callers must append a newline themselves.
///
/// @see
/// https://modelcontextprotocol.io/specification/2026-07-28/server/utilities/caching#cacheable-results
SOURCEMETA_CORE_MCP_EXPORT
auto mcp_write_result(
std::ostream &stream, MCPProtocolVersion version, JSON::StringView method,
const JSON &identifier, JSON &&result,
std::optional<MCPCachePolicy> cache_policy,
std::optional<MCPImplementation> server_info = std::nullopt) -> void;

/// @ingroup mcp
/// Build the JSON-RPC envelope returned in response to an MCP `initialize`
/// request. The revision is the one the client asked for when the server
Expand Down
14 changes: 7 additions & 7 deletions src/core/mcp/notifications.cc
Original file line number Diff line number Diff line change
Expand Up @@ -206,20 +206,20 @@ auto mcp_make_notification(const MCPProtocolVersion version,
} else if (method == MCP_METHOD_NOTIFICATIONS_MESSAGE) {
const auto *level{
parameters.try_at("level", sourcemeta::core::MCP_HASH_LEVEL)};
const auto resolved_level{(level != nullptr && level->is_string())
? mcp_resolve_log_level(level->to_string())
: std::nullopt};
internal::require(
(level != nullptr) && level->is_string() &&
internal::valid_log_level(level->to_string()) &&
resolved_level.has_value() &&
parameters.defines("data", sourcemeta::core::MCP_HASH_DATA) &&
optional_string(parameters, "logger",
sourcemeta::core::MCP_HASH_LOGGER),
"Invalid logging notification");
if (version == MCPProtocolVersion::V_2026_07_28 && from_server) {
const auto threshold{mcp_resolve_log_level(*request_context->log_level)};
internal::require(
threshold.has_value() &&
mcp_log_level_enabled(*mcp_resolve_log_level(level->to_string()),
*threshold),
"Logging notification is below the request threshold");
internal::require(threshold.has_value() &&
mcp_log_level_enabled(*resolved_level, *threshold),
"Logging notification is below the request threshold");
}
} else if (method == MCP_METHOD_NOTIFICATIONS_CANCELLED) {
const auto *identifier{
Expand Down
11 changes: 9 additions & 2 deletions src/core/mcp/parameter_headers.cc
Original file line number Diff line number Diff line change
Expand Up @@ -64,8 +64,15 @@ auto require_parameters(const std::span<const MCPHeaderParameter> parameters)
auto safe_parameter(const JSON &value, const MCPHeaderParameter &parameter)
-> bool {
if (parameter.type == JSON::Type::Integer) {
return value.is_integral() && value >= JSON{INT64_C(-9007199254740991)} &&
value <= JSON{INT64_C(9007199254740991)};
if (value.is_integer()) {
const auto integer{value.to_integer()};
return integer >= INT64_C(-9007199254740991) &&
integer <= INT64_C(9007199254740991);
}
static const JSON MIN_SAFE_INTEGER{INT64_C(-9007199254740991)};
static const JSON MAX_SAFE_INTEGER{INT64_C(9007199254740991)};
return value.is_integral() && value >= MIN_SAFE_INTEGER &&
value <= MAX_SAFE_INTEGER;
}
return value.type() == parameter.type;
}
Expand Down
88 changes: 70 additions & 18 deletions src/core/mcp/results.cc
Original file line number Diff line number Diff line change
Expand Up @@ -207,14 +207,8 @@ namespace {
// is reachable from the interim and subscription results too, whose own result
// type rules out the complete-result decorator
// https://modelcontextprotocol.io/specification/2026-07-28/basic/index#responses
auto attach_server_info(const MCPProtocolVersion version,
sourcemeta::core::JSON &result,
const std::optional<MCPImplementation> &server_info)
-> void {
if (!server_info.has_value() || !mcp_requires_result_type(version)) {
return;
}
auto info{internal::make_implementation(version, *server_info)};
auto attach_server_info(sourcemeta::core::JSON &result,
sourcemeta::core::JSON &&info) -> void {
if (auto *meta{result.try_at("_meta", MCP_HASH_META)};
meta != nullptr && meta->is_object()) {
// Caller-supplied metadata may already identify the server
Expand All @@ -227,6 +221,17 @@ auto attach_server_info(const MCPProtocolVersion version,
}
}

auto attach_server_info(const MCPProtocolVersion version,
sourcemeta::core::JSON &result,
const std::optional<MCPImplementation> &server_info)
-> void {
if (!server_info.has_value() || !mcp_requires_result_type(version)) {
return;
}
attach_server_info(result,
internal::make_implementation(version, *server_info));
}

auto decorate_result(
const MCPProtocolVersion version, sourcemeta::core::JSON &result,
const std::optional<MCPImplementation> &server_info = std::nullopt)
Expand Down Expand Up @@ -256,12 +261,20 @@ auto decorate_result(
"or extension results");
}

auto info{
server_info.has_value()
? std::optional<sourcemeta::core::JSON>{internal::make_implementation(
version, *server_info)}
: std::nullopt};

if (result.try_at("resultType", MCP_HASH_RESULT_TYPE) == nullptr) {
result.assign_assume_new("resultType", sourcemeta::core::JSON{"complete"},
MCP_HASH_RESULT_TYPE);
}

attach_server_info(version, result, server_info);
if (info) {
attach_server_info(result, std::move(*info));
}
}

} // namespace
Expand Down Expand Up @@ -1066,11 +1079,11 @@ auto mcp_make_completion_result(const MCPProtocolVersion version, JSON values,
return result;
}

auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version,
const JSON::StringView method, const JSON &identifier,
const JSON &result,
const std::optional<MCPCachePolicy> cache_policy,
const std::optional<MCPImplementation> server_info)
namespace {
auto validate_write_result(const MCPProtocolVersion version,
const JSON::StringView method,
const JSON &identifier, const JSON &result,
const std::optional<MCPCachePolicy> &cache_policy)
-> void {
internal::require(internal::valid_id(identifier), "Invalid MCP identifier");
internal::require(mcp_is_request_method(version, method) &&
Expand Down Expand Up @@ -1102,6 +1115,10 @@ auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version,
internal::require(cache_policy.has_value(),
"Cacheable results require a policy");
}
if (cache_policy) {
internal::require(cache_policy->ttl_ms >= 0,
"A cache time to live cannot be negative");
}
const JSON *entries = nullptr;
if (method == MCP_METHOD_TOOLS_LIST) {
entries = result.try_at("tools", sourcemeta::core::MCP_HASH_TOOLS);
Expand Down Expand Up @@ -1164,11 +1181,46 @@ auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version,
cursor) {
internal::require(cursor->is_string(), "Invalid result cursor");
}
auto payload{result};
decorate_result(version, payload, server_info);
}

auto write_result_impl(std::ostream &stream, const MCPProtocolVersion version,
const JSON::StringView method, const JSON &identifier,
JSON &&result,
const std::optional<MCPCachePolicy> &cache_policy,
const std::optional<MCPImplementation> &server_info)
-> void {
// NOLINTNEXTLINE(performance-unnecessary-copy-initialization)
const auto envelope_identifier{identifier};
const JSON::String operation{method};
decorate_result(version, result, server_info);
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

@augmentcode augmentcode Bot Oct 16, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could you check the remaining borrowed inputs at src/core/mcp/results.cc:1180–1182 when they alias result? Adding resultType can reallocate the object's storage, leaving a short-string method view (e.g. result.at("method").to_string() containing "tools/list") or server_info.icons pointing to a top-level array dangling before decorate_cacheable_result/attach_server_info reads it, causing a use-after-free.

Severity: medium

Fix This in Augment

🤖 Was this useful? React with 👍 or 👎, or 🚀 if it prevented an incident/outage.

if (cache_policy) {
decorate_cacheable_result(version, payload, *cache_policy, method);
decorate_cacheable_result(version, result, *cache_policy, operation);
}
stringify(jsonrpc_make_success(identifier, std::move(payload)), stream);
stringify(jsonrpc_make_success(envelope_identifier, std::move(result)),
stream);
}
} // namespace

auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version,
const JSON::StringView method, const JSON &identifier,
JSON &&result,
const std::optional<MCPCachePolicy> cache_policy,
const std::optional<MCPImplementation> server_info)
-> void {
validate_write_result(version, method, identifier, result, cache_policy);
write_result_impl(stream, version, method, identifier, std::move(result),
cache_policy, server_info);
}

auto mcp_write_result(std::ostream &stream, const MCPProtocolVersion version,
const JSON::StringView method, const JSON &identifier,
const JSON &result,
const std::optional<MCPCachePolicy> cache_policy,
const std::optional<MCPImplementation> server_info)
-> void {
validate_write_result(version, method, identifier, result, cache_policy);
auto payload{result};
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
write_result_impl(stream, version, method, identifier, std::move(payload),
cache_policy, server_info);
}
} // namespace sourcemeta::core
32 changes: 32 additions & 0 deletions test/mcp/mcp_headers_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2360,6 +2360,38 @@ TEST(make_parameter_headers_omits_an_absent_value) {

// Integer values must stay within the safe range for JavaScript
// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#schema-extension
TEST(make_parameter_headers_accepts_safe_integer_boundaries) {
const std::array<sourcemeta::core::MCPHeaderParameter, 1> descriptors{
{{.name = "Count",
.path = {"number"},
.type = sourcemeta::core::JSON::Type::Integer}}};
const auto min_headers{sourcemeta::core::mcp_make_parameter_headers(
descriptors, sourcemeta::core::parse_json(
R"JSON({ "number": -9007199254740991 })JSON"))};
const std::vector<std::pair<std::string, std::string>> expected_min{
{"Mcp-Param-Count", "-9007199254740991"}};
EXPECT_EQ(min_headers, expected_min);

const auto max_headers{sourcemeta::core::mcp_make_parameter_headers(
descriptors, sourcemeta::core::parse_json(
R"JSON({ "number": 9007199254740991 })JSON"))};
const std::vector<std::pair<std::string, std::string>> expected_max{
{"Mcp-Param-Count", "9007199254740991"}};
EXPECT_EQ(max_headers, expected_max);
}

TEST(make_parameter_headers_rejects_integer_below_min_safe) {
const std::array<sourcemeta::core::MCPHeaderParameter, 1> descriptors{
{{.name = "Count",
.path = {"number"},
.type = sourcemeta::core::JSON::Type::Integer}}};
EXPECT_MCP_REJECTS(
sourcemeta::core::mcp_make_parameter_headers(
descriptors, sourcemeta::core::parse_json(
R"JSON({ "number": -9007199254740992 })JSON")),
"Invalid mirrored argument type or unsafe integer");
}

TEST(make_parameter_headers_rejects_an_unsafe_integer) {
const std::array<sourcemeta::core::MCPHeaderParameter, 1> descriptors{
{{.name = "Count",
Expand Down
95 changes: 95 additions & 0 deletions test/mcp/mcp_results_test.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6417,6 +6417,101 @@ TEST(write_result_resource_templates_list_2026_07_28) {
"\"resultType\":\"complete\",\"ttlMs\":123,\"cacheScope\":\"public\"}}");
}

TEST(write_result_moves_rvalue_payload) {
std::ostringstream stream;
auto result{sourcemeta::core::parse_json(R"JSON({ "tools": [] })JSON")};
sourcemeta::core::mcp_write_result(
stream, sourcemeta::core::MCPProtocolVersion::V_2026_07_28, "tools/list",
sourcemeta::core::JSON{1}, std::move(result),
sourcemeta::core::MCPCachePolicy{
.ttl_ms = 123, .scope = sourcemeta::core::MCPCacheScope::Public});
EXPECT_EQ(
stream.str(),
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"tools\":[],\"resultType\":"
"\"complete\",\"ttlMs\":123,\"cacheScope\":\"public\"}}");
}

TEST(write_result_preserves_const_lvalue_payload) {
std::ostringstream stream;
const auto result{sourcemeta::core::parse_json(R"JSON({ "tools": [] })JSON")};
sourcemeta::core::mcp_write_result(
stream, sourcemeta::core::MCPProtocolVersion::V_2026_07_28, "tools/list",
sourcemeta::core::JSON{1}, result,
sourcemeta::core::MCPCachePolicy{
.ttl_ms = 123, .scope = sourcemeta::core::MCPCacheScope::Public});
EXPECT_EQ(result, sourcemeta::core::parse_json(R"JSON({ "tools": [] })JSON"));
EXPECT_EQ(
stream.str(),
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"tools\":[],\"resultType\":"
"\"complete\",\"ttlMs\":123,\"cacheScope\":\"public\"}}");
}

TEST(write_result_supports_aliased_identifier) {
std::ostringstream stream;
auto result{
sourcemeta::core::parse_json(R"JSON({ "id": 1, "tools": [] })JSON")};
sourcemeta::core::mcp_write_result(
stream, sourcemeta::core::MCPProtocolVersion::V_2026_07_28, "tools/list",
result.at("id"), std::move(result),
sourcemeta::core::MCPCachePolicy{
.ttl_ms = 123, .scope = sourcemeta::core::MCPCacheScope::Public});
EXPECT_EQ(stream.str(),
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"id\":1,\"tools\":[],"
"\"resultType\":"
"\"complete\",\"ttlMs\":123,\"cacheScope\":\"public\"}}");
}

TEST(write_result_supports_aliased_method) {
std::ostringstream stream;
auto result{sourcemeta::core::parse_json(
R"JSON({ "op": "tools/list", "tools": [] })JSON")};
const sourcemeta::core::JSON::StringView method_view{
result.at("op").to_string()};
sourcemeta::core::mcp_write_result(
stream, sourcemeta::core::MCPProtocolVersion::V_2026_07_28, method_view,
sourcemeta::core::JSON{1}, std::move(result),
sourcemeta::core::MCPCachePolicy{
.ttl_ms = 123, .scope = sourcemeta::core::MCPCacheScope::Public});
EXPECT_EQ(
stream.str(),
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"op\":\"tools/list\","
"\"tools\":[],\"resultType\":\"complete\",\"ttlMs\":123,\"cacheScope\":"
"\"public\"}}");
}

TEST(write_result_supports_aliased_server_info) {
std::ostringstream stream;
auto result{sourcemeta::core::parse_json(
R"JSON({ "srv": "my-server", "ver": "1.0", "tools": [] })JSON")};
const sourcemeta::core::MCPImplementation server{
.name = result.at("srv").to_string(),
.version = result.at("ver").to_string()};
sourcemeta::core::mcp_write_result(
stream, sourcemeta::core::MCPProtocolVersion::V_2026_07_28, "tools/list",
sourcemeta::core::JSON{1}, std::move(result),
sourcemeta::core::MCPCachePolicy{
.ttl_ms = 123, .scope = sourcemeta::core::MCPCacheScope::Public},
server);
EXPECT_EQ(
stream.str(),
"{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"srv\":\"my-server\","
"\"ver\":\"1.0\",\"tools\":[],\"resultType\":\"complete\",\"_meta\":"
"{\"io.modelcontextprotocol/serverInfo\":{\"name\":\"my-server\","
"\"version\":\"1.0\"}},\"ttlMs\":123,\"cacheScope\":\"public\"}}");
}

TEST(write_result_lvalue_rejects_invalid_identifier_before_copy) {
std::ostringstream stream;
const auto result{sourcemeta::core::parse_json(R"JSON({ "tools": [] })JSON")};
EXPECT_MCP_REJECTS(
sourcemeta::core::mcp_write_result(
stream, sourcemeta::core::MCPProtocolVersion::V_2026_07_28,
"tools/list", sourcemeta::core::JSON{nullptr}, result,
sourcemeta::core::MCPCachePolicy{
.ttl_ms = 123, .scope = sourcemeta::core::MCPCacheScope::Public}),
"Invalid MCP identifier");
}

TEST(write_result_prompts_list_2026_07_28) {
std::ostringstream stream;
sourcemeta::core::mcp_write_result(
Expand Down
Loading