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
9 changes: 9 additions & 0 deletions benchmark/CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
set(BENCHMARK_SOURCES)

if(SOURCEMETA_CORE_MCP)
list(APPEND BENCHMARK_SOURCES mcp.cc)
endif()

if(SOURCEMETA_CORE_REGEX)
list(APPEND BENCHMARK_SOURCES regex.cc)
endif()
Expand Down Expand Up @@ -66,6 +70,11 @@ if(BENCHMARK_SOURCES)
target_compile_definitions(sourcemeta_core_benchmarks
PRIVATE CURRENT_DIRECTORY="${CMAKE_CURRENT_SOURCE_DIR}")

if(SOURCEMETA_CORE_MCP)
target_link_libraries(sourcemeta_core_benchmarks
PRIVATE sourcemeta::core::mcp)
endif()

if(SOURCEMETA_CORE_REGEX)
target_link_libraries(sourcemeta_core_benchmarks
PRIVATE sourcemeta::core::regex)
Expand Down
93 changes: 93 additions & 0 deletions benchmark/mcp.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
#include <sourcemeta/core/benchmark.h>
#include <sourcemeta/core/mcp.h>

#include <cstddef> // std::size_t
#include <stdexcept> // std::runtime_error
#include <string> // std::string, std::to_string
#include <utility> // std::pair, std::move
#include <vector> // std::vector

namespace {
using namespace sourcemeta::core;

auto validate_headers(BenchmarkState &state, const std::size_t count) -> void {
std::vector<std::string> names;
names.reserve(count);
auto arguments{JSON::make_object()};
for (std::size_t index{0}; index < count; ++index) {
names.push_back("field" + std::to_string(index));
arguments.assign(names.back(), JSON{index % 2 == 0 ? "plain" : " café "});
}

std::vector<MCPHeaderParameter> parameters;
parameters.reserve(count);
for (const auto &name : names) {
parameters.push_back(
{.name = name, .path = {name}, .type = JSON::Type::String});
}

// Put unrelated headers first to exercise the per-parameter linear search.
std::vector<std::pair<std::string, std::string>> owned;
owned.reserve(count * 2);
for (std::size_t index{0}; index < count; ++index) {
owned.emplace_back("Other-" + std::to_string(index), "ignored");
}
for (auto &header : mcp_make_parameter_headers(parameters, arguments)) {
owned.push_back(std::move(header));
}
std::vector<std::pair<JSON::StringView, JSON::StringView>> headers;
headers.reserve(owned.size());
for (const auto &header : owned) {
headers.emplace_back(header.first, header.second);
}

constexpr auto VERSION{MCPProtocolVersion::V_2026_07_28};
const JSON identifier{1};
if (mcp_validate_parameter_headers(VERSION, identifier, parameters, arguments,
headers)) {
throw std::runtime_error{"Invalid MCP header benchmark fixture"};
}
for (auto iteration : state) {
const auto result{mcp_validate_parameter_headers(
VERSION, identifier, parameters, arguments, headers)};
benchmark_do_not_optimize(result);
}
}
} // namespace

BENCHMARK(MCP_Validate_Headers_2) { validate_headers(state, 2); }
BENCHMARK(MCP_Validate_Headers_64) { validate_headers(state, 64); }

BENCHMARK(MCP_Intersect_Disjoint_512) {
using namespace sourcemeta::core;
std::vector<std::string> requested;
std::vector<std::string> supported;
requested.reserve(512);
supported.reserve(512);
for (std::size_t index{0}; index < 512; ++index) {
requested.push_back("file:///bench/" + std::to_string(index));
supported.push_back("file:///bench/" + std::to_string(index + 512));
}

MCPSubscriptionFilter left;
MCPSubscriptionFilter right;
left.resource_subscriptions.emplace();
right.resource_subscriptions.emplace();
for (const auto &uri : requested) {
left.resource_subscriptions->push_back(uri);
}
for (const auto &uri : supported) {
right.resource_subscriptions->push_back(uri);
}

// No matches forces a complete supported-list scan for each requested URI.
const auto control{mcp_intersect_subscription_filters(left, right)};
if (!control.resource_subscriptions ||
!control.resource_subscriptions->empty()) {
throw std::runtime_error{"Invalid MCP subscription benchmark fixture"};
}
for (auto iteration : state) {
const auto result{mcp_intersect_subscription_filters(left, right)};
benchmark_do_not_optimize(result);
}
}
7 changes: 7 additions & 0 deletions cmake/common/targets/executable.cmake
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,13 @@ function(sourcemeta_executable)
# program already calls
if(SOURCEMETA_CORE_ALLOCATOR_TARGET)
target_link_libraries("${TARGET_NAME}" PRIVATE ${SOURCEMETA_CORE_ALLOCATOR_TARGET})
if(SOURCEMETA_OS_LINUX AND NOT BUILD_SHARED_LIBS AND Mimalloc_FOUND)
# Load the replacement entry points before the static C++ runtime can
# supply its own new/delete definitions. Override allocator references on
# this executable, including explicit allocator links from its caller.
set_property(TARGET "${TARGET_NAME}" PROPERTY
"LINK_LIBRARY_OVERRIDE_${SOURCEMETA_CORE_ALLOCATOR_TARGET}" WHOLE_ARCHIVE)
endif()
endif()

# See https://best.openssf.org/Compiler-Hardening-Guides/Compiler-Options-Hardening-Guide-for-C-and-C++.html
Expand Down
105 changes: 85 additions & 20 deletions src/core/mcp/parameter_headers.cc

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Maybe worth landing the relevant benchmark first on main, so we can then see in this PR the performance difference of these code changes?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sure,
Let's Land benchmark first.

Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,16 @@
#include <sourcemeta/core/text.h>
#include <sourcemeta/core/unicode.h>

#include <set> // std::set
#include <string> // std::string, std::to_string
#include <vector> // std::vector
#include <algorithm> // std::sort, std::lower_bound, std::lexicographical_compare
#include <iterator> // std::next
#include <set> // std::set
#include <string> // std::string, std::to_string
#include <vector> // std::vector

namespace {
using sourcemeta::core::JSON;
using sourcemeta::core::MCPHeaderParameter;
constexpr JSON::StringView PARAMETER_HEADER_PREFIX{"Mcp-Param-"};

auto value_at(const JSON &arguments, const MCPHeaderParameter &parameter)
-> const JSON * {
Expand All @@ -33,12 +36,23 @@ auto value_at(const JSON &arguments, const MCPHeaderParameter &parameter)
// The specification constrains each mirrored parameter name, type and location
// separately, so each constraint reports itself
// https://modelcontextprotocol.io/specification/2026-07-28/basic/transports/streamable-http#schema-extension
struct NameLess {
auto operator()(const JSON::StringView left,
const JSON::StringView right) const -> bool {
return std::lexicographical_compare(
left.begin(), left.end(), right.begin(), right.end(),
[](const char left_character, const char right_character) {
return sourcemeta::core::to_lowercase(left_character) <
sourcemeta::core::to_lowercase(right_character);
});
}
};

auto require_parameters(const std::span<const MCPHeaderParameter> parameters)
-> void {
std::set<std::string> names;
std::set<JSON::StringView, NameLess> names;
std::size_t index{0};
for (const auto &parameter : parameters) {
std::string name{parameter.name};
sourcemeta::core::to_lowercase(name);
// A name must be non-empty and match HTTP field-name token syntax, which
// also rules out control characters
sourcemeta::core::internal::require(
Expand All @@ -55,9 +69,21 @@ auto require_parameters(const std::span<const MCPHeaderParameter> parameters)
parameter.type == JSON::Type::Integer ||
parameter.type == JSON::Type::Boolean,
"Parameter headers only mirror strings, integers and booleans");
bool unique{true};
if (parameters.size() <= 8) {
for (std::size_t previous{0}; previous < index; ++previous) {
if (sourcemeta::core::equals_ignore_case(parameters[previous].name,
parameter.name)) {
unique = false;
break;
}
}
} else {
unique = names.insert(parameter.name).second;
}
++index;
sourcemeta::core::internal::require(
names.insert(std::move(name)).second,
"Parameter header names must be case-insensitively unique");
unique, "Parameter header names must be case-insensitively unique");
}
}

Expand Down Expand Up @@ -120,7 +146,8 @@ auto mcp_make_parameter_headers(
if (const auto *value{value_at(arguments, parameter)}; value) {
internal::require(safe_parameter(*value, parameter),
"Invalid mirrored argument type or unsafe integer");
result.emplace_back("Mcp-Param-" + std::string{parameter.name},
result.emplace_back(std::string{PARAMETER_HEADER_PREFIX} +
std::string{parameter.name},
mcp_encode_header_value(parameter_text(*value)));
}
}
Expand All @@ -141,24 +168,61 @@ auto mcp_validate_parameter_headers(
return mcp_make_error(version, &identifier, JSONRPC_CODE_INVALID_PARAMS,
"Invalid tool arguments");
}
// Index only larger collections. Small requests keep the allocation-free
// scan; unrelated headers and duplicates of unused names remain opaque.
std::vector<std::pair<JSON::StringView, JSON::StringView>> indexed;
const bool use_index{parameters.size() > 8 && headers.size() > 16};
if (use_index) {
indexed.reserve(headers.size());
for (const auto &entry : headers) {
if (starts_with_ignore_case(entry.first, PARAMETER_HEADER_PREFIX)) {
indexed.emplace_back(entry.first.substr(PARAMETER_HEADER_PREFIX.size()),
entry.second);
}
}
std::sort(indexed.begin(), indexed.end(),
[](const auto &left, const auto &right) {
return NameLess{}(left.first, right.first);
});
}
for (const auto &parameter : parameters) {
const std::string name{"Mcp-Param-" + std::string{parameter.name}};

std::optional<JSON::StringView> header;
for (const auto &candidate : headers) {
if (equals_ignore_case(candidate.first, name)) {
if (header) {
if (use_index) {
const auto found{std::lower_bound(indexed.begin(), indexed.end(),
parameter.name,
[](const auto &entry, const auto name) {
return NameLess{}(entry.first, name);
})};
if (found != indexed.end() &&
equals_ignore_case(found->first, parameter.name)) {
header = found->second;
if (std::next(found) != indexed.end() &&
equals_ignore_case(std::next(found)->first, parameter.name)) {
return mcp_make_error_header_mismatch(version, &identifier,
"Duplicate parameter header");
}
header = candidate.second;
}
} else {
for (const auto &candidate : headers) {
if (starts_with_ignore_case(candidate.first, PARAMETER_HEADER_PREFIX) &&
equals_ignore_case(
candidate.first.substr(PARAMETER_HEADER_PREFIX.size()),
parameter.name)) {
if (header) {
return mcp_make_error_header_mismatch(version, &identifier,
"Duplicate parameter header");
}
header = candidate.second;
}
}
}
const auto *value{value_at(arguments, parameter)};
if (value == nullptr) {
if (header) {
return mcp_make_error_header_mismatch(version, &identifier, name,
*header);
return mcp_make_error_header_mismatch(
version, &identifier,
std::string{PARAMETER_HEADER_PREFIX} + std::string{parameter.name},
*header);
}
continue;
}
Expand All @@ -184,9 +248,10 @@ auto mcp_validate_parameter_headers(
}
}
if (!matches) {
return mcp_make_error_header_mismatch(version, &identifier, name,
header.value_or(""),
parameter_text(*value));
return mcp_make_error_header_mismatch(
version, &identifier,
std::string{PARAMETER_HEADER_PREFIX} + std::string{parameter.name},
header.value_or(""), parameter_text(*value));
}
}
return std::nullopt;
Expand Down
Loading
Loading